Add a new native-lua implementation.

This commit is contained in:
Paul Chote
2014-03-27 22:40:17 +13:00
parent f6efc9c5bc
commit d73af0190f
29 changed files with 1929 additions and 21 deletions

View File

@@ -37,7 +37,7 @@
CSC = dmcs CSC = dmcs
CSFLAGS = -nologo -warn:4 -debug:full -optimize- -codepage:utf8 -unsafe -warnaserror CSFLAGS = -nologo -warn:4 -debug:full -optimize- -codepage:utf8 -unsafe -warnaserror
DEFINE = DEBUG;TRACE 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_TARGET = OpenRA.Editor.exe
editor_KIND = winexe editor_KIND = winexe
editor_DEPS = $(game_TARGET) 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_EXTRA = -resource:OpenRA.Editor.Form1.resources -resource:OpenRA.Editor.MapSelect.resources
editor_FLAGS = -win32icon:OpenRA.Editor/OpenRA.Editor.Icon.ico editor_FLAGS = -win32icon:OpenRA.Editor/OpenRA.Editor.Icon.ico

View File

@@ -71,6 +71,9 @@
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="Eluant">
<HintPath>..\thirdparty\Eluant.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="ActorPropertiesDialog.cs"> <Compile Include="ActorPropertiesDialog.cs">

40
OpenRA.Game/Actor.cs Executable file → Normal file
View File

@@ -12,13 +12,16 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using Eluant;
using Eluant.ObjectBinding;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Scripting;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA namespace OpenRA
{ {
public class Actor public class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding
{ {
public readonly ActorInfo Info; public readonly ActorInfo Info;
@@ -240,5 +243,40 @@ namespace OpenRA
health.Value.InflictDamage(this, attacker, health.Value.MaxHP, null, true); health.Value.InflictDamage(this, attacker, health.Value.MaxHP, null, true);
} }
#region Scripting interface
Lazy<ScriptActorInterface> 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<Actor>(out a) || !right.TryGetClrValue<Actor>(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
} }
} }

View File

@@ -10,13 +10,13 @@
using System; using System;
using System.Drawing; using System.Drawing;
using Eluant;
using Eluant.ObjectBinding;
using OpenRA.Scripting;
namespace OpenRA namespace OpenRA
{ {
/// <summary> public struct CPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding
/// Cell coordinate position in the world (coarse).
/// </summary>
public struct CPos
{ {
public readonly int X, Y; public readonly int X, Y;
@@ -60,6 +60,56 @@ namespace OpenRA
public override string ToString() { return "{0},{1}".F(X, Y); } 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<CPos>(out a) || !right.TryGetClrValue<CVec>(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<CPos>(out a) || !right.TryGetClrValue<CVec>(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<CPos>(out a) || !right.TryGetClrValue<CPos>(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 public static class RectangleExtensions

View File

@@ -10,13 +10,13 @@
using System; using System;
using System.Drawing; using System.Drawing;
using Eluant;
using Eluant.ObjectBinding;
using OpenRA.Scripting;
namespace OpenRA namespace OpenRA
{ {
/// <summary> public struct CVec : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding
/// Cell coordinate vector (coarse).
/// </summary>
public struct CVec
{ {
public readonly int X, Y; public readonly int X, Y;
@@ -82,5 +82,60 @@ namespace OpenRA
new CVec(1, 0), new CVec(1, 0),
new CVec(1, 1), new CVec(1, 1),
}; };
#region Scripting interface
public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right)
{
CVec a, b;
if (!left.TryGetClrValue<CVec>(out a) || !right.TryGetClrValue<CVec>(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<CVec>(out a) || !right.TryGetClrValue<CVec>(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<CVec>(out a) || !right.TryGetClrValue<CVec>(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
} }
} }

View File

@@ -74,6 +74,9 @@
<Package>mono.nat</Package> <Package>mono.nat</Package>
<HintPath>..\thirdparty\Mono.Nat.dll</HintPath> <HintPath>..\thirdparty\Mono.Nat.dll</HintPath>
</Reference> </Reference>
<Reference Include="Eluant">
<HintPath>..\thirdparty\Eluant.dll</HintPath>
</Reference>
<Reference Include="Tao.OpenAl, Version=1.1.0.1, Culture=neutral, PublicKeyToken=a7579dda88828311"> <Reference Include="Tao.OpenAl, Version=1.1.0.1, Culture=neutral, PublicKeyToken=a7579dda88828311">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\thirdparty\Tao\Tao.OpenAl.dll</HintPath> <HintPath>..\thirdparty\Tao\Tao.OpenAl.dll</HintPath>
@@ -236,6 +239,13 @@
<Compile Include="Widgets\SpriteWidget.cs" /> <Compile Include="Widgets\SpriteWidget.cs" />
<Compile Include="Widgets\SpriteSequenceWidget.cs" /> <Compile Include="Widgets\SpriteSequenceWidget.cs" />
<Compile Include="Widgets\RGBASpriteWidget.cs" /> <Compile Include="Widgets\RGBASpriteWidget.cs" />
<Compile Include="Scripting\ScriptContext.cs" />
<Compile Include="Scripting\ScriptActorInterface.cs" />
<Compile Include="Scripting\ScriptObjectWrapper.cs" />
<Compile Include="Scripting\ScriptTypes.cs" />
<Compile Include="Scripting\ScriptMemberWrapper.cs" />
<Compile Include="Scripting\ScriptMemberExts.cs" />
<Compile Include="Scripting\ScriptPlayerInterface.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="FileSystem\D2kSoundResources.cs" /> <Compile Include="FileSystem\D2kSoundResources.cs" />
@@ -359,4 +369,7 @@
<Target Name="AfterBuild"> <Target Name="AfterBuild">
</Target> </Target>
--> -->
<ItemGroup>
<Folder Include="Scripting\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,6 @@
#region Copyright & License Information #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 * 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 * available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation. For more information, * as published by the Free Software Foundation. For more information,
@@ -8,12 +8,16 @@
*/ */
#endregion #endregion
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Eluant;
using Eluant.ObjectBinding;
using OpenRA.FileFormats; using OpenRA.FileFormats;
using OpenRA.Network; using OpenRA.Network;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Scripting;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA namespace OpenRA
@@ -21,7 +25,7 @@ namespace OpenRA
public enum PowerState { Normal, Low, Critical }; public enum PowerState { Normal, Low, Critical };
public enum WinState { Won, Lost, Undefined }; public enum WinState { Won, Lost, Undefined };
public class Player public class Player : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding
{ {
public Actor PlayerActor; public Actor PlayerActor;
public WinState WinState = WinState.Undefined; public WinState WinState = WinState.Undefined;
@@ -106,5 +110,35 @@ namespace OpenRA
// Observers are considered as allies // Observers are considered as allies
return p == null || Stances[p] == Stance.Ally; return p == null || Stances[p] == Stance.Ally;
} }
#region Scripting interface
Lazy<ScriptPlayerInterface> 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<Player>(out a) || !right.TryGetClrValue<Player>(out b))
return false;
return a == b;
}
public LuaValue ToString(LuaRuntime runtime)
{
return "Player ({0})".F(PlayerName);
}
#endregion
} }
} }

View File

@@ -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);
}
}
}

View File

@@ -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<ScriptGlobalAttribute>(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<ActorInfo, Type[]> ActorCommands;
public readonly Type[] PlayerCommands;
public ScriptContext(World world, WorldRenderer worldRenderer,
IEnumerable<string> scripts)
{
runtime = new MemoryConstrainedLuaRuntime();
World = world;
WorldRenderer = worldRenderer;
knownActorCommands = Game.modData.ObjectCreator
.GetTypesImplementing<ScriptActorProperties>()
.ToArray();
ActorCommands = new Cache<ActorInfo, Type[]>(FilterActorCommands);
PlayerCommands = Game.modData.ObjectCreator
.GetTypesImplementing<ScriptPlayerProperties>()
.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<string>)FatalError))
runtime.Globals["FatalError"] = fn;
runtime.Globals["MaxUserScriptInstructions"] = MaxUserScriptInstructions;
using (var registerGlobal = (LuaFunction)runtime.Globals["RegisterSandboxedGlobal"])
{
using (var fn = runtime.CreateFunctionFromDelegate((Action<string>)Console.WriteLine))
registerGlobal.Call("print", fn).Dispose();
// Register global tables
var bindings = Game.modData.ObjectCreator.GetTypesImplementing<ScriptGlobal>();
foreach (var b in bindings)
{
var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var p = c.GetParameters();
return p.Length == 1 && p.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<T> 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(); }
}
}

View File

@@ -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<string, string> LuaTypeNameReplacements = new Dictionary<string, string>()
{
{ "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<string>();
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);
}
}
}

View File

@@ -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<LuaVararg, LuaValue>)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<MemberInfo> 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;
});
}
}
}

View File

@@ -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<string, ScriptMemberWrapper> members;
public ScriptObjectWrapper(ScriptContext context)
{
this.context = context;
}
protected void Bind(IEnumerable<object> clrObjects)
{
members = new Dictionary<string, ScriptMemberWrapper>();
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);
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<T>(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;
}
}
}

View File

@@ -10,13 +10,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Eluant;
using Eluant.ObjectBinding;
using OpenRA.Scripting;
namespace OpenRA namespace OpenRA
{ {
/// <summary> public struct WPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding
/// 3d World position - 1024 units = 1 cell.
/// </summary>
public struct WPos
{ {
public readonly int X, Y, Z; public readonly int X, Y, Z;
@@ -59,6 +59,71 @@ namespace OpenRA
} }
public override string ToString() { return "{0},{1},{2}".F(X, Y, Z); } 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<WPos>(out a) || !right.TryGetClrValue<WVec>(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<WPos>(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<WPos>(out b);
return new LuaCustomClrObject(a - b);
}
else if (rightType == typeof(WVec))
{
WVec b;
right.TryGetClrValue<WVec>(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<WPos>(out a) || !right.TryGetClrValue<WPos>(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 public static class IEnumerableExtensions

View File

@@ -9,13 +9,13 @@
#endregion #endregion
using System; using System;
using Eluant;
using Eluant.ObjectBinding;
using OpenRA.Scripting;
namespace OpenRA namespace OpenRA
{ {
/// <summary> public struct WVec : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding
/// 3d World vector for describing offsets and distances - 1024 units = 1 cell.
/// </summary>
public struct WVec
{ {
public readonly int X, Y, Z; public readonly int X, Y, Z;
@@ -87,5 +87,61 @@ namespace OpenRA
} }
public override string ToString() { return "{0},{1},{2}".F(X, Y, Z); } 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<WVec>(out a) || !right.TryGetClrValue<WVec>(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<WVec>(out a) || !right.TryGetClrValue<WVec>(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<WVec>(out a) || !right.TryGetClrValue<WVec>(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
} }
} }

View File

@@ -69,6 +69,9 @@
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="Eluant">
<HintPath>..\thirdparty\Eluant.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Activities\HarvesterDockSequence.cs" /> <Compile Include="Activities\HarvesterDockSequence.cs" />

View File

@@ -73,6 +73,9 @@
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="Eluant">
<HintPath>..\thirdparty\Eluant.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="ThrowsShrapnel.cs" /> <Compile Include="ThrowsShrapnel.cs" />

View File

@@ -85,6 +85,9 @@
<Reference Include="MaxMind.GeoIP2"> <Reference Include="MaxMind.GeoIP2">
<HintPath>..\thirdparty\MaxMind.GeoIP2.dll</HintPath> <HintPath>..\thirdparty\MaxMind.GeoIP2.dll</HintPath>
</Reference> </Reference>
<Reference Include="Eluant">
<HintPath>..\thirdparty\Eluant.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Activities\CaptureActor.cs" /> <Compile Include="Activities\CaptureActor.cs" />
@@ -498,6 +501,7 @@
<Compile Include="Render\WithBuildingPlacedAnimation.cs" /> <Compile Include="Render\WithBuildingPlacedAnimation.cs" />
<Compile Include="StartGameNotification.cs" /> <Compile Include="StartGameNotification.cs" />
<Compile Include="Widgets\ConfirmationDialogs.cs" /> <Compile Include="Widgets\ConfirmationDialogs.cs" />
<Compile Include="Scripting\LuaScript.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj"> <ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">

View File

@@ -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<SpawnMapActorsInfo>
{
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);
}
}
}

View File

@@ -36,6 +36,9 @@
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="Eluant">
<HintPath>..\thirdparty\Eluant.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ItemGroup> <ItemGroup>

View File

@@ -17,10 +17,14 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Eluant;
using Eluant.ObjectBinding;
using OpenRA.FileFormats; using OpenRA.FileFormats;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.GameRules; using OpenRA.GameRules;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Scripting;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA.Utility namespace OpenRA.Utility
@@ -340,6 +344,157 @@ namespace OpenRA.Utility
Console.Write(doc.ToString()); Console.Write(doc.ToString());
} }
static string[] RequiredTraitNames(Type t)
{
// Returns the inner types of all the Requires<T> interfaces on this type
var outer = t.GetInterfaces()
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>));
// 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 ```<table name>.<function name>```.\n" +
"* Individual actors expose a collection of properties and commands that query information of modify their state.\n" +
" * Some commands, marked as <em>queued activity</em>, 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<ScriptGlobal>()
.OrderBy(t => t.Name);
Console.WriteLine("<h3>Global Tables</h3>");
foreach (var t in tables)
{
var name = t.GetCustomAttributes<ScriptGlobalAttribute>(true).First().Name;
var members = ScriptMemberWrapper.WrappableMembers(t);
Console.WriteLine("<table align=\"center\" width=\"1024\"><tr><th colspan=\"2\" width=\"1024\">{0}</th></tr>", name);
foreach (var m in members.OrderBy(m => m.Name))
{
string desc = m.HasAttribute<DescAttribute>() ? m.GetCustomAttributes<DescAttribute>(true).First().Lines.JoinWith("\n") : "";
Console.WriteLine("<tr><td align=\"right\" width=\"50%\"><strong>{0}</strong></td><td>{1}</td></tr>".F(m.LuaDocString(), desc));
}
Console.WriteLine("</table>");
}
Console.WriteLine("<h3>Actor Properties / Commands</h3>");
var actorCategories = Game.modData.ObjectCreator.GetTypesImplementing<ScriptActorProperties>().SelectMany(cg =>
{
var catAttr = cg.GetCustomAttributes<ScriptPropertyGroupAttribute>(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("<table align=\"center\" width=\"1024\"><tr><th colspan=\"2\" width=\"1024\">{0}</th></tr>", kv.Key);
foreach (var property in kv.OrderBy(p => p.Item2.Name))
{
var mi = property.Item2;
var required = property.Item3;
var hasDesc = mi.HasAttribute<DescAttribute>();
var hasRequires = required.Any();
var isActivity = mi.HasAttribute<ScriptActorPropertyActivityAttribute>();
Console.WriteLine("<tr><td width=\"50%\" align=\"right\"><strong>{0}</strong>", mi.LuaDocString());
if (isActivity)
Console.WriteLine("<br /><em>Queued Activity</em>");
Console.WriteLine("</td><td>");
if (hasDesc)
Console.WriteLine(mi.GetCustomAttributes<DescAttribute>(false).First().Lines.JoinWith("\n"));
if (hasDesc && hasRequires)
Console.WriteLine("<br />");
if (hasRequires)
Console.WriteLine("<b>Requires {1}:</b> {0}".F(required.JoinWith(", "), required.Length == 1 ? "Trait" : "Traits"));
Console.WriteLine("</td></tr>");
}
Console.WriteLine("</table>");
}
Console.WriteLine("<h3>Player Properties / Commands</h3>");
var playerCategories = Game.modData.ObjectCreator.GetTypesImplementing<ScriptPlayerProperties>().SelectMany(cg =>
{
var catAttr = cg.GetCustomAttributes<ScriptPropertyGroupAttribute>(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("<table align=\"center\" width=\"1024\"><tr><th colspan=\"2\" width=\"1024\">{0}</th></tr>", kv.Key);
foreach (var property in kv.OrderBy(p => p.Item2.Name))
{
var mi = property.Item2;
var required = property.Item3;
var hasDesc = mi.HasAttribute<DescAttribute>();
var hasRequires = required.Any();
var isActivity = mi.HasAttribute<ScriptActorPropertyActivityAttribute>();
Console.WriteLine("<tr><td width=\"50%\" align=\"right\"><strong>{0}</strong>", mi.LuaDocString());
if (isActivity)
Console.WriteLine("<br /><em>Queued Activity</em>");
Console.WriteLine("</td><td>");
if (hasDesc)
Console.WriteLine(mi.GetCustomAttributes<DescAttribute>(false).First().Lines.JoinWith("\n"));
if (hasDesc && hasRequires)
Console.WriteLine("<br />");
if (hasRequires)
Console.WriteLine("<b>Requires {1}:</b> {0}".F(required.JoinWith(", "), required.Length == 1 ? "Trait" : "Traits"));
Console.WriteLine("</td></tr>");
}
Console.WriteLine("</table>");
}
}
[Desc("MAPFILE", "Generate hash of specified oramap file.")] [Desc("MAPFILE", "Generate hash of specified oramap file.")]
public static void GetMapHash(string[] args) public static void GetMapHash(string[] args)
{ {

View File

@@ -71,6 +71,9 @@
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\thirdparty\ICSharpCode.SharpZipLib.dll</HintPath> <HintPath>..\thirdparty\ICSharpCode.SharpZipLib.dll</HintPath>
</Reference> </Reference>
<Reference Include="Eluant">
<HintPath>..\thirdparty\Eluant.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Command.cs" /> <Compile Include="Command.cs" />

View File

@@ -27,6 +27,7 @@ namespace OpenRA.Utility
{ "--remap", Command.RemapShp }, { "--remap", Command.RemapShp },
{ "--transpose", Command.TransposeShp }, { "--transpose", Command.TransposeShp },
{ "--docs", Command.ExtractTraitDocs }, { "--docs", Command.ExtractTraitDocs },
{ "--lua-docs", Command.ExtractLuaDocs },
{ "--map-hash", Command.GetMapHash }, { "--map-hash", Command.GetMapHash },
{ "--map-preview", Command.GenerateMinimap }, { "--map-preview", Command.GenerateMinimap },
{ "--map-upgrade-v5", Command.UpgradeV5Map }, { "--map-upgrade-v5", Command.UpgradeV5Map },

163
lua/sandbox.lua Normal file
View File

@@ -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

44
lua/scriptwrapper.lua Normal file
View File

@@ -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

411
lua/stacktraceplus.lua Normal file
View File

@@ -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 ("<failed to get printable value>: '%s'"):format(err) end
end
-- Private:
-- Parses a line, looking for possible function definitions (in a very na<6E>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

BIN
thirdparty/Eluant.dll vendored Executable file

Binary file not shown.

4
thirdparty/Eluant.dll.config vendored Normal file
View File

@@ -0,0 +1,4 @@
<configuration>
<dllmap os="linux" dll="lua51.dll" target="liblua5.1.so" />
<dllmap os="osx" dll="lua51.dll" target="liblua.5.1.dylib" />
</configuration>