Move most of ToolManager's functionality to new ToolManagerBase, to support testing external tools
This commit is contained in:
parent
ae76497aa4
commit
74e210e80b
|
@ -109,5 +109,7 @@ namespace BizHawk.Client.Common
|
|||
|
||||
/// <remarks>only referenced from TAStudio</remarks>
|
||||
void UpdateWindowTitle();
|
||||
|
||||
public EmuClientApi EmuClient { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string>();
|
||||
UnsupportedCores = unsupportedCores ?? Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
public ToolAttribute(bool released, string[] supportedSystems) : this(released, supportedSystems, null) {}
|
||||
|
||||
public bool Released { get; }
|
||||
|
||||
public IEnumerable<string> SupportedSystems { get; }
|
||||
|
||||
public IEnumerable<string> 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<string>();
|
||||
UnsupportedCores = unsupportedCores ?? Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
public ToolAttribute(bool released, string[] supportedSystems) : this(released, supportedSystems, null) {}
|
||||
|
||||
public bool Released { get; }
|
||||
|
||||
public IEnumerable<string> SupportedSystems { get; }
|
||||
|
||||
public IEnumerable<string> UnsupportedCores { get; }
|
||||
}
|
||||
}
|
|
@ -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<IToolForm> _tools = new List<IToolForm>();
|
||||
|
||||
private IExternalApiProvider ApiProvider
|
||||
{
|
||||
get => _apiProvider;
|
||||
set => _mainFormTools.EmuClient = (EmuClientApi)(_apiProvider = value).GetApi<IEmuClientApi>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ToolManagerBase"/> class.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the tool dialog T (T must implements <see cref="IToolForm"/>) , 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
|
||||
/// </summary>
|
||||
/// <param name="toolType">Type of tool you want to load</param>
|
||||
/// <param name="focus">Define if the tool form has to get the focus or not (Default is true)</param>
|
||||
/// <returns>An instantiated <see cref="IToolForm"/></returns>
|
||||
/// <exception cref="ArgumentException">Raised if <paramref name="toolType"/> can't cast into IToolForm </exception>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the tool dialog T (T must implement <see cref="IToolForm"/>) , if it does not exist it will be created, if it is already open, it will be focused
|
||||
/// </summary>
|
||||
/// <param name="focus">Define if the tool form has to get the focus or not (Default is true)</param>
|
||||
/// <param name="toolPath">Path to the .dll of the external tool</param>
|
||||
/// <typeparam name="T">Type of tool you want to load</typeparam>
|
||||
/// <returns>An instantiated <see cref="IToolForm"/></returns>
|
||||
public T Load<T>(bool focus = true, string toolPath = "")
|
||||
where T : class, IToolForm
|
||||
{
|
||||
if (!IsAvailable<T>()) return null;
|
||||
|
||||
var existingTool = _tools.OfType<T>().FirstOrDefault();
|
||||
if (existingTool != null)
|
||||
{
|
||||
if (existingTool.IsLoaded)
|
||||
{
|
||||
if (focus)
|
||||
{
|
||||
existingTool.Show();
|
||||
existingTool.Focus();
|
||||
}
|
||||
|
||||
return existingTool;
|
||||
}
|
||||
|
||||
_tools.Remove(existingTool);
|
||||
}
|
||||
|
||||
if (CreateInstance<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>Loads the external tool's entry form.</summary>
|
||||
public IExternalToolForm LoadExternalToolForm(string toolPath, string customFormTypeName, bool focus = true, bool skipExtToolWarning = false)
|
||||
{
|
||||
var existingTool = _tools.OfType<IExternalToolForm>().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<string, object> 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<string, object> data, List<PropertyInfo> props)
|
||||
{
|
||||
data.Clear();
|
||||
foreach (var prop in props)
|
||||
{
|
||||
data.Add(prop.Name, prop.GetValue(tool, BindingFlags.GetProperty, Type.DefaultBinder, null, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a given IToolForm is already loaded
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to check</typeparam>
|
||||
/// <remarks>yo why do we have 4 versions of this, each with slightly different behaviour in edge cases --yoshi</remarks>
|
||||
public bool IsLoaded<T>() where T : IToolForm
|
||||
=> _tools.OfType<T>().FirstOrDefault()?.IsActive is true;
|
||||
|
||||
public bool IsLoaded(Type toolType)
|
||||
=> _tools.Find(t => t.GetType() == toolType)?.IsActive is true;
|
||||
|
||||
public abstract bool IsOnScreen(Point topLeft);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an instance of T exists
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to check</typeparam>
|
||||
public bool Has<T>() where T : IToolForm
|
||||
=> _tools.Exists(static t => t is T && t.IsActive);
|
||||
|
||||
/// <returns><see langword="true"/> iff a tool of the given <paramref name="toolType"/> is <see cref="IToolForm.IsActive">active</see></returns>
|
||||
public bool Has(Type toolType)
|
||||
=> typeof(IToolForm).IsAssignableFrom(toolType)
|
||||
&& _tools.Exists(t => toolType.IsInstanceOfType(t) && t.IsActive);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance of T, or creates and returns a new instance
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to get</typeparam>
|
||||
public IToolForm Get<T>() where T : class, IToolForm
|
||||
{
|
||||
return Load<T>(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns the instance of <paramref name="toolType"/>, regardless of whether it's loaded,<br/>
|
||||
/// but doesn't create and load a new instance if it's not found
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// does not check <paramref name="toolType"/> is a class implementing <see cref="IToolForm"/>;<br/>
|
||||
/// you may pass any class or interface
|
||||
/// </remarks>
|
||||
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<Type> AvailableTools { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Calls UpdateValues() on an instance of T, if it exists
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to update</typeparam>
|
||||
public void UpdateValues<T>() where T : IToolForm
|
||||
{
|
||||
var tool = _tools.OfType<T>().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<IToolForm>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls Restart() on an instance of T, if it exists
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to restart</typeparam>
|
||||
public void Restart<T>() where T : IToolForm
|
||||
=> _tools.OfType<T>().FirstOrDefault()?.Restart();
|
||||
|
||||
/// <summary>
|
||||
/// Runs AskSave on every tool dialog, false is returned if any tool returns false
|
||||
/// </summary>
|
||||
public bool AskSave()
|
||||
{
|
||||
if (_config.SuppressAskSave) // User has elected to not be nagged
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _tools
|
||||
.Select(tool => tool.AskSaveChanges())
|
||||
.All(result => result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If T exists, this call will close the tool, and remove it from memory
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to close</typeparam>
|
||||
public void Close<T>() where T : IToolForm
|
||||
{
|
||||
var tool = _tools.OfType<T>().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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of an IToolForm and return it
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool you want to create</typeparam>
|
||||
/// <param name="dllPath">Path .dll for an external tool</param>
|
||||
/// <returns>New instance of an IToolForm</returns>
|
||||
private IToolForm CreateInstance<T>(string dllPath)
|
||||
where T : IToolForm
|
||||
{
|
||||
return CreateInstance(typeof(T), dllPath);
|
||||
}
|
||||
|
||||
protected abstract IExternalToolForm CreateInstanceFrom(string dllPath, string toolTypeName);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of an IToolForm and return it
|
||||
/// </summary>
|
||||
/// <param name="toolType">Type of tool you want to create</param>
|
||||
/// <param name="dllPath">Path dll for an external tool</param>
|
||||
/// <param name="toolTypeName">For external tools, <see cref="Type.FullName"/> of the entry form's type (<paramref name="toolType"/> will be <see cref="IExternalToolForm"/>)</param>
|
||||
/// <returns>New instance of an IToolForm</returns>
|
||||
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<string> 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<ToolAttribute>().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<T>() => 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<T>() where T : class, IToolForm, new()
|
||||
{
|
||||
T tool = _tools.OfType<T>().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);
|
||||
}
|
||||
}
|
|
@ -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<IToolForm> _tools = new List<IToolForm>();
|
||||
|
||||
private IExternalApiProvider ApiProvider
|
||||
{
|
||||
get => _apiProvider;
|
||||
set => _owner.EmuClient = (EmuClientApi) (_apiProvider = value).GetApi<IEmuClientApi>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ToolManager"/> class.
|
||||
/// </summary>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the tool dialog T (T must implements <see cref="IToolForm"/>) , 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
|
||||
/// </summary>
|
||||
/// <param name="toolType">Type of tool you want to load</param>
|
||||
/// <param name="focus">Define if the tool form has to get the focus or not (Default is true)</param>
|
||||
/// <returns>An instantiated <see cref="IToolForm"/></returns>
|
||||
/// <exception cref="ArgumentException">Raised if <paramref name="toolType"/> can't cast into IToolForm </exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the tool dialog T (T must implement <see cref="IToolForm"/>) , if it does not exist it will be created, if it is already open, it will be focused
|
||||
/// </summary>
|
||||
/// <param name="focus">Define if the tool form has to get the focus or not (Default is true)</param>
|
||||
/// <param name="toolPath">Path to the .dll of the external tool</param>
|
||||
/// <typeparam name="T">Type of tool you want to load</typeparam>
|
||||
/// <returns>An instantiated <see cref="IToolForm"/></returns>
|
||||
public T Load<T>(bool focus = true, string toolPath = "")
|
||||
where T : class, IToolForm
|
||||
{
|
||||
if (!IsAvailable<T>()) return null;
|
||||
|
||||
var existingTool = _tools.OfType<T>().FirstOrDefault();
|
||||
if (existingTool != null)
|
||||
{
|
||||
if (existingTool.IsLoaded)
|
||||
{
|
||||
if (focus)
|
||||
{
|
||||
existingTool.Show();
|
||||
existingTool.Focus();
|
||||
}
|
||||
|
||||
return existingTool;
|
||||
}
|
||||
|
||||
_tools.Remove(existingTool);
|
||||
}
|
||||
|
||||
if (CreateInstance<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>Loads the external tool's entry form.</summary>
|
||||
public IExternalToolForm LoadExternalToolForm(string toolPath, string customFormTypeName, bool focus = true, bool skipExtToolWarning = false)
|
||||
{
|
||||
var existingTool = _tools.OfType<IExternalToolForm>().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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a given IToolForm is already loaded
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to check</typeparam>
|
||||
/// <remarks>yo why do we have 4 versions of this, each with slightly different behaviour in edge cases --yoshi</remarks>
|
||||
public bool IsLoaded<T>() where T : IToolForm
|
||||
=> _tools.OfType<T>().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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an instance of T exists
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to check</typeparam>
|
||||
public bool Has<T>() where T : IToolForm
|
||||
=> _tools.Exists(static t => t is T && t.IsActive);
|
||||
|
||||
/// <returns><see langword="true"/> iff a tool of the given <paramref name="toolType"/> is <see cref="IToolForm.IsActive">active</see></returns>
|
||||
public bool Has(Type toolType)
|
||||
=> typeof(IToolForm).IsAssignableFrom(toolType)
|
||||
&& _tools.Exists(t => toolType.IsInstanceOfType(t) && t.IsActive);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance of T, or creates and returns a new instance
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to get</typeparam>
|
||||
public IToolForm Get<T>() where T : class, IToolForm
|
||||
{
|
||||
return Load<T>(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns the instance of <paramref name="toolType"/>, regardless of whether it's loaded,<br/>
|
||||
/// but doesn't create and load a new instance if it's not found
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// does not check <paramref name="toolType"/> is a class implementing <see cref="IToolForm"/>;<br/>
|
||||
/// you may pass any class or interface
|
||||
/// </remarks>
|
||||
public IToolForm/*?*/ LazyGet(Type toolType)
|
||||
=> _tools.Find(t => toolType.IsAssignableFrom(t.GetType()));
|
||||
|
||||
internal static readonly IDictionary<Type, (Image/*?*/ Icon, string Name)> IconAndNameCache = new Dictionary<Type, (Image/*?*/ Icon, string Name)>
|
||||
{
|
||||
[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<Type> AvailableTools => EmuHawk.ReflectionCache.Types
|
||||
public override IEnumerable<Type> AvailableTools => EmuHawk.ReflectionCache.Types
|
||||
.Where(t => !t.IsInterface && typeof(IToolForm).IsAssignableFrom(t) && IsAvailable(t));
|
||||
|
||||
/// <summary>
|
||||
/// Calls UpdateValues() on an instance of T, if it exists
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to update</typeparam>
|
||||
public void UpdateValues<T>() where T : IToolForm
|
||||
protected override void MaybeClearCheats()
|
||||
{
|
||||
var tool = _tools.OfType<T>().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<Cheats>())
|
||||
{
|
||||
_owner.CheatList.NewList(GenerateDefaultCheatFilename(), autosave: true);
|
||||
}
|
||||
|
||||
var unavailable = new List<IToolForm>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls Restart() on an instance of T, if it exists
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to restart</typeparam>
|
||||
public void Restart<T>() where T : IToolForm
|
||||
=> _tools.OfType<T>().FirstOrDefault()?.Restart();
|
||||
|
||||
/// <summary>
|
||||
/// Runs AskSave on every tool dialog, false is returned if any tool returns false
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If T exists, this call will close the tool, and remove it from memory
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool to close</typeparam>
|
||||
public void Close<T>() where T : IToolForm
|
||||
{
|
||||
var tool = _tools.OfType<T>().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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of an IToolForm and return it
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of tool you want to create</typeparam>
|
||||
/// <param name="dllPath">Path .dll for an external tool</param>
|
||||
/// <returns>New instance of an IToolForm</returns>
|
||||
private IToolForm CreateInstance<T>(string dllPath)
|
||||
where T : IToolForm
|
||||
{
|
||||
return CreateInstance(typeof(T), dllPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of an IToolForm and return it
|
||||
/// </summary>
|
||||
/// <param name="toolType">Type of tool you want to create</param>
|
||||
/// <param name="dllPath">Path dll for an external tool</param>
|
||||
/// <param name="toolTypeName">For external tools, <see cref="Type.FullName"/> of the entry form's type (<paramref name="toolType"/> will be <see cref="IExternalToolForm"/>)</param>
|
||||
/// <returns>New instance of an IToolForm</returns>
|
||||
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<string> 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<ToolAttribute>().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<T>() => 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<T>() where T : class, IToolForm, new()
|
||||
{
|
||||
T tool = _tools.OfType<T>().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<string> PossibleToolTypeNames { get; } = EmuHawk.ReflectionCache.Types.Select(t => t.AssemblyQualifiedName).ToList();
|
||||
|
||||
public RamWatch RamWatch => GetTool<RamWatch>();
|
||||
|
||||
|
@ -809,7 +362,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
public TAStudio TAStudio => GetTool<TAStudio>();
|
||||
|
||||
public void LoadRamWatch(bool loadDialog)
|
||||
public override void LoadRamWatch(bool loadDialog)
|
||||
{
|
||||
if (IsLoaded<RamWatch>() && !_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<HexEditor>();
|
||||
UpdateValues<Cheats>();
|
||||
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TestExternalAPI>();
|
||||
Assert.AreEqual(0, externalApi.frameCount);
|
||||
toolManager.UpdateToolsBefore();
|
||||
toolManager.UpdateToolsAfter();
|
||||
Assert.AreEqual(1, externalApi.frameCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<string> 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<ExternalToolAttribute>();
|
||||
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<Type> 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();
|
||||
}
|
||||
}
|
|
@ -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<bool>? 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<string>? 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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue