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 0000000000..2eef902457 Binary files /dev/null and b/thirdparty/Eluant.dll differ 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 @@ + + + +