From d73af0190f5801678b65603653be89ea62caaaad Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Thu, 27 Mar 2014 22:40:17 +1300 Subject: [PATCH] Add a new native-lua implementation. --- Makefile | 4 +- OpenRA.Editor/OpenRA.Editor.csproj | 3 + OpenRA.Game/Actor.cs | 40 +- OpenRA.Game/CPos.cs | 58 ++- OpenRA.Game/CVec.cs | 63 ++- OpenRA.Game/OpenRA.Game.csproj | 13 + OpenRA.Game/Player.cs | 38 +- OpenRA.Game/Scripting/ScriptActorInterface.cs | 45 ++ OpenRA.Game/Scripting/ScriptContext.cs | 233 ++++++++++ OpenRA.Game/Scripting/ScriptMemberExts.cs | 74 ++++ OpenRA.Game/Scripting/ScriptMemberWrapper.cs | 127 ++++++ OpenRA.Game/Scripting/ScriptObjectWrapper.cs | 77 ++++ .../Scripting/ScriptPlayerInterface.cs | 45 ++ OpenRA.Game/Scripting/ScriptTypes.cs | 153 +++++++ OpenRA.Game/WPos.cs | 73 +++- OpenRA.Game/WVec.cs | 64 ++- OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj | 3 + OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj | 3 + OpenRA.Mods.RA/OpenRA.Mods.RA.csproj | 4 + OpenRA.Mods.RA/Scripting/LuaScript.cs | 46 ++ OpenRA.Mods.TS/OpenRA.Mods.TS.csproj | 3 + OpenRA.Utility/Command.cs | 155 +++++++ OpenRA.Utility/OpenRA.Utility.csproj | 3 + OpenRA.Utility/Program.cs | 1 + lua/sandbox.lua | 163 +++++++ lua/scriptwrapper.lua | 44 ++ lua/stacktraceplus.lua | 411 ++++++++++++++++++ thirdparty/Eluant.dll | Bin 0 -> 64000 bytes thirdparty/Eluant.dll.config | 4 + 29 files changed, 1929 insertions(+), 21 deletions(-) mode change 100755 => 100644 OpenRA.Game/Actor.cs create mode 100644 OpenRA.Game/Scripting/ScriptActorInterface.cs create mode 100644 OpenRA.Game/Scripting/ScriptContext.cs create mode 100644 OpenRA.Game/Scripting/ScriptMemberExts.cs create mode 100644 OpenRA.Game/Scripting/ScriptMemberWrapper.cs create mode 100644 OpenRA.Game/Scripting/ScriptObjectWrapper.cs create mode 100644 OpenRA.Game/Scripting/ScriptPlayerInterface.cs create mode 100644 OpenRA.Game/Scripting/ScriptTypes.cs create mode 100644 OpenRA.Mods.RA/Scripting/LuaScript.cs create mode 100644 lua/sandbox.lua create mode 100644 lua/scriptwrapper.lua create mode 100644 lua/stacktraceplus.lua create mode 100755 thirdparty/Eluant.dll create mode 100644 thirdparty/Eluant.dll.config diff --git a/Makefile b/Makefile index 7976931e3b..db3d43c0e5 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ CSC = dmcs CSFLAGS = -nologo -warn:4 -debug:full -optimize- -codepage:utf8 -unsafe -warnaserror DEFINE = DEBUG;TRACE -COMMON_LIBS = System.dll System.Core.dll System.Drawing.dll System.Xml.dll thirdparty/ICSharpCode.SharpZipLib.dll thirdparty/FuzzyLogicLibrary.dll thirdparty/Mono.Nat.dll thirdparty/MaxMind.Db.dll thirdparty/MaxMind.GeoIP2.dll +COMMON_LIBS = System.dll System.Core.dll System.Drawing.dll System.Xml.dll thirdparty/ICSharpCode.SharpZipLib.dll thirdparty/FuzzyLogicLibrary.dll thirdparty/Mono.Nat.dll thirdparty/MaxMind.Db.dll thirdparty/MaxMind.GeoIP2.dll thirdparty/Eluant.dll @@ -175,7 +175,7 @@ editor_SRCS := $(shell find OpenRA.Editor/ -iname '*.cs') editor_TARGET = OpenRA.Editor.exe editor_KIND = winexe editor_DEPS = $(game_TARGET) -editor_LIBS = System.Windows.Forms.dll System.Data.dll System.Drawing.dll $(editor_DEPS) +editor_LIBS = System.Windows.Forms.dll System.Data.dll System.Drawing.dll $(editor_DEPS) thirdparty/Eluant.dll editor_EXTRA = -resource:OpenRA.Editor.Form1.resources -resource:OpenRA.Editor.MapSelect.resources editor_FLAGS = -win32icon:OpenRA.Editor/OpenRA.Editor.Icon.ico diff --git a/OpenRA.Editor/OpenRA.Editor.csproj b/OpenRA.Editor/OpenRA.Editor.csproj index 0b0834efec..214c0ac113 100644 --- a/OpenRA.Editor/OpenRA.Editor.csproj +++ b/OpenRA.Editor/OpenRA.Editor.csproj @@ -71,6 +71,9 @@ + + ..\thirdparty\Eluant.dll + diff --git a/OpenRA.Game/Actor.cs b/OpenRA.Game/Actor.cs old mode 100755 new mode 100644 index 0aece8c9b4..bfaa3b4b62 --- a/OpenRA.Game/Actor.cs +++ b/OpenRA.Game/Actor.cs @@ -12,13 +12,16 @@ using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using Eluant; +using Eluant.ObjectBinding; using OpenRA.Graphics; using OpenRA.Primitives; +using OpenRA.Scripting; using OpenRA.Traits; namespace OpenRA { - public class Actor + public class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding { public readonly ActorInfo Info; @@ -240,5 +243,40 @@ namespace OpenRA health.Value.InflictDamage(this, attacker, health.Value.MaxHP, null, true); } + + #region Scripting interface + + Lazy luaInterface; + public void OnScriptBind(ScriptContext context) + { + luaInterface = Exts.Lazy(() => new ScriptActorInterface(context, this)); + } + + public LuaValue this[LuaRuntime runtime, LuaValue keyValue] + { + get { return luaInterface.Value[runtime, keyValue]; } + set { luaInterface.Value[runtime, keyValue] = value; } + } + + public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) + { + Actor a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + return false; + + return a == b; + } + + public LuaValue ToString(LuaRuntime runtime) + { + return "Actor ({0})".F(this); + } + + public bool HasScriptProperty(string name) + { + return luaInterface.Value.ContainsKey(name); + } + + #endregion } } diff --git a/OpenRA.Game/CPos.cs b/OpenRA.Game/CPos.cs index 2fef5e5dda..34f69f97a9 100644 --- a/OpenRA.Game/CPos.cs +++ b/OpenRA.Game/CPos.cs @@ -10,13 +10,13 @@ using System; using System.Drawing; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.Scripting; namespace OpenRA { - /// - /// Cell coordinate position in the world (coarse). - /// - public struct CPos + public struct CPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding { public readonly int X, Y; @@ -60,6 +60,56 @@ namespace OpenRA public override string ToString() { return "{0},{1}".F(X, Y); } + #region Scripting interface + + public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) + { + CPos a; + CVec b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + throw new LuaException("Attempted to call CPos.Add(CPos, CVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, right.WrappedClrType().Name)); + + return new LuaCustomClrObject(a + b); + } + + public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right) + { + CPos a; + CVec b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + throw new LuaException("Attempted to call CPos.Subtract(CPos, CVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, right.WrappedClrType().Name)); + + return new LuaCustomClrObject(a - b); + } + + public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) + { + CPos a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + return false; + + return a == b; + } + + public LuaValue this[LuaRuntime runtime, LuaValue key] + { + get + { + switch (key.ToString()) + { + case "X": return X; + case "Y": return Y; + default: throw new LuaException("CPos does not define a member '{0}'".F(key)); + } + } + + set + { + throw new LuaException("CPos is read-only. Use CPos.New to create a new value"); + } + } + + #endregion } public static class RectangleExtensions diff --git a/OpenRA.Game/CVec.cs b/OpenRA.Game/CVec.cs index a77ae4aa1d..8d3784e4b2 100644 --- a/OpenRA.Game/CVec.cs +++ b/OpenRA.Game/CVec.cs @@ -10,13 +10,13 @@ using System; using System.Drawing; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.Scripting; namespace OpenRA { - /// - /// Cell coordinate vector (coarse). - /// - public struct CVec + public struct CVec : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding { public readonly int X, Y; @@ -82,5 +82,60 @@ namespace OpenRA new CVec(1, 0), new CVec(1, 1), }; + + #region Scripting interface + + public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) + { + CVec a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + throw new LuaException("Attempted to call CVec.Add(CVec, CVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, right.WrappedClrType().Name)); + + return new LuaCustomClrObject(a + b); + } + + public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right) + { + CVec a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + throw new LuaException("Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, right.WrappedClrType().Name)); + + return new LuaCustomClrObject(a - b); + } + + public LuaValue Minus(LuaRuntime runtime) + { + return new LuaCustomClrObject(-this); + } + + public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) + { + CVec a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + return false; + + return a == b; + } + + public LuaValue this[LuaRuntime runtime, LuaValue key] + { + get + { + switch (key.ToString()) + { + case "X": return X; + case "Y": return Y; + case "Facing": return Traits.Util.GetFacing(this, 0); + default: throw new LuaException("CVec does not define a member '{0}'".F(key)); + } + } + + set + { + throw new LuaException("WVec is read-only. Use CVec.New to create a new value"); + } + } + + #endregion } } diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 2271cc2038..14e6702f1b 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -74,6 +74,9 @@ mono.nat ..\thirdparty\Mono.Nat.dll + + ..\thirdparty\Eluant.dll + False ..\thirdparty\Tao\Tao.OpenAl.dll @@ -236,6 +239,13 @@ + + + + + + + @@ -359,4 +369,7 @@ --> + + + diff --git a/OpenRA.Game/Player.cs b/OpenRA.Game/Player.cs index c12cdd487f..aac6af1f8c 100644 --- a/OpenRA.Game/Player.cs +++ b/OpenRA.Game/Player.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) * 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. For more information, @@ -8,12 +8,16 @@ */ #endregion +using System; using System.Collections.Generic; using System.Linq; +using Eluant; +using Eluant.ObjectBinding; using OpenRA.FileFormats; using OpenRA.Network; using OpenRA.Graphics; using OpenRA.Primitives; +using OpenRA.Scripting; using OpenRA.Traits; namespace OpenRA @@ -21,7 +25,7 @@ namespace OpenRA public enum PowerState { Normal, Low, Critical }; public enum WinState { Won, Lost, Undefined }; - public class Player + public class Player : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding { public Actor PlayerActor; public WinState WinState = WinState.Undefined; @@ -106,5 +110,35 @@ namespace OpenRA // Observers are considered as allies return p == null || Stances[p] == Stance.Ally; } + + #region Scripting interface + + Lazy luaInterface; + public void OnScriptBind(ScriptContext context) + { + luaInterface = Exts.Lazy(() => new ScriptPlayerInterface(context, this)); + } + + public LuaValue this[LuaRuntime runtime, LuaValue keyValue] + { + get { return luaInterface.Value[runtime, keyValue]; } + set { luaInterface.Value[runtime, keyValue] = value; } + } + + public LuaValue Equals (LuaRuntime runtime, LuaValue left, LuaValue right) + { + Player a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + return false; + + return a == b; + } + + public LuaValue ToString(LuaRuntime runtime) + { + return "Player ({0})".F(PlayerName); + } + + #endregion } } diff --git a/OpenRA.Game/Scripting/ScriptActorInterface.cs b/OpenRA.Game/Scripting/ScriptActorInterface.cs new file mode 100644 index 0000000000..8d77995c54 --- /dev/null +++ b/OpenRA.Game/Scripting/ScriptActorInterface.cs @@ -0,0 +1,45 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.FileFormats; +using OpenRA.Traits; + +namespace OpenRA.Scripting +{ + public class ScriptActorInterface : ScriptObjectWrapper + { + readonly Actor actor; + + protected override string DuplicateKeyError(string memberName) { return "Actor '{0}' defines the command '{1}' on multiple traits".F(actor.Info.Name, memberName); } + protected override string MemberNotFoundError(string memberName) { return "Actor '{0}' does not define a property '{1}'".F(actor.Info.Name, memberName); } + + public ScriptActorInterface(ScriptContext context, Actor actor) + : base(context) + { + this.actor = actor; + + var args = new [] { actor }; + var objects = context.ActorCommands[actor.Info].Select(cg => + { + var groupCtor = cg.GetConstructor(new Type[] { typeof(Actor) }); + return groupCtor.Invoke(args); + }); + + Bind(objects); + } + } +} diff --git a/OpenRA.Game/Scripting/ScriptContext.cs b/OpenRA.Game/Scripting/ScriptContext.cs new file mode 100644 index 0000000000..dc8c269476 --- /dev/null +++ b/OpenRA.Game/Scripting/ScriptContext.cs @@ -0,0 +1,233 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Eluant; +using OpenRA.FileSystem; +using OpenRA.Graphics; +using OpenRA.Primitives; +using OpenRA.Support; +using OpenRA.Scripting; +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 + public class ScriptPropertyGroupAttribute : Attribute + { + public readonly string Category; + public ScriptPropertyGroupAttribute(string category) { Category = category; } + } + + public class ScriptActorPropertyActivityAttribute : Attribute { } + + public abstract class ScriptActorProperties + { + protected readonly Actor self; + public ScriptActorProperties(Actor self) { this.self = self; } + } + + public abstract class ScriptPlayerProperties + { + protected readonly Player player; + public ScriptPlayerProperties(Player player) { this.player = player; } + } + + // For global-level bindings + public abstract class ScriptGlobal : ScriptObjectWrapper + { + protected override string DuplicateKeyError(string memberName) { return "Table '{0}' defines multiple members '{1}'".F(Name, memberName); } + protected override string MemberNotFoundError(string memberName) { return "Table '{0}' does not define a property '{1}'".F(Name, memberName); } + + public readonly string Name; + public ScriptGlobal(ScriptContext context) + : base(context) + { + // The 'this.' resolves the actual (subclass) type + var type = this.GetType(); + var names = type.GetCustomAttributes(true); + if (names.Count() != 1) + throw new InvalidOperationException("[LuaGlobal] attribute not found for global table '{0}'".F(type)); + + Name = names.First().Name; + Bind(new [] { this }); + } + } + + public class ScriptGlobalAttribute : Attribute + { + public readonly string Name; + public ScriptGlobalAttribute(string name) { Name = name; } + } + + public class ScriptContext + { + public World World { get; private set; } + public WorldRenderer WorldRenderer { get; private set; } + + readonly MemoryConstrainedLuaRuntime runtime; + readonly LuaFunction tick; + + // 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; + + readonly Type[] knownActorCommands; + public readonly Cache ActorCommands; + public readonly Type[] PlayerCommands; + + public ScriptContext(World world, WorldRenderer worldRenderer, + IEnumerable scripts) + { + runtime = new MemoryConstrainedLuaRuntime(); + + World = world; + WorldRenderer = worldRenderer; + knownActorCommands = Game.modData.ObjectCreator + .GetTypesImplementing() + .ToArray(); + + ActorCommands = new Cache(FilterActorCommands); + PlayerCommands = Game.modData.ObjectCreator + .GetTypesImplementing() + .ToArray(); + + runtime.DoBuffer(GlobalFileSystem.Open(Path.Combine("lua", "scriptwrapper.lua")).ReadAllText(), "scriptwrapper.lua").Dispose(); + tick = (LuaFunction)runtime.Globals["Tick"]; + + // Register globals + using (var fn = runtime.CreateFunctionFromDelegate((Action)FatalError)) + runtime.Globals["FatalError"] = fn; + + runtime.Globals["MaxUserScriptInstructions"] = MaxUserScriptInstructions; + + using (var registerGlobal = (LuaFunction)runtime.Globals["RegisterSandboxedGlobal"]) + { + using (var fn = runtime.CreateFunctionFromDelegate((Action)Console.WriteLine)) + registerGlobal.Call("print", fn).Dispose(); + + // Register global tables + var bindings = Game.modData.ObjectCreator.GetTypesImplementing(); + foreach (var b in bindings) + { + var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => + { + var p = c.GetParameters(); + return p.Length == 1 && p.First().ParameterType == typeof(ScriptContext); + }); + + if (ctor == null) + throw new InvalidOperationException("{0} must define a constructor that takes a ScriptContext context parameter".F(b.Name)); + + var binding = (ScriptGlobal)ctor.Invoke(new [] { this }); + using (var obj = binding.ToLuaValue(this)) + registerGlobal.Call(binding.Name, obj).Dispose(); + } + } + + // System functions do not count towards the memory limit + runtime.MaxMemoryUse = runtime.MemoryUse + MaxUserScriptMemory; + + using (var loadScript = (LuaFunction)runtime.Globals["ExecuteSandboxedScript"]) + { + foreach (var s in scripts) + loadScript.Call(s, GlobalFileSystem.Open(s).ReadAllText()).Dispose(); + } + } + + bool error; + public void FatalError(string message) + { + Console.WriteLine("Fatal Lua Error: {0}", message); + error = true; + } + + public void RegisterMapActor(string name, Actor a) + { + using (var registerGlobal = (LuaFunction)runtime.Globals["RegisterSandboxedGlobal"]) + { + if (runtime.Globals.ContainsKey(name)) + throw new LuaException("The global name '{0}' is reserved, and may not be used by a map actor".F(name)); + + using (var obj = a.ToLuaValue(this)) + registerGlobal.Call(name, obj).Dispose(); + } + } + + public void WorldLoaded() + { + if (error) + return; + + using (var worldLoaded = (LuaFunction)runtime.Globals["WorldLoaded"]) + worldLoaded.Call().Dispose(); + } + + public void Tick(Actor self) + { + if (error) + return; + + using (new PerfSample("tick_lua")) + tick.Call().Dispose(); + } + + public void Dispose() + { + if (runtime == null) + return; + + GC.SuppressFinalize(this); + runtime.Dispose(); + } + + ~ScriptContext() + { + if (runtime != null) + Game.RunAfterTick(Dispose); + } + + static Type[] ExtractRequiredTypes(Type t) + { + // Returns the inner types of all the Requires interfaces on this type + var outer = t.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>)); + + return outer.SelectMany(i => i.GetGenericArguments()).ToArray(); + } + + static readonly object[] NoArguments = new object[0]; + Type[] FilterActorCommands(ActorInfo ai) + { + var method = typeof(TypeDictionary).GetMethod("Contains"); + return knownActorCommands.Where(c => ExtractRequiredTypes(c) + .All(t => (bool)method.MakeGenericMethod(t).Invoke(ai.Traits, NoArguments))) + .ToArray(); + } + + public LuaTable CreateTable() { return runtime.CreateTable(); } + } +} diff --git a/OpenRA.Game/Scripting/ScriptMemberExts.cs b/OpenRA.Game/Scripting/ScriptMemberExts.cs new file mode 100644 index 0000000000..bec20b597a --- /dev/null +++ b/OpenRA.Game/Scripting/ScriptMemberExts.cs @@ -0,0 +1,74 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.FileFormats; +using OpenRA.Traits; + +namespace OpenRA.Scripting +{ + public static class ScriptMemberExts + { + static readonly Dictionary LuaTypeNameReplacements = new Dictionary() + { + { "Void", "void" }, + { "Int32", "int" }, + { "String", "string" }, + { "Boolean", "bool" } + }; + + public static string LuaDocString(this Type t) + { + string ret; + if (!LuaTypeNameReplacements.TryGetValue(t.Name, out ret)) + ret = t.Name; + return ret; + } + + public static string LuaDocString(this ParameterInfo pi) + { + var ret = "{0} {1}".F(pi.ParameterType.LuaDocString(), pi.Name); + if (pi.IsOptional) + ret += " = {0}".F(pi.DefaultValue); + + return ret; + } + + public static string LuaDocString(this MemberInfo mi) + { + if (mi is MethodInfo) + { + var methodInfo = mi as MethodInfo; + var parameters = methodInfo.GetParameters().Select(pi => pi.LuaDocString()); + return "{0} {1}({2})".F(methodInfo.ReturnType.LuaDocString(), mi.Name, parameters.JoinWith(", ")); + } + + if (mi is PropertyInfo) + { + var pi = mi as PropertyInfo; + var types = new List(); + if (pi.GetGetMethod() != null) + types.Add("get;"); + if (pi.GetSetMethod() != null) + types.Add("set;"); + + return "{0} {1} {{ {2} }}".F(pi.PropertyType.LuaDocString(), mi.Name, types.JoinWith(" ")); + } + + return "Unknown field: {0}".F(mi.Name); + } + } +} diff --git a/OpenRA.Game/Scripting/ScriptMemberWrapper.cs b/OpenRA.Game/Scripting/ScriptMemberWrapper.cs new file mode 100644 index 0000000000..fa6bf7491a --- /dev/null +++ b/OpenRA.Game/Scripting/ScriptMemberWrapper.cs @@ -0,0 +1,127 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.FileFormats; +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) + { + if (!IsMethod) + throw new LuaException("Trying to invoke a ScriptMemberWrapper that isn't a method!"); + + var mi = Member as MethodInfo; + var pi = mi.GetParameters(); + + object[] 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 '{0}' of '{1}' is not optional.".F(pi[i].LuaDocString(), Member.LuaDocString())); + + clrArgs[i] = pi[i].DefaultValue; + continue; + } + + if (!args[i].TryGetClrValue(pi[i].ParameterType, out clrArgs[i])) + throw new LuaException("Unable to convert parameter {0} to {1}".F(i, pi[i].ParameterType.Name)); + } + + var ret = (Member as MethodInfo).Invoke(Target, clrArgs); + return ret.ToLuaValue(context); + } + + public LuaValue Get(LuaRuntime runtime) + { + if (IsMethod) + return runtime.CreateFunctionFromDelegate((Func)Invoke); + + if (IsGetProperty) + { + var pi = Member as PropertyInfo; + return pi.GetValue(Target, null).ToLuaValue(context); + } + + throw new LuaException("The property '{0}' is write-only".F(Member.Name)); + } + + public void Set(LuaRuntime runtime, LuaValue value) + { + if (IsSetProperty) + { + var pi = Member as PropertyInfo; + object clrValue; + if (!value.TryGetClrValue(pi.PropertyType, out clrValue)) + throw new LuaException("Unable to convert '{0}' to Clr type '{1}'".F(value.WrappedClrType().Name, pi.PropertyType)); + + pi.SetValue(Target, clrValue, null); + } + else + throw new LuaException("The property '{0}' is read-only".F(Member.Name)); + } + + public static IEnumerable WrappableMembers(Type t) + { + // Only expose defined public non-static methods that were explicitly declared by the author + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + return t.GetMembers(flags).Where(mi => + { + // Properties are always wrappable + if (mi is PropertyInfo) + return true; + + // 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) + return true; + + // Fields aren't allowed + return false; + }); + } + } +} diff --git a/OpenRA.Game/Scripting/ScriptObjectWrapper.cs b/OpenRA.Game/Scripting/ScriptObjectWrapper.cs new file mode 100644 index 0000000000..67edd1c70e --- /dev/null +++ b/OpenRA.Game/Scripting/ScriptObjectWrapper.cs @@ -0,0 +1,77 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.FileFormats; +using OpenRA.Traits; + +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; + Dictionary members; + + public ScriptObjectWrapper(ScriptContext context) + { + this.context = context; + } + + protected void Bind(IEnumerable clrObjects) + { + members = new Dictionary(); + 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)); + } + } + } + + public bool ContainsKey(string key) { return members.ContainsKey(key); } + + public LuaValue this[LuaRuntime runtime, LuaValue keyValue] + { + get + { + var name = keyValue.ToString(); + ScriptMemberWrapper wrapper; + if (!members.TryGetValue(name, out wrapper)) + throw new LuaException(MemberNotFoundError(name)); + + return wrapper.Get(runtime); + } + + set + { + var name = keyValue.ToString(); + ScriptMemberWrapper wrapper; + if (!members.TryGetValue(name, out wrapper)) + throw new LuaException(MemberNotFoundError(name)); + + wrapper.Set(runtime, value); + } + } + } +} diff --git a/OpenRA.Game/Scripting/ScriptPlayerInterface.cs b/OpenRA.Game/Scripting/ScriptPlayerInterface.cs new file mode 100644 index 0000000000..0d78e043fc --- /dev/null +++ b/OpenRA.Game/Scripting/ScriptPlayerInterface.cs @@ -0,0 +1,45 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.FileFormats; +using OpenRA.Traits; + +namespace OpenRA.Scripting +{ + public class ScriptPlayerInterface : ScriptObjectWrapper + { + readonly Player player; + + protected override string DuplicateKeyError(string memberName) { return "Player '{0}' defines the command '{1}' on multiple traits".F(player.PlayerName, memberName); } + protected override string MemberNotFoundError(string memberName) { return "Player '{0}' does not define a property '{1}'".F(player.PlayerName, memberName); } + + public ScriptPlayerInterface(ScriptContext context, Player player) + : base(context) + { + this.player = player; + + var args = new [] { player }; + var objects = context.PlayerCommands.Select(cg => + { + var groupCtor = cg.GetConstructor(new Type[] { typeof(Player) }); + return groupCtor.Invoke(args); + }); + + Bind(objects); + } + } +} diff --git a/OpenRA.Game/Scripting/ScriptTypes.cs b/OpenRA.Game/Scripting/ScriptTypes.cs new file mode 100644 index 0000000000..b892f3efd5 --- /dev/null +++ b/OpenRA.Game/Scripting/ScriptTypes.cs @@ -0,0 +1,153 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Eluant; +using OpenRA.FileSystem; +using OpenRA.Graphics; +using OpenRA.Primitives; +using OpenRA.Support; +using OpenRA.Scripting; +using OpenRA.Traits; + +namespace OpenRA.Scripting +{ + public static class LuaValueExts + { + public static Type WrappedClrType(this LuaValue value) + { + object inner; + if (value.TryGetClrObject(out inner)) + return inner.GetType(); + + return value.GetType(); + } + + public static bool TryGetClrValue(this LuaValue value, out T clrObject) + { + object temp; + var ret = value.TryGetClrValue(typeof(T), out temp); + clrObject = ret ? (T)temp : default(T); + return ret; + } + + public static bool TryGetClrValue(this LuaValue value, Type t, out object clrObject) + { + object temp; + + // Value wraps a CLR object + if (value.TryGetClrObject(out temp)) + { + if (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 && 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 (value is LuaNumber && t.IsAssignableFrom(typeof(int))) + { + clrObject = (int)(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; + } + + // 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) + return (LuaValue)obj; + + if (obj == null) + return LuaNil.Instance; + + if (obj is double) + return (LuaValue)(double)obj; + + if (obj is int) + return (LuaValue)(int)obj; + + if (obj is bool) + return (LuaValue)(bool)obj; + + if (obj is string) + return (LuaValue)(string)obj; + + if (obj is IScriptBindable) + { + // Object needs additional notification / context + var notify = obj as IScriptNotifyBind; + if (notify != null) + notify.OnScriptBind(context); + + return new LuaCustomClrObject(obj); + } + + throw new InvalidOperationException("Cannot convert type '{0}' to Lua. Class must implement IScriptBindable.".F(obj.GetType())); + } + + public static LuaTable ToLuaTable(this IEnumerable collection, ScriptContext context) + { + var i = 1; + var table = context.CreateTable(); + foreach (var x in collection) + table.Add(i++, x.ToLuaValue(context)); + return table; + } + } +} diff --git a/OpenRA.Game/WPos.cs b/OpenRA.Game/WPos.cs index dab241089b..c2e25ebcde 100644 --- a/OpenRA.Game/WPos.cs +++ b/OpenRA.Game/WPos.cs @@ -10,13 +10,13 @@ using System.Collections.Generic; using System.Linq; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.Scripting; namespace OpenRA { - /// - /// 3d World position - 1024 units = 1 cell. - /// - public struct WPos + public struct WPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding { public readonly int X, Y, Z; @@ -59,6 +59,71 @@ namespace OpenRA } public override string ToString() { return "{0},{1},{2}".F(X, Y, Z); } + + #region Scripting interface + + public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) + { + WPos a; + WVec b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + throw new LuaException("Attempted to call WPos.Add(WPos, WVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, right.WrappedClrType().Name)); + + return new LuaCustomClrObject(a + b); + } + + public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right) + { + WPos a; + var rightType = right.WrappedClrType(); + if (!left.TryGetClrValue(out a)) + throw new LuaException("Attempted to call WPos.Subtract(WPos, WVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, rightType)); + + if (rightType == typeof(WPos)) + { + WPos b; + right.TryGetClrValue(out b); + return new LuaCustomClrObject(a - b); + } + else if (rightType == typeof(WVec)) + { + WVec b; + right.TryGetClrValue(out b); + return new LuaCustomClrObject(a - b); + } + + throw new LuaException("Attempted to call WPos.Subtract(WPos, WVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, rightType)); + } + + public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) + { + WPos a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + return false; + + return a == b; + } + + public LuaValue this[LuaRuntime runtime, LuaValue key] + { + get + { + switch (key.ToString()) + { + case "X": return X; + case "Y": return Y; + case "Z": return Z; + default: throw new LuaException("WPos does not define a member '{0}'".F(key)); + } + } + + set + { + throw new LuaException("WPos is read-only. Use WPos.New to create a new value"); + } + } + + #endregion } public static class IEnumerableExtensions diff --git a/OpenRA.Game/WVec.cs b/OpenRA.Game/WVec.cs index 4efee912a8..20a7e6c69f 100644 --- a/OpenRA.Game/WVec.cs +++ b/OpenRA.Game/WVec.cs @@ -9,13 +9,13 @@ #endregion using System; +using Eluant; +using Eluant.ObjectBinding; +using OpenRA.Scripting; namespace OpenRA { - /// - /// 3d World vector for describing offsets and distances - 1024 units = 1 cell. - /// - public struct WVec + public struct WVec : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding { public readonly int X, Y, Z; @@ -87,5 +87,61 @@ namespace OpenRA } public override string ToString() { return "{0},{1},{2}".F(X, Y, Z); } + + #region Scripting interface + + public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) + { + WVec a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + throw new LuaException("Attempted to call WVec.Add(WVec, WVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, right.WrappedClrType().Name)); + + return new LuaCustomClrObject(a + b); + } + + public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right) + { + WVec a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + throw new LuaException("Attempted to call WVec.Subtract(WVec, WVec) with invalid arguments ({0}, {1})".F(left.WrappedClrType().Name, right.WrappedClrType().Name)); + + return new LuaCustomClrObject(a - b); + } + + public LuaValue Minus(LuaRuntime runtime) + { + return new LuaCustomClrObject(-this); + } + + public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) + { + WVec a, b; + if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) + return false; + + return a == b; + } + + public LuaValue this[LuaRuntime runtime, LuaValue key] + { + get + { + switch (key.ToString()) + { + case "X": return X; + case "Y": return Y; + case "Z": return Z; + case "Facing": return Traits.Util.GetFacing(this, 0); + default: throw new LuaException("WVec does not define a member '{0}'".F(key)); + } + } + + set + { + throw new LuaException("WVec is read-only. Use WVec.New to create a new value"); + } + } + + #endregion } } diff --git a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj index 53418d30cd..4b16f796a0 100644 --- a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj +++ b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj @@ -69,6 +69,9 @@ + + ..\thirdparty\Eluant.dll + diff --git a/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj b/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj index 1cd3725cac..48f37fc793 100644 --- a/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj +++ b/OpenRA.Mods.D2k/OpenRA.Mods.D2k.csproj @@ -73,6 +73,9 @@ + + ..\thirdparty\Eluant.dll + diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 97ac3bedb6..c213a501b9 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -85,6 +85,9 @@ ..\thirdparty\MaxMind.GeoIP2.dll + + ..\thirdparty\Eluant.dll + @@ -498,6 +501,7 @@ + diff --git a/OpenRA.Mods.RA/Scripting/LuaScript.cs b/OpenRA.Mods.RA/Scripting/LuaScript.cs new file mode 100644 index 0000000000..0e27b3b445 --- /dev/null +++ b/OpenRA.Mods.RA/Scripting/LuaScript.cs @@ -0,0 +1,46 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using OpenRA.Graphics; +using OpenRA.Scripting; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA.Scripting +{ + public class LuaScriptInfo : ITraitInfo, Requires + { + public readonly string[] Scripts = { }; + + public object Create(ActorInitializer init) { return new LuaScript(this); } + } + + public class LuaScript : ITick, IWorldLoaded + { + readonly LuaScriptInfo info; + ScriptContext context; + + public LuaScript(LuaScriptInfo info) + { + this.info = info; + } + + public void WorldLoaded(World world, WorldRenderer worldRenderer) + { + var scripts = info.Scripts ?? new string[0]; + context = new ScriptContext(world, worldRenderer, scripts); + context.WorldLoaded(); + } + + public void Tick(Actor self) + { + context.Tick(self); + } + } +} diff --git a/OpenRA.Mods.TS/OpenRA.Mods.TS.csproj b/OpenRA.Mods.TS/OpenRA.Mods.TS.csproj index c58811f794..52f8d20d0e 100644 --- a/OpenRA.Mods.TS/OpenRA.Mods.TS.csproj +++ b/OpenRA.Mods.TS/OpenRA.Mods.TS.csproj @@ -36,6 +36,9 @@ + + ..\thirdparty\Eluant.dll + diff --git a/OpenRA.Utility/Command.cs b/OpenRA.Utility/Command.cs index 94bb043c42..ff9aee024f 100644 --- a/OpenRA.Utility/Command.cs +++ b/OpenRA.Utility/Command.cs @@ -17,10 +17,14 @@ using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; +using Eluant; +using Eluant.ObjectBinding; using OpenRA.FileFormats; using OpenRA.FileSystem; using OpenRA.GameRules; using OpenRA.Graphics; +using OpenRA.Primitives; +using OpenRA.Scripting; using OpenRA.Traits; namespace OpenRA.Utility @@ -340,6 +344,157 @@ namespace OpenRA.Utility Console.Write(doc.ToString()); } + static string[] RequiredTraitNames(Type t) + { + // Returns the inner types of all the Requires interfaces on this type + var outer = t.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>)); + + // Get the inner types + var inner = outer.SelectMany(i => i.GetGenericArguments()).ToArray(); + + // Remove the namespace and the trailing "Info" + return inner.Select(i => i.Name.Split(new [] { '.' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()) + .Select(s => s.EndsWith("Info") ? s.Remove(s.Length - 4, 4) : s) + .ToArray(); + } + + [Desc("MOD", "Generate Lua API documentation in MarkDown format.")] + public static void ExtractLuaDocs(string[] args) + { + Game.modData = new ModData(args[1]); + Rules.LoadRules(Game.modData.Manifest, new Map()); + + Console.WriteLine("This is an automatically generated lising of the new Lua map scripting API, generated for {0} of OpenRA.", Game.modData.Manifest.Mod.Version); + Console.WriteLine(); + Console.WriteLine("OpenRA allows custom maps and missions to be scripted using Lua 5.1.\n" + + "These scripts run in a sandbox that prevents access to unsafe functions (e.g. OS or file access), " + + "and limits the memory and CPU usage of the scripts."); + Console.WriteLine(); + Console.WriteLine("You can access this interface by adding the [LuaScript](Traits#luascript) trait to the world actor in your map rules (note, you must replace the spaces in the snippet below with a single tab for each level of indentation):"); + Console.WriteLine("```\nRules:\n\tWorld:\n\t\tLuaScript:\n\t\t\tScripts: myscript.lua\n```"); + Console.WriteLine(); + Console.WriteLine("Map scripts can interact with the game engine in three ways:\n" + + "* Global tables provide functions for interacting with the global world state, or performing general helper tasks.\n" + + "They exist in the global namespace, and can be called directly using ```.```.\n" + + "* Individual actors expose a collection of properties and commands that query information of modify their state.\n" + + " * Some commands, marked as queued activity, are asynchronous. Activities are queued on the actor, and will run in " + + "sequence until the queue is empty or the Stop command is called. Actors that are not performing an activity are Idle " + + "(actor.IsIdle will return true). The properties and commands available on each actor depends on the traits that the actor " + + "specifies in its rule definitions.\n" + + "* Individual players explose a collection of properties and commands that query information of modify their state.\n" + + "The properties and commands available on each actor depends on the traits that the actor specifies in its rule definitions.\n"); + Console.WriteLine(); + + var tables = Game.modData.ObjectCreator.GetTypesImplementing() + .OrderBy(t => t.Name); + + Console.WriteLine("

Global Tables

"); + + foreach (var t in tables) + { + var name = t.GetCustomAttributes(true).First().Name; + var members = ScriptMemberWrapper.WrappableMembers(t); + + Console.WriteLine("
", name); + foreach (var m in members.OrderBy(m => m.Name)) + { + string desc = m.HasAttribute() ? m.GetCustomAttributes(true).First().Lines.JoinWith("\n") : ""; + Console.WriteLine("".F(m.LuaDocString(), desc)); + } + Console.WriteLine("
{0}
{0}{1}
"); + } + + Console.WriteLine("

Actor Properties / Commands

"); + + var actorCategories = Game.modData.ObjectCreator.GetTypesImplementing().SelectMany(cg => + { + var catAttr = cg.GetCustomAttributes(false).FirstOrDefault(); + var category = catAttr != null ? catAttr.Category : "Unsorted"; + + var required = RequiredTraitNames(cg); + return ScriptMemberWrapper.WrappableMembers(cg).Select(mi => Tuple.Create(category, mi, required)); + }).GroupBy(g => g.Item1).OrderBy(g => g.Key); + + foreach (var kv in actorCategories) + { + Console.WriteLine("", kv.Key); + + foreach (var property in kv.OrderBy(p => p.Item2.Name)) + { + var mi = property.Item2; + var required = property.Item3; + var hasDesc = mi.HasAttribute(); + var hasRequires = required.Any(); + var isActivity = mi.HasAttribute(); + + Console.WriteLine(""); + } + Console.WriteLine("
{0}
{0}", mi.LuaDocString()); + + if (isActivity) + Console.WriteLine("
Queued Activity"); + + Console.WriteLine("
"); + + if (hasDesc) + Console.WriteLine(mi.GetCustomAttributes(false).First().Lines.JoinWith("\n")); + + if (hasDesc && hasRequires) + Console.WriteLine("
"); + + if (hasRequires) + Console.WriteLine("Requires {1}: {0}".F(required.JoinWith(", "), required.Length == 1 ? "Trait" : "Traits")); + + Console.WriteLine("
"); + } + + Console.WriteLine("

Player Properties / Commands

"); + + var playerCategories = Game.modData.ObjectCreator.GetTypesImplementing().SelectMany(cg => + { + var catAttr = cg.GetCustomAttributes(false).FirstOrDefault(); + var category = catAttr != null ? catAttr.Category : "Unsorted"; + + var required = RequiredTraitNames(cg); + return ScriptMemberWrapper.WrappableMembers(cg).Select(mi => Tuple.Create(category, mi, required)); + }).GroupBy(g => g.Item1).OrderBy(g => g.Key); + + foreach (var kv in playerCategories) + { + Console.WriteLine("", kv.Key); + + foreach (var property in kv.OrderBy(p => p.Item2.Name)) + { + var mi = property.Item2; + var required = property.Item3; + var hasDesc = mi.HasAttribute(); + var hasRequires = required.Any(); + var isActivity = mi.HasAttribute(); + + Console.WriteLine(""); + } + + Console.WriteLine("
{0}
{0}", mi.LuaDocString()); + + if (isActivity) + Console.WriteLine("
Queued Activity"); + + Console.WriteLine("
"); + + if (hasDesc) + Console.WriteLine(mi.GetCustomAttributes(false).First().Lines.JoinWith("\n")); + + if (hasDesc && hasRequires) + Console.WriteLine("
"); + + if (hasRequires) + Console.WriteLine("Requires {1}: {0}".F(required.JoinWith(", "), required.Length == 1 ? "Trait" : "Traits")); + + Console.WriteLine("
"); + } + } + [Desc("MAPFILE", "Generate hash of specified oramap file.")] public static void GetMapHash(string[] args) { diff --git a/OpenRA.Utility/OpenRA.Utility.csproj b/OpenRA.Utility/OpenRA.Utility.csproj index 574554d0ca..5a269debca 100644 --- a/OpenRA.Utility/OpenRA.Utility.csproj +++ b/OpenRA.Utility/OpenRA.Utility.csproj @@ -71,6 +71,9 @@ False ..\thirdparty\ICSharpCode.SharpZipLib.dll
+ + ..\thirdparty\Eluant.dll + diff --git a/OpenRA.Utility/Program.cs b/OpenRA.Utility/Program.cs index bdc1845261..fb7f078eed 100644 --- a/OpenRA.Utility/Program.cs +++ b/OpenRA.Utility/Program.cs @@ -27,6 +27,7 @@ namespace OpenRA.Utility { "--remap", Command.RemapShp }, { "--transpose", Command.TransposeShp }, { "--docs", Command.ExtractTraitDocs }, + { "--lua-docs", Command.ExtractLuaDocs }, { "--map-hash", Command.GetMapHash }, { "--map-preview", Command.GenerateMinimap }, { "--map-upgrade-v5", Command.UpgradeV5Map }, diff --git a/lua/sandbox.lua b/lua/sandbox.lua new file mode 100644 index 0000000000..89374e9335 --- /dev/null +++ b/lua/sandbox.lua @@ -0,0 +1,163 @@ +local sandbox = { + _VERSION = "sandbox 0.5", + _DESCRIPTION = "A pure-lua solution for running untrusted Lua code.", + _URL = "https://github.com/kikito/sandbox.lua", + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique García Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] +} + +-- The base environment is merged with the given env option (or an empty table, if no env provided) +-- +local BASE_ENV = {} + +-- List of non-safe packages/functions: +-- +-- * string.rep: can be used to allocate millions of bytes in 1 operation +-- * {set|get}metatable: can be used to modify the metatable of global objects (strings, integers) +-- * collectgarbage: can affect performance of other systems +-- * dofile: can access the server filesystem +-- * _G: It has access to everything. It can be mocked to other things though. +-- * load{file|string}: All unsafe because they can grant acces to global env +-- * raw{get|set|equal}: Potentially unsafe +-- * module|require|module: Can modify the host settings +-- * string.dump: Can display confidential server info (implementation of functions) +-- * string.rep: Can allocate millions of bytes in one go +-- * math.randomseed: Can affect the host sytem +-- * io.*, os.*: Most stuff there is non-save + + +-- Safe packages/functions below +([[ + +_VERSION assert error ipairs next pairs +pcall select tonumber tostring type unpack xpcall + +coroutine.create coroutine.resume coroutine.running coroutine.status +coroutine.wrap coroutine.yield + +math.abs math.acos math.asin math.atan math.atan2 math.ceil +math.cos math.cosh math.deg math.exp math.fmod math.floor +math.frexp math.huge math.ldexp math.log math.log10 math.max +math.min math.modf math.pi math.pow math.rad +math.sin math.sinh math.sqrt math.tan math.tanh + +os.clock os.difftime os.time + +string.byte string.char string.find string.format string.gmatch +string.gsub string.len string.lower string.match string.reverse +string.sub string.upper + +table.insert table.maxn table.remove table.sort + +]]):gsub('%S+', function(id) + local module, method = id:match('([^%.]+)%.([^%.]+)') + if module then + BASE_ENV[module] = BASE_ENV[module] or {} + BASE_ENV[module][method] = _G[module][method] + else + BASE_ENV[id] = _G[id] + end +end) + +local function protect_module(module, module_name) + return setmetatable({}, { + __index = module, + __newindex = function(_, attr_name, _) + error('Can not modify ' .. module_name .. '.' .. attr_name .. '. Protected by the sandbox.') + end + }) +end + +('coroutine math os string table'):gsub('%S+', function(module_name) + BASE_ENV[module_name] = protect_module(BASE_ENV[module_name], module_name) +end) + +-- auxiliary functions/variables + +local string_rep = string.rep + +local function merge(dest, source) + for k,v in pairs(source) do + dest[k] = dest[k] or v + end + return dest +end + +local function sethook(f, key, quota) + if type(debug) ~= 'table' or type(debug.sethook) ~= 'function' then return end + debug.sethook(f, key, quota) +end + +local function cleanup() + sethook() + string.rep = string_rep +end + +-- Public interface: sandbox.protect +function sandbox.protect(f, options) + if type(f) == 'string' then f = assert(loadstring(f)) end + + options = options or {} + + local quota = false + if options.quota ~= false then + quota = options.quota or 500000 + end + + local env = merge(options.env or {}, BASE_ENV) + env._G = env._G or env + + setfenv(f, env) + + return function(...) + + if quota then + local timeout = function() + cleanup() + error('Quota exceeded: ' .. tostring(quota)) + end + sethook(timeout, "", quota) + end + + string.rep = nil + + local ok, result = pcall(f, ...) + + cleanup() + + if not ok then error(result) end + return result + end +end + +-- Public interface: sandbox.run +function sandbox.run(f, options, ...) + return sandbox.protect(f, options)(...) +end + +-- make sandbox(f) == sandbox.protect(f) +setmetatable(sandbox, {__call = function(_,f,o) return sandbox.protect(f,o) end}) + +return sandbox diff --git a/lua/scriptwrapper.lua b/lua/scriptwrapper.lua new file mode 100644 index 0000000000..1603dd06c2 --- /dev/null +++ b/lua/scriptwrapper.lua @@ -0,0 +1,44 @@ +environment = {} + +-- Reset package path +package.path = "./lua/?.lua;./mods/common/lua/?.lua" + +-- Note: sandbox has been customized to remove math.random +local sandbox = require('sandbox') +local stp = require('stacktraceplus') + +local PrintStackTrace = function(msg) + return stp.stacktrace("", 2) .. "\nError message\n===============\n" .. msg .. "\n===============" +end + +local TryRunSandboxed = function(fn) + local success, err = xpcall(function() sandbox.run(fn, {env = environment, quota = MaxUserScriptInstructions}) end, PrintStackTrace) + if not success then + FatalError(err) + end +end + +WorldLoaded = function() + if environment.WorldLoaded ~= nil then + TryRunSandboxed(environment.WorldLoaded) + end +end + +Tick = function() + if environment.Tick ~= nil then + TryRunSandboxed(environment.Tick) + end +end + +ExecuteSandboxedScript = function(file, contents) + local script = loadstring(contents, file) + if (script == nil) then + FatalError("Error parsing " .. file) + else + TryRunSandboxed(script) + end +end + +RegisterSandboxedGlobal = function(key, value) + environment[key] = value +end \ No newline at end of file diff --git a/lua/stacktraceplus.lua b/lua/stacktraceplus.lua new file mode 100644 index 0000000000..8eba226946 --- /dev/null +++ b/lua/stacktraceplus.lua @@ -0,0 +1,411 @@ +-- tables +local _G = _G +local string, io, debug, coroutine = string, io, debug, coroutine + +-- functions +local tostring, print, require = tostring, print, require +local next, assert = next, assert +local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs +local error = error + +assert(debug, "debug table must be available at this point") + +local io_open = io.open +local string_gmatch = string.gmatch +local string_sub = string.sub +local table_concat = table.concat + +local _M = { + max_tb_output_len = 70 -- controls the maximum length of the 'stringified' table before cutting with ' (more...)' +} + +-- this tables should be weak so the elements in them won't become uncollectable +local m_known_tables = { [_G] = "_G (global table)" } +local function add_known_module(name, desc) + local ok, mod = pcall(require, name) + if ok then + m_known_tables[mod] = desc + end +end + +add_known_module("string", "string module") +add_known_module("io", "io module") +add_known_module("os", "os module") +add_known_module("table", "table module") +add_known_module("math", "math module") +add_known_module("package", "package module") +add_known_module("debug", "debug module") +add_known_module("coroutine", "coroutine module") + +-- lua5.2 +add_known_module("bit32", "bit32 module") +-- luajit +add_known_module("bit", "bit module") +add_known_module("jit", "jit module") + + +local m_user_known_tables = {} + +local m_known_functions = {} +for _, name in ipairs{ + -- Lua 5.2, 5.1 + "assert", + "collectgarbage", + "dofile", + "error", + "getmetatable", + "ipairs", + "load", + "loadfile", + "next", + "pairs", + "pcall", + "print", + "rawequal", + "rawget", + "rawlen", + "rawset", + "require", + "select", + "setmetatable", + "tonumber", + "tostring", + "type", + "xpcall", + + -- Lua 5.1 + "gcinfo", + "getfenv", + "loadstring", + "module", + "newproxy", + "setfenv", + "unpack", + -- TODO: add table.* etc functions +} do + if _G[name] then + m_known_functions[_G[name]] = name + end +end + + + +local m_user_known_functions = {} + +local function safe_tostring (value) + local ok, err = pcall(tostring, value) + if ok then return err else return (": '%s'"):format(err) end +end + +-- Private: +-- Parses a line, looking for possible function definitions (in a very naïve way) +-- Returns '(anonymous)' if no function name was found in the line +local function ParseLine(line) + assert(type(line) == "string") + --print(line) + local match = line:match("^%s*function%s+(%w+)") + if match then + --print("+++++++++++++function", match) + return match + end + match = line:match("^%s*local%s+function%s+(%w+)") + if match then + --print("++++++++++++local", match) + return match + end + match = line:match("^%s*local%s+(%w+)%s+=%s+function") + if match then + --print("++++++++++++local func", match) + return match + end + match = line:match("%s*function%s*%(") -- this is an anonymous function + if match then + --print("+++++++++++++function2", match) + return "(anonymous)" + end + return "(anonymous)" +end + +-- Private: +-- Tries to guess a function's name when the debug info structure does not have it. +-- It parses either the file or the string where the function is defined. +-- Returns '?' if the line where the function is defined is not found +local function GuessFunctionName(info) + --print("guessing function name") + if type(info.source) == "string" and info.source:sub(1,1) == "@" then + local file, err = io_open(info.source:sub(2), "r") + if not file then + print("file not found: "..tostring(err)) -- whoops! + return "?" + end + local line + for i = 1, info.linedefined do + line = file:read("*l") + end + if not line then + print("line not found") -- whoops! + return "?" + end + return ParseLine(line) + else + local line + local lineNumber = 0 + for l in string_gmatch(info.source, "([^\n]+)\n-") do + lineNumber = lineNumber + 1 + if lineNumber == info.linedefined then + line = l + break + end + end + if not line then + print("line not found") -- whoops! + return "?" + end + return ParseLine(line) + end +end + +--- +-- Dumper instances are used to analyze stacks and collect its information. +-- +local Dumper = {} + +Dumper.new = function(thread) + local t = { lines = {} } + for k,v in pairs(Dumper) do t[k] = v end + + t.dumping_same_thread = (thread == coroutine.running()) + + -- if a thread was supplied, bind it to debug.info and debug.get + -- we also need to skip this additional level we are introducing in the callstack (only if we are running + -- in the same thread we're inspecting) + if type(thread) == "thread" then + t.getinfo = function(level, what) + if t.dumping_same_thread and type(level) == "number" then + level = level + 1 + end + return debug.getinfo(thread, level, what) + end + t.getlocal = function(level, loc) + if t.dumping_same_thread then + level = level + 1 + end + return debug.getlocal(thread, level, loc) + end + else + t.getinfo = debug.getinfo + t.getlocal = debug.getlocal + end + + return t +end + +-- helpers for collecting strings to be used when assembling the final trace +function Dumper:add (text) + self.lines[#self.lines + 1] = text +end +function Dumper:add_f (fmt, ...) + self:add(fmt:format(...)) +end +function Dumper:concat_lines () + return table_concat(self.lines) +end + +--- +-- Private: +-- Iterates over the local variables of a given function. +-- +-- @param level The stack level where the function is. +-- +function Dumper:DumpLocals (level) + local prefix = "\t " + local i = 1 + + if self.dumping_same_thread then + level = level + 1 + end + + local name, value = self.getlocal(level, i) + if not name then + return + end + self:add("\tLocal variables:\r\n") + while name do + if type(value) == "number" then + self:add_f("%s%s = number: %g\r\n", prefix, name, value) + elseif type(value) == "boolean" then + self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value)) + elseif type(value) == "string" then + self:add_f("%s%s = string: %q\r\n", prefix, name, value) + elseif type(value) == "userdata" then + self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value)) + elseif type(value) == "nil" then + self:add_f("%s%s = nil\r\n", prefix, name) + elseif type(value) == "table" then + if m_known_tables[value] then + self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value]) + elseif m_user_known_tables[value] then + self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value]) + else + local txt = "{" + for k,v in pairs(value) do + txt = txt..safe_tostring(k)..":"..safe_tostring(v) + if #txt > _M.max_tb_output_len then + txt = txt.." (more...)" + break + end + if next(value, k) then txt = txt..", " end + end + self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt.."}") + end + elseif type(value) == "function" then + local info = self.getinfo(value, "nS") + local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value] + if info.what == "C" then + self:add_f("%s%s = C %s\r\n", prefix, name, (fun_name and ("function: " .. fun_name) or tostring(value))) + else + local source = info.short_src + if source:sub(2,7) == "string" then + source = source:sub(9) + end + --for k,v in pairs(info) do print(k,v) end + fun_name = fun_name or GuessFunctionName(info) + self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name, info.linedefined, source) + end + elseif type(value) == "thread" then + self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value)) + end + i = i + 1 + name, value = self.getlocal(level, i) + end +end + + +--- +-- Public: +-- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc. +-- This function is suitable to be used as an error handler with pcall or xpcall +-- +-- @param thread An optional thread whose stack is to be inspected (defaul is the current thread) +-- @param message An optional error string or object. +-- @param level An optional number telling at which level to start the traceback (default is 1) +-- +-- Returns a string with the stack trace and a string with the original error. +-- +function _M.stacktrace(thread, message, level) + if type(thread) ~= "thread" then + -- shift parameters left + thread, message, level = nil, thread, message + end + + thread = thread or coroutine.running() + + level = level or 1 + + local dumper = Dumper.new(thread) + + local original_error + + if type(message) == "table" then + dumper:add("an error object {\r\n") + local first = true + for k,v in pairs(message) do + if first then + dumper:add(" ") + first = false + else + dumper:add(",\r\n ") + end + dumper:add(safe_tostring(k)) + dumper:add(": ") + dumper:add(safe_tostring(v)) + end + dumper:add("\r\n}") + original_error = dumper:concat_lines() + elseif type(message) == "string" then + dumper:add(message) + original_error = message + end + + dumper:add("\r\n") + dumper:add[[ +Stack Traceback +=============== +]] + --print(error_message) + + local level_to_show = level + if dumper.dumping_same_thread then level = level + 1 end + + local info = dumper.getinfo(level, "nSlf") + while info do + if info.what == "main" then + if string_sub(info.source, 1, 1) == "@" then + dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show, string_sub(info.source, 2), info.currentline) + else + dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.short_src, info.currentline) + end + elseif info.what == "C" then + --print(info.namewhat, info.name) + --for k,v in pairs(info) do print(k,v, type(v)) end + local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or tostring(info.func) + dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name) + --dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value))) + elseif info.what == "tail" then + --print("tail") + --for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name) + dumper:add_f("(%d) tail call\r\n", level_to_show) + dumper:DumpLocals(level) + elseif info.what == "Lua" then + local source = info.short_src + local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name + if source:sub(2, 7) == "string" then + source = source:sub(9) + end + local was_guessed = false + if not function_name or function_name == "?" then + --for k,v in pairs(info) do print(k,v, type(v)) end + function_name = GuessFunctionName(info) + was_guessed = true + end + -- test if we have a file name + local function_type = (info.namewhat == "") and "function" or info.namewhat + if info.source and info.source:sub(1, 1) == "@" then + dumper:add_f("(%d) Lua %s '%s' at file '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "") + elseif info.source and info.source:sub(1,1) == '#' then + dumper:add_f("(%d) Lua %s '%s' at template '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "") + else + dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type, function_name, info.currentline, source) + end + dumper:DumpLocals(level) + else + dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what) + end + + level = level + 1 + level_to_show = level_to_show + 1 + info = dumper.getinfo(level, "nSlf") + end + + return dumper:concat_lines(), original_error +end + +-- +-- Adds a table to the list of known tables +function _M.add_known_table(tab, description) + if m_known_tables[tab] then + error("Cannot override an already known table") + end + m_user_known_tables[tab] = description +end + +-- +-- Adds a function to the list of known functions +function _M.add_known_function(fun, description) + if m_known_functions[fun] then + error("Cannot override an already known function") + end + m_user_known_functions[fun] = description +end + +return _M diff --git a/thirdparty/Eluant.dll b/thirdparty/Eluant.dll new file mode 100755 index 0000000000000000000000000000000000000000..2eef90245795d748c31bbdac54863f856185d84f GIT binary patch literal 64000 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 literal 0 HcmV?d00001 diff --git a/thirdparty/Eluant.dll.config b/thirdparty/Eluant.dll.config new file mode 100644 index 0000000000..5a54723682 --- /dev/null +++ b/thirdparty/Eluant.dll.config @@ -0,0 +1,4 @@ + + + +