Skip to content

Improve Lua type documentation and bindings.#21501

Merged
PunkPun merged 1 commit intoOpenRA:bleedfrom
RoosterDragon:emmy
Aug 3, 2024
Merged

Improve Lua type documentation and bindings.#21501
PunkPun merged 1 commit intoOpenRA:bleedfrom
RoosterDragon:emmy

Conversation

@RoosterDragon
Copy link
Member

@RoosterDragon RoosterDragon commented Jul 27, 2024

The ExtractEmmyLuaAPI utility command, invoked with --emmy-lua-api, produces a documentation file that is used by the OpenRA Lua Language Extension 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.


Contributes to #20632 (comment)

As an example, let's introduce some errors in allies01.

SetUnitStances = function()
Utils.Do(Map.NamedActors, function(a)
if a.Owner == Greece then
a.Stance = "Defend"
end
end)
end

Source

Diagnostics

The new definitions allow it to tell us:

  • That we are expecting too many args from the function.
  • That we made a typo on a.Owner - this is because it is now able to infer that a is an actor, because Utils.Do is operating on a collection of actors.
  • That the return value from the function isn't needed.

These errors would've not been raised previously.


Below, please find a diff of the output from the utility command.

Diff for the utility command 'ra --emmy-lua-api'
diff --git a/emmy.lua b/emmy.lua
--- a/emmy.lua
+++ b/emmy.lua
@@ -1,6 +1,6 @@
 -- This is an automatically generated Lua API definition generated for {DEV_VERSION} of OpenRA.
 -- https://wiki.openra.net/Utility was used with the --emmy-lua-api parameter.
--- See https://docs.openra.net/en/latest/release/lua/ for human readable documentation.
+-- See https://docs.openra.net/en/release/lua/ for human readable documentation.
 
 --- This file only lists function "signatures", causing Lua Diagnostics errors: "Annotations specify that a return value is required here."
 --- and Lua Diagnostics warnings "Unused local" for the functions' parameters.
@@ -17,11 +17,14 @@ function Tick() end
 
 
 --- Base engine types.
+
 ---@class cpos
 ---@field X integer
 ---@field Y integer
+---@field Layer integer
 ---@operator add(cvec): cpos
 ---@operator sub(cvec): cpos
+---@operator sub(cpos): cvec
 
 ---@class wpos
 ---@field X integer
@@ -29,6 +32,7 @@ function Tick() end
 ---@field Z integer
 ---@operator add(wvec): wpos
 ---@operator sub(wvec): wpos
+---@operator sub(wpos): wvec
 
 ---@class wangle
 ---@field Angle integer
@@ -37,19 +41,32 @@ function Tick() end
 
 ---@class wdist
 ---@field Length integer
+---@operator add(wdist): wdist
+---@operator sub(wdist): wdist
+---@operator unm(wdist): wdist
+---@operator mul(integer): wdist
+---@operator div(integer): wdist
 
 ---@class wvec
 ---@field X integer
 ---@field Y integer
 ---@field Z integer
+---@field Facing wangle
 ---@operator add(wvec): wvec
 ---@operator sub(wvec): wvec
+---@operator unm(wvec): wvec
+---@operator mul(integer): wvec
+---@operator div(integer): wvec
 
 ---@class cvec
 ---@field X integer
 ---@field Y integer
+---@field Length integer
 ---@operator add(cvec): cvec
 ---@operator sub(cvec): cvec
+---@operator unm(cvec): cvec
+---@operator mul(integer): cvec
+---@operator div(integer): cvec
 
 ---@class color
 local color = { };
@@ -60,7 +77,6 @@ local color = { };
 ---@field Location cpos?
 ---@field Owner player | string
 ---@field Facing wangle?
----@field TerrainOrientation WRot?
 ---@field CreationActivityDelay integer?
 ---@field SubCell SubCell?
 ---@field CenterPosition wpos?
@@ -79,7 +95,7 @@ local color = { };
 ---@field Plug string?
 ---@field RallyPoint cpos[]?
 ---@field ProductionSpawnLocation cpos?
----@field Region CVec[]?
+---@field Region cvec[]?
 ---@field ScriptTags string[]?
 ---@field TurretFacing wangle?
 ---@field BodyAnimationFrame integer?
@@ -128,6 +144,7 @@ Actor = {
     ---@return integer
     BuildTime = function(type, queue) end;
 
+    --- Returns the cost of the requested unit given by the Valued trait.
     ---@param type string
     ---@return integer
     Cost = function(type) end;
@@ -148,32 +165,40 @@ Actor = {
 ---Global variable provided by the game scripting engine.
 Angle = {
 
+    --- 768 units = 90 degrees
     ---@type wangle
     East = nil;
 
-    --- Create an arbitrary angle.
+    --- Create an arbitrary angle. 1024 units = 360 degrees. North is 0. Units increase *counter* clockwise. Comparisongiven to degrees increasing clockwise.
     ---@param a integer
     ---@return wangle
     New = function(a) end;
 
+    --- 0/1024 units = 0/360 degrees
     ---@type wangle
     North = nil;
 
+    --- 896 units = 45 degrees
     ---@type wangle
     NorthEast = nil;
 
+    --- 128 units = 315 degrees
     ---@type wangle
     NorthWest = nil;
 
+    --- 512 units = 180 degrees
     ---@type wangle
     South = nil;
 
+    --- 640 units = 135 degrees
     ---@type wangle
     SouthEast = nil;
 
+    --- 384 units = 225 degrees
     ---@type wangle
     SouthWest = nil;
 
+    --- 256 units = 270 degrees
     ---@type wangle
     West = nil;
 }
@@ -200,36 +225,47 @@ Camera = {
 ---Global variable provided by the game scripting engine.
 HSLColor = {
 
+    --- FromHex("00FFFF")
     ---@type color
     Aqua = nil;
 
+    --- FromHex("000000")
     ---@type color
     Black = nil;
 
+    --- FromHex("0000FF")
     ---@type color
     Blue = nil;
 
+    --- FromHex("A52A2A")
     ---@type color
     Brown = nil;
 
+    --- FromHex("00FFFF")
     ---@type color
     Cyan = nil;
 
+    --- FromHex("00008B")
     ---@type color
     DarkBlue = nil;
 
+    --- FromHex("008B8B")
     ---@type color
     DarkCyan = nil;
 
+    --- FromHex("A9A9A9")
     ---@type color
     DarkGray = nil;
 
+    --- FromHex("006400")
     ---@type color
     DarkGreen = nil;
 
+    --- FromHex("FF8C00")
     ---@type color
     DarkOrange = nil;
 
+    --- FromHex("8B0000")
     ---@type color
     DarkRed = nil;
 
@@ -246,48 +282,63 @@ HSLColor = {
     ---@return color
     FromRGB = function(red, green, blue, alpha) end;
 
+    --- FromHex("FF00FF")
     ---@type color
     Fuchsia = nil;
 
+    --- FromHex("FFD700")
     ---@type color
     Gold = nil;
 
+    --- FromHex("808080")
     ---@type color
     Gray = nil;
 
+    --- FromHex("008000")
     ---@type color
     Green = nil;
 
+    --- FromHex("7CFC00")
     ---@type color
     LawnGreen = nil;
 
+    --- FromHex("ADD8E6")
     ---@type color
     LightBlue = nil;
 
+    --- FromHex("E0FFFF")
     ---@type color
     LightCyan = nil;
 
+    --- FromHex("D3D3D3")
     ---@type color
     LightGray = nil;
 
+    --- FromHex("90EE90")
     ---@type color
     LightGreen = nil;
 
+    --- FromHex("FFFFE0")
     ---@type color
     LightYellow = nil;
 
+    --- FromHex("00FF00")
     ---@type color
     Lime = nil;
 
+    --- FromHex("32CD32")
     ---@type color
     LimeGreen = nil;
 
+    --- FromHex("FF00FF")
     ---@type color
     Magenta = nil;
 
+    --- FromHex("800000")
     ---@type color
     Maroon = nil;
 
+    --- FromHex("000080")
     ---@type color
     Navy = nil;
 
@@ -298,33 +349,43 @@ HSLColor = {
     ---@return color
     New = function(hue, saturation, luminosity) end;
 
+    --- FromHex("808000")
     ---@type color
     Olive = nil;
 
+    --- FromHex("FFA500")
     ---@type color
     Orange = nil;
 
+    --- FromHex("FF4500")
     ---@type color
     OrangeRed = nil;
 
+    --- FromHex("800080")
     ---@type color
     Purple = nil;
 
+    --- FromHex("FF0000")
     ---@type color
     Red = nil;
 
+    --- FromHex("FA8072")
     ---@type color
     Salmon = nil;
 
+    --- FromHex("87CEEB")
     ---@type color
     SkyBlue = nil;
 
+    --- FromHex("008080")
     ---@type color
     Teal = nil;
 
+    --- FromHex("FFFFFF")
     ---@type color
     White = nil;
 
+    --- FromHex("FFFF00")
     ---@type color
     Yellow = nil;
 }
@@ -332,12 +393,19 @@ HSLColor = {
 ---Global variable provided by the game scripting engine.
 CPos = {
 
-    --- Create a new CPos with the specified coordinates.
+    --- Create a new CPos with the specified coordinates on the ground (layer = 0).
     ---@param x integer
     ---@param y integer
     ---@return cpos
     New = function(x, y) end;
 
+    --- Create a new CPos with the specified coordinates on the specified layer. The ground is layer 0, other layers have a unique ID. Examples include tunnels, underground, and elevated bridges.
+    ---@param x integer
+    ---@param y integer
+    ---@param layer integer
+    ---@return cpos
+    NewWithLayer = function(x, y, layer) end;
+
     --- The cell coordinate origin.
     ---@type cpos
     Zero = nil;
@@ -360,21 +428,27 @@ CVec = {
 ---Global variable provided by the game scripting engine.
 DateTime = {
 
+    --- Get the current day (1-31).
     ---@type integer
     CurrentDay = nil;
 
+    --- Get the current hour (0-23).
     ---@type integer
     CurrentHour = nil;
 
+    --- Get the current minute (0-59).
     ---@type integer
     CurrentMinute = nil;
 
+    --- Get the current month (1-12).
     ---@type integer
     CurrentMonth = nil;
 
+    --- Get the current second (0-59).
     ---@type integer
     CurrentSecond = nil;
 
+    --- Get the current year (1-9999).
     ---@type integer
     CurrentYear = nil;
 
@@ -409,9 +483,11 @@ DateTime = {
 ---Global variable provided by the game scripting engine.
 Lighting = {
 
+    --- Strength of the lighting (0-1).
     ---@type number
     Ambient = nil;
 
+    --- Blue component (0-1).
     ---@type number
     Blue = nil;
 
@@ -420,9 +496,11 @@ Lighting = {
     ---@param ticks? integer
     Flash = function(type, ticks) end;
 
+    --- Green component (0-1).
     ---@type number
     Green = nil;
 
+    --- Red component (0-1).
     ---@type number
     Red = nil;
 }
@@ -433,14 +511,14 @@ Map = {
     --- Returns a table of all actors within the requested rectangle, filtered using the specified function.
     ---@param topLeft wpos
     ---@param bottomRight wpos
-    ---@param filter? function
+    ---@param filter? fun(a: actor):boolean
     ---@return actor[]
     ActorsInBox = function(topLeft, bottomRight, filter) end;
 
     --- Returns a table of all actors within the requested region, filtered using the specified function.
     ---@param location wpos
     ---@param radius wdist
-    ---@param filter? function
+    ---@param filter? fun(a: actor):boolean
     ---@return actor[]
     ActorsInCircle = function(location, radius, filter) end;
 
@@ -469,9 +547,9 @@ Map = {
     ClosestEdgeCell = function(givenCell) end;
 
     --- Returns the first cell on the visible border of the map from the given cell,
-    --- matching the filter function called as function(CPos cell).
+    --- matching the filter function called as function(cell: cpos):boolean.
     ---@param givenCell cpos
-    ---@param filter function
+    ---@param filter fun(cell: cpos):boolean
     ---@return cpos
     ClosestMatchingEdgeCell = function(givenCell, filter) end;
 
@@ -490,13 +568,13 @@ Map = {
 
     --- Returns the value of a `ScriptLobbyDropdown` selected in the game lobby.
     ---@param id string
-    ---@return any
+    ---@return string
     LobbyOption = function(id) end;
 
     --- Returns the value of a `ScriptLobbyDropdown` selected in the game lobby or fallback to a default value.
     ---@param id string
     ---@param fallback string
-    ---@return any
+    ---@return string
     LobbyOptionOrDefault = function(id, fallback) end;
 
     --- Returns the actor that was specified with a given name in the map file (or nil, if the actor is dead or not found).
@@ -561,17 +639,17 @@ Media = {
 
     --- Play a video fullscreen. File name has to include the file extension.
     ---@param videoFileName string
-    ---@param onPlayComplete? function
+    ---@param onPlayComplete? fun()
     PlayMovieFullscreen = function(videoFileName, onPlayComplete) end;
 
     --- Play a video in the radar window. File name has to include the file extension.
     ---@param videoFileName string
-    ---@param onPlayComplete? function
+    ---@param onPlayComplete? fun()
     PlayMovieInRadar = function(videoFileName, onPlayComplete) end;
 
     --- Play track defined in music.yaml or map.yaml, or keep track empty for playing a random song.
     ---@param track? string
-    ---@param onPlayComplete? function
+    ---@param onPlayComplete? fun()
     PlayMusic = function(track, onPlayComplete) end;
 
     --- Play a sound file
@@ -605,7 +683,7 @@ Player = {
     GetPlayer = function(name) end;
 
     --- Returns a table of players filtered by the specified function.
-    ---@param filter function
+    ---@param filter fun(p: player):boolean
     ---@return player[]
     GetPlayers = function(filter) end;
 }
@@ -624,25 +702,25 @@ Radar = {
 ---Global variable provided by the game scripting engine.
 Reinforcements = {
 
-    --- Send reinforcements consisting of multiple units. Supports ground-based, naval and air units. The first member of the entryPath array will be the units' spawnpoint, while the last one will be their destination. If actionFunc is given, it will be executed once a unit has reached its destination. actionFunc will be called as actionFunc(Actor actor). Returns a table containing the deployed units.
+    --- Send reinforcements consisting of multiple units. Supports ground-based, naval and air units. The first member of the entryPath array will be the units' spawnpoint, while the last one will be their destination. If actionFunc is given, it will be executed once a unit has reached its destination. actionFunc will be called as actionFunc(a: actor). Returns a table containing the deployed units.
     ---@param owner player
     ---@param actorTypes string[]
     ---@param entryPath cpos[]
     ---@param interval? integer
-    ---@param actionFunc? function
+    ---@param actionFunc? fun(a: actor)
     ---@return actor[]
     Reinforce = function(owner, actorTypes, entryPath, interval, actionFunc) end;
 
-    --- Send reinforcements in a transport. A transport can be a ground unit (APC etc.), ships and aircraft. The first member of the entryPath array will be the spawnpoint for the transport, while the last one will be its destination. The last member of the exitPath array is be the place where the transport will be removed from the game. When the transport has reached the destination, it will unload its cargo unless a custom actionFunc has been supplied. Afterwards, the transport will follow the exitPath and leave the map, unless a custom exitFunc has been supplied. actionFunc will be called as actionFunc(Actor transport, Actor[] cargo). exitFunc will be called as exitFunc(Actor transport). dropRange determines how many cells away the transport will try to land if the actual destination is blocked (if the transport is an aircraft). Returns a table in which the first value is the transport, and the second a table containing the deployed units.
+    --- Send reinforcements in a transport. A transport can be a ground unit (APC etc.), ships and aircraft. The first member of the entryPath array will be the spawnpoint for the transport, while the last one will be its destination. The last member of the exitPath array is be the place where the transport will be removed from the game. When the transport has reached the destination, it will unload its cargo unless a custom actionFunc has been supplied. Afterwards, the transport will follow the exitPath and leave the map, unless a custom exitFunc has been supplied. actionFunc will be called as actionFunc(transport: actor, cargo: actor[]). exitFunc will be called as exitFunc(transport: actor). dropRange determines how many cells away the transport will try to land if the actual destination is blocked (if the transport is an aircraft). Returns a table in which the first value is the transport, and the second a table containing the deployed units.
     ---@param owner player
     ---@param actorType string
-    ---@param cargoTypes string[]
+    ---@param cargoTypes string[]|nil
     ---@param entryPath cpos[]
     ---@param exitPath? cpos[]
-    ---@param actionFunc? function
-    ---@param exitFunc? function
+    ---@param actionFunc? fun(transport: actor, cargo: actor[])
+    ---@param exitFunc? fun(transport: actor)
     ---@param dropRange? integer
-    ---@return table
+    ---@return { [1]: actor, [2]: actor[] }
     ReinforceWithTransport = function(owner, actorType, cargoTypes, entryPath, exitPath, actionFunc, exitFunc, dropRange) end;
 }
 
@@ -651,7 +729,7 @@ Trigger = {
 
     --- Call a function after a specified delay. The callback function will be called as func().
     ---@param delay integer
-    ---@param func function
+    ---@param func fun()
     AfterDelay = function(delay, func) end;
 
     --- Removes the specified trigger from this actor. Note that the removal will only take effect at the end of a tick, so you must not add new triggers at the same time that you are calling this function.
@@ -663,153 +741,153 @@ Trigger = {
     ---@param actor actor
     ClearAll = function(actor) end;
 
-    --- Call a function when this actor is added to the world. The callback function will be called as func(Actor self).
+    --- Call a function when this actor is added to the world. The callback function will be called as func(self: actor).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(self: actor)
     OnAddedToWorld = function(actor, func) end;
 
     --- Call a function when all of the actors in a group are killed. The callback function will be called as func().
     ---@param actors actor[]
-    ---@param func function
+    ---@param func fun()
     OnAllKilled = function(actors, func) end;
 
     --- Call a function when all of the actors in a group have been killed or captured. The callback function will be called as func().
     ---@param actors actor[]
-    ---@param func function
+    ---@param func fun()
     OnAllKilledOrCaptured = function(actors, func) end;
 
     --- 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().
     ---@param actors actor[]
-    ---@param func function
+    ---@param func fun()
     OnAllRemovedFromWorld = function(actors, func) end;
 
-    --- Call a function when one of the actors in a group is killed. The callback function will be called as func(Actorkilled).
+    --- Call a function when one of the actors in a group is killed. The callback function will be called as func(killed: actor).
     ---@param actors actor[]
-    ---@param func function
+    ---@param func fun(killed: actor)
     OnAnyKilled = function(actors, func) end;
 
-    --- Call a function when any actor produces another actor. The callback function will be called as func(Actor producer, Actor produced, string productionType).
-    ---@param func function
+    --- Call a function when any actor produces another actor. The callback function will be called as func(producer: actor, produced: actor, productionType: string).
+    ---@param func fun(producer: actor, produced: actor, productionType: string)
     OnAnyProduction = function(func) end;
 
-    --- Call a function when this actor is captured. The callback function will be called as func(Actor self, Actor captor, Player oldOwner, Player newOwner).
+    --- Call a function when this actor is captured. The callback function will be called as func(self: actor, captor: actor, oldOwner: player, newOwner: player).
     ---@param actors actor
-    ---@param func function
+    ---@param func fun(self: actor, captor: actor, oldOwner: player, newOwner: player)
     OnCapture = function(actors, func) end;
 
-    --- Call a function when the actor is damaged. The callback function will be called as func(Actor self, Actor attacker, int damage).
+    --- Call a function when the actor is damaged. The callback function will be called as func(self: actor, attacker: actor, damage: integer).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(self: actor, attacker: actor, damage: integer)
     OnDamaged = function(actor, func) end;
 
-    --- 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 player actor needs the 'EnemyWatcher' trait. The actors to discover need the 'AnnounceOnSeen' trait.
+    --- 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(discovered: actor, discoverer: player). The player actor needs the 'EnemyWatcher' trait. The actors to discover need the 'AnnounceOnSeen' trait.
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(discovered: actor, discoverer: player)
     OnDiscovered = function(actor, func) end;
 
-    --- 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).
+    --- Call a function when a ground-based actor enters this cell footprint. Returns the trigger ID for later removal using RemoveFootprintTrigger(id: integer). The callback function will be called as func(a: actor, id: integer).
     ---@param cells cpos[]
-    ---@param func function
+    ---@param func fun(a: actor, id: integer)
     ---@return integer
     OnEnteredFootprint = function(cells, func) end;
 
-    --- 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).
+    --- Call a function when an actor enters this range. Returns the trigger ID for later removal using RemoveProximityTrigger(id: integer). The callback function will be called as func(a: actor, id: integer).
     ---@param pos wpos
     ---@param range wdist
-    ---@param func function
+    ---@param func fun(a: actor, id: integer)
     ---@return integer
     OnEnteredProximityTrigger = function(pos, range, func) end;
 
-    --- 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).
+    --- Call a function when a ground-based actor leaves this cell footprint. Returns the trigger ID for later removal using RemoveFootprintTrigger(id: integer). The callback function will be called as func(a: actor, id: integer).
     ---@param cells cpos[]
-    ---@param func function
+    ---@param func fun(a: actor, id: integer)
     ---@return integer
     OnExitedFootprint = function(cells, func) end;
 
-    --- 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).
+    --- Call a function when an actor leaves this range. Returns the trigger ID for later removal using RemoveProximityTrigger(id: integer). The callback function will be called as func(a: actor, id: integer).
     ---@param pos wpos
     ---@param range wdist
-    ---@param func function
+    ---@param func fun(a: actor, id: integer)
     ---@return integer
     OnExitedProximityTrigger = function(pos, range, func) end;
 
-    --- Call a function each tick that the actor is idle. The callback function will be called as func(Actor self).
+    --- Call a function each tick that the actor is idle. The callback function will be called as func(self: actor).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(self: actor)
     OnIdle = function(actor, func) end;
 
-    --- Call a function when this actor is infiltrated. The callback function will be called as func(Actor self, Actor infiltrator).
+    --- Call a function when this actor is infiltrated. The callback function will be called as func(self: actor, infiltrator: actor).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(self: actor, infiltrator: actor)
     OnInfiltrated = function(actor, func) end;
 
-    --- Call a function when the actor is killed. The callback function will be called as func(Actor self, Actor killer).
+    --- Call a function when the actor is killed. The callback function will be called as func(self: actor, killer: actor).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(self: actor, killer: actor)
     OnKilled = function(actor, func) end;
 
     --- Call a function when this actor is killed or captured. The callback function will be called as func().
     ---@param actor actor
-    ---@param func function
+    ---@param func fun()
     OnKilledOrCaptured = function(actor, func) end;
 
-    --- Call a function when this player is assigned a new objective. The callback function will be called as func(Player player, int objectiveID).
+    --- Call a function when this player is assigned a new objective. The callback function will be called as func(p: player, objectiveId: integer).
     ---@param player player
-    ---@param func function
+    ---@param func fun(p: player, objectiveId: integer)
     OnObjectiveAdded = function(player, func) end;
 
-    --- Call a function when this player completes an objective. The callback function will be called as func(Player player, int objectiveID).
+    --- Call a function when this player completes an objective. The callback function will be called as func(p: player, objectiveId: integer).
     ---@param player player
-    ---@param func function
+    ---@param func fun(p: player, objectiveId: integer)
     OnObjectiveCompleted = function(player, func) end;
 
-    --- Call a function when this player fails an objective. The callback function will be called as func(Player player, int objectiveID).
+    --- Call a function when this player fails an objective. The callback function will be called as func(p: player, objectiveId: integer).
     ---@param player player
-    ---@param func function
+    ---@param func fun(p: player, objectiveId: integer)
     OnObjectiveFailed = function(player, func) end;
 
-    --- Call a function for each passenger when it enters a transport. The callback function will be called as func(Actor transport, Actor passenger).
+    --- Call a function for each passenger when it enters a transport. The callback function will be called as func(transport: actor, passenger: actor).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(transport: actor, passenger: actor)
     OnPassengerEntered = function(actor, func) end;
 
-    --- Call a function for each passenger when it exits a transport. The callback function will be called as func(Actor transport, Actor passenger).
+    --- Call a function for each passenger when it exits a transport. The callback function will be called as func(transport: actor, passenger: actor).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(transport: actor, passenger: actor)
     OnPassengerExited = function(actor, func) end;
 
-    --- 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 player actor needs the 'EnemyWatcher' trait. The actors to discover need the 'AnnounceOnSeen' trait.
+    --- Call a function when this player is discovered by an enemy or neutral player. 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.
     ---@param discovered player
-    ---@param func function
+    ---@param func fun(discovered: player, discoverer: player, discoveredActor: actor)
     OnPlayerDiscovered = function(discovered, func) end;
 
-    --- Call a function when this player fails any primary objective. The callback function will be called as func(Player player).
+    --- Call a function when this player fails any primary objective. The callback function will be called as func(p: player).
     ---@param player player
-    ---@param func function
+    ---@param func fun(p: player)
     OnPlayerLost = function(player, func) end;
 
-    --- Call a function when this player completes all primary objectives. The callback function will be called as func(Player player).
+    --- Call a function when this player completes all primary objectives. The callback function will be called as func(p: player).
     ---@param player player
-    ---@param func function
+    ---@param func fun(p: player)
     OnPlayerWon = function(player, func) end;
 
-    --- Call a function when this actor produces another actor. The callback function will be called as func(Actor producer, Actor produced).
+    --- Call a function when this actor produces another actor. The callback function will be called as func(producer: actor, produced: actor).
     ---@param actors actor
-    ---@param func function
+    ---@param func fun(producer: actor, produced: actor)
     OnProduction = function(actors, func) end;
 
-    --- Call a function when this actor is removed from the world. The callback function will be called as func(Actor self).
+    --- Call a function when this actor is removed from the world. The callback function will be called as func(self: actor).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(self: actor)
     OnRemovedFromWorld = function(actor, func) end;
 
-    --- Call a function when this actor is sold. The callback function will be called as func(Actor self).
+    --- Call a function when this actor is sold. The callback function will be called as func(self: actor).
     ---@param actor actor
-    ---@param func function
+    ---@param func fun(self: actor)
     OnSold = function(actor, func) end;
 
     --- Call a function when the game timer expires. The callback function will be called as func().
-    ---@param func function
+    ---@param func fun()
     OnTimerExpired = function(func) end;
 
     --- Removes a previously created footprint trigger.
@@ -829,36 +907,41 @@ UserInterface = {
     ---@param color? color?
     SetMissionText = function(text, color) end;
 
-    ---@param text string
-    ---@param table? table
+    --- Translates text into the users language. The translation key must be added to the language files (*.ftl). Args can be passed to be substituted into the resulting message.
+    ---@param translationKey string
+    ---@param args? { string: any }
     ---@return string
-    Translate = function(text, table) end;
+    Translate = function(translationKey, args) end;
 }
 
 ---Global variable provided by the game scripting engine.
 Utils = {
 
     --- Returns true if func returns true for all elements in a collection.
-    ---@param collection table
-    ---@param func function
+    ---@generic T
+    ---@param collection T[]
+    ---@param func fun(item: T):boolean?
     ---@return boolean
     All = function(collection, func) end;
 
     --- Returns true if func returns true for any element in a collection.
-    ---@param collection table
-    ---@param func function
+    ---@generic T
+    ---@param collection T[]
+    ---@param func fun(item: T):boolean?
     ---@return boolean
     Any = function(collection, func) end;
 
     --- Concatenates two Lua tables into a single table.
-    ---@param firstCollection table
-    ---@param secondCollection table
-    ---@return table
+    ---@generic T
+    ---@param firstCollection T[]
+    ---@param secondCollection T[]
+    ---@return T[]
     Concat = function(firstCollection, secondCollection) end;
 
     --- Calls a function on every element in a collection.
-    ---@param collection table
-    ---@param func function
+    ---@generic T
+    ---@param collection T[]
+    ---@param func fun(item: T)
     Do = function(collection, func) end;
 
     --- Expands the given footprint one step along the coordinate axes, and (if requested) diagonals.
@@ -874,8 +957,9 @@ Utils = {
     FormatTime = function(ticks, leadingMinuteZero) end;
 
     --- Returns a random value from a collection.
-    ---@param collection table
-    ---@return any
+    ---@generic T
+    ---@param collection T[]
+    ---@return T
     Random = function(collection) end;
 
     --- Returns a random integer x in the range low <= x < high.
@@ -885,26 +969,30 @@ Utils = {
     RandomInteger = function(low, high) end;
 
     --- Returns the collection in a random order.
-    ---@param collection table
-    ---@return table
+    ---@generic T
+    ---@param collection T[]
+    ---@return T[]
     Shuffle = function(collection) end;
 
     --- Skips over the first numElements members of a table and return the rest.
-    ---@param table table
+    ---@generic T
+    ---@param table T[]
     ---@param numElements integer
-    ---@return table
+    ---@return T[]
     Skip = function(table, numElements) end;
 
     --- Returns the first n values from a collection.
+    ---@generic T
     ---@param n integer
-    ---@param source table
-    ---@return table
+    ---@param source T[]
+    ---@return T[]
     Take = function(n, source) end;
 
     --- Returns the original collection filtered with the func.
-    ---@param collection table
-    ---@param func function
-    ---@return table
+    ---@generic T
+    ---@param collection T[]
+    ---@param func fun(item: T):boolean?
+    ---@return T[]
     Where = function(collection, func) end;
 }
 
@@ -1068,11 +1156,11 @@ local __actor = {
     ---@param wait? integer
     Patrol = function(waypoints, loop, wait) end;
 
-    --- Patrol along a set of given waypoints until a condition becomes true. The actor will wait for `wait` ticks at each waypoint.
+    --- Patrol along a set of given waypoints until a condition becomes true. The actor will wait for `wait` ticks at each waypoint. The callback function will be called as func(self: actor):boolean.
     --- *Queued Activity*
     --- **Requires Traits:** [AttackBase](https://docs.openra.net/en/release/traits/#attackbase), [IMove](https://docs.openra.net/en/release/traits/#imove)
     ---@param waypoints cpos[]
-    ---@param func function
+    ---@param func fun(self: actor):boolean
     ---@param wait? integer
     PatrolUntil = function(waypoints, func, wait) end;
 
@@ -1194,7 +1282,7 @@ local __actor = {
 
     --- Run an arbitrary Lua function.
     --- *Queued Activity*
-    ---@param func function
+    ---@param func fun()
     CallFunc = function(func) end;
 
     --- Wait for a specified number of game ticks (25 ticks = 1 second).
@@ -1257,7 +1345,7 @@ local __actor = {
 
     --- Kill the actor. damageTypes may be omitted, specified as a string, or as table of strings.
     --- **Requires Trait:** [IHealth](https://docs.openra.net/en/release/traits/#ihealth)
-    ---@param damageTypes? any
+    ---@param damageTypes? string|{ [unknown]: string }
     Kill = function(damageTypes) end;
 
     --- Maximum health of the actor.
@@ -1340,10 +1428,10 @@ local __actor = {
     ---@param productionType? string
     Produce = function(actorType, factionVariant, productionType) end;
 
-    --- Build the specified set of actors using a TD-style (per building) production queue. The function will return true if production could be started, false otherwise. If an actionFunc is given, it will be called as actionFunc(Actor[] actors) once production of all actors has been completed.  The actors array is guaranteed to only contain alive actors.
+    --- Build the specified set of actors using a TD-style (per building) production queue. The function will return true if production could be started, false otherwise. If an actionFunc is given, it will be called as actionFunc(actors: actor[]) once production of all actors has been completed.  The actors array is guaranteed to only contain alive actors.
     --- **Requires Traits:** [ProductionQueue](https://docs.openra.net/en/release/traits/#productionqueue), [ScriptTriggers](https://docs.openra.net/en/release/traits/#scripttriggers)
     ---@param actorTypes string[]
-    ---@param actionFunc? function
+    ---@param actionFunc? fun(actors: actor[])
     ---@return boolean
     Build = function(actorTypes, actionFunc) end;
 
@@ -1412,7 +1500,7 @@ local __actor = {
 
     --- Chronoshift a group of actors. A duration of 0 will teleport the actors permanently.
     --- **Requires Trait:** [ChronoshiftPower](https://docs.openra.net/en/release/traits/#chronoshiftpower)
-    ---@param unitLocationPairs table
+    ---@param unitLocationPairs { [actor]: cpos }
     ---@param duration? integer
     ---@param killCargo? boolean
     Chronoshift = function(unitLocationPairs, duration, killCargo) end;
@@ -1440,6 +1528,7 @@ local __actor = {
 }
 
 ---@class player
+--- Get or set the current experience.
 --- **Requires Trait:** [PlayerExperience](https://docs.openra.net/en/release/traits/#playerexperience)
 ---@field Experience integer
 --- Whether the player should receive a notification when low on power.
@@ -1661,10 +1750,10 @@ local __player = {
     ---@type string
     PowerState = nil;
 
-    --- Build the specified set of actors using classic (RA-style) production queues. The function will return true if production could be started, false otherwise. If an actionFunc is given, it will be called as actionFunc(Actor[] actors)once production of all actors has been completed. The actors array is guaranteed to only contain alive actors. Note: This function will fail to work when called during the first tick.
+    --- Build the specified set of actors using classic (RA-style) production queues. The function will return true if production could be started, false otherwise. If an actionFunc is given, it will be called as actionFunc(actors: actor[]) once production of all actors has been completed. The actors array is guaranteed to only contain alive actors. Note: This function will fail to work when called during the first tick.
     --- **Requires Traits:** [ClassicProductionQueue](https://docs.openra.net/en/release/traits/#classicproductionqueue), [ScriptTriggers](https://docs.openra.net/en/release/traits/#scripttriggers)
     ---@param actorTypes string[]
-    ---@param actionFunc? function
+    ---@param actionFunc? fun(actors: actor[])
     ---@return boolean
     Build = function(actorTypes, actionFunc) end;

@JovialFeline
Copy link
Contributor

That certainly beats the doc update I was scrounging together. 👌

@RoosterDragon
Copy link
Member Author

That looks complementary - improved descriptions is always going to be a helpful addition.

Copy link
Member

@PunkPun PunkPun left a comment

Choose a reason for hiding this comment

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

this PR begs for unit testing. I'm not sure, should we manually try running all missions to test?

@RoosterDragon
Copy link
Member Author

The type definitions don't have any effect on the missions at all. They're purely a developer aid. So no need to test every mission manually, their behaviour will be the same as before anyway.

@RoosterDragon RoosterDragon force-pushed the emmy branch 2 times, most recently from 3ac3f39 to c2af5f8 Compare August 1, 2024 20:16
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.
Copy link
Member

@PunkPun PunkPun left a comment

Choose a reason for hiding this comment

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

how do I get the showcased errors to show up? I've built this branch, refreshed VSCode, have the lua extension installed, and I'm not getting any of the new errors?

@RoosterDragon
Copy link
Member Author

My approach was to open any lua file and hit go to definition on something from the engine, e.g. Utils.Do. This takes you to the extension file at, e.g. C:\Users\<user>\.vscode\extensions\openra.vscode-openra-lua-2.0.3\api\dev\library\OpenRA.lua

You can replace the contents of this file with the output from the utility command ra --emmy-lua-api to test the new definitions.

@penev92 is there a better way to test out new definitions? I am wondering if my approch above is needless faff.

@penev92
Copy link
Member

penev92 commented Aug 3, 2024

That's how I test(ed) things too, so you're doing fine. 👍

Copy link
Member

@PunkPun PunkPun left a comment

Choose a reason for hiding this comment

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

OK

this also seems to have caught an error in allies03b.lua that wasn't adressed

@PunkPun PunkPun merged commit ab28e6a into OpenRA:bleed Aug 3, 2024
@PunkPun
Copy link
Member

PunkPun commented Aug 3, 2024

changelog

@RoosterDragon RoosterDragon deleted the emmy branch August 3, 2024 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments