From ab28e6a75a898665d65f672ba821987e33a79485 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Sat, 27 Jul 2024 13:06:22 +0100 Subject: [PATCH] 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. --- OpenRA.Game/CPos.cs | 14 +- OpenRA.Game/CVec.cs | 42 ++++- OpenRA.Game/WAngle.cs | 19 ++- OpenRA.Game/WDist.cs | 43 ++++- OpenRA.Game/WPos.cs | 14 +- OpenRA.Game/WVec.cs | 41 ++++- .../Properties/ChronosphereProperties.cs | 5 +- OpenRA.Mods.Common/ActorInitializer.cs | 2 +- .../Scripting/Global/ActorGlobal.cs | 3 +- .../Scripting/Global/AngleGlobal.cs | 11 +- .../Scripting/Global/ColorGlobal.cs | 36 ++++ .../Scripting/Global/CoordinateGlobals.cs | 30 +++- .../Scripting/Global/DateTimeGlobal.cs | 6 + .../Scripting/Global/LightingGlobal.cs | 4 + .../Scripting/Global/MapGlobal.cs | 14 +- .../Scripting/Global/MediaGlobal.cs | 6 +- .../Scripting/Global/PlayerGlobal.cs | 2 +- .../Scripting/Global/ReinforcementsGlobal.cs | 16 +- .../Scripting/Global/TriggerGlobal.cs | 117 ++++++------- .../Scripting/Global/UserInterfaceGlobal.cs | 12 +- .../Scripting/Global/UtilsGlobal.cs | 38 ++++- .../Scripting/Properties/CombatProperties.cs | 5 +- .../Scripting/Properties/GeneralProperties.cs | 2 +- .../Scripting/Properties/HealthProperties.cs | 2 +- .../Properties/PlayerExperienceProperties.cs | 1 + .../Properties/ProductionProperties.cs | 8 +- .../ScriptEmmyTypeOverrideAttribute.cs | 32 ++++ .../UtilityCommands/ExtractEmmyLuaAPI.cs | 155 ++++++++++++------ mods/d2k/maps/atreides-01a/atreides01a.lua | 2 - mods/d2k/maps/atreides-01b/atreides01b.lua | 2 - mods/d2k/maps/atreides-03a/atreides03a.lua | 2 - mods/d2k/maps/atreides-03b/atreides03b.lua | 2 - mods/d2k/maps/harkonnen-01a/harkonnen01a.lua | 2 - mods/d2k/maps/harkonnen-01b/harkonnen01b.lua | 2 - mods/d2k/maps/ordos-01a/ordos01a.lua | 2 - mods/d2k/maps/ordos-01b/ordos01b.lua | 2 - 36 files changed, 499 insertions(+), 197 deletions(-) create mode 100644 OpenRA.Mods.Common/Scripting/ScriptEmmyTypeOverrideAttribute.cs diff --git a/OpenRA.Game/CPos.cs b/OpenRA.Game/CPos.cs index 3a2be3e763..6715026e30 100644 --- a/OpenRA.Game/CPos.cs +++ b/OpenRA.Game/CPos.cs @@ -16,7 +16,8 @@ using OpenRA.Scripting; namespace OpenRA { - public readonly struct CPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable + public readonly struct CPos : IEquatable, IScriptBindable, + ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, ILuaToStringBinding { // Coordinates are packed in a 32 bit signed int // X and Y are 12 bits (signed): -2048...2047 @@ -96,7 +97,8 @@ namespace OpenRA public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out CPos a) || !right.TryGetClrValue(out CVec b)) - throw new LuaException($"Attempted to call CPos.Add(CPos, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + throw new LuaException("Attempted to call CPos.Add(CPos, CVec) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); return new LuaCustomClrObject(a + b); } @@ -105,7 +107,8 @@ namespace OpenRA { var rightType = right.WrappedClrType(); if (!left.TryGetClrValue(out CPos a)) - throw new LuaException($"Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})"); + throw new LuaException("Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments " + + $"({left.WrappedClrType().Name}, {rightType.Name})"); if (rightType == typeof(CPos)) { @@ -118,7 +121,8 @@ namespace OpenRA return new LuaCustomClrObject(a - b); } - throw new LuaException($"Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})"); + throw new LuaException("Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments " + + $"({left.WrappedClrType().Name}, {rightType.Name})"); } public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) @@ -145,6 +149,8 @@ namespace OpenRA set => throw new LuaException("CPos is read-only. Use CPos.New to create a new value"); } + public LuaValue ToString(LuaRuntime runtime) => ToString(); + #endregion } } diff --git a/OpenRA.Game/CVec.cs b/OpenRA.Game/CVec.cs index c1676fa413..c4706ce394 100644 --- a/OpenRA.Game/CVec.cs +++ b/OpenRA.Game/CVec.cs @@ -17,8 +17,9 @@ using OpenRA.Scripting; namespace OpenRA { - public readonly struct CVec : IScriptBindable, - ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable + public readonly struct CVec : IEquatable, IScriptBindable, + ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaUnaryMinusBinding, + ILuaMultiplicationBinding, ILuaDivisionBinding, ILuaTableBinding, ILuaToStringBinding { public readonly int X, Y; @@ -77,7 +78,8 @@ namespace OpenRA public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b)) - throw new LuaException($"Attempted to call CVec.Add(CVec, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + throw new LuaException("Attempted to call CVec.Add(CVec, CVec) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); return new LuaCustomClrObject(a + b); } @@ -85,16 +87,12 @@ namespace OpenRA public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b)) - throw new LuaException($"Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + throw new LuaException("Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); return new LuaCustomClrObject(a - b); } - public LuaValue Minus(LuaRuntime runtime) - { - return new LuaCustomClrObject(-this); - } - public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b)) @@ -103,6 +101,29 @@ namespace OpenRA return a == b; } + public LuaValue Minus(LuaRuntime runtime) + { + return new LuaCustomClrObject(-this); + } + + public LuaValue Multiply(LuaRuntime runtime, LuaValue left, LuaValue right) + { + if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out int b)) + throw new LuaException("Attempted to call CVec.Multiply(CVec, integer) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + + return new LuaCustomClrObject(a * b); + } + + public LuaValue Divide(LuaRuntime runtime, LuaValue left, LuaValue right) + { + if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out int b)) + throw new LuaException("Attempted to call CVec.Multiply(CVec, integer) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + + return new LuaCustomClrObject(a / b); + } + public LuaValue this[LuaRuntime runtime, LuaValue key] { get @@ -111,6 +132,7 @@ namespace OpenRA { case "X": return X; case "Y": return Y; + case "Length": return Length; default: throw new LuaException($"CVec does not define a member '{key}'"); } } @@ -118,6 +140,8 @@ namespace OpenRA set => throw new LuaException("CVec is read-only. Use CVec.New to create a new value"); } + public LuaValue ToString(LuaRuntime runtime) => ToString(); + #endregion } } diff --git a/OpenRA.Game/WAngle.cs b/OpenRA.Game/WAngle.cs index 7b97e21018..d258081b09 100644 --- a/OpenRA.Game/WAngle.cs +++ b/OpenRA.Game/WAngle.cs @@ -19,7 +19,8 @@ namespace OpenRA /// /// 1D angle - 1024 units = 360 degrees. /// - public readonly struct WAngle : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, IEquatable + public readonly struct WAngle : IEquatable, IScriptBindable, + ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, ILuaToStringBinding { public readonly int Angle; public int AngleSquared => Angle * Angle; @@ -257,6 +258,22 @@ namespace OpenRA return a == b; } + public LuaValue this[LuaRuntime runtime, LuaValue key] + { + get + { + switch (key.ToString()) + { + case "Angle": return Angle; + default: throw new LuaException($"WAngle does not define a member '{key}'"); + } + } + + set => throw new LuaException("WAngle is read-only. Use Angle.New to create a new value"); + } + + public LuaValue ToString(LuaRuntime runtime) => ToString(); + #endregion } } diff --git a/OpenRA.Game/WDist.cs b/OpenRA.Game/WDist.cs index 66180ca8ce..9e53577628 100644 --- a/OpenRA.Game/WDist.cs +++ b/OpenRA.Game/WDist.cs @@ -21,8 +21,10 @@ namespace OpenRA /// /// 1d world distance - 1024 units = 1 cell. /// - public readonly struct WDist : IComparable, IComparable, IEquatable, - IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding + public readonly struct WDist : IComparable, IComparable, IEquatable, IScriptBindable, + ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaUnaryMinusBinding, + ILuaMultiplicationBinding, ILuaDivisionBinding, ILuaLessThanBinding, ILuaLessThanOrEqualToBinding, + ILuaTableBinding, ILuaToStringBinding { public readonly int Length; public long LengthSquared => (long)Length * Length; @@ -137,6 +139,40 @@ namespace OpenRA return a == b; } + public LuaValue Minus(LuaRuntime runtime) => new LuaCustomClrObject(-this); + + public LuaValue Multiply(LuaRuntime runtime, LuaValue left, LuaValue right) + { + if (!left.TryGetClrValue(out WDist a) || !right.TryGetClrValue(out int b)) + throw new LuaException("Attempted to call WDist.Multiply(WDist, integer) with invalid arguments."); + + return new LuaCustomClrObject(a * b); + } + + public LuaValue Divide(LuaRuntime runtime, LuaValue left, LuaValue right) + { + if (!left.TryGetClrValue(out WDist a) || !right.TryGetClrValue(out int b)) + throw new LuaException("Attempted to call WDist.Divide(WDist, integer) with invalid arguments."); + + return new LuaCustomClrObject(a / b); + } + + public LuaValue LessThan(LuaRuntime runtime, LuaValue left, LuaValue right) + { + if (!left.TryGetClrValue(out WDist a) || !right.TryGetClrValue(out WDist b)) + throw new LuaException("Attempted to call WDist.LessThan(WDist, WDist) with invalid arguments."); + + return a < b; + } + + public LuaValue LessThanOrEqualTo(LuaRuntime runtime, LuaValue left, LuaValue right) + { + if (!left.TryGetClrValue(out WDist a) || !right.TryGetClrValue(out WDist b)) + throw new LuaException("Attempted to call WDist.LessThanOrEqualTo(WDist, WDist) with invalid arguments."); + + return a <= b; + } + public LuaValue this[LuaRuntime runtime, LuaValue key] { get @@ -150,6 +186,9 @@ namespace OpenRA set => throw new LuaException("WDist is read-only. Use WDist.New to create a new value"); } + + public LuaValue ToString(LuaRuntime runtime) => ToString(); + #endregion } } diff --git a/OpenRA.Game/WPos.cs b/OpenRA.Game/WPos.cs index 18de50de8a..ee593ff2bd 100644 --- a/OpenRA.Game/WPos.cs +++ b/OpenRA.Game/WPos.cs @@ -17,7 +17,8 @@ using OpenRA.Scripting; namespace OpenRA { - public readonly struct WPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable + public readonly struct WPos : IEquatable, IScriptBindable, + ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, ILuaToStringBinding { public readonly int X, Y, Z; @@ -82,7 +83,8 @@ namespace OpenRA public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out WPos a) || !right.TryGetClrValue(out WVec b)) - throw new LuaException($"Attempted to call WPos.Add(WPos, WVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + throw new LuaException("Attempted to call WPos.Add(WPos, WVec) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); return new LuaCustomClrObject(a + b); } @@ -91,7 +93,8 @@ namespace OpenRA { var rightType = right.WrappedClrType(); if (!left.TryGetClrValue(out WPos a)) - throw new LuaException($"Attempted to call WPos.Subtract(WPos, (WPos|WVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})"); + throw new LuaException("Attempted to call WPos.Subtract(WPos, (WPos|WVec)) with invalid arguments " + + $"({left.WrappedClrType().Name}, {rightType.Name})"); if (rightType == typeof(WPos)) { @@ -104,7 +107,8 @@ namespace OpenRA return new LuaCustomClrObject(a - b); } - throw new LuaException($"Attempted to call WPos.Subtract(WPos, (WPos|WVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})"); + throw new LuaException("Attempted to call WPos.Subtract(WPos, (WPos|WVec)) with invalid arguments " + + $"({left.WrappedClrType().Name}, {rightType.Name})"); } public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) @@ -131,6 +135,8 @@ namespace OpenRA set => throw new LuaException("WPos is read-only. Use WPos.New to create a new value"); } + public LuaValue ToString(LuaRuntime runtime) => ToString(); + #endregion } diff --git a/OpenRA.Game/WVec.cs b/OpenRA.Game/WVec.cs index b403d9d5f7..fb073fa0f8 100644 --- a/OpenRA.Game/WVec.cs +++ b/OpenRA.Game/WVec.cs @@ -17,8 +17,9 @@ using OpenRA.Support; namespace OpenRA { - public readonly struct WVec : IScriptBindable, - ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable + public readonly struct WVec : IEquatable, IScriptBindable, + ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaUnaryMinusBinding, + ILuaMultiplicationBinding, ILuaDivisionBinding, ILuaTableBinding, ILuaToStringBinding { public readonly int X, Y, Z; @@ -112,7 +113,8 @@ namespace OpenRA public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out WVec a) || !right.TryGetClrValue(out WVec b)) - throw new LuaException($"Attempted to call WVec.Add(WVec, WVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + throw new LuaException("Attempted to call WVec.Add(WVec, WVec) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); return new LuaCustomClrObject(a + b); } @@ -120,16 +122,12 @@ namespace OpenRA public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out WVec a) || !right.TryGetClrValue(out WVec b)) - throw new LuaException($"Attempted to call WVec.Subtract(WVec, WVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + throw new LuaException("Attempted to call WVec.Subtract(WVec, WVec) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); return new LuaCustomClrObject(a - b); } - public LuaValue Minus(LuaRuntime runtime) - { - return new LuaCustomClrObject(-this); - } - public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out WVec a) || !right.TryGetClrValue(out WVec b)) @@ -138,6 +136,29 @@ namespace OpenRA return a == b; } + public LuaValue Minus(LuaRuntime runtime) + { + return new LuaCustomClrObject(-this); + } + + public LuaValue Multiply(LuaRuntime runtime, LuaValue left, LuaValue right) + { + if (!left.TryGetClrValue(out WVec a) || !right.TryGetClrValue(out int b)) + throw new LuaException("Attempted to call WVec.Multiply(WVec, integer) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + + return new LuaCustomClrObject(a * b); + } + + public LuaValue Divide(LuaRuntime runtime, LuaValue left, LuaValue right) + { + if (!left.TryGetClrValue(out WVec a) || !right.TryGetClrValue(out int b)) + throw new LuaException("Attempted to call WVec.Divide(WVec, integer) with invalid arguments " + + $"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})"); + + return new LuaCustomClrObject(a / b); + } + public LuaValue this[LuaRuntime runtime, LuaValue key] { get @@ -155,6 +176,8 @@ namespace OpenRA set => throw new LuaException("WVec is read-only. Use WVec.New to create a new value"); } + public LuaValue ToString(LuaRuntime runtime) => ToString(); + #endregion } } diff --git a/OpenRA.Mods.Cnc/Scripting/Properties/ChronosphereProperties.cs b/OpenRA.Mods.Cnc/Scripting/Properties/ChronosphereProperties.cs index fc67d8c6c0..4c02aaa91d 100644 --- a/OpenRA.Mods.Cnc/Scripting/Properties/ChronosphereProperties.cs +++ b/OpenRA.Mods.Cnc/Scripting/Properties/ChronosphereProperties.cs @@ -11,6 +11,7 @@ using Eluant; using OpenRA.Mods.Cnc.Traits; +using OpenRA.Mods.Common.Scripting; using OpenRA.Scripting; using OpenRA.Traits; @@ -23,7 +24,7 @@ namespace OpenRA.Mods.Cnc.Scripting : base(context, self) { } [Desc("Chronoshift a group of actors. A duration of 0 will teleport the actors permanently.")] - public void Chronoshift(LuaTable unitLocationPairs, int duration = 0, bool killCargo = false) + public void Chronoshift([ScriptEmmyTypeOverride("{ [actor]: cpos }")] LuaTable unitLocationPairs, int duration = 0, bool killCargo = false) { foreach (var kv in unitLocationPairs) { @@ -33,7 +34,7 @@ namespace OpenRA.Mods.Cnc.Scripting using (kv.Value) { if (!kv.Key.TryGetClrValue(out actor) || !kv.Value.TryGetClrValue(out cell)) - throw new LuaException($"Chronoshift requires a table of Actor,CPos pairs. Received {kv.Key.WrappedClrType().Name},{kv.Value.WrappedClrType().Name}"); + throw new LuaException($"Chronoshift requires a table of actor,cpos pairs. Received {kv.Key.WrappedClrType().Name},{kv.Value.WrappedClrType().Name}"); } var cs = actor.TraitsImplementing() diff --git a/OpenRA.Mods.Common/ActorInitializer.cs b/OpenRA.Mods.Common/ActorInitializer.cs index b8ba2a5c67..e528321faa 100644 --- a/OpenRA.Mods.Common/ActorInitializer.cs +++ b/OpenRA.Mods.Common/ActorInitializer.cs @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common : base(value) { } } - public class TerrainOrientationInit : ValueActorInit, ISingleInstanceInit + public class TerrainOrientationInit : ValueActorInit, ISingleInstanceInit, ISuppressInitExport { public TerrainOrientationInit(WRot value) : base(value) { } diff --git a/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs index 9dc5d0c130..3b2abccc2f 100644 --- a/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs @@ -94,7 +94,7 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Create a new actor. initTable specifies a list of key-value pairs that defines the initial parameters for the actor's traits.")] - public Actor Create(string type, bool addToWorld, LuaTable initTable) + public Actor Create(string type, bool addToWorld, [ScriptEmmyTypeOverride("initTable")] LuaTable initTable) { var initDict = new TypeDictionary(); @@ -176,6 +176,7 @@ namespace OpenRA.Mods.Common.Scripting return pi != null ? pi.GetCruiseAltitude().Length : 0; } + [Desc("Returns the cost of the requested unit given by the Valued trait.")] public int Cost(string type) { if (!Context.World.Map.Rules.Actors.TryGetValue(type, out var ai)) diff --git a/OpenRA.Mods.Common/Scripting/Global/AngleGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/AngleGlobal.cs index 8b50493849..5ef71f6f19 100644 --- a/OpenRA.Mods.Common/Scripting/Global/AngleGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/AngleGlobal.cs @@ -19,16 +19,25 @@ namespace OpenRA.Mods.Common.Scripting.Global public AngleGlobal(ScriptContext context) : base(context) { } + [Desc("0/1024 units = 0/360 degrees")] public WAngle North => WAngle.Zero; + [Desc("128 units = 315 degrees")] public WAngle NorthWest => new(128); + [Desc("256 units = 270 degrees")] public WAngle West => new(256); + [Desc("384 units = 225 degrees")] public WAngle SouthWest => new(384); + [Desc("512 units = 180 degrees")] public WAngle South => new(512); + [Desc("640 units = 135 degrees")] public WAngle SouthEast => new(640); + [Desc("768 units = 90 degrees")] public WAngle East => new(768); + [Desc("896 units = 45 degrees")] public WAngle NorthEast => new(896); - [Desc("Create an arbitrary angle.")] + [Desc("Create an arbitrary angle. 1024 units = 360 degrees. North is 0. " + + "Units increase *counter* clockwise. Comparison given to degrees increasing clockwise.")] public WAngle New(int a) { return new WAngle(a); } } } diff --git a/OpenRA.Mods.Common/Scripting/Global/ColorGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/ColorGlobal.cs index c6b09000d1..56fc46b282 100644 --- a/OpenRA.Mods.Common/Scripting/Global/ColorGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/ColorGlobal.cs @@ -51,41 +51,77 @@ namespace OpenRA.Mods.Common.Scripting.Global throw new LuaException("Invalid rrggbb[aa] hex string."); } + [Desc("FromHex(\"00FFFF\")")] public Color Aqua => Color.Aqua; + [Desc("FromHex(\"000000\")")] public Color Black => Color.Black; + [Desc("FromHex(\"0000FF\")")] public Color Blue => Color.Blue; + [Desc("FromHex(\"A52A2A\")")] public Color Brown => Color.Brown; + [Desc("FromHex(\"00FFFF\")")] public Color Cyan => Color.Cyan; + [Desc("FromHex(\"00008B\")")] public Color DarkBlue => Color.DarkBlue; + [Desc("FromHex(\"008B8B\")")] public Color DarkCyan => Color.DarkCyan; + [Desc("FromHex(\"A9A9A9\")")] public Color DarkGray => Color.DarkGray; + [Desc("FromHex(\"006400\")")] public Color DarkGreen => Color.DarkGreen; + [Desc("FromHex(\"FF8C00\")")] public Color DarkOrange => Color.DarkOrange; + [Desc("FromHex(\"8B0000\")")] public Color DarkRed => Color.DarkRed; + [Desc("FromHex(\"FF00FF\")")] public Color Fuchsia => Color.Fuchsia; + [Desc("FromHex(\"FFD700\")")] public Color Gold => Color.Gold; + [Desc("FromHex(\"808080\")")] public Color Gray => Color.Gray; + [Desc("FromHex(\"008000\")")] public Color Green => Color.Green; + [Desc("FromHex(\"7CFC00\")")] public Color LawnGreen => Color.LawnGreen; + [Desc("FromHex(\"ADD8E6\")")] public Color LightBlue => Color.LightBlue; + [Desc("FromHex(\"E0FFFF\")")] public Color LightCyan => Color.LightCyan; + [Desc("FromHex(\"D3D3D3\")")] public Color LightGray => Color.LightGray; + [Desc("FromHex(\"90EE90\")")] public Color LightGreen => Color.LightGreen; + [Desc("FromHex(\"FFFFE0\")")] public Color LightYellow => Color.LightYellow; + [Desc("FromHex(\"00FF00\")")] public Color Lime => Color.Lime; + [Desc("FromHex(\"32CD32\")")] public Color LimeGreen => Color.LimeGreen; + [Desc("FromHex(\"FF00FF\")")] public Color Magenta => Color.Magenta; + [Desc("FromHex(\"800000\")")] public Color Maroon => Color.Maroon; + [Desc("FromHex(\"000080\")")] public Color Navy => Color.Navy; + [Desc("FromHex(\"808000\")")] public Color Olive => Color.Olive; + [Desc("FromHex(\"FFA500\")")] public Color Orange => Color.Orange; + [Desc("FromHex(\"FF4500\")")] public Color OrangeRed => Color.OrangeRed; + [Desc("FromHex(\"800080\")")] public Color Purple => Color.Purple; + [Desc("FromHex(\"FF0000\")")] public Color Red => Color.Red; + [Desc("FromHex(\"FA8072\")")] public Color Salmon => Color.Salmon; + [Desc("FromHex(\"87CEEB\")")] public Color SkyBlue => Color.SkyBlue; + [Desc("FromHex(\"008080\")")] public Color Teal => Color.Teal; + [Desc("FromHex(\"FFFF00\")")] public Color Yellow => Color.Yellow; + [Desc("FromHex(\"FFFFFF\")")] public Color White => Color.White; } } diff --git a/OpenRA.Mods.Common/Scripting/Global/CoordinateGlobals.cs b/OpenRA.Mods.Common/Scripting/Global/CoordinateGlobals.cs index cd50c65423..008849296e 100644 --- a/OpenRA.Mods.Common/Scripting/Global/CoordinateGlobals.cs +++ b/OpenRA.Mods.Common/Scripting/Global/CoordinateGlobals.cs @@ -8,6 +8,9 @@ * information, see COPYING. */ #endregion +using System.Linq; +using Eluant; +using OpenRA.Mods.Common.Traits; using OpenRA.Scripting; namespace OpenRA.Mods.Common.Scripting @@ -18,9 +21,34 @@ namespace OpenRA.Mods.Common.Scripting public CPosGlobal(ScriptContext context) : base(context) { } - [Desc("Create a new CPos with the specified coordinates.")] + [Desc("Create a new CPos with the specified coordinates on the ground (layer = 0).")] public CPos New(int x, int y) { return new CPos(x, y); } + [Desc("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.")] + public CPos NewWithLayer(int x, int y, byte layer) + { + if (layer != 0) + { + var worldCmls = Context.World.GetCustomMovementLayers(); + if (layer >= worldCmls.Length || worldCmls[layer] == null) + { + var layerNames = typeof(CustomMovementLayerType) + .GetFields() + .Select(f => (Index: (byte)f.GetRawConstantValue(), f.Name)) + .ToArray(); + var validLayers = new[] { (Index: (byte)0, Name: "Ground") } + .Concat(worldCmls + .Where(cml => cml != null) + .Select(cml => layerNames.Single(ln => ln.Index == cml.Index))); + throw new LuaException($"Layer {layer} does not exist on this map. " + + $"Valid layers on this map are: {string.Join(", ", validLayers.Select(x => $"{x.Index} ({x.Name})"))}"); + } + } + + return new CPos(x, y, layer); + } + [Desc("The cell coordinate origin.")] public CPos Zero => CPos.Zero; } diff --git a/OpenRA.Mods.Common/Scripting/Global/DateTimeGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/DateTimeGlobal.cs index 234fda4fc7..00a2fc506c 100644 --- a/OpenRA.Mods.Common/Scripting/Global/DateTimeGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/DateTimeGlobal.cs @@ -44,11 +44,17 @@ namespace OpenRA.Mods.Common.Scripting return seconds * ticksPerSecond; } + [Desc("Get the current year (1-9999).")] public int CurrentYear => DateTime.Now.Year; + [Desc("Get the current month (1-12).")] public int CurrentMonth => DateTime.Now.Month; + [Desc("Get the current day (1-31).")] public int CurrentDay => DateTime.Now.Day; + [Desc("Get the current hour (0-23).")] public int CurrentHour => DateTime.Now.Hour; + [Desc("Get the current minute (0-59).")] public int CurrentMinute => DateTime.Now.Minute; + [Desc("Get the current second (0-59).")] public int CurrentSecond => DateTime.Now.Second; [Desc("Converts the number of minutes into game time (ticks).")] diff --git a/OpenRA.Mods.Common/Scripting/Global/LightingGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/LightingGlobal.cs index d9bc2da36f..1a6c5b3dbc 100644 --- a/OpenRA.Mods.Common/Scripting/Global/LightingGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/LightingGlobal.cs @@ -36,24 +36,28 @@ namespace OpenRA.Mods.Common.Scripting effect.Enable(ticks); } + [Desc("Red component (0-1).")] public double Red { get => tintEffect?.Red ?? 1; set { if (tintEffect != null) tintEffect.Red = (float)value; } } + [Desc("Green component (0-1).")] public double Green { get => tintEffect?.Green ?? 1; set { if (tintEffect != null) tintEffect.Green = (float)value; } } + [Desc("Blue component (0-1).")] public double Blue { get => tintEffect?.Blue ?? 1; set { if (tintEffect != null) tintEffect.Blue = (float)value; } } + [Desc("Strength of the lighting (0-1).")] public double Ambient { get => tintEffect?.Ambient ?? 1; diff --git a/OpenRA.Mods.Common/Scripting/Global/MapGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/MapGlobal.cs index 0973759a7b..1a26ad364f 100644 --- a/OpenRA.Mods.Common/Scripting/Global/MapGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/MapGlobal.cs @@ -37,14 +37,14 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Returns a table of all actors within the requested region, filtered using the specified function.")] - public Actor[] ActorsInCircle(WPos location, WDist radius, LuaFunction filter = null) + public Actor[] ActorsInCircle(WPos location, WDist radius, [ScriptEmmyTypeOverride("fun(a: actor):boolean")] LuaFunction filter = null) { var actors = Context.World.FindActorsInCircle(location, radius); return FilteredObjects(actors, filter).ToArray(); } [Desc("Returns a table of all actors within the requested rectangle, filtered using the specified function.")] - public Actor[] ActorsInBox(WPos topLeft, WPos bottomRight, LuaFunction filter = null) + public Actor[] ActorsInBox(WPos topLeft, WPos bottomRight, [ScriptEmmyTypeOverride("fun(a: actor):boolean")] LuaFunction filter = null) { var actors = Context.World.ActorMap.ActorsInBox(topLeft, bottomRight); return FilteredObjects(actors, filter).ToArray(); @@ -81,8 +81,8 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Returns the first cell on the visible border of the map from the given cell,", - "matching the filter function called as function(CPos cell).")] - public CPos ClosestMatchingEdgeCell(CPos givenCell, LuaFunction filter) + "matching the filter function called as function(cell: cpos):boolean.")] + public CPos ClosestMatchingEdgeCell(CPos givenCell, [ScriptEmmyTypeOverride("fun(cell: cpos):boolean")] LuaFunction filter) { return FilteredObjects(Context.World.Map.AllEdgeCells.OrderBy(c => (givenCell - c).Length), filter).FirstOrDefault(); } @@ -106,7 +106,7 @@ namespace OpenRA.Mods.Common.Scripting public bool IsPausedShellmap => Context.World.Type == WorldType.Shellmap && gameSettings.PauseShellmap; [Desc("Returns the value of a `" + nameof(ScriptLobbyDropdown) + "` selected in the game lobby.")] - public LuaValue LobbyOption(string id) + public string LobbyOption(string id) { var option = Context.World.WorldActor.TraitsImplementing() .FirstOrDefault(sld => sld.Info.ID == id); @@ -120,8 +120,8 @@ namespace OpenRA.Mods.Common.Scripting return option.Value; } - [Desc("Returns the value of a `ScriptLobbyDropdown` selected in the game lobby or fallback to a default value.")] - public LuaValue LobbyOptionOrDefault(string id, string fallback) + [Desc("Returns the value of a `" + nameof(ScriptLobbyDropdown) + "` selected in the game lobby or fallback to a default value.")] + public string LobbyOptionOrDefault(string id, string fallback) { var option = Context.World.WorldActor.TraitsImplementing() .FirstOrDefault(sld => sld.Info.ID == id); diff --git a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs index e4a1a95499..28743ad94a 100644 --- a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs @@ -52,7 +52,7 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Play track defined in music.yaml or map.yaml, or keep track empty for playing a random song.")] - public void PlayMusic(string track = null, LuaFunction onPlayComplete = null) + public void PlayMusic(string track = null, [ScriptEmmyTypeOverride("fun()")] LuaFunction onPlayComplete = null) { if (!playlist.IsMusicAvailable) return; @@ -94,14 +94,14 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Play a video fullscreen. File name has to include the file extension.")] - public void PlayMovieFullscreen(string videoFileName, LuaFunction onPlayComplete = null) + public void PlayMovieFullscreen(string videoFileName, [ScriptEmmyTypeOverride("fun()")] LuaFunction onPlayComplete = null) { var onComplete = WrapOnPlayComplete(onPlayComplete); Media.PlayFMVFullscreen(world, videoFileName, onComplete); } [Desc("Play a video in the radar window. File name has to include the file extension.")] - public void PlayMovieInRadar(string videoFileName, LuaFunction onPlayComplete = null) + public void PlayMovieInRadar(string videoFileName, [ScriptEmmyTypeOverride("fun()")] LuaFunction onPlayComplete = null) { var onComplete = WrapOnPlayComplete(onPlayComplete); Media.PlayFMVInRadar(videoFileName, onComplete); diff --git a/OpenRA.Mods.Common/Scripting/Global/PlayerGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/PlayerGlobal.cs index cd41a413a7..9cc88f5455 100644 --- a/OpenRA.Mods.Common/Scripting/Global/PlayerGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/PlayerGlobal.cs @@ -28,7 +28,7 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Returns a table of players filtered by the specified function.")] - public Player[] GetPlayers(LuaFunction filter) + public Player[] GetPlayers([ScriptEmmyTypeOverride("fun(p: player):boolean")] LuaFunction filter) { return FilteredObjects(Context.World.Players, filter).ToArray(); } diff --git a/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs index eaa6e9ac51..b05f9dcf3d 100644 --- a/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs @@ -76,9 +76,10 @@ namespace OpenRA.Mods.Common.Scripting "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). " + + "will be called as actionFunc(a: actor). " + "Returns a table containing the deployed units.")] - public Actor[] Reinforce(Player owner, string[] actorTypes, CPos[] entryPath, int interval = 25, LuaFunction actionFunc = null) + public Actor[] Reinforce(Player owner, string[] actorTypes, CPos[] entryPath, int interval = 25, + [ScriptEmmyTypeOverride("fun(a: actor)")] LuaFunction actionFunc = null) { var actors = new List(); for (var i = 0; i < actorTypes.Length; i++) @@ -115,13 +116,18 @@ namespace OpenRA.Mods.Common.Scripting "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). " + + "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.")] - public LuaTable ReinforceWithTransport(Player owner, string actorType, string[] cargoTypes, CPos[] entryPath, CPos[] exitPath = null, - LuaFunction actionFunc = null, LuaFunction exitFunc = null, int dropRange = 3) + [return: ScriptEmmyTypeOverride("{ [1]: actor, [2]: actor[] }")] + public LuaTable ReinforceWithTransport(Player owner, string actorType, + [ScriptEmmyTypeOverride("string[]|nil")] string[] cargoTypes, + CPos[] entryPath, CPos[] exitPath = null, + [ScriptEmmyTypeOverride("fun(transport: actor, cargo: actor[])")] LuaFunction actionFunc = null, + [ScriptEmmyTypeOverride("fun(transport: actor)")] LuaFunction exitFunc = null, + int dropRange = 3) { var transport = CreateActor(owner, actorType, true, entryPath[0], entryPath.Length > 1 ? entryPath[1] : null); var cargo = transport.TraitOrDefault(); diff --git a/OpenRA.Mods.Common/Scripting/Global/TriggerGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/TriggerGlobal.cs index 7fd63addcb..c370bebf31 100644 --- a/OpenRA.Mods.Common/Scripting/Global/TriggerGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/TriggerGlobal.cs @@ -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); } diff --git a/OpenRA.Mods.Common/Scripting/Global/UserInterfaceGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/UserInterfaceGlobal.cs index e5bff665a0..2bf4010713 100644 --- a/OpenRA.Mods.Common/Scripting/Global/UserInterfaceGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/UserInterfaceGlobal.cs @@ -34,12 +34,14 @@ namespace OpenRA.Mods.Common.Scripting.Global luaLabel.GetColor = () => c; } - public string Translate(string text, LuaTable table = null) + [Desc("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.")] + public string Translate(string translationKey, [ScriptEmmyTypeOverride("{ string: any }")] LuaTable args = null) { - if (table != null) + if (args != null) { var argumentDictionary = new Dictionary(); - foreach (var kv in table) + foreach (var kv in args) { using (kv.Key) using (kv.Value) @@ -53,10 +55,10 @@ namespace OpenRA.Mods.Common.Scripting.Global } } - return TranslationProvider.GetString(text, argumentDictionary); + return TranslationProvider.GetString(translationKey, argumentDictionary); } - return TranslationProvider.GetString(text); + return TranslationProvider.GetString(translationKey); } } } diff --git a/OpenRA.Mods.Common/Scripting/Global/UtilsGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/UtilsGlobal.cs index e8535e4172..6d7bf08c02 100644 --- a/OpenRA.Mods.Common/Scripting/Global/UtilsGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/UtilsGlobal.cs @@ -23,14 +23,18 @@ namespace OpenRA.Mods.Common.Scripting : base(context) { } [Desc("Calls a function on every element in a collection.")] - public void Do(LuaValue[] collection, LuaFunction func) + public void Do( + [ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] collection, + [ScriptEmmyTypeOverride("fun(item: T)", "T")] LuaFunction func) { foreach (var c in collection) func.Call(c).Dispose(); } [Desc("Returns true if func returns true for any element in a collection.")] - public bool Any(LuaValue[] collection, LuaFunction func) + public bool Any( + [ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] collection, + [ScriptEmmyTypeOverride("fun(item: T):boolean?", "T")] LuaFunction func) { foreach (var c in collection) using (var ret = func.Call(c)) @@ -42,7 +46,9 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Returns true if func returns true for all elements in a collection.")] - public bool All(LuaValue[] collection, LuaFunction func) + public bool All( + [ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] collection, + [ScriptEmmyTypeOverride("fun(item: T):boolean?", "T")] LuaFunction func) { foreach (var c in collection) using (var ret = func.Call(c)) @@ -54,7 +60,10 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Returns the original collection filtered with the func.")] - public LuaTable Where(LuaValue[] collection, LuaFunction func) + [return: ScriptEmmyTypeOverride("T[]", "T")] + public LuaTable Where( + [ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] collection, + [ScriptEmmyTypeOverride("fun(item: T):boolean?", "T")] LuaFunction func) { var t = Context.CreateTable(); @@ -68,13 +77,19 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Returns the first n values from a collection.")] - public LuaValue[] Take(int n, LuaValue[] source) + [return: ScriptEmmyTypeOverride("T[]", "T")] + public LuaValue[] Take( + int n, + [ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] source) { return source.Take(n).Select(v => v.CopyReference()).ToArray(); } [Desc("Skips over the first numElements members of a table and return the rest.")] - public LuaTable Skip(LuaTable table, int numElements) + [return: ScriptEmmyTypeOverride("T[]", "T")] + public LuaTable Skip( + [ScriptEmmyTypeOverride("T[]", "T")] LuaTable table, + int numElements) { var t = Context.CreateTable(); @@ -86,7 +101,10 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Concatenates two Lua tables into a single table.")] - public LuaTable Concat(LuaValue[] firstCollection, LuaValue[] secondCollection) + [return: ScriptEmmyTypeOverride("T[]", "T")] + public LuaTable Concat( + [ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] firstCollection, + [ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] secondCollection) { var t = Context.CreateTable(); @@ -100,13 +118,15 @@ namespace OpenRA.Mods.Common.Scripting } [Desc("Returns a random value from a collection.")] - public LuaValue Random(LuaValue[] collection) + [return: ScriptEmmyTypeOverride("T", "T")] + public LuaValue Random([ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] collection) { return collection.Random(Context.World.SharedRandom).CopyReference(); } [Desc("Returns the collection in a random order.")] - public LuaValue[] Shuffle(LuaValue[] collection) + [return: ScriptEmmyTypeOverride("T[]", "T")] + public LuaValue[] Shuffle([ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] collection) { return collection.Shuffle(Context.World.SharedRandom).ToArray(); } diff --git a/OpenRA.Mods.Common/Scripting/Properties/CombatProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/CombatProperties.cs index bee9368ed4..bf3c352ca0 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/CombatProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/CombatProperties.cs @@ -63,8 +63,9 @@ namespace OpenRA.Mods.Common.Scripting [ScriptActorPropertyActivity] [Desc("Patrol along a set of given waypoints until a condition becomes true. " + - "The actor will wait for `wait` ticks at each waypoint.")] - public void PatrolUntil(CPos[] waypoints, LuaFunction func, int wait = 0) + "The actor will wait for `wait` ticks at each waypoint. " + + "The callback function will be called as func(self: actor):boolean.")] + public void PatrolUntil(CPos[] waypoints, [ScriptEmmyTypeOverride("fun(self: actor):boolean")] LuaFunction func, int wait = 0) { Patrol(waypoints, false, wait); diff --git a/OpenRA.Mods.Common/Scripting/Properties/GeneralProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/GeneralProperties.cs index 5922fcdc4b..1b8eadf278 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/GeneralProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/GeneralProperties.cs @@ -137,7 +137,7 @@ namespace OpenRA.Mods.Common.Scripting [ScriptActorPropertyActivity] [Desc("Run an arbitrary Lua function.")] - public void CallFunc(LuaFunction func) + public void CallFunc([ScriptEmmyTypeOverride("fun()")] LuaFunction func) { Self.QueueActivity(new CallLuaFunc(func, Context)); } diff --git a/OpenRA.Mods.Common/Scripting/Properties/HealthProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/HealthProperties.cs index 881c3c7f89..e7dc423ee8 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/HealthProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/HealthProperties.cs @@ -37,7 +37,7 @@ namespace OpenRA.Mods.Common.Scripting public int MaxHealth => health.MaxHP; [Desc("Kill the actor. damageTypes may be omitted, specified as a string, or as table of strings.")] - public void Kill(object damageTypes = null) + public void Kill([ScriptEmmyTypeOverride("string|{ [unknown]: string }")] object damageTypes = null) { Damage damage; if (damageTypes is string d) diff --git a/OpenRA.Mods.Common/Scripting/Properties/PlayerExperienceProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/PlayerExperienceProperties.cs index a6ba975f76..152808e157 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/PlayerExperienceProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/PlayerExperienceProperties.cs @@ -26,6 +26,7 @@ namespace OpenRA.Mods.Common.Scripting exp = player.PlayerActor.Trait(); } + [Desc("Get or set the current experience.")] public int Experience { get => exp.Experience; diff --git a/OpenRA.Mods.Common/Scripting/Properties/ProductionProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/ProductionProperties.cs index 42c0777550..4ef6437f8f 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/ProductionProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/ProductionProperties.cs @@ -130,10 +130,10 @@ namespace OpenRA.Mods.Common.Scripting [Desc("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 " + + "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.")] - public bool Build(string[] actorTypes, LuaFunction actionFunc = null) + public bool Build(string[] actorTypes, [ScriptEmmyTypeOverride("fun(actors: actor[])")] LuaFunction actionFunc = null) { if (triggers.HasAnyCallbacksFor(Trigger.OnProduction)) return false; @@ -234,11 +234,11 @@ namespace OpenRA.Mods.Common.Scripting [Desc("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 " + + "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.")] - public bool Build(string[] actorTypes, LuaFunction actionFunc = null) + public bool Build(string[] actorTypes, [ScriptEmmyTypeOverride("fun(actors: actor[])")] LuaFunction actionFunc = null) { var typeToQueueMap = new Dictionary(); foreach (var actorType in actorTypes.Distinct()) diff --git a/OpenRA.Mods.Common/Scripting/ScriptEmmyTypeOverrideAttribute.cs b/OpenRA.Mods.Common/Scripting/ScriptEmmyTypeOverrideAttribute.cs new file mode 100644 index 0000000000..bfd7e9553a --- /dev/null +++ b/OpenRA.Mods.Common/Scripting/ScriptEmmyTypeOverrideAttribute.cs @@ -0,0 +1,32 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Mods.Common.UtilityCommands; + +namespace OpenRA.Mods.Common.Scripting +{ + /// + /// Used to override the Emmy Lua type generated by the utility command. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)] + public sealed class ScriptEmmyTypeOverrideAttribute : Attribute + { + public readonly string TypeDeclaration; + public readonly string GenericTypeDeclaration; + + public ScriptEmmyTypeOverrideAttribute(string typeDeclaration, string genericTypeDeclaration = null) + { + TypeDeclaration = typeDeclaration; + GenericTypeDeclaration = genericTypeDeclaration; + } + } +} diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs index 7a3e0b41c8..962cefd501 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; +using OpenRA.Mods.Common.Scripting; using OpenRA.Scripting; using OpenRA.Traits; @@ -35,7 +36,7 @@ namespace OpenRA.Mods.Common.UtilityCommands var version = utility.ModData.Manifest.Metadata.Version; Console.WriteLine($"-- This is an automatically generated Lua API definition generated for {version} of OpenRA."); Console.WriteLine("-- https://wiki.openra.net/Utility was used with the --emmy-lua-api parameter."); - Console.WriteLine("-- See https://docs.openra.net/en/latest/release/lua/ for human readable documentation."); + Console.WriteLine("-- See https://docs.openra.net/en/release/lua/ for human readable documentation."); Console.WriteLine(); WriteDiagnosticsDisabling(); @@ -87,11 +88,14 @@ namespace OpenRA.Mods.Common.UtilityCommands Console.WriteLine(); Console.WriteLine(); Console.WriteLine("--- Base engine types."); + Console.WriteLine(); Console.WriteLine("---@class cpos"); Console.WriteLine("---@field X integer"); Console.WriteLine("---@field Y integer"); + Console.WriteLine("---@field Layer integer"); Console.WriteLine("---@operator add(cvec): cpos"); Console.WriteLine("---@operator sub(cvec): cpos"); + Console.WriteLine("---@operator sub(cpos): cvec"); Console.WriteLine(); Console.WriteLine("---@class wpos"); Console.WriteLine("---@field X integer"); @@ -99,6 +103,7 @@ namespace OpenRA.Mods.Common.UtilityCommands Console.WriteLine("---@field Z integer"); Console.WriteLine("---@operator add(wvec): wpos"); Console.WriteLine("---@operator sub(wvec): wpos"); + Console.WriteLine("---@operator sub(wpos): wvec"); Console.WriteLine(); Console.WriteLine("---@class wangle"); Console.WriteLine("---@field Angle integer"); @@ -107,19 +112,32 @@ namespace OpenRA.Mods.Common.UtilityCommands Console.WriteLine(); Console.WriteLine("---@class wdist"); Console.WriteLine("---@field Length integer"); + Console.WriteLine("---@operator add(wdist): wdist"); + Console.WriteLine("---@operator sub(wdist): wdist"); + Console.WriteLine("---@operator unm(wdist): wdist"); + Console.WriteLine("---@operator mul(integer): wdist"); + Console.WriteLine("---@operator div(integer): wdist"); Console.WriteLine(); Console.WriteLine("---@class wvec"); Console.WriteLine("---@field X integer"); Console.WriteLine("---@field Y integer"); Console.WriteLine("---@field Z integer"); + Console.WriteLine("---@field Facing wangle"); Console.WriteLine("---@operator add(wvec): wvec"); Console.WriteLine("---@operator sub(wvec): wvec"); + Console.WriteLine("---@operator unm(wvec): wvec"); + Console.WriteLine("---@operator mul(integer): wvec"); + Console.WriteLine("---@operator div(integer): wvec"); Console.WriteLine(); Console.WriteLine("---@class cvec"); Console.WriteLine("---@field X integer"); Console.WriteLine("---@field Y integer"); + Console.WriteLine("---@field Length integer"); Console.WriteLine("---@operator add(cvec): cvec"); Console.WriteLine("---@operator sub(cvec): cvec"); + Console.WriteLine("---@operator unm(cvec): cvec"); + Console.WriteLine("---@operator mul(integer): cvec"); + Console.WriteLine("---@operator div(integer): cvec"); Console.WriteLine(); Console.WriteLine("---@class color"); Console.WriteLine("local color = { };"); @@ -146,7 +164,7 @@ namespace OpenRA.Mods.Common.UtilityCommands if (p.ParameterType.IsEnum) localEnums.Add(p.ParameterType); - return p.ParameterType.EmmyLuaString(); + return p.EmmyLuaString($"{init.Name}").TypeDeclaration; }))) .Where(s => !s.Contains(", ")) .Distinct()); @@ -207,6 +225,8 @@ namespace OpenRA.Mods.Common.UtilityCommands foreach (var line in lines) Console.WriteLine($" --- {line}"); } + else + throw new NotSupportedException($"Missing {nameof(DescAttribute)} on {t.Name} {member.Name}"); if (member is PropertyInfo propertyInfo) { @@ -214,15 +234,20 @@ namespace OpenRA.Mods.Common.UtilityCommands foreach (var obsolete in attributes.OfType()) Console.WriteLine($" ---@deprecated {obsolete.Message}"); - Console.WriteLine($" ---@type {propertyInfo.PropertyType.EmmyLuaString()}"); + Console.WriteLine($" ---@type {propertyInfo.PropertyType.EmmyLuaString($"{t.Name} {member.Name}")}"); body = propertyInfo.Name + " = nil;"; } if (member is MethodInfo methodInfo) { var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - Console.WriteLine($" ---@param {parameter.EmmyLuaString()}"); + var luaParameters = parameters + .Select(parameter => parameter.NameAndEmmyLuaString($"{t.Name} {member.Name}")) + .ToArray(); + foreach (var generic in luaParameters.Select(p => p.Generic).Where(g => !string.IsNullOrEmpty(g)).Distinct()) + Console.WriteLine($" ---@generic {generic}"); + foreach (var nameAndType in luaParameters.Select(p => p.NameAndType)) + Console.WriteLine($" ---@param {nameAndType}"); var parameterString = parameters.Select(p => p.Name).JoinWith(", "); @@ -230,9 +255,8 @@ namespace OpenRA.Mods.Common.UtilityCommands foreach (var obsolete in attributes.OfType()) Console.WriteLine($" ---@deprecated {obsolete.Message}"); - var returnType = methodInfo.ReturnType.EmmyLuaString(); - if (returnType != "Void") - Console.WriteLine($" ---@return {returnType}"); + if (methodInfo.ReturnType != typeof(void)) + Console.WriteLine($" ---@return {methodInfo.ReturnTypeEmmyLuaString($"{t.Name} {member.Name}")}"); body = member.Name + $" = function({parameterString}) end;"; } @@ -274,7 +298,7 @@ namespace OpenRA.Mods.Common.UtilityCommands if (duplicateMembers.Contains(memberInfo.Name)) Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index"); - Console.WriteLine($"---@field {propertyInfo.Name} {propertyInfo.PropertyType.EmmyLuaString()}"); + Console.WriteLine($"---@field {propertyInfo.Name} {propertyInfo.PropertyType.EmmyLuaString($"{memberInfo.DeclaringType.Name} {memberInfo.Name}")}"); } } @@ -292,7 +316,7 @@ namespace OpenRA.Mods.Common.UtilityCommands if (duplicateMembers.Contains(memberInfo.Name)) Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index"); - Console.WriteLine($" ---@type {propertyInfo.PropertyType.EmmyLuaString()}"); + Console.WriteLine($" ---@type {propertyInfo.PropertyType.EmmyLuaString($"{memberInfo.DeclaringType.Name} {memberInfo.Name}")}"); Console.WriteLine($" {propertyInfo.Name} = nil;"); } @@ -307,14 +331,18 @@ namespace OpenRA.Mods.Common.UtilityCommands Console.WriteLine($" ---@deprecated {obsolete.Message}"); var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - Console.WriteLine($" ---@param {parameter.EmmyLuaString()}"); + var luaParameters = parameters + .Select(parameter => parameter.NameAndEmmyLuaString($"{memberInfo.DeclaringType.Name} {memberInfo.Name}")) + .ToArray(); + foreach (var generic in luaParameters.Select(p => p.Generic).Where(g => !string.IsNullOrEmpty(g)).Distinct()) + Console.WriteLine($" ---@generic {generic}"); + foreach (var nameAndType in luaParameters.Select(p => p.NameAndType)) + Console.WriteLine($" ---@param {nameAndType}"); var parameterString = parameters.Select(p => p.Name).JoinWith(", "); - var returnType = methodInfo.ReturnType.EmmyLuaString(); - if (returnType != "Void") - Console.WriteLine($" ---@return {returnType}"); + if (methodInfo.ReturnType != typeof(void)) + Console.WriteLine($" ---@return {methodInfo.ReturnTypeEmmyLuaString($"{memberInfo.DeclaringType.Name} {memberInfo.Name}")}"); if (duplicateMembers.Contains(methodInfo.Name)) Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index"); @@ -336,6 +364,8 @@ namespace OpenRA.Mods.Common.UtilityCommands foreach (var line in lines) Console.WriteLine($"{new string(' ', indentation * 4)}--- {line}"); } + else + throw new NotSupportedException($"Missing {nameof(DescAttribute)} on {memberInfo.DeclaringType.Name} {memberInfo.Name}"); if (isActivity) Console.WriteLine( @@ -358,65 +388,90 @@ namespace OpenRA.Mods.Common.UtilityCommands { static readonly Dictionary LuaTypeNameReplacements = new() { + // These are weak type mappings, don't add these. + // Instead, use ScriptEmmyTypeOverrideAttribute to provide a specific type. + ////{ "Object", "any" }, + ////{ "LuaValue", "any" }, + ////{ "LuaTable", "table" }, + ////{ "LuaFunction", "function" }, + { "Byte", "integer" }, { "UInt32", "integer" }, { "Int32", "integer" }, { "String", "string" }, - { "String[]", "string[]" }, { "Boolean", "boolean" }, { "Double", "number" }, - { "Object", "any" }, - { "LuaTable", "table" }, - { "LuaValue", "any" }, - { "LuaValue[]", "table" }, - { "LuaFunction", "function" }, { "WVec", "wvec" }, { "CVec", "cvec" }, { "CPos", "cpos" }, - { "CPos[]", "cpos[]" }, { "WPos", "wpos" }, { "WAngle", "wangle" }, - { "WAngle[]", "wangle[]" }, { "WDist", "wdist" }, { "Color", "color" }, { "Actor", "actor" }, - { "Actor[]", "actor[]" }, { "Player", "player" }, - { "Player[]", "player[]" }, }; - public static string EmmyLuaString(this Type type) + public static string EmmyLuaString(this Type type, string notSupportedExceptionContext) { - if (!LuaTypeNameReplacements.TryGetValue(type.Name, out var replacement)) - replacement = type.Name; + if (type.IsArray) + return EmmaLuaStringInner(type.GetElementType(), notSupportedExceptionContext) + "[]"; - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + return EmmaLuaStringInner(type, notSupportedExceptionContext); + + static string EmmaLuaStringInner(Type type, string context) { - var argument = type.GetGenericArguments().Select(p => p.Name).First(); - if (LuaTypeNameReplacements.TryGetValue(argument, out var genericReplacement)) - replacement = $"{genericReplacement}?"; - else - replacement = $"{type.GetGenericArguments().Select(p => p.Name).First()}?"; - } + if (LuaTypeNameReplacements.TryGetValue(type.Name, out var replacement)) + return replacement; - return replacement; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var argument = type.GetGenericArguments()[0].Name; + if (LuaTypeNameReplacements.TryGetValue(argument, out var genericReplacement)) + return $"{genericReplacement}?"; + } + + if (type.IsEnum) + return type.Name; + + // This may indicate we are trying to export a type we have not added support for yet. + // Consider adding support for this type. + // This may mean updating WriteManual and adding IScriptBindable to the type. + // Or adding an entry to LuaTypeNameReplacements. + // Or use ScriptEmmyTypeOverride to provide a custom type for a parameter. + // Or consider using ISuppressInitExport if the parameter is coming from an init we don't want to expose to Lua. + // Or, it may need a different approach than the ones listed above. + throw new NotSupportedException( + $"Command lacks support for exposing type to Lua: `{type}` required by `{context}`. " + + $"Consider applying {nameof(ScriptEmmyTypeOverrideAttribute)} or {nameof(ISuppressInitExport)}"); + } } - public static string EmmyLuaString(this ParameterInfo parameterInfo) + public static string ReturnTypeEmmyLuaString(this MethodInfo methodInfo, string notSupportedExceptionContext) + { + var overrideAttr = methodInfo.ReturnTypeCustomAttributes + .GetCustomAttributes(typeof(ScriptEmmyTypeOverrideAttribute), false) + .Cast() + .SingleOrDefault(); + if (overrideAttr != null) + return overrideAttr.TypeDeclaration; + + return methodInfo.ReturnType.EmmyLuaString(notSupportedExceptionContext); + } + + public static (string TypeDeclaration, string GenericTypeDeclaration) EmmyLuaString(this ParameterInfo parameterInfo, string notSupportedExceptionContext) + { + var overrideAttr = parameterInfo.GetCustomAttribute(); + if (overrideAttr != null) + return (overrideAttr.TypeDeclaration, overrideAttr.GenericTypeDeclaration); + + return (parameterInfo.ParameterType.EmmyLuaString(notSupportedExceptionContext), null); + } + + public static (string NameAndType, string Generic) NameAndEmmyLuaString(this ParameterInfo parameterInfo, string notSupportedExceptionContext) { var optional = parameterInfo.IsOptional ? "?" : ""; - - var parameterType = parameterInfo.ParameterType.EmmyLuaString(); - - // A hack for ActorGlobal.Create(). - if (parameterInfo.Name == "initTable") - parameterType = "initTable"; - - return $"{parameterInfo.Name}{optional} {parameterType}"; - } - - public static string EmmyLuaString(this PropertyInfo propertyInfo) - { - return $"{propertyInfo.Name} {propertyInfo.PropertyType.EmmyLuaString()}"; + var (typeDeclaration, genericTypeDeclaration) = parameterInfo.EmmyLuaString(notSupportedExceptionContext); + return ($"{parameterInfo.Name}{optional} {typeDeclaration}", genericTypeDeclaration); } } } diff --git a/mods/d2k/maps/atreides-01a/atreides01a.lua b/mods/d2k/maps/atreides-01a/atreides01a.lua index f8ceae20e5..074ce3a16d 100644 --- a/mods/d2k/maps/atreides-01a/atreides01a.lua +++ b/mods/d2k/maps/atreides-01a/atreides01a.lua @@ -113,8 +113,6 @@ WorldLoaded = function() Trigger.AfterDelay(DateTime.Seconds(3), function() Harkonnen.MarkCompletedObjective(KillAtreides) end) - - return true end end) end diff --git a/mods/d2k/maps/atreides-01b/atreides01b.lua b/mods/d2k/maps/atreides-01b/atreides01b.lua index 7963857b19..18056e755b 100644 --- a/mods/d2k/maps/atreides-01b/atreides01b.lua +++ b/mods/d2k/maps/atreides-01b/atreides01b.lua @@ -113,8 +113,6 @@ WorldLoaded = function() Trigger.AfterDelay(DateTime.Seconds(3), function() Harkonnen.MarkCompletedObjective(KillAtreides) end) - - return true end end) end diff --git a/mods/d2k/maps/atreides-03a/atreides03a.lua b/mods/d2k/maps/atreides-03a/atreides03a.lua index e8c9463cde..b82d63f9dd 100644 --- a/mods/d2k/maps/atreides-03a/atreides03a.lua +++ b/mods/d2k/maps/atreides-03a/atreides03a.lua @@ -138,8 +138,6 @@ WorldLoaded = function() Trigger.AfterDelay(DateTime.Seconds(3), function() Ordos.MarkCompletedObjective(KillAtreides) end) - - return true end end) end diff --git a/mods/d2k/maps/atreides-03b/atreides03b.lua b/mods/d2k/maps/atreides-03b/atreides03b.lua index e14b62c770..085f00447d 100644 --- a/mods/d2k/maps/atreides-03b/atreides03b.lua +++ b/mods/d2k/maps/atreides-03b/atreides03b.lua @@ -138,8 +138,6 @@ WorldLoaded = function() Trigger.AfterDelay(DateTime.Seconds(3), function() Ordos.MarkCompletedObjective(KillAtreides) end) - - return true end end) end diff --git a/mods/d2k/maps/harkonnen-01a/harkonnen01a.lua b/mods/d2k/maps/harkonnen-01a/harkonnen01a.lua index 7b4e88852f..0d56172fd4 100644 --- a/mods/d2k/maps/harkonnen-01a/harkonnen01a.lua +++ b/mods/d2k/maps/harkonnen-01a/harkonnen01a.lua @@ -113,8 +113,6 @@ WorldLoaded = function() Trigger.AfterDelay(DateTime.Seconds(3), function() Harkonnen.MarkCompletedObjective(KillAtreides) end) - - return true end end) end diff --git a/mods/d2k/maps/harkonnen-01b/harkonnen01b.lua b/mods/d2k/maps/harkonnen-01b/harkonnen01b.lua index 7b4e88852f..0d56172fd4 100644 --- a/mods/d2k/maps/harkonnen-01b/harkonnen01b.lua +++ b/mods/d2k/maps/harkonnen-01b/harkonnen01b.lua @@ -113,8 +113,6 @@ WorldLoaded = function() Trigger.AfterDelay(DateTime.Seconds(3), function() Harkonnen.MarkCompletedObjective(KillAtreides) end) - - return true end end) end diff --git a/mods/d2k/maps/ordos-01a/ordos01a.lua b/mods/d2k/maps/ordos-01a/ordos01a.lua index b9c413b042..3f2396fcbd 100644 --- a/mods/d2k/maps/ordos-01a/ordos01a.lua +++ b/mods/d2k/maps/ordos-01a/ordos01a.lua @@ -113,8 +113,6 @@ WorldLoaded = function() Trigger.AfterDelay(DateTime.Seconds(3), function() Harkonnen.MarkCompletedObjective(KillAtreides) end) - - return true end end) end diff --git a/mods/d2k/maps/ordos-01b/ordos01b.lua b/mods/d2k/maps/ordos-01b/ordos01b.lua index 263228a22e..ca9711fc5e 100644 --- a/mods/d2k/maps/ordos-01b/ordos01b.lua +++ b/mods/d2k/maps/ordos-01b/ordos01b.lua @@ -113,8 +113,6 @@ WorldLoaded = function() Trigger.AfterDelay(DateTime.Seconds(3), function() Harkonnen.MarkCompletedObjective(KillOrdos) end) - - return true end end) end