diff --git a/src/BizHawk.Client.Common/IMainFormForTools.cs b/src/BizHawk.Client.Common/IMainFormForTools.cs
index 453e772b5e..1d4ca05ff9 100644
--- a/src/BizHawk.Client.Common/IMainFormForTools.cs
+++ b/src/BizHawk.Client.Common/IMainFormForTools.cs
@@ -109,5 +109,7 @@ namespace BizHawk.Client.Common
/// only referenced from TAStudio
void UpdateWindowTitle();
+
+ public EmuClientApi EmuClient { get; set; }
}
}
diff --git a/src/BizHawk.Client.EmuHawk/ToolAttribute.cs b/src/BizHawk.Client.Common/ToolAttribute.cs
similarity index 92%
rename from src/BizHawk.Client.EmuHawk/ToolAttribute.cs
rename to src/BizHawk.Client.Common/ToolAttribute.cs
index 1f22ffac89..7f9e608f1b 100644
--- a/src/BizHawk.Client.EmuHawk/ToolAttribute.cs
+++ b/src/BizHawk.Client.Common/ToolAttribute.cs
@@ -1,25 +1,25 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace BizHawk.Client.EmuHawk
-{
- [AttributeUsage(AttributeTargets.Class)]
- public sealed class ToolAttribute : Attribute
- {
- public ToolAttribute(bool released, string[] supportedSystems, string[] unsupportedCores)
- {
- Released = released;
- SupportedSystems = supportedSystems ?? Enumerable.Empty();
- UnsupportedCores = unsupportedCores ?? Enumerable.Empty();
- }
-
- public ToolAttribute(bool released, string[] supportedSystems) : this(released, supportedSystems, null) {}
-
- public bool Released { get; }
-
- public IEnumerable SupportedSystems { get; }
-
- public IEnumerable UnsupportedCores { get; }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace BizHawk.Client.Common
+{
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class ToolAttribute : Attribute
+ {
+ public ToolAttribute(bool released, string[] supportedSystems, string[] unsupportedCores)
+ {
+ Released = released;
+ SupportedSystems = supportedSystems ?? Enumerable.Empty();
+ UnsupportedCores = unsupportedCores ?? Enumerable.Empty();
+ }
+
+ public ToolAttribute(bool released, string[] supportedSystems) : this(released, supportedSystems, null) {}
+
+ public bool Released { get; }
+
+ public IEnumerable SupportedSystems { get; }
+
+ public IEnumerable UnsupportedCores { get; }
+ }
+}
diff --git a/src/BizHawk.Client.Common/tools/ToolManagerBase.cs b/src/BizHawk.Client.Common/tools/ToolManagerBase.cs
new file mode 100644
index 0000000000..634f2a1d46
--- /dev/null
+++ b/src/BizHawk.Client.Common/tools/ToolManagerBase.cs
@@ -0,0 +1,600 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.ComponentModel;
+
+using BizHawk.Common;
+using BizHawk.Common.CollectionExtensions;
+using BizHawk.Common.ReflectionExtensions;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Client.Common
+{
+ public abstract class ToolManagerBase : IToolManager
+ {
+ protected readonly IMainFormForTools _mainFormTools;
+ private readonly IMainFormForApi _mainFormApi;
+ protected Config _config;
+ protected readonly DisplayManagerBase _displayManager;
+ private readonly ExternalToolManager _extToolManager;
+ protected readonly InputManager _inputManager;
+ private IExternalApiProvider _apiProvider;
+ protected IEmulator _emulator;
+ protected readonly IMovieSession _movieSession;
+ protected IGameInfo _game;
+
+ // TODO: merge ToolHelper code where logical
+ // For instance, add an IToolForm property called UsesCheats, so that a UpdateCheatRelatedTools() method can update all tools of this type
+ // Also a UsesRam, and similar method
+ private readonly List _tools = new List();
+
+ private IExternalApiProvider ApiProvider
+ {
+ get => _apiProvider;
+ set => _mainFormTools.EmuClient = (EmuClientApi)(_apiProvider = value).GetApi();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ToolManagerBase(
+ IMainFormForTools owner,
+ IMainFormForApi mainFormApi,
+ Config config,
+ DisplayManagerBase displayManager,
+ ExternalToolManager extToolManager,
+ InputManager inputManager,
+ IEmulator emulator,
+ IMovieSession movieSession,
+ IGameInfo game)
+ {
+ _mainFormTools = owner;
+ _mainFormApi = mainFormApi;
+ _config = config;
+ _displayManager = displayManager;
+ _extToolManager = extToolManager;
+ _inputManager = inputManager;
+ _emulator = emulator;
+ _movieSession = movieSession;
+ _game = game;
+ ApiProvider = ApiManager.Restart(_emulator.ServiceProvider, _mainFormApi, _displayManager, _inputManager, _movieSession, this, _config, _emulator, _game);
+ }
+
+ ///
+ /// Loads the tool dialog T (T must implements ) , if it does not exist it will be created, if it is already open, it will be focused
+ /// This method should be used only if you can't use the generic one
+ ///
+ /// Type of tool you want to load
+ /// Define if the tool form has to get the focus or not (Default is true)
+ /// An instantiated
+ /// Raised if can't cast into IToolForm
+ public IToolForm Load(Type toolType, bool focus = true)
+ {
+ if (!typeof(IToolForm).IsAssignableFrom(toolType))
+ {
+ throw new ArgumentException(message: $"Type {toolType.Name} does not implement {nameof(IToolForm)}.", paramName: nameof(toolType));
+ }
+ var mi = typeof(ToolManagerBase).GetMethod(nameof(Load), new[] { typeof(bool), typeof(string) })!.MakeGenericMethod(toolType);
+ return (IToolForm)mi.Invoke(this, new object[] { focus, "" });
+ }
+
+ // If the form inherits ToolFormBase, it will set base properties such as Tools, Config, etc
+ protected abstract void SetBaseProperties(IToolForm form);
+
+
+ protected abstract void SetFormParent(IToolForm form);
+
+ ///
+ /// Loads the tool dialog T (T must implement ) , if it does not exist it will be created, if it is already open, it will be focused
+ ///
+ /// Define if the tool form has to get the focus or not (Default is true)
+ /// Path to the .dll of the external tool
+ /// Type of tool you want to load
+ /// An instantiated
+ public T Load(bool focus = true, string toolPath = "")
+ where T : class, IToolForm
+ {
+ if (!IsAvailable()) return null;
+
+ var existingTool = _tools.OfType().FirstOrDefault();
+ if (existingTool != null)
+ {
+ if (existingTool.IsLoaded)
+ {
+ if (focus)
+ {
+ existingTool.Show();
+ existingTool.Focus();
+ }
+
+ return existingTool;
+ }
+
+ _tools.Remove(existingTool);
+ }
+
+ if (CreateInstance(toolPath) is not T newTool) return null;
+
+ SetFormParent(newTool);
+ if (!ServiceInjector.UpdateServices(_emulator.ServiceProvider, newTool)) return null; //TODO pass `true` for `mayCache` when from EmuHawk assembly
+ SetBaseProperties(newTool);
+ var toolTypeName = typeof(T).FullName!;
+ // auto settings
+ if (newTool is IToolFormAutoConfig autoConfigTool)
+ {
+ AttachSettingHooks(autoConfigTool, _config.CommonToolSettings.GetValueOrPutNew(toolTypeName));
+ }
+ // custom settings
+ if (HasCustomConfig(newTool))
+ {
+ InstallCustomConfig(newTool, _config.CustomToolSettings.GetValueOrPutNew(toolTypeName));
+ }
+
+ newTool.Restart();
+ newTool.Show();
+ return newTool;
+ }
+
+ /// Loads the external tool's entry form.
+ public IExternalToolForm LoadExternalToolForm(string toolPath, string customFormTypeName, bool focus = true, bool skipExtToolWarning = false)
+ {
+ var existingTool = _tools.OfType().FirstOrDefault(t => t.GetType().Assembly.Location == toolPath);
+ if (existingTool != null)
+ {
+ if (existingTool.IsActive)
+ {
+ if (focus)
+ {
+ existingTool.Show();
+ existingTool.Focus();
+ }
+ return existingTool;
+ }
+
+ _tools.Remove(existingTool);
+ }
+
+ var newTool = (IExternalToolForm)CreateInstance(typeof(IExternalToolForm), toolPath, customFormTypeName, skipExtToolWarning: skipExtToolWarning);
+ if (newTool == null) return null;
+ SetFormParent(newTool);
+ if (!(ServiceInjector.UpdateServices(_emulator.ServiceProvider, newTool) && ApiInjector.UpdateApis(ApiProvider, newTool))) return null;
+ SetBaseProperties(newTool);
+ // auto settings
+ if (newTool is IToolFormAutoConfig autoConfigTool)
+ {
+ AttachSettingHooks(autoConfigTool, _config.CommonToolSettings.GetValueOrPutNew(customFormTypeName));
+ }
+ // custom settings
+ if (HasCustomConfig(newTool))
+ {
+ InstallCustomConfig(newTool, _config.CustomToolSettings.GetValueOrPutNew(customFormTypeName));
+ }
+
+ newTool.Restart();
+ newTool.Show();
+ return newTool;
+ }
+
+ public void AutoLoad()
+ {
+ var genericSettings = _config.CommonToolSettings
+ .Where(kvp => kvp.Value.AutoLoad)
+ .Select(kvp => kvp.Key);
+
+ var customSettings = _config.CustomToolSettings
+ .Where(list => list.Value.Any(kvp => kvp.Value is ToolDialogSettings settings && settings.AutoLoad))
+ .Select(kvp => kvp.Key);
+
+ var typeNames = genericSettings.Concat(customSettings);
+
+ foreach (var typename in typeNames)
+ {
+ // this type resolution might not be sufficient. more investigation is needed
+ Type t = Type.GetType(typename);
+ if (t == null)
+ {
+ Console.WriteLine("BENIGN: Couldn't find type {0}", typename);
+ }
+ else
+ {
+ if (!IsLoaded(t))
+ {
+ Load(t, false);
+ }
+ }
+ }
+ }
+
+ protected abstract void AttachSettingHooks(IToolFormAutoConfig tool, ToolDialogSettings settings);
+
+ private static bool HasCustomConfig(IToolForm tool)
+ {
+ return tool.GetType().GetPropertiesWithAttrib(typeof(ConfigPersistAttribute)).Any();
+ }
+
+ protected abstract void SetFormClosingEvent(IToolForm form, Action action);
+
+ private void InstallCustomConfig(IToolForm tool, Dictionary data)
+ {
+ Type type = tool.GetType();
+ var props = type.GetPropertiesWithAttrib(typeof(ConfigPersistAttribute)).ToList();
+ if (props.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var prop in props)
+ {
+ if (data.TryGetValue(prop.Name, out var val))
+ {
+ if (val is string str && prop.PropertyType != typeof(string))
+ {
+ // if a type has a TypeConverter, and that converter can convert to string,
+ // that will be used in place of object markup by JSON.NET
+
+ // but that doesn't work with $type metadata, and JSON.NET fails to fall
+ // back on regular object serialization when needed. so try to undo a TypeConverter
+ // operation here
+ var converter = TypeDescriptor.GetConverter(prop.PropertyType);
+ val = converter.ConvertFromString(null, CultureInfo.InvariantCulture, str);
+ }
+ else if (val is not bool && prop.PropertyType.IsPrimitive)
+ {
+ // numeric constants are similarly hosed
+ val = Convert.ChangeType(val, prop.PropertyType, CultureInfo.InvariantCulture);
+ }
+
+ prop.SetValue(tool, val, null);
+ }
+ }
+
+ SetFormClosingEvent(tool, () => SaveCustomConfig(tool, data, props));
+ }
+
+ private static void SaveCustomConfig(IToolForm tool, Dictionary data, List props)
+ {
+ data.Clear();
+ foreach (var prop in props)
+ {
+ data.Add(prop.Name, prop.GetValue(tool, BindingFlags.GetProperty, Type.DefaultBinder, null, CultureInfo.InvariantCulture));
+ }
+ }
+
+ ///
+ /// Determines whether a given IToolForm is already loaded
+ ///
+ /// Type of tool to check
+ /// yo why do we have 4 versions of this, each with slightly different behaviour in edge cases --yoshi
+ public bool IsLoaded() where T : IToolForm
+ => _tools.OfType().FirstOrDefault()?.IsActive is true;
+
+ public bool IsLoaded(Type toolType)
+ => _tools.Find(t => t.GetType() == toolType)?.IsActive is true;
+
+ public abstract bool IsOnScreen(Point topLeft);
+
+ ///
+ /// Returns true if an instance of T exists
+ ///
+ /// Type of tool to check
+ public bool Has() where T : IToolForm
+ => _tools.Exists(static t => t is T && t.IsActive);
+
+ /// iff a tool of the given is active
+ public bool Has(Type toolType)
+ => typeof(IToolForm).IsAssignableFrom(toolType)
+ && _tools.Exists(t => toolType.IsInstanceOfType(t) && t.IsActive);
+
+ ///
+ /// Gets the instance of T, or creates and returns a new instance
+ ///
+ /// Type of tool to get
+ public IToolForm Get() where T : class, IToolForm
+ {
+ return Load(false);
+ }
+
+ ///
+ /// returns the instance of , regardless of whether it's loaded,
+ /// but doesn't create and load a new instance if it's not found
+ ///
+ ///
+ /// does not check is a class implementing ;
+ /// you may pass any class or interface
+ ///
+ public IToolForm/*?*/ LazyGet(Type toolType)
+ => _tools.Find(t => toolType.IsAssignableFrom(t.GetType()));
+
+ private static PropertyInfo/*?*/ _PInfo_FormBase_WindowTitleStatic = null;
+
+ protected abstract bool CaptureIconAndName(object tool, Type toolType, ref Image/*?*/ icon, ref string/*?*/ name);
+
+ private void CaptureIconAndName(object tool, Type toolType)
+ {
+ Image/*?*/ icon = null;
+ string/*?*/ name = null;
+ CaptureIconAndName(tool, toolType, ref icon, ref name);
+ }
+
+ public abstract (Image/*?*/ Icon, string Name) GetIconAndNameFor(Type toolType);
+
+ public abstract IEnumerable AvailableTools { get; }
+
+ ///
+ /// Calls UpdateValues() on an instance of T, if it exists
+ ///
+ /// Type of tool to update
+ public void UpdateValues() where T : IToolForm
+ {
+ var tool = _tools.OfType().FirstOrDefault();
+ if (tool?.IsActive is true)
+ {
+ tool.UpdateValues(ToolFormUpdateType.General);
+ }
+ }
+
+ protected abstract void MaybeClearCheats();
+
+ public void Restart(Config config, IEmulator emulator, IGameInfo game)
+ {
+ _config = config;
+ _emulator = emulator;
+ _game = game;
+ ApiProvider = ApiManager.Restart(_emulator.ServiceProvider, _mainFormApi, _displayManager, _inputManager, _movieSession, this, _config, _emulator, _game);
+
+ MaybeClearCheats();
+
+ var unavailable = new List();
+
+ foreach (var tool in _tools)
+ {
+ SetBaseProperties(tool);
+ if (ServiceInjector.UpdateServices(_emulator.ServiceProvider, tool)
+ && (tool is not IExternalToolForm || ApiInjector.UpdateApis(ApiProvider, tool)))
+ {
+ if (tool.IsActive) tool.Restart();
+ }
+ else
+ {
+ unavailable.Add(tool);
+ if (tool is IExternalToolForm) ApiInjector.ClearApis(tool);
+ }
+ }
+
+ foreach (var tool in unavailable)
+ {
+ tool.Close();
+ _tools.Remove(tool);
+ }
+ }
+
+ ///
+ /// Calls Restart() on an instance of T, if it exists
+ ///
+ /// Type of tool to restart
+ public void Restart() where T : IToolForm
+ => _tools.OfType().FirstOrDefault()?.Restart();
+
+ ///
+ /// Runs AskSave on every tool dialog, false is returned if any tool returns false
+ ///
+ public bool AskSave()
+ {
+ if (_config.SuppressAskSave) // User has elected to not be nagged
+ {
+ return true;
+ }
+
+ return _tools
+ .Select(tool => tool.AskSaveChanges())
+ .All(result => result);
+ }
+
+ ///
+ /// If T exists, this call will close the tool, and remove it from memory
+ ///
+ /// Type of tool to close
+ public void Close() where T : IToolForm
+ {
+ var tool = _tools.OfType().FirstOrDefault();
+ if (tool != null)
+ {
+ tool.Close();
+ _tools.Remove(tool);
+ }
+ }
+
+ public void Close(Type toolType)
+ {
+ var tool = _tools.Find(toolType.IsInstanceOfType);
+ if (tool != null)
+ {
+ tool.Close();
+ _tools.Remove(tool);
+ }
+ }
+
+ public void Close()
+ {
+ _tools.ForEach(t => t.Close());
+ _tools.Clear();
+ }
+
+ ///
+ /// Create a new instance of an IToolForm and return it
+ ///
+ /// Type of tool you want to create
+ /// Path .dll for an external tool
+ /// New instance of an IToolForm
+ private IToolForm CreateInstance(string dllPath)
+ where T : IToolForm
+ {
+ return CreateInstance(typeof(T), dllPath);
+ }
+
+ protected abstract IExternalToolForm CreateInstanceFrom(string dllPath, string toolTypeName);
+
+ ///
+ /// Create a new instance of an IToolForm and return it
+ ///
+ /// Type of tool you want to create
+ /// Path dll for an external tool
+ /// For external tools, of the entry form's type ( will be )
+ /// New instance of an IToolForm
+ private IToolForm CreateInstance(Type toolType, string dllPath, string toolTypeName = null, bool skipExtToolWarning = false)
+ {
+ IToolForm tool;
+
+ // Specific case for custom tools
+ // TODO: Use AppDomain in order to be able to unload the assembly
+ // Hard stuff as we need a proxy object that inherit from MarshalByRefObject.
+ if (toolType == typeof(IExternalToolForm))
+ {
+ if (!skipExtToolWarning)
+ {
+ if (!_mainFormTools.ShowMessageBox2(
+ "Are you sure want to load this external tool?\r\nAccept ONLY if you trust the source and if you know what you're doing. In any other case, choose no.",
+ "Confirm loading",
+ EMsgBoxIcon.Question))
+ {
+ return null;
+ }
+ }
+
+ try
+ {
+ //tool = Activator.CreateInstanceFrom(dllPath, toolTypeName ?? "BizHawk.Client.EmuHawk.CustomMainForm").Unwrap() as IExternalToolForm;
+ tool = CreateInstanceFrom(dllPath, toolTypeName);
+ if (tool == null)
+ {
+ _mainFormTools.ShowMessageBox($"It seems that the object CustomMainForm does not implement {nameof(IExternalToolForm)}. Please review the code.", "No, no, no. Wrong Way !", EMsgBoxIcon.Warning);
+ return null;
+ }
+ }
+ catch (MissingMethodException)
+ {
+ _mainFormTools.ShowMessageBox("It seems that the object CustomMainForm does not have a public default constructor. Please review the code.", "No, no, no. Wrong Way !", EMsgBoxIcon.Warning);
+ return null;
+ }
+ catch (TypeLoadException)
+ {
+ _mainFormTools.ShowMessageBox("It seems that the object CustomMainForm does not exists. Please review the code.", "No, no, no. Wrong Way !", EMsgBoxIcon.Warning);
+ return null;
+ }
+ }
+ else
+ {
+ tool = (IToolForm)Activator.CreateInstance(toolType);
+ }
+ CaptureIconAndName(tool, toolType);
+ // Add to our list of tools
+ _tools.Add(tool);
+ return tool;
+ }
+
+ public void UpdateToolsBefore()
+ {
+ foreach (var tool in _tools)
+ {
+ if (tool.IsActive)
+ {
+ tool.UpdateValues(ToolFormUpdateType.PreFrame);
+ }
+ }
+ }
+
+ public void UpdateToolsAfter()
+ {
+ foreach (var tool in _tools)
+ {
+ if (tool.IsActive)
+ {
+ tool.UpdateValues(ToolFormUpdateType.PostFrame);
+ }
+ }
+ }
+
+ public void FastUpdateBefore()
+ {
+ foreach (var tool in _tools)
+ {
+ if (tool.IsActive)
+ {
+ tool.UpdateValues(ToolFormUpdateType.FastPreFrame);
+ }
+ }
+ }
+
+ public void FastUpdateAfter()
+ {
+ foreach (var tool in _tools)
+ {
+ if (tool.IsActive)
+ {
+ tool.UpdateValues(ToolFormUpdateType.FastPostFrame);
+ }
+ }
+ }
+
+ protected abstract IList PossibleToolTypeNames { get; }
+
+ public bool IsAvailable(Type tool)
+ {
+ if (!ServiceInjector.IsAvailable(_emulator.ServiceProvider, tool)) return false;
+ if (typeof(IExternalToolForm).IsAssignableFrom(tool) && !ApiInjector.IsAvailable(ApiProvider, tool)) return false;
+ if (!PossibleToolTypeNames.Contains(tool.AssemblyQualifiedName) && !_extToolManager.PossibleExtToolTypeNames.Contains(tool.AssemblyQualifiedName)) return false; // not a tool
+
+ ToolAttribute attr = tool.GetCustomAttributes(false).OfType().SingleOrDefault();
+ if (attr == null)
+ {
+ return true; // no ToolAttribute on given type -> assumed all supported
+ }
+
+ return !attr.UnsupportedCores.Contains(_emulator.Attributes().CoreName) // not unsupported
+ && (!attr.SupportedSystems.Any() || attr.SupportedSystems.Contains(_emulator.SystemId)); // supported (no supported list -> assumed all supported)
+ }
+
+ public bool IsAvailable() => IsAvailable(typeof(T));
+
+ // Note: Referencing these properties creates an instance of the tool and persists it. They should be referenced by type if this is not desired
+
+ protected T GetTool() where T : class, IToolForm, new()
+ {
+ T tool = _tools.OfType().FirstOrDefault();
+ if (tool != null)
+ {
+ if (tool.IsActive)
+ {
+ return tool;
+ }
+
+ _tools.Remove(tool);
+ }
+ tool = new T();
+ CaptureIconAndName(tool, typeof(T));
+ _tools.Add(tool);
+ return tool;
+ }
+
+ public abstract void LoadRamWatch(bool loadDialog);
+
+ public string GenerateDefaultCheatFilename()
+ {
+ var path = _config.PathEntries.CheatsAbsolutePath(_game.System);
+
+ var f = new FileInfo(path);
+ if (f.Directory != null && f.Directory.Exists == false)
+ {
+ f.Directory.Create();
+ }
+
+ return Path.Combine(path, $"{_game.FilesystemSafeName()}.cht");
+ }
+
+ public abstract void UpdateCheatRelatedTools(object sender, CheatCollection.CheatListEventArgs e);
+ }
+}
diff --git a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs
index 980a5b81b2..7b36715bad 100644
--- a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Reflection;
using System.ComponentModel;
@@ -10,36 +9,21 @@ using System.Windows.Forms;
using BizHawk.Client.Common;
using BizHawk.Common;
-using BizHawk.Common.CollectionExtensions;
using BizHawk.Common.ReflectionExtensions;
using BizHawk.Emulation.Common;
using BizHawk.WinForms.Controls;
namespace BizHawk.Client.EmuHawk
{
- public class ToolManager : IToolManager
+ public class ToolManager : ToolManagerBase, IToolManager
{
- private readonly MainForm _owner;
- private Config _config;
- private readonly DisplayManagerBase _displayManager;
- private readonly ExternalToolManager _extToolManager;
- private readonly InputManager _inputManager;
- private IExternalApiProvider _apiProvider;
- private IEmulator _emulator;
- private readonly IMovieSession _movieSession;
- private IGameInfo _game;
+ private readonly MainForm _ownerForm;
// TODO: merge ToolHelper code where logical
// For instance, add an IToolForm property called UsesCheats, so that a UpdateCheatRelatedTools() method can update all tools of this type
// Also a UsesRam, and similar method
private readonly List _tools = new List();
- private IExternalApiProvider ApiProvider
- {
- get => _apiProvider;
- set => _owner.EmuClient = (EmuClientApi) (_apiProvider = value).GetApi();
- }
-
///
/// Initializes a new instance of the class.
///
@@ -51,168 +35,19 @@ namespace BizHawk.Client.EmuHawk
InputManager inputManager,
IEmulator emulator,
IMovieSession movieSession,
- IGameInfo game)
+ IGameInfo game) : base(owner, owner, config, displayManager, extToolManager, inputManager, emulator, movieSession, game)
{
-
-
- _owner = owner;
- _config = config;
- _displayManager = displayManager;
- _extToolManager = extToolManager;
- _inputManager = inputManager;
- _emulator = emulator;
- _movieSession = movieSession;
- _game = game;
- ApiProvider = ApiManager.Restart(_emulator.ServiceProvider, _owner, _displayManager, _inputManager, _movieSession, this, _config, _emulator, _game);
- }
-
- ///
- /// Loads the tool dialog T (T must implements ) , if it does not exist it will be created, if it is already open, it will be focused
- /// This method should be used only if you can't use the generic one
- ///
- /// Type of tool you want to load
- /// Define if the tool form has to get the focus or not (Default is true)
- /// An instantiated
- /// Raised if can't cast into IToolForm
- public IToolForm Load(Type toolType, bool focus = true)
- {
- if (!typeof(IToolForm).IsAssignableFrom(toolType))
- {
- throw new ArgumentException(message: $"Type {toolType.Name} does not implement {nameof(IToolForm)}.", paramName: nameof(toolType));
- }
- var mi = typeof(ToolManager).GetMethod(nameof(Load), new[] { typeof(bool), typeof(string) })!.MakeGenericMethod(toolType);
- return (IToolForm) mi.Invoke(this, new object[] { focus, "" });
+ _ownerForm = owner;
}
// If the form inherits ToolFormBase, it will set base properties such as Tools, Config, etc
- private void SetBaseProperties(IToolForm form)
+ protected override void SetBaseProperties(IToolForm form)
{
if (form is not FormBase f) return;
f.Config = _config;
if (form is not ToolFormBase tool) return;
- tool.SetToolFormBaseProps(_displayManager, _inputManager, _owner, _movieSession, this, _game);
- }
-
- ///
- /// Loads the tool dialog T (T must implement ) , if it does not exist it will be created, if it is already open, it will be focused
- ///
- /// Define if the tool form has to get the focus or not (Default is true)
- /// Path to the .dll of the external tool
- /// Type of tool you want to load
- /// An instantiated
- public T Load(bool focus = true, string toolPath = "")
- where T : class, IToolForm
- {
- if (!IsAvailable()) return null;
-
- var existingTool = _tools.OfType().FirstOrDefault();
- if (existingTool != null)
- {
- if (existingTool.IsLoaded)
- {
- if (focus)
- {
- existingTool.Show();
- existingTool.Focus();
- }
-
- return existingTool;
- }
-
- _tools.Remove(existingTool);
- }
-
- if (CreateInstance(toolPath) is not T newTool) return null;
-
- if (newTool is Form form) form.Owner = _owner;
- if (!ServiceInjector.UpdateServices(_emulator.ServiceProvider, newTool)) return null; //TODO pass `true` for `mayCache` when from EmuHawk assembly
- SetBaseProperties(newTool);
- var toolTypeName = typeof(T).FullName!;
- // auto settings
- if (newTool is IToolFormAutoConfig autoConfigTool)
- {
- AttachSettingHooks(autoConfigTool, _config.CommonToolSettings.GetValueOrPutNew(toolTypeName));
- }
- // custom settings
- if (HasCustomConfig(newTool))
- {
- InstallCustomConfig(newTool, _config.CustomToolSettings.GetValueOrPutNew(toolTypeName));
- }
-
- newTool.Restart();
- newTool.Show();
- return newTool;
- }
-
- /// Loads the external tool's entry form.
- public IExternalToolForm LoadExternalToolForm(string toolPath, string customFormTypeName, bool focus = true, bool skipExtToolWarning = false)
- {
- var existingTool = _tools.OfType().FirstOrDefault(t => t.GetType().Assembly.Location == toolPath);
- if (existingTool != null)
- {
- if (existingTool.IsActive)
- {
- if (focus)
- {
- existingTool.Show();
- existingTool.Focus();
- }
- return existingTool;
- }
-
- _tools.Remove(existingTool);
- }
-
- var newTool = (IExternalToolForm) CreateInstance(typeof(IExternalToolForm), toolPath, customFormTypeName, skipExtToolWarning: skipExtToolWarning);
- if (newTool == null) return null;
- if (newTool is Form form) form.Owner = _owner;
- if (!(ServiceInjector.UpdateServices(_emulator.ServiceProvider, newTool) && ApiInjector.UpdateApis(ApiProvider, newTool))) return null;
- SetBaseProperties(newTool);
- // auto settings
- if (newTool is IToolFormAutoConfig autoConfigTool)
- {
- AttachSettingHooks(autoConfigTool, _config.CommonToolSettings.GetValueOrPutNew(customFormTypeName));
- }
- // custom settings
- if (HasCustomConfig(newTool))
- {
- InstallCustomConfig(newTool, _config.CustomToolSettings.GetValueOrPutNew(customFormTypeName));
- }
-
- newTool.Restart();
- newTool.Show();
- return newTool;
- }
-
- public void AutoLoad()
- {
- var genericSettings = _config.CommonToolSettings
- .Where(kvp => kvp.Value.AutoLoad)
- .Select(kvp => kvp.Key);
-
- var customSettings = _config.CustomToolSettings
- .Where(list => list.Value.Any(kvp => kvp.Value is ToolDialogSettings settings && settings.AutoLoad))
- .Select(kvp => kvp.Key);
-
- var typeNames = genericSettings.Concat(customSettings);
-
- foreach (var typename in typeNames)
- {
- // this type resolution might not be sufficient. more investigation is needed
- Type t = Type.GetType(typename);
- if (t == null)
- {
- Console.WriteLine("BENIGN: Couldn't find type {0}", typename);
- }
- else
- {
- if (!IsLoaded(t))
- {
- Load(t, false);
- }
- }
- }
+ tool.SetToolFormBaseProps(_displayManager, _inputManager, _mainFormTools, _movieSession, this, _game);
}
private void RefreshSettings(Form form, ToolStripItemCollection menu, ToolDialogSettings settings, int idx)
@@ -234,7 +69,7 @@ namespace BizHawk.Client.EmuHawk
((ToolStripMenuItem)menu[idx + 3]).Checked = settings.AutoLoad;
// do we need to do this OnShown() as well?
- form.Owner = settings.FloatingWindow ? null : _owner;
+ form.Owner = settings.FloatingWindow ? null : _ownerForm;
}
private void AddCloseButton(ToolStripMenuItem subMenu, Form form)
@@ -255,7 +90,7 @@ namespace BizHawk.Client.EmuHawk
subMenu.DropDownItems.Add(closeMenuItem);
}
- private void AttachSettingHooks(IToolFormAutoConfig tool, ToolDialogSettings settings)
+ protected override void AttachSettingHooks(IToolFormAutoConfig tool, ToolDialogSettings settings)
{
var form = (Form)tool;
ToolStripItemCollection dest = null;
@@ -356,7 +191,7 @@ namespace BizHawk.Client.EmuHawk
bool val = !((ToolStripMenuItem)o).Checked;
settings.FloatingWindow = val;
((ToolStripMenuItem)o).Checked = val;
- form.Owner = val ? null : _owner;
+ form.Owner = val ? null : _ownerForm;
};
dest[idx + 3].Click += (o, e) =>
{
@@ -426,55 +261,12 @@ namespace BizHawk.Client.EmuHawk
}
}
- ///
- /// Determines whether a given IToolForm is already loaded
- ///
- /// Type of tool to check
- /// yo why do we have 4 versions of this, each with slightly different behaviour in edge cases --yoshi
- public bool IsLoaded() where T : IToolForm
- => _tools.OfType().FirstOrDefault()?.IsActive is true;
-
- public bool IsLoaded(Type toolType)
- => _tools.Find(t => t.GetType() == toolType)?.IsActive is true;
-
- public bool IsOnScreen(Point topLeft)
+ public override bool IsOnScreen(Point topLeft)
{
return Screen.AllScreens.Any(
screen => screen.WorkingArea.Contains(topLeft));
}
- ///
- /// Returns true if an instance of T exists
- ///
- /// Type of tool to check
- public bool Has() where T : IToolForm
- => _tools.Exists(static t => t is T && t.IsActive);
-
- /// iff a tool of the given is active
- public bool Has(Type toolType)
- => typeof(IToolForm).IsAssignableFrom(toolType)
- && _tools.Exists(t => toolType.IsInstanceOfType(t) && t.IsActive);
-
- ///
- /// Gets the instance of T, or creates and returns a new instance
- ///
- /// Type of tool to get
- public IToolForm Get() where T : class, IToolForm
- {
- return Load(false);
- }
-
- ///
- /// returns the instance of , regardless of whether it's loaded,
- /// but doesn't create and load a new instance if it's not found
- ///
- ///
- /// does not check is a class implementing ;
- /// you may pass any class or interface
- ///
- public IToolForm/*?*/ LazyGet(Type toolType)
- => _tools.Find(t => toolType.IsAssignableFrom(t.GetType()));
-
internal static readonly IDictionary IconAndNameCache = new Dictionary
{
[typeof(LogWindow)] = (LogWindow.ToolIcon.ToBitmap(), "Log Window"), // can't do this lazily, see https://github.com/TASEmulators/BizHawk/issues/2741#issuecomment-1421014589
@@ -485,7 +277,7 @@ namespace BizHawk.Client.EmuHawk
private static PropertyInfo PInfo_FormBase_WindowTitleStatic
=> _PInfo_FormBase_WindowTitleStatic ??= typeof(FormBase).GetProperty("WindowTitleStatic", BindingFlags.NonPublic | BindingFlags.Instance);
- private static bool CaptureIconAndName(object tool, Type toolType, ref Image/*?*/ icon, ref string/*?*/ name)
+ protected override bool CaptureIconAndName(object tool, Type toolType, ref Image/*?*/ icon, ref string/*?*/ name)
{
if (IconAndNameCache.ContainsKey(toolType)) return true;
Form winform = null;
@@ -515,14 +307,14 @@ namespace BizHawk.Client.EmuHawk
return false;
}
- private static void CaptureIconAndName(object tool, Type toolType)
+ private void CaptureIconAndName(object tool, Type toolType)
{
Image/*?*/ icon = null;
string/*?*/ name = null;
CaptureIconAndName(tool, toolType, ref icon, ref name);
}
- public (Image/*?*/ Icon, string Name) GetIconAndNameFor(Type toolType)
+ public override (Image/*?*/ Icon, string Name) GetIconAndNameFor(Type toolType)
{
if (IconAndNameCache.TryGetValue(toolType, out var tuple)) return tuple;
Image/*?*/ icon = null;
@@ -538,262 +330,23 @@ namespace BizHawk.Client.EmuHawk
string.IsNullOrWhiteSpace(name) ? toolType.Name : name);
}
- public IEnumerable AvailableTools => EmuHawk.ReflectionCache.Types
+ public override IEnumerable AvailableTools => EmuHawk.ReflectionCache.Types
.Where(t => !t.IsInterface && typeof(IToolForm).IsAssignableFrom(t) && IsAvailable(t));
- ///
- /// Calls UpdateValues() on an instance of T, if it exists
- ///
- /// Type of tool to update
- public void UpdateValues() where T : IToolForm
+ protected override void MaybeClearCheats()
{
- var tool = _tools.OfType().FirstOrDefault();
- if (tool?.IsActive is true)
- {
- tool.UpdateValues(ToolFormUpdateType.General);
- }
- }
-
- public void Restart(Config config, IEmulator emulator, IGameInfo game)
- {
- _config = config;
- _emulator = emulator;
- _game = game;
- ApiProvider = ApiManager.Restart(_emulator.ServiceProvider, _owner, _displayManager, _inputManager, _movieSession, this, _config, _emulator, _game);
- // If Cheat tool is loaded, restarting will restart the list too anyway
if (!Has())
{
- _owner.CheatList.NewList(GenerateDefaultCheatFilename(), autosave: true);
- }
-
- var unavailable = new List();
-
- foreach (var tool in _tools)
- {
- SetBaseProperties(tool);
- if (ServiceInjector.UpdateServices(_emulator.ServiceProvider, tool)
- && (tool is not IExternalToolForm || ApiInjector.UpdateApis(ApiProvider, tool)))
- {
- if (tool.IsActive) tool.Restart();
- }
- else
- {
- unavailable.Add(tool);
- if (tool is IExternalToolForm) ApiInjector.ClearApis(tool);
- }
- }
-
- foreach (var tool in unavailable)
- {
- tool.Close();
- _tools.Remove(tool);
+ _mainFormTools.CheatList.NewList(GenerateDefaultCheatFilename(), autosave: true);
}
}
- ///
- /// Calls Restart() on an instance of T, if it exists
- ///
- /// Type of tool to restart
- public void Restart() where T : IToolForm
- => _tools.OfType().FirstOrDefault()?.Restart();
-
- ///
- /// Runs AskSave on every tool dialog, false is returned if any tool returns false
- ///
- public bool AskSave()
+ protected override IExternalToolForm CreateInstanceFrom(string dllPath, string toolTypeName)
{
- if (_config.SuppressAskSave) // User has elected to not be nagged
- {
- return true;
- }
-
- return _tools
- .Select(tool => tool.AskSaveChanges())
- .All(result => result);
+ return Activator.CreateInstanceFrom(dllPath, toolTypeName ?? "BizHawk.Client.EmuHawk.CustomMainForm").Unwrap() as IExternalToolForm;
}
- ///
- /// If T exists, this call will close the tool, and remove it from memory
- ///
- /// Type of tool to close
- public void Close() where T : IToolForm
- {
- var tool = _tools.OfType().FirstOrDefault();
- if (tool != null)
- {
- tool.Close();
- _tools.Remove(tool);
- }
- }
-
- public void Close(Type toolType)
- {
- var tool = _tools.Find(toolType.IsInstanceOfType);
- if (tool != null)
- {
- tool.Close();
- _tools.Remove(tool);
- }
- }
-
- public void Close()
- {
- _tools.ForEach(t => t.Close());
- _tools.Clear();
- }
-
- ///
- /// Create a new instance of an IToolForm and return it
- ///
- /// Type of tool you want to create
- /// Path .dll for an external tool
- /// New instance of an IToolForm
- private IToolForm CreateInstance(string dllPath)
- where T : IToolForm
- {
- return CreateInstance(typeof(T), dllPath);
- }
-
- ///
- /// Create a new instance of an IToolForm and return it
- ///
- /// Type of tool you want to create
- /// Path dll for an external tool
- /// For external tools, of the entry form's type ( will be )
- /// New instance of an IToolForm
- private IToolForm CreateInstance(Type toolType, string dllPath, string toolTypeName = null, bool skipExtToolWarning = false)
- {
- IToolForm tool;
-
- // Specific case for custom tools
- // TODO: Use AppDomain in order to be able to unload the assembly
- // Hard stuff as we need a proxy object that inherit from MarshalByRefObject.
- if (toolType == typeof(IExternalToolForm))
- {
- if (!skipExtToolWarning)
- {
- if (!_owner.ShowMessageBox2(
- "Are you sure want to load this external tool?\r\nAccept ONLY if you trust the source and if you know what you're doing. In any other case, choose no.",
- "Confirm loading",
- EMsgBoxIcon.Question))
- {
- return null;
- }
- }
-
- try
- {
- tool = Activator.CreateInstanceFrom(dllPath, toolTypeName ?? "BizHawk.Client.EmuHawk.CustomMainForm").Unwrap() as IExternalToolForm;
- if (tool == null)
- {
- _owner.ShowMessageBox($"It seems that the object CustomMainForm does not implement {nameof(IExternalToolForm)}. Please review the code.", "No, no, no. Wrong Way !", EMsgBoxIcon.Warning);
- return null;
- }
- }
- catch (MissingMethodException)
- {
- _owner.ShowMessageBox("It seems that the object CustomMainForm does not have a public default constructor. Please review the code.", "No, no, no. Wrong Way !", EMsgBoxIcon.Warning);
- return null;
- }
- catch (TypeLoadException)
- {
- _owner.ShowMessageBox("It seems that the object CustomMainForm does not exists. Please review the code.", "No, no, no. Wrong Way !", EMsgBoxIcon.Warning);
- return null;
- }
- }
- else
- {
- tool = (IToolForm)Activator.CreateInstance(toolType);
- }
- CaptureIconAndName(tool, toolType);
- // Add to our list of tools
- _tools.Add(tool);
- return tool;
- }
-
- public void UpdateToolsBefore()
- {
- foreach (var tool in _tools)
- {
- if (tool.IsActive)
- {
- tool.UpdateValues(ToolFormUpdateType.PreFrame);
- }
- }
- }
-
- public void UpdateToolsAfter()
- {
- foreach (var tool in _tools)
- {
- if (tool.IsActive)
- {
- tool.UpdateValues(ToolFormUpdateType.PostFrame);
- }
- }
- }
-
- public void FastUpdateBefore()
- {
- foreach (var tool in _tools)
- {
- if (tool.IsActive)
- {
- tool.UpdateValues(ToolFormUpdateType.FastPreFrame);
- }
- }
- }
-
- public void FastUpdateAfter()
- {
- foreach (var tool in _tools)
- {
- if (tool.IsActive)
- {
- tool.UpdateValues(ToolFormUpdateType.FastPostFrame);
- }
- }
- }
-
- private static readonly IList PossibleToolTypeNames = EmuHawk.ReflectionCache.Types.Select(t => t.AssemblyQualifiedName).ToList();
-
- public bool IsAvailable(Type tool)
- {
- if (!ServiceInjector.IsAvailable(_emulator.ServiceProvider, tool)) return false;
- if (typeof(IExternalToolForm).IsAssignableFrom(tool) && !ApiInjector.IsAvailable(ApiProvider, tool)) return false;
- if (!PossibleToolTypeNames.Contains(tool.AssemblyQualifiedName) && !_extToolManager.PossibleExtToolTypeNames.Contains(tool.AssemblyQualifiedName)) return false; // not a tool
-
- ToolAttribute attr = tool.GetCustomAttributes(false).OfType().SingleOrDefault();
- if (attr == null)
- {
- return true; // no ToolAttribute on given type -> assumed all supported
- }
-
- return !attr.UnsupportedCores.Contains(_emulator.Attributes().CoreName) // not unsupported
- && (!attr.SupportedSystems.Any() || attr.SupportedSystems.Contains(_emulator.SystemId)); // supported (no supported list -> assumed all supported)
- }
-
- public bool IsAvailable() => IsAvailable(typeof(T));
-
- // Note: Referencing these properties creates an instance of the tool and persists it. They should be referenced by type if this is not desired
-
- private T GetTool() where T : class, IToolForm, new()
- {
- T tool = _tools.OfType().FirstOrDefault();
- if (tool != null)
- {
- if (tool.IsActive)
- {
- return tool;
- }
-
- _tools.Remove(tool);
- }
- tool = new T();
- CaptureIconAndName(tool, typeof(T));
- _tools.Add(tool);
- return tool;
- }
+ protected override IList PossibleToolTypeNames { get; } = EmuHawk.ReflectionCache.Types.Select(t => t.AssemblyQualifiedName).ToList();
public RamWatch RamWatch => GetTool();
@@ -809,7 +362,7 @@ namespace BizHawk.Client.EmuHawk
public TAStudio TAStudio => GetTool();
- public void LoadRamWatch(bool loadDialog)
+ public override void LoadRamWatch(bool loadDialog)
{
if (IsLoaded() && !_config.DisplayRamWatch)
{
@@ -832,20 +385,7 @@ namespace BizHawk.Client.EmuHawk
}
}
- public string GenerateDefaultCheatFilename()
- {
- var path = _config.PathEntries.CheatsAbsolutePath(_game.System);
-
- var f = new FileInfo(path);
- if (f.Directory != null && f.Directory.Exists == false)
- {
- f.Directory.Create();
- }
-
- return Path.Combine(path, $"{_game.FilesystemSafeName()}.cht");
- }
-
- public void UpdateCheatRelatedTools(object sender, CheatCollection.CheatListEventArgs e)
+ public override void UpdateCheatRelatedTools(object sender, CheatCollection.CheatListEventArgs e)
{
if (!_emulator.HasMemoryDomains())
{
@@ -857,7 +397,17 @@ namespace BizHawk.Client.EmuHawk
UpdateValues();
UpdateValues();
- _owner.UpdateCheatStatus();
+ _ownerForm.UpdateCheatStatus();
+ }
+
+ protected override void SetFormParent(IToolForm form)
+ {
+ if (form is Form formform) formform.Owner = _ownerForm;
+ }
+
+ protected override void SetFormClosingEvent(IToolForm form, Action action)
+ {
+ ((Form)form).FormClosing += (o, e) => action();
}
}
}
diff --git a/src/BizHawk.Tests/Client.Common/Api/ExternalToolTests.cs b/src/BizHawk.Tests/Client.Common/Api/ExternalToolTests.cs
index 0efc11acf1..a160549cf1 100644
--- a/src/BizHawk.Tests/Client.Common/Api/ExternalToolTests.cs
+++ b/src/BizHawk.Tests/Client.Common/Api/ExternalToolTests.cs
@@ -2,7 +2,9 @@
using System.Linq;
using System.Reflection;
using BizHawk.Client.Common;
-
+using BizHawk.Emulation.Common;
+using BizHawk.Tests.Implementations;
+using BizHawk.Tests.Mocks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace BizHawk.Tests.Client.Common.Api
@@ -10,26 +12,47 @@ namespace BizHawk.Tests.Client.Common.Api
[TestClass]
public class ExternalToolTests
{
+ private Config config = new();
+
[ClassInitialize]
public static void TestInitialize(TestContext context)
{
// Move our .dll to a directory by itself, so that the ExternalToolManager will only find us.
string asmName = Assembly.GetExecutingAssembly().Location;
Directory.CreateDirectory("extTools");
- File.Copy(asmName, "extTools/ExternalToolTests.dll");
+ File.Copy(asmName, "extTools/ExternalToolTests.dll", true);
+ }
+
+ [TestInitialize]
+ public void TestSetup()
+ {
+ config.PathEntries.Paths.Find(
+ static (e) => string.Equals(e.Type, "External Tools", System.StringComparison.Ordinal)
+ )!.Path = "./extTools";
}
[TestMethod]
public void TestExternalToolIsFound()
{
- Config config = new Config();
- config.PathEntries.Paths.Find((e) => string.Equals(e.Type, "External Tools", System.StringComparison.Ordinal))!.Path = "./extTools";
- string t = config.PathEntries[PathEntryCollection.GLOBAL, "External Tools"].Path;
ExternalToolManager manager = new ExternalToolManager(config, () => ("", ""), (p1, p2, p3) => true);
Assert.IsTrue(manager.ToolStripItems.Count != 0);
var item = manager.ToolStripItems.First(static (info) => info.Text == "TEST");
Assert.AreEqual("TEST", item.Text);
}
+
+ [TestMethod]
+ public void TestExternalToolIsCalled()
+ {
+ IMainFormForApi mainFormApi = new MockMainFormForApi(new NullEmulator());
+ DisplayManagerBase displayManager = new TestDisplayManager(mainFormApi.Emulator);
+ TestToolManager toolManager = new TestToolManager(mainFormApi, config, displayManager);
+
+ TestExternalAPI externalApi = toolManager.Load();
+ Assert.AreEqual(0, externalApi.frameCount);
+ toolManager.UpdateToolsBefore();
+ toolManager.UpdateToolsAfter();
+ Assert.AreEqual(1, externalApi.frameCount);
+ }
}
}
diff --git a/src/BizHawk.Tests/Implementations/TestExternalAPI.cs b/src/BizHawk.Tests/Implementations/TestExternalAPI.cs
index 41f43a3ba3..5f8dbd5279 100644
--- a/src/BizHawk.Tests/Implementations/TestExternalAPI.cs
+++ b/src/BizHawk.Tests/Implementations/TestExternalAPI.cs
@@ -6,10 +6,10 @@ namespace BizHawk.Tests.Implementations
public class TestExternalAPI : IExternalToolForm
{
public ApiContainer? _maybeAPIContainer { get; set; }
- private ApiContainer APIs
+ internal ApiContainer APIs
=> _maybeAPIContainer!;
- private int frameCount = 0;
+ internal int frameCount = 0;
public bool IsActive => true;
diff --git a/src/BizHawk.Tests/Implementations/TestToolManager.cs b/src/BizHawk.Tests/Implementations/TestToolManager.cs
new file mode 100644
index 0000000000..6de05ce549
--- /dev/null
+++ b/src/BizHawk.Tests/Implementations/TestToolManager.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Reflection;
+using BizHawk.Client.Common;
+using BizHawk.Tests.Mocks;
+
+namespace BizHawk.Tests.Implementations
+{
+ internal class TestToolManager : ToolManagerBase
+ {
+ public TestToolManager(IMainFormForApi mainFormApi, Config config, DisplayManagerBase displayManager)
+ : base(new MockMainFormForTools(),
+ mainFormApi,
+ config,
+ displayManager,
+ new ExternalToolManager(config, () => ("", ""), (p1, p2, p3) => false),
+ null,
+ mainFormApi.Emulator,
+ mainFormApi.MovieSession,
+ null)
+ { }
+
+
+ protected override IList PossibleToolTypeNames { get; } = ReflectionCache.Types
+ .Where(static (t) => typeof(IExternalApi).IsAssignableFrom(t))
+ .Select(static (t) => t.AssemblyQualifiedName!)
+ .ToList();
+ protected override bool CaptureIconAndName(object tool, Type toolType, ref Image? icon, ref string name)
+ {
+ ExternalToolAttribute? eta = toolType.GetCustomAttribute();
+ if (eta == null)
+ throw new NotImplementedException(); // not an external tool
+
+ icon = null;
+ name = eta.Name;
+ return true;
+ }
+
+ protected override void SetFormParent(IToolForm form) { }
+ protected override void SetBaseProperties(IToolForm form) { }
+
+ public override IEnumerable AvailableTools => throw new NotImplementedException();
+
+ public override (Image Icon, string Name) GetIconAndNameFor(Type toolType) => throw new NotImplementedException();
+ public override bool IsOnScreen(Point topLeft) => throw new NotImplementedException();
+ public override void LoadRamWatch(bool loadDialog) => throw new NotImplementedException();
+ public override void UpdateCheatRelatedTools(object sender, CheatCollection.CheatListEventArgs e) => throw new NotImplementedException();
+ protected override void AttachSettingHooks(IToolFormAutoConfig tool, ToolDialogSettings settings) => throw new NotImplementedException();
+
+ protected override IExternalToolForm CreateInstanceFrom(string dllPath, string toolTypeName) => throw new NotImplementedException();
+ protected override void MaybeClearCheats() => throw new NotImplementedException();
+ protected override void SetFormClosingEvent(IToolForm form, Action action) => throw new NotImplementedException();
+ }
+}
diff --git a/src/BizHawk.Tests/Mocks/MockMainFormForTools.cs b/src/BizHawk.Tests/Mocks/MockMainFormForTools.cs
new file mode 100644
index 0000000000..dafac87937
--- /dev/null
+++ b/src/BizHawk.Tests/Mocks/MockMainFormForTools.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+
+using BizHawk.Bizware.BizwareGL;
+using BizHawk.Client.Common;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Tests.Mocks
+{
+ internal class MockMainFormForTools : IMainFormForTools
+ {
+ public EmuClientApi? EmuClient { get ; set; }
+
+ public CheatCollection CheatList => throw new NotImplementedException();
+
+ public string CurrentlyOpenRom => throw new NotImplementedException();
+
+ public LoadRomArgs CurrentlyOpenRomArgs => throw new NotImplementedException();
+
+ public bool EmulatorPaused => throw new NotImplementedException();
+
+ public FirmwareManager FirmwareManager => throw new NotImplementedException();
+
+ public bool GameIsClosing => throw new NotImplementedException();
+
+ public bool HoldFrameAdvance { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public bool InvisibleEmulation { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+ public bool IsSeeking => throw new NotImplementedException();
+
+ public bool IsTurboing => throw new NotImplementedException();
+
+ public int? PauseOnFrame { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public bool PressRewind { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public bool BlockFrameAdvance { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+ public event Action? OnPauseChanged;
+
+ public void AddOnScreenMessage(string message, int? duration = null) => throw new NotImplementedException();
+ public BitmapBuffer CaptureOSD() => throw new NotImplementedException();
+ public void DisableRewind() => throw new NotImplementedException();
+ public void EnableRewind(bool enabled) => throw new NotImplementedException();
+ public bool EnsureCoreIsAccurate() => throw new NotImplementedException();
+ public void FrameAdvance() => throw new NotImplementedException();
+ public void FrameBufferResized() => throw new NotImplementedException();
+ public bool LoadQuickSave(int slot, bool suppressOSD = false) => throw new NotImplementedException();
+ public bool LoadRom(string path, LoadRomArgs args) => throw new NotImplementedException();
+ public BitmapBuffer MakeScreenshotImage() => throw new NotImplementedException();
+ public void MaybePauseFromMenuOpened() => throw new NotImplementedException();
+ public void MaybeUnpauseFromMenuClosed() => throw new NotImplementedException();
+ public void PauseEmulator() => throw new NotImplementedException();
+ public void RelinquishControl(IControlMainform master) => throw new NotImplementedException();
+ public void SeekFrameAdvance() => throw new NotImplementedException();
+ public void SetMainformMovieInfo() => throw new NotImplementedException();
+ public IReadOnlyList? ShowFileMultiOpenDialog(IDialogParent dialogParent, string? filterStr, ref int filterIndex, string initDir, bool discardCWDChange = false, string? initFileName = null, bool maySelectMultiple = false, string? windowTitle = null) => throw new NotImplementedException();
+ public string? ShowFileSaveDialog(IDialogParent dialogParent, bool discardCWDChange, string? fileExt, string? filterStr, string initDir, string? initFileName, bool muteOverwriteWarning) => throw new NotImplementedException();
+ public void ShowMessageBox(IDialogParent? owner, string text, string? caption = null, EMsgBoxIcon? icon = null) => throw new NotImplementedException();
+ public bool ShowMessageBox2(IDialogParent? owner, string text, string? caption = null, EMsgBoxIcon? icon = null, bool useOKCancel = false) => throw new NotImplementedException();
+ public bool? ShowMessageBox3(IDialogParent? owner, string text, string? caption = null, EMsgBoxIcon? icon = null) => throw new NotImplementedException();
+ public bool StartNewMovie(IMovie movie, bool record) => throw new NotImplementedException();
+ public void StartSound() => throw new NotImplementedException();
+ public void StopSound() => throw new NotImplementedException();
+ public void TakeBackControl() => throw new NotImplementedException();
+ public void Throttle() => throw new NotImplementedException();
+ public void TogglePause() => throw new NotImplementedException();
+ public void UnpauseEmulator() => throw new NotImplementedException();
+ public void Unthrottle() => throw new NotImplementedException();
+ public void UpdateDumpInfo(RomStatus? newStatus = null) => throw new NotImplementedException();
+ public void UpdateStatusSlots() => throw new NotImplementedException();
+ public void UpdateWindowTitle() => throw new NotImplementedException();
+ }
+}