Initial commit: OpenRA game engine
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:
57
OpenRA.Game/Scripting/ScriptActorInterface.cs
Normal file
57
OpenRA.Game/Scripting/ScriptActorInterface.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
#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;
|
||||
|
||||
namespace OpenRA.Scripting
|
||||
{
|
||||
public class ScriptActorInterface : ScriptObjectWrapper
|
||||
{
|
||||
readonly Actor actor;
|
||||
|
||||
protected override string DuplicateKeyError(string memberName) { return $"Actor '{actor.Info.Name}' defines the command '{memberName}' on multiple traits"; }
|
||||
protected override string MemberNotFoundError(string memberName)
|
||||
{
|
||||
var actorName = actor.Info.Name;
|
||||
if (actor.IsDead)
|
||||
actorName += " (dead)";
|
||||
|
||||
return $"Actor '{actorName}' does not define a property '{memberName}'";
|
||||
}
|
||||
|
||||
public ScriptActorInterface(ScriptContext context, Actor actor)
|
||||
: base(context)
|
||||
{
|
||||
this.actor = actor;
|
||||
|
||||
InitializeBindings();
|
||||
}
|
||||
|
||||
void InitializeBindings()
|
||||
{
|
||||
var commandClasses = Context.ActorCommands[actor.Info];
|
||||
|
||||
// Destroyed actors cannot have their traits queried. In rare cases the actor may have already been destroyed.
|
||||
if (actor.Disposed)
|
||||
commandClasses = commandClasses.Where(c => c.HasAttribute<ExposedForDestroyedActors>()).ToArray();
|
||||
|
||||
Bind(CreateObjects(commandClasses, [Context, actor]));
|
||||
}
|
||||
|
||||
public void OnActorDestroyed()
|
||||
{
|
||||
// Remove bindings not available to destroyed actors.
|
||||
foreach (var commandClass in Context.ActorCommands[actor.Info])
|
||||
if (!commandClass.HasAttribute<ExposedForDestroyedActors>())
|
||||
Unbind(commandClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
346
OpenRA.Game/Scripting/ScriptContext.cs
Normal file
346
OpenRA.Game/Scripting/ScriptContext.cs
Normal file
@@ -0,0 +1,346 @@
|
||||
#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.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Eluant;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Scripting
|
||||
{
|
||||
// Tag interfaces specifying the type of bindings to create
|
||||
public interface IScriptBindable { }
|
||||
|
||||
// For objects that need the context to create their bindings
|
||||
public interface IScriptNotifyBind
|
||||
{
|
||||
void OnScriptBind(ScriptContext context);
|
||||
}
|
||||
|
||||
// For traitinfos that provide actor / player commands
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ScriptPropertyGroupAttribute(string category) : Attribute
|
||||
{
|
||||
public readonly string Category = category;
|
||||
}
|
||||
|
||||
// For property groups that are safe to initialize invoke on destroyed actors
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ExposedForDestroyedActors : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)]
|
||||
public sealed class ScriptActorPropertyActivityAttribute : Attribute { }
|
||||
|
||||
public abstract class ScriptActorProperties(ScriptContext context, Actor self)
|
||||
{
|
||||
protected readonly Actor Self = self;
|
||||
protected readonly ScriptContext Context = context;
|
||||
}
|
||||
|
||||
public abstract class ScriptPlayerProperties(ScriptContext context, Player player)
|
||||
{
|
||||
protected readonly Player Player = player;
|
||||
protected readonly ScriptContext Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides global bindings in Lua code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Instance methods and properties declared in derived classes will be made available in Lua. Use
|
||||
/// <see cref="ScriptGlobalAttribute"/> on your derived class to specify the name exposed in Lua. It is recommended
|
||||
/// to apply <see cref="DescAttribute"/> against each method or property to provide a description of what it does.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Any parameters to your method that are <see cref="LuaValue"/>s will be disposed automatically when your method
|
||||
/// completes. If you need to return any of these values, or need them to live longer than your method, you must
|
||||
/// use <see cref="LuaValue.CopyReference"/> to get your own copy of the value. Any copied values you return will
|
||||
/// be disposed automatically, but you assume responsibility for disposing any other copies.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class ScriptGlobal : ScriptObjectWrapper
|
||||
{
|
||||
protected override string DuplicateKeyError(string memberName) { return $"Table '{Name}' defines multiple members '{memberName}'"; }
|
||||
protected override string MemberNotFoundError(string memberName) { return $"Table '{Name}' does not define a property '{memberName}'"; }
|
||||
|
||||
public readonly string Name;
|
||||
|
||||
protected ScriptGlobal(ScriptContext context)
|
||||
: base(context)
|
||||
{
|
||||
// GetType resolves the actual (subclass) type
|
||||
var type = GetType();
|
||||
var names = type.GetCustomAttributes<ScriptGlobalAttribute>(true);
|
||||
if (names.Length != 1)
|
||||
throw new InvalidOperationException($"[ScriptGlobal] attribute not found for global table '{type}'");
|
||||
|
||||
Name = names[0].Name;
|
||||
Bind([this]);
|
||||
}
|
||||
|
||||
protected IEnumerable<T> FilteredObjects<T>(IEnumerable<T> objects, LuaFunction filter)
|
||||
{
|
||||
if (filter != null)
|
||||
{
|
||||
objects = objects.Where(a =>
|
||||
{
|
||||
using (var luaObject = a.ToLuaValue(Context))
|
||||
using (var filterResult = filter.Call(luaObject))
|
||||
using (var result = filterResult[0])
|
||||
return result.ToBoolean();
|
||||
});
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ScriptGlobalAttribute(string name) : Attribute
|
||||
{
|
||||
public readonly string Name = name;
|
||||
}
|
||||
|
||||
public sealed class ScriptContext : IDisposable
|
||||
{
|
||||
// Restrict user scripts (excluding system libraries) to 50 MB of memory use
|
||||
const int MaxUserScriptMemory = 50 * 1024 * 1024;
|
||||
|
||||
// Restrict the number of instructions that will be run per map function call
|
||||
const int MaxUserScriptInstructions = 1000000;
|
||||
|
||||
public World World { get; }
|
||||
public WorldRenderer WorldRenderer { get; }
|
||||
|
||||
readonly MemoryConstrainedLuaRuntime runtime;
|
||||
readonly LuaFunction tick;
|
||||
|
||||
readonly Type[] knownActorCommands;
|
||||
public readonly Cache<ActorInfo, Type[]> ActorCommands;
|
||||
public readonly Type[] PlayerCommands;
|
||||
|
||||
public string ErrorMessage;
|
||||
|
||||
bool disposed;
|
||||
|
||||
public ScriptContext(World world, WorldRenderer worldRenderer,
|
||||
IEnumerable<string> scripts)
|
||||
{
|
||||
runtime = new MemoryConstrainedLuaRuntime();
|
||||
|
||||
Log.AddChannel("lua", "lua.log");
|
||||
|
||||
World = world;
|
||||
WorldRenderer = worldRenderer;
|
||||
knownActorCommands = Game.ModData.ObjectCreator
|
||||
.GetTypesImplementing<ScriptActorProperties>()
|
||||
.ToArray();
|
||||
|
||||
ActorCommands = new Cache<ActorInfo, Type[]>(FilterActorCommands);
|
||||
|
||||
var knownPlayerCommands = Game.ModData.ObjectCreator
|
||||
.GetTypesImplementing<ScriptPlayerProperties>()
|
||||
.ToArray();
|
||||
PlayerCommands = FilterCommands(world.Map.Rules.Actors[SystemActors.Player], knownPlayerCommands);
|
||||
|
||||
// Safe functions for http://lua-users.org/wiki/SandBoxes
|
||||
// assert, error have been removed as well as albeit safe
|
||||
var allowedGlobals = new string[]
|
||||
{
|
||||
"ipairs", "next", "pairs",
|
||||
"pcall", "select", "tonumber", "tostring", "type", "unpack", "xpcall",
|
||||
"math", "string", "table"
|
||||
};
|
||||
|
||||
foreach (var fieldName in runtime.Globals.Keys)
|
||||
if (!allowedGlobals.Contains(fieldName.ToString()))
|
||||
runtime.Globals[fieldName] = null;
|
||||
|
||||
var forbiddenMath = new string[]
|
||||
{
|
||||
"random", // not desync safe, unsuitable
|
||||
"randomseed" // maybe unsafe as it affects the host RNG
|
||||
};
|
||||
|
||||
var mathGlobal = (LuaTable)runtime.Globals["math"];
|
||||
foreach (var mathFunction in mathGlobal.Keys)
|
||||
if (forbiddenMath.Contains(mathFunction.ToString()))
|
||||
mathGlobal[mathFunction] = null;
|
||||
|
||||
// Register globals
|
||||
runtime.Globals["EngineDir"] = Platform.EngineDir;
|
||||
|
||||
using (var fn = runtime.CreateFunctionFromDelegate((Action<string>)FatalError))
|
||||
runtime.Globals["FatalError"] = fn;
|
||||
|
||||
runtime.Globals["MaxUserScriptInstructions"] = MaxUserScriptInstructions;
|
||||
|
||||
using (var fn = runtime.CreateFunctionFromDelegate(LogDebugMessage))
|
||||
runtime.Globals["print"] = fn;
|
||||
|
||||
// Register global tables
|
||||
var bindings = Game.ModData.ObjectCreator.GetTypesImplementing<ScriptGlobal>();
|
||||
foreach (var b in bindings)
|
||||
{
|
||||
var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
|
||||
{
|
||||
var p = c.GetParameters();
|
||||
return p.Length == 1 && p[0].ParameterType == typeof(ScriptContext);
|
||||
});
|
||||
|
||||
if (ctor == null)
|
||||
throw new InvalidOperationException($"{b.Name} must define a constructor that takes a {nameof(ScriptContext)} context parameter");
|
||||
|
||||
var binding = (ScriptGlobal)ctor.Invoke([this]);
|
||||
using (var obj = binding.ToLuaValue(this))
|
||||
runtime.Globals.Add(binding.Name, obj);
|
||||
}
|
||||
|
||||
// System functions do not count towards the memory limit
|
||||
runtime.MaxMemoryUse = runtime.MemoryUse + MaxUserScriptMemory;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var script in scripts)
|
||||
runtime.DoBuffer(world.Map.Open(script).ReadAllText(), script).Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
FatalError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
tick = runtime.Globals["Tick"] as LuaFunction;
|
||||
}
|
||||
|
||||
void LogDebugMessage(string message)
|
||||
{
|
||||
Console.WriteLine($"Lua debug: {message}");
|
||||
Log.Write("lua", message);
|
||||
}
|
||||
|
||||
public bool FatalErrorOccurred { get; private set; }
|
||||
public void FatalError(Exception e)
|
||||
{
|
||||
ErrorMessage = e.Message;
|
||||
|
||||
Console.WriteLine($"Fatal Lua Error: {e.Message}");
|
||||
Console.WriteLine(e.StackTrace);
|
||||
|
||||
Log.Write("lua", $"Fatal Lua Error: {e.Message}");
|
||||
Log.Write("lua", e.StackTrace);
|
||||
|
||||
FatalErrorOccurred = true;
|
||||
|
||||
World.AddFrameEndTask(w => World.EndGame());
|
||||
}
|
||||
|
||||
void FatalError(string message)
|
||||
{
|
||||
var stacktrace = new StackTrace().ToString();
|
||||
|
||||
Console.WriteLine($"Fatal Lua Error: {message}");
|
||||
Console.WriteLine(stacktrace);
|
||||
|
||||
Log.Write("lua", message);
|
||||
Log.Write("lua", stacktrace);
|
||||
|
||||
FatalErrorOccurred = true;
|
||||
|
||||
World.AddFrameEndTask(w => World.EndGame());
|
||||
}
|
||||
|
||||
public void RegisterMapActor(string name, Actor a)
|
||||
{
|
||||
if (runtime.Globals.ContainsKey(name))
|
||||
throw new LuaException($"The global name '{name}' is reserved, and may not be used by a map actor");
|
||||
|
||||
using (var obj = a.ToLuaValue(this))
|
||||
runtime.Globals.Add(name, obj);
|
||||
}
|
||||
|
||||
public void WorldLoaded()
|
||||
{
|
||||
if (FatalErrorOccurred || runtime.Globals["WorldLoaded"] is not LuaFunction worldLoaded)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
worldLoaded.Call().Dispose();
|
||||
}
|
||||
catch (LuaException e)
|
||||
{
|
||||
FatalError(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
worldLoaded?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (FatalErrorOccurred || disposed || tick == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using (new PerfSample("tick_lua"))
|
||||
tick.Call().Dispose();
|
||||
}
|
||||
catch (LuaException e)
|
||||
{
|
||||
FatalError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
disposed = true;
|
||||
runtime?.Dispose();
|
||||
}
|
||||
|
||||
static IEnumerable<Type> ExtractRequiredTypes(Type t)
|
||||
{
|
||||
// Returns the inner types of all the Requires<T> interfaces on this type
|
||||
var outer = t.GetInterfaces()
|
||||
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>));
|
||||
|
||||
return outer.SelectMany(i => i.GetGenericArguments());
|
||||
}
|
||||
|
||||
static readonly object[] NoArguments = [];
|
||||
Type[] FilterActorCommands(ActorInfo ai)
|
||||
{
|
||||
return FilterCommands(ai, knownActorCommands);
|
||||
}
|
||||
|
||||
Type[] FilterCommands(ActorInfo ai, Type[] knownCommands)
|
||||
{
|
||||
var method = typeof(ActorInfo).GetMethod(nameof(ActorInfo.HasTraitInfo));
|
||||
return knownCommands.Where(c => ExtractRequiredTypes(c)
|
||||
.All(t => (bool)method.MakeGenericMethod(t).Invoke(ai, NoArguments)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public LuaTable CreateTable() { return runtime.CreateTable(); }
|
||||
}
|
||||
}
|
||||
74
OpenRA.Game/Scripting/ScriptMemberExts.cs
Normal file
74
OpenRA.Game/Scripting/ScriptMemberExts.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
#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.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace OpenRA.Scripting
|
||||
{
|
||||
public static class ScriptMemberExts
|
||||
{
|
||||
static readonly FrozenDictionary<string, string> LuaTypeNameReplacements = new Dictionary<string, string>
|
||||
{
|
||||
{ "Void", "void" },
|
||||
{ "Int32", "int" },
|
||||
{ "String", "string" },
|
||||
{ "Boolean", "bool" }
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
public static string LuaDocString(this Type t)
|
||||
{
|
||||
if (!LuaTypeNameReplacements.TryGetValue(t.Name, out var ret))
|
||||
ret = t.Name;
|
||||
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
ret = $"{t.GetGenericArguments()[0].LuaDocString()}?";
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static string LuaDocString(this ParameterInfo pi)
|
||||
{
|
||||
var ret = $"{pi.ParameterType.LuaDocString()} {pi.Name}";
|
||||
if (pi.IsOptional)
|
||||
ret += $" = {pi.DefaultValue ?? "nil"}";
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static string LuaDocString(this MemberInfo mi)
|
||||
{
|
||||
var methodInfo = mi as MethodInfo;
|
||||
if (methodInfo != null)
|
||||
{
|
||||
var parameters = methodInfo.GetParameters().Select(pi => pi.LuaDocString());
|
||||
return $"{methodInfo.ReturnType.LuaDocString()} {mi.Name}({parameters.JoinWith(", ")})";
|
||||
}
|
||||
|
||||
var propertyInfo = mi as PropertyInfo;
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
var types = new List<string>();
|
||||
if (propertyInfo.GetGetMethod() != null)
|
||||
types.Add("get;");
|
||||
if (propertyInfo.GetSetMethod() != null)
|
||||
types.Add("set;");
|
||||
|
||||
return $"{propertyInfo.PropertyType.LuaDocString()} {mi.Name} {{ {types.JoinWith(" ")} }}";
|
||||
}
|
||||
|
||||
return $"Unknown field: {mi.Name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
157
OpenRA.Game/Scripting/ScriptMemberWrapper.cs
Normal file
157
OpenRA.Game/Scripting/ScriptMemberWrapper.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
#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 Eluant;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Scripting
|
||||
{
|
||||
public class ScriptMemberWrapper
|
||||
{
|
||||
readonly ScriptContext context;
|
||||
public readonly object Target;
|
||||
public readonly MemberInfo Member;
|
||||
|
||||
public readonly bool IsMethod;
|
||||
public readonly bool IsGetProperty;
|
||||
public readonly bool IsSetProperty;
|
||||
|
||||
public ScriptMemberWrapper(ScriptContext context, object target, MemberInfo mi)
|
||||
{
|
||||
this.context = context;
|
||||
Target = target;
|
||||
Member = mi;
|
||||
|
||||
var property = mi as PropertyInfo;
|
||||
if (property != null)
|
||||
{
|
||||
IsGetProperty = property.GetGetMethod() != null;
|
||||
IsSetProperty = property.GetSetMethod() != null;
|
||||
}
|
||||
else
|
||||
IsMethod = true;
|
||||
}
|
||||
|
||||
LuaValue Invoke(LuaVararg args)
|
||||
{
|
||||
object[] clrArgs = null;
|
||||
try
|
||||
{
|
||||
if (!IsMethod)
|
||||
throw new LuaException($"Trying to invoke a {nameof(ScriptMemberWrapper)} that isn't a method!");
|
||||
|
||||
var mi = (MethodInfo)Member;
|
||||
var pi = mi.GetParameters();
|
||||
|
||||
clrArgs = new object[pi.Length];
|
||||
|
||||
var argCount = args.Count;
|
||||
for (var i = 0; i < pi.Length; i++)
|
||||
{
|
||||
if (i >= argCount)
|
||||
{
|
||||
if (!pi[i].IsOptional)
|
||||
throw new LuaException($"Argument '{pi[i].LuaDocString()}' of '{Member.LuaDocString()}' is not optional.");
|
||||
|
||||
clrArgs[i] = pi[i].DefaultValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!args[i].TryGetClrValue(pi[i].ParameterType, out clrArgs[i]))
|
||||
throw new LuaException($"Unable to convert parameter {i} to {pi[i].ParameterType.Name}");
|
||||
}
|
||||
|
||||
return mi.Invoke(Target, clrArgs).ToLuaValue(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up all the Lua arguments that were given to us.
|
||||
foreach (var arg in args)
|
||||
arg.Dispose();
|
||||
args.Dispose();
|
||||
|
||||
// If we created any arrays of LuaValues to pass around, we need to dispose those too.
|
||||
if (clrArgs != null)
|
||||
{
|
||||
foreach (var arg in clrArgs)
|
||||
{
|
||||
if (arg is not LuaValue[] table)
|
||||
continue;
|
||||
|
||||
foreach (var value in table)
|
||||
value.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LuaValue Get(LuaRuntime runtime)
|
||||
{
|
||||
if (IsMethod)
|
||||
return runtime.CreateFunctionFromDelegate(Invoke);
|
||||
|
||||
if (IsGetProperty)
|
||||
return ((PropertyInfo)Member).GetValue(Target, null).ToLuaValue(context);
|
||||
|
||||
throw new LuaException($"The property '{Member.Name}' is write-only");
|
||||
}
|
||||
|
||||
public void Set(LuaValue value)
|
||||
{
|
||||
if (IsSetProperty)
|
||||
{
|
||||
var pi = (PropertyInfo)Member;
|
||||
if (!value.TryGetClrValue(pi.PropertyType, out var clrValue))
|
||||
throw new LuaException($"Unable to convert '{value.WrappedClrType().Name}' to CLR type '{pi.PropertyType}'");
|
||||
|
||||
pi.SetValue(Target, clrValue, null);
|
||||
}
|
||||
else
|
||||
throw new LuaException($"The property '{Member.Name}' is read-only");
|
||||
}
|
||||
|
||||
public static IEnumerable<MemberInfo> WrappableMembers(Type t)
|
||||
{
|
||||
// Only expose defined public non-static methods that were explicitly declared by the author
|
||||
const BindingFlags Flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
foreach (var mi in t.GetMembers(Flags))
|
||||
{
|
||||
// Properties are always wrappable
|
||||
if (mi is PropertyInfo)
|
||||
yield return mi;
|
||||
|
||||
// Methods are allowed if they aren't generic, and aren't generated by the compiler
|
||||
var method = mi as MethodInfo;
|
||||
if (method != null && !method.IsGenericMethodDefinition && !method.IsSpecialName)
|
||||
yield return mi;
|
||||
|
||||
// Fields aren't allowed
|
||||
}
|
||||
}
|
||||
|
||||
public static string[] RequiredTraitNames(Type t)
|
||||
{
|
||||
// Returns the inner types of all the Requires<T> interfaces on this type
|
||||
var types = t.GetInterfaces()
|
||||
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>));
|
||||
|
||||
// Remove the namespace and the trailing "Info"
|
||||
return types.SelectMany(i => i.GetGenericArguments())
|
||||
.Select(g => g.Name.Split('.', StringSplitOptions.RemoveEmptyEntries).LastOrDefault())
|
||||
.Select(s => s.EndsWith("Info", StringComparison.Ordinal) ? s[..^4] : s)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
95
OpenRA.Game/Scripting/ScriptObjectWrapper.cs
Normal file
95
OpenRA.Game/Scripting/ScriptObjectWrapper.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
#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 Eluant;
|
||||
using Eluant.ObjectBinding;
|
||||
|
||||
namespace OpenRA.Scripting
|
||||
{
|
||||
public abstract class ScriptObjectWrapper : IScriptBindable, ILuaTableBinding
|
||||
{
|
||||
protected abstract string DuplicateKeyError(string memberName);
|
||||
protected abstract string MemberNotFoundError(string memberName);
|
||||
|
||||
protected readonly ScriptContext Context;
|
||||
readonly Dictionary<string, ScriptMemberWrapper> members = [];
|
||||
|
||||
protected ScriptObjectWrapper(ScriptContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
protected static object[] CreateObjects(Type[] types, object[] constructorArgs)
|
||||
{
|
||||
var i = 0;
|
||||
var argTypes = new Type[constructorArgs.Length];
|
||||
foreach (var ca in constructorArgs)
|
||||
argTypes[i++] = ca.GetType();
|
||||
|
||||
var objects = new object[types.Length];
|
||||
i = 0;
|
||||
foreach (var type in types)
|
||||
objects[i++] = type.GetConstructor(argTypes).Invoke(constructorArgs);
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
protected void Bind(object[] clrObjects)
|
||||
{
|
||||
members.Clear();
|
||||
|
||||
foreach (var obj in clrObjects)
|
||||
{
|
||||
var wrappable = ScriptMemberWrapper.WrappableMembers(obj.GetType());
|
||||
foreach (var m in wrappable)
|
||||
{
|
||||
if (members.ContainsKey(m.Name))
|
||||
throw new LuaException(DuplicateKeyError(m.Name));
|
||||
|
||||
members.Add(m.Name, new ScriptMemberWrapper(Context, obj, m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void Unbind(Type targetType)
|
||||
{
|
||||
// NOTE: In newer versions of .NET modifying the collection by calling Remove while iterating over it is valid
|
||||
foreach (var m in members)
|
||||
if (targetType == m.Value.Target.GetType())
|
||||
members.Remove(m.Key);
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key) { return members.ContainsKey(key); }
|
||||
|
||||
public LuaValue this[LuaRuntime runtime, LuaValue keyValue]
|
||||
{
|
||||
get
|
||||
{
|
||||
var name = keyValue.ToString();
|
||||
if (!members.TryGetValue(name, out var wrapper))
|
||||
throw new LuaException(MemberNotFoundError(name));
|
||||
|
||||
return wrapper.Get(runtime);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
var name = keyValue.ToString();
|
||||
if (!members.TryGetValue(name, out var wrapper))
|
||||
throw new LuaException(MemberNotFoundError(name));
|
||||
|
||||
wrapper.Set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
OpenRA.Game/Scripting/ScriptPlayerInterface.cs
Normal file
30
OpenRA.Game/Scripting/ScriptPlayerInterface.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
#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
|
||||
|
||||
namespace OpenRA.Scripting
|
||||
{
|
||||
public class ScriptPlayerInterface : ScriptObjectWrapper
|
||||
{
|
||||
readonly Player player;
|
||||
|
||||
protected override string DuplicateKeyError(string memberName) =>
|
||||
$"Player '{player.ResolvedPlayerName}' defines the command '{memberName}' on multiple traits";
|
||||
protected override string MemberNotFoundError(string memberName) =>
|
||||
$"Player '{player.ResolvedPlayerName}' does not define a property '{memberName}'";
|
||||
|
||||
public ScriptPlayerInterface(ScriptContext context, Player player)
|
||||
: base(context)
|
||||
{
|
||||
this.player = player;
|
||||
Bind(CreateObjects(context.PlayerCommands, [context, player]));
|
||||
}
|
||||
}
|
||||
}
|
||||
192
OpenRA.Game/Scripting/ScriptTypes.cs
Normal file
192
OpenRA.Game/Scripting/ScriptTypes.cs
Normal 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 Eluant;
|
||||
|
||||
namespace OpenRA.Scripting
|
||||
{
|
||||
public static class LuaValueExts
|
||||
{
|
||||
public static Type WrappedClrType(this LuaValue value)
|
||||
{
|
||||
if (value.TryGetClrObject(out var inner))
|
||||
return inner.GetType();
|
||||
|
||||
return value.GetType();
|
||||
}
|
||||
|
||||
public static bool TryGetClrValue<T>(this LuaValue value, out T clrObject)
|
||||
{
|
||||
var ret = value.TryGetClrValue(typeof(T), out var temp);
|
||||
clrObject = ret ? (T)temp : default;
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static bool TryGetClrValue(this LuaValue value, Type t, out object clrObject)
|
||||
{
|
||||
// Is t a nullable?
|
||||
// If yes, get the underlying type
|
||||
var nullable = Nullable.GetUnderlyingType(t);
|
||||
if (nullable != null)
|
||||
t = nullable;
|
||||
|
||||
// Value wraps a CLR object
|
||||
if (value.TryGetClrObject(out var temp) && temp.GetType() == t)
|
||||
{
|
||||
clrObject = temp;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is LuaNil && !t.IsValueType)
|
||||
{
|
||||
clrObject = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is LuaBoolean && t.IsAssignableFrom(typeof(bool)))
|
||||
{
|
||||
clrObject = value.ToBoolean();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is LuaNumber)
|
||||
{
|
||||
if (t.IsAssignableFrom(typeof(double)))
|
||||
{
|
||||
clrObject = value.ToNumber().Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Need an explicit test for double -> int
|
||||
// TODO: Lua 5.3 will introduce an integer type, so this will be able to go away
|
||||
if (t.IsAssignableFrom(typeof(int)))
|
||||
{
|
||||
clrObject = (int)value.ToNumber().Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (t.IsAssignableFrom(typeof(short)))
|
||||
{
|
||||
clrObject = (short)value.ToNumber().Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (t.IsAssignableFrom(typeof(byte)))
|
||||
{
|
||||
clrObject = (byte)value.ToNumber().Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (value is LuaString && t.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
clrObject = value.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is LuaFunction && t.IsAssignableFrom(typeof(LuaFunction)))
|
||||
{
|
||||
clrObject = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is LuaTable && t.IsAssignableFrom(typeof(LuaTable)))
|
||||
{
|
||||
clrObject = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Translate LuaTable<int, object> -> object[]
|
||||
if (value is LuaTable table && t.IsArray)
|
||||
{
|
||||
var innerType = t.GetElementType();
|
||||
var array = Array.CreateInstance(innerType, table.Count);
|
||||
var i = 0;
|
||||
|
||||
foreach (var kv in table)
|
||||
{
|
||||
using (kv.Key)
|
||||
{
|
||||
object element;
|
||||
if (innerType == typeof(LuaValue))
|
||||
element = kv.Value;
|
||||
else
|
||||
{
|
||||
var elementHasClrValue = kv.Value.TryGetClrValue(innerType, out element);
|
||||
if (!elementHasClrValue || element is not LuaValue)
|
||||
kv.Value.Dispose();
|
||||
if (!elementHasClrValue)
|
||||
throw new LuaException($"Unable to convert table value of type {kv.Value.WrappedClrType()} to type {innerType}");
|
||||
}
|
||||
|
||||
array.SetValue(element, i++);
|
||||
}
|
||||
}
|
||||
|
||||
clrObject = array;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Value isn't of the requested type.
|
||||
// Set a default output value and return false
|
||||
// Value types are assumed to specify a default constructor
|
||||
clrObject = t.IsValueType ? Activator.CreateInstance(t) : null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static LuaValue ToLuaValue(this object obj, ScriptContext context)
|
||||
{
|
||||
{
|
||||
if (obj is LuaValue v)
|
||||
return v;
|
||||
|
||||
if (obj == null)
|
||||
return LuaNil.Instance;
|
||||
|
||||
if (obj is double d)
|
||||
return d;
|
||||
|
||||
if (obj is int i)
|
||||
return i;
|
||||
|
||||
if (obj is bool b)
|
||||
return b;
|
||||
|
||||
if (obj is string s)
|
||||
return s;
|
||||
}
|
||||
|
||||
if (obj is IScriptBindable)
|
||||
{
|
||||
// Object needs additional notification / context
|
||||
var notify = obj as IScriptNotifyBind;
|
||||
notify?.OnScriptBind(context);
|
||||
|
||||
return new LuaCustomClrObject(obj);
|
||||
}
|
||||
|
||||
if (obj is Array array)
|
||||
{
|
||||
var i = 1;
|
||||
var table = context.CreateTable();
|
||||
|
||||
foreach (var x in array)
|
||||
using (LuaValue key = i++, value = x.ToLuaValue(context))
|
||||
table.Add(key, value);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Cannot convert type '{obj.GetType()}' to Lua. Class must implement IScriptBindable.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user