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.
167 lines
6.0 KiB
C#
167 lines
6.0 KiB
C#
#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 System.Linq;
|
|
using Eluant;
|
|
using OpenRA.Mods.Common.Traits;
|
|
using OpenRA.Scripting;
|
|
|
|
namespace OpenRA.Mods.Common.Scripting
|
|
{
|
|
[ScriptGlobal("Map")]
|
|
public class MapGlobal : ScriptGlobal
|
|
{
|
|
readonly SpawnMapActors sma;
|
|
readonly World world;
|
|
readonly GameSettings gameSettings;
|
|
|
|
public MapGlobal(ScriptContext context)
|
|
: base(context)
|
|
{
|
|
sma = context.World.WorldActor.Trait<SpawnMapActors>();
|
|
world = context.World;
|
|
gameSettings = Game.Settings.Game;
|
|
|
|
// Register map actors as globals (yuck!)
|
|
foreach (var kv in sma.Actors)
|
|
context.RegisterMapActor(kv.Key, kv.Value);
|
|
}
|
|
|
|
[Desc("Returns a table of all actors within the requested region, filtered using the specified function.")]
|
|
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, [ScriptEmmyTypeOverride("fun(a: actor):boolean")] LuaFunction filter = null)
|
|
{
|
|
var actors = Context.World.ActorMap.ActorsInBox(topLeft, bottomRight);
|
|
return FilteredObjects(actors, filter).ToArray();
|
|
}
|
|
|
|
// HACK: This API method abuses the coordinate system, and should be removed
|
|
// in favour of proper actor queries. See #8549.
|
|
[Obsolete("This function will be removed in future versions. Use Map.ActorsInWorld instead.")]
|
|
[Desc("Returns the location of the top-left corner of the map (assuming zero terrain height).")]
|
|
public WPos TopLeft => Context.World.Map.ProjectedTopLeft;
|
|
|
|
// HACK: This API method abuses the coordinate system, and should be removed
|
|
// in favour of proper actor queries. See #8549.
|
|
[Obsolete("This function will be removed in future versions. Use Map.ActorsInWorld instead.")]
|
|
[Desc("Returns the location of the bottom-right corner of the map (assuming zero terrain height).")]
|
|
public WPos BottomRight => Context.World.Map.ProjectedBottomRight;
|
|
|
|
[Desc("Returns a random cell inside the visible region of the map.")]
|
|
public CPos RandomCell()
|
|
{
|
|
return Context.World.Map.ChooseRandomCell(Context.World.SharedRandom);
|
|
}
|
|
|
|
[Desc("Returns a random cell on the visible border of the map.")]
|
|
public CPos RandomEdgeCell()
|
|
{
|
|
return Context.World.Map.ChooseRandomEdgeCell(Context.World.SharedRandom);
|
|
}
|
|
|
|
[Desc("Returns the closest cell on the visible border of the map from the given cell.")]
|
|
public CPos ClosestEdgeCell(CPos givenCell)
|
|
{
|
|
return Context.World.Map.ChooseClosestEdgeCell(givenCell);
|
|
}
|
|
|
|
[Desc("Returns the first cell on the visible border of the map from the given cell,",
|
|
"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();
|
|
}
|
|
|
|
[Desc("Returns the center of a cell in world coordinates.")]
|
|
public WPos CenterOfCell(CPos cell)
|
|
{
|
|
return Context.World.Map.CenterOfCell(cell);
|
|
}
|
|
|
|
[Desc("Returns the type of the terrain at the target cell.")]
|
|
public string TerrainType(CPos cell)
|
|
{
|
|
return Context.World.Map.GetTerrainInfo(cell).Type;
|
|
}
|
|
|
|
[Desc("Returns true if there is only one human player.")]
|
|
public bool IsSinglePlayer => Context.World.LobbyInfo.NonBotPlayers.Count() == 1;
|
|
|
|
[Desc("Returns true if this is a shellmap and the player has paused animations.")]
|
|
public bool IsPausedShellmap => Context.World.Type == WorldType.Shellmap && gameSettings.PauseShellmap;
|
|
|
|
[Desc("Returns the value of a `" + nameof(ScriptLobbyDropdown) + "` selected in the game lobby.")]
|
|
public string LobbyOption(string id)
|
|
{
|
|
var option = Context.World.WorldActor.TraitsImplementing<ScriptLobbyDropdown>()
|
|
.FirstOrDefault(sld => sld.Info.ID == id);
|
|
|
|
if (option == null)
|
|
{
|
|
Log.Write("lua", $"A {nameof(ScriptLobbyDropdown)} with ID `{id}` was not found.");
|
|
return null;
|
|
}
|
|
|
|
return option.Value;
|
|
}
|
|
|
|
[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<ScriptLobbyDropdown>()
|
|
.FirstOrDefault(sld => sld.Info.ID == id);
|
|
|
|
if (option == null)
|
|
return fallback;
|
|
|
|
return option.Value;
|
|
}
|
|
|
|
[Desc("Returns a table of all the actors that were specified in the map file.")]
|
|
public Actor[] NamedActors => sma.Actors.Values.ToArray();
|
|
|
|
[Desc("Returns the actor that was specified with a given name in " +
|
|
"the map file (or nil, if the actor is dead or not found).")]
|
|
public Actor NamedActor(string actorName)
|
|
{
|
|
if (!sma.Actors.TryGetValue(actorName, out var ret))
|
|
return null;
|
|
|
|
if (ret.Disposed)
|
|
return null;
|
|
|
|
return ret;
|
|
}
|
|
|
|
[Desc("Returns true if actor was originally specified in the map file.")]
|
|
public bool IsNamedActor(Actor actor)
|
|
{
|
|
return actor.ActorID <= sma.LastMapActorID && actor.ActorID > sma.LastMapActorID - sma.Actors.Count;
|
|
}
|
|
|
|
[Desc("Returns a table of all actors tagged with the given string.")]
|
|
public Actor[] ActorsWithTag(string tag)
|
|
{
|
|
return Context.World.ActorsHavingTrait<ScriptTags>(t => t.HasTag(tag)).ToArray();
|
|
}
|
|
|
|
[Desc("Returns a table of all the actors that are currently on the map/in the world.")]
|
|
public Actor[] ActorsInWorld => world.Actors.ToArray();
|
|
}
|
|
}
|