• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

luttje / Key2Joy / 6738508468

02 Nov 2023 09:38PM UTC coverage: 43.012% (-0.8%) from 43.795%
6738508468

Pull #66

github

web-flow
Merge b13824648 into 6a3d02880
Pull Request #66: Bugfix recently discovered input bugs

754 of 2413 branches covered (0.0%)

Branch coverage included in aggregate %.

203 of 203 new or added lines in 9 files covered. (100.0%)

3859 of 8312 relevant lines covered (46.43%)

11968.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.17
/Core/Key2Joy.Core/Key2JoyManager.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Diagnostics;
4
using System.IO;
5
using System.Linq;
6
using System.Reflection;
7
using CommonServiceLocator;
8
using Key2Joy.Config;
9
using Key2Joy.Contracts.Mapping.Actions;
10
using Key2Joy.Contracts.Mapping.Triggers;
11
using Key2Joy.Interop;
12
using Key2Joy.Interop.Commands;
13
using Key2Joy.LowLevelInput.SimulatedGamePad;
14
using Key2Joy.LowLevelInput.XInput;
15
using Key2Joy.Mapping;
16
using Key2Joy.Mapping.Actions.Logic;
17
using Key2Joy.Mapping.Triggers.GamePad;
18
using Key2Joy.Mapping.Triggers.Keyboard;
19
using Key2Joy.Mapping.Triggers.Mouse;
20
using Key2Joy.Plugins;
21
using Key2Joy.Util;
22

23
namespace Key2Joy;
24

25
public delegate bool AppCommandRunner(AppCommand command);
26

27
public class Key2JoyManager : IKey2JoyManager
28
{
29
    /// <summary>
30
    /// Directory where plugins are located
31
    /// </summary>
32
    public const string PluginsDirectory = "Plugins";
33

34
    public event EventHandler<StatusChangedEventArgs> StatusChanged;
35

36
    public static Key2JoyManager instance;
37

38
    public static Key2JoyManager Instance
39
    {
40
        get
41
        {
42
            if (instance == null)
4!
43
            {
44
                throw new Exception("Key2JoyManager not initialized using InitSafely yet!");
×
45
            }
46

47
            return instance;
4✔
48
        }
49
    }
50

51
    private const string READY_MESSAGE = "Key2Joy is ready";
52
    private static AppCommandRunner commandRunner;
53
    private MappingProfile armedProfile;
54
    private List<AbstractTriggerListener> armedListeners;
55
    private IHaveHandleAndInvoke handleAndInvoker;
56

57
    private Key2JoyManager()
2✔
58
    { }
2✔
59

60
    /// <summary>
61
    /// Ensures Key2Joy is running and ready to accept commands as long as the main loop does not end.
62
    /// </summary>
63
    /// <param name="commandRunner"></param>
64
    /// <param name="mainLoop"></param>
65
    /// <param name="configManager">Optionally a custom config manager (probably only useful for unit testing)</param>
66
    public static void InitSafely(AppCommandRunner commandRunner, Action<PluginSet> mainLoop, IConfigManager configManager = null)
67
    {
68
        // Setup dependency injection and services
69
        var serviceLocator = new DependencyServiceLocator();
2✔
70
        ServiceLocator.SetLocatorProvider(() => serviceLocator);
10✔
71

72
#pragma warning disable IDE0001 // Simplify Names
73
        serviceLocator.Register<IConfigManager>(configManager ??= new ConfigManager());
2!
74
#pragma warning restore IDE0001 // Simplify Names
75

76
        var gamePadService = new SimulatedGamePadService();
2✔
77
        serviceLocator.Register<ISimulatedGamePadService>(gamePadService);
2✔
78

79
        var xInputService = new XInputService();
2✔
80
        serviceLocator.Register<IXInputService>(xInputService);
2✔
81

82
        var commandRepository = new CommandRepository();
2✔
83
        serviceLocator.Register<ICommandRepository>(commandRepository);
2✔
84

85
        instance = new Key2JoyManager();
2✔
86
        serviceLocator.Register<IKey2JoyManager>(instance);
2✔
87

88
        // Load plugins
89
        var pluginDirectoriesPaths = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
2✔
90
        pluginDirectoriesPaths = Path.Combine(pluginDirectoriesPaths, PluginsDirectory);
2✔
91

92
        PluginSet plugins = new(pluginDirectoriesPaths);
2✔
93
        plugins.LoadAll();
2✔
94
        plugins.RefreshPluginTypes();
2✔
95

96
        foreach (var loadState in plugins.AllPluginLoadStates.Values)
8✔
97
        {
98
            if (loadState.LoadState == PluginLoadStates.FailedToLoad)
2!
99
            {
100
                System.Windows.MessageBox.Show(
×
101
                    $"One of your plugins located at {loadState.AssemblyPath} failed to load. This was the error: " +
×
102
                    loadState.LoadErrorMessage,
×
103
                    "Failed to load plugin!",
×
104
                    System.Windows.MessageBoxButton.OK,
×
105
                    System.Windows.MessageBoxImage.Warning
×
106
                );
×
107
            }
108
        }
109

110
        Key2JoyManager.commandRunner = commandRunner;
2✔
111

112
        var interopServer = new InteropServer(instance, commandRepository);
2✔
113

114
        try
115
        {
116
            interopServer.RestartListening();
2✔
117
            mainLoop(plugins);
2✔
118
        }
2✔
119
        finally
120
        {
121
            interopServer.StopListening();
2✔
122
            gamePadService.ShutDown();
2✔
123
        }
2✔
124
    }
2✔
125

126
    // Run the event on the same thread as the main control/form
127
    public void CallOnUiThread(Action action) => this.handleAndInvoker.Invoke(action);
×
128

129
    internal static bool RunAppCommand(AppCommand command) => commandRunner != null && commandRunner(command);
×
130

131
    public void SetHandlerWithInvoke(IHaveHandleAndInvoke handleAndInvoker)
132
    {
133
        this.handleAndInvoker = handleAndInvoker;
×
134

135
        Console.WriteLine(READY_MESSAGE);
×
136
    }
×
137

138
    public bool GetIsArmed(MappingProfile profile = null)
139
    {
140
        if (profile == null)
×
141
        {
142
            return this.armedProfile != null;
×
143
        }
144

145
        return this.armedProfile == profile;
×
146
    }
147

148
    /// <inheritdoc/>
149
    public void ArmMappings(MappingProfile profile, bool withExplicitTriggerListeners = true)
150
    {
151
        this.armedProfile = profile;
2✔
152

153
        var allListeners = this.armedListeners = new();
2✔
154

155
        if (withExplicitTriggerListeners)
2!
156
        {
157
            allListeners.AddRange(new List<AbstractTriggerListener> {
×
158
                // Trigger listeners that should explicitly loaded. This ensures that they're available for scripts
×
159
                // even if no mapping option is mapped to be triggered by it.
×
160
                // Always add these listeners so scripts can ask them if stuff has happened.
×
161
                KeyboardTriggerListener.Instance,
×
162
                MouseButtonTriggerListener.Instance,
×
163
                MouseMoveTriggerListener.Instance,
×
164
                GamePadButtonTriggerListener.Instance,
×
165
                GamePadStickTriggerListener.Instance,
×
166
                GamePadTriggerTriggerListener.Instance,
×
167
            });
×
168
        }
169

170
        var allActions = (IList<AbstractAction>)profile.MappedOptions.Select(m => m.Action).ToList();
4✔
171

172
        var xInputService = ServiceLocator.Current.GetInstance<IXInputService>();
2✔
173
        // We must recognize physical devices before any simulated ones are added.
174
        // Otherwise we wont be able to tell the difference.
175
        xInputService.RecognizePhysicalDevices();
2✔
176
        xInputService.StartPolling();
2✔
177

178
        try
179
        {
180
            foreach (var mappedOption in profile.MappedOptions)
8✔
181
            {
182
                if (mappedOption.Trigger == null)
2✔
183
                {
184
                    continue;
185
                }
186

187
                var listener = mappedOption.Trigger.GetTriggerListener();
2✔
188

189
                if (!allListeners.Contains(listener))
2✔
190
                {
191
                    allListeners.Add(listener);
2✔
192
                }
193

194
                mappedOption.Action.OnStartListening(listener, ref allActions);
2✔
195
                listener.AddMappedOption(mappedOption);
2✔
196
            }
197

198
            var allListenersForSharing = (IList<AbstractTriggerListener>)allListeners;
2✔
199

200
            foreach (var listener in allListeners)
8✔
201
            {
202
                listener.StartListening(ref allListenersForSharing);
2✔
203
            }
204

205
            StatusChanged?.Invoke(this, new StatusChangedEventArgs
2!
206
            {
2✔
207
                IsEnabled = true,
2✔
208
                Profile = this.armedProfile
2✔
209
            });
2✔
210
        }
2✔
211
        catch (MappingArmingFailedException ex)
212
        {
213
            this.DisarmMappings();
×
214
            throw ex;
×
215
        }
216
    }
2✔
217

218
    public void DisarmMappings()
219
    {
220
        var listeners = this.armedListeners;
2✔
221

222
        // Clear all intervals
223
        IdPool.CancelAll();
2✔
224

225
        foreach (var mappedOption in this.armedProfile.MappedOptions)
8✔
226
        {
227
            if (mappedOption.Trigger == null)
2✔
228
            {
229
                continue;
230
            }
231

232
            var listener = mappedOption.Trigger.GetTriggerListener();
2✔
233
            mappedOption.Action.OnStopListening(listener);
2✔
234

235
            if (!listeners.Contains(listener))
2!
236
            {
237
                listeners.Add(listener);
×
238
            }
239
        }
240

241
        foreach (var listener in listeners)
8✔
242
        {
243
            listener.StopListening();
2✔
244
        }
245

246
        var xInputService = ServiceLocator.Current.GetInstance<IXInputService>();
2✔
247
        xInputService.StopPolling();
2✔
248

249
        var gamePadService = ServiceLocator.Current.GetInstance<ISimulatedGamePadService>();
2✔
250
        gamePadService.EnsureAllUnplugged();
2✔
251

252
        this.armedProfile = null;
2✔
253

254
        StatusChanged?.Invoke(this, new StatusChangedEventArgs
2!
255
        {
2✔
256
            IsEnabled = false,
2✔
257
        });
2✔
258
    }
×
259

260
    /// <summary>
261
    /// Starts Key2Joy, pausing until it's ready
262
    /// </summary>
263
    public static void StartKey2Joy(bool startMinimized = true, bool pauseUntilReady = true)
264
    {
265
        var configManager = ServiceLocator.Current.GetInstance<IConfigManager>();
×
266
        var executablePath = configManager.GetConfigState().LastInstallPath;
×
267

268
        if (executablePath == null)
×
269
        {
270
            Console.WriteLine("Error! Key2Joy executable path is not known, please start Key2Joy at least once!");
×
271
            return;
×
272
        }
273

274
        if (!File.Exists(executablePath))
×
275
        {
276
            Console.WriteLine("Error! Key2Joy executable path is invalid, please start Key2Joy at least once (and don't move the executable)!");
×
277
            return;
×
278
        }
279

280
        Process process = new()
×
281
        {
×
282
            StartInfo = new ProcessStartInfo
×
283
            {
×
284
                FileName = executablePath,
×
285
                Arguments = startMinimized ? "--minimized" : "",
×
286
                UseShellExecute = false,
×
287
                RedirectStandardOutput = true
×
288
            }
×
289
        };
×
290

291
        process.Start();
×
292

293
        if (!pauseUntilReady)
×
294
        {
295
            return;
×
296
        }
297

298
        while (!process.StandardOutput.EndOfStream)
×
299
        {
300
            if (process.StandardOutput.ReadLine() == READY_MESSAGE)
×
301
            {
302
                break;
303
            }
304
        }
305
    }
×
306
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc