Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c9ef322
Make loading of EmuHawk Lua libraries optional, from the perspective …
SuuperW Dec 11, 2025
4d075ed
Remove final references to EmuHawk types in LuaLibraries.
SuuperW Dec 11, 2025
882b087
Make an interface for API use of ToolManager.
SuuperW Dec 11, 2025
70cdb80
Move things to BizHawk.Client.Common
SuuperW Dec 11, 2025
b600697
Create tests for Lua events.
SuuperW Dec 11, 2025
7fca33b
remove unused properties and unnecessary parameter
SuuperW Dec 4, 2025
750eb0a
simplify constructors
SuuperW Dec 4, 2025
badf2a8
Make current file tracking non-static by moving it to LuaLibraries in…
SuuperW Dec 7, 2025
652ed59
Make Sandbox method non-static, in preparation for future commits.
SuuperW Dec 9, 2025
c7ddd98
Remove static DefaultLogger, use exception callback parameter instead.
SuuperW Dec 9, 2025
d3f5990
Handle setting CurrentFile in Sandbox. Fixes some issues where callba…
SuuperW Dec 9, 2025
d61ed59
Catch all exceptions in sandbox, there's no reason to differentiate b…
SuuperW Dec 9, 2025
7e83a04
Use the sandbox exception logic.
SuuperW Dec 9, 2025
d966ec5
Make each LuaFile own its own registered functions.
SuuperW Dec 4, 2025
4193789
Don't stop scripts when they have registered functions. Passes most p…
SuuperW Dec 10, 2025
49f0c11
Make LuaFile.State setter private, and handle state change through me…
SuuperW Dec 4, 2025
bd83878
Move handling of special Lua callbacks out of NamedLuaFunction, inclu…
SuuperW Dec 5, 2025
4e19989
Keep Lua scripts running if a LuaWinform is open, like if an event is…
SuuperW Dec 10, 2025
ee623e1
Make TAStudio callbacks behave like other Lua callbacks: support more…
SuuperW Dec 10, 2025
e00377a
Fix forms.destroy; don't attempt to update/clear current file/thread …
SuuperW Dec 10, 2025
cf97f61
fix: functions registered without a file weren't handled
SuuperW Feb 12, 2026
8f1a26e
fixes for Lua forms
SuuperW Feb 14, 2026
dba50eb
complete TODO: keep track of origin script and include that as well, …
SuuperW Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (var implType in ReflectionCache_Biz_Cli_Com.Types
foreach (var implType in ReflectionCache.Types

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. I think the explicit type name is better.

.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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't like this (used in EmuHawk.ToolApi..cctor). public static state should be immutable, and certainly not affected by call order.

Copy link
Copy Markdown
Contributor Author

@SuuperW SuuperW Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not affected by call order. The order of things in the list doesn't matter. And if it did matter, it would be an existing problem, not introduced in this PR. (Since the order given by ReflectionCache is not defined.)

The only place it is called is in other static constructors. It is not mutated after static initialization.

I do agree that this solution isn't ideal, but I do not think it's any worse than using reflection to find the types. Finding a good solution to this is outside the scope of this PR.

EDIT: There was an issue here actually, with the static constructor not running. That has been fixed by moving it to ToolManager.

{
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;
Expand All @@ -38,7 +40,7 @@ private static ApiContainer Register(
DisplayManagerBase displayManager,
InputManager inputManager,
IMovieSession movieSession,
ToolManager toolManager,
IToolLoader toolManager,
Config config,
IEmulator emulator,
IGameInfo game,
Expand All @@ -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,
Expand All @@ -74,7 +76,7 @@ public static IExternalApiProvider Restart(
DisplayManagerBase displayManager,
InputManager inputManager,
IMovieSession movieSession,
ToolManager toolManager,
IToolLoader toolManager,
Config config,
IEmulator emulator,
IGameInfo game,
Expand All @@ -92,7 +94,7 @@ public static ApiContainer RestartLua(
DisplayManagerBase displayManager,
InputManager inputManager,
IMovieSession movieSession,
ToolManager toolManager,
IToolLoader toolManager,
Config config,
IEmulator emulator,
IGameInfo game,
Expand Down
105 changes: 105 additions & 0 deletions src/BizHawk.Client.Common/IMainFormForTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using BizHawk.Bizware.Graphics;
using BizHawk.Emulation.Common;

namespace BizHawk.Client.Common
{
public interface IMainFormForTools : IDialogController
{
/// <remarks>referenced by 3 or more tools</remarks>
CheatCollection CheatList { get; }

/// <remarks>referenced by 3 or more tools</remarks>
string CurrentlyOpenRom { get; }

/// <remarks>referenced from HexEditor and RetroAchievements</remarks>
LoadRomArgs CurrentlyOpenRomArgs { get; }

/// <remarks>only referenced from TAStudio</remarks>
bool EmulatorPaused { get; }

/// <remarks>only referenced from PlaybackBox</remarks>
bool HoldFrameAdvance { get; set; }

/// <remarks>only referenced from BasicBot</remarks>
bool InvisibleEmulation { get; set; }

/// <remarks>only referenced from LuaConsole</remarks>
bool IsTurboing { get; }

/// <remarks>only referenced from TAStudio</remarks>
bool IsFastForwarding { get; }

/// <remarks>referenced from PlayMovie and TAStudio</remarks>
int? PauseOnFrame { get; set; }

/// <remarks>only referenced from PlaybackBox</remarks>
bool PressRewind { get; set; }

/// <remarks>referenced from BookmarksBranchesBox and VideoWriterChooserForm</remarks>
BitmapBuffer CaptureOSD();

/// <remarks>only referenced from TAStudio</remarks>
void DisableRewind();

/// <remarks>only referenced from TAStudio</remarks>
void EnableRewind(bool enabled);

/// <remarks>only referenced from TAStudio</remarks>
bool EnsureCoreIsAccurate();

/// <remarks>only referenced from TAStudio</remarks>
void FrameAdvance(bool discardApiHawkSurfaces = true);

/// <remarks>only referenced from LuaConsole</remarks>
/// <param name="forceWindowResize">Override <see cref="Common.Config.ResizeWithFramebuffer"/></param>
void FrameBufferResized(bool forceWindowResize = false);

/// <remarks>only referenced from BasicBot</remarks>
bool LoadQuickSave(int slot, bool suppressOSD = false);

/// <remarks>referenced from MultiDiskBundler and RetroAchievements</remarks>
bool LoadRom(string path, LoadRomArgs args);

/// <remarks>only referenced from BookmarksBranchesBox</remarks>
BitmapBuffer MakeScreenshotImage();

/// <remarks>referenced from ToolFormBase</remarks>
void MaybePauseFromMenuOpened();

/// <remarks>referenced from ToolFormBase</remarks>
void MaybeUnpauseFromMenuClosed();

/// <remarks>referenced by 3 or more tools</remarks>
void PauseEmulator();

/// <remarks>only referenced from TAStudio</remarks>
bool BlockFrameAdvance { get; set; }

/// <remarks>referenced from PlaybackBox and TAStudio</remarks>
void SetMainformMovieInfo();

/// <remarks>referenced by 3 or more tools</remarks>
bool StartNewMovie(IMovie movie, bool newMovie);

/// <remarks>only referenced from BasicBot</remarks>
void Throttle();

/// <remarks>only referenced from TAStudio</remarks>
void TogglePause();

/// <remarks>referenced by 3 or more tools</remarks>
void UnpauseEmulator();

/// <remarks>only referenced from BasicBot</remarks>
void Unthrottle();

/// <remarks>only referenced from LogWindow</remarks>
void UpdateDumpInfo(RomStatus? newStatus = null);

/// <remarks>only referenced from BookmarksBranchesBox</remarks>
void UpdateStatusSlots();

/// <remarks>only referenced from TAStudio</remarks>
void UpdateWindowTitle();
}
}
25 changes: 25 additions & 0 deletions src/BizHawk.Client.Common/lua/ApiGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace BizHawk.Client.Common
{
[Flags]
public enum ApiGroup
{
NONE = 0,

/// <summary>
/// yield or frameadvance
/// </summary>
YIELDING = 1,

/// <summary>
/// any method that may result in (re)booting a core, such as loading a ROM
/// </summary>
BOOTING = 2,

/// <summary>
/// any method that may result in saving or loading a savestate
/// </summary>
STATES = 4,

PROHIBITED_MID_FRAME = YIELDING | BOOTING | STATES,
}
}
13 changes: 10 additions & 3 deletions src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace BizHawk.Client.Common
[Description("A library for manipulating the EmuHawk client UI")]
public sealed class ClientLuaLibrary : LuaLibraryBase
{
public Lazy<string> AllAPINames { get; set; }

[OptionalService]
private IVideoProvider VideoProvider { get; set; }

Expand Down Expand Up @@ -70,7 +72,7 @@ public void ClearAutohold()
[LuaMethod("closerom", "Closes the loaded Rom")]
public void CloseRom()
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.BOOTING))
{
throw new InvalidOperationException("client.closerom() is not allowed during input/memory callbacks");
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -193,7 +200,7 @@ public void OpenRamSearch()
[LuaMethod("openrom", "Loads a ROM from the given path. Returns true if the ROM was successfully loaded, otherwise false.")]
public bool OpenRom(string path)
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.BOOTING))
{
throw new InvalidOperationException("client.openrom() is not allowed during input/memory callbacks");
}
Expand Down Expand Up @@ -233,7 +240,7 @@ public void PauseAv()
[LuaMethod("reboot_core", "Reboots the currently loaded core")]
public void RebootCore()
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.BOOTING))
{
throw new InvalidOperationException("client.reboot_core() is not allowed during input/memory callbacks");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public MemorySavestateLuaLibrary(ILuaLibraries luaLibsImpl, ApiContainer apiCont
[LuaMethod("savecorestate", "creates a core savestate and stores it in memory. Note: a core savestate is only the raw data from the core, and not extras such as movie input logs, or framebuffers. Returns a unique identifer for the savestate")]
public string SaveCoreStateToMemory()
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.STATES))
{
throw new InvalidOperationException("memorysavestate.savecorestate() is not allowed during input/memory callbacks");
}
Expand All @@ -25,7 +25,7 @@ public string SaveCoreStateToMemory()
[LuaMethod("loadcorestate", "loads an in memory state with the given identifier")]
public void LoadCoreStateFromMemory(string identifier)
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.STATES))
{
throw new InvalidOperationException("memorysavestate.loadcorestate() is not allowed during input/memory callbacks");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public string Mode()
[LuaMethod("play_from_start", "Resets the core to frame 0 with the currently loaded movie in playback mode. If a path to a movie is specified, attempts to load it, then continues with playback if it was successful. Returns true iff successful.")]
public bool PlayFromStart(string path = "")
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.BOOTING))
{
throw new InvalidOperationException("movie.play_from_start() is not allowed during input/memory callbacks");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public SaveStateLuaLibrary(ILuaLibraries luaLibsImpl, ApiContainer apiContainer,
[LuaMethod("load", "Loads a savestate with the given path. Returns true iff succeeded. If EmuHawk is deferring quicksaves, to TAStudio for example, that form will do what it likes (and the path is ignored).")]
public bool Load(string path, bool suppressOSD = false)
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.STATES))
{
throw new InvalidOperationException("savestate.load() is not allowed during input/memory callbacks");
}
Expand All @@ -35,7 +35,7 @@ public bool Load(string path, bool suppressOSD = false)
[LuaMethod("loadslot", "Loads the savestate at the given slot number (must be an integer between 1 and 10). Returns true iff succeeded. If EmuHawk is deferring quicksaves, to TAStudio for example, that form will do what it likes with the slot number.")]
public bool LoadSlot(int slotNum, bool suppressOSD = false)
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.STATES))
{
throw new InvalidOperationException("savestate.loadslot() is not allowed during input/memory callbacks");
}
Expand All @@ -59,7 +59,7 @@ public bool LoadSlot(int slotNum, bool suppressOSD = false)
[LuaMethod("save", "Saves a state at the given path. If EmuHawk is deferring quicksaves, to TAStudio for example, that form will do what it likes (and the path is ignored).")]
public bool Save(string path, bool suppressOSD = false)
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.STATES))
{
throw new InvalidOperationException("savestate.save() is not allowed during input/memory callbacks");
}
Expand All @@ -78,7 +78,7 @@ public bool Save(string path, bool suppressOSD = false)
[LuaMethod("saveslot", "Saves a state at the given save slot (must be an integer between 1 and 10). If EmuHawk is deferring quicksaves, to TAStudio for example, that form will do what it likes with the slot number.")]
public bool SaveSlot(int slotNum, bool suppressOSD = false)
{
if (_luaLibsImpl.IsInInputOrMemoryCallback)
if (_luaLibsImpl.ProhibitedApis.HasFlag(ApiGroup.STATES))
{
throw new InvalidOperationException("savestate.saveslot() is not allowed during input/memory callbacks");
}
Expand Down
9 changes: 6 additions & 3 deletions src/BizHawk.Client.Common/lua/ILuaLibraries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ namespace BizHawk.Client.Common
{
public interface ILuaLibraries
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this interface exist, again? b3c7f0f eliminated the multiple-implementations aspect, so is this just an abstraction to make it usable from Client.Common? I feel like we should be able to get rid of it.

Copy link
Copy Markdown
Contributor Author

@SuuperW SuuperW Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should those callback properties stay? I do not see any value in keeping such callback logic in NamedLuaFunction. They logically belong with the API code, and are clutter in NamedLuaFunction. I also do not see any difference between these two and any of the other special callbacks. (And there are a good number of them, including some for TAStudio that were never put in NamedLuaFunction.)

It could make sense to keep them if these two are combined, as a way of putting all the sets of IsInInputOrMemoryCallback in one place. Although I think that could use a better name, such as IsInCallbackDuringFrame. Then again, an even better solution might be to set it at the start and end of every frame (and call it FrameIsRunning or something like that) rather than waiting until a callback happens. Since the purpose appears to be preventing calls to certain API methods that should not run mid-frame.

EDIT: The above looks like it should have been a reply to the conversation above about *Callback. Text below is for this conversation.

I am not sure why ILuaLibraries exists. Your guess is the same thing I would guess. I can remove it if you want, but I think that change can just as well be done after/outside this PR.

{
LuaFile CurrentFile { get; }

/// <remarks>pretty hacky... we don't want a lua script to be able to restart itself by rebooting the core</remarks>
bool IsRebootingCore { get; set; }

bool IsUpdateSupressed { get; set; }

/// <remarks>not really sure if this is the right place to put it, multiple different places need this...</remarks>
bool IsInInputOrMemoryCallback { get; set; }

PathEntryCollection PathEntries { get; }

ApiGroup ProhibitedApis { get; }

NLuaTableHelper GetTableHelper();

void Sandbox(LuaFile luaFile, Action callback, Action<string> exceptionCallback = null, ApiGroup prohibitedApis = ApiGroup.NONE);
}
}
29 changes: 14 additions & 15 deletions src/BizHawk.Client.Common/lua/INamedLuaFunction.cs
Original file line number Diff line number Diff line change
@@ -1,28 +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; }

/// <summary>for <c>doom.on_prandom</c>; single param: info on what changed the RNG index</summary>
Action<string> RandomCallback { get; }

/// <summary>for <c>doom.on_intercept</c>; single param: blockmap block the intercept happened in</summary>
Action<int> InterceptCallback { get; }

/// <summary>for <c>doom.on_use and doom.on_cross</c>; two params: pointers to activated line and to mobj that triggered it</summary>
Action<long, long> LineCallback { get; }

string Name { get; }

/// <summary>
/// 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 <see cref="Call(object[])"/> and possibly does other related Lua setup and cleanup tasks.
/// 3) That C# function is added to the non-Lua callback system.
/// 4) <see cref="OnRemove"/> is assigned an <see cref="Action"/> that removes the C# function from the non-Lua callback.
/// </summary>
Action OnRemove { get; set; }

/// <summary>
/// Calls the Lua function with the given arguments.
/// </summary>
object[] Call(params object[] args);
}
}
7 changes: 7 additions & 0 deletions src/BizHawk.Client.Common/lua/IPrintingLibrary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace BizHawk.Client.Common
{
public interface IPrintingLibrary
{
void Log(params object[] outputs);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in LuaLibraryBase with the other log function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why LuaLibraryBase? If you're thinking we can then remove the LogCallback property in the various API classes, that won't work. (For one, that's in the base API classes, not the Lua wrappers. For two, it is set differently for Lua Console, external tools, and tests.)
Anyway it cannot be done right now because the Lua log method references LuaConsole, an EmuHawk type.

}
}
Loading