#region Copyright & License Information /* * Copyright 2007-2020 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, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using Eluant; using OpenRA.Graphics; using OpenRA.Primitives; using OpenRA.Support; using OpenRA.Traits; namespace OpenRA.Scripting { // Tag interfaces specifying the type of bindings to create public interface IScriptBindable { } // For objects that need the context to create their bindings public interface IScriptNotifyBind { void OnScriptBind(ScriptContext context); } // For traitinfos that provide actor / player commands public sealed class ScriptPropertyGroupAttribute : Attribute { public readonly string Category; public ScriptPropertyGroupAttribute(string category) { Category = category; } } // For property groups that are safe to initialize invoke on destroyed actors public sealed class ExposedForDestroyedActors : Attribute { } public sealed class ScriptActorPropertyActivityAttribute : Attribute { } public abstract class ScriptActorProperties { protected readonly Actor Self; protected readonly ScriptContext Context; public ScriptActorProperties(ScriptContext context, Actor self) { Self = self; Context = context; } } public abstract class ScriptPlayerProperties { protected readonly Player Player; protected readonly ScriptContext Context; public ScriptPlayerProperties(ScriptContext context, Player player) { Player = player; Context = context; } } /// /// Provides global bindings in Lua code. /// /// /// Instance methods and properties declared in derived classes will be made available in Lua. Use /// on your derived class to specify the name exposed in Lua. It is recommended /// to apply against each method or property to provide a description of what it does. /// /// Any parameters to your method that are s will be disposed automatically when your method /// completes. If you need to return any of these values, or need them to live longer than your method, you must /// use to get your own copy of the value. Any copied values you return will /// be disposed automatically, but you assume responsibility for disposing any other copies. /// 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) { // GetType resolves the actual (subclass) type var type = GetType(); var names = type.GetCustomAttributes(true); if (names.Length != 1) throw new InvalidOperationException("[ScriptGlobal] attribute not found for global table '{0}'".F(type)); Name = names.First().Name; Bind(new[] { this }); } protected IEnumerable FilteredObjects(IEnumerable objects, LuaFunction filter) { if (filter != null) { objects = objects.Where(a => { using (var luaObject = a.ToLuaValue(Context)) using (var filterResult = filter.Call(luaObject)) using (var result = filterResult.First()) return result.ToBoolean(); }); } return objects; } } public sealed class ScriptGlobalAttribute : Attribute { public readonly string Name; public ScriptGlobalAttribute(string name) { Name = name; } } public sealed class ScriptContext : IDisposable { // Restrict user scripts (excluding system libraries) to 50 MB of memory use const int MaxUserScriptMemory = 50 * 1024 * 1024; // Restrict the number of instructions that will be run per map function call const int MaxUserScriptInstructions = 1000000; public World World { get; private set; } public WorldRenderer WorldRenderer { get; private set; } readonly MemoryConstrainedLuaRuntime runtime; readonly LuaFunction tick; readonly Type[] knownActorCommands; public readonly Cache ActorCommands; public readonly Type[] PlayerCommands; bool disposed; public ScriptContext(World world, WorldRenderer worldRenderer, IEnumerable scripts) { runtime = new MemoryConstrainedLuaRuntime(); Log.AddChannel("lua", "lua.log"); World = world; WorldRenderer = worldRenderer; knownActorCommands = Game.ModData.ObjectCreator .GetTypesImplementing() .ToArray(); ActorCommands = new Cache(FilterActorCommands); var knownPlayerCommands = Game.ModData.ObjectCreator .GetTypesImplementing() .ToArray(); PlayerCommands = FilterCommands(world.Map.Rules.Actors["player"], knownPlayerCommands); runtime.Globals["GameDir"] = Platform.GameDir; runtime.DoBuffer(File.Open(Platform.ResolvePath(".", "lua", "scriptwrapper.lua"), FileMode.Open, FileAccess.Read).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)LogDebugMessage)) 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, world.Map.Open(s).ReadAllText()).Dispose(); } } void LogDebugMessage(string message) { Console.WriteLine("Lua debug: {0}", message); Log.Write("lua", message); } public bool FatalErrorOccurred { get; private set; } public void FatalError(string message) { var stacktrace = new StackTrace().ToString(); Console.WriteLine("Fatal Lua Error: {0}", message); Console.WriteLine(stacktrace); Log.Write("lua", "Fatal Lua Error: {0}", message); Log.Write("lua", stacktrace); FatalErrorOccurred = true; World.AddFrameEndTask(w => { World.EndGame(); World.SetPauseState(true); World.PauseStateLocked = 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 (FatalErrorOccurred) return; using (var worldLoaded = (LuaFunction)runtime.Globals["WorldLoaded"]) worldLoaded.Call().Dispose(); } public void Tick(Actor self) { if (FatalErrorOccurred || disposed) return; using (new PerfSample("tick_lua")) tick.Call().Dispose(); } public void Dispose() { if (disposed) return; disposed = true; runtime?.Dispose(); } static IEnumerable 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()); } static readonly object[] NoArguments = new object[0]; Type[] FilterActorCommands(ActorInfo ai) { return FilterCommands(ai, knownActorCommands); } Type[] FilterCommands(ActorInfo ai, Type[] knownCommands) { var method = typeof(ActorInfo).GetMethod("HasTraitInfo"); return knownCommands.Where(c => ExtractRequiredTypes(c) .All(t => (bool)method.MakeGenericMethod(t).Invoke(ai, NoArguments))) .ToArray(); } public LuaTable CreateTable() { return runtime.CreateTable(); } } }