Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
#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.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Eluant;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Actor")]
public class ActorGlobal : ScriptGlobal
{
public ActorGlobal(ScriptContext context)
: base(context) { }
static ActorInit CreateInit(string initName, LuaValue value)
{
// Find the requested type
var initInstance = initName.Split(ActorInfo.TraitInstanceSeparator);
var initType = Game.ModData.ObjectCreator.FindType(initInstance[0] + "Init");
if (initType == null)
throw new LuaException($"Unknown initializer type '{initInstance[0]}'");
// Construct the ActorInit.
var init = (ActorInit)RuntimeHelpers.GetUninitializedObject(initType);
if (initInstance.Length > 1)
initType.GetField(nameof(ActorInit.InstanceName)).SetValue(init, initInstance[1]);
if (value is LuaTable tableValue && init is CompositeActorInit compositeInit)
{
var args = compositeInit.InitializeArgs();
var initValues = new Dictionary<string, object>();
foreach (var kv in tableValue)
{
using (kv.Key)
using (kv.Value)
{
var key = kv.Key.ToString();
if (!args.TryGetValue(key, out var type))
throw new LuaException($"Unknown initializer type '{initInstance[0]}.{key}'");
var isActorReference = type == typeof(ActorInitActorReference);
if (isActorReference)
type = kv.Value is LuaString ? typeof(string) : typeof(Actor);
if (!kv.Value.TryGetClrValue(type, out var clrValue))
throw new LuaException($"Invalid data type for '{initInstance[0]}.{key}' (expected {type.Name}, got {kv.Value.WrappedClrType()})");
if (isActorReference)
clrValue = type == typeof(string) ? new ActorInitActorReference((string)clrValue) : new ActorInitActorReference((Actor)clrValue);
initValues[key] = clrValue;
}
}
compositeInit.Initialize(initValues);
return init;
}
var initializers = initType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(m => m.Name == "Initialize" && m.GetParameters().Length == 1)
.ToList();
foreach (var initializer in initializers)
{
var parameterType = initializer.GetParameters().First().ParameterType;
var valueType = parameterType.IsEnum ? Enum.GetUnderlyingType(parameterType) : parameterType;
// Try and coerce the table value to the required type
if (!value.TryGetClrValue(valueType, out var clrValue))
continue;
initializer.Invoke(init, [clrValue]);
return init;
}
var types = initializers.Select(y => y.GetParameters()[0].ParameterType.Name).JoinWith(", ");
throw new LuaException($"Invalid data type for '{initInstance[0]}' (expected one of {types})");
}
[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, [ScriptEmmyTypeOverride("initTable")] LuaTable initTable)
{
var initDict = new TypeDictionary();
// Convert table entries into ActorInits
foreach (var kv in initTable)
{
using (kv.Key)
using (kv.Value)
initDict.Add(CreateInit(kv.Key.ToString(), kv.Value));
}
var owner = initDict.GetOrDefault<OwnerInit>();
if (owner == null)
throw new LuaException($"Tried to create actor '{type}' with an invalid or no owner init!");
// The actor must be added to the world at the end of the tick
var a = Context.World.CreateActor(false, type, initDict);
if (addToWorld)
Context.World.AddFrameEndTask(w => w.Add(a));
return a;
}
[Desc("Returns the build time (in ticks) of the requested unit type.",
"An optional second value can be used to exactly specify the producing queue type.")]
public int BuildTime(string type, string queue = null)
{
if (!Context.World.Map.Rules.Actors.TryGetValue(type, out var ai))
throw new LuaException($"Unknown actor type '{type}'");
var bi = ai.TraitInfoOrDefault<BuildableInfo>();
if (bi == null)
return 0;
var time = bi.BuildDuration;
if (time == -1)
{
var valued = ai.TraitInfoOrDefault<ValuedInfo>();
if (valued == null)
return 0;
else
time = valued.Cost;
}
int pbi;
if (queue != null)
{
var pqueue = Context.World.Map.Rules.Actors.Values.SelectMany(a => a.TraitInfos<ProductionQueueInfo>()
.Where(x => x.Type == queue)).FirstOrDefault();
if (pqueue == null)
throw new LuaException($"The specified queue '{queue}' does not exist!");
pbi = pqueue.BuildDurationModifier;
}
else
{
var pqueue = Context.World.Map.Rules.Actors.Values.SelectMany(a => a.TraitInfos<ProductionQueueInfo>()
.Where(x => bi.Queue.Contains(x.Type))).FirstOrDefault();
if (pqueue == null)
throw new LuaException($"No actors can produce actor '{type}'!");
pbi = pqueue.BuildDurationModifier;
}
time = time * bi.BuildDurationModifier * pbi / 10000;
return time;
}
[Desc("Returns the cruise altitude of the requested unit type (zero if it is ground-based).")]
public int CruiseAltitude(string type)
{
if (!Context.World.Map.Rules.Actors.TryGetValue(type, out var ai))
throw new LuaException($"Unknown actor type '{type}'");
var pi = ai.TraitInfoOrDefault<ICruiseAltitudeInfo>();
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))
throw new LuaException($"Unknown actor type '{type}'");
var vi = ai.TraitInfoOrDefault<ValuedInfo>();
if (vi == null)
throw new LuaException($"Actor type '{type}' does not have the Valued trait required to get the Cost.");
return vi.Cost;
}
}
}

View File

@@ -0,0 +1,43 @@
#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 OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting.Global
{
[ScriptGlobal("Angle")]
public class AngleGlobal : ScriptGlobal
{
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. 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); }
}
}

View File

@@ -0,0 +1,54 @@
#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 Eluant;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Beacon")]
public class BeaconGlobal : ScriptGlobal
{
readonly RadarPings radarPings;
public BeaconGlobal(ScriptContext context)
: base(context)
{
radarPings = context.World.WorldActor.TraitOrDefault<RadarPings>();
}
[Desc("Creates a new beacon that stays for the specified time at the specified WPos. " +
"Does not remove player set beacons, nor gets removed by placing them. " +
"Requires the 'PlaceBeacon' trait on the player actor.")]
public void New(Player owner, WPos position, int duration = 750, bool showRadarPings = true)
{
var beacon = owner.PlayerActor.Info.TraitInfoOrDefault<PlaceBeaconInfo>();
if (beacon == null)
throw new LuaException("The player actor has no 'PlaceBeacon' trait.");
var playerBeacon = new Beacon(owner, position, duration, beacon.Palette, beacon.IsPlayerPalette,
beacon.BeaconImage, beacon.BeaconSequence, beacon.ArrowSequence, beacon.CircleSequence);
owner.PlayerActor.World.AddFrameEndTask(w => w.Add(playerBeacon));
if (showRadarPings && radarPings != null)
{
radarPings.Add(
() => owner.IsAlliedWith(owner.World.RenderPlayer),
position,
owner.Color,
duration);
}
}
}
}

View File

@@ -0,0 +1,29 @@
#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 OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Camera")]
public class CameraGlobal : ScriptGlobal
{
public CameraGlobal(ScriptContext context)
: base(context) { }
[Desc("The center of the visible viewport.")]
public WPos Position
{
get => Context.WorldRenderer.Viewport.CenterPosition;
set => Context.WorldRenderer.Viewport.Center(value);
}
}
}

View File

@@ -0,0 +1,127 @@
#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 Eluant;
using OpenRA.Primitives;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting.Global
{
// Kept as HSLColor for backwards compatibility
[ScriptGlobal("HSLColor")]
public class ColorGlobal : ScriptGlobal
{
public ColorGlobal(ScriptContext context)
: base(context) { }
[Desc("Create a new color with the specified hue/saturation/luminosity.")]
public Color New(int hue, int saturation, int luminosity)
{
var h = (byte)hue.Clamp(0, 255);
var s = (byte)saturation.Clamp(0, 255);
var l = (byte)luminosity.Clamp(0, 255);
return Color.FromAhsl(255, h / 255f, s / 255f, l / 255f);
}
[Desc("Create a new color with the specified red/green/blue/[alpha] values.")]
public Color FromRGB(int red, int green, int blue, int alpha = 255)
{
return Color.FromArgb(
alpha.Clamp(0, 255),
red.Clamp(0, 255),
green.Clamp(0, 255),
blue.Clamp(0, 255));
}
[Desc("Create a new color with the specified red/green/blue/[alpha] hex string (rrggbb[aa]).")]
public Color FromHex(string value)
{
if (Color.TryParse(value, out var color))
return color;
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;
}
}

View File

@@ -0,0 +1,107 @@
#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.Linq;
using Eluant;
using OpenRA.Mods.Common.Traits;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("CPos")]
public class CPosGlobal : ScriptGlobal
{
public CPosGlobal(ScriptContext context)
: base(context) { }
[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;
}
[ScriptGlobal("CVec")]
public class CVecGlobal : ScriptGlobal
{
public CVecGlobal(ScriptContext context)
: base(context) { }
[Desc("Create a new CVec with the specified coordinates.")]
public CVec New(int x, int y) { return new CVec(x, y); }
[Desc("The cell zero-vector.")]
public CVec Zero => CVec.Zero;
}
[ScriptGlobal("WPos")]
public class WPosGlobal : ScriptGlobal
{
public WPosGlobal(ScriptContext context)
: base(context) { }
[Desc("Create a new WPos with the specified coordinates.")]
public WPos New(int x, int y, int z) { return new WPos(x, y, z); }
[Desc("The world coordinate origin.")]
public WPos Zero => WPos.Zero;
}
[ScriptGlobal("WVec")]
public class WVecGlobal : ScriptGlobal
{
public WVecGlobal(ScriptContext context)
: base(context) { }
[Desc("Create a new WVec with the specified coordinates.")]
public WVec New(int x, int y, int z) { return new WVec(x, y, z); }
[Desc("The world zero-vector.")]
public WVec Zero => WVec.Zero;
}
[ScriptGlobal("WDist")]
public class WDistGlobal : ScriptGlobal
{
public WDistGlobal(ScriptContext context)
: base(context) { }
[Desc("Create a new WDist.")]
public WDist New(int r) { return new WDist(r); }
[Desc("Create a new WDist by cell distance.")]
public WDist FromCells(int numCells) { return WDist.FromCells(numCells); }
}
}

View File

@@ -0,0 +1,94 @@
#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 Eluant;
using OpenRA.Mods.Common.Traits;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("DateTime")]
public class DateGlobal : ScriptGlobal
{
readonly TimeLimitManager tlm;
readonly int ticksPerSecond;
public DateGlobal(ScriptContext context)
: base(context)
{
tlm = context.World.WorldActor.TraitOrDefault<TimeLimitManager>();
var gameSpeeds = Game.ModData.GetOrCreate<GameSpeeds>();
var defaultGameSpeed = gameSpeeds.Speeds[gameSpeeds.DefaultSpeed];
ticksPerSecond = 1000 / defaultGameSpeed.Timestep;
}
[Desc("True on the 31st of October.")]
[Obsolete("Use CurrentMonth and CurrentDay instead.")]
public bool IsHalloween => DateTime.Today.Month == 10 && DateTime.Today.Day == 31;
[Desc("Get the current game time (in ticks).")]
public int GameTime => Context.World.WorldTick;
[Desc("Converts the number of seconds into game time (ticks).")]
public int Seconds(int seconds)
{
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).")]
public int Minutes(int minutes)
{
return Seconds(minutes * 60);
}
[Desc("Return or set the time limit (in ticks). When setting, the time limit will count from now. Setting the time limit to 0 will disable it.")]
public int TimeLimit
{
get => tlm?.TimeLimit ?? 0;
set
{
if (tlm != null)
tlm.TimeLimit = value == 0 ? 0 : value + GameTime;
else
throw new LuaException("Cannot set TimeLimit, TimeLimitManager trait is missing.");
}
}
[Desc("The notification string used for custom time limit warnings. See the TimeLimitManager trait documentation for details.")]
public string TimeLimitNotification
{
get => tlm?.Notification;
set
{
if (tlm != null)
tlm.Notification = value;
else
throw new LuaException("Cannot set TimeLimitNotification, TimeLimitManager trait is missing.");
}
}
}
}

View File

@@ -0,0 +1,67 @@
#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.Collections.Generic;
using OpenRA.Mods.Common.Traits;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Lighting")]
public class LightingGlobal : ScriptGlobal
{
readonly IEnumerable<FlashPostProcessEffect> flashEffects;
readonly TintPostProcessEffect tintEffect;
public LightingGlobal(ScriptContext context)
: base(context)
{
flashEffects = context.World.WorldActor.TraitsImplementing<FlashPostProcessEffect>();
tintEffect = context.World.WorldActor.TraitOrDefault<TintPostProcessEffect>();
}
[Desc("Controls the `" + nameof(FlashPostProcessEffect) + "` trait.")]
public void Flash(string type = null, int ticks = -1)
{
foreach (var effect in flashEffects)
if (effect.Info.Type == type)
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;
set { if (tintEffect != null) tintEffect.Ambient = (float)value; }
}
}
}

View File

@@ -0,0 +1,166 @@
#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();
}
}

View File

@@ -0,0 +1,182 @@
#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 Eluant;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Media")]
public class MediaGlobal : ScriptGlobal
{
readonly World world;
readonly MusicPlaylist playlist;
public MediaGlobal(ScriptContext context)
: base(context)
{
world = context.World;
playlist = world.WorldActor.Trait<MusicPlaylist>();
}
[Desc("Play an announcer voice listed in notifications.yaml")]
public void PlaySpeechNotification(Player player, string notification)
{
Game.Sound.PlayNotification(world.Map.Rules, player, "Speech", notification, player?.Faction.InternalName);
}
[Desc("Play a sound listed in notifications.yaml")]
public void PlaySoundNotification(Player player, string notification)
{
Game.Sound.PlayNotification(world.Map.Rules, player, "Sounds", notification, player?.Faction.InternalName);
}
[Desc("Play a sound file")]
public void PlaySound(string file)
{
// TODO: Investigate how scripts use this function, and think about exposing the UI vs World distinction if needed
Game.Sound.Play(SoundType.World, file);
}
[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, [ScriptEmmyTypeOverride("fun()")] LuaFunction onPlayComplete = null)
{
if (!playlist.IsMusicAvailable)
return;
var musicInfo = !string.IsNullOrEmpty(track)
? GetMusicTrack(track)
: playlist.GetNextSong();
var onComplete = WrapOnPlayComplete(onPlayComplete);
playlist.Play(musicInfo, onComplete);
}
[Desc("Play track defined in music.yaml or map.yaml as background music." +
" If music is already playing use Media.StopMusic() to stop it" +
" and the background music will start automatically." +
" Keep the track empty to disable background music.")]
public void SetBackgroundMusic(string track = null)
{
if (!playlist.IsMusicAvailable)
return;
playlist.SetBackgroundMusic(string.IsNullOrEmpty(track) ? null : GetMusicTrack(track));
}
MusicInfo GetMusicTrack(string track)
{
var music = world.Map.Rules.Music;
if (music.ContainsKey(track))
return music[track];
Log.Write("lua", "Missing music track: " + track);
return null;
}
[Desc("Stop the current song.")]
public void StopMusic()
{
playlist.Stop();
}
[Desc("Play a video fullscreen. File name has to include the file extension.")]
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, [ScriptEmmyTypeOverride("fun()")] LuaFunction onPlayComplete = null)
{
var onComplete = WrapOnPlayComplete(onPlayComplete);
Media.PlayFMVInRadar(videoFileName, onComplete);
}
[Desc("Display a text message to all players.")]
public void DisplayMessage(string text, string prefix = "Mission", Color? color = null)
{
if (string.IsNullOrEmpty(text))
return;
var c = color ?? Color.White;
TextNotificationsManager.AddMissionLine(prefix, text, c);
}
[Desc("Display a text message only to this player.")]
public void DisplayMessageToPlayer(Player player, string text, string prefix = "Mission", Color? color = null)
{
if (world.LocalPlayer != player)
return;
DisplayMessage(text, prefix, color);
}
[Desc("Display a system message to the player. If 'prefix' is nil the default system prefix is used.")]
public void DisplaySystemMessage(string text, string prefix = null)
{
if (string.IsNullOrEmpty(text))
return;
if (string.IsNullOrEmpty(prefix))
TextNotificationsManager.AddSystemLine(text);
else
TextNotificationsManager.AddSystemLine(prefix, text);
}
[Desc("Displays a debug message to the player, if \"Show Map Debug Messages\" is checked in the settings.")]
public void Debug(string format)
{
if (string.IsNullOrEmpty(format) || !Game.Settings.Debug.LuaDebug)
return;
TextNotificationsManager.Debug(format);
}
[Desc("Display a text message at the specified location.")]
public void FloatingText(string text, WPos position, int duration = 30, Color? color = null)
{
if (string.IsNullOrEmpty(text) || !world.Map.Contains(world.Map.CellContaining(position)))
return;
var c = color ?? Color.White;
world.AddFrameEndTask(w => w.Add(new FloatingText(position, c, text, duration)));
}
Action WrapOnPlayComplete(LuaFunction onPlayComplete)
{
if (onPlayComplete != null)
{
var f = (LuaFunction)onPlayComplete.CopyReference();
return () =>
{
try
{
using (f)
f.Call().Dispose();
}
catch (LuaException e)
{
Context.FatalError(e);
}
};
}
else
return () => { };
}
}
}

View File

@@ -0,0 +1,36 @@
#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.Linq;
using Eluant;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Player")]
public class PlayerGlobal : ScriptGlobal
{
public PlayerGlobal(ScriptContext context)
: base(context) { }
[Desc("Returns the player with the specified internal name, or nil if a match is not found.")]
public Player GetPlayer(string name)
{
return Context.World.Players.FirstOrDefault(p => p.InternalName == name);
}
[Desc("Returns a table of players filtered by the specified function.")]
public Player[] GetPlayers([ScriptEmmyTypeOverride("fun(p: player):boolean")] LuaFunction filter)
{
return FilteredObjects(Context.World.Players, filter).ToArray();
}
}
}

View File

@@ -0,0 +1,35 @@
#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 OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Radar")]
public class RadarGlobal : ScriptGlobal
{
readonly RadarPings radarPings;
public RadarGlobal(ScriptContext context)
: base(context)
{
radarPings = context.World.WorldActor.TraitOrDefault<RadarPings>();
}
[Desc("Creates a new radar ping that stays for the specified time at the specified WPos.")]
public void Ping(Player player, WPos position, Color color, int duration = 750)
{
radarPings?.Add(() => player.World.RenderPlayer == player, position, color, duration);
}
}
}

View File

@@ -0,0 +1,204 @@
#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.Collections.Generic;
using System.Linq;
using Eluant;
using OpenRA.Activities;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Scripting;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Reinforcements")]
public class ReinforcementsGlobal : ScriptGlobal
{
public ReinforcementsGlobal(ScriptContext context)
: base(context)
{
}
Actor CreateActor(Player owner, string actorType, bool addToWorld, CPos? entryLocation = null, CPos? nextLocation = null)
{
if (!Context.World.Map.Rules.Actors.TryGetValue(actorType, out var ai))
throw new LuaException($"Unknown actor type '{actorType}'");
var initDict = new TypeDictionary
{
new OwnerInit(owner)
};
if (entryLocation.HasValue)
{
initDict.Add(new LocationInit(entryLocation.Value));
var pi = ai.TraitInfoOrDefault<AircraftInfo>();
if (pi != null)
initDict.Add(new CenterPositionInit(owner.World.Map.CenterOfCell(entryLocation.Value) + new WVec(0, 0, pi.CruiseAltitude.Length)));
}
if (entryLocation.HasValue && nextLocation.HasValue)
{
var facing = Context.World.Map.FacingBetween(CPos.Zero, CPos.Zero + (nextLocation.Value - entryLocation.Value), WAngle.Zero);
initDict.Add(new FacingInit(facing));
}
// The actor must be added to the world at the end of the tick.
var a = Context.World.CreateActor(false, actorType, initDict);
if (addToWorld)
Context.World.AddFrameEndTask(w => w.Add(a));
return a;
}
static void Move(Actor actor, CPos dest)
{
var move = actor.TraitOrDefault<IMove>();
if (move == null)
return;
actor.QueueActivity(move.MoveTo(dest, 2));
}
[Desc("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.")]
public Actor[] Reinforce(Player owner, string[] actorTypes, CPos[] entryPath, int interval = 25,
[ScriptEmmyTypeOverride("fun(a: actor)")] LuaFunction actionFunc = null)
{
var actors = new List<Actor>();
for (var i = 0; i < actorTypes.Length; i++)
{
var af = actionFunc != null ? (LuaFunction)actionFunc.CopyReference() : null;
var actor = CreateActor(owner, actorTypes[i], false, entryPath[0], entryPath.Length > 1 ? entryPath[1] : null);
actors.Add(actor);
var actionDelay = i * interval;
Activity queuedActivity = null;
if (af != null)
{
queuedActivity = new CallFunc(() =>
{
using (af)
using (var a = actor.ToLuaValue(Context))
af.Call(a);
});
}
// We need to exclude the spawn location from the movement path
var path = entryPath.Skip(1).ToArray();
Context.World.AddFrameEndTask(w => w.Add(new SpawnActorEffect(actor, actionDelay, path, queuedActivity)));
}
return actors.ToArray();
}
[Desc("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.")]
[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<Cargo>();
var passengers = new List<Actor>();
if (cargo != null && cargoTypes != null)
{
foreach (var cargoType in cargoTypes)
{
var passenger = CreateActor(owner, cargoType, false, entryPath[0]);
passengers.Add(passenger);
cargo.Load(transport, passenger);
}
}
for (var i = 1; i < entryPath.Length; i++)
Move(transport, entryPath[i]);
if (actionFunc != null)
{
var af = (LuaFunction)actionFunc.CopyReference();
transport.QueueActivity(new CallFunc(() =>
{
using (af)
using (LuaValue t = transport.ToLuaValue(Context), p = passengers.ToArray().ToLuaValue(Context))
af.Call(t, p);
}));
}
else
{
var aircraft = transport.TraitOrDefault<Aircraft>();
// Scripted cargo aircraft must turn to default position before unloading.
// TODO: pass facing through UnloadCargo instead.
if (aircraft != null)
transport.QueueActivity(new Land(transport, Target.FromCell(transport.World, entryPath[^1]), WDist.FromCells(dropRange)));
if (cargo != null)
transport.QueueActivity(new UnloadCargo(transport, WDist.FromCells(dropRange)));
}
if (exitFunc != null)
{
var ef = (LuaFunction)exitFunc.CopyReference();
transport.QueueActivity(new CallFunc(() =>
{
using (ef)
using (var t = transport.ToLuaValue(Context))
ef.Call(t);
}));
}
else if (exitPath != null)
{
foreach (var wpt in exitPath)
Move(transport, wpt);
transport.QueueActivity(new RemoveSelf());
}
var ret = Context.CreateTable();
using (LuaValue
tKey = 1,
tValue = transport.ToLuaValue(Context),
pKey = 2,
pValue = passengers.ToArray().ToLuaValue(Context))
{
ret.Add(tKey, tValue);
ret.Add(pKey, pValue);
}
return ret;
}
}
}

View File

@@ -0,0 +1,588 @@
#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.Effects;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Trigger")]
public class TriggerGlobal : ScriptGlobal
{
public TriggerGlobal(ScriptContext context)
: base(context) { }
public static ScriptTriggers GetScriptTriggers(Actor actor)
{
var events = actor.TraitOrDefault<ScriptTriggers>();
if (events == null)
throw new LuaException($"Actor '{actor.Info.Name}' requires the ScriptTriggers trait before attaching a trigger");
return events;
}
[Desc("Call a function after a specified delay. The callback function will be called as func().")]
public void AfterDelay(int delay, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
var f = (LuaFunction)func.CopyReference();
void DoCall()
{
try
{
using (f)
f.Call().Dispose();
}
catch (Exception e)
{
Context.FatalError(e);
}
}
Context.World.AddFrameEndTask(w => w.Add(new DelayedAction(delay, DoCall)));
}
[Desc("Call a function for each passenger when it enters a transport. " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnPassengerEntered, func, Context);
}
[Desc("Call a function for each passenger when it exits a transport. " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnPassengerExited, func, Context);
}
[Desc("Call a function each tick that the actor is idle. " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnIdle, func, Context);
}
[Desc("Call a function when the actor is damaged. " +
"Repairs or other negative damage can activate this trigger. The callback " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnDamaged, func, Context);
}
[Desc("Call a function when the actor is killed. The callback " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnKilled, func, Context);
}
[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, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
if (actors == null)
throw new NullReferenceException(nameof(actors));
var group = actors.ToList();
var f = (LuaFunction)func.CopyReference();
void OnMemberKilled(Actor m)
{
try
{
group.Remove(m);
if (group.Count == 0)
using (f)
f.Call();
}
catch (Exception e)
{
Context.FatalError(e);
}
}
foreach (var a in group)
GetScriptTriggers(a).OnKilledInternal += OnMemberKilled;
}
[Desc("Call a function when one of the actors in a group is killed. " +
"This trigger is only called once. The callback " +
"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();
void OnMemberKilled(Actor m)
{
try
{
if (called)
return;
using (f)
using (var killed = m.ToLuaValue(Context))
f.Call(killed).Dispose();
called = true;
}
catch (Exception e)
{
Context.FatalError(e);
}
}
if (actors == null)
throw new NullReferenceException(nameof(actors));
foreach (var a in actors)
GetScriptTriggers(a).OnKilledInternal += OnMemberKilled;
}
[Desc("Call a function when this actor produces another actor. " +
"The callback function will be called as func(producer: actor, produced: actor).")]
public void OnProduction(Actor actor, [ScriptEmmyTypeOverride("fun(producer: actor, produced: actor)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnProduction, func, Context);
}
[Desc("Call a function when any actor produces another actor. The callback " +
"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(p: player).")]
public void OnPlayerWon(Player player, [ScriptEmmyTypeOverride("fun(p: player)")] LuaFunction func)
{
if (player == null)
throw new NullReferenceException(nameof(player));
GetScriptTriggers(player.PlayerActor).RegisterCallback(Trigger.OnPlayerWon, func, Context);
}
[Desc("Call a function when this player fails any primary objective. " +
"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));
GetScriptTriggers(player.PlayerActor).RegisterCallback(Trigger.OnPlayerLost, func, Context);
}
[Desc("Call a function when this player is assigned a new objective. " +
"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));
GetScriptTriggers(player.PlayerActor).RegisterCallback(Trigger.OnObjectiveAdded, func, Context);
}
[Desc("Call a function when this player completes an objective. " +
"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));
GetScriptTriggers(player.PlayerActor).RegisterCallback(Trigger.OnObjectiveCompleted, func, Context);
}
[Desc("Call a function when this player fails an objective. " +
"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));
GetScriptTriggers(player.PlayerActor).RegisterCallback(Trigger.OnObjectiveFailed, func, Context);
}
[Desc("Call a function when this player places a building. " +
"The callback function will be called as func(p: player, placed: actor).")]
public void OnBuildingPlaced(Player player, [ScriptEmmyTypeOverride("fun(p: player, placed: actor)")] LuaFunction func)
{
if (player == null)
throw new NullReferenceException(nameof(player));
GetScriptTriggers(player.PlayerActor).RegisterCallback(Trigger.OnBuildingPlaced, func, Context);
}
[Desc("Call a function when this actor is added to the world. " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnAddedToWorld, func, Context);
}
[Desc("Call a function when this actor is removed from the world. " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnRemovedFromWorld, func, Context);
}
[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, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
if (actors == null)
throw new NullReferenceException(nameof(actors));
var group = actors.ToList();
var f = (LuaFunction)func.CopyReference();
void OnMemberRemoved(Actor m)
{
try
{
if (!group.Remove(m))
return;
if (group.Count == 0)
{
// Functions can only be .Call()ed once, so operate on a copy so we can reuse it later
var temp = (LuaFunction)f.CopyReference();
using (temp)
temp.Call().Dispose();
}
}
catch (Exception e)
{
Context.FatalError(e);
}
}
void OnMemberAdded(Actor m)
{
try
{
if (!actors.Contains(m) || group.Contains(m))
return;
group.Add(m);
}
catch (Exception e)
{
Context.FatalError(e);
}
}
foreach (var a in group)
{
GetScriptTriggers(a).OnRemovedInternal += OnMemberRemoved;
GetScriptTriggers(a).OnAddedInternal += OnMemberAdded;
}
}
[Desc("Call a function when this actor is captured. The callback function " +
"will be called as func(self: actor, captor: actor, oldOwner: player, newOwner: player).")]
public void OnCapture(Actor actor, [ScriptEmmyTypeOverride("fun(self: actor, captor: actor, oldOwner: player, newOwner: player)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnCapture, func, Context);
}
[Desc("Call a function when this actor is killed or captured. " +
"This trigger is only called once. " +
"The callback function will be called as func().")]
public void OnKilledOrCaptured(Actor actor, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
var called = false;
var f = (LuaFunction)func.CopyReference();
void OnKilledOrCaptured(Actor m)
{
try
{
if (called)
return;
using (f)
f.Call().Dispose();
called = true;
}
catch (Exception e)
{
Context.FatalError(e);
}
}
if (actor == null)
throw new NullReferenceException(nameof(actor));
GetScriptTriggers(actor).OnCapturedInternal += OnKilledOrCaptured;
GetScriptTriggers(actor).OnKilledInternal += OnKilledOrCaptured;
}
[Desc("Call a function when all of the actors in a group have been killed or captured. " +
"This trigger is only called once. " +
"The callback function will be called as func().")]
public void OnAllKilledOrCaptured(Actor[] actors, [ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
if (actors == null)
throw new NullReferenceException(nameof(actors));
var group = actors.ToList();
var f = (LuaFunction)func.CopyReference();
void OnMemberKilledOrCaptured(Actor m)
{
try
{
if (!group.Remove(m))
return;
if (group.Count == 0)
using (f)
f.Call().Dispose();
}
catch (Exception e)
{
Context.FatalError(e);
}
}
foreach (var a in group)
{
GetScriptTriggers(a).OnCapturedInternal += OnMemberKilledOrCaptured;
GetScriptTriggers(a).OnKilledInternal += OnMemberKilledOrCaptured;
}
}
[Desc("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).")]
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();
var triggerId = 0;
void InvokeEntry(Actor a)
{
try
{
using (var luaActor = a.ToLuaValue(Context))
using (var id = triggerId.ToLuaValue(Context))
onEntry.Call(luaActor, id).Dispose();
}
catch (Exception e)
{
Context.FatalError(e);
}
}
triggerId = Context.World.ActorMap.AddCellTrigger(cells, InvokeEntry, null);
return triggerId;
}
[Desc("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).")]
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();
var triggerId = 0;
void InvokeExit(Actor a)
{
try
{
using (var luaActor = a.ToLuaValue(Context))
using (var id = triggerId.ToLuaValue(Context))
onExit.Call(luaActor, id).Dispose();
}
catch (Exception e)
{
Context.FatalError(e);
}
}
triggerId = Context.World.ActorMap.AddCellTrigger(cells, null, InvokeExit);
return triggerId;
}
[Desc("Removes a previously created footprint trigger.")]
public void RemoveFootprintTrigger(int id)
{
Context.World.ActorMap.RemoveCellTrigger(id);
}
[Desc("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).")]
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();
var triggerId = 0;
void InvokeEntry(Actor a)
{
try
{
using (var luaActor = a.ToLuaValue(Context))
using (var id = triggerId.ToLuaValue(Context))
onEntry.Call(luaActor, id).Dispose();
}
catch (Exception e)
{
Context.FatalError(e);
}
}
triggerId = Context.World.ActorMap.AddProximityTrigger(pos, range, WDist.Zero, InvokeEntry, null);
return triggerId;
}
[Desc("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).")]
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();
var triggerId = 0;
void InvokeExit(Actor a)
{
try
{
using (var luaActor = a.ToLuaValue(Context))
using (var id = triggerId.ToLuaValue(Context))
onExit.Call(luaActor, id).Dispose();
}
catch (Exception e)
{
Context.FatalError(e);
}
}
triggerId = Context.World.ActorMap.AddProximityTrigger(pos, range, WDist.Zero, null, InvokeExit);
return triggerId;
}
[Desc("Removes a previously created proximity trigger.")]
public void RemoveProximityTrigger(int id)
{
Context.World.ActorMap.RemoveProximityTrigger(id);
}
[Desc("Call a function when this actor is infiltrated. The callback function " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnInfiltrated, func, Context);
}
[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(discovered: actor, discoverer: player). " +
"The player actor needs the 'EnemyWatcher' trait. The actors to discover need the 'AnnounceOnSeen' trait.")]
public void OnDiscovered(Actor actor, [ScriptEmmyTypeOverride("fun(discovered: actor, discoverer: player)")] LuaFunction func)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnDiscovered, func, Context);
}
[Desc("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.")]
public void OnPlayerDiscovered(
Player discovered, [ScriptEmmyTypeOverride("fun(discovered: player, discoverer: player, discoveredActor: actor)")] LuaFunction func)
{
if (discovered == null)
throw new NullReferenceException(nameof(discovered));
GetScriptTriggers(discovered.PlayerActor).RegisterCallback(Trigger.OnPlayerDiscovered, func, Context);
}
[Desc("Call a function when this actor is sold. The callback function " +
"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));
GetScriptTriggers(actor).RegisterCallback(Trigger.OnSold, func, Context);
}
[Desc("Call a function when the game timer expires. The callback function will be called as func().")]
public void OnTimerExpired([ScriptEmmyTypeOverride("fun()")] LuaFunction func)
{
GetScriptTriggers(Context.World.WorldActor).RegisterCallback(Trigger.OnTimerExpired, func, Context);
}
[Desc("Removes all triggers 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.")]
public void ClearAll(Actor actor)
{
if (actor == null)
throw new NullReferenceException(nameof(actor));
GetScriptTriggers(actor).ClearAll();
}
[Desc("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.")]
public void Clear(Actor actor, string triggerName)
{
var trigger = Enum.Parse<Trigger>(triggerName);
if (actor == null)
throw new NullReferenceException(nameof(actor));
GetScriptTriggers(actor).Clear(trigger);
}
}
}

View File

@@ -0,0 +1,65 @@
#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 Eluant;
using OpenRA.Mods.Common.Widgets;
using OpenRA.Primitives;
using OpenRA.Scripting;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Scripting.Global
{
[ScriptGlobal("UserInterface")]
public class UserInterfaceGlobal : ScriptGlobal
{
public UserInterfaceGlobal(ScriptContext context)
: base(context) { }
[Desc("Displays a text message at the top center of the screen.")]
public void SetMissionText(string text, Color? color = null)
{
var luaLabel = Ui.Root.Get("INGAME_ROOT").Get<LabelWidget>("MISSION_TEXT");
luaLabel.GetText = () => text;
var c = color ?? Color.White;
luaLabel.GetColor = () => c;
}
[Desc("Formats a language string for a given string key defined in the language files (*.ftl). " +
"Args can be passed to be substituted into the resulting message.")]
public string GetFluentMessage(string key, [ScriptEmmyTypeOverride("{ string: any }")] LuaTable args = null)
{
if (args != null)
{
var argumentDictionary = new object[args.Count * 2];
var i = 0;
foreach (var kv in args)
{
using (kv.Key)
using (kv.Value)
{
if (!kv.Key.TryGetClrValue<string>(out var variable) || !kv.Value.TryGetClrValue<object>(out var value))
throw new LuaException(
"String arguments requires a table of [\"string\"]=value pairs. " +
$"Received {kv.Key.WrappedClrType().Name},{kv.Value.WrappedClrType().Name}");
argumentDictionary[i++] = variable;
argumentDictionary[i++] = value;
}
}
return FluentProvider.GetMessage(key, argumentDictionary);
}
return FluentProvider.GetMessage(key);
}
}
}

View File

@@ -0,0 +1,155 @@
#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.Linq;
using Eluant;
using OpenRA.Mods.Common.Widgets;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.Scripting
{
[ScriptGlobal("Utils")]
public class UtilsGlobal : ScriptGlobal
{
public UtilsGlobal(ScriptContext context)
: base(context) { }
[Desc("Calls a function on every element in a collection.")]
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(
[ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] collection,
[ScriptEmmyTypeOverride("fun(item: T):boolean?", "T")] LuaFunction func)
{
foreach (var c in collection)
using (var ret = func.Call(c))
using (var result = ret.FirstOrDefault())
if (result != null && result.ToBoolean())
return true;
return false;
}
[Desc("Returns true if func returns true for all elements in a collection.")]
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))
using (var result = ret.FirstOrDefault())
if (result == null || !result.ToBoolean())
return false;
return true;
}
[Desc("Returns the original collection filtered with the 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();
foreach (var c in collection)
using (var ret = func.Call(c))
using (var result = ret.FirstOrDefault())
if (result != null && result.ToBoolean())
t.Add(t.Count + 1, c);
return t;
}
[Desc("Returns the first n values from a collection.")]
[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.")]
[return: ScriptEmmyTypeOverride("T[]", "T")]
public LuaTable Skip(
[ScriptEmmyTypeOverride("T[]", "T")] LuaTable table,
int numElements)
{
var t = Context.CreateTable();
for (var i = numElements; i <= table.Count; i++)
using (LuaValue key = t.Count + 1, value = table[i])
t.Add(key, value);
return t;
}
[Desc("Concatenates two Lua tables into a single table.")]
[return: ScriptEmmyTypeOverride("T[]", "T")]
public LuaTable Concat(
[ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] firstCollection,
[ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] secondCollection)
{
var t = Context.CreateTable();
foreach (var e in firstCollection)
t.Add(t.Count + 1, e);
foreach (var e in secondCollection)
t.Add(t.Count + 1, e);
return t;
}
[Desc("Returns a random value from a 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.")]
[return: ScriptEmmyTypeOverride("T[]", "T")]
public LuaValue[] Shuffle([ScriptEmmyTypeOverride("T[]", "T")] LuaValue[] collection)
{
return collection.Shuffle(Context.World.SharedRandom).ToArray();
}
[Desc("Expands the given footprint one step along the coordinate axes, and (if requested) diagonals.")]
public CPos[] ExpandFootprint(CPos[] footprint, bool allowDiagonal)
{
return Util.ExpandFootprint(footprint, allowDiagonal).ToArray();
}
[Desc("Returns a random integer x in the range low &lt;= x &lt; high.")]
public int RandomInteger(int low, int high)
{
if (high <= low)
return low;
return Context.World.SharedRandom.Next(low, high);
}
[Desc("Returns the ticks formatted to HH:MM:SS.")]
public string FormatTime(int ticks, bool leadingMinuteZero = true)
{
return WidgetUtils.FormatTime(ticks, leadingMinuteZero, 40);
}
}
}