diff --git a/src/BizHawk.Client.Common/lua/Documentation/LuaCatsGenerator.cs b/src/BizHawk.Client.Common/lua/Documentation/LuaCatsGenerator.cs
new file mode 100644
index 00000000000..906a2b7f471
--- /dev/null
+++ b/src/BizHawk.Client.Common/lua/Documentation/LuaCatsGenerator.cs
@@ -0,0 +1,294 @@
+#nullable enable
+
+#pragma warning disable MA0136 // Raw String contains an implicit end of line character, line endings will be normalized
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+using BizHawk.Common;
+using BizHawk.Common.ReflectionExtensions;
+using NLua;
+
+namespace BizHawk.Client.Common;
+
+///
+/// Generates API definitions in the LuaCATS format.
+///
+///
+/// See https://luals.github.io/wiki/annotations
+///
+internal static class LuaCatsGenerator
+{
+ private static readonly Dictionary TypeConversions = new()
+ {
+ [typeof(object)] = "any",
+ [typeof(byte)] = "integer",
+ [typeof(sbyte)] = "integer",
+ [typeof(int)] = "integer",
+ [typeof(uint)] = "integer",
+ [typeof(short)] = "integer",
+ [typeof(ushort)] = "integer",
+ [typeof(long)] = "integer",
+ [typeof(ulong)] = "integer",
+ [typeof(float)] = "number",
+ [typeof(double)] = "number",
+ [typeof(decimal)] = "number",
+ [typeof(string)] = "string",
+ [typeof(bool)] = "boolean",
+ [typeof(byte[])] = "string",
+ [typeof(Memory)] = "string",
+ [typeof(ReadOnlyMemory)] = "string",
+ [typeof(LuaFunction)] = "function",
+ [typeof(LuaTable)] = "table",
+ [typeof(System.Drawing.Color)] = "dotnetcolor",
+ };
+
+ private const string Classes = """
+---@class dotnetcolor : userdata
+
+---A color in one of the following formats:
+--- - Number in the format `0xAARRGGBB`
+--- - String in the format `"#RRGGBB"` or `"#AARRGGBB"`
+--- - A CSS3/X11 color name e.g. `"blue"`, `"palegoldenrod"`
+--- - Color created with `forms.createcolor`
+---@alias color dotnetcolor | integer | string
+
+---@alias surface
+---| "emucore" # Draw on the emulated screen. Resolution depends on emulated system and game. Drawing is scaled with the rest of the display.
+---| "client" # Draw on the BizHawk window. Resolution depends on the window size. Drawing is not scaled.
+""";
+
+ private const string Preamble = """
+-- https://tasvideos.org/Bizhawk
+
+error("This is a definition file for Lua Language Server and not a usable script")
+
+---@meta _
+""";
+
+ private static string? GetHardcodedType(ParameterInfo parameter)
+ {
+ // Technically any string parameter can be passed a number in BizHawk's Lua API, but let's just focus on the ones where it's commonly used
+ // like `gui.text` and `forms.settext` instead of polluting the entire API surface
+ if (parameter.Name is "message" or "caption" && parameter.ParameterType == typeof(string))
+ {
+ return "string|number";
+ }
+
+ if (parameter.Member.DeclaringType.Name == "GuiLuaLibrary" && parameter.Name == "surfaceName" && parameter.ParameterType == typeof(string))
+ {
+ return "surface";
+ }
+
+ return null;
+ }
+
+ public static void Generate(LuaDocumentation docs, string path)
+ {
+ var sb0 = new StringBuilder();
+
+ sb0.AppendLine($"-- Lua functions available in EmuHawk {VersionInfo.MainVersion}");
+ sb0.AppendLine(Preamble);
+ sb0.AppendLine();
+ sb0.AppendLine(Classes);
+ sb0.AppendLine();
+
+ File.WriteAllText(Path.Combine(path, "classes.d.lua"), sb0.ToString().ReplaceLineEndings());
+
+ foreach (var libraryGroup in docs.GroupBy(func => func.Library).OrderBy(group => group.Key))
+ {
+ string library = libraryGroup.Key;
+ string libraryDescription = libraryGroup.First().LibraryDescription;
+ var libraryType = libraryGroup.First().Method.DeclaringType;
+ var filePath = Path.Combine(path, library + ".d.lua");
+ var sb = new StringBuilder();
+
+ sb.AppendLine($"-- Lua functions available in EmuHawk {VersionInfo.MainVersion}");
+ sb.AppendLine(Preamble);
+ sb.AppendLine();
+
+ if (!string.IsNullOrEmpty(libraryDescription))
+ {
+ sb.AppendLine(FormatMarkdown(libraryDescription));
+ }
+
+ sb.AppendLine($"---@class {SafeLibraryTypeName(library)}");
+ if (!typeof(LuaLibraryBase).IsAssignableFrom(libraryType)) sb.Append("local "); // don't make LuaCanvas global
+ sb.AppendLine($"{library} = {{}}");
+ sb.AppendLine();
+
+ foreach (var func in libraryGroup.OrderBy(func => func.Name))
+ {
+ if (!string.IsNullOrEmpty(func.Description))
+ {
+ sb.AppendLine(FormatMarkdown(func.Description));
+ }
+
+ if (func.Example != null)
+ {
+ sb.AppendLine("---");
+ sb.AppendLine("---Example:");
+ sb.AppendLine("---");
+ sb.AppendLine(FormatMarkdown(func.Example, "---\t"));
+ }
+
+ if (func.IsDeprecated)
+ {
+ sb.AppendLine("---@deprecated");
+ }
+
+ foreach (var parameter in func.Method.GetParameters())
+ {
+ if (IsParams(parameter))
+ {
+ sb.Append("---@vararg");
+ }
+ else
+ {
+ sb.Append($"---@param {parameter.Name}");
+ if (parameter.HasDefaultValue || (parameter.IsNRTOrNullableT() ?? true))
+ {
+ sb.Append('?');
+ }
+ }
+
+ sb.Append(' ');
+ sb.Append(GetLuaType(parameter));
+ if (IsZeroIndexed(parameter))
+ {
+ sb.Append(" Zero-indexed array.");
+ }
+ if (parameter.HasDefaultValue && parameter.DefaultValue is not null and not "")
+ {
+ sb.Append($" Defaults to `{FormatValue(parameter.DefaultValue)}`");
+ }
+ sb.AppendLine();
+ }
+
+ if (func.Method.ReturnType != typeof(void))
+ {
+ sb.Append("---@return ");
+ var luaType = GetLuaType(func.Method.ReturnParameter);
+ var nilable = func.Method.ReturnParameter.IsNRTOrNullableT() ?? true;
+ var wrapType = nilable && luaType.IndexOfAny([ ':', '|' ]) != -1; // ? is ambiguous on complex types like `string|int` or `fun(): string`
+ if (wrapType) sb.Append('(');
+ sb.Append(luaType);
+ if (wrapType) sb.Append(')');
+ if (nilable) sb.Append('?');
+ if (IsZeroIndexed(func.Method.ReturnParameter))
+ {
+ sb.Append(" # Zero-indexed array.");
+ }
+ sb.AppendLine();
+ }
+
+ sb.Append($"function {library}.{func.Name}(");
+
+ foreach (var parameter in func.Method.GetParameters())
+ {
+ if (parameter.Position > 0)
+ {
+ sb.Append(", ");
+ }
+ sb.Append(IsParams(parameter) ? "..." : parameter.Name);
+ }
+
+ sb.AppendLine(") end");
+ sb.AppendLine();
+ }
+ File.WriteAllText(filePath, sb.ToString().ReplaceLineEndings());
+ }
+ }
+
+ private static string FormatMarkdown(string value, string prefix = "---")
+ {
+ // prefix every line
+ value = Regex.Replace(value, "^", prefix, RegexOptions.Multiline);
+ // replace {{wiki markup}} with `markdown`
+ value = Regex.Replace(value, "{{(.+?)}}", "`$1`");
+ // replace wiki image markup with markdown
+ value = Regex.Replace(value, @"\[(?.+?)\|alt=(?.+?)\]", "");
+ return value;
+ }
+
+ private static string FormatValue(object value) => value switch
+ {
+ string str => $"\"{str}\"",
+ true => "true",
+ false => "false",
+ null => "nil",
+ _ => value.ToString(),
+ };
+
+ ///
+ /// Avoid name collisions with existing Lua types.
+ /// Only for the @class annotation, not the name of the global.
+ ///
+ ///
+ private static string SafeLibraryTypeName(string name) => name switch
+ {
+ "userdata" => $"biz{name}",
+ _ => name,
+ };
+
+ private static string GetLuaType(ParameterInfo parameter)
+ {
+ if (GetOverrideType(parameter) is string overrideType)
+ {
+ return overrideType;
+ }
+
+ if (GetHardcodedType(parameter) is string hardcodedType)
+ {
+ return hardcodedType;
+ }
+
+ if (parameter.GetCustomAttribute() is not null)
+ {
+ return "color"; // see Preamble
+ }
+
+ if (parameter.ParameterType.IsArray && IsParams(parameter))
+ {
+ // no [] array modifier for varargs
+ return GetLuaType(parameter.ParameterType.GetElementType());
+ }
+
+ return GetLuaType(parameter.ParameterType);
+ }
+
+ private static string GetLuaType(Type type)
+ {
+ // try this twice, before and after extracting the array/nullable type
+ if (TypeConversions.TryGetValue(type, out string luaType))
+ {
+ return luaType;
+ }
+
+ if (type.IsArray)
+ {
+ return GetLuaType(type.GetElementType()) + "[]";
+ }
+
+ if (Nullable.GetUnderlyingType(type) is Type underlyingType)
+ {
+ type = underlyingType;
+ }
+
+ if (TypeConversions.TryGetValue(type, out luaType))
+ {
+ return luaType;
+ }
+
+ throw new NotSupportedException($"Unknown type {type.FullName} used in API. Generator must be updated to handle this.");
+ }
+
+ private static bool IsParams(ParameterInfo parameter) => parameter.GetCustomAttribute() is not null;
+
+ private static bool IsZeroIndexed(ParameterInfo parameter) => parameter.GetCustomAttribute() is not null;
+
+ private static string? GetOverrideType(ParameterInfo parameter) => parameter.GetCustomAttribute()?.Type;
+}
diff --git a/src/BizHawk.Client.Common/lua/LuaAttributes.cs b/src/BizHawk.Client.Common/lua/LuaAttributes.cs
index ffa75dd81d0..cc9624f923c 100644
--- a/src/BizHawk.Client.Common/lua/LuaAttributes.cs
+++ b/src/BizHawk.Client.Common/lua/LuaAttributes.cs
@@ -44,4 +44,14 @@ public LuaLibraryAttribute(bool released)
///
[AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter)]
public sealed class LuaZeroIndexedAttribute : Attribute { }
+
+ ///
+ /// Specifies a custom type to be used in LuaCATS annotations, for documentation purposes only.
+ /// See and for possible values.
+ ///
+ [AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter)]
+ public sealed class LuaCatsTypeAttribute(string Type) : Attribute
+ {
+ public string Type { get; } = Type;
+ }
}
diff --git a/src/BizHawk.Client.Common/lua/LuaDocumentation.cs b/src/BizHawk.Client.Common/lua/LuaDocumentation.cs
index 2174d646916..eff42860d74 100644
--- a/src/BizHawk.Client.Common/lua/LuaDocumentation.cs
+++ b/src/BizHawk.Client.Common/lua/LuaDocumentation.cs
@@ -199,6 +199,11 @@ public string ToNotepadPlusPlusAutoComplete()
{
return ""; // TODO
}
+
+ public void ToLuaLanguageServerDefinitions(string path)
+ {
+ LuaCatsGenerator.Generate(this, path);
+ }
}
public class LibraryFunction
diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs
index 4c06f27caa2..17583a1b9ae 100644
--- a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs
@@ -360,6 +360,7 @@ public void Text(
description: "Creates a dedicated canvas window, returning a table containing some callbacks for drawing. These are the LuaCanvas functions in the API reference."
+ " 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
+ [return: LuaCatsType("LuaCanvas")]
public LuaTable CreateCanvas(int width, int height, int? x = null, int? y = null)
{
var canvas = new LuaCanvas(APIs.Emulation, PathEntries, width, height, x, y, _th, LogOutputCallback);
diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaAutocompleteInstaller.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaAutocompleteInstaller.cs
index c2b6878b186..aa18d23d1c4 100644
--- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaAutocompleteInstaller.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaAutocompleteInstaller.cs
@@ -46,17 +46,19 @@ public void InstallBizLua(TextEditors editor, LuaDocumentation docs)
private string AppDataFolder => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ private string ProgramFilesFolder => Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
+
private bool IsSublimeInstalled()
{
// The most likely location of the app, eventually we should consider looking through the registry or installed apps as a more robust way to detect it;
- string exePath = @"C:\Program Files\Sublime Text 2\sublime_text.exe";
+ string exePath = Path.Combine(ProgramFilesFolder, @"Sublime Text 2\sublime_text.exe");
return File.Exists(exePath);
}
private bool IsNotepadInstalled()
{
// The most likely location of the app, eventually we should consider looking through the registry or installed apps as a more robust way to detect it;
- string exePath = @"C:\Program Files (x86)\Notepad++\notepad++.exe";
+ string exePath = Path.Combine(ProgramFilesFolder, @"Notepad++\notepad++.exe");
return File.Exists(exePath);
}
diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs
index 53f366891db..23646d0f481 100644
--- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs
@@ -73,8 +73,9 @@ private void InitializeComponent()
this.DisableScriptsOnLoadMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
this.ReturnAllIfNoneSelectedMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
this.ReloadWhenScriptFileChangesMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
- this.toolStripSeparator4 = new BizHawk.WinForms.Controls.ToolStripSeparatorEx();
- this.RegisterToTextEditorsSubMenu = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
+ this.integrationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.GenerateLuaCatsDefinitionMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
+ this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.RegisterSublimeText2MenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
this.RegisterNotePadMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
this.HelpSubMenu = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
@@ -177,6 +178,7 @@ private void InitializeComponent()
this.FileSubMenu,
this.ScriptSubMenu,
this.SettingsSubMenu,
+ this.integrationToolStripMenuItem,
this.HelpSubMenu});
this.menuStrip1.TabIndex = 1;
//
@@ -348,9 +350,7 @@ private void InitializeComponent()
this.SettingsSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.DisableScriptsOnLoadMenuItem,
this.ReturnAllIfNoneSelectedMenuItem,
- this.ReloadWhenScriptFileChangesMenuItem,
- this.toolStripSeparator4,
- this.RegisterToTextEditorsSubMenu});
+ this.ReloadWhenScriptFileChangesMenuItem});
this.SettingsSubMenu.Text = "&Settings";
this.SettingsSubMenu.DropDownOpened += new System.EventHandler(this.OptionsSubMenu_DropDownOpened);
//
@@ -369,22 +369,36 @@ private void InitializeComponent()
this.ReloadWhenScriptFileChangesMenuItem.Text = "Reload When Script File Changes";
this.ReloadWhenScriptFileChangesMenuItem.Click += new System.EventHandler(this.ReloadWhenScriptFileChangesMenuItem_Click);
//
- // RegisterToTextEditorsSubMenu
+ // integrationToolStripMenuItem
//
- this.RegisterToTextEditorsSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.integrationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.GenerateLuaCatsDefinitionMenuItem,
+ this.toolStripSeparator1,
this.RegisterSublimeText2MenuItem,
this.RegisterNotePadMenuItem});
- this.RegisterToTextEditorsSubMenu.Text = "Register To Text Editors";
- this.RegisterToTextEditorsSubMenu.DropDownOpened += new System.EventHandler(this.RegisterToTextEditorsSubMenu_DropDownOpened);
+ this.integrationToolStripMenuItem.Name = "integrationToolStripMenuItem";
+ this.integrationToolStripMenuItem.Size = new System.Drawing.Size(82, 20);
+ this.integrationToolStripMenuItem.Text = "&Integration";
+ this.integrationToolStripMenuItem.DropDownOpened += new System.EventHandler(this.IntegrationMenu_DropDownOpened);
+ //
+ // GenerateLuaCatsDefinitionMenuItem
+ //
+ this.GenerateLuaCatsDefinitionMenuItem.Text = "Export &LuaCATS (Lua Language Server) File";
+ this.GenerateLuaCatsDefinitionMenuItem.Click += new System.EventHandler(this.GenerateLuaCatsDefinitionMenuItem_Click);
+ //
+ // toolStripSeparator1
+ //
+ this.toolStripSeparator1.Name = "toolStripSeparator1";
+ this.toolStripSeparator1.Size = new System.Drawing.Size(322, 6);
//
// RegisterSublimeText2MenuItem
//
- this.RegisterSublimeText2MenuItem.Text = "&Sublime Text 2";
+ this.RegisterSublimeText2MenuItem.Text = "Register to &Sublime Text 2";
this.RegisterSublimeText2MenuItem.Click += new System.EventHandler(this.RegisterSublimeText2MenuItem_Click);
//
// RegisterNotePadMenuItem
//
- this.RegisterNotePadMenuItem.Text = "Notepad++";
+ this.RegisterNotePadMenuItem.Text = "Register to &Notepad++";
this.RegisterNotePadMenuItem.Click += new System.EventHandler(this.RegisterNotePadMenuItem_Click);
//
// HelpSubMenu
@@ -786,14 +800,15 @@ private void InitializeComponent()
private System.Windows.Forms.SplitContainer splitContainer1;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx ReturnAllIfNoneSelectedMenuItem;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx ReloadWhenScriptFileChangesMenuItem;
- private BizHawk.WinForms.Controls.ToolStripSeparatorEx toolStripSeparator4;
- private BizHawk.WinForms.Controls.ToolStripMenuItemEx RegisterToTextEditorsSubMenu;
- private BizHawk.WinForms.Controls.ToolStripMenuItemEx RegisterSublimeText2MenuItem;
- private BizHawk.WinForms.Controls.ToolStripMenuItemEx RegisterNotePadMenuItem;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx SelectAllContextItem;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx CopyContextItem;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx ClearRegisteredFunctionsContextItem;
private BizHawk.WinForms.Controls.ToolStripSeparatorEx toolStripSeparator5;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx ClearRegisteredFunctionsLogContextItem;
+ private System.Windows.Forms.ToolStripMenuItem integrationToolStripMenuItem;
+ private ToolStripMenuItemEx GenerateLuaCatsDefinitionMenuItem;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
+ private ToolStripMenuItemEx RegisterSublimeText2MenuItem;
+ private ToolStripMenuItemEx RegisterNotePadMenuItem;
}
}
\ No newline at end of file
diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs
index ff9ea53a77d..9d485f63be7 100644
--- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs
@@ -1145,7 +1145,7 @@ private void ReloadWhenScriptFileChangesMenuItem_Click(object sender, EventArgs
}
}
- private void RegisterToTextEditorsSubMenu_DropDownOpened(object sender, EventArgs e)
+ private void IntegrationMenu_DropDownOpened(object sender, EventArgs e)
{
// Hide until this one is implemented
RegisterNotePadMenuItem.Visible = false;
@@ -1205,6 +1205,38 @@ private void RegisterNotePadMenuItem_Click(object sender, EventArgs e)
_luaAutoInstaller.InstallBizLua(LuaAutocompleteInstaller.TextEditors.NotePad, LuaImp.Docs);
}
+ private void GenerateLuaCatsDefinitionMenuItem_Click(object sender, EventArgs e)
+ {
+ DialogResult result;
+ string selectedPath;
+ string description = "Set the directory for LuaCATS definitions";
+ if (OSTailoredCode.IsUnixHost)
+ {
+ // FolderBrowserEx doesn't work in Mono for obvious reasons
+ using var f = new FolderBrowserDialog
+ {
+ Description = description,
+ SelectedPath = Config!.PathEntries.LuaAbsolutePath(),
+ };
+ result = f.ShowDialog();
+ selectedPath = f.SelectedPath;
+ }
+ else
+ {
+ using var f = new FolderBrowserEx
+ {
+ Description = description,
+ SelectedPath = Config!.PathEntries.LuaAbsolutePath(),
+ };
+ result = f.ShowDialog();
+ selectedPath = f.SelectedPath;
+ }
+ if (result.IsOk())
+ {
+ LuaImp.Docs.ToLuaLanguageServerDefinitions(selectedPath);
+ }
+ }
+
private void FunctionsListMenuItem_Click(object sender, EventArgs e)
{
new LuaFunctionsForm(LuaImp.Docs).Show();