diff --git a/src/BizHawk.Client.EmuHawk/Api/ApiManager.cs b/src/BizHawk.Client.Common/Api/ApiManager.cs similarity index 76% rename from src/BizHawk.Client.EmuHawk/Api/ApiManager.cs rename to src/BizHawk.Client.Common/Api/ApiManager.cs index 77e38d440d6..ae0310ab38d 100644 --- a/src/BizHawk.Client.EmuHawk/Api/ApiManager.cs +++ b/src/BizHawk.Client.Common/Api/ApiManager.cs @@ -4,27 +4,29 @@ using System.Linq; using System.Reflection; -using BizHawk.Client.Common; using BizHawk.Emulation.Common; -namespace BizHawk.Client.EmuHawk +namespace BizHawk.Client.Common { public static class ApiManager { - private static readonly IReadOnlyList<(Type ImplType, Type InterfaceType, ConstructorInfo Ctor, Type[] CtorTypes)> _apiTypes; + private static readonly List<(Type ImplType, Type InterfaceType, ConstructorInfo Ctor, Type[] CtorTypes)> _apiTypes = new(); static ApiManager() { - var list = new List<(Type, Type, ConstructorInfo, Type[])>(); - foreach (var implType in ReflectionCache_Biz_Cli_Com.Types.Concat(ReflectionCache.Types) + foreach (var implType in ReflectionCache_Biz_Cli_Com.Types .Where(t => /*t.IsClass &&*/t.IsSealed)) // small optimisation; api impl. types are all sealed classes { - var interfaceType = implType.GetInterfaces().FirstOrDefault(t => typeof(IExternalApi).IsAssignableFrom(t) && t != typeof(IExternalApi)); - if (interfaceType == null) continue; // if we couldn't determine what it's implementing, then it's not an api impl. type - var ctor = implType.GetConstructors().Single(); - list.Add((implType, interfaceType, ctor, ctor.GetParameters().Select(pi => pi.ParameterType).ToArray())); + AddApiType(implType); } - _apiTypes = list.ToArray(); + } + + public static void AddApiType(Type type) + { + var interfaceType = type.GetInterfaces().FirstOrDefault(t => typeof(IExternalApi).IsAssignableFrom(t) && t != typeof(IExternalApi)); + if (interfaceType == null) return; // if we couldn't determine what it's implementing, then it's not an api impl. type + var ctor = type.GetConstructors().Single(); + _apiTypes.Add((type, interfaceType, ctor, ctor.GetParameters().Select(pi => pi.ParameterType).ToArray())); } private static ApiContainer? _container; @@ -38,7 +40,7 @@ private static ApiContainer Register( DisplayManagerBase displayManager, InputManager inputManager, IMovieSession movieSession, - ToolManager toolManager, + IToolLoader toolManager, Config config, IEmulator emulator, IGameInfo game, @@ -52,7 +54,7 @@ private static ApiContainer Register( [typeof(DisplayManagerBase)] = displayManager, [typeof(InputManager)] = inputManager, [typeof(IMovieSession)] = movieSession, - [typeof(ToolManager)] = toolManager, + [typeof(IToolLoader)] = toolManager, [typeof(Config)] = config, [typeof(IEmulator)] = emulator, [typeof(IGameInfo)] = game, @@ -74,7 +76,7 @@ public static IExternalApiProvider Restart( DisplayManagerBase displayManager, InputManager inputManager, IMovieSession movieSession, - ToolManager toolManager, + IToolLoader toolManager, Config config, IEmulator emulator, IGameInfo game, @@ -92,7 +94,7 @@ public static ApiContainer RestartLua( DisplayManagerBase displayManager, InputManager inputManager, IMovieSession movieSession, - ToolManager toolManager, + IToolLoader toolManager, Config config, IEmulator emulator, IGameInfo game, diff --git a/src/BizHawk.Client.Common/IMainFormForTools.cs b/src/BizHawk.Client.Common/IMainFormForTools.cs new file mode 100644 index 00000000000..5212a46cfdd --- /dev/null +++ b/src/BizHawk.Client.Common/IMainFormForTools.cs @@ -0,0 +1,105 @@ +using BizHawk.Bizware.Graphics; +using BizHawk.Emulation.Common; + +namespace BizHawk.Client.Common +{ + public interface IMainFormForTools : IDialogController + { + /// referenced by 3 or more tools + CheatCollection CheatList { get; } + + /// referenced by 3 or more tools + string CurrentlyOpenRom { get; } + + /// referenced from HexEditor and RetroAchievements + LoadRomArgs CurrentlyOpenRomArgs { get; } + + /// only referenced from TAStudio + bool EmulatorPaused { get; } + + /// only referenced from PlaybackBox + bool HoldFrameAdvance { get; set; } + + /// only referenced from BasicBot + bool InvisibleEmulation { get; set; } + + /// only referenced from LuaConsole + bool IsTurboing { get; } + + /// only referenced from TAStudio + bool IsFastForwarding { get; } + + /// referenced from PlayMovie and TAStudio + int? PauseOnFrame { get; set; } + + /// only referenced from PlaybackBox + bool PressRewind { get; set; } + + /// referenced from BookmarksBranchesBox and VideoWriterChooserForm + BitmapBuffer CaptureOSD(); + + /// only referenced from TAStudio + void DisableRewind(); + + /// only referenced from TAStudio + void EnableRewind(bool enabled); + + /// only referenced from TAStudio + bool EnsureCoreIsAccurate(); + + /// only referenced from TAStudio + void FrameAdvance(bool discardApiHawkSurfaces = true); + + /// only referenced from LuaConsole + /// Override + void FrameBufferResized(bool forceWindowResize = false); + + /// only referenced from BasicBot + bool LoadQuickSave(int slot, bool suppressOSD = false); + + /// referenced from MultiDiskBundler and RetroAchievements + bool LoadRom(string path, LoadRomArgs args); + + /// only referenced from BookmarksBranchesBox + BitmapBuffer MakeScreenshotImage(); + + /// referenced from ToolFormBase + void MaybePauseFromMenuOpened(); + + /// referenced from ToolFormBase + void MaybeUnpauseFromMenuClosed(); + + /// referenced by 3 or more tools + void PauseEmulator(); + + /// only referenced from TAStudio + bool BlockFrameAdvance { get; set; } + + /// referenced from PlaybackBox and TAStudio + void SetMainformMovieInfo(); + + /// referenced by 3 or more tools + bool StartNewMovie(IMovie movie, bool newMovie); + + /// only referenced from BasicBot + void Throttle(); + + /// only referenced from TAStudio + void TogglePause(); + + /// referenced by 3 or more tools + void UnpauseEmulator(); + + /// only referenced from BasicBot + void Unthrottle(); + + /// only referenced from LogWindow + void UpdateDumpInfo(RomStatus? newStatus = null); + + /// only referenced from BookmarksBranchesBox + void UpdateStatusSlots(); + + /// only referenced from TAStudio + void UpdateWindowTitle(); + } +} diff --git a/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs b/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs index 1dfe7c9688f..34edc54e2c2 100644 --- a/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs +++ b/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs @@ -17,6 +17,8 @@ namespace BizHawk.Client.Common [Description("A library for manipulating the EmuHawk client UI")] public sealed class ClientLuaLibrary : LuaLibraryBase { + public Lazy AllAPINames { get; set; } + [OptionalService] private IVideoProvider VideoProvider { get; set; } @@ -111,6 +113,11 @@ public void SeekFrame(int frame) public int GetApproxFramerate() => APIs.EmuClient.GetApproxFramerate(); + [LuaMethodExample("local stconget = client.getluafunctionslist( );")] + [LuaMethod("getluafunctionslist", "returns a list of implemented functions")] + public string GetLuaFunctionsList() + => AllAPINames.Value; + [LuaMethodExample("local incliget = client.gettargetscanlineintensity( );")] [LuaMethod("gettargetscanlineintensity", "Gets the current scanline intensity setting, used for the scanline display filter")] public int GetTargetScanlineIntensity() diff --git a/src/BizHawk.Client.Common/lua/ILuaLibraries.cs b/src/BizHawk.Client.Common/lua/ILuaLibraries.cs index 6825004aafd..e44a905cd36 100644 --- a/src/BizHawk.Client.Common/lua/ILuaLibraries.cs +++ b/src/BizHawk.Client.Common/lua/ILuaLibraries.cs @@ -2,6 +2,8 @@ namespace BizHawk.Client.Common { public interface ILuaLibraries { + LuaFile CurrentFile { get; } + /// pretty hacky... we don't want a lua script to be able to restart itself by rebooting the core bool IsRebootingCore { get; set; } @@ -13,5 +15,7 @@ public interface ILuaLibraries PathEntryCollection PathEntries { get; } NLuaTableHelper GetTableHelper(); + + void Sandbox(LuaFile luaFile, Action callback, Action exceptionCallback = null); } } \ No newline at end of file diff --git a/src/BizHawk.Client.Common/lua/INamedLuaFunction.cs b/src/BizHawk.Client.Common/lua/INamedLuaFunction.cs index 22b1c778b83..30dc884affa 100644 --- a/src/BizHawk.Client.Common/lua/INamedLuaFunction.cs +++ b/src/BizHawk.Client.Common/lua/INamedLuaFunction.cs @@ -1,25 +1,27 @@ -using BizHawk.Emulation.Common; - namespace BizHawk.Client.Common { public interface INamedLuaFunction { - Action InputCallback { get; } - Guid Guid { get; } string GuidStr { get; } - MemoryCallbackDelegate MemCallback { get; } - - /// for doom.on_prandom; single param: caller of RNG, per categories in source - Action RandomCallback { get; } - - /// for doom.on_use and doom.on_cross; two params: pointers to activated line and to mobj that triggered it - Action LineCallback { get; } - string Name { get; } + /// + /// Will be called when the Lua function is unregistered / removed from the list of active callbacks. + /// The intended use case is to support callback systems that don't directly support Lua. + /// Here's what that looks like: + /// 1) A NamedLuaFunction is created and added to it's owner's list of registered functions, as normal with all Lua functions. + /// 2) A C# function is created for this specific NamedLuaFunction, which calls the Lua function via and possibly does other related Lua setup and cleanup tasks. + /// 3) That C# function is added to the non-Lua callback system. + /// 4) is assigned an that removes the C# function from the non-Lua callback. + /// Action OnRemove { get; set; } + + /// + /// Calls the Lua function with the given arguments. + /// + object[] Call(object[] args); } } diff --git a/src/BizHawk.Client.Common/lua/IPrintingLibrary.cs b/src/BizHawk.Client.Common/lua/IPrintingLibrary.cs new file mode 100644 index 00000000000..77ad10c5242 --- /dev/null +++ b/src/BizHawk.Client.Common/lua/IPrintingLibrary.cs @@ -0,0 +1,7 @@ +namespace BizHawk.Client.Common +{ + public interface IPrintingLibrary + { + void Log(params object[] outputs); + } +} diff --git a/src/BizHawk.Client.Common/lua/IRegisterFunctions.cs b/src/BizHawk.Client.Common/lua/IRegisterFunctions.cs new file mode 100644 index 00000000000..d15ee1f30f8 --- /dev/null +++ b/src/BizHawk.Client.Common/lua/IRegisterFunctions.cs @@ -0,0 +1,7 @@ +namespace BizHawk.Client.Common +{ + public interface IRegisterFunctions + { + LuaLibraryBase.NLFAddCallback CreateAndRegisterNamedFunction { get; set; } + } +} diff --git a/src/BizHawk.Client.Common/lua/LuaFile.cs b/src/BizHawk.Client.Common/lua/LuaFile.cs index 0d777f186a9..5e6dc8f5c91 100644 --- a/src/BizHawk.Client.Common/lua/LuaFile.cs +++ b/src/BizHawk.Client.Common/lua/LuaFile.cs @@ -1,54 +1,63 @@ -using NLua; +using System.Collections.Generic; +using System.Linq; + +using NLua; namespace BizHawk.Client.Common { + /// + /// If a owns an instance of this interface, it will not stop until all such instances are removed. + /// This is similar to how a script with registered callbacks does not stop until all callbacks are removed. + /// + public interface IKeepFileRunning : IDisposable { } + public class LuaFile { - public LuaFile(string path) + public LuaFile(string path, Action onFunctionListChange) { - Name = ""; Path = path; - State = RunState.Running; - FrameWaiting = false; - } - - public LuaFile(string name, string path) - { - Name = name; - Path = path; - IsSeparator = false; - - // the current directory for the lua task will start off wherever the lua file is located - CurrentDirectory = System.IO.Path.GetDirectoryName(path); + State = RunState.Disabled; + Functions = new(onFunctionListChange); } - private LuaFile(bool isSeparator) + private LuaFile(bool isSeparator) : this("", () => { }) { IsSeparator = isSeparator; - Name = ""; - Path = ""; - State = RunState.Disabled; } public static LuaFile SeparatorInstance => new(true); - public string Name { get; set; } public string Path { get; } public bool Enabled => State != RunState.Disabled; public bool Paused => State == RunState.Paused; public bool IsSeparator { get; } - public LuaThread Thread { get; set; } + public LuaThread Thread { get; private set; } public bool FrameWaiting { get; set; } - public string CurrentDirectory { get; set; } + public bool RunningEventsOnly { get; set; } = false; + + public LuaFunctionList Functions { get; } + + private List _disposables = new(); public enum RunState { Disabled, Running, Paused, + AwaitingStart, } - public RunState State { get; set; } + public RunState State { get; private set; } + + public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable); + + public void RemoveDisposable(IDisposable disposable) => _disposables.Remove(disposable); + + public bool ShouldKeepRunning() + { + return _disposables.Exists((d) => d is IKeepFileRunning) + || Functions.Any(f => f.Event != NamedLuaFunction.EVENT_TYPE_ENGINESTOP); + } public void Stop() { @@ -58,25 +67,40 @@ public void Stop() } State = RunState.Disabled; + + foreach (NamedLuaFunction func in Functions + .Where(l => l.Event == NamedLuaFunction.EVENT_TYPE_ENGINESTOP) + .ToList()) + { + func.Call(); + } + Functions.Clear(); + + foreach (IDisposable disposable in _disposables.ToList()) + disposable.Dispose(); + _disposables.Clear(); + Thread.Dispose(); Thread = null; } - public void Toggle() + public void Start(LuaThread thread) { - switch (State) - { - case RunState.Paused: - State = RunState.Running; - break; - case RunState.Disabled: - State = RunState.Running; - FrameWaiting = false; - break; - default: - State = RunState.Disabled; - break; - } + if (Thread is not null) throw new InvalidOperationException("Cannot start an already started Lua file."); + + Thread = thread; + State = RunState.Running; + FrameWaiting = false; + RunningEventsOnly = false; + + // Execution will not actually begin until the client calls LuaConsole.ResumeScripts + } + + public void ScheduleStart() + { + if (State != RunState.Disabled) throw new InvalidOperationException("A Lua file that wasn't stopped was scheduled to start."); + + State = RunState.AwaitingStart; } public void TogglePause() diff --git a/src/BizHawk.Client.Common/lua/LuaFileList.cs b/src/BizHawk.Client.Common/lua/LuaFileList.cs index 15b2493c0a0..fceb0620c44 100644 --- a/src/BizHawk.Client.Common/lua/LuaFileList.cs +++ b/src/BizHawk.Client.Common/lua/LuaFileList.cs @@ -36,7 +36,7 @@ public LuaFileList(IReadOnlyCollection collection, Action onChanged) public void StopAllScripts() { - ForEach(lf => lf.State = LuaFile.RunState.Disabled); + ForEach(lf => lf.Stop()); } public new void Clear() @@ -65,7 +65,7 @@ public void StopAllScripts() return base.Remove(item); } - public bool Load(string path, bool disableOnLoad) + public bool Load(string path, bool disableOnLoad, Action onFunctionListChange) { var file = new FileInfo(path); if (!file.Exists) @@ -91,10 +91,10 @@ public bool Load(string path, bool disableOnLoad) scriptPath = Path.GetFullPath(Path.Combine(directory ?? "", scriptPath)); } - Add(new LuaFile(scriptPath) - { - State = !disableOnLoad && line.StartsWith('1') ? LuaFile.RunState.Running : LuaFile.RunState.Disabled, - }); + LuaFile lf = new LuaFile(scriptPath, onFunctionListChange); + Add(lf); + if (!disableOnLoad && line.StartsWith('1')) + lf.ScheduleStart(); } } diff --git a/src/BizHawk.Client.Common/lua/LuaFunctionList.cs b/src/BizHawk.Client.Common/lua/LuaFunctionList.cs index 780d061af8e..71126508c01 100644 --- a/src/BizHawk.Client.Common/lua/LuaFunctionList.cs +++ b/src/BizHawk.Client.Common/lua/LuaFunctionList.cs @@ -1,6 +1,5 @@ using System.Collections; using System.Collections.Generic; -using System.Linq; namespace BizHawk.Client.Common { @@ -40,21 +39,6 @@ private bool RemoveInner(NamedLuaFunction function) return true; } - public void RemoveForFile(LuaFile file) - { - var functionsToRemove = _functions.Where(l => l.LuaFile.Path == file.Path || ReferenceEquals(l.LuaFile.Thread, file.Thread)).ToList(); - - foreach (var function in functionsToRemove) - { - _ = RemoveInner(function); - } - - if (functionsToRemove.Count != 0) - { - Changed(); - } - } - public void Clear() { if (Count is 0) return; diff --git a/src/BizHawk.Client.Common/lua/LuaHelperLibs/DoomLuaLibrary.cs b/src/BizHawk.Client.Common/lua/LuaHelperLibs/DoomLuaLibrary.cs index 2016d741406..0fb7759cadc 100644 --- a/src/BizHawk.Client.Common/lua/LuaHelperLibs/DoomLuaLibrary.cs +++ b/src/BizHawk.Client.Common/lua/LuaHelperLibs/DoomLuaLibrary.cs @@ -11,7 +11,7 @@ namespace BizHawk.Client.Common { [Description("Functions specific to Doom games (functions may not run when a Doom game is not loaded)")] - public sealed class DoomLuaLibrary : LuaLibraryBase + public sealed class DoomLuaLibrary : LuaLibraryBase, IRegisterFunctions { public NLFAddCallback CreateAndRegisterNamedFunction { get; set; } @@ -43,9 +43,16 @@ public string OnPrandom(LuaFunction luaf, string name = null) } var callbacks = dsda.RandomCallbacks; - var nlf = CreateAndRegisterNamedFunction(luaf, "OnPrandom", LogOutputCallback, CurrentFile, name: name); - callbacks.Add(nlf.RandomCallback); - nlf.OnRemove += () => callbacks.Remove(nlf.RandomCallback); + var nlf = CreateAndRegisterNamedFunction(luaf, "OnPrandom", name: name); + Action RandomCallback = pr_class => + { + _luaLibsImpl.IsInInputOrMemoryCallback = true; + nlf.Call([ pr_class ]); + _luaLibsImpl.IsInInputOrMemoryCallback = false; + }; + + callbacks.Add(RandomCallback); + nlf.OnRemove += () => callbacks.Remove(RandomCallback); return nlf.GuidStr; } @@ -68,9 +75,16 @@ public string OnUse(LuaFunction luaf, string name = null) } var callbacks = dsda.UseCallbacks; - var nlf = CreateAndRegisterNamedFunction(luaf, "OnUse", LogOutputCallback, CurrentFile, name: name); - callbacks.Add(nlf.LineCallback); - nlf.OnRemove += () => callbacks.Remove(nlf.LineCallback); + var nlf = CreateAndRegisterNamedFunction(luaf, "OnUse", name: name); + Action LineCallback = (line, thing) => + { + _luaLibsImpl.IsInInputOrMemoryCallback = true; + nlf.Call([ line, thing ]); + _luaLibsImpl.IsInInputOrMemoryCallback = false; + }; + + callbacks.Add(LineCallback); + nlf.OnRemove += () => callbacks.Remove(LineCallback); return nlf.GuidStr; } @@ -93,9 +107,16 @@ public string OnCross(LuaFunction luaf, string name = null) } var callbacks = dsda.CrossCallbacks; - var nlf = CreateAndRegisterNamedFunction(luaf, "OnCross", LogOutputCallback, CurrentFile, name: name); - callbacks.Add(nlf.LineCallback); - nlf.OnRemove += () => callbacks.Remove(nlf.LineCallback); + var nlf = CreateAndRegisterNamedFunction(luaf, "OnCross", name: name); + Action LineCallback = (line, thing) => + { + _luaLibsImpl.IsInInputOrMemoryCallback = true; + nlf.Call([ line, thing ]); + _luaLibsImpl.IsInInputOrMemoryCallback = false; + }; + + callbacks.Add(LineCallback); + nlf.OnRemove += () => callbacks.Remove(LineCallback); return nlf.GuidStr; } } diff --git a/src/BizHawk.Client.Common/lua/LuaHelperLibs/EventsLuaLibrary.cs b/src/BizHawk.Client.Common/lua/LuaHelperLibs/EventsLuaLibrary.cs index 706fd9dcbbe..7507cee6a8c 100644 --- a/src/BizHawk.Client.Common/lua/LuaHelperLibs/EventsLuaLibrary.cs +++ b/src/BizHawk.Client.Common/lua/LuaHelperLibs/EventsLuaLibrary.cs @@ -8,7 +8,7 @@ namespace BizHawk.Client.Common { [Description("A library for registering lua functions to emulator events.\n All events support multiple registered methods.\nAll registered event methods can be named and return a Guid when registered")] - public sealed class EventsLuaLibrary : LuaLibraryBase + public sealed class EventsLuaLibrary : LuaLibraryBase, IRegisterFunctions { private static readonly string EMPTY_UUID_STR = Guid.Empty.ToString("D"); @@ -36,14 +36,22 @@ public EventsLuaLibrary(ILuaLibraries luaLibsImpl, ApiContainer apiContainer, Ac private void AddMemCallbackOnCore(INamedLuaFunction nlf, MemoryCallbackType kind, string/*?*/ scope, uint? address) { var memCallbackImpl = DebuggableCore.MemoryCallbacks; + MemoryCallbackDelegate memCallback = (addr, val, flags) => + { + _luaLibsImpl.IsInInputOrMemoryCallback = true; + uint? ret = nlf.Call([ addr, val, flags ]) is [ long n ] ? unchecked((uint) n) : null; + _luaLibsImpl.IsInInputOrMemoryCallback = false; + return ret; + }; + memCallbackImpl.Add(new MemoryCallback( ProcessScope(scope), kind, "Lua Hook", - nlf.MemCallback, + memCallback, address, null)); - nlf.OnRemove += () => memCallbackImpl.Remove(nlf.MemCallback); + nlf.OnRemove += () => memCallbackImpl.Remove(memCallback); } private void LogMemoryCallbacksNotImplemented(bool isWildcard) @@ -65,20 +73,20 @@ public bool CanUseCallbackParams(string subset = null) [LuaMethodExample("local steveonf = event.onframeend(\r\n\tfunction()\r\n\t\tconsole.log( \"Calls the given lua function at the end of each frame, after all emulation and drawing has completed. Note: this is the default behavior of lua scripts\" );\r\n\tend\r\n\t, \"Frame name\" );")] [LuaMethod("onframeend", "Calls the given lua function at the end of each frame, after all emulation and drawing has completed. Note: this is the default behavior of lua scripts")] public string OnFrameEnd(LuaFunction luaf, string name = null) - => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_POSTFRAME, LogOutputCallback, CurrentFile, name: name) + => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_POSTFRAME, name: name) .GuidStr; [LuaMethodExample("local steveonf = event.onframestart(\r\n\tfunction()\r\n\t\tconsole.log( \"Calls the given lua function at the beginning of each frame before any emulation and drawing occurs\" );\r\n\tend\r\n\t, \"Frame name\" );")] [LuaMethod("onframestart", "Calls the given lua function at the beginning of each frame before any emulation and drawing occurs")] public string OnFrameStart(LuaFunction luaf, string name = null) - => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_PREFRAME, LogOutputCallback, CurrentFile, name: name) + => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_PREFRAME, name: name) .GuidStr; [LuaMethodExample("local steveoni = event.oninputpoll(\r\n\tfunction()\r\n\t\tconsole.log( \"Calls the given lua function after each time the emulator core polls for input\" );\r\n\tend\r\n\t, \"Frame name\" );")] [LuaMethod("oninputpoll", "Calls the given lua function after each time the emulator core polls for input")] public string OnInputPoll(LuaFunction luaf, string name = null) { - var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_INPUTPOLL, LogOutputCallback, CurrentFile, name: name); + var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_INPUTPOLL, name: name); //TODO should we bother registering the function if the service isn't supported? none of the other events work this way --yoshi if (InputPollableCore != null) @@ -86,8 +94,14 @@ public string OnInputPoll(LuaFunction luaf, string name = null) try { var inputCallbackImpl = InputPollableCore.InputCallbacks; - inputCallbackImpl.Add(nlf.InputCallback); - nlf.OnRemove += () => inputCallbackImpl.Remove(nlf.InputCallback); + Action InputCallback = () => + { + _luaLibsImpl.IsInInputOrMemoryCallback = true; + nlf.Call(Array.Empty()); + _luaLibsImpl.IsInInputOrMemoryCallback = false; + }; + inputCallbackImpl.Add(InputCallback); + nlf.OnRemove += () => inputCallbackImpl.Remove(InputCallback); return nlf.GuidStr; } catch (NotImplementedException) @@ -109,7 +123,7 @@ private void LogNotImplemented() [LuaMethodExample("local steveonl = event.onloadstate(\r\n\tfunction()\r\n\tconsole.log( \"Fires after a state is loaded. Receives a lua function name, and registers it to the event immediately following a successful savestate event\" );\r\nend\", \"Frame name\" );")] [LuaMethod("onloadstate", "Fires after a state is loaded. Your callback can have 1 parameter, which will be the name of the loaded state.")] public string OnLoadState(LuaFunction luaf, string name = null) - => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_LOADSTATE, LogOutputCallback, CurrentFile, name: name) + => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_LOADSTATE, name: name) .GuidStr; [LuaDeprecatedMethod] @@ -144,7 +158,7 @@ public string OnBusExec( return EMPTY_UUID_STR; } - var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_MEMEXEC, LogOutputCallback, CurrentFile, name: name); + var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_MEMEXEC, name: name); AddMemCallbackOnCore(nlf, MemoryCallbackType.Execute, scope, address); return nlf.GuidStr; } @@ -188,7 +202,7 @@ public string OnBusExecAny( return EMPTY_UUID_STR; } - var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_MEMEXECANY, LogOutputCallback, CurrentFile, name: name); + var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_MEMEXECANY, name: name); AddMemCallbackOnCore(nlf, MemoryCallbackType.Execute, scope, address: null); return nlf.GuidStr; } @@ -232,7 +246,7 @@ public string OnBusRead( return EMPTY_UUID_STR; } - var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_MEMREAD, LogOutputCallback, CurrentFile, name: name); + var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_MEMREAD, name: name); AddMemCallbackOnCore(nlf, MemoryCallbackType.Read, scope, address); return nlf.GuidStr; } @@ -277,7 +291,7 @@ public string OnBusWrite( return EMPTY_UUID_STR; } - var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_MEMWRITE, LogOutputCallback, CurrentFile, name: name); + var nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_MEMWRITE, name: name); AddMemCallbackOnCore(nlf, MemoryCallbackType.Write, scope, address); return nlf.GuidStr; } @@ -295,19 +309,19 @@ public string OnBusWrite( [LuaMethodExample("local steveons = event.onsavestate(\r\n\tfunction()\r\n\t\tconsole.log( \"Fires after a state is saved\" );\r\n\tend\r\n\t, \"Frame name\" );")] [LuaMethod("onsavestate", "Fires after a state is saved. Your callback can have 1 parameter, which will be the name of the saved state.")] public string OnSaveState(LuaFunction luaf, string name = null) - => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_SAVESTATE, LogOutputCallback, CurrentFile, name: name) + => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_SAVESTATE, name: name) .GuidStr; [LuaMethodExample("local steveone = event.onexit(\r\n\tfunction()\r\n\t\tconsole.log( \"Fires after the calling script has stopped\" );\r\n\tend\r\n\t, \"Frame name\" );")] [LuaMethod("onexit", "Fires after the calling script has stopped")] public string OnExit(LuaFunction luaf, string name = null) - => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_ENGINESTOP, LogOutputCallback, CurrentFile, name: name) + => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_ENGINESTOP, name: name) .GuidStr; [LuaMethodExample("local closeGuid = event.onconsoleclose(\r\n\tfunction()\r\n\t\tconsole.log( \"Fires when the emulator console closes\" );\r\n\tend\r\n\t, \"Frame name\" );")] [LuaMethod("onconsoleclose", "Fires when the emulator console closes")] public string OnConsoleClose(LuaFunction luaf, string name = null) - => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_CONSOLECLOSE, LogOutputCallback, CurrentFile, name: name) + => CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_CONSOLECLOSE, name: name) .GuidStr; [LuaMethodExample("if ( event.unregisterbyid( \"4d1810b7 - 0d28 - 4acb - 9d8b - d87721641551\" ) ) then\r\n\tconsole.log( \"Removes the registered function that matches the guid.If a function is found and remove the function will return true.If unable to find a match, the function will return false.\" );\r\nend;")] diff --git a/src/BizHawk.Client.Common/lua/LuaLibraries.cs b/src/BizHawk.Client.Common/lua/LuaLibraries.cs new file mode 100644 index 00000000000..2a89936917a --- /dev/null +++ b/src/BizHawk.Client.Common/lua/LuaLibraries.cs @@ -0,0 +1,475 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; + +using NLua; +using NLua.Native; + +using BizHawk.Common; +using BizHawk.Common.StringExtensions; +using BizHawk.Emulation.Common; + +namespace BizHawk.Client.Common +{ + public class LuaLibraries : ILuaLibraries + { + public static readonly bool IsAvailable = LuaNativeMethodLoader.EnsureNativeMethodsLoaded(); + + private void EnumerateLuaFunctions(string name, Type type, LuaLibraryBase instance) + { + var libraryDesc = type.GetCustomAttributes(typeof(DescriptionAttribute), false).Cast() + .Select(static descAttr => descAttr.Description) + .FirstOrDefault() ?? string.Empty; + if (instance != null) _lua.NewTable(name); + foreach (var method in type.GetMethods()) + { + var foundAttrs = method.GetCustomAttributes(typeof(LuaMethodAttribute), false); + if (foundAttrs.Length == 0) continue; + if (instance != null) _lua.RegisterFunction($"{name}.{((LuaMethodAttribute)foundAttrs[0]).Name}", instance, method); + LibraryFunction libFunc = new( + library: name, + libraryDescription: libraryDesc, + method, + suggestInREPL: instance != null + ); + Docs.Add(libFunc); + } + } + + public LuaLibraries( + List scriptList, + IEmulatorServiceProvider serviceProvider, + IMainFormForApi mainForm, + Config config, + Action printCallback, + IDialogParent dialogParent, + ApiContainer apiContainer) + { + if (!IsAvailable) + { + throw new InvalidOperationException("The Lua dynamic library was not able to be loaded"); + } + + _th = new NLuaTableHelper(_lua, printCallback); + _mainForm = mainForm; + _defaultExceptionCallback = printCallback; + _logToLuaConsoleCallback = (o) => + { + foreach (object obj in o) + printCallback(obj.ToString()); + }; + LuaWait = new AutoResetEvent(false); + PathEntries = config.PathEntries; + ScriptList = scriptList; + Docs.Clear(); + _apiContainer = apiContainer; + + // Register lua libraries + foreach (var lib in ReflectionCache_Biz_Cli_Com.Types + .Where(static t => typeof(LuaLibraryBase).IsAssignableFrom(t) && t.IsSealed)) + { + if (VersionInfo.DeveloperBuild + || lib.GetCustomAttribute(inherit: false)?.Released is not false) + { + if (!ServiceInjector.IsAvailable(serviceProvider, lib)) + { + Util.DebugWriteLine($"couldn't instantiate {lib.Name}, adding to docs only"); + EnumerateLuaFunctions( + lib.Name.RemoveSuffix("LuaLibrary").ToLowerInvariant(), // why tf aren't we doing this for all of them? or grabbing it from an attribute? + lib, + instance: null); + continue; + } + + var instance = (LuaLibraryBase)Activator.CreateInstance(lib, this, _apiContainer, printCallback); + if (!ServiceInjector.UpdateServices(serviceProvider, instance, mayCache: true)) throw new Exception("Lua lib has required service(s) that can't be fulfilled"); + + if (instance is ClientLuaLibrary clientLib) + { + clientLib.MainForm = _mainForm; + clientLib.AllAPINames = new(() => string.Join("\n", Docs.Select(static lf => lf.Name)) + "\n"); // Docs may not be fully populated now, depending on order of ReflectionCache.Types, but definitely will be when this is read + } + else if (instance is EventsLuaLibrary eventsLib) + { + eventsLib.RemoveNamedFunctionMatching = RemoveNamedFunctionMatching; + } + + AddLibrary(instance); + } + } + + _lua.RegisterFunction("print", this, typeof(LuaLibraries).GetMethod(nameof(Print))); + + var packageTable = (LuaTable) _lua["package"]; + var luaPath = PathEntries.LuaAbsolutePath(); + if (OSTailoredCode.IsUnixHost) + { + // add %exe%/Lua to library resolution pathset (LUA_PATH) + // this is done already on windows, but not on linux it seems? + packageTable["path"] = $"{luaPath}/?.lua;{luaPath}?/init.lua;{packageTable["path"]}"; + // we need to modifiy the cpath so it looks at our lua dir too, and remove the relative pathing + // we do this on Windows too, but keep in mind Linux uses .so and Windows use .dll + // TODO: Does the relative pathing issue Windows has also affect Linux? I'd assume so... + packageTable["cpath"] = $"{luaPath}/?.so;{luaPath}/loadall.so;{packageTable["cpath"]}"; + packageTable["cpath"] = ((string)packageTable["cpath"]).Replace(";./?.so", ""); + } + else + { + packageTable["cpath"] = $"{luaPath}\\?.dll;{luaPath}\\loadall.dll;{packageTable["cpath"]}"; + packageTable["cpath"] = ((string)packageTable["cpath"]).Replace(";.\\?.dll", ""); + } + + EmulationLuaLibrary.FrameAdvanceCallback = FrameAdvance; + EmulationLuaLibrary.YieldCallback = EmuYield; + } + + private ApiContainer _apiContainer; + + private GuiApi GuiAPI => (GuiApi)_apiContainer.Gui; + + private readonly IMainFormForApi _mainForm; + + private Lua _lua = new(); + + private Thread _currentHostThread; + private readonly Lock ThreadMutex = new(); + + private Stack _runningFiles = new(); + public LuaFile CurrentFile => _runningFiles.Peek(); + + private readonly NLuaTableHelper _th; + + private readonly Action _defaultExceptionCallback; + + private static Action _logToLuaConsoleCallback; + + private List _disposables = new(); + + private int _resumeState = 0; + private bool InCallback => _resumeState == 0; + + public LuaDocumentation Docs { get; } = new LuaDocumentation(); + + private EmulationLuaLibrary EmulationLuaLibrary => (EmulationLuaLibrary)Libraries[typeof(EmulationLuaLibrary)]; + + public bool IsRebootingCore { get; set; } + + public bool IsUpdateSupressed { get; set; } + + public bool IsInInputOrMemoryCallback { get; set; } + + private readonly IDictionary Libraries = new Dictionary(); + + private EventWaitHandle LuaWait; + + public PathEntryCollection PathEntries { get; private set; } + + public List ScriptList { get; } + + public NLuaTableHelper GetTableHelper() => _th; + + public void Restart( + IEmulatorServiceProvider newServiceProvider, + Config config, + ApiContainer apiContainer) + { + _apiContainer = apiContainer; + PathEntries = config.PathEntries; + foreach (var lib in Libraries.Values) + { + lib.APIs = _apiContainer; + if (!ServiceInjector.UpdateServices(newServiceProvider, lib, mayCache: true)) + { + throw new Exception("Lua lib has required service(s) that can't be fulfilled"); + } + + lib.Restarted(); + } + } + + public void AddLibrary(LuaLibraryBase lib) + { + if (lib is IRegisterFunctions rfLib) + { + rfLib.CreateAndRegisterNamedFunction = CreateAndRegisterNamedFunction; + } + + if (lib is IDisposable disposable) + { + _disposables.Add(disposable); + } + + EnumerateLuaFunctions(lib.Name, lib.GetType(), lib); + Libraries.Add(lib.GetType(), lib); + + if (lib is IPrintingLibrary printLib) + _logToLuaConsoleCallback = printLib.Log; + } + + public void AddTypeToDocs(Type type) + { + EnumerateLuaFunctions(type.GetType().Name, type, null); + } + + public bool FrameAdvanceRequested { get; private set; } + + public void CallSaveStateEvent(string name) + { + foreach (LuaFile file in ScriptList) + { + foreach (var func in file.Functions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_SAVESTATE).ToList()) + { + func.Call(name); + } + } + } + + public void CallLoadStateEvent(string name) + { + foreach (LuaFile file in ScriptList) + { + foreach (var func in file.Functions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_LOADSTATE).ToList()) + { + func.Call(name); + } + } + } + + public void CallFrameBeforeEvent() + { + if (IsUpdateSupressed) return; + + foreach (LuaFile file in ScriptList) + { + foreach (var func in file.Functions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_PREFRAME).ToList()) + { + func.Call(); + } + } + } + + public void CallFrameAfterEvent() + { + if (IsUpdateSupressed) return; + + foreach (LuaFile file in ScriptList) + { + foreach (var func in file.Functions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_POSTFRAME).ToList()) + { + func.Call(); + } + } + } + + public void Close() + { + foreach (LuaFile file in ScriptList) + { + foreach (var closeCallback in file.Functions + .Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_CONSOLECLOSE) + .ToList()) + { + closeCallback.Call(); + } + + file.Functions.Clear(); + } + + ScriptList.Clear(); + foreach (IDisposable disposable in _disposables) + { + disposable.Dispose(); + } + _disposables.Clear(); + _lua.Dispose(); + _lua = null; + } + + private INamedLuaFunction CreateAndRegisterNamedFunction( + LuaFunction function, + string theEvent, + string name = null) + { + var nlf = new NamedLuaFunction(function, theEvent, _defaultExceptionCallback, CurrentFile, this, name); + CurrentFile.Functions.Add(nlf); + return nlf; + } + + private bool RemoveNamedFunctionMatching(Func predicate) + { + if (CurrentFile.Functions.FirstOrDefault(predicate) is not NamedLuaFunction nlf) return false; + CurrentFile.Functions.Remove(nlf); + return true; + } + + public void Sandbox(LuaFile luaFile, Action callback, Action exceptionCallback = null) + { + _resumeState = Math.Max(0, _resumeState - 1); + + bool setThread = SetCurrentThread(luaFile); + _runningFiles.Push(luaFile); + LuaSandbox.GetSandbox(luaFile.Thread).Sandbox(callback, exceptionCallback ?? _defaultExceptionCallback); + _runningFiles.Pop(); + if (setThread) ClearCurrentThread(); + } + + public LuaThread SpawnCoroutineAndSandbox(string file) + { + var content = File.ReadAllText(file); + var main = _lua.LoadString(content, "main"); + var thread = _lua.NewThread(main); + LuaSandbox.CreateSandbox(thread, Path.GetDirectoryName(file)); + return thread; + } + + public LuaThread SpawnBlankCoroutineAndSandbox(string directory) + { + var main = _lua.LoadString("", "main"); + var thread = _lua.NewThread(main); + LuaSandbox.CreateSandbox(thread, Path.GetDirectoryName(directory)); + return thread; + } + + /// + /// resumes suspended scripts + /// + /// should frame waiters be waken up? only use this immediately before a frame of emulation + /// true if any script stopped + public bool ResumeScripts(bool includeFrameWaiters) + { + if (ScriptList.Count == 0 || IsUpdateSupressed) + { + return false; + } + + bool anyStopped = false; + foreach (var lf in ScriptList.Where(static lf => lf.State is LuaFile.RunState.Running)) + { + var prohibit = lf.FrameWaiting && !includeFrameWaiters; + if (!prohibit) + { + bool shouldStop = true; + bool waitForFrame = false; + bool hadException = false; + if (!lf.RunningEventsOnly) + { + (waitForFrame, shouldStop) = ResumeScript(lf, (s) => + { + Print(s); + hadException = true; + }); + } + // An exception in a main loop should stop everything. The code meant to run each frame cannot be run anymore. + if (hadException) + shouldStop = true; + else + shouldStop = shouldStop && !lf.ShouldKeepRunning(); + + if (shouldStop) + { + anyStopped = true; + lf.Stop(); + } + + lf.FrameWaiting = waitForFrame; + } + } + + return anyStopped; + } + + public object[] ExecuteString(string command) + { + const string ChunkName = "input"; // shows up in error messages + + // Use LoadString to separate parsing and execution, to tell syntax errors and runtime errors apart + LuaFunction func; + try + { + // Adding a return is necessary to get out return values of functions and turn expressions ("1+1" etc.) into valid statements + func = _lua.LoadString($"return {command}", ChunkName); + } + catch (Exception) + { + // command may be a valid statement without the added "return" + // if previous attempt couldn't be parsed, run the raw command + return _lua.DoString(command, ChunkName); + } + + using (func) + { + return func.Call(); + } + } + + private void ClearCurrentThread() + { + lock (ThreadMutex) + { + _currentHostThread = null; + } + } + + /// attempted to have Lua running in two host threads at once + private bool SetCurrentThread(LuaFile luaFile) + { + lock (ThreadMutex) + { + bool wasNull = _currentHostThread is null; + if (!wasNull && _currentHostThread != Thread.CurrentThread) + { + throw new InvalidOperationException("Can't run lua from two host threads at the same time!"); + } + _currentHostThread = Thread.CurrentThread; + return wasNull; + } + } + + public (bool WaitForFrame, bool Terminated) ResumeScript(LuaFile lf, Action exceptionCallback) + { + var result = (WaitForFrame: false, Terminated: true); + LuaStatus? execResult = null; + _resumeState = 2; + Sandbox(lf, () => execResult = lf.Thread.Resume(), exceptionCallback); + if (execResult == null) return result; + + if (execResult == LuaStatus.Yield) + result = (WaitForFrame: FrameAdvanceRequested, Terminated: false); + else if (execResult != LuaStatus.OK) + exceptionCallback($"{nameof(lf.Thread.Resume)}() returned {execResult}?"); + + FrameAdvanceRequested = false; + lf.RunningEventsOnly = result.Terminated; + return result; + } + + public static void Print(params object[] outputs) + { + _logToLuaConsoleCallback(outputs); + } + + private void FrameAdvance() + { + if (InCallback) + { + // Throw so that callback execution stops, avoiding potentail infinite loops. Unfortunately the message in the console will be ugly. + throw new Exception("emu.frameadvance is not available in events or callbacks"); + } + FrameAdvanceRequested = true; + CurrentFile.Thread.Yield(); + } + + private void EmuYield() + { + if (InCallback) + { + // Throw so that callback execution stops, avoiding potentail infinite loops. Unfortunately the message in the console will be ugly. + throw new Exception("emu.yield is not available in events or callbacks"); + } + CurrentFile.Thread.Yield(); + } + } +} diff --git a/src/BizHawk.Client.Common/lua/LuaLibraryBase.cs b/src/BizHawk.Client.Common/lua/LuaLibraryBase.cs index d530de1278d..a7c9b1d773d 100644 --- a/src/BizHawk.Client.Common/lua/LuaLibraryBase.cs +++ b/src/BizHawk.Client.Common/lua/LuaLibraryBase.cs @@ -1,5 +1,3 @@ -using System.Threading; - using BizHawk.Emulation.Common; using NLua; @@ -11,8 +9,6 @@ public abstract class LuaLibraryBase public delegate INamedLuaFunction NLFAddCallback( LuaFunction function, string theEvent, - Action logCallback, - LuaFile luaFile, string name = null); public delegate bool NLFRemoveCallback(Func predicate); @@ -28,11 +24,6 @@ protected LuaLibraryBase(ILuaLibraries luaLibsImpl, ApiContainer apiContainer, A PathEntries = _luaLibsImpl.PathEntries; } - protected static LuaFile CurrentFile { get; private set; } - - private static Thread _currentHostThread; - private static readonly Lock ThreadMutex = new(); - public abstract string Name { get; } public ApiContainer APIs { get; set; } @@ -43,33 +34,9 @@ protected LuaLibraryBase(ILuaLibraries luaLibsImpl, ApiContainer apiContainer, A protected readonly NLuaTableHelper _th; - public static void ClearCurrentThread() - { - lock (ThreadMutex) - { - _currentHostThread = null; - CurrentFile = null; - } - } - /// for implementors to reset any fields whose value depends on or a service public virtual void Restarted() {} - /// attempted to have Lua running in two host threads at once - public static void SetCurrentThread(LuaFile luaFile) - { - lock (ThreadMutex) - { - if (_currentHostThread != null) - { - throw new InvalidOperationException("Can't have lua running in two host threads at a time!"); - } - - _currentHostThread = Thread.CurrentThread; - CurrentFile = luaFile; - } - } - protected void Log(string message) => LogOutputCallback?.Invoke(message); } diff --git a/src/BizHawk.Client.Common/lua/LuaSandbox.cs b/src/BizHawk.Client.Common/lua/LuaSandbox.cs index 2d909ad20b2..2cbe66ecb0a 100644 --- a/src/BizHawk.Client.Common/lua/LuaSandbox.cs +++ b/src/BizHawk.Client.Common/lua/LuaSandbox.cs @@ -9,8 +9,6 @@ public class LuaSandbox { private static readonly ConditionalWeakTable SandboxForThread = new(); - public static Action DefaultLogger { get; set; } - public void SetSandboxCurrentDirectory(string dir) { _currentDirectory = dir; @@ -41,7 +39,7 @@ private bool CoolSetCurrentDirectory(string path, string currDirSpeedHack = null return true; } - private void Sandbox(Action callback, Action exceptionCallback) + public void Sandbox(Action callback, Action exceptionCallback) { string savedEnvironmentCurrDir = null; try @@ -55,7 +53,7 @@ private void Sandbox(Action callback, Action exceptionCallback) EnvironmentSandbox.Sandbox(callback); } - catch (NLua.Exceptions.LuaException ex) + catch (Exception ex) { var exStr = ex.ToString() + '\n'; if (ex.InnerException is not null) @@ -64,8 +62,7 @@ private void Sandbox(Action callback, Action exceptionCallback) } Console.Write(exStr); - DefaultLogger(exStr); - exceptionCallback?.Invoke(); + exceptionCallback.Invoke(exStr); } finally { @@ -106,10 +103,5 @@ public static LuaSandbox GetSandbox(LuaThread thread) throw new InvalidOperationException("HOARY GORILLA HIJINX"); } } - - public static void Sandbox(LuaThread thread, Action callback, Action exceptionCallback = null) - { - GetSandbox(thread).Sandbox(callback, exceptionCallback); - } } } diff --git a/src/BizHawk.Client.Common/lua/NamedLuaFunction.cs b/src/BizHawk.Client.Common/lua/NamedLuaFunction.cs index ff1cf0a42c0..320999e5e9a 100644 --- a/src/BizHawk.Client.Common/lua/NamedLuaFunction.cs +++ b/src/BizHawk.Client.Common/lua/NamedLuaFunction.cs @@ -1,7 +1,5 @@ using NLua; -using BizHawk.Emulation.Common; - namespace BizHawk.Client.Common { public sealed class NamedLuaFunction : INamedLuaFunction @@ -30,104 +28,23 @@ public sealed class NamedLuaFunction : INamedLuaFunction private readonly LuaFunction _function; + private readonly ILuaLibraries _luaImp; + + private readonly Action _exceptionCallback; + public Action/*?*/ OnRemove { get; set; } = null; public NamedLuaFunction(LuaFunction function, string theEvent, Action logCallback, LuaFile luaFile, - Func createThreadCallback, ILuaLibraries luaLibraries, string name = null) + ILuaLibraries luaLibraries, string name = null) { _function = function; + _luaImp = luaLibraries; + _exceptionCallback = logCallback; Name = name ?? "Anonymous"; Event = theEvent; - CreateThreadCallback = createThreadCallback; - - // When would a file be null? - // When a script is loaded with a callback, but no infinite loop so it closes - // Then that callback proceeds to register more callbacks - // In these situations, we will generate a thread for this new callback on the fly here - // Scenarios like this suggest that a thread being managed by a LuaFile is a bad idea, - // and we should refactor - if (luaFile == null) - { - DetachFromScript(); - } - else - { - LuaFile = luaFile; - } + LuaFile = luaFile; Guid = Guid.NewGuid(); - - Callback = args => - { - try - { - return _function.Call(args); - } - catch (Exception ex) - { - logCallback($"error running function attached by the event {Event}\nError message: {ex.Message}"); - } - return null; - }; - InputCallback = () => - { - luaLibraries.IsInInputOrMemoryCallback = true; - try - { - Callback(Array.Empty()); - } - finally - { - luaLibraries.IsInInputOrMemoryCallback = false; - } - }; - MemCallback = (addr, val, flags) => - { - luaLibraries.IsInInputOrMemoryCallback = true; - try - { - return Callback([ addr, val, flags ]) is [ long n ] ? unchecked((uint) n) : null; - } - finally - { - luaLibraries.IsInInputOrMemoryCallback = false; - } - }; - RandomCallback = pr_class => - { - luaLibraries.IsInInputOrMemoryCallback = true; - try - { - Callback([ pr_class ]); - } - finally - { - luaLibraries.IsInInputOrMemoryCallback = false; - } - }; - LineCallback = (line, thing) => - { - luaLibraries.IsInInputOrMemoryCallback = true; - try - { - Callback([ line, thing ]); - } - finally - { - luaLibraries.IsInInputOrMemoryCallback = false; - } - }; - } - - public void DetachFromScript() - { - var thread = CreateThreadCallback(); - - // Current dir will have to do for now, but this will inevitably not be desired - // Users will expect it to be the same directly as the thread that spawned this callback - // But how do we know what that directory was? - LuaSandbox.CreateSandbox(thread, "."); - LuaFile = new LuaFile(".") { Thread = thread }; } public Guid Guid { get; } @@ -137,28 +54,22 @@ public string GuidStr public string Name { get; } - public LuaFile LuaFile { get; private set; } - - private Func CreateThreadCallback { get; } + private LuaFile LuaFile { get; } public string Event { get; } - private Func Callback { get; } - - public Action InputCallback { get; } - - public MemoryCallbackDelegate MemCallback { get; } - - public Action RandomCallback { get; } - - public Action LineCallback { get; } - public void Call(string name = null) { - LuaSandbox.Sandbox(LuaFile.Thread, () => - { - _function.Call(name); - }); + _luaImp.Sandbox(LuaFile, () => _ = _function.Call(name), (s) => + _exceptionCallback($"error running function attached by the event {Event}\nError message: {s}")); + } + + public object[] Call(params object[] args) + { + object[] ret = null; + _luaImp.Sandbox(LuaFile, () => ret = _function.Call(args), (s) => + _exceptionCallback($"error running function attached by the event {Event}\nError message: {s}")); + return ret; } } } diff --git a/src/BizHawk.Client.Common/tools/Interfaces/IToolLoader.cs b/src/BizHawk.Client.Common/tools/Interfaces/IToolLoader.cs new file mode 100644 index 00000000000..ea5de9924d8 --- /dev/null +++ b/src/BizHawk.Client.Common/tools/Interfaces/IToolLoader.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace BizHawk.Client.Common +{ + public interface IToolLoader + { + IEnumerable AvailableTools { get; } + + /// + /// 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 + T Load(bool focus = true, string toolPath = "") + where T : class, IToolForm; + + /// + /// 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 + IToolForm Load(Type toolType, bool focus = true); + + void LoadRamWatch(bool loadDialog); + } +} diff --git a/src/BizHawk.Client.EmuHawk/Api/Libraries/ToolApi.cs b/src/BizHawk.Client.EmuHawk/Api/Libraries/ToolApi.cs index 8f3d87d9b32..0023c47900f 100644 --- a/src/BizHawk.Client.EmuHawk/Api/Libraries/ToolApi.cs +++ b/src/BizHawk.Client.EmuHawk/Api/Libraries/ToolApi.cs @@ -8,17 +8,17 @@ namespace BizHawk.Client.EmuHawk { public sealed class ToolApi : IToolApi { - private readonly ToolManager _toolManager; + private readonly IToolLoader _toolLoader; - public IEnumerable AvailableTools => _toolManager.AvailableTools.ToList(); // defensive copy in case ToolManager's implementation changes + public IEnumerable AvailableTools => _toolLoader.AvailableTools.ToList(); // defensive copy in case ToolManager's implementation changes - public ToolApi(ToolManager toolManager) => _toolManager = toolManager; + public ToolApi(IToolLoader toolLoader) => _toolLoader = toolLoader; public IToolForm GetTool(string name) { var toolType = Util.GetTypeByName(name).FirstOrDefault(x => typeof(IToolForm).IsAssignableFrom(x) && !x.IsInterface); if (toolType == null) return null; - return _toolManager.Load(toolType); + return _toolLoader.Load(toolType); } public object CreateInstance(string name) @@ -27,18 +27,18 @@ public object CreateInstance(string name) return found != null ? Activator.CreateInstance(found) : null; } - public void OpenCheats() => _toolManager.Load(); + public void OpenCheats() => _toolLoader.Load(); - public void OpenHexEditor() => _toolManager.Load(); + public void OpenHexEditor() => _toolLoader.Load(); - public void OpenRamWatch() => _toolManager.LoadRamWatch(loadDialog: true); + public void OpenRamWatch() => _toolLoader.LoadRamWatch(loadDialog: true); - public void OpenRamSearch() => _toolManager.Load(); + public void OpenRamSearch() => _toolLoader.Load(); - public void OpenTasStudio() => _toolManager.Load(); + public void OpenTasStudio() => _toolLoader.Load(); - public void OpenToolBox() => _toolManager.Load(); + public void OpenToolBox() => _toolLoader.Load(); - public void OpenTraceLogger() => _toolManager.Load(); + public void OpenTraceLogger() => _toolLoader.Load(); } } diff --git a/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs b/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs deleted file mode 100644 index 520fe9b8873..00000000000 --- a/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs +++ /dev/null @@ -1,106 +0,0 @@ -using BizHawk.Bizware.Graphics; -using BizHawk.Client.Common; -using BizHawk.Emulation.Common; - -namespace BizHawk.Client.EmuHawk -{ - public interface IMainFormForTools : IDialogController - { - /// referenced by 3 or more tools - CheatCollection CheatList { get; } - - /// referenced by 3 or more tools - string CurrentlyOpenRom { get; } - - /// referenced from and RetroAchievements - LoadRomArgs CurrentlyOpenRomArgs { get; } - - /// only referenced from - bool EmulatorPaused { get; } - - /// only referenced from - bool HoldFrameAdvance { get; set; } - - /// only referenced from - bool InvisibleEmulation { get; set; } - - /// only referenced from - bool IsTurboing { get; } - - /// only referenced from - bool IsFastForwarding { get; } - - /// referenced from and - int? PauseOnFrame { get; set; } - - /// only referenced from - bool PressRewind { get; set; } - - /// referenced from and - BitmapBuffer CaptureOSD(); - - /// only referenced from - void DisableRewind(); - - /// only referenced from - void EnableRewind(bool enabled); - - /// only referenced from - bool EnsureCoreIsAccurate(); - - /// only referenced from - void FrameAdvance(bool discardApiHawkSurfaces = true); - - /// only referenced from - /// Override - void FrameBufferResized(bool forceWindowResize = false); - - /// only referenced from - bool LoadQuickSave(int slot, bool suppressOSD = false); - - /// referenced from and RetroAchievements - bool LoadRom(string path, LoadRomArgs args); - - /// only referenced from - BitmapBuffer MakeScreenshotImage(); - - /// referenced from - void MaybePauseFromMenuOpened(); - - /// referenced from - void MaybeUnpauseFromMenuClosed(); - - /// referenced by 3 or more tools - void PauseEmulator(); - - /// only referenced from - bool BlockFrameAdvance { get; set; } - - /// referenced from and - void SetMainformMovieInfo(); - - /// referenced by 3 or more tools - bool StartNewMovie(IMovie movie, bool newMovie); - - /// only referenced from - void Throttle(); - - /// only referenced from - void TogglePause(); - - /// referenced by 3 or more tools - void UnpauseEmulator(); - - /// only referenced from - void Unthrottle(); - - /// only referenced from - void UpdateDumpInfo(RomStatus? newStatus = null); - - /// only referenced from - void UpdateStatusSlots(); - - /// referenced from and RetroAchievements - void UpdateWindowTitle(); - } -} diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/ConsoleLuaLibrary.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/ConsoleLuaLibrary.cs index 0ca1878ab8e..44e3a04fa5f 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/ConsoleLuaLibrary.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/ConsoleLuaLibrary.cs @@ -7,10 +7,8 @@ namespace BizHawk.Client.EmuHawk { - public sealed class ConsoleLuaLibrary : LuaLibraryBase + public sealed class ConsoleLuaLibrary : LuaLibraryBase, IPrintingLibrary { - public Lazy AllAPINames { get; set; } - public ToolManager Tools { get; set; } public ConsoleLuaLibrary(ILuaLibraries luaLibsImpl, ApiContainer apiContainer, Action logOutputCallback) @@ -28,11 +26,6 @@ public void Clear() } } - [LuaMethodExample("local stconget = console.getluafunctionslist( );")] - [LuaMethod("getluafunctionslist", "returns a list of implemented functions")] - public string GetLuaFunctionsList() - => AllAPINames.Value; - [LuaMethodExample("console.log( \"New log.\" );")] [LuaMethod("log", "Outputs the given object to the output box on the Lua Console dialog. Note: Can accept a LuaTable")] public void Log(params object[] outputs) diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/FormsLuaLibrary.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/FormsLuaLibrary.cs index fe9505ad53e..37cc39344b8 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/FormsLuaLibrary.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/FormsLuaLibrary.cs @@ -12,7 +12,7 @@ namespace BizHawk.Client.EmuHawk { [Description("A library for creating and managing custom dialogs")] - public sealed class FormsLuaLibrary : LuaLibraryBase + public sealed class FormsLuaLibrary : LuaLibraryBase, IDisposable { private const string DESC_LINE_OPT_CTRL_POS = " If the x and y parameters are both nil/unset, the control's Location property won't be set. If both are specified, the control will be positioned at (x, y) within the given form."; @@ -58,6 +58,11 @@ private static void SetSize(Control control, int width, int height) private static void SetText(Control control, string caption) => control.Text = caption ?? string.Empty; + public void Dispose() + { + DestroyAll(); + } + [LuaMethodExample("forms.addclick( 332, function()\r\n\tconsole.log( \"adds the given lua function as a click event to the given control\" );\r\nend );")] [LuaMethod("addclick", "adds the given lua function as a click event to the given control")] public void AddClick(long handle, LuaFunction clickEvent) @@ -148,9 +153,10 @@ public bool Destroy(long handle) [LuaMethod("destroyall", "Closes and removes all Lua created dialogs")] public void DestroyAll() { - for (var i = _luaForms.Count - 1; i >= 0; i--) + // A form's close handler may close other forms. + foreach (LuaWinform form in _luaForms.ToArray()) { - _luaForms[i].Close(); + if (!form.IsDisposed) form.Close(); } _luaForms.Clear(); } @@ -287,7 +293,7 @@ public long NewForm( if (OwnerForm is not IWin32Window ownerForm) throw new Exception("IDialogParent must implement IWin32Window"); - var form = new LuaWinform(CurrentFile, WindowClosed); + var form = new LuaWinform(_luaLibsImpl.CurrentFile, _luaLibsImpl, WindowClosed, onClose); _luaForms.Add(form); if (width.HasValue && height.HasValue) { @@ -300,21 +306,6 @@ public long NewForm( form.Icon = SystemIcons.Application; form.Show(ownerForm); - form.FormClosed += (o, e) => - { - if (onClose != null) - { - try - { - onClose.Call(); - } - catch (Exception ex) - { - Log(ex.ToString()); - } - } - }; - return (long)form.Handle; } diff --git a/src/BizHawk.Client.Common/lua/CommonLibs/GuiLuaLibrary.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs similarity index 96% rename from src/BizHawk.Client.Common/lua/CommonLibs/GuiLuaLibrary.cs rename to src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs index 1a2bf002a69..4c06f27caa2 100644 --- a/src/BizHawk.Client.Common/lua/CommonLibs/GuiLuaLibrary.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs @@ -1,16 +1,15 @@ using System.Drawing; using System.Linq; +using BizHawk.Client.Common; using NLua; -namespace BizHawk.Client.Common +namespace BizHawk.Client.EmuHawk { public sealed class GuiLuaLibrary : LuaLibraryBase, IDisposable { private DisplaySurfaceID _rememberedSurfaceID = DisplaySurfaceID.EmuCore; - public Func CreateLuaCanvasCallback { get; set; } - public GuiLuaLibrary(ILuaLibraries luaLibsImpl, ApiContainer apiContainer, Action logOutputCallback) : base(luaLibsImpl, apiContainer, logOutputCallback) {} @@ -362,7 +361,14 @@ public void Text( + " The width and height parameters determine the size of the canvas." + " If the x and y parameters are both nil/unset, the form (window) will appear at the default position. If both are specified, the form will be positioned at (x, y) on the screen.")] // technically x can be specified w/o y but let's leave that as UB public LuaTable CreateCanvas(int width, int height, int? x = null, int? y = null) - => CreateLuaCanvasCallback(width, height, x, y); + { + var canvas = new LuaCanvas(APIs.Emulation, PathEntries, width, height, x, y, _th, LogOutputCallback); + canvas.Show(); + LuaFile lf = _luaLibsImpl.CurrentFile; + lf.AddDisposable(canvas); + canvas.FormClosed += (s, e) => lf.RemoveDisposable(canvas); + return _th.ObjectToTable(canvas); + } [LuaMethodExample("gui.use_surface( \"client\" );")] [LuaMethod("use_surface", "Stores the name of a surface to draw on, so you don't need to pass it to every draw function. The default is \"emucore\", and the other valid value is \"client\".")] diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/TAStudioLuaLibrary.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/TAStudioLuaLibrary.cs index 806c5f90c05..d1f1c9bbca0 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/TAStudioLuaLibrary.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/TAStudioLuaLibrary.cs @@ -14,7 +14,7 @@ namespace BizHawk.Client.EmuHawk { [Description("A library for manipulating the Tastudio dialog of the EmuHawk client")] [LuaLibrary(released: true)] - public sealed class TAStudioLuaLibrary : LuaLibraryBase + public sealed class TAStudioLuaLibrary : LuaLibraryBase, IRegisterFunctions { private const string DESC_LINE_EDIT_QUEUE_APPLY = " Edits will take effect once you call {{tastudio.applyinputchanges}}."; @@ -28,6 +28,8 @@ private const string DESC_LINE_BRANCH_CHANGE_CB public ToolManager Tools { get; set; } + public NLFAddCallback CreateAndRegisterNamedFunction { get; set; } + public TAStudioLuaLibrary(ILuaLibraries luaLibsImpl, ApiContainer apiContainer, Action logOutputCallback) : base(luaLibsImpl, apiContainer, logOutputCallback) {} @@ -606,31 +608,46 @@ public void SetMarker(int frame, string message = null) [LuaMethodExample("tastudio.onqueryitembg( function( currentindex, itemname )\r\n\tconsole.log( \"called during the background draw event of the tastudio listview. luaf must be a function that takes 2 params: index, column. The first is the integer row index of the listview, and the 2nd is the string column name. luaf should return a value that can be parsed into a .NET Color object (string color name, or integer value)\" );\r\nend );")] [LuaMethod("onqueryitembg", "called during the background draw event of the tastudio listview. luaf must be a function that takes 2 params: index, column. The first is the integer row index of the listview, and the 2nd is the string column name. luaf should return a value that can be parsed into a .NET Color object (string color name, or integer value)")] - public void OnQueryItemBg(LuaFunction luaf) + public string OnQueryItemBg(LuaFunction luaf, string name = null) { if (Engaged()) { - Tastudio.QueryItemBgColorCallback = (index, name) => _th.SafeParseColor(luaf.Call(index, name)?.FirstOrDefault()); + var nlf = CreateAndRegisterNamedFunction(luaf, "OnQueryItemBg", name: name); + TAStudio.QueryColor callback = (index, name) => _th.SafeParseColor(luaf.Call(index, name)?.FirstOrDefault()); + + Tastudio.AddQueryBgColorCallback(callback); + nlf.OnRemove += () => Tastudio.RemoveQueryBgColorCallback(callback); + + return nlf.GuidStr; } + return ""; } [LuaMethodExample("tastudio.onqueryitemtext( function( currentindex, itemname )\r\n\tconsole.log( \"called during the text draw event of the tastudio listview. luaf must be a function that takes 2 params: index, column. The first is the integer row index of the listview, and the 2nd is the string column name. luaf should return a value that can be parsed into a .NET Color object (string color name, or integer value)\" );\r\nend );")] [LuaMethod("onqueryitemtext", "Called during the text draw event of the tastudio listview. {{luaf}} must be a function that takes 2 params: {{(index, column)}}. The first is the integer row index of the listview, and the 2nd is the string column name. The callback should return a string to be displayed.")] - public void OnQueryItemText(LuaFunction luaf) + public string OnQueryItemText(LuaFunction luaf, string name = null) { if (Engaged()) { - Tastudio.QueryItemTextCallback = (index, name) => luaf.Call(index, name)?.FirstOrDefault()?.ToString(); + var nlf = CreateAndRegisterNamedFunction(luaf, "OnQueryItemText", name: name); + TAStudio.QueryText callback = (index, name) => luaf.Call(index, name)?.FirstOrDefault()?.ToString(); + + Tastudio.AddQueryItemTextCallback(callback); + nlf.OnRemove += () => Tastudio.RemoveQueryItemTextCallback(callback); + + return nlf.GuidStr; } + return ""; } [LuaMethodExample("tastudio.onqueryitemicon( function( currentindex, itemname )\r\n\tconsole.log( \"called during the icon draw event of the tastudio listview. luaf must be a function that takes 2 params: index, column. The first is the integer row index of the listview, and the 2nd is the string column name. luaf should return a value that can be parsed into a .NET Color object (string color name, or integer value)\" );\r\nend );")] [LuaMethod("onqueryitemicon", "Called during the icon draw event of the tastudio listview. {{luaf}} must be a function that takes 2 params: {{(index, column)}}. The first is the integer row index of the listview, and the 2nd is the string column name. The callback should return a string, the path to the {{.ico}} file to be displayed. The file will be cached, so if you change the file on disk, call {{tastudio.clearIconCache()}}.")] - public void OnQueryItemIcon(LuaFunction luaf) + public string OnQueryItemIcon(LuaFunction luaf, string name = null) { if (Engaged()) { - Tastudio.QueryItemIconCallback = (index, name) => + var nlf = CreateAndRegisterNamedFunction(luaf, "OnQueryItemIcon", name: name); + TAStudio.QueryIcon callback = (index, name) => { var result = luaf.Call(index, name); if (result?.FirstOrDefault() is not null) @@ -640,7 +657,13 @@ public void OnQueryItemIcon(LuaFunction luaf) return null; }; + + Tastudio.AddQueryItemIconCallback(callback); + nlf.OnRemove += () => Tastudio.RemoveQueryItemIconCallback(callback); + + return nlf.GuidStr; } + return ""; } [LuaMethodExample("tastudio.clearIconCache();")] @@ -653,28 +676,36 @@ public void ClearIconCache() [LuaMethodExample("tastudio.ongreenzoneinvalidated( function( currentindex )\r\n\tconsole.log( \"Called whenever the greenzone is invalidated.\" );\r\nend );")] [LuaMethod("ongreenzoneinvalidated", "Called whenever the movie is modified in a way that could invalidate savestates in the movie's state history. Called regardless of whether any states were actually invalidated. Your callback can have 1 parameter, which will be the last frame before the invalidated ones. That is, the first of the modified frames.")] - public void OnGreenzoneInvalidated(LuaFunction luaf) + public string OnGreenzoneInvalidated(LuaFunction luaf, string name = null) { if (Engaged()) { - Tastudio.GreenzoneInvalidatedCallback = index => - { - luaf.Call(index); - }; + var nlf = CreateAndRegisterNamedFunction(luaf, "OnGreenzoneInvalidated", name: name); + Action callback = index => luaf.Call(index); + + Tastudio.GreenzoneInvalidatedCallback += callback; + nlf.OnRemove += () => Tastudio.GreenzoneInvalidatedCallback -= callback; + + return nlf.GuidStr; } + return ""; } [LuaMethodExample("tastudio.onbranchload( function( currentindex )\r\n\tconsole.log( \"Called whenever a branch is loaded.\" );\r\nend );")] [LuaMethod("onbranchload", "called whenever a branch is loaded. luaf must be a function that takes the integer branch index as a parameter")] - public void OnBranchLoad(LuaFunction luaf) + public string OnBranchLoad(LuaFunction luaf, string name = null) { if (Engaged()) { - Tastudio.BranchLoadedCallback = index => - { - luaf.Call(index); - }; + var nlf = CreateAndRegisterNamedFunction(luaf, "OnBranchLoad", name: name); + Action callback = index => luaf.Call(index); + + Tastudio.BranchLoadedCallback += callback; + nlf.OnRemove += () => Tastudio.BranchLoadedCallback -= callback; + + return nlf.GuidStr; } + return ""; } [LuaMethodExample(""" @@ -684,15 +715,19 @@ public void OnBranchLoad(LuaFunction luaf) name: "onbranchsave", description: "Sets a callback which fires after any branch is created or updated." + DESC_LINE_BRANCH_CHANGE_CB)] - public void OnBranchSave(LuaFunction luaf) + public string OnBranchSave(LuaFunction luaf, string name = null) { if (Engaged()) { - Tastudio.BranchSavedCallback = index => - { - luaf.Call(index); - }; + var nlf = CreateAndRegisterNamedFunction(luaf, "OnBranchSave", name: name); + Action callback = index => luaf.Call(index); + + Tastudio.BranchSavedCallback += callback; + nlf.OnRemove += () => Tastudio.BranchSavedCallback -= callback; + + return nlf.GuidStr; } + return ""; } [LuaMethodExample(""" @@ -702,15 +737,19 @@ public void OnBranchSave(LuaFunction luaf) name: "onbranchremove", description: "Sets a callback which fires after any branch is removed." + DESC_LINE_BRANCH_CHANGE_CB)] - public void OnBranchRemove(LuaFunction luaf) + public string OnBranchRemove(LuaFunction luaf, string name = null) { if (Engaged()) { - Tastudio.BranchRemovedCallback = index => - { - luaf.Call(index); - }; + var nlf = CreateAndRegisterNamedFunction(luaf, "OnBranchRemove", name: name); + Action callback = index => luaf.Call(index); + + Tastudio.BranchRemovedCallback += callback; + nlf.OnRemove += () => Tastudio.BranchRemovedCallback -= callback; + + return nlf.GuidStr; } + return ""; } } } diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaCanvas.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaCanvas.cs index 8b103066604..8fde7902e60 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaCanvas.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaCanvas.cs @@ -14,7 +14,9 @@ namespace BizHawk.Client.EmuHawk [Description("Represents a canvas object returned by the gui.createcanvas() method")] public sealed class LuaCanvas : Form { - private readonly EmulationLuaLibrary _emuLib; + private readonly IEmulationApi _emuLib; + + private readonly PathEntryCollection _pathEntrys; private readonly NLuaTableHelper _th; @@ -23,7 +25,8 @@ public sealed class LuaCanvas : Form private readonly LuaPictureBox luaPictureBox; public LuaCanvas( - EmulationLuaLibrary emuLib, + IEmulationApi emuLib, + PathEntryCollection pathEntrys, int width, int height, int? x, @@ -32,6 +35,7 @@ public LuaCanvas( Action logOutputCallback) { _emuLib = emuLib; + _pathEntrys = pathEntrys; _th = tableHelper; LogOutputCallback = logOutputCallback; @@ -485,7 +489,7 @@ public int GetMouseY() [LuaMethod("save_image_to_disk", "Saves everything that's been drawn to a .png file at the given path. Relative paths are relative to the path set for \"Screenshots\" for the current system.")] public void SaveImageToDisk(string path) { - luaPictureBox.Image.Save(path.MakeAbsolute(_emuLib.PathEntries.ScreenshotAbsolutePathFor(_emuLib.GetSystemId()))); + luaPictureBox.Image.Save(path.MakeAbsolute(_pathEntrys.ScreenshotAbsolutePathFor(_emuLib.GetSystemId()))); } } } diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs index 1e6eb414cbb..7080b86fe78 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Windows.Forms; using BizHawk.Client.Common; @@ -151,13 +150,17 @@ public LuaConsole() LuaListView.QueryItemIcon += LuaListView_QueryItemImage; // this is bad, in case we ever have more than one gui part running lua.. not sure how much other badness there is like that - LuaSandbox.DefaultLogger = WriteToOutputWindow; _defaultSplitDistance = splitContainer1.SplitterDistance; } private LuaLibraries LuaImp; - private IEnumerable SelectedItems => LuaListView.SelectedRows.Select(index => LuaImp.ScriptList[index]); + private ConsoleLuaLibrary _consoleLib; + + private LuaFile _nonFile; + private LuaFileList _openedFiles; + + private IEnumerable SelectedItems => LuaListView.SelectedRows.Select(index => _openedFiles[index]); private IEnumerable SelectedFiles => SelectedItems.Where(x => !x.IsSeparator); @@ -218,22 +221,25 @@ public override void Restart() // we don't use runningScripts here as the other scripts need to be stopped too foreach (var file in LuaImp.ScriptList) { - DisableLuaScript(file); + file.Stop(); } } - LuaFileList newScripts = new(LuaImp?.ScriptList, onChanged: SessionChangedCallback); - LuaFunctionList registeredFuncList = new(onChanged: UpdateRegisteredFunctionsDialog); + _openedFiles = new(_openedFiles, onChanged: SessionChangedCallback); LuaImp?.Close(); LuaImp = new LuaLibraries( - newScripts, - registeredFuncList, + _openedFiles.Where(static lf => !lf.IsSeparator).ToList(), Emulator.ServiceProvider, MainFormForApi, Config, - Tools, + WriteToOutputWindow, (IDialogParent)MainForm, // not sure why neither main form interface implements IDialogParent, but our MainForm class does apiContainer); + _consoleLib = new ConsoleLuaLibrary(LuaImp, apiContainer, WriteToOutputWindow) { Tools = Tools }; + LuaImp.AddLibrary(_consoleLib); + LuaImp.AddLibrary(new FormsLuaLibrary(LuaImp, apiContainer, WriteToOutputWindow) { OwnerForm = (IDialogParent)MainForm }); + LuaImp.AddLibrary(new TAStudioLuaLibrary(LuaImp, apiContainer, WriteToOutputWindow) { Tools = Tools }); + LuaImp.AddLibrary(new GuiLuaLibrary(LuaImp, apiContainer, WriteToOutputWindow)); InputBox.AutoCompleteCustomSource.Clear(); InputBox.AutoCompleteCustomSource.AddRange(LuaImp.Docs.Where(static f => f.SuggestInREPL) @@ -242,24 +248,13 @@ public override void Restart() foreach (var file in runningScripts) { - try - { - LuaSandbox.Sandbox(file.Thread, () => - { - LuaImp.SpawnAndSetFileThread(file.Path, file); - LuaSandbox.CreateSandbox(file.Thread, Path.GetDirectoryName(file.Path)); - file.State = LuaFile.RunState.Running; - }, () => - { - file.State = LuaFile.RunState.Disabled; - }); - } - catch (Exception ex) - { - DialogController.ShowMessageBox(ex.ToString()); - } + EnableLuaFile(file); } + _nonFile = new LuaFile(Config.PathEntries.LuaAbsolutePath(), UpdateRegisteredFunctionsDialog); + _nonFile.Start(LuaImp.SpawnBlankCoroutineAndSandbox(null)); + LuaImp.ScriptList.Insert(0, _nonFile); + UpdateDialog(); } @@ -282,7 +277,7 @@ private void AddFileWatches() if (Settings.ReloadOnScriptFileChange) { ClearFileWatches(); - foreach (var item in LuaImp.ScriptList.Where(s => !s.IsSeparator)) + foreach (var item in _openedFiles.Where(s => !s.IsSeparator)) { CreateFileWatcher(item); } @@ -333,7 +328,7 @@ private void RemoveFileWatcher(LuaFile item) private void OnLuaFileChanged(LuaFile item) { - if (item.Enabled && LuaImp.ScriptList.Contains(item)) + if (item.Enabled && _openedFiles.Contains(item)) { RefreshLuaScript(item); } @@ -343,7 +338,7 @@ public void LoadLuaFile(string path) { var absolutePath = Path.GetFullPath(path); - var alreadyLoadedFile = LuaImp.ScriptList.FirstOrDefault(t => absolutePath == t.Path); + var alreadyLoadedFile = _openedFiles.Find(t => absolutePath == t.Path); if (alreadyLoadedFile is not null) { if (!alreadyLoadedFile.Enabled && !Settings.DisableLuaScriptsOnLoad) @@ -353,21 +348,17 @@ public void LoadLuaFile(string path) } else { - var luaFile = new LuaFile("", absolutePath); + var luaFile = new LuaFile(absolutePath, UpdateRegisteredFunctionsDialog); LuaImp.ScriptList.Add(luaFile); - LuaListView.RowCount = LuaImp.ScriptList.Count; + _openedFiles.Add(luaFile); + LuaListView.RowCount = _openedFiles.Count; Config.RecentLua.Add(absolutePath); if (!Settings.DisableLuaScriptsOnLoad) { - luaFile.State = LuaFile.RunState.Running; EnableLuaFile(luaFile); } - else - { - luaFile.State = LuaFile.RunState.Disabled; - } if (Settings.ReloadOnScriptFileChange) { @@ -382,7 +373,7 @@ public void RemoveLuaFile(string path) { var absolutePath = Path.GetFullPath(path); - var luaFile = LuaImp.ScriptList.FirstOrDefault(t => absolutePath == t.Path); + var luaFile = _openedFiles.FirstOrDefault(t => absolutePath == t.Path); if (luaFile is not null) { RemoveLuaFile(luaFile); @@ -394,23 +385,24 @@ private void RemoveLuaFile(LuaFile item) { if (!item.IsSeparator) { - DisableLuaScript(item); + item.Stop(); RemoveFileWatcher(item); } LuaImp.ScriptList.Remove(item); + _openedFiles.Remove(item); } private void RemoveAllLuaFiles() { - while (LuaImp.ScriptList.Count > 0) + while (_openedFiles.Count > 0) { - RemoveLuaFile(LuaImp.ScriptList[LuaImp.ScriptList.Count - 1]); + RemoveLuaFile(_openedFiles[_openedFiles.Count - 1]); } } private void UpdateDialog() { - LuaListView.RowCount = LuaImp.ScriptList.Count; + LuaListView.RowCount = _openedFiles.Count; UpdateNumberOfScripts(); UpdateRegisteredFunctionsDialog(); } @@ -418,8 +410,8 @@ private void UpdateDialog() private void SessionChangedCallback() { OutputMessages.Text = - (LuaImp.ScriptList.Changes ? "* " : "") + - Path.GetFileName(LuaImp.ScriptList.Filename); + (_openedFiles.Changes ? "* " : "") + + Path.GetFileName(_openedFiles.Filename); } private void LuaListView_QueryItemImage(int index, RollColumn column, ref Bitmap bitmap, ref int offsetX, ref int offsetY) @@ -429,12 +421,12 @@ private void LuaListView_QueryItemImage(int index, RollColumn column, ref Bitmap return; } - if (LuaImp.ScriptList[index].IsSeparator) + if (_openedFiles[index].IsSeparator) { return; } - bitmap = LuaImp.ScriptList[index].State switch + bitmap = _openedFiles[index].State switch { LuaFile.RunState.Running => Resources.ts_h_arrow_green, LuaFile.RunState.Paused => Resources.Pause, @@ -444,7 +436,7 @@ private void LuaListView_QueryItemImage(int index, RollColumn column, ref Bitmap private void LuaListView_QueryItemBkColor(int index, RollColumn column, ref Color color) { - var lf = LuaImp.ScriptList[index]; + var lf = _openedFiles[index]; if (lf.IsSeparator) color = BackColor; else if (lf.Paused) color = Color.LightPink; else if (lf.Enabled) color = Color.LightCyan; @@ -454,18 +446,18 @@ private void LuaListView_QueryItemText(int index, RollColumn column, out string { text = ""; - if (LuaImp.ScriptList[index].IsSeparator) + if (_openedFiles[index].IsSeparator) { return; } if (column.Name == ScriptColumnName) { - text = Path.GetFileNameWithoutExtension(LuaImp.ScriptList[index].Path); // TODO: how about allow the user to name scripts? + text = Path.GetFileNameWithoutExtension(_openedFiles[index].Path); // TODO: how about allow the user to name scripts? } else if (column.Name == PathColumnName) { - text = DressUpRelative(LuaImp.ScriptList[index].Path); + text = DressUpRelative(_openedFiles[index].Path); } } @@ -477,9 +469,9 @@ private string DressUpRelative(string path) private void UpdateNumberOfScripts() { var message = ""; - var total = LuaImp.ScriptList.Count(file => !file.IsSeparator); - var active = LuaImp.ScriptList.Count(file => !file.IsSeparator && file.Enabled); - var paused = LuaImp.ScriptList.Count(static lf => !lf.IsSeparator && lf.Paused); + var total = _openedFiles.Count(file => !file.IsSeparator); + var active = _openedFiles.Count(file => !file.IsSeparator && file.Enabled); + var paused = _openedFiles.Count(static lf => !lf.IsSeparator && lf.Paused); if (total == 1) { @@ -534,17 +526,25 @@ public void ClearOutputWindow() }); } + private void SyncScriptList() + { + LuaImp.ScriptList.Clear(); + LuaImp.ScriptList.Add(_nonFile); + LuaImp.ScriptList.AddRange(_openedFiles.Where(static lf => !lf.IsSeparator)); + } + public bool LoadLuaSession(string path) { RemoveAllLuaFiles(); - var result = LuaImp.ScriptList.Load(path, Settings.DisableLuaScriptsOnLoad); + var result = _openedFiles.Load(path, Settings.DisableLuaScriptsOnLoad, UpdateRegisteredFunctionsDialog); + SyncScriptList(); - foreach (var script in LuaImp.ScriptList) + foreach (var script in _openedFiles) { if (!script.IsSeparator) { - if (script.Enabled) + if (script.State == LuaFile.RunState.AwaitingStart) { EnableLuaFile(script); } @@ -553,7 +553,7 @@ public bool LoadLuaSession(string path) } } - LuaImp.ScriptList.Changes = false; + _openedFiles.Changes = false; Config.RecentLuaSession.Add(path); UpdateDialog(); AddFileWatches(); @@ -627,65 +627,27 @@ private void ResetDrawSurfacePadding() /// should frame waiters be waken up? only use this immediately before a frame of emulation public void ResumeScripts(bool includeFrameWaiters) { - if (LuaImp.ScriptList.Count is 0 - || LuaImp.IsUpdateSupressed - || (MainForm.IsTurboing && !Config.RunLuaDuringTurbo)) + if (MainForm.IsTurboing && !Config.RunLuaDuringTurbo) { return; } - foreach (var lf in LuaImp.ScriptList.Where(static lf => lf.State is LuaFile.RunState.Running && lf.Thread is not null)) + bool anyStopped = LuaImp.ResumeScripts(includeFrameWaiters); + if (anyStopped) { - try - { - LuaSandbox.Sandbox(lf.Thread, () => - { - var prohibit = lf.FrameWaiting && !includeFrameWaiters; - if (!prohibit) - { - var (waitForFrame, terminated) = LuaImp.ResumeScript(lf); - if (terminated) - { - LuaImp.CallExitEvent(lf); - lf.Stop(); - DetachRegisteredFunctions(lf); - UpdateDialog(); - } - - lf.FrameWaiting = waitForFrame; - } - }, () => - { - lf.Stop(); - DetachRegisteredFunctions(lf); - LuaListView.Refresh(); - }); - } - catch (Exception ex) - { - DialogController.ShowMessageBox(ex.ToString()); - } + UpdateDialog(); } _messageCount = 0; } - private void DetachRegisteredFunctions(LuaFile lf) - { - foreach (var nlf in LuaImp.RegisteredFunctions - .Where(f => f.LuaFile == lf)) - { - nlf.DetachFromScript(); - } - } - private FileInfo GetSaveFileFromUser() { string initDir; string initFileName; - if (!string.IsNullOrWhiteSpace(LuaImp.ScriptList.Filename)) + if (!string.IsNullOrWhiteSpace(_openedFiles.Filename)) { - (initDir, initFileName, _) = LuaImp.ScriptList.Filename.SplitPathToDirFileAndExt(); + (initDir, initFileName, _) = _openedFiles.Filename.SplitPathToDirFileAndExt(); } else { @@ -705,7 +667,7 @@ private FileWriteResult SaveSessionAs() var file = GetSaveFileFromUser(); if (file != null) { - FileWriteResult saveResult = LuaImp.ScriptList.Save(file.FullName); + FileWriteResult saveResult = _openedFiles.Save(file.FullName); if (saveResult.IsError) { this.ErrorMessageBox(saveResult); @@ -724,7 +686,7 @@ private FileWriteResult SaveSessionAs() private void LoadSessionFromRecent(string path) { var load = true; - if (LuaImp.ScriptList.Changes) + if (_openedFiles.Changes) { load = AskSaveChanges(); } @@ -740,7 +702,7 @@ private void LoadSessionFromRecent(string path) public override bool AskSaveChanges() { - if (!LuaImp.ScriptList.Changes || string.IsNullOrEmpty(LuaImp.ScriptList.Filename)) return true; + if (!_openedFiles.Changes || string.IsNullOrEmpty(_openedFiles.Filename)) return true; var result = DialogController.DoWithTempMute(() => this.ModalMessageBox3( caption: "Closing with Unsaved Changes", icon: EMsgBoxIcon.Question, @@ -751,7 +713,7 @@ public override bool AskSaveChanges() TryAgainResult saveResult = this.DoWithTryAgainBox(SaveOrSaveAs, "Failed to save Lua session."); return saveResult != TryAgainResult.Canceled; } - else LuaImp.ScriptList.Changes = false; + else _openedFiles.Changes = false; return true; } @@ -761,18 +723,18 @@ private void UpdateRegisteredFunctionsDialog() foreach (var form in Application.OpenForms.OfType().ToList()) { - form.UpdateValues(LuaImp.RegisteredFunctions); + form.UpdateValues(LuaImp.ScriptList); } } private FileWriteResult SaveOrSaveAs() { - if (!string.IsNullOrWhiteSpace(LuaImp.ScriptList.Filename)) + if (!string.IsNullOrWhiteSpace(_openedFiles.Filename)) { - FileWriteResult result = LuaImp.ScriptList.Save(LuaImp.ScriptList.Filename); + FileWriteResult result = _openedFiles.Save(_openedFiles.Filename); if (!result.IsError) { - Config.RecentLuaSession.Add(LuaImp.ScriptList.Filename); + Config.RecentLuaSession.Add(_openedFiles.Filename); } return result; } @@ -784,7 +746,7 @@ private FileWriteResult SaveOrSaveAs() private void FileSubMenu_DropDownOpened(object sender, EventArgs e) { - SaveSessionMenuItem.Enabled = LuaImp.ScriptList.Changes; + SaveSessionMenuItem.Enabled = _openedFiles.Changes; } private void RecentSessionsSubMenu_DropDownOpened(object sender, EventArgs e) @@ -795,12 +757,12 @@ private void RecentScriptsSubMenu_DropDownOpened(object sender, EventArgs e) private void NewSessionMenuItem_Click(object sender, EventArgs e) { - var result = !LuaImp.ScriptList.Changes || AskSaveChanges(); + var result = !_openedFiles.Changes || AskSaveChanges(); if (result) { RemoveAllLuaFiles(); - LuaImp.ScriptList.Clear(); + _nonFile.Functions.Clear(); ClearOutputWindow(); UpdateDialog(); } @@ -819,17 +781,17 @@ private void OpenSessionMenuItem_Click(object sender, EventArgs e) private void SaveSessionMenuItem_Click(object sender, EventArgs e) { - if (LuaImp.ScriptList.Changes) + if (_openedFiles.Changes) { FileWriteResult result = SaveOrSaveAs(); if (result.IsError) { this.ErrorMessageBox(result, "Failed to save Lua session."); - OutputMessages.Text = $"Failed to save {Path.GetFileName(LuaImp.ScriptList.Filename)}"; + OutputMessages.Text = $"Failed to save {Path.GetFileName(_openedFiles.Filename)}"; } else { - OutputMessages.Text = $"{Path.GetFileName(LuaImp.ScriptList.Filename)} saved."; + OutputMessages.Text = $"{Path.GetFileName(_openedFiles.Filename)} saved."; } } } @@ -852,9 +814,9 @@ private void ScriptSubMenu_DropDownOpened(object sender, EventArgs e) MoveDownMenuItem.Enabled = LuaListView.AnyRowsSelected; - SelectAllMenuItem.Enabled = LuaImp.ScriptList.Count is not 0; - StopAllScriptsMenuItem.Enabled = LuaImp.ScriptList.Any(script => script.Enabled); - RegisteredFunctionsMenuItem.Enabled = LuaImp.RegisteredFunctions.Count is not 0; + SelectAllMenuItem.Enabled = _openedFiles.Count is not 0; + StopAllScriptsMenuItem.Enabled = _openedFiles.Any(script => script.Enabled); + RegisteredFunctionsMenuItem.Enabled = true; } private void NewScriptMenuItem_Click(object sender, EventArgs e) @@ -862,9 +824,9 @@ private void NewScriptMenuItem_Click(object sender, EventArgs e) var luaDir = Config!.PathEntries.LuaAbsolutePath(); string initDir; string ext; - if (!string.IsNullOrWhiteSpace(LuaImp.ScriptList.Filename)) + if (!string.IsNullOrWhiteSpace(_openedFiles.Filename)) { - (initDir, ext, _) = LuaImp.ScriptList.Filename.SplitPathToDirFileAndExt(); + (initDir, ext, _) = _openedFiles.Filename.SplitPathToDirFileAndExt(); } else { @@ -897,15 +859,16 @@ private void NewScriptMenuItem_Click(object sender, EventArgs e) } } File.Copy(sourceFileName: templatePath, destFileName: result, overwrite: true); - LuaImp.ScriptList.Add(new LuaFile(Path.GetFileNameWithoutExtension(result), result)); - Config!.RecentLua.Add(result); - UpdateDialog(); Process.Start(new ProcessStartInfo { Verb = "Open", FileName = result, }); - AddFileWatches(); + + bool temp = Settings.DisableLuaScriptsOnLoad; + Settings.DisableLuaScriptsOnLoad = true; // don't start the new empty file + LoadLuaFile(result); + Settings.DisableLuaScriptsOnLoad = temp; } private void OpenScriptMenuItem_Click(object sender, EventArgs e) @@ -929,7 +892,7 @@ private void OpenScriptMenuItem_Click(object sender, EventArgs e) private void ToggleScriptMenuItem_Click(object sender, EventArgs e) { var files = !SelectedFiles.Any() && Settings.ToggleAllIfNoneSelected - ? LuaImp.ScriptList + ? _openedFiles : SelectedFiles; foreach (var file in files) { @@ -943,20 +906,11 @@ private void EnableLuaFile(LuaFile item) { try { - LuaSandbox.Sandbox(null, () => - { - LuaImp.SpawnAndSetFileThread(item.Path, item); - LuaSandbox.CreateSandbox(item.Thread, Path.GetDirectoryName(item.Path)); - }, () => - { - item.State = LuaFile.RunState.Disabled; - }); - - // there used to be a call here which did a redraw of the Gui/OSD, which included a call to `Tools.UpdateToolsAfter` --yoshi + item.Start(LuaImp.SpawnCoroutineAndSandbox(item.Path)); } catch (IOException) { - item.State = LuaFile.RunState.Disabled; + item.Stop(); WriteLine($"Unable to access file {item.Path}"); } catch (Exception ex) @@ -1001,7 +955,7 @@ private void RemoveScriptMenuItem_Click(object sender, EventArgs e) DisplayManager.ClearApiHawkSurfaces(); DisplayManager.ClearApiHawkTextureCache(); DisplayManager.OSD.ClearGuiText(); - if (!LuaImp.ScriptList.Any(static lf => !lf.IsSeparator)) ResetDrawSurfacePadding(); // just removed last script, reset padding + if (!_openedFiles.Any(static lf => !lf.IsSeparator)) ResetDrawSurfacePadding(); // just removed last script, reset padding } } @@ -1013,7 +967,7 @@ private void DuplicateScriptMenuItem_Click(object sender, EventArgs e) if (script.IsSeparator) { - LuaImp.ScriptList.Add(LuaFile.SeparatorInstance); + _openedFiles.Add(LuaFile.SeparatorInstance); UpdateDialog(); return; } @@ -1027,14 +981,16 @@ private void DuplicateScriptMenuItem_Click(object sender, EventArgs e) if (result is null) return; string text = File.ReadAllText(script.Path); File.WriteAllText(result, text); - LuaImp.ScriptList.Add(new LuaFile(Path.GetFileNameWithoutExtension(result), result)); - Config!.RecentLua.Add(result); - UpdateDialog(); Process.Start(new ProcessStartInfo { Verb = "Open", FileName = result, }); + + bool temp = Settings.DisableLuaScriptsOnLoad; + Settings.DisableLuaScriptsOnLoad = true; // don't start the new file + LoadLuaFile(result); + Settings.DisableLuaScriptsOnLoad = temp; } } @@ -1045,7 +1001,7 @@ private void ClearConsoleMenuItem_Click(object sender, EventArgs e) private void InsertSeparatorMenuItem_Click(object sender, EventArgs e) { - LuaImp.ScriptList.Insert(LuaListView.SelectionStartIndex ?? LuaImp.ScriptList.Count, LuaFile.SeparatorInstance); + _openedFiles.Insert(LuaListView.SelectionStartIndex ?? _openedFiles.Count, LuaFile.SeparatorInstance); UpdateDialog(); } @@ -1059,10 +1015,11 @@ private void MoveUpMenuItem_Click(object sender, EventArgs e) foreach (var index in indices) { - var file = LuaImp.ScriptList[index]; - LuaImp.ScriptList.Remove(file); - LuaImp.ScriptList.Insert(index - 1, file); + var file = _openedFiles[index]; + _openedFiles.Remove(file); + _openedFiles.Insert(index - 1, file); } + SyncScriptList(); var newIndices = indices.Select(t => t - 1); @@ -1079,17 +1036,18 @@ private void MoveDownMenuItem_Click(object sender, EventArgs e) { var indices = LuaListView.SelectedRows.ToList(); if (indices.Count == 0 - || indices[indices.Count - 1] == LuaImp.ScriptList.Count - 1) // at end already + || indices[indices.Count - 1] == _openedFiles.Count - 1) // at end already { return; } for (var i = indices.Count - 1; i >= 0; i--) { - var file = LuaImp.ScriptList[indices[i]]; - LuaImp.ScriptList.Remove(file); - LuaImp.ScriptList.Insert(indices[i] + 1, file); + var file = _openedFiles[indices[i]]; + _openedFiles.Remove(file); + _openedFiles.Insert(indices[i] + 1, file); } + SyncScriptList(); var newIndices = indices.Select(t => t + 1); @@ -1107,34 +1065,31 @@ private void SelectAllMenuItem_Click(object sender, EventArgs e) private void StopAllScriptsMenuItem_Click(object sender, EventArgs e) { - foreach (var file in LuaImp.ScriptList) + foreach (var file in _openedFiles) { - DisableLuaScript(file); + file.Stop(); } UpdateDialog(); } private void RegisteredFunctionsMenuItem_Click(object sender, EventArgs e) { - if (LuaImp.RegisteredFunctions.Count is not 0) + var alreadyOpen = false; + foreach (Form form in Application.OpenForms) { - var alreadyOpen = false; - foreach (Form form in Application.OpenForms) + if (form is LuaRegisteredFunctionsList) { - if (form is LuaRegisteredFunctionsList) - { - alreadyOpen = true; - form.Activate(); - } + alreadyOpen = true; + form.Activate(); } + } - if (!alreadyOpen) + if (!alreadyOpen) + { + new LuaRegisteredFunctionsList(LuaImp.ScriptList) { - new LuaRegisteredFunctionsList(LuaImp.RegisteredFunctions) - { - StartLocation = this.ChildPointToScreen(LuaListView), - }.Show(); - } + StartLocation = this.ChildPointToScreen(LuaListView), + }.Show(); } } @@ -1245,13 +1200,13 @@ private void ScriptListContextMenu_Opening(object sender, CancelEventArgs e) ScriptContextSeparator.Visible = LuaImp.ScriptList.Exists(file => file.Enabled); - ClearRegisteredFunctionsContextItem.Enabled = LuaImp.RegisteredFunctions.Count is not 0; + ClearRegisteredFunctionsContextItem.Enabled = LuaImp.ScriptList.Exists(lf => lf.Functions.Count != 0); } private void ConsoleContextMenu_Opening(object sender, CancelEventArgs e) { RegisteredFunctionsContextItem.Enabled = ClearRegisteredFunctionsLogContextItem.Enabled - = LuaImp.RegisteredFunctions.Count is not 0; + = LuaImp.ScriptList.Exists(lf => lf.Functions.Count != 0); CopyContextItem.Enabled = OutputBox.SelectedText.Length is not 0; ClearConsoleContextItem.Enabled = SelectAllContextItem.Enabled = OutputBox.Text.Length is not 0; } @@ -1284,7 +1239,10 @@ private void CopyContextItem_Click(object sender, EventArgs e) } private void ClearRegisteredFunctionsContextMenuItem_Click(object sender, EventArgs e) - => LuaImp.RegisteredFunctions.Clear(); + { + foreach (LuaFile lf in LuaImp.ScriptList) + lf.Functions.Clear(); + } public bool LoadByFileExtension(string path, out bool abort) { @@ -1363,45 +1321,34 @@ private void OutputBox_KeyDown(object sender, KeyEventArgs e) private void LuaListView_ColumnClick(object sender, InputRoll.ColumnClickEventArgs e) { var columnToSort = e.Column!.Name; - var luaListTemp = new List(); if (columnToSort != _lastColumnSorted) { _sortReverse = false; } - // For getting the name of the .lua file, for some reason this field is kept blank in LuaFile.cs? - // The Name variable gets emptied again near the end just in case it would break something. - for (var i = 0; i < LuaImp.ScriptList.Count; i++) - { - var words = Regex.Split(LuaImp.ScriptList[i].Path, ".lua"); - var split = words[0].Split(Path.DirectorySeparatorChar); - - luaListTemp.Add(LuaImp.ScriptList[i]); - luaListTemp[i].Name = split[split.Length - 1]; - } - // Script, Path + List luaListTemp; switch (columnToSort) { case "Script": - luaListTemp = luaListTemp - .OrderBy(lf => lf.Name, _sortReverse) + luaListTemp = _openedFiles + .OrderBy(lf => Path.GetFileNameWithoutExtension(lf.Path), _sortReverse) .ThenBy(lf => lf.Path) .ToList(); break; - case "PathName": - luaListTemp = luaListTemp + default: // case "PathName": + luaListTemp = _openedFiles .OrderBy(lf => lf.Path, _sortReverse) - .ThenBy(lf => lf.Name) + .ThenBy(lf => Path.GetFileNameWithoutExtension(lf.Path)) .ToList(); break; } - for (var i = 0; i < LuaImp.ScriptList.Count; i++) + for (var i = 0; i < _openedFiles.Count; i++) { - LuaImp.ScriptList[i] = luaListTemp[i]; - LuaImp.ScriptList[i].Name = ""; + _openedFiles[i] = luaListTemp[i]; } + SyncScriptList(); UpdateDialog(); _lastColumnSorted = columnToSort; @@ -1411,7 +1358,7 @@ private void LuaListView_ColumnClick(object sender, InputRoll.ColumnClickEventAr private void RefreshScriptMenuItem_Click(object sender, EventArgs e) { var files = !SelectedFiles.Any() && Settings.ToggleAllIfNoneSelected - ? LuaImp.ScriptList + ? _openedFiles : SelectedFiles; foreach (var file in files) RefreshLuaScript(file); UpdateDialog(); @@ -1433,7 +1380,7 @@ private void InputBox_KeyDown(object sender, KeyEventArgs e) return; } - LuaSandbox.Sandbox(null, () => + LuaImp.Sandbox(_nonFile, () => { var prevMessageCount = _messageCount; var results = LuaImp.ExecuteString(rawCommand); @@ -1442,7 +1389,7 @@ private void InputBox_KeyDown(object sender, KeyEventArgs e) // if output didn't change, Print will take care of writing out "(no return)" if (results is not ([ ] or [ null ]) || _messageCount == prevMessageCount) { - LuaLibraries.Print(results); + _consoleLib.Log(results); } }); @@ -1519,9 +1466,9 @@ protected void DragEnterWrapper(object sender, DragEventArgs e) private void LuaListView_DoubleClick(object sender, EventArgs e) { var index = LuaListView.CurrentCell?.RowIndex; - if (index < LuaImp.ScriptList.Count) + if (index < _openedFiles.Count) { - var file = LuaImp.ScriptList[index.Value]; + var file = _openedFiles[index.Value]; ToggleLuaScript(file); UpdateDialog(); } @@ -1534,36 +1481,19 @@ private void ToggleLuaScript(LuaFile file) return; } - file.Toggle(); _lastScriptUsed = file; - if (file.Enabled && file.Thread is null) + if (file.Enabled) { - LuaImp.RegisteredFunctions.RemoveForFile(file); // First remove any existing registered functions for this file - EnableLuaFile(file); + file.Stop(); } - else if (!file.Enabled && file.Thread is not null) + else { - DisableLuaScript(file); - // there used to be a call here which did a redraw of the Gui/OSD, which included a call to `Tools.UpdateToolsAfter` --yoshi + EnableLuaFile(file); } LuaListView.Refresh(); } - private void DisableLuaScript(LuaFile file) - { - if (file.IsSeparator) return; - - file.State = LuaFile.RunState.Disabled; - - if (file.Thread is not null) - { - LuaImp.CallExitEvent(file); - LuaImp.RegisteredFunctions.RemoveForFile(file); - file.Stop(); - } - } - private void RefreshLuaScript(LuaFile file) { ToggleLuaScript(file); diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaLibraries.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaLibraries.cs deleted file mode 100644 index 93e4ea1af05..00000000000 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaLibraries.cs +++ /dev/null @@ -1,405 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; - -using NLua; -using NLua.Native; - -using BizHawk.Client.Common; -using BizHawk.Common; -using BizHawk.Common.StringExtensions; -using BizHawk.Emulation.Common; - -namespace BizHawk.Client.EmuHawk -{ - public class LuaLibraries : ILuaLibraries - { - public static readonly bool IsAvailable = LuaNativeMethodLoader.EnsureNativeMethodsLoaded(); - - public LuaLibraries( - LuaFileList scriptList, - LuaFunctionList registeredFuncList, - IEmulatorServiceProvider serviceProvider, - IMainFormForApi mainForm, - Config config, - ToolManager toolManager, - IDialogParent dialogParent, - ApiContainer apiContainer) - { - if (!IsAvailable) - { - throw new InvalidOperationException("The Lua dynamic library was not able to be loaded"); - } - - void EnumerateLuaFunctions(string name, Type type, LuaLibraryBase instance) - { - var libraryDesc = type.GetCustomAttributes(typeof(DescriptionAttribute), false).Cast() - .Select(static descAttr => descAttr.Description) - .FirstOrDefault() ?? string.Empty; - if (instance != null) _lua.NewTable(name); - foreach (var method in type.GetMethods()) - { - var foundAttrs = method.GetCustomAttributes(typeof(LuaMethodAttribute), false); - if (foundAttrs.Length == 0) continue; - if (instance != null) _lua.RegisterFunction($"{name}.{((LuaMethodAttribute)foundAttrs[0]).Name}", instance, method); - LibraryFunction libFunc = new( - library: name, - libraryDescription: libraryDesc, - method, - suggestInREPL: instance != null - ); - Docs.Add(libFunc); - } - } - - _th = new NLuaTableHelper(_lua, LogToLuaConsole); - _mainForm = mainForm; - LuaWait = new AutoResetEvent(false); - PathEntries = config.PathEntries; - RegisteredFunctions = registeredFuncList; - ScriptList = scriptList; - Docs.Clear(); - _apiContainer = apiContainer; - - // Register lua libraries - foreach (var lib in ReflectionCache_Biz_Cli_Com.Types.Concat(ReflectionCache.Types) - .Where(static t => typeof(LuaLibraryBase).IsAssignableFrom(t) && t.IsSealed)) - { - if (VersionInfo.DeveloperBuild - || lib.GetCustomAttribute(inherit: false)?.Released is not false) - { - if (!ServiceInjector.IsAvailable(serviceProvider, lib)) - { - Util.DebugWriteLine($"couldn't instantiate {lib.Name}, adding to docs only"); - EnumerateLuaFunctions( - lib.Name.RemoveSuffix("LuaLibrary").ToLowerInvariant(), // why tf aren't we doing this for all of them? or grabbing it from an attribute? - lib, - instance: null); - continue; - } - - var instance = (LuaLibraryBase)Activator.CreateInstance(lib, this, _apiContainer, (Action)LogToLuaConsole); - if (!ServiceInjector.UpdateServices(serviceProvider, instance, mayCache: true)) throw new Exception("Lua lib has required service(s) that can't be fulfilled"); - - if (instance is ClientLuaLibrary clientLib) - { - clientLib.MainForm = _mainForm; - } - else if (instance is ConsoleLuaLibrary consoleLib) - { - consoleLib.AllAPINames = new(() => string.Join("\n", Docs.Select(static lf => lf.Name)) + "\n"); // Docs may not be fully populated now, depending on order of ReflectionCache.Types, but definitely will be when this is read - consoleLib.Tools = toolManager; - _logToLuaConsoleCallback = consoleLib.Log; - } - else if (instance is DoomLuaLibrary doomLib) - { - doomLib.CreateAndRegisterNamedFunction = CreateAndRegisterNamedFunction; - } - else if (instance is EventsLuaLibrary eventsLib) - { - eventsLib.CreateAndRegisterNamedFunction = CreateAndRegisterNamedFunction; - eventsLib.RemoveNamedFunctionMatching = RemoveNamedFunctionMatching; - } - else if (instance is FormsLuaLibrary formsLib) - { - - formsLib.OwnerForm = dialogParent; - } - else if (instance is GuiLuaLibrary guiLib) - { - // emu lib may be null now, depending on order of ReflectionCache.Types, but definitely won't be null when this is called - guiLib.CreateLuaCanvasCallback = (width, height, x, y) => - { - var canvas = new LuaCanvas(EmulationLuaLibrary, width, height, x, y, _th, LogToLuaConsole); - canvas.Show(); - return _th.ObjectToTable(canvas); - }; - } - else if (instance is TAStudioLuaLibrary tastudioLib) - { - tastudioLib.Tools = toolManager; - } - - EnumerateLuaFunctions(instance.Name, lib, instance); - Libraries.Add(lib, instance); - } - } - - _lua.RegisterFunction("print", this, typeof(LuaLibraries).GetMethod(nameof(Print))); - - var packageTable = (LuaTable) _lua["package"]; - var luaPath = PathEntries.LuaAbsolutePath(); - if (OSTailoredCode.IsUnixHost) - { - // add %exe%/Lua to library resolution pathset (LUA_PATH) - // this is done already on windows, but not on linux it seems? - packageTable["path"] = $"{luaPath}/?.lua;{luaPath}?/init.lua;{packageTable["path"]}"; - // we need to modifiy the cpath so it looks at our lua dir too, and remove the relative pathing - // we do this on Windows too, but keep in mind Linux uses .so and Windows use .dll - // TODO: Does the relative pathing issue Windows has also affect Linux? I'd assume so... - packageTable["cpath"] = $"{luaPath}/?.so;{luaPath}/loadall.so;{packageTable["cpath"]}"; - packageTable["cpath"] = ((string)packageTable["cpath"]).Replace(";./?.so", ""); - } - else - { - packageTable["cpath"] = $"{luaPath}\\?.dll;{luaPath}\\loadall.dll;{packageTable["cpath"]}"; - packageTable["cpath"] = ((string)packageTable["cpath"]).Replace(";.\\?.dll", ""); - } - - EmulationLuaLibrary.FrameAdvanceCallback = FrameAdvance; - EmulationLuaLibrary.YieldCallback = EmuYield; - - EnumerateLuaFunctions(nameof(LuaCanvas), typeof(LuaCanvas), null); // add LuaCanvas to Lua function reference table - } - - private ApiContainer _apiContainer; - - private GuiApi GuiAPI => (GuiApi)_apiContainer.Gui; - - private readonly IMainFormForApi _mainForm; - - private Lua _lua = new(); - private LuaThread _currThread; - - private readonly NLuaTableHelper _th; - - private static Action _logToLuaConsoleCallback = a => Console.WriteLine("a Lua lib is logging during init and the console lib hasn't been initialised yet"); - - private FormsLuaLibrary FormsLibrary => (FormsLuaLibrary)Libraries[typeof(FormsLuaLibrary)]; - - public LuaDocumentation Docs { get; } = new LuaDocumentation(); - - private EmulationLuaLibrary EmulationLuaLibrary => (EmulationLuaLibrary)Libraries[typeof(EmulationLuaLibrary)]; - - public bool IsRebootingCore { get; set; } - - public bool IsUpdateSupressed { get; set; } - - public bool IsInInputOrMemoryCallback { get; set; } - - private readonly IDictionary Libraries = new Dictionary(); - - private EventWaitHandle LuaWait; - - public PathEntryCollection PathEntries { get; private set; } - - public LuaFileList ScriptList { get; } - - private static void LogToLuaConsole(object outputs) => _logToLuaConsoleCallback(new[] { outputs }); - - public NLuaTableHelper GetTableHelper() => _th; - - public void Restart( - IEmulatorServiceProvider newServiceProvider, - Config config, - ApiContainer apiContainer) - { - _apiContainer = apiContainer; - PathEntries = config.PathEntries; - foreach (var lib in Libraries.Values) - { - lib.APIs = _apiContainer; - if (!ServiceInjector.UpdateServices(newServiceProvider, lib, mayCache: true)) - { - throw new Exception("Lua lib has required service(s) that can't be fulfilled"); - } - - lib.Restarted(); - } - } - - public bool FrameAdvanceRequested { get; private set; } - - public LuaFunctionList RegisteredFunctions { get; } - - public void CallSaveStateEvent(string name) - { - try - { - foreach (var lf in RegisteredFunctions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_SAVESTATE).ToList()) - { - lf.Call(name); - } - } - catch (Exception e) - { - LogToLuaConsole($"error running function attached by lua function event.onsavestate\nError message: {e.Message}"); - } - } - - public void CallLoadStateEvent(string name) - { - try - { - foreach (var lf in RegisteredFunctions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_LOADSTATE).ToList()) - { - lf.Call(name); - } - } - catch (Exception e) - { - LogToLuaConsole($"error running function attached by lua function event.onloadstate\nError message: {e.Message}"); - } - } - - public void CallFrameBeforeEvent() - { - if (IsUpdateSupressed) return; - - try - { - foreach (var lf in RegisteredFunctions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_PREFRAME).ToList()) - { - lf.Call(); - } - } - catch (Exception e) - { - LogToLuaConsole($"error running function attached by lua function event.onframestart\nError message: {e.Message}"); - } - } - - public void CallFrameAfterEvent() - { - if (IsUpdateSupressed) return; - - try - { - foreach (var lf in RegisteredFunctions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_POSTFRAME).ToList()) - { - lf.Call(); - } - } - catch (Exception e) - { - LogToLuaConsole($"error running function attached by lua function event.onframeend\nError message: {e.Message}"); - } - } - - public void CallExitEvent(LuaFile lf) - { - foreach (var exitCallback in RegisteredFunctions - .Where(l => l.Event == NamedLuaFunction.EVENT_TYPE_ENGINESTOP - && (l.LuaFile.Path == lf.Path || ReferenceEquals(l.LuaFile.Thread, lf.Thread))) - .ToList()) - { - exitCallback.Call(); - } - } - - public void Close() - { - foreach (var closeCallback in RegisteredFunctions - .Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_CONSOLECLOSE) - .ToList()) - { - closeCallback.Call(); - } - - RegisteredFunctions.Clear(); - ScriptList.Clear(); - FormsLibrary.DestroyAll(); - _lua.Dispose(); - _lua = null; - } - - private INamedLuaFunction CreateAndRegisterNamedFunction( - LuaFunction function, - string theEvent, - Action logCallback, - LuaFile luaFile, - string name = null) - { - var nlf = new NamedLuaFunction(function, theEvent, logCallback, luaFile, () => _lua.NewThread(), this, name); - RegisteredFunctions.Add(nlf); - return nlf; - } - - private bool RemoveNamedFunctionMatching(Func predicate) - { - if (RegisteredFunctions.FirstOrDefault(predicate) is not NamedLuaFunction nlf) return false; - RegisteredFunctions.Remove(nlf); - return true; - } - - public LuaThread SpawnCoroutine(string file) - { - var content = File.ReadAllText(file); - var main = _lua.LoadString(content, "main"); - return _lua.NewThread(main); - } - - public void SpawnAndSetFileThread(string pathToLoad, LuaFile lf) - => lf.Thread = SpawnCoroutine(pathToLoad); - - public object[] ExecuteString(string command) - { - const string ChunkName = "input"; // shows up in error messages - - // Use LoadString to separate parsing and execution, to tell syntax errors and runtime errors apart - LuaFunction func; - try - { - // Adding a return is necessary to get out return values of functions and turn expressions ("1+1" etc.) into valid statements - func = _lua.LoadString($"return {command}", ChunkName); - } - catch (Exception) - { - // command may be a valid statement without the added "return" - // if previous attempt couldn't be parsed, run the raw command - return _lua.DoString(command, ChunkName); - } - - using (func) - { - return func.Call(); - } - } - - public (bool WaitForFrame, bool Terminated) ResumeScript(LuaFile lf) - { - _currThread = lf.Thread; - - try - { - LuaLibraryBase.SetCurrentThread(lf); - - var execResult = _currThread.Resume(); - - _currThread = null; - var result = execResult switch - { - LuaStatus.OK => (WaitForFrame: false, Terminated: true), - LuaStatus.Yield => (WaitForFrame: FrameAdvanceRequested, Terminated: false), - _ => throw new InvalidOperationException($"{nameof(_currThread.Resume)}() returned {execResult}?"), - }; - - FrameAdvanceRequested = false; - return result; - } - finally - { - LuaLibraryBase.ClearCurrentThread(); - } - } - - public static void Print(params object[] outputs) - { - _logToLuaConsoleCallback(outputs); - } - - private void FrameAdvance() - { - FrameAdvanceRequested = true; - _currThread.Yield(); - } - - private void EmuYield() - { - _currThread.Yield(); - } - } -} diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaRegisteredFunctionsList.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaRegisteredFunctionsList.cs index 34672a4f743..af651270722 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaRegisteredFunctionsList.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaRegisteredFunctionsList.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; @@ -7,20 +8,25 @@ namespace BizHawk.Client.EmuHawk { public partial class LuaRegisteredFunctionsList : Form { - private LuaFunctionList _registeredFunctions; + private List _scriptList; - public LuaRegisteredFunctionsList(LuaFunctionList registeredFunctions) + private IEnumerable AllFunctions { - _registeredFunctions = registeredFunctions; + get => _scriptList.SelectMany(lf => lf.Functions); + } + + public LuaRegisteredFunctionsList(List scripts) + { + _scriptList = scripts; InitializeComponent(); Icon = Properties.Resources.TextDocIcon; } public Point StartLocation { get; set; } = new Point(0, 0); - public void UpdateValues(LuaFunctionList registeredFunctions) + public void UpdateValues(List scripts) { - _registeredFunctions = registeredFunctions; + _scriptList = scripts; PopulateListView(); } @@ -43,7 +49,7 @@ private void PopulateListView() { FunctionView.Items.Clear(); - var functions = _registeredFunctions + var functions = AllFunctions .OrderBy(f => f.Event) .ThenBy(f => f.Name); foreach (var nlf in functions) @@ -75,7 +81,8 @@ private void CallFunction() foreach (int index in indices) { var guid = FunctionView.Items[index].SubItems[2].Text; - _registeredFunctions[guid].Call(); + Guid.TryParseExact(guid, format: "D", out var parsed); + AllFunctions.First(nlf => nlf.Guid == parsed).Call(); } } } @@ -88,8 +95,13 @@ private void RemoveFunctionButton() foreach (int index in indices) { var guid = FunctionView.Items[index].SubItems[2].Text; - var nlf = _registeredFunctions[guid]; - _registeredFunctions.Remove(nlf); + Guid.TryParseExact(guid, format: "D", out var parsed); + foreach (LuaFile file in _scriptList) + { + var nlf = AllFunctions.FirstOrDefault(nlf => nlf.Guid == parsed); + if (nlf is not null) + file.Functions.Remove(nlf); + } } PopulateListView(); @@ -108,7 +120,8 @@ private void FunctionView_DoubleClick(object sender, EventArgs e) private void RemoveAllBtn_Click(object sender, EventArgs e) { - _registeredFunctions.Clear(); + foreach (LuaFile file in _scriptList) + file.Functions.Clear(); PopulateListView(); } @@ -117,7 +130,7 @@ private void DoButtonsStatus() var indexes = FunctionView.SelectedIndices; CallButton.Enabled = indexes.Count > 0; RemoveButton.Enabled = indexes.Count > 0; - RemoveAllBtn.Enabled = _registeredFunctions.Count is not 0; + RemoveAllBtn.Enabled = AllFunctions.Any(); } private void FunctionView_KeyDown(object sender, KeyEventArgs e) diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaWinform.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaWinform.Designer.cs index 9a8f3509986..f1f7486b5db 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaWinform.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaWinform.Designer.cs @@ -13,9 +13,11 @@ partial class LuaWinform /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { - if (disposing && (components != null)) + if (disposing) { - components.Dispose(); + // Should we be calling the Lua close callback? Debatable, but we must call the closing event so our list of active forms can be updated. + this.OnClosing(new()); + components?.Dispose(); } base.Dispose(disposing); } diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaWinform.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaWinform.cs index e8052cb9adf..bfd29d4729b 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaWinform.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaWinform.cs @@ -5,30 +5,47 @@ namespace BizHawk.Client.EmuHawk { - public partial class LuaWinform : Form + public partial class LuaWinform : Form, IKeepFileRunning { public List ControlEvents { get; } = new List(); - private readonly string _currentDirectory = Environment.CurrentDirectory; private readonly LuaFile _ownerFile; + private readonly ILuaLibraries _luaImp; + private readonly LuaFunction/*?*/ _closeCallback; + private bool _closed = false; public bool BlocksInputWhenFocused { get; set; } = true; - public LuaWinform(LuaFile ownerFile, Action formsWindowClosedCallback) + public LuaWinform(LuaFile ownerFile, ILuaLibraries luaLibraries, Action formsWindowClosedCallback, LuaFunction luaCloseCallback = null) { _ownerFile = ownerFile; + _luaImp = luaLibraries; + _closeCallback = luaCloseCallback; + InitializeComponent(); + Icon = Properties.Resources.TextDocIcon; StartPosition = FormStartPosition.CenterParent; - Closing += (o, e) => formsWindowClosedCallback(Handle); + + _ownerFile.AddDisposable(this); + Closing += (o, e) => + { + if (_closed) return; + _closed = true; + + formsWindowClosedCallback(Handle); // This one first. _closeCallback may end up disposing this instance. + if (_closeCallback != null) + { + _luaImp.Sandbox(_ownerFile, () => _ = _closeCallback.Call()); + } + _ownerFile.RemoveDisposable(this); + }; } public void DoLuaEvent(IntPtr handle) { - // re: https://github.com/TASEmulators/BizHawk/issues/1957 - `ownerFile` can be null if the script that generated the form ended, which will happen if the script does not have a `while true` loop - LuaSandbox.Sandbox(_ownerFile?.Thread, () => + _luaImp.Sandbox(_ownerFile, () => { - Environment.CurrentDirectory = _currentDirectory; foreach (LuaEvent luaEvent in ControlEvents) { if (luaEvent.Control == handle) diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Callbacks.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Callbacks.cs index 8c41e3972dc..f424ea17c71 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Callbacks.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Callbacks.cs @@ -1,13 +1,53 @@ +using System.Collections.Generic; using System.Drawing; namespace BizHawk.Client.EmuHawk { public partial class TAStudio { + public delegate Color? QueryColor(int index, string column); + public delegate string/*?*/ QueryText(int index, string column); + public delegate Bitmap/*?*/ QueryIcon(int index, string column); + // Everything here is currently for Lua - public Func QueryItemBgColorCallback { get; set; } - public Func QueryItemTextCallback { get; set; } - public Func QueryItemIconCallback { get; set; } + private List _queryColorCallbacks = new(); + public void AddQueryBgColorCallback(QueryColor query) => _queryColorCallbacks.Add(query); + public void RemoveQueryBgColorCallback(QueryColor query) => _queryColorCallbacks.Remove(query); + private Color? QueryItemBgColorCallback(int index, string column) + { + foreach (QueryColor q in _queryColorCallbacks) + { + Color? ret = q(index, column); + if (ret != null) return ret; + } + return null; + } + + private List _queryTextCallbacks = new(); + public void AddQueryItemTextCallback(QueryText query) => _queryTextCallbacks.Add(query); + public void RemoveQueryItemTextCallback(QueryText query) => _queryTextCallbacks.Remove(query); + private string/*?*/ QueryItemTextCallback(int index, string column) + { + foreach (QueryText q in _queryTextCallbacks) + { + string/*?*/ ret = q(index, column); + if (ret != null) return ret; + } + return null; + } + + private List _queryIconCallbacks = new(); + public void AddQueryItemIconCallback(QueryIcon query) => _queryIconCallbacks.Add(query); + public void RemoveQueryItemIconCallback(QueryIcon query) => _queryIconCallbacks.Remove(query); + private Bitmap/*?*/ QueryItemIconCallback(int index, string column) + { + foreach (QueryIcon q in _queryIconCallbacks) + { + Bitmap/*?*/ ret = q(index, column); + if (ret != null) return ret; + } + return null; + } public Action GreenzoneInvalidatedCallback { get; set; } public Action BranchLoadedCallback { get; set; } diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs index 5b56e68789d..f5a0941219e 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs @@ -146,7 +146,7 @@ private void TasView_QueryItemIcon(int index, RollColumn column, ref Bitmap bitm return; } - var overrideIcon = QueryItemIconCallback?.Invoke(index, column.Name); + var overrideIcon = QueryItemIconCallback(index, column.Name); if (overrideIcon != null) { @@ -201,7 +201,7 @@ private void TasView_QueryItemBkColor(int index, RollColumn column, ref Color co return; } - Color? overrideColor = QueryItemBgColorCallback?.Invoke(index, column.Name); + Color? overrideColor = QueryItemBgColorCallback(index, column.Name); if (overrideColor.HasValue) { @@ -327,7 +327,7 @@ private void TasView_QueryItemText(int index, RollColumn column, out string text return; } - var overrideText = QueryItemTextCallback?.Invoke(index, column.Name); + var overrideText = QueryItemTextCallback(index, column.Name); if (overrideText != null) { text = overrideText; diff --git a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs index 494c094156a..b37e5b5bbee 100644 --- a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs +++ b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs @@ -16,8 +16,15 @@ namespace BizHawk.Client.EmuHawk { - public class ToolManager + public class ToolManager : IToolLoader { + static ToolManager() + { + // APIs are used by tools, so this seems like a good place to add API types to the manager. + // Only API types not visible to BizHawk.Client.Common need to be added here. + ApiManager.AddApiType(typeof(ToolApi)); + } + private readonly MainForm _owner; private Config _config; private readonly DisplayManager _displayManager; @@ -71,15 +78,7 @@ private IExternalApiProvider GetOrInitApiProvider() _game, _owner.DialogController); - /// - /// 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 - internal IToolForm Load(Type toolType, bool focus = true) + public IToolForm Load(Type toolType, bool focus = true) { if (!typeof(IToolForm).IsAssignableFrom(toolType)) { @@ -102,13 +101,6 @@ private void SetBaseProperties(IToolForm form) if (form is LuaConsole luaConsole) luaConsole.MainFormForApi = _owner; } - /// - /// 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 { diff --git a/src/BizHawk.Tests.Client.Common/BizHawk.Tests.Client.Common.csproj b/src/BizHawk.Tests.Client.Common/BizHawk.Tests.Client.Common.csproj index 86e6f698719..7ff4016b1ee 100644 --- a/src/BizHawk.Tests.Client.Common/BizHawk.Tests.Client.Common.csproj +++ b/src/BizHawk.Tests.Client.Common/BizHawk.Tests.Client.Common.csproj @@ -6,6 +6,12 @@ + + + + + Always + diff --git a/src/BizHawk.Tests.Client.Common/FakeMainFormForApi.cs b/src/BizHawk.Tests.Client.Common/FakeMainFormForApi.cs new file mode 100644 index 00000000000..7c9f534065c --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/FakeMainFormForApi.cs @@ -0,0 +1,69 @@ +using System.Drawing; +using BizHawk.Client.Common; +using BizHawk.Client.Common.cheats; +using BizHawk.Emulation.Common; + +namespace BizHawk.Tests.Client.Common +{ + internal class FakeMainFormForApi : IMainFormForApi + { + public CheatCollection CheatList => throw new NotImplementedException(); + + public Point DesktopLocation => throw new NotImplementedException(); + + public IEmulator Emulator => throw new NotImplementedException(); + + public bool EmulatorPaused => 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 bool IsRewinding => throw new NotImplementedException(); + + public (HttpCommunication HTTP, MemoryMappedFiles MMF, SocketServer Sockets) NetworkingHelpers => (null!, null!, null!); + + public bool PauseAvi { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + +#pragma warning disable CS0067 // Events are never used + public event BeforeQuickLoadEventHandler? QuicksaveLoad; + public event BeforeQuickSaveEventHandler? QuicksaveSave; + public event EventHandler? RomLoaded; + public event StateLoadedEventHandler? SavestateLoaded; + public event StateSavedEventHandler? SavestateSaved; +#pragma warning disable CS0067 // Events are never used + + public void ClearHolds() => throw new NotImplementedException(); + public void ClickSpeedItem(int num) => throw new NotImplementedException(); + public void CloseEmulator(int? exitCode = null) => throw new NotImplementedException(); + public IDecodeResult DecodeCheatForAPI(string code, out MemoryDomain domain) => throw new NotImplementedException(); + public void EnableRewind(bool enabled) => throw new NotImplementedException(); + public FileWriteResult FlushSaveRAM(bool autosave = false) => throw new NotImplementedException(); + public void FrameAdvance(bool discardApiHawkSurfaces = true) => throw new NotImplementedException(); + public void FrameBufferResized(bool forceWindowResize = false) => throw new NotImplementedException(); + public void FrameSkipMessage() => throw new NotImplementedException(); + public int GetApproxFramerate() => throw new NotImplementedException(); + public bool LoadMovie(string filename, string? archive = null) => throw new NotImplementedException(); + public void LoadNullRom(bool clearSram = false) => 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 bool LoadState(string path, string userFriendlyStateName, bool suppressOSD = false) => throw new NotImplementedException(); + public void PauseEmulator() => throw new NotImplementedException(); + public bool RebootCore() => throw new NotImplementedException(); + public void Render() => throw new NotImplementedException(); + public bool RestartMovie() => throw new NotImplementedException(); + public FileWriteResult SaveQuickSave(int slot, bool suppressOSD = false) => throw new NotImplementedException(); + public FileWriteResult SaveState(string path, string userFriendlyStateName, bool suppressOSD = false) => throw new NotImplementedException(); + public void SeekFrameAdvance() => throw new NotImplementedException(); + public void StepRunLoop_Throttle() => throw new NotImplementedException(); + public void StopMovie(bool saveChanges = true) => throw new NotImplementedException(); + public void TakeScreenshot() => throw new NotImplementedException(); + public void TakeScreenshot(string path) => throw new NotImplementedException(); + public void TakeScreenshotToClipboard() => throw new NotImplementedException(); + public void TogglePause() => throw new NotImplementedException(); + public void ToggleSound() => throw new NotImplementedException(); + public void UnpauseEmulator() => throw new NotImplementedException(); + } +} diff --git a/src/BizHawk.Tests.Client.Common/Movie/FakeEmulator.cs b/src/BizHawk.Tests.Client.Common/Movie/FakeEmulator.cs index 4c2661b4b52..2a2f5e3e921 100644 --- a/src/BizHawk.Tests.Client.Common/Movie/FakeEmulator.cs +++ b/src/BizHawk.Tests.Client.Common/Movie/FakeEmulator.cs @@ -5,6 +5,7 @@ namespace BizHawk.Tests.Client.Common.Movie { + [Core("Fake", "Author", false, false)] internal class FakeEmulator : IEmulator, IStatable, IInputPollable { private BasicServiceProvider _serviceProvider; @@ -35,19 +36,21 @@ static FakeEmulator() public int LagCount { get; set; } public bool IsLagFrame { get; set; } -#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations - public IInputCallbackSystem InputCallbacks => throw new NotImplementedException(); -#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations + private InputCallbackSystem _inputCallbacks = new(); + public IInputCallbackSystem InputCallbacks => _inputCallbacks; public FakeEmulator() { _serviceProvider = new(this); } + public bool PollInputOnFrameAdvance = true; + public void Dispose() { } public bool FrameAdvance(IController controller, bool render, bool renderSound = true) { Frame++; + if (PollInputOnFrameAdvance) InputCallbacks.Call(); return true; } diff --git a/src/BizHawk.Tests.Client.Common/lua/BasicLuaLibrariesTests.cs b/src/BizHawk.Tests.Client.Common/lua/BasicLuaLibrariesTests.cs new file mode 100644 index 00000000000..04e3801b535 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/BasicLuaLibrariesTests.cs @@ -0,0 +1,50 @@ +using BizHawk.Client.Common; + +namespace BizHawk.Tests.Client.Common.lua +{ + [DoNotParallelize] + [TestClass] + public class BasicLuaTests + { + [TestMethod] + public void CanPrint() + { + // arrange + LuaTestContext context = new(); + context.AddScript("say_foo.lua", true); + + // act + context.RunYielding(); + + // assert + context.AssertLogMatches("foo"); + } + + [TestMethod] + public void ScriptCanStart() + { + // arrange + LuaTestContext context = new(); + + // act + context.AddScript("say_foo.lua", true); + + // assert + Assert.AreEqual(LuaFile.RunState.Running, context.GetScriptState(0)); + } + + [TestMethod] + public void ScriptCanStop() + { + // arrange + LuaTestContext context = new(); + context.AddScript("say_foo.lua", true); + + // act + context.RunYielding(); + + // assert + Assert.AreEqual(LuaFile.RunState.Disabled, context.GetScriptState(0)); + } + } +} diff --git a/src/BizHawk.Tests.Client.Common/lua/LuaEventTests.cs b/src/BizHawk.Tests.Client.Common/lua/LuaEventTests.cs new file mode 100644 index 00000000000..c030c98c003 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/LuaEventTests.cs @@ -0,0 +1,220 @@ +using BizHawk.Client.Common; + +namespace BizHawk.Tests.Client.Common.lua +{ + [DoNotParallelize] + [TestClass] + public class LuaEventTests + { + [TestMethod] + public void ScriptWithRegisteredFunctionShowsAsRunning() + { + // arrange + LuaTestContext context = new(); + context.AddScript("register_frame_end.lua", true); + + // act + context.RunYielding(); + + // assert + Assert.AreEqual(LuaFile.RunState.Running, context.GetScriptState(0)); + } + + [TestMethod] + public void UnregisteringLastEventStopsScript() + { + // arrange + LuaTestContext context = new(); + context.AddScript("unregister_event.lua", true); + context.RunYielding(); + Assert.AreEqual(LuaFile.RunState.Running, context.GetScriptState(0), "Test is invalid: arrange phase failed."); + + // act + context.RunFrameWaiting(); + + // assert + Assert.AreEqual(LuaFile.RunState.Disabled, context.GetScriptState(0)); + } + + [TestMethod] + public void ExitEventDoesNotKeepScriptActive() + { + // arrange + LuaTestContext context = new(); + context.AddScript("register_exit.lua", true); + + // act + context.RunYielding(); + + // assert + Assert.AreEqual(LuaFile.RunState.Disabled, context.GetScriptState(0)); + context.AssertLogMatches("foo"); + } + + [TestMethod] + public void ExceptionPrintsMessage() + { + // arrange + LuaTestContext context = new(); + context.AddScript("exception.lua", true); + + // act + context.RunYielding(); + + // assert + Assert.AreEqual(1, context.loggedMessages.Count); + } + + [TestMethod] + public void ExceptionInCallbackPrintsMessage() + { + // arrange + LuaTestContext context = new(); + context.AddScript("callback_exception.lua", true); + context.RunYielding(); + + // act + context.RunFrameWaiting(); + + // assert + Assert.AreEqual(1, context.loggedMessages.Count); + } + + [TestMethod] + public void ExceptionInCallbackDoesNotStopScript() + { + // arrange + LuaTestContext context = new(); + context.AddScript("callback_exception.lua", true); + context.RunYielding(); + Assert.AreEqual(LuaFile.RunState.Running, context.GetScriptState(0), "Test is invalid: arrange phase failed."); + + // act + context.RunFrameWaiting(); + + // assert + Assert.AreEqual(LuaFile.RunState.Running, context.GetScriptState(0)); + } + + // Lua itself should not know anything about the specialness of callbacks. + // "special" callback here refers to events raised by C# callback systems like input polling, which use some different code + [TestMethod] + public void ExceptionInSpecialCallbackPrintsMessage() + { + // arrange + LuaTestContext context = new(); + context.AddScript("input_poll_exception.lua", true); + context.RunYielding(); + + // act + context.RunFrameWaiting(); + + // assert + Assert.AreEqual(1, context.loggedMessages.Count); + } + + [TestMethod] + public void CurrentDirectoryIsSet() + { + // arrange + LuaTestContext context = new(); + context.AddScript("check_file_visible.lua", true); + + // act + context.RunYielding(); + + // assert + context.AssertLogMatches("pass"); + } + + [TestMethod] + public void CurrentDirectoryIsSetInCallback() + { + // arrange + LuaTestContext context = new(); + context.AddScript("check_file_visible_callback_1.lua", true); + context.RunYielding(); + + // act + context.RunFrameWaiting(); + + // assert + context.AssertLogMatches("pass"); + } + + [TestMethod] + public void CurrentDirectoryIsSetInSpecialCallback() + { + // arrange + LuaTestContext context = new(); + context.AddScript("check_file_visible_callback_2.lua", true); + context.RunYielding(); + + // act + context.RunFrameWaiting(); + + // assert + context.AssertLogMatches("pass"); + } + + [TestMethod] + public void CurrentDirectoryIsSetInCallbackRegisteredFromCallback() + { + // arrange + LuaTestContext context = new(); + context.AddScript("check_file_visible_callback_3.lua", true); + context.RunFrameWaiting(); + + // act + context.RunFrameWaiting(); + context.RunFrameWaiting(); + + // assert + context.AssertLogMatches("pass"); + } + + [TestMethod] + public void ExitingScriptUnregistersFunctions() + { + // arrange + LuaTestContext context = new(); + context.AddScript("register_exit.lua", true); + + // act + context.RunYielding(); + + // assert + Assert.AreEqual(0, context.FunctionsRegisteredToScript(0).Count); + } + + [TestMethod] + public void StoppingScriptUnregistersFunctions() + { + // arrange + LuaTestContext context = new(); + context.AddScript("register_frame_end.lua", true); + context.RunYielding(); + Assert.AreEqual(LuaFile.RunState.Running, context.GetScriptState(0), "Test is invalid: arrange phase failed."); + + // act + context.StopScript(0); + + // assert + Assert.AreEqual(0, context.FunctionsRegisteredToScript(0).Count); + } + + [TestMethod] + public void ScriptStoppedByExceptionUnregistersFunctions() + { + // arrange + LuaTestContext context = new(); + context.AddScript("register_then_exception.lua", true); + + // act + context.RunYielding(); + + // assert + Assert.AreEqual(0, context.FunctionsRegisteredToScript(0).Count); + } + } +} diff --git a/src/BizHawk.Tests.Client.Common/lua/LuaTestContext.cs b/src/BizHawk.Tests.Client.Common/lua/LuaTestContext.cs new file mode 100644 index 00000000000..9e1856c0524 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/LuaTestContext.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Common; +using BizHawk.Tests.Client.Common.Movie; + +namespace BizHawk.Tests.Client.Common.lua +{ + internal class LuaTestContext + { + private static readonly string BASE_SCRIPT_PATH = Path.Combine(Environment.CurrentDirectory, "lua/scripts"); + + private LuaLibraries lua; + + public List loggedMessages = new(); + + private FakeEmulator emulator = new(); + + public LuaTestContext() + { + Action print = loggedMessages.Add; + FakeMainFormForApi mainApi = new(); + Config config = new(); + + ApiContainer apiContainer = ApiManager.RestartLua( + emulator.ServiceProvider, + print, + mainApi, + new SimpleGDIPDisplayManager(config, emulator), + null!, + null!, + null!, + config, + emulator, + new GameInfo(), + null!); + + + lua = new( + new LuaFileList([ ], () => { }), + emulator.ServiceProvider, + mainApi, + config, + print, + null!, + apiContainer + ); + } + + public void AddScript(string path, bool enable) + { + string absolutePath = Path.GetFullPath(Path.Combine(BASE_SCRIPT_PATH, path)); + LuaFile luaFile = new(absolutePath, () => { }); + lua.ScriptList.Add(luaFile); + if (enable) + luaFile.Start(lua.SpawnCoroutineAndSandbox(luaFile.Path)); + } + + public void StopScript(int id) + { + lua.ScriptList[id].Stop(); + } + + public void RunYielding() + { + lua.ResumeScripts(false); + } + + public void RunFrameWaiting() + { + Controller c = new(emulator.ControllerDefinition); + emulator.FrameAdvance(c, true); + lua.CallFrameAfterEvent(); + lua.ResumeScripts(true); + } + + public void AssertLogMatches(params string[] messages) + { + Assert.AreEqual(messages.Length, loggedMessages.Count); + for (int i = 0; i < messages.Length; i++) + { + Assert.AreEqual(messages[i], loggedMessages[i]); + } + } + + public LuaFile.RunState GetScriptState(int id) + { + return lua.ScriptList[id].State; + } + + public List FunctionsRegisteredToScript(int id) + { + return lua.ScriptList[id].Functions.ToList(); + } + } +} diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/callback_exception.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/callback_exception.lua new file mode 100644 index 00000000000..9ce6d41d97a --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/callback_exception.lua @@ -0,0 +1 @@ +event.onframeend(function() a() end) diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible.lua new file mode 100644 index 00000000000..f1c7bcae0c5 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible.lua @@ -0,0 +1,7 @@ +local f = io.open("check_file_visible.lua", "r") +if f == nil then + print("fail") +else + io.close(f) + print("pass") +end diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_1.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_1.lua new file mode 100644 index 00000000000..e3c8e53e3dd --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_1.lua @@ -0,0 +1,11 @@ +local function frameEnd() + local f = io.open("check_file_visible_callback_1.lua", "r") + if f == nil then + print("fail") + else + io.close(f) + print("pass") + end +end + +event.onframeend(frameEnd) diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_2.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_2.lua new file mode 100644 index 00000000000..ab751769e6d --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_2.lua @@ -0,0 +1,11 @@ +local function inputPoll() + local f = io.open("check_file_visible_callback_2.lua", "r") + if f == nil then + print("fail") + else + io.close(f) + print("pass") + end +end + +event.oninputpoll(inputPoll) diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_3.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_3.lua new file mode 100644 index 00000000000..64106251122 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/check_file_visible_callback_3.lua @@ -0,0 +1,21 @@ +local function lookForFile() + local f = io.open("check_file_visible_callback_3.lua", "r") + if f == nil then + print("fail") + else + io.close(f) + print("pass") + end +end + +local eventId = nil +local function frameEnd() + event.onframeend(lookForFile) + event.unregisterbyid(eventId) +end + +eventId = event.onframeend(frameEnd) + +while true do + emu.frameadvance() +end diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/exception.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/exception.lua new file mode 100644 index 00000000000..0e034c053bc --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/exception.lua @@ -0,0 +1 @@ +a() diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/input_poll_exception.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/input_poll_exception.lua new file mode 100644 index 00000000000..c90e25295e3 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/input_poll_exception.lua @@ -0,0 +1 @@ +event.oninputpoll(function() a() end) diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/register_exit.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/register_exit.lua new file mode 100644 index 00000000000..db56154f1b7 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/register_exit.lua @@ -0,0 +1 @@ +event.onexit(function() print("foo") end) diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/register_frame_end.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/register_frame_end.lua new file mode 100644 index 00000000000..ee405dda2fa --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/register_frame_end.lua @@ -0,0 +1 @@ +event.onframeend(function() end) diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/register_then_exception.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/register_then_exception.lua new file mode 100644 index 00000000000..1e9084e9a75 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/register_then_exception.lua @@ -0,0 +1,3 @@ +event.onframeend(function() end) + +a() diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/say_foo.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/say_foo.lua new file mode 100644 index 00000000000..158527e6ac3 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/say_foo.lua @@ -0,0 +1 @@ +print("foo") diff --git a/src/BizHawk.Tests.Client.Common/lua/scripts/unregister_event.lua b/src/BizHawk.Tests.Client.Common/lua/scripts/unregister_event.lua new file mode 100644 index 00000000000..d2544c249b4 --- /dev/null +++ b/src/BizHawk.Tests.Client.Common/lua/scripts/unregister_event.lua @@ -0,0 +1,6 @@ +local id = nil +local function frameEnd() + event.unregisterbyid(id) +end + +id = event.onframeend(frameEnd)