diff --git a/src/BizHawk.Client.Common/Api/Interfaces/IEmuClient.cs b/src/BizHawk.Client.Common/Api/Interfaces/IEmuClient.cs
new file mode 100644
index 0000000000..b3fa3101aa
--- /dev/null
+++ b/src/BizHawk.Client.Common/Api/Interfaces/IEmuClient.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Drawing;
+
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Client.Common
+{
+ public interface IEmuClient
+ {
+ SystemInfo RunningSystem { get; }
+
+ ///
+ /// Occurs before a quickload is done (just after user has pressed the shortcut button
+ /// or has click on the item menu)
+ ///
+ event BeforeQuickLoadEventHandler BeforeQuickLoad;
+
+ ///
+ /// Occurs before a quicksave is done (just after user has pressed the shortcut button
+ /// or has click on the item menu)
+ ///
+ event BeforeQuickSaveEventHandler BeforeQuickSave;
+
+ ///
+ /// Occurs when a ROM is successfully loaded
+ ///
+ event EventHandler RomLoaded;
+
+ ///
+ /// Occurs when a savestate is successfully loaded
+ ///
+ event StateLoadedEventHandler StateLoaded;
+
+ ///
+ /// Occurs when a savestate is successfully saved
+ ///
+ event StateSavedEventHandler StateSaved;
+
+ int BorderHeight();
+
+ int BorderWidth();
+
+ int BufferHeight();
+
+ int BufferWidth();
+
+ void ClearAutohold();
+
+ void CloseEmulator();
+
+ void CloseEmulatorWithCode(int exitCode);
+
+ void CloseRom();
+
+ void DisplayMessages(bool value);
+
+ ///
+ /// THE FrameAdvance stuff
+ ///
+ void DoFrameAdvance();
+
+ ///
+ /// THE FrameAdvance stuff
+ /// Auto unpause emulation
+ ///
+ void DoFrameAdvanceAndUnpause();
+
+ void EnableRewind(bool enabled);
+
+ void FrameSkip(int numFrames);
+
+ ///
+ /// Gets a for specified player
+ ///
+ /// Player (one based) you want current inputs
+ /// A populated with current inputs
+ /// Raised when you specify a player less than 1 or greater than maximum allows (see SystemInfo class to get this information)
+ Joypad GetInput(int player);
+
+ bool GetSoundOn();
+
+ int GetTargetScanlineIntensity();
+
+ int GetWindowSize();
+
+ ///
+ /// Use with for CamHack.
+ /// Refer to MainForm.InvisibleEmulation for the workflow details.
+ ///
+ void InvisibleEmulation(bool invisible);
+
+ bool IsPaused();
+
+ bool IsSeeking();
+
+ bool IsTurbo();
+
+ ///
+ /// Load a savestate specified by its name
+ ///
+ /// Savestate friendly name
+ void LoadState(string name);
+
+ ///
+ /// Raised before a quickload is done (just after pressing shortcut button)
+ ///
+ /// Object who raised the event
+ /// Slot used for quickload
+ /// A boolean that can be set if users want to handle save themselves; if so, BizHawk won't do anything
+ void OnBeforeQuickLoad(object sender, string quickSaveSlotName, out bool eventHandled);
+
+ ///
+ /// Raised before a quicksave is done (just after pressing shortcut button)
+ ///
+ /// Object who raised the event
+ /// Slot used for quicksave
+ /// A boolean that can be set if users want to handle save themselves; if so, BizHawk won't do anything
+ void OnBeforeQuickSave(object sender, string quickSaveSlotName, out bool eventHandled);
+
+ ///
+ /// Raise when a rom is successfully Loaded
+ ///
+ void OnRomLoaded(IEmulator emu);
+
+ ///
+ /// Raise when a state is loaded
+ ///
+ /// Object who raised the event
+ /// User friendly name for saved state
+ void OnStateLoaded(object sender, string stateName);
+
+ ///
+ /// Raise when a state is saved
+ ///
+ /// Object who raised the event
+ /// User friendly name for saved state
+ void OnStateSaved(object sender, string stateName);
+
+ void OpenRom(string path);
+
+ void Pause();
+
+ void PauseAv();
+
+ void RebootCore();
+
+ void SaveRam();
+
+ ///
+ /// Save a state with specified name
+ ///
+ /// Savestate friendly name
+ void SaveState(string name);
+
+ int ScreenHeight();
+
+ void Screenshot(string path = null);
+
+ void ScreenshotToClipboard();
+
+ int ScreenWidth();
+
+ ///
+ /// Use with for CamHack.
+ /// Refer to MainForm.InvisibleEmulation for the workflow details.
+ ///
+ void SeekFrame(int frame);
+
+ ///
+ /// Sets the extra padding added to the 'native' surface so that you can draw HUD elements in predictable placements
+ ///
+ /// Left padding
+ /// Top padding
+ /// Right padding
+ /// Bottom padding
+ void SetExtraPadding(int left, int top = 0, int right = 0, int bottom = 0);
+
+ ///
+ /// Sets the extra padding added to the 'native' surface so that you can draw HUD elements in predictable placements
+ ///
+ /// Left padding
+ /// Top padding
+ /// Right padding
+ /// Bottom padding
+ void SetGameExtraPadding(int left, int top = 0, int right = 0, int bottom = 0);
+
+ ///
+ /// Set inputs in specified to specified player
+ ///
+ /// Player (one based) whom inputs must be set
+ /// with inputs
+ /// Raised when you specify a player less than 1 or greater than maximum allows (see SystemInfo class to get this information)
+ /// Still have some strange behaviour with multiple inputs; so this feature is still in beta
+ void SetInput(int player, Joypad joypad);
+
+ void SetScreenshotOSD(bool value);
+
+ void SetSoundOn(bool enable);
+
+ void SetTargetScanlineIntensity(int val);
+
+ void SetWindowSize(int size);
+
+ void SpeedMode(int percent);
+
+ void TogglePause();
+
+ Point TransformPoint(Point point);
+
+ void Unpause();
+
+ void UnpauseAv();
+
+ ///
+ /// Resume the emulation
+ ///
+ void UnpauseEmulation();
+
+ void UpdateEmulatorAndVP(IEmulator emu = null);
+
+ int Xpos();
+
+ int Ypos();
+ }
+}
diff --git a/src/BizHawk.Client.EmuHawk/Api/Joypad.cs b/src/BizHawk.Client.Common/Api/Joypad.cs
similarity index 95%
rename from src/BizHawk.Client.EmuHawk/Api/Joypad.cs
rename to src/BizHawk.Client.Common/Api/Joypad.cs
index a2f2fe58de..79d1eaa680 100644
--- a/src/BizHawk.Client.EmuHawk/Api/Joypad.cs
+++ b/src/BizHawk.Client.Common/Api/Joypad.cs
@@ -1,8 +1,8 @@
using System;
-using BizHawk.Client.Common;
+
using BizHawk.Common;
-namespace BizHawk.Client.EmuHawk
+namespace BizHawk.Client.Common
{
///
/// This class holds a joypad for any type of console
@@ -20,7 +20,7 @@ namespace BizHawk.Client.EmuHawk
/// What this is used for
/// Which player this controller is assigned to
/// not in range 1..max where max is .
- internal Joypad(SystemInfo system, int player)
+ public Joypad(SystemInfo system, int player)
{
if (!1.RangeTo(system.MaxControllers).Contains(player))
{
diff --git a/src/BizHawk.Client.EmuHawk/Api/EventTypes.cs b/src/BizHawk.Client.Common/EventTypes.cs
similarity index 93%
rename from src/BizHawk.Client.EmuHawk/Api/EventTypes.cs
rename to src/BizHawk.Client.Common/EventTypes.cs
index bbc9c82175..c9b4bfae8f 100644
--- a/src/BizHawk.Client.EmuHawk/Api/EventTypes.cs
+++ b/src/BizHawk.Client.Common/EventTypes.cs
@@ -1,13 +1,13 @@
using System;
-namespace BizHawk.Client.EmuHawk
+namespace BizHawk.Client.Common
{
///
/// This class holds event data for BeforeQuickLoad event
///
public sealed class BeforeQuickLoadEventArgs : EventArgs
{
- internal BeforeQuickLoadEventArgs(string name)
+ public BeforeQuickLoadEventArgs(string name)
{
Name = name;
}
@@ -35,7 +35,7 @@ namespace BizHawk.Client.EmuHawk
///
public sealed class BeforeQuickSaveEventArgs : EventArgs
{
- internal BeforeQuickSaveEventArgs(string name)
+ public BeforeQuickSaveEventArgs(string name)
{
Name = name;
}
@@ -66,7 +66,7 @@ namespace BizHawk.Client.EmuHawk
/// Initialize a new instance of
///
/// User friendly name of loaded state
- internal StateLoadedEventArgs(string stateName)
+ public StateLoadedEventArgs(string stateName)
{
Name = stateName;
}
@@ -86,7 +86,7 @@ namespace BizHawk.Client.EmuHawk
/// Initialize a new instance of
///
/// User friendly name of loaded state
- internal StateSavedEventArgs(string stateName)
+ public StateSavedEventArgs(string stateName)
{
Name = stateName;
}
diff --git a/src/BizHawk.Client.EmuHawk/Api/ClientApi.cs b/src/BizHawk.Client.EmuHawk/Api/ClientApi.cs
index 1370f26c1a..7aed212f74 100644
--- a/src/BizHawk.Client.EmuHawk/Api/ClientApi.cs
+++ b/src/BizHawk.Client.EmuHawk/Api/ClientApi.cs
@@ -1,449 +1,211 @@
using System;
-using System.Collections.Generic;
using System.Drawing;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using System.Windows.Forms;
using BizHawk.Client.Common;
-using BizHawk.Common;
using BizHawk.Emulation.Common;
-using BizHawk.Emulation.Cores.Nintendo.Gameboy;
-using BizHawk.Emulation.Cores.PCEngine;
-using BizHawk.Emulation.Cores.Sega.MasterSystem;
-// ReSharper disable UnusedMember.Global
namespace BizHawk.Client.EmuHawk
{
- ///
- /// This class contains some methods that
- /// interact with BizHawk client
- ///
public static class ClientApi
{
- private static IEmulator Emulator { get; set; }
+ ///
+ public static SystemInfo RunningSystem => GlobalWin.ClientApi.RunningSystem;
- private static IVideoProvider VideoProvider { get; set; }
-
- private static readonly IReadOnlyCollection JoypadButtonsArray = Enum.GetValues(typeof(JoypadButton)).Cast().ToList(); //TODO can the return of GetValues be cast to JoypadButton[]? --yoshi
-
- internal static readonly BizHawkSystemIdToEnumConverter SystemIdConverter = new BizHawkSystemIdToEnumConverter();
-
- private static readonly JoypadStringToEnumConverter JoypadConverter = new JoypadStringToEnumConverter();
-
- private static List _allJoyPads;
-
- ///
- /// Occurs before a quickload is done (just after user has pressed the shortcut button
- /// or has click on the item menu)
- ///
- public static event BeforeQuickLoadEventHandler BeforeQuickLoad;
-
- ///
- /// Occurs before a quicksave is done (just after user has pressed the shortcut button
- /// or has click on the item menu)
- ///
- public static event BeforeQuickSaveEventHandler BeforeQuickSave;
-
- ///
- /// Occurs when a ROM is successfully loaded
- ///
- public static event EventHandler RomLoaded;
-
- ///
- /// Occurs when a savestate is successfully loaded
- ///
- public static event StateLoadedEventHandler StateLoaded;
-
- ///
- /// Occurs when a savestate is successfully saved
- ///
- public static event StateSavedEventHandler StateSaved;
-
- public static void UpdateEmulatorAndVP(IEmulator emu = null)
+ ///
+ public static event BeforeQuickLoadEventHandler BeforeQuickLoad
{
- Emulator = emu;
- VideoProvider = emu.AsVideoProviderOrDefault();
+ add => GlobalWin.ClientApi.BeforeQuickLoad += value;
+ remove => GlobalWin.ClientApi.BeforeQuickLoad -= value;
}
- ///
- /// THE FrameAdvance stuff
- ///
- public static void DoFrameAdvance()
+ ///
+ public static event BeforeQuickSaveEventHandler BeforeQuickSave
{
- GlobalWin.MainForm.FrameAdvance();
- GlobalWin.MainForm.StepRunLoop_Throttle();
- GlobalWin.MainForm.Render();
+ add => GlobalWin.ClientApi.BeforeQuickSave += value;
+ remove => GlobalWin.ClientApi.BeforeQuickSave -= value;
}
- ///
- /// THE FrameAdvance stuff
- /// Auto unpause emulation
- ///
- public static void DoFrameAdvanceAndUnpause()
+ ///
+ public static event EventHandler RomLoaded
{
- DoFrameAdvance();
- UnpauseEmulation();
+ add => GlobalWin.ClientApi.RomLoaded += value;
+ remove => GlobalWin.ClientApi.RomLoaded -= value;
}
- ///
- /// Use with for CamHack.
- /// Refer to for the workflow details.
- ///
- public static void SeekFrame(int frame)
+ ///
+ public static event StateLoadedEventHandler StateLoaded
{
- var wasPaused = GlobalWin.MainForm.EmulatorPaused;
- while (Emulator.Frame != frame) GlobalWin.MainForm.SeekFrameAdvance();
- if (!wasPaused) GlobalWin.MainForm.UnpauseEmulator();
+ add => GlobalWin.ClientApi.StateLoaded += value;
+ remove => GlobalWin.ClientApi.StateLoaded -= value;
}
- ///
- /// Use with for CamHack.
- /// Refer to for the workflow details.
- ///
- public static void InvisibleEmulation(bool invisible) => GlobalWin.MainForm.InvisibleEmulation = invisible;
-
- ///
- /// Gets a for specified player
- ///
- /// Player (one based) you want current inputs
- /// A populated with current inputs
- /// Raised when you specify a player less than 1 or greater than maximum allows (see SystemInfo class to get this information)
- public static Joypad GetInput(int player)
+ ///
+ public static event StateSavedEventHandler StateSaved
{
- if (!1.RangeTo(RunningSystem.MaxControllers).Contains(player))
- throw new IndexOutOfRangeException($"{RunningSystem.DisplayName} does not support {player} controller(s)");
- GetAllInputs();
- return _allJoyPads[player - 1];
+ add => GlobalWin.ClientApi.StateSaved += value;
+ remove => GlobalWin.ClientApi.StateSaved -= value;
}
- ///
- /// Load a savestate specified by its name
- ///
- /// Savestate friendly name
- public static void LoadState(string name) => GlobalWin.MainForm.LoadState(Path.Combine(GlobalWin.Config.PathEntries.SaveStateAbsolutePath(GlobalWin.Game.System), $"{name}.State"), name, suppressOSD: false);
+ ///
+ public static int BorderHeight() => GlobalWin.ClientApi.BorderHeight();
- ///
- /// Raised before a quickload is done (just after pressing shortcut button)
- ///
- /// Object who raised the event
- /// Slot used for quickload
- /// A boolean that can be set if users want to handle save themselves; if so, BizHawk won't do anything
- public static void OnBeforeQuickLoad(object sender, string quickSaveSlotName, out bool eventHandled)
- {
- if (BeforeQuickLoad == null)
- {
- eventHandled = false;
- return;
- }
- var e = new BeforeQuickLoadEventArgs(quickSaveSlotName);
- BeforeQuickLoad(sender, e);
- eventHandled = e.Handled;
- }
+ ///
+ public static int BorderWidth() => GlobalWin.ClientApi.BorderWidth();
+ ///
+ public static int BufferHeight() => GlobalWin.ClientApi.BufferHeight();
- ///
- /// Raised before a quicksave is done (just after pressing shortcut button)
- ///
- /// Object who raised the event
- /// Slot used for quicksave
- /// A boolean that can be set if users want to handle save themselves; if so, BizHawk won't do anything
- public static void OnBeforeQuickSave(object sender, string quickSaveSlotName, out bool eventHandled)
- {
- if (BeforeQuickSave == null)
- {
- eventHandled = false;
- return;
- }
- var e = new BeforeQuickSaveEventArgs(quickSaveSlotName);
- BeforeQuickSave(sender, e);
- eventHandled = e.Handled;
- }
+ ///
+ public static int BufferWidth() => GlobalWin.ClientApi.BufferWidth();
+ ///
+ public static void ClearAutohold() => GlobalWin.ClientApi.ClearAutohold();
- ///
- /// Raise when a state is loaded
- ///
- /// Object who raised the event
- /// User friendly name for saved state
- public static void OnStateLoaded(object sender, string stateName) => StateLoaded?.Invoke(sender, new StateLoadedEventArgs(stateName));
+ ///
+ public static void CloseEmulator() => GlobalWin.ClientApi.CloseEmulator();
- ///
- /// Raise when a state is saved
- ///
- /// Object who raised the event
- /// User friendly name for saved state
- public static void OnStateSaved(object sender, string stateName) => StateSaved?.Invoke(sender, new StateSavedEventArgs(stateName));
+ ///
+ public static void CloseEmulatorWithCode(int exitCode) => GlobalWin.ClientApi.CloseEmulatorWithCode(exitCode);
- ///
- /// Raise when a rom is successfully Loaded
- ///
- public static void OnRomLoaded(IEmulator emu)
- {
- Emulator = emu;
- VideoProvider = emu.AsVideoProviderOrDefault();
- RomLoaded?.Invoke(null, EventArgs.Empty);
+ ///
+ public static void CloseRom() => GlobalWin.ClientApi.CloseRom();
- try
- {
- _allJoyPads = new List(RunningSystem.MaxControllers);
- for (var i = 1; i <= RunningSystem.MaxControllers; i++)
- _allJoyPads.Add(new Joypad(RunningSystem, i));
- }
- catch (Exception e)
- {
- Console.Error.WriteLine("Apihawk is garbage and may not work this session.");
- Console.Error.WriteLine(e);
- }
- }
+ ///
+ public static void DisplayMessages(bool value) => GlobalWin.ClientApi.DisplayMessages(value);
- ///
- /// Save a state with specified name
- ///
- /// Savestate friendly name
- public static void SaveState(string name) => GlobalWin.MainForm.SaveState(Path.Combine(GlobalWin.Config.PathEntries.SaveStateAbsolutePath(GlobalWin.Game.System), $"{name}.State"), name, fromLua: false);
+ ///
+ public static void DoFrameAdvance() => GlobalWin.ClientApi.DoFrameAdvance();
- ///
- /// Sets the extra padding added to the 'native' surface so that you can draw HUD elements in predictable placements
- ///
- /// Left padding
- /// Top padding
- /// Right padding
- /// Bottom padding
- public static void SetGameExtraPadding(int left, int top = 0, int right = 0, int bottom = 0)
- {
- GlobalWin.DisplayManager.GameExtraPadding = new Padding(left, top, right, bottom);
- GlobalWin.MainForm.FrameBufferResized();
- }
+ ///
+ public static void DoFrameAdvanceAndUnpause() => GlobalWin.ClientApi.DoFrameAdvanceAndUnpause();
- ///
- /// Sets the extra padding added to the 'native' surface so that you can draw HUD elements in predictable placements
- ///
- /// Left padding
- /// Top padding
- /// Right padding
- /// Bottom padding
- public static void SetExtraPadding(int left, int top = 0, int right = 0, int bottom = 0)
- {
- GlobalWin.DisplayManager.ClientExtraPadding = new Padding(left, top, right, bottom);
- GlobalWin.MainForm.FrameBufferResized();
- }
+ ///
+ public static void EnableRewind(bool enabled) => GlobalWin.ClientApi.EnableRewind(enabled);
- ///
- /// Set inputs in specified to specified player
- ///
- /// Player (one based) whom inputs must be set
- /// with inputs
- /// Raised when you specify a player less than 1 or greater than maximum allows (see SystemInfo class to get this information)
- /// Still have some strange behaviour with multiple inputs; so this feature is still in beta
- public static void SetInput(int player, Joypad joypad)
- {
- if (!1.RangeTo(RunningSystem.MaxControllers).Contains(player)) throw new IndexOutOfRangeException($"{RunningSystem.DisplayName} does not support {player} controller(s)");
+ ///
+ public static void FrameSkip(int numFrames) => GlobalWin.ClientApi.FrameSkip(numFrames);
- if (joypad.Inputs == 0)
- {
- GlobalWin.InputManager.AutofireStickyXorAdapter.ClearStickies();
- }
- else
- {
- foreach (var button in JoypadButtonsArray.Where(button => joypad.Inputs.HasFlag(button)))
- {
- GlobalWin.InputManager.AutofireStickyXorAdapter.SetSticky(
- RunningSystem == SystemInfo.GB
- ? $"{JoypadConverter.ConvertBack(button, RunningSystem)}"
- : $"P{player} {JoypadConverter.ConvertBack(button, RunningSystem)}",
- isSticky: true
- );
- }
- }
+ ///
+ public static Joypad GetInput(int player) => GlobalWin.ClientApi.GetInput(player);
-#if false // Using this breaks joypad usage (even in UI); have to figure out why
- if ((RunningSystem.AvailableButtons & JoypadButton.AnalogStick) == JoypadButton.AnalogStick)
- {
- for (var i = 1; i <= RunningSystem.MaxControllers; i++)
- {
- GlobalWin.InputManager.AutofireStickyXorAdapter.SetAxis($"P{i} X Axis", _allJoyPads[i - 1].AnalogX);
- GlobalWin.InputManager.AutofireStickyXorAdapter.SetAxis($"P{i} Y Axis", _allJoyPads[i - 1].AnalogY);
- }
- }
-#endif
- }
+ ///
+ public static bool GetSoundOn() => GlobalWin.ClientApi.GetSoundOn();
- ///
- /// Resume the emulation
- ///
- public static void UnpauseEmulation() => GlobalWin.MainForm.UnpauseEmulator();
+ ///
+ public static int GetTargetScanlineIntensity() => GlobalWin.ClientApi.GetTargetScanlineIntensity();
- ///
- /// Gets all current inputs for each joypad and store
- /// them in class collection
- ///
- private static void GetAllInputs()
- {
- var joypadAdapter = GlobalWin.InputManager.AutofireStickyXorAdapter;
+ ///
+ public static int GetWindowSize() => GlobalWin.ClientApi.GetWindowSize();
- var pressedButtons = joypadAdapter.Definition.BoolButtons.Where(b => joypadAdapter.IsPressed(b));
+ ///
+ public static void InvisibleEmulation(bool invisible) => GlobalWin.ClientApi.InvisibleEmulation(invisible);
- foreach (var j in _allJoyPads) j.ClearInputs();
+ ///
+ public static bool IsPaused() => GlobalWin.ClientApi.IsPaused();
- Parallel.ForEach(pressedButtons, button =>
- {
- if (RunningSystem == SystemInfo.GB) _allJoyPads[0].AddInput(JoypadConverter.Convert(button));
- else if (int.TryParse(button.Substring(1, 2), out var player)) _allJoyPads[player - 1].AddInput(JoypadConverter.Convert(button.Substring(3)));
- });
+ ///
+ public static bool IsSeeking() => GlobalWin.ClientApi.IsSeeking();
- if ((RunningSystem.AvailableButtons & JoypadButton.AnalogStick) == JoypadButton.AnalogStick)
- {
- for (var i = 1; i <= RunningSystem.MaxControllers; i++)
- {
- _allJoyPads[i - 1].AnalogX = joypadAdapter.AxisValue($"P{i} X Axis");
- _allJoyPads[i - 1].AnalogY = joypadAdapter.AxisValue($"P{i} Y Axis");
- }
- }
- }
+ ///
+ public static bool IsTurbo() => GlobalWin.ClientApi.IsTurbo();
- public static void CloseEmulator() => GlobalWin.MainForm.CloseEmulator();
+ ///
+ public static void LoadState(string name) => GlobalWin.ClientApi.LoadState(name);
- public static void CloseEmulatorWithCode(int exitCode) => GlobalWin.MainForm.CloseEmulator(exitCode);
+ ///
+ public static void OnBeforeQuickLoad(object sender, string quickSaveSlotName, out bool eventHandled) => GlobalWin.ClientApi.OnBeforeQuickLoad(sender, quickSaveSlotName, out eventHandled);
- public static int BorderHeight() => GlobalWin.DisplayManager.TransformPoint(new Point(0, 0)).Y;
+ ///
+ public static void OnBeforeQuickSave(object sender, string quickSaveSlotName, out bool eventHandled) => GlobalWin.ClientApi.OnBeforeQuickSave(sender, quickSaveSlotName, out eventHandled);
- public static int BorderWidth() => GlobalWin.DisplayManager.TransformPoint(new Point(0, 0)).X;
+ ///
+ public static void OnRomLoaded(IEmulator emu) => GlobalWin.ClientApi.OnRomLoaded(emu);
- public static int BufferHeight() => VideoProvider.BufferHeight;
+ ///
+ public static void OnStateLoaded(object sender, string stateName) => GlobalWin.ClientApi.OnStateLoaded(sender, stateName);
- public static int BufferWidth() => VideoProvider.BufferWidth;
+ ///
+ public static void OnStateSaved(object sender, string stateName) => GlobalWin.ClientApi.OnStateSaved(sender, stateName);
- public static void ClearAutohold() => GlobalWin.MainForm.ClearHolds();
+ ///
+ public static void OpenRom(string path) => GlobalWin.ClientApi.OpenRom(path);
- public static void CloseRom() => GlobalWin.MainForm.CloseRom();
+ ///
+ public static void Pause() => GlobalWin.ClientApi.Pause();
- public static void DisplayMessages(bool value) => GlobalWin.Config.DisplayMessages = value;
+ ///
+ public static void PauseAv() => GlobalWin.ClientApi.PauseAv();
- public static void EnableRewind(bool enabled) => GlobalWin.MainForm.EnableRewind(enabled);
+ ///
+ public static void RebootCore() => GlobalWin.ClientApi.RebootCore();
- public static void FrameSkip(int numFrames)
- {
- if (numFrames > 0)
- {
- GlobalWin.Config.FrameSkip = numFrames;
- GlobalWin.MainForm.FrameSkipMessage();
- }
- else
- {
- Console.WriteLine("Invalid frame skip value");
- }
- }
+ ///
+ public static void SaveRam() => GlobalWin.ClientApi.SaveRam();
- public static int GetTargetScanlineIntensity() => GlobalWin.Config.TargetScanlineFilterIntensity;
+ ///
+ public static void SaveState(string name) => GlobalWin.ClientApi.SaveState(name);
- public static int GetWindowSize() => GlobalWin.Config.TargetZoomFactors[Emulator.SystemId];
+ ///
+ public static int ScreenHeight() => GlobalWin.ClientApi.ScreenHeight();
- public static void SetSoundOn(bool enable)
- {
- if (enable != GlobalWin.Config.SoundEnabled) GlobalWin.MainForm.ToggleSound();
- }
+ ///
+ public static void Screenshot(string path = null) => GlobalWin.ClientApi.Screenshot(path);
- public static bool GetSoundOn() => GlobalWin.Config.SoundEnabled;
+ ///
+ public static void ScreenshotToClipboard() => GlobalWin.ClientApi.ScreenshotToClipboard();
- public static bool IsPaused() => GlobalWin.MainForm.EmulatorPaused;
+ ///
+ public static int ScreenWidth() => GlobalWin.ClientApi.ScreenWidth();
- public static bool IsTurbo() => GlobalWin.MainForm.IsTurboing;
+ ///
+ public static void SeekFrame(int frame) => GlobalWin.ClientApi.SeekFrame(frame);
- public static bool IsSeeking() => GlobalWin.MainForm.IsSeeking;
+ ///
+ public static void SetExtraPadding(int left, int top = 0, int right = 0, int bottom = 0) => GlobalWin.ClientApi.SetExtraPadding(left, top, right, bottom);
- public static void OpenRom(string path) => GlobalWin.MainForm.LoadRom(path, new MainForm.LoadRomArgs { OpenAdvanced = OpenAdvancedSerializer.ParseWithLegacy(path) });
+ ///
+ public static void SetGameExtraPadding(int left, int top = 0, int right = 0, int bottom = 0) => GlobalWin.ClientApi.SetGameExtraPadding(left, top, right, bottom);
- public static void Pause() => GlobalWin.MainForm.PauseEmulator();
+ ///
+ public static void SetInput(int player, Joypad joypad) => GlobalWin.ClientApi.SetInput(player, joypad);
- public static void PauseAv() => GlobalWin.MainForm.PauseAvi = true;
+ ///
+ public static void SetScreenshotOSD(bool value) => GlobalWin.ClientApi.SetScreenshotOSD(value);
- public static void RebootCore() => GlobalWin.MainForm.RebootCore();
+ ///
+ public static void SetSoundOn(bool enable) => GlobalWin.ClientApi.SetSoundOn(enable);
- public static void SaveRam() => GlobalWin.MainForm.FlushSaveRAM();
+ ///
+ public static void SetTargetScanlineIntensity(int val) => GlobalWin.ClientApi.SetTargetScanlineIntensity(val);
- public static int ScreenHeight() => GlobalWin.MainForm.PresentationPanel.NativeSize.Height;
+ ///
+ public static void SetWindowSize(int size) => GlobalWin.ClientApi.SetWindowSize(size);
- public static void Screenshot(string path = null)
- {
- if (path == null) GlobalWin.MainForm.TakeScreenshot();
- else GlobalWin.MainForm.TakeScreenshot(path);
- }
+ ///
+ public static void SpeedMode(int percent) => GlobalWin.ClientApi.SpeedMode(percent);
- public static void ScreenshotToClipboard() => GlobalWin.MainForm.TakeScreenshotToClipboard();
+ ///
+ public static void TogglePause() => GlobalWin.ClientApi.TogglePause();
- public static void SetTargetScanlineIntensity(int val) => GlobalWin.Config.TargetScanlineFilterIntensity = val;
+ ///
+ public static Point TransformPoint(Point point) => GlobalWin.ClientApi.TransformPoint(point);
- public static void SetScreenshotOSD(bool value) => GlobalWin.Config.ScreenshotCaptureOsd = value;
+ ///
+ public static void Unpause() => GlobalWin.ClientApi.Unpause();
- public static int ScreenWidth() => GlobalWin.MainForm.PresentationPanel.NativeSize.Width;
+ ///
+ public static void UnpauseAv() => GlobalWin.ClientApi.UnpauseAv();
- public static void SetWindowSize(int size)
- {
- if (size == 1 || size == 2 || size == 3 || size == 4 || size == 5 || size == 10)
- {
- GlobalWin.Config.TargetZoomFactors[Emulator.SystemId] = size;
- GlobalWin.MainForm.FrameBufferResized();
- GlobalWin.OSD.AddMessage($"Window size set to {size}x");
- }
- else
- {
- Console.WriteLine("Invalid window size");
- }
- }
+ ///
+ public static void UnpauseEmulation() => GlobalWin.ClientApi.UnpauseEmulation();
- public static void SpeedMode(int percent)
- {
- if (percent.StrictlyBoundedBy(0.RangeTo(6400))) GlobalWin.MainForm.ClickSpeedItem(percent);
- else Console.WriteLine("Invalid speed value");
- }
+ ///
+ public static void UpdateEmulatorAndVP(IEmulator emu = null) => GlobalWin.ClientApi.UpdateEmulatorAndVP(emu);
- public static void TogglePause() => GlobalWin.MainForm.TogglePause();
+ ///
+ public static int Xpos() => GlobalWin.ClientApi.Xpos();
- public static Point TransformPoint(Point point) => GlobalWin.DisplayManager.TransformPoint(point);
-
- public static void Unpause() => GlobalWin.MainForm.UnpauseEmulator();
-
- public static void UnpauseAv() => GlobalWin.MainForm.PauseAvi = false;
-
- public static int Xpos() => GlobalWin.MainForm.DesktopLocation.X;
-
- public static int Ypos() => GlobalWin.MainForm.DesktopLocation.Y;
-
- ///
- /// Gets current emulated system
- ///
- public static SystemInfo RunningSystem
- {
- get
- {
- switch (GlobalWin.Emulator.SystemId)
- {
- case "PCE" when GlobalWin.Emulator is PCEngine pceHawk:
- return pceHawk.Type switch
- {
- NecSystemType.TurboGrafx => SystemInfo.PCE,
- NecSystemType.TurboCD => SystemInfo.PCECD,
- NecSystemType.SuperGrafx => SystemInfo.SGX,
- _ => throw new ArgumentOutOfRangeException()
- };
- case "PCE":
- return SystemInfo.PCE; // not always accurate, but anyone wanting accuracy has probably figured out how to use IEmu.GetSystemId()
- case "SMS":
- var sms = (SMS) GlobalWin.Emulator;
- return sms.IsSG1000
- ? SystemInfo.SG
- : sms.IsGameGear
- ? SystemInfo.GG
- : SystemInfo.SMS;
- case "GB":
- if (GlobalWin.Emulator is Gameboy gb) return gb.IsCGBMode() ? SystemInfo.GBC : SystemInfo.GB;
- return SystemInfo.DualGB;
- default:
- return SystemInfo.FindByCoreSystem(SystemIdConverter.Convert(GlobalWin.Emulator.SystemId));
- }
- }
- }
+ ///
+ public static int Ypos() => GlobalWin.ClientApi.Ypos();
}
}
diff --git a/src/BizHawk.Client.EmuHawk/Api/Libraries/EmuClientApi.cs b/src/BizHawk.Client.EmuHawk/Api/Libraries/EmuClientApi.cs
new file mode 100644
index 0000000000..8739667ffd
--- /dev/null
+++ b/src/BizHawk.Client.EmuHawk/Api/Libraries/EmuClientApi.cs
@@ -0,0 +1,340 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+using BizHawk.Client.Common;
+using BizHawk.Common;
+using BizHawk.Emulation.Common;
+using BizHawk.Emulation.Cores.Nintendo.Gameboy;
+using BizHawk.Emulation.Cores.PCEngine;
+using BizHawk.Emulation.Cores.Sega.MasterSystem;
+
+namespace BizHawk.Client.EmuHawk
+{
+ public class EmuClientApi : IEmuClient
+ {
+ private List _allJoyPads;
+
+ private IEmulator Emulator { get; set; }
+
+ private readonly IReadOnlyCollection JoypadButtonsArray = Enum.GetValues(typeof(JoypadButton)).Cast().ToList(); //TODO can the return of GetValues be cast to JoypadButton[]? --yoshi
+
+ private readonly JoypadStringToEnumConverter JoypadConverter = new JoypadStringToEnumConverter();
+
+ public SystemInfo RunningSystem
+ {
+ get
+ {
+ switch (GlobalWin.Emulator.SystemId)
+ {
+ case "PCE" when GlobalWin.Emulator is PCEngine pceHawk:
+ return pceHawk.Type switch
+ {
+ NecSystemType.TurboGrafx => SystemInfo.PCE,
+ NecSystemType.TurboCD => SystemInfo.PCECD,
+ NecSystemType.SuperGrafx => SystemInfo.SGX,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ case "PCE":
+ return SystemInfo.PCE; // not always accurate, but anyone wanting accuracy has probably figured out how to use IEmu.GetSystemId()
+ case "SMS":
+ var sms = (SMS) GlobalWin.Emulator;
+ return sms.IsSG1000
+ ? SystemInfo.SG
+ : sms.IsGameGear
+ ? SystemInfo.GG
+ : SystemInfo.SMS;
+ case "GB":
+ if (GlobalWin.Emulator is Gameboy gb) return gb.IsCGBMode() ? SystemInfo.GBC : SystemInfo.GB;
+ return SystemInfo.DualGB;
+ default:
+ return SystemInfo.FindByCoreSystem(SystemIdConverter.Convert(GlobalWin.Emulator.SystemId));
+ }
+ }
+ }
+
+ internal readonly BizHawkSystemIdToEnumConverter SystemIdConverter = new BizHawkSystemIdToEnumConverter();
+
+ private IVideoProvider VideoProvider { get; set; }
+
+ public event BeforeQuickLoadEventHandler BeforeQuickLoad;
+
+ public event BeforeQuickSaveEventHandler BeforeQuickSave;
+
+ public event EventHandler RomLoaded;
+
+ public event StateLoadedEventHandler StateLoaded;
+
+ public event StateSavedEventHandler StateSaved;
+
+ public int BorderHeight() => GlobalWin.DisplayManager.TransformPoint(new Point(0, 0)).Y;
+
+ public int BorderWidth() => GlobalWin.DisplayManager.TransformPoint(new Point(0, 0)).X;
+
+ public int BufferHeight() => VideoProvider.BufferHeight;
+
+ public int BufferWidth() => VideoProvider.BufferWidth;
+
+ public void ClearAutohold() => GlobalWin.MainForm.ClearHolds();
+
+ public void CloseEmulator() => GlobalWin.MainForm.CloseEmulator();
+
+ public void CloseEmulatorWithCode(int exitCode) => GlobalWin.MainForm.CloseEmulator(exitCode);
+
+ public void CloseRom() => GlobalWin.MainForm.CloseRom();
+
+ public void DisplayMessages(bool value) => GlobalWin.Config.DisplayMessages = value;
+
+ public void DoFrameAdvance()
+ {
+ GlobalWin.MainForm.FrameAdvance();
+ GlobalWin.MainForm.StepRunLoop_Throttle();
+ GlobalWin.MainForm.Render();
+ }
+
+ public void DoFrameAdvanceAndUnpause()
+ {
+ DoFrameAdvance();
+ UnpauseEmulation();
+ }
+
+ public void EnableRewind(bool enabled) => GlobalWin.MainForm.EnableRewind(enabled);
+
+ public void FrameSkip(int numFrames)
+ {
+ if (numFrames > 0)
+ {
+ GlobalWin.Config.FrameSkip = numFrames;
+ GlobalWin.MainForm.FrameSkipMessage();
+ }
+ else
+ {
+ Console.WriteLine("Invalid frame skip value");
+ }
+ }
+
+ private void GetAllInputs()
+ {
+ var joypadAdapter = GlobalWin.InputManager.AutofireStickyXorAdapter;
+
+ var pressedButtons = joypadAdapter.Definition.BoolButtons.Where(b => joypadAdapter.IsPressed(b));
+
+ foreach (var j in _allJoyPads) j.ClearInputs();
+
+ Parallel.ForEach(pressedButtons, button =>
+ {
+ if (RunningSystem == SystemInfo.GB) _allJoyPads[0].AddInput(JoypadConverter.Convert(button));
+ else if (int.TryParse(button.Substring(1, 2), out var player)) _allJoyPads[player - 1].AddInput(JoypadConverter.Convert(button.Substring(3)));
+ });
+
+ if ((RunningSystem.AvailableButtons & JoypadButton.AnalogStick) == JoypadButton.AnalogStick)
+ {
+ for (var i = 1; i <= RunningSystem.MaxControllers; i++)
+ {
+ _allJoyPads[i - 1].AnalogX = joypadAdapter.AxisValue($"P{i} X Axis");
+ _allJoyPads[i - 1].AnalogY = joypadAdapter.AxisValue($"P{i} Y Axis");
+ }
+ }
+ }
+
+ public Joypad GetInput(int player)
+ {
+ if (!1.RangeTo(RunningSystem.MaxControllers).Contains(player))
+ throw new IndexOutOfRangeException($"{RunningSystem.DisplayName} does not support {player} controller(s)");
+ GetAllInputs();
+ return _allJoyPads[player - 1];
+ }
+
+ public bool GetSoundOn() => GlobalWin.Config.SoundEnabled;
+
+ public int GetTargetScanlineIntensity() => GlobalWin.Config.TargetScanlineFilterIntensity;
+
+ public int GetWindowSize() => GlobalWin.Config.TargetZoomFactors[Emulator.SystemId];
+
+ public void InvisibleEmulation(bool invisible) => GlobalWin.MainForm.InvisibleEmulation = invisible;
+
+ public bool IsPaused() => GlobalWin.MainForm.EmulatorPaused;
+
+ public bool IsSeeking() => GlobalWin.MainForm.IsSeeking;
+
+ public bool IsTurbo() => GlobalWin.MainForm.IsTurboing;
+
+ public void LoadState(string name) => GlobalWin.MainForm.LoadState(Path.Combine(GlobalWin.Config.PathEntries.SaveStateAbsolutePath(GlobalWin.Game.System), $"{name}.State"), name, suppressOSD: false);
+
+ public void OnBeforeQuickLoad(object sender, string quickSaveSlotName, out bool eventHandled)
+ {
+ if (BeforeQuickLoad == null)
+ {
+ eventHandled = false;
+ return;
+ }
+ var e = new BeforeQuickLoadEventArgs(quickSaveSlotName);
+ BeforeQuickLoad(sender, e);
+ eventHandled = e.Handled;
+ }
+
+ public void OnBeforeQuickSave(object sender, string quickSaveSlotName, out bool eventHandled)
+ {
+ if (BeforeQuickSave == null)
+ {
+ eventHandled = false;
+ return;
+ }
+ var e = new BeforeQuickSaveEventArgs(quickSaveSlotName);
+ BeforeQuickSave(sender, e);
+ eventHandled = e.Handled;
+ }
+
+ public void OnRomLoaded(IEmulator emu)
+ {
+ Emulator = emu;
+ VideoProvider = emu.AsVideoProviderOrDefault();
+ RomLoaded?.Invoke(null, EventArgs.Empty);
+
+ try
+ {
+ _allJoyPads = new List(RunningSystem.MaxControllers);
+ for (var i = 1; i <= RunningSystem.MaxControllers; i++)
+ _allJoyPads.Add(new Joypad(RunningSystem, i));
+ }
+ catch (Exception e)
+ {
+ Console.Error.WriteLine("Apihawk is garbage and may not work this session.");
+ Console.Error.WriteLine(e);
+ }
+ }
+
+ public void OnStateLoaded(object sender, string stateName) => StateLoaded?.Invoke(sender, new StateLoadedEventArgs(stateName));
+
+ public void OnStateSaved(object sender, string stateName) => StateSaved?.Invoke(sender, new StateSavedEventArgs(stateName));
+
+ public void OpenRom(string path) => GlobalWin.MainForm.LoadRom(path, new MainForm.LoadRomArgs { OpenAdvanced = OpenAdvancedSerializer.ParseWithLegacy(path) });
+
+ public void Pause() => GlobalWin.MainForm.PauseEmulator();
+
+ public void PauseAv() => GlobalWin.MainForm.PauseAvi = true;
+
+ public void RebootCore() => GlobalWin.MainForm.RebootCore();
+
+ public void SaveRam() => GlobalWin.MainForm.FlushSaveRAM();
+
+ public void SaveState(string name) => GlobalWin.MainForm.SaveState(Path.Combine(GlobalWin.Config.PathEntries.SaveStateAbsolutePath(GlobalWin.Game.System), $"{name}.State"), name, fromLua: false);
+
+ public int ScreenHeight() => GlobalWin.MainForm.PresentationPanel.NativeSize.Height;
+
+ public void Screenshot(string path)
+ {
+ if (path == null) GlobalWin.MainForm.TakeScreenshot();
+ else GlobalWin.MainForm.TakeScreenshot(path);
+ }
+
+ public void ScreenshotToClipboard() => GlobalWin.MainForm.TakeScreenshotToClipboard();
+
+ public int ScreenWidth() => GlobalWin.MainForm.PresentationPanel.NativeSize.Width;
+
+ public void SeekFrame(int frame)
+ {
+ var wasPaused = GlobalWin.MainForm.EmulatorPaused;
+ while (Emulator.Frame != frame) GlobalWin.MainForm.SeekFrameAdvance();
+ if (!wasPaused) GlobalWin.MainForm.UnpauseEmulator();
+ }
+
+ public void SetExtraPadding(int left, int top, int right, int bottom)
+ {
+ GlobalWin.DisplayManager.ClientExtraPadding = new Padding(left, top, right, bottom);
+ GlobalWin.MainForm.FrameBufferResized();
+ }
+
+ public void SetGameExtraPadding(int left, int top, int right, int bottom)
+ {
+ GlobalWin.DisplayManager.GameExtraPadding = new Padding(left, top, right, bottom);
+ GlobalWin.MainForm.FrameBufferResized();
+ }
+
+ public void SetInput(int player, Joypad joypad)
+ {
+ if (!1.RangeTo(RunningSystem.MaxControllers).Contains(player)) throw new IndexOutOfRangeException($"{RunningSystem.DisplayName} does not support {player} controller(s)");
+
+ if (joypad.Inputs == 0)
+ {
+ GlobalWin.InputManager.AutofireStickyXorAdapter.ClearStickies();
+ }
+ else
+ {
+ foreach (var button in JoypadButtonsArray.Where(button => joypad.Inputs.HasFlag(button)))
+ {
+ GlobalWin.InputManager.AutofireStickyXorAdapter.SetSticky(
+ RunningSystem == SystemInfo.GB
+ ? $"{JoypadConverter.ConvertBack(button, RunningSystem)}"
+ : $"P{player} {JoypadConverter.ConvertBack(button, RunningSystem)}",
+ isSticky: true
+ );
+ }
+ }
+
+#if false // Using this breaks joypad usage (even in UI); have to figure out why
+ if ((RunningSystem.AvailableButtons & JoypadButton.AnalogStick) == JoypadButton.AnalogStick)
+ {
+ for (var i = 1; i <= RunningSystem.MaxControllers; i++)
+ {
+ GlobalWin.InputManager.AutofireStickyXorAdapter.SetAxis($"P{i} X Axis", _allJoyPads[i - 1].AnalogX);
+ GlobalWin.InputManager.AutofireStickyXorAdapter.SetAxis($"P{i} Y Axis", _allJoyPads[i - 1].AnalogY);
+ }
+ }
+#endif
+ }
+
+ public void SetScreenshotOSD(bool value) => GlobalWin.Config.ScreenshotCaptureOsd = value;
+
+ public void SetSoundOn(bool enable)
+ {
+ if (enable != GlobalWin.Config.SoundEnabled) GlobalWin.MainForm.ToggleSound();
+ }
+
+ public void SetTargetScanlineIntensity(int val) => GlobalWin.Config.TargetScanlineFilterIntensity = val;
+
+ public void SetWindowSize(int size)
+ {
+ if (size == 1 || size == 2 || size == 3 || size == 4 || size == 5 || size == 10)
+ {
+ GlobalWin.Config.TargetZoomFactors[Emulator.SystemId] = size;
+ GlobalWin.MainForm.FrameBufferResized();
+ GlobalWin.OSD.AddMessage($"Window size set to {size}x");
+ }
+ else
+ {
+ Console.WriteLine("Invalid window size");
+ }
+ }
+
+ public void SpeedMode(int percent)
+ {
+ if (percent.StrictlyBoundedBy(0.RangeTo(6400))) GlobalWin.MainForm.ClickSpeedItem(percent);
+ else Console.WriteLine("Invalid speed value");
+ }
+
+ public void TogglePause() => GlobalWin.MainForm.TogglePause();
+
+ public Point TransformPoint(Point point) => GlobalWin.DisplayManager.TransformPoint(point);
+
+ public void Unpause() => GlobalWin.MainForm.UnpauseEmulator();
+
+ public void UnpauseAv() => GlobalWin.MainForm.PauseAvi = false;
+
+ public void UnpauseEmulation() => GlobalWin.MainForm.UnpauseEmulator();
+
+ public void UpdateEmulatorAndVP(IEmulator emu)
+ {
+ Emulator = emu;
+ VideoProvider = emu.AsVideoProviderOrDefault();
+ }
+
+ public int Xpos() => GlobalWin.MainForm.DesktopLocation.X;
+
+ public int Ypos() => GlobalWin.MainForm.DesktopLocation.Y;
+ }
+}
diff --git a/src/BizHawk.Client.EmuHawk/GlobalWin.cs b/src/BizHawk.Client.EmuHawk/GlobalWin.cs
index a5f9ae269d..4ae81959e0 100644
--- a/src/BizHawk.Client.EmuHawk/GlobalWin.cs
+++ b/src/BizHawk.Client.EmuHawk/GlobalWin.cs
@@ -47,5 +47,7 @@ namespace BizHawk.Client.EmuHawk
public static GameInfo Game { get; set; }
public static IMovieSession MovieSession { get; set; }
public static InputManager InputManager { get; } = new InputManager();
+
+ public static EmuClientApi ClientApi { get; set; }
}
}
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs
index 14b766e7f0..6c65110e4d 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.cs
@@ -275,6 +275,7 @@ namespace BizHawk.Client.EmuHawk
public MainForm(string[] args)
{
GlobalWin.MainForm = this;
+ GlobalWin.ClientApi = new EmuClientApi();
//do this threaded stuff early so it has plenty of time to run in background
Database.InitializeDatabase(Path.Combine(PathUtils.ExeDirectoryPath, "gamedb", "gamedb.txt"));
diff --git a/src/BizHawk.Client.EmuHawk/tools/ExternalToolManager.cs b/src/BizHawk.Client.EmuHawk/tools/ExternalToolManager.cs
index 3979146488..4165c63460 100644
--- a/src/BizHawk.Client.EmuHawk/tools/ExternalToolManager.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/ExternalToolManager.cs
@@ -99,7 +99,7 @@ namespace BizHawk.Client.EmuHawk
item.Tag = (externalToolFile.Location, entryPoint.FullName); // Tag set => no errors (show custom icon even when disabled)
if (applicabilityAttrs.Count == 1)
{
- var system = ClientApi.SystemIdConverter.Convert(GlobalWin.Emulator.SystemId);
+ var system = GlobalWin.ClientApi.SystemIdConverter.Convert(GlobalWin.Emulator.SystemId);
if (applicabilityAttrs[0].NotApplicableTo(system))
{
item.ToolTipText = system == CoreSystem.Null