Skip to content

Commit 5846303

Browse files
committed
feat(scripting-lua): expose C# enums to Lua deterministically via RegisterScriptEnum
Add RegisterScriptEnum<TEnum>() / RegisterScriptEnum(Type) container extensions and a ScriptEnumData descriptor. Registered enums flow to the engine as a typed list and are exposed during initialization (RegisterEnums), independently of Lua meta-file generation, which previously ran after enum registration and on a background thread — so enums were not reliably exposed. Explicitly-registered enums are also fed to the documentation generator so the LSP meta file includes them.
1 parent 30c78b3 commit 5846303

4 files changed

Lines changed: 127 additions & 5 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace SquidStd.Scripting.Lua.Data.Internal;
2+
3+
/// <summary>
4+
/// Record describing an enum type to expose to Lua as a read-only global table, keyed by member
5+
/// name. Registered through <c>RegisterScriptEnum</c> and consumed during engine initialization.
6+
/// </summary>
7+
public sealed record ScriptEnumData
8+
{
9+
/// <summary>
10+
/// The .NET enum type to expose to Lua.
11+
/// </summary>
12+
public Type EnumType { get; }
13+
14+
/// <summary>
15+
/// Initializes a new instance of the ScriptEnumData record.
16+
/// </summary>
17+
/// <param name="enumType">The .NET enum type to expose to Lua.</param>
18+
/// <exception cref="ArgumentNullException">Thrown when enumType is null.</exception>
19+
/// <exception cref="ArgumentException">Thrown when enumType is not an enum.</exception>
20+
public ScriptEnumData(Type enumType)
21+
{
22+
ArgumentNullException.ThrowIfNull(enumType);
23+
24+
if (!enumType.IsEnum)
25+
{
26+
throw new ArgumentException("Type must be an enum.", nameof(enumType));
27+
}
28+
29+
EnumType = enumType;
30+
}
31+
}

src/SquidStd.Scripting.Lua/Extensions/Scripts/AddScriptModuleExtension.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,29 @@ public IContainer RegisterScriptModule(Type scriptModule)
6565
/// <returns>The container for method chaining.</returns>
6666
public IContainer RegisterScriptModule<TScriptModule>() where TScriptModule : class
6767
=> container.RegisterScriptModule(typeof(TScriptModule));
68+
69+
/// <summary>
70+
/// Registers an enum type to expose to Lua as a read-only, case-insensitive global table
71+
/// keyed by member name. The exposure is deterministic: the enum is registered during engine
72+
/// initialization, independently of Lua meta-file generation.
73+
/// </summary>
74+
/// <param name="enumType">The enum type to expose.</param>
75+
/// <returns>The container for method chaining.</returns>
76+
/// <exception cref="ArgumentNullException">Thrown when enumType is null.</exception>
77+
/// <exception cref="ArgumentException">Thrown when enumType is not an enum.</exception>
78+
public IContainer RegisterScriptEnum(Type enumType)
79+
{
80+
container.AddToRegisterTypedList(new ScriptEnumData(enumType));
81+
82+
return container;
83+
}
84+
85+
/// <summary>
86+
/// Registers an enum type to expose to Lua using a generic type parameter.
87+
/// </summary>
88+
/// <typeparam name="TEnum">The enum type to expose.</typeparam>
89+
/// <returns>The container for method chaining.</returns>
90+
public IContainer RegisterScriptEnum<TEnum>() where TEnum : struct, Enum
91+
=> container.RegisterScriptEnum(typeof(TEnum));
6892
}
6993
}

src/SquidStd.Scripting.Lua/Services/LuaScriptEngineService.cs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class LuaScriptEngineService : IScriptEngineService, IDisposable
5252
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> _manualModuleFunctions = new();
5353
private readonly ConcurrentDictionary<string, string> _scriptCache = new();
5454
private readonly List<ScriptModuleData> _scriptModules;
55+
private readonly List<ScriptEnumData> _scriptEnums;
5556
private readonly IContainer _serviceProvider;
5657

5758
private int _cacheHits;
@@ -85,7 +86,8 @@ public LuaScriptEngineService(
8586
IContainer serviceProvider,
8687
LuaEngineConfig engineConfig,
8788
List<ScriptModuleData> scriptModules = null,
88-
List<ScriptUserData> loadedUserData = null
89+
List<ScriptUserData> loadedUserData = null,
90+
List<ScriptEnumData> scriptEnums = null
8991
)
9092
{
9193
JsonUtils.RegisterJsonContext(SquidStdScriptJsonContext.Default);
@@ -94,6 +96,7 @@ public LuaScriptEngineService(
9496
loadedUserData ??= new();
9597

9698
_scriptModules = scriptModules;
99+
_scriptEnums = scriptEnums ?? new();
97100
_directoriesConfig = directoriesConfig;
98101
_serviceProvider = serviceProvider;
99102
_engineConfig = engineConfig;
@@ -1101,6 +1104,16 @@ private async Task GenerateLuaMetaFileAsync(CancellationToken cancellationToken)
11011104
LuaDocumentationGenerator.AddClassToGenerate(userData.UserType);
11021105
}
11031106

1107+
// Document explicitly-registered enums (RegisterScriptEnum) too.
1108+
foreach (var scriptEnum in _scriptEnums)
1109+
{
1110+
if (scriptEnum?.EnumType is { IsEnum: true } enumType &&
1111+
!LuaDocumentationGenerator.FoundEnums.Contains(enumType))
1112+
{
1113+
LuaDocumentationGenerator.FoundEnums.Add(enumType);
1114+
}
1115+
}
1116+
11041117
AddConstant("engine_version", _engineConfig.EngineVersion);
11051118

11061119
// Generate meta.lua
@@ -1420,9 +1433,25 @@ private void RegisterEnum(Type enumType)
14201433
[RequiresUnreferencedCode("Enum metadata is discovered dynamically when building Lua documentation.")]
14211434
private void RegisterEnums()
14221435
{
1423-
var enumsFound = LuaDocumentationGenerator.FoundEnums.ToArray();
1436+
// Explicitly registered enums (RegisterScriptEnum) are the deterministic source; the enums
1437+
// discovered by the documentation generator are folded in for backward compatibility. A set
1438+
// keyed by type prevents registering the same enum twice.
1439+
var enumTypes = new HashSet<Type>();
1440+
1441+
foreach (var scriptEnum in _scriptEnums)
1442+
{
1443+
if (scriptEnum?.EnumType is not null)
1444+
{
1445+
enumTypes.Add(scriptEnum.EnumType);
1446+
}
1447+
}
1448+
1449+
foreach (var enumType in LuaDocumentationGenerator.FoundEnums.ToArray())
1450+
{
1451+
enumTypes.Add(enumType);
1452+
}
14241453

1425-
foreach (var enumType in enumsFound)
1454+
foreach (var enumType in enumTypes)
14261455
{
14271456
RegisterEnum(enumType);
14281457
}

tests/SquidStd.Tests/Scripting/Lua/LuaScriptEngineServiceTests.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,41 @@ public async Task StartAsync_AttachesEventBridgeAndAddsBootstrapConstants()
213213
Assert.Equal("SquidStd", engine.ExecuteFunction("ENGINE").Data);
214214
}
215215

216+
[Fact]
217+
public async Task RegisterScriptEnum_ExposesReadOnlyCaseInsensitiveGlobalTable()
218+
{
219+
using var temp = new TempDirectory();
220+
using var container = new Container();
221+
using var engine = CreateEngine(
222+
temp,
223+
container,
224+
scriptEnums: [new ScriptEnumData(typeof(SampleColor))]
225+
);
226+
227+
await engine.StartAsync(CancellationToken.None);
228+
229+
// Snake-cased global name, member values, and case-insensitive access.
230+
Assert.Equal(2d, engine.ExecuteFunction("sample_color.Green").Data);
231+
Assert.Equal(4d, engine.ExecuteFunction("sample_color.blue").Data);
232+
233+
// Unknown members read as nil.
234+
Assert.Equal(true, engine.ExecuteFunction("sample_color.Missing == nil").Data);
235+
236+
// Adding a new member is rejected (the enum table is guarded against extension).
237+
Assert.Equal(
238+
false,
239+
engine.ExecuteFunction(
240+
"(function() local ok = pcall(function() sample_color.NewColor = 99 end); return ok end)()"
241+
).Data
242+
);
243+
}
244+
216245
private static LuaScriptEngineService CreateEngine(
217246
TempDirectory temp,
218247
IContainer container,
219248
List<ScriptModuleData>? scriptModules = null,
220-
List<ScriptUserData>? loadedUserData = null
249+
List<ScriptUserData>? loadedUserData = null,
250+
List<ScriptEnumData>? scriptEnums = null
221251
)
222252
{
223253
var scriptsDirectory = temp.Combine("scripts");
@@ -229,10 +259,18 @@ private static LuaScriptEngineService CreateEngine(
229259
container,
230260
new(luarcDirectory, scriptsDirectory, "SquidStd", "1.0.0"),
231261
scriptModules,
232-
loadedUserData
262+
loadedUserData,
263+
scriptEnums
233264
);
234265
}
235266

267+
private enum SampleColor
268+
{
269+
Red = 1,
270+
Green = 2,
271+
Blue = 4
272+
}
273+
236274
public sealed class FiveArgumentUserData(int first, int second, int third, int fourth, int fifth)
237275
{
238276
public int Total { get; } = first + second + third + fourth + fifth;

0 commit comments

Comments
 (0)