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

luttje / Key2Joy / 9836432308

08 Jul 2024 08:46AM UTC coverage: 43.937% (-0.8%) from 44.758%
9836432308

Pull #66

github

web-flow
Merge aa8186077 into 3bfa3ba36
Pull Request #66: Bugfix recently discovered input bugs

793 of 2455 branches covered (32.3%)

Branch coverage included in aggregate %.

19 of 278 new or added lines in 20 files covered. (6.83%)

43 existing lines in 11 files now uncovered.

4012 of 8481 relevant lines covered (47.31%)

1567.5 hits per line

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

54.05
/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 System.Windows.Forms;
8
using CommonServiceLocator;
9
using Key2Joy.Config;
10
using Key2Joy.Contracts.Mapping.Actions;
11
using Key2Joy.Contracts.Mapping.Triggers;
12
using Key2Joy.Interop;
13
using Key2Joy.Interop.Commands;
14
using Key2Joy.LowLevelInput.SimulatedGamePad;
15
using Key2Joy.LowLevelInput.XInput;
16
using Key2Joy.Mapping;
17
using Key2Joy.Mapping.Actions.Logic;
18
using Key2Joy.Mapping.Triggers;
19
using Key2Joy.Mapping.Triggers.GamePad;
20
using Key2Joy.Mapping.Triggers.Keyboard;
21
using Key2Joy.Mapping.Triggers.Mouse;
22
using Key2Joy.Plugins;
23
using Key2Joy.Util;
24

25
namespace Key2Joy;
26

27
public delegate bool AppCommandRunner(AppCommand command);
28

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

36
    public event EventHandler<StatusChangedEventArgs> StatusChanged;
37

38
    public static Key2JoyManager instance;
39

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

49
            return instance;
4✔
50
        }
51
    }
52

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

59
    private List<AbstractTriggerListener> wndProcListeners = new List<AbstractTriggerListener>();
2✔
60

61
    private Key2JoyManager()
2✔
62
    { }
2✔
63

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

76
#pragma warning disable IDE0001 // Simplify Names
77
        serviceLocator.Register<IConfigManager>(configManager ??= new ConfigManager());
2!
78
#pragma warning restore IDE0001 // Simplify Names
79

80
        var gamePadService = new SimulatedGamePadService();
2✔
81
        serviceLocator.Register<ISimulatedGamePadService>(gamePadService);
2✔
82

83
        var xInputService = new XInputService();
2✔
84
        serviceLocator.Register<IXInputService>(xInputService);
2✔
85

86
        var commandRepository = new CommandRepository();
2✔
87
        serviceLocator.Register<ICommandRepository>(commandRepository);
2✔
88

89
        instance = new Key2JoyManager();
2✔
90
        serviceLocator.Register<IKey2JoyManager>(instance);
2✔
91

92
        // Load plugins
93
        var pluginDirectoriesPaths = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
2✔
94
        pluginDirectoriesPaths = Path.Combine(pluginDirectoriesPaths, PluginsDirectory);
2✔
95

96
        PluginSet plugins = new(pluginDirectoriesPaths);
2✔
97
        plugins.LoadAll();
2✔
98
        plugins.RefreshPluginTypes();
2✔
99

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

114
        Key2JoyManager.commandRunner = commandRunner;
2✔
115

116
        var interopServer = new InteropServer(instance, commandRepository);
2✔
117

118
        try
119
        {
120
            interopServer.RestartListening();
2✔
121
            mainLoop(plugins);
2✔
122
        }
2✔
123
        finally
124
        {
125
            interopServer.StopListening();
2✔
126
            gamePadService.ShutDown();
2✔
127
        }
2✔
128
    }
2✔
129

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

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

135
    public void SetHandlerWithInvoke(IHaveHandleAndInvoke handleAndInvoker)
136
    {
137
        this.handleAndInvoker = handleAndInvoker;
×
138

139
        Console.WriteLine(READY_MESSAGE);
×
140
    }
×
141

142
    public bool GetIsArmed(MappingProfile profile = null)
143
    {
144
        if (profile == null)
×
145
        {
146
            return this.armedProfile != null;
×
147
        }
148

149
        return this.armedProfile == profile;
×
150
    }
151

152
    /// <inheritdoc/>
153
    public void ArmMappings(MappingProfile profile, bool withExplicitTriggerListeners = true)
154
    {
155
        this.armedProfile = profile;
2✔
156

157
        var allListeners = this.armedListeners = new();
2✔
158

159
        if (withExplicitTriggerListeners)
2!
160
        {
NEW
161
            allListeners.AddRange([
×
NEW
162
                // Trigger listeners that should explicitly loaded. This ensures that they're available for scripts
×
NEW
163
                // even if no mapping option is mapped to be triggered by it.
×
NEW
164
                // Always add these listeners so scripts can ask them if stuff has happened.
×
NEW
165
                KeyboardTriggerListener.NewInstance(),
×
NEW
166
                MouseButtonTriggerListener.NewInstance(),
×
NEW
167
                MouseMoveTriggerListener.NewInstance(),
×
NEW
168
                GamePadButtonTriggerListener.NewInstance(),
×
NEW
169
                GamePadStickTriggerListener.NewInstance(),
×
NEW
170
                GamePadTriggerTriggerListener.NewInstance(),
×
NEW
171
            ]);
×
172
        }
173

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

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

182
        try
183
        {
184
            foreach (var mappedOption in profile.MappedOptions)
8✔
185
            {
186
                if (mappedOption.Trigger == null)
2✔
187
                {
188
                    continue;
189
                }
190

191
                var listener = mappedOption.Trigger.GetTriggerListener();
2✔
192

193
                if (!allListeners.Contains(listener))
2✔
194
                {
195
                    allListeners.Add(listener);
2✔
196
                }
197

198
                if (listener.HasWndProcHandle)
2!
199
                {
NEW
200
                    wndProcListeners.Add(listener);
×
201
                }
202

203
                mappedOption.Action.OnStartListening(listener, ref allActions);
2✔
204
                listener.AddMappedOption(mappedOption);
2✔
205
            }
206

207
            var allListenersForSharing = (IList<AbstractTriggerListener>)allListeners;
2✔
208

209
            foreach (var listener in allListeners)
8✔
210
            {
211
                listener.StartListening(ref allListenersForSharing);
2✔
212
            }
213

214
            StatusChanged?.Invoke(this, new StatusChangedEventArgs
2!
215
            {
2✔
216
                IsEnabled = true,
2✔
217
                Profile = this.armedProfile
2✔
218
            });
2✔
219
        }
2✔
220
        catch (MappingArmingFailedException ex)
221
        {
222
            this.DisarmMappings();
×
223
            throw ex;
×
224
        }
225
    }
2✔
226

227
    public void CallWndProc(ref Message m)
228
    {
NEW
229
        foreach (var wndProcListener in wndProcListeners)
×
230
        {
NEW
231
            wndProcListener.WndProc(ref m);
×
232
        }
NEW
233
    }
×
234

235
    public void DisarmMappings()
236
    {
237
        var listeners = this.armedListeners;
2✔
238

239
        // Clear all intervals
240
        IdPool.CancelAll();
2✔
241

242
        foreach (var mappedOption in this.armedProfile.MappedOptions)
8✔
243
        {
244
            if (mappedOption.Trigger == null)
2✔
245
            {
246
                continue;
247
            }
248

249
            var listener = mappedOption.Trigger.GetTriggerListener();
2✔
250
            mappedOption.Action.OnStopListening(listener);
2✔
251

252
            if (!listeners.Contains(listener))
2!
253
            {
UNCOV
254
                listeners.Add(listener);
×
255
            }
256
        }
257

258
        wndProcListeners.Clear();
2✔
259

260
        foreach (var listener in listeners)
8✔
261
        {
262
            listener.StopListening();
2✔
263
        }
264

265
        var xInputService = ServiceLocator.Current.GetInstance<IXInputService>();
2✔
266
        xInputService.StopPolling();
2✔
267

268
        var gamePadService = ServiceLocator.Current.GetInstance<ISimulatedGamePadService>();
2✔
269
        gamePadService.EnsureAllUnplugged();
2✔
270

271
        this.armedProfile = null;
2✔
272

273
        StatusChanged?.Invoke(this, new StatusChangedEventArgs
2!
274
        {
2✔
275
            IsEnabled = false,
2✔
276
        });
2✔
277
    }
×
278

279
    /// <summary>
280
    /// Starts Key2Joy, pausing until it's ready
281
    /// </summary>
282
    public static void StartKey2Joy(bool startMinimized = true, bool pauseUntilReady = true)
283
    {
284
        var configManager = ServiceLocator.Current.GetInstance<IConfigManager>();
×
285
        var executablePath = configManager.GetConfigState().LastInstallPath;
×
286

287
        if (executablePath == null)
×
288
        {
289
            Console.WriteLine("Error! Key2Joy executable path is not known, please start Key2Joy at least once!");
×
290
            return;
×
291
        }
292

293
        if (!File.Exists(executablePath))
×
294
        {
295
            Console.WriteLine("Error! Key2Joy executable path is invalid, please start Key2Joy at least once (and don't move the executable)!");
×
296
            return;
×
297
        }
298

299
        Process process = new()
×
300
        {
×
301
            StartInfo = new ProcessStartInfo
×
302
            {
×
303
                FileName = executablePath,
×
304
                Arguments = startMinimized ? "--minimized" : "",
×
305
                UseShellExecute = false,
×
306
                RedirectStandardOutput = true
×
307
            }
×
308
        };
×
309

310
        process.Start();
×
311

312
        if (!pauseUntilReady)
×
313
        {
314
            return;
×
315
        }
316

317
        while (!process.StandardOutput.EndOfStream)
×
318
        {
319
            if (process.StandardOutput.ReadLine() == READY_MESSAGE)
×
320
            {
321
                break;
322
            }
323
        }
324
    }
×
325
}
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