Improve Lua type documentation and bindings.

The ExtractEmmyLuaAPI utility command, invoked with `--emmy-lua-api`, produces a documentation file that is used by the [OpenRA Lua Language Extension](https://marketplace.visualstudio.com/items?itemName=openra.vscode-openra-lua) to provide documentation and type information is VSCode and VSCode compatible editors when editing the Lua scripts.

We improve the documentation and types produced by this utility in a few ways:
- Require descriptions to be provided for all items.
- Fix the type definitions of the base engine types (cpos, wpos, wangle, wdist, wvec, cvec) to match with the actual bindings on the C# side. Add some extra bindings for these types to increase their utility.
- Introduce ScriptEmmyTypeOverrideAttribute to allow the C# side of the bindings to provide a more specific type. The utility command now requires this to be used to avoid accidentally exporting poor type information.
- Fix a handful of scripts where the new type information revealed warnings.

The ability to ScriptEmmyTypeOverrideAttribute allows parameters and return types to provide a more specific type compared to the previous, weak, type definition. For example LuaValue mapped to `any`, LuaTable mapped to `table`, and LuaFunction mapped to `function`. These types are all non-specific. `any` can be anything, `table` is a table without known types for its keys or values, `function` is a function with an unknown signature.

Now, we can provide specific types. , e.g. instead of `table`, ReinforcementsGlobal.ReinforceWithTransport is able to specify `{ [1]: actor, [2]: actor[] }` - a table with keys 1 and 2, whose values are an actor, and a table of actors respectively. The callback functions in MapGlobal now have signatures, e.g. instead of `function` we have `fun(a: actor):boolean`. In UtilsGlobal, we also make use of generic types. These work in a similar fashion to generics in C#. These methods operate on collections, we can introduce a generic parameter named `T` for the type of the items in those collections. Now the return type and callback parameters can also use that generic type. This means the return type or callback functions operate on the same type as whatever type is in the collection you pass in. e.g. Utils.Do accepts a collection typed as `T[]` with a callback function invoked on each item typed as `fun(item: T)`. If you pass in actors, the callback operates on an actor. If you pass in strings, the callback operates on a string, etc.

Overall, these changes should result in an improved user experience for those editing OpenRA Lua scripts in a compatible IDE.
This commit is contained in:
RoosterDragon
2024-07-27 13:06:22 +01:00
committed by Gustas
parent 14ef6b5774
commit ab28e6a75a
36 changed files with 499 additions and 197 deletions

View File

@@ -33,7 +33,7 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function after a specified delay. The callback function will be called as func().")]
public void AfterDelay(int delay, LuaFunction func)
public void AfterDelay(int delay, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
var f = (LuaFunction)func.CopyReference();
void DoCall()
@@ -53,8 +53,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function for each passenger when it enters a transport. " +
"The callback function will be called as func(Actor transport, Actor passenger).")]
public void OnPassengerEntered(Actor actor, LuaFunction func)
"The callback function will be called as func(transport: actor, passenger: actor).")]
public void OnPassengerEntered(Actor actor, [ScriptEmmyTypeOverride("fun(transport: actor, passenger: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -63,8 +63,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function for each passenger when it exits a transport. " +
"The callback function will be called as func(Actor transport, Actor passenger).")]
public void OnPassengerExited(Actor actor, LuaFunction func)
"The callback function will be called as func(transport: actor, passenger: actor).")]
public void OnPassengerExited(Actor actor, [ScriptEmmyTypeOverride("fun(transport: actor, passenger: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -73,8 +73,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function each tick that the actor is idle. " +
"The callback function will be called as func(Actor self).")]
public void OnIdle(Actor actor, LuaFunction func)
"The callback function will be called as func(self: actor).")]
public void OnIdle(Actor actor, [ScriptEmmyTypeOverride("fun(self: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -83,8 +83,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when the actor is damaged. The callback " +
"function will be called as func(Actor self, Actor attacker, int damage).")]
public void OnDamaged(Actor actor, LuaFunction func)
"function will be called as func(self: actor, attacker: actor, damage: integer).")]
public void OnDamaged(Actor actor, [ScriptEmmyTypeOverride("fun(self: actor, attacker: actor, damage: integer)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -93,8 +93,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when the actor is killed. The callback " +
"function will be called as func(Actor self, Actor killer).")]
public void OnKilled(Actor actor, LuaFunction func)
"function will be called as func(self: actor, killer: actor).")]
public void OnKilled(Actor actor, [ScriptEmmyTypeOverride("fun(self: actor, killer: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -104,7 +104,7 @@ namespace OpenRA.Mods.Common.Scripting
[Desc("Call a function when all of the actors in a group are killed. The callback " +
"function will be called as func().")]
public void OnAllKilled(Actor[] actors, LuaFunction func)
public void OnAllKilled(Actor[] actors, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
if (actors == null)
throw new NullReferenceException(nameof(actors));
@@ -131,8 +131,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when one of the actors in a group is killed. The callback " +
"function will be called as func(Actor killed).")]
public void OnAnyKilled(Actor[] actors, LuaFunction func)
"function will be called as func(killed: actor).")]
public void OnAnyKilled(Actor[] actors, [ScriptEmmyTypeOverride("fun(killed: actor)")] LuaFunction func)
{
var called = false;
var f = (LuaFunction)func.CopyReference();
@@ -163,8 +163,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this actor produces another actor. " +
"The callback function will be called as func(Actor producer, Actor produced).")]
public void OnProduction(Actor actors, LuaFunction func)
"The callback function will be called as func(producer: actor, produced: actor).")]
public void OnProduction(Actor actors, [ScriptEmmyTypeOverride("fun(producer: actor, produced: actor)")] LuaFunction func)
{
if (actors == null)
throw new NullReferenceException(nameof(actors));
@@ -173,15 +173,15 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when any actor produces another actor. The callback " +
"function will be called as func(Actor producer, Actor produced, string productionType).")]
public void OnAnyProduction(LuaFunction func)
"function will be called as func(producer: actor, produced: actor, productionType: string).")]
public void OnAnyProduction([ScriptEmmyTypeOverride("fun(producer: actor, produced: actor, productionType: string)")] LuaFunction func)
{
GetScriptTriggers(Context.World.WorldActor).RegisterCallback(Trigger.OnOtherProduction, func, Context);
}
[Desc("Call a function when this player completes all primary objectives. " +
"The callback function will be called as func(Player player).")]
public void OnPlayerWon(Player player, LuaFunction func)
"The callback function will be called as func(p: player).")]
public void OnPlayerWon(Player player, [ScriptEmmyTypeOverride("fun(p: player)")] LuaFunction func)
{
if (player == null)
throw new NullReferenceException(nameof(player));
@@ -190,8 +190,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this player fails any primary objective. " +
"The callback function will be called as func(Player player).")]
public void OnPlayerLost(Player player, LuaFunction func)
"The callback function will be called as func(p: player).")]
public void OnPlayerLost(Player player, [ScriptEmmyTypeOverride("fun(p: player)")] LuaFunction func)
{
if (player == null)
throw new NullReferenceException(nameof(player));
@@ -200,8 +200,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this player is assigned a new objective. " +
"The callback function will be called as func(Player player, int objectiveID).")]
public void OnObjectiveAdded(Player player, LuaFunction func)
"The callback function will be called as func(p: player, objectiveId: integer).")]
public void OnObjectiveAdded(Player player, [ScriptEmmyTypeOverride("fun(p: player, objectiveId: integer)")] LuaFunction func)
{
if (player == null)
throw new NullReferenceException(nameof(player));
@@ -210,8 +210,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this player completes an objective. " +
"The callback function will be called as func(Player player, int objectiveID).")]
public void OnObjectiveCompleted(Player player, LuaFunction func)
"The callback function will be called as func(p: player, objectiveId: integer).")]
public void OnObjectiveCompleted(Player player, [ScriptEmmyTypeOverride("fun(p: player, objectiveId: integer)")] LuaFunction func)
{
if (player == null)
throw new NullReferenceException(nameof(player));
@@ -220,8 +220,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this player fails an objective. " +
"The callback function will be called as func(Player player, int objectiveID).")]
public void OnObjectiveFailed(Player player, LuaFunction func)
"The callback function will be called as func(p: player, objectiveId: integer).")]
public void OnObjectiveFailed(Player player, [ScriptEmmyTypeOverride("fun(p: player, objectiveId: integer)")] LuaFunction func)
{
if (player == null)
throw new NullReferenceException(nameof(player));
@@ -230,8 +230,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this actor is added to the world. " +
"The callback function will be called as func(Actor self).")]
public void OnAddedToWorld(Actor actor, LuaFunction func)
"The callback function will be called as func(self: actor).")]
public void OnAddedToWorld(Actor actor, [ScriptEmmyTypeOverride("fun(self: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -240,8 +240,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this actor is removed from the world. " +
"The callback function will be called as func(Actor self).")]
public void OnRemovedFromWorld(Actor actor, LuaFunction func)
"The callback function will be called as func(self: actor).")]
public void OnRemovedFromWorld(Actor actor, [ScriptEmmyTypeOverride("fun(self: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -251,7 +251,7 @@ namespace OpenRA.Mods.Common.Scripting
[Desc("Call a function when all of the actors in a group have been removed from the world. " +
"The callback function will be called as func().")]
public void OnAllRemovedFromWorld(Actor[] actors, LuaFunction func)
public void OnAllRemovedFromWorld(Actor[] actors, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
if (actors == null)
throw new NullReferenceException(nameof(actors));
@@ -303,8 +303,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this actor is captured. The callback function " +
"will be called as func(Actor self, Actor captor, Player oldOwner, Player newOwner).")]
public void OnCapture(Actor actors, LuaFunction func)
"will be called as func(self: actor, captor: actor, oldOwner: player, newOwner: player).")]
public void OnCapture(Actor actors, [ScriptEmmyTypeOverride("fun(self: actor, captor: actor, oldOwner: player, newOwner: player)")] LuaFunction func)
{
if (actors == null)
throw new NullReferenceException(nameof(actors));
@@ -314,7 +314,7 @@ namespace OpenRA.Mods.Common.Scripting
[Desc("Call a function when this actor is killed or captured. " +
"The callback function will be called as func().")]
public void OnKilledOrCaptured(Actor actor, LuaFunction func)
public void OnKilledOrCaptured(Actor actor, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
var called = false;
@@ -346,7 +346,7 @@ namespace OpenRA.Mods.Common.Scripting
[Desc("Call a function when all of the actors in a group have been killed or captured. " +
"The callback function will be called as func().")]
public void OnAllKilledOrCaptured(Actor[] actors, LuaFunction func)
public void OnAllKilledOrCaptured(Actor[] actors, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
if (actors == null)
throw new NullReferenceException(nameof(actors));
@@ -379,9 +379,9 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when a ground-based actor enters this cell footprint. " +
"Returns the trigger id for later removal using RemoveFootprintTrigger(int id). " +
"The callback function will be called as func(Actor a, int id).")]
public int OnEnteredFootprint(CPos[] cells, LuaFunction func)
"Returns the trigger ID for later removal using RemoveFootprintTrigger(id: integer). " +
"The callback function will be called as func(a: actor, id: integer).")]
public int OnEnteredFootprint(CPos[] cells, [ScriptEmmyTypeOverride("fun(a: actor, id: integer)")] LuaFunction func)
{
// We can't easily dispose onEntry, so we'll have to rely on finalization for it.
var onEntry = (LuaFunction)func.CopyReference();
@@ -406,9 +406,9 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when a ground-based actor leaves this cell footprint. " +
"Returns the trigger id for later removal using RemoveFootprintTrigger(int id). " +
"The callback function will be called as func(Actor a, int id).")]
public int OnExitedFootprint(CPos[] cells, LuaFunction func)
"Returns the trigger ID for later removal using RemoveFootprintTrigger(id: integer). " +
"The callback function will be called as func(a: actor, id: integer).")]
public int OnExitedFootprint(CPos[] cells, [ScriptEmmyTypeOverride("fun(a: actor, id: integer)")] LuaFunction func)
{
// We can't easily dispose onExit, so we'll have to rely on finalization for it.
var onExit = (LuaFunction)func.CopyReference();
@@ -439,9 +439,9 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when an actor enters this range. " +
"Returns the trigger id for later removal using RemoveProximityTrigger(int id). " +
"The callback function will be called as func(Actor a, int id).")]
public int OnEnteredProximityTrigger(WPos pos, WDist range, LuaFunction func)
"Returns the trigger ID for later removal using RemoveProximityTrigger(id: integer). " +
"The callback function will be called as func(a: actor, id: integer).")]
public int OnEnteredProximityTrigger(WPos pos, WDist range, [ScriptEmmyTypeOverride("fun(a: actor, id: integer)")] LuaFunction func)
{
// We can't easily dispose onEntry, so we'll have to rely on finalization for it.
var onEntry = (LuaFunction)func.CopyReference();
@@ -466,9 +466,9 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when an actor leaves this range. " +
"Returns the trigger id for later removal using RemoveProximityTrigger(int id). " +
"The callback function will be called as func(Actor a, int id).")]
public int OnExitedProximityTrigger(WPos pos, WDist range, LuaFunction func)
"Returns the trigger ID for later removal using RemoveProximityTrigger(id: integer). " +
"The callback function will be called as func(a: actor, id: integer).")]
public int OnExitedProximityTrigger(WPos pos, WDist range, [ScriptEmmyTypeOverride("fun(a: actor, id: integer)")] LuaFunction func)
{
// We can't easily dispose onExit, so we'll have to rely on finalization for it.
var onExit = (LuaFunction)func.CopyReference();
@@ -499,8 +499,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this actor is infiltrated. The callback function " +
"will be called as func(Actor self, Actor infiltrator).")]
public void OnInfiltrated(Actor actor, LuaFunction func)
"will be called as func(self: actor, infiltrator: actor).")]
public void OnInfiltrated(Actor actor, [ScriptEmmyTypeOverride("fun(self: actor, infiltrator: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -509,9 +509,9 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this actor is discovered by an enemy or a player with a Neutral stance. " +
"The callback function will be called as func(Actor discovered, Player discoverer). " +
"The callback function will be called as func(discovered: actor, discoverer: player). " +
"The player actor needs the 'EnemyWatcher' trait. The actors to discover need the 'AnnounceOnSeen' trait.")]
public void OnDiscovered(Actor actor, LuaFunction func)
public void OnDiscovered(Actor actor, [ScriptEmmyTypeOverride("fun(discovered: actor, discoverer: player)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -520,9 +520,10 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this player is discovered by an enemy or neutral player. " +
"The callback function will be called as func(Player discovered, Player discoverer, Actor discoveredActor)." +
"The callback function will be called as func(discovered: player, discoverer: player, discoveredActor: actor)." +
"The player actor needs the 'EnemyWatcher' trait. The actors to discover need the 'AnnounceOnSeen' trait.")]
public void OnPlayerDiscovered(Player discovered, LuaFunction func)
public void OnPlayerDiscovered(
Player discovered, [ScriptEmmyTypeOverride("fun(discovered: player, discoverer: player, discoveredActor: actor)")] LuaFunction func)
{
if (discovered == null)
throw new NullReferenceException(nameof(discovered));
@@ -531,8 +532,8 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when this actor is sold. The callback function " +
"will be called as func(Actor self).")]
public void OnSold(Actor actor, LuaFunction func)
"will be called as func(self: actor).")]
public void OnSold(Actor actor, [ScriptEmmyTypeOverride("fun(self: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
@@ -541,7 +542,7 @@ namespace OpenRA.Mods.Common.Scripting
}
[Desc("Call a function when the game timer expires. The callback function will be called as func().")]
public void OnTimerExpired(LuaFunction func)
public void OnTimerExpired([ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
GetScriptTriggers(Context.World.WorldActor).RegisterCallback(Trigger.OnTimerExpired, func, Context);
}