using UnityEngine; using System.Collections; using System.Collections.Generic; using System; using System.Linq; using System.Diagnostics; using System.Text.RegularExpressions; using MoonSharp.Interpreter; using MoonSharp.Interpreter.Interop; using MoonSharp.Interpreter.Loaders; using MoonSharp.RemoteDebugger; namespace Fungus { public class LuaEnvironment : MonoBehaviour { /// /// Custom file loader for MoonSharp that loads in all Lua scripts in the project. /// Scripts must be placed in a Resources/Lua directory. /// protected class LuaScriptLoader : ScriptLoaderBase { // Give the script loader access to the list of accessible Lua Modules. private IEnumerable luaScripts; public LuaScriptLoader(IEnumerable luaScripts) { this.luaScripts = luaScripts; } /// // Bypasses the standard path resolution logic for require. /// protected override string ResolveModuleName(string modname, string[] paths) { return modname; } public override object LoadFile(string file, Table globalContext) { foreach (TextAsset luaScript in luaScripts) { // Case insensitive string compare to allow standard Lua naming conventions in code if (String.Compare(luaScript.name, file, true) == 0) { return luaScript.text; } } return ""; } public override bool ScriptFileExists(string name) { foreach (TextAsset luaScript in luaScripts) { // Case insensitive string compare to allow standard Lua naming conventions in code if (String.Compare(luaScript.name, name, true) == 0) { return true; } } return false; } } /// /// Returns the first Lua Environment found in the scene, or creates one if none exists. /// This is a slow operation, call it once at startup and cache the returned value. /// public static LuaEnvironment GetLua() { LuaEnvironment luaEnvironment = GameObject.FindObjectOfType(); if (luaEnvironment == null) { GameObject prefab = Resources.Load("Prefabs/LuaEnvironment"); if (prefab != null) { GameObject go = Instantiate(prefab) as GameObject; go.name = "LuaEnvironment"; luaEnvironment = go.GetComponent(); } } return luaEnvironment; } protected Script interpreter; /// /// Returns the MoonSharp interpreter instance used to run Lua code. /// public Script Interpreter { get { return interpreter; } } /// /// Launches the remote Lua debugger in your browser and breaks execution at the first executed Lua command. /// [Tooltip("Launches the remote Lua debugger in your browser and breaks execution at the first executed Lua command.")] public bool remoteDebugger = false; /// /// Lua script file which defines the global string table used for localisation. /// [Tooltip("Lua script file which defines the global string table used for localisation.")] public TextAsset stringTable; /// /// The currently selected language in the string table. Affects variable substitution. /// [Tooltip("The currently selected language in the string table. Affects variable substitution.")] public string activeLanguage = "en"; /// /// Time scale factor to apply when running Lua scripts. /// This allows pausing of time based operations by setting timescale to 0. /// Use the GetTime() and GetDeltaTime() functions to get scaled time values. /// If negative, then GetTime() and GetDeltaTime() return the same values as the standard Time class. /// [Tooltip("Time scale factor to apply when running Lua scripts. If negative then uses the same values as the standard Time class.")] public float timeScale = -1f; /// /// A text file listing the c# types that can be accessed from Lua. /// [HideInInspector] public List registerTypes = new List(); /// /// Instance of remote debugging service when debugging option is enabled. /// protected RemoteDebuggerService remoteDebuggerService; /// /// Flag used to avoid startup dependency issues. /// protected bool initialised = false; /// /// Cached referenence to the string table (if loaded). /// protected Table stringTableCached; protected virtual void Start() { InitInterpreter(); } /// /// Initialise the Lua interpreter so we can start running Lua code. /// public virtual void InitInterpreter() { if (initialised) { return; } Script.DefaultOptions.DebugPrint = (s) => { UnityEngine.Debug.Log(s); }; // In some use cases (e.g. downloadable Lua files) some Lua modules can pose a potential security risk. // You can restrict which core lua modules are available here if needed. See the MoonSharp documentation for details. interpreter = new Script(CoreModules.Preset_Complete); InitLuaScriptFiles(); InitTypes(); InitCustomObjects(); InitBindings(); InitStringTable(); if (remoteDebugger) { ActivateRemoteDebugger(interpreter); } initialised = true; } /// /// Register all Lua files in the project so they can be accessed at runtime. /// protected virtual void InitLuaScriptFiles() { object[] result = Resources.LoadAll("Lua", typeof(TextAsset)); interpreter.Options.ScriptLoader = new LuaScriptLoader(result.OfType()); } /// /// Registers all listed c# types for interop with Lua. /// You can also register types directly in the Awake method of any /// monobehavior in your scene using UserData.RegisterType() directly. /// protected virtual void InitTypes() { // Register types foreach (TextAsset textFile in registerTypes) { if (textFile == null) { continue; } char[] separators = { '\r', '\n' }; foreach (string typeName in textFile.text.Split(separators, StringSplitOptions.RemoveEmptyEntries)) { // Skip comments and empty lines if (typeName.StartsWith("#") || typeName.Trim() == "") { continue; } RegisterType(typeName); } } } /// /// Register a type given it's assembly qualified name. /// public static void RegisterType(string typeName) { bool extensionType = false; string registerName = typeName; if (typeName.StartsWith("E:")) { extensionType = true; registerName = registerName.Substring(2); } System.Type t = System.Type.GetType(registerName); if (t == null) { UnityEngine.Debug.LogWarning("Type not found: " + typeName); return; } // Registering System.Object breaks MoonSharp's automated conversion of Lists and Dictionaries to Lua tables. if (t == typeof(System.Object)) { return; } if (!UserData.IsTypeRegistered(t)) { if (extensionType) { UserData.RegisterExtensionType(t); } else { UserData.RegisterType(t); } } } /// /// Register some commonly used Unity classes and objects for Lua interop. /// To register more class objects externally to this class, register them in the Awake method of any /// monobehavior in your scene. /// protected virtual void InitCustomObjects() { // Add the CLR class objects to a global unity table Table unityTable = new Table(interpreter); interpreter.Globals["unity"] = unityTable; // Static classes unityTable["time"] = UserData.CreateStatic(typeof(Time)); unityTable["fungusprefs"] = UserData.CreateStatic(typeof(FungusPrefs)); UserData.RegisterType(typeof(PODTypeFactory)); unityTable["factory"] = UserData.CreateStatic(typeof(PODTypeFactory)); // This Lua Environment component unityTable["luaenvironment"] = this; // Provide access to the Unity Test Tools (if available). Type testType = Type.GetType("IntegrationTest"); if (testType != null) { UserData.RegisterType(testType); unityTable["test"] = UserData.CreateStatic(testType); } // Example of how to register an enum // UserData.RegisterType(); // interpreter.Globals.Set("MyEnum", UserData.CreateStatic()); } /// /// Binds all gameobjects and components defined in scene LuaBindings to the global table. /// protected virtual void InitBindings() { LuaBindingsBase[] bindings = GameObject.FindObjectsOfType(); foreach (LuaBindingsBase binding in bindings) { binding.AddBindings(interpreter.Globals); } } /// /// Loads the global string table used for localisation. /// protected virtual void InitStringTable() { if (stringTable != null) { try { DynValue stringTableRes = interpreter.DoString(stringTable.text); if (stringTableRes.Type == DataType.Table) { stringTableCached = stringTableRes.Table; interpreter.Globals["stringtable"] = stringTableCached; } } catch (ScriptRuntimeException ex) { UnityEngine.Debug.LogError("Lua runtime error: " + ex.DecoratedMessage); } catch (InterpreterException ex) { UnityEngine.Debug.LogError(ex.DecoratedMessage); } } } /// /// Returns a string from the string table for this key. /// The string returned depends on the active language. /// public virtual string GetString(string key) { if (stringTableCached != null) { // Match against string table and active language DynValue stringTableVar = stringTableCached.Get(key); if (stringTableVar.Type == DataType.Table) { DynValue languageEntry = stringTableVar.Table.Get(activeLanguage); if (languageEntry.Type == DataType.String) { return languageEntry.String; } } } return ""; } /// /// Substitutes specially formatted tokens in the text with global variables and string table values. /// The string table value used depends on the currently loaded string table and active language. /// public virtual string Substitute(string text) { string subbedText = text; // Instantiate the regular expression object. Regex r = new Regex("\\[\\$.*?\\]"); // Match the regular expression pattern against a text string. var results = r.Matches(text); foreach (Match match in results) { string key = match.Value.Substring(2, match.Value.Length - 3); if (stringTableCached != null) { // Match against string table and active language DynValue stringTableVar = stringTableCached.Get(key); if (stringTableVar.Type == DataType.Table) { DynValue languageEntry = stringTableVar.Table.Get(activeLanguage); if (languageEntry.Type == DataType.String) { subbedText = subbedText.Replace(match.Value, languageEntry.String); } continue; } } // Match against global variables DynValue globalVar = interpreter.Globals.Get(key); if (globalVar.Type != DataType.Nil) { subbedText = subbedText.Replace(match.Value, globalVar.ToPrintString()); continue; } } return subbedText; } /// /// Returns the time since level load, multiplied by timeScale. /// If timeScale is negative then returns the same as Time.timeSinceLevelLoaded. /// public float GetTime() { if (timeScale < 0f) { return Time.timeSinceLevelLoad; } else { return Time.unscaledTime * timeScale; } } /// /// Returns the delta time this frame, multiplied by timeScale. /// If timeScale is negative then returns the same as Time.deltaTime. /// public float GetDeltaTime() { if (timeScale < 0f) { return Time.deltaTime; } else { return Time.deltaTime * timeScale; } } /// /// Load and run a string containing Lua script. May be run as a coroutine. /// The Lua code to be run. /// A descriptive name to be used in error reports. /// Run the Lua code as a coroutine to support asynchronous operations. /// Method to callback when the Lua code finishes exection. Supports return parameters. /// public void DoLuaString(string luaString, string friendlyName, bool runAsCoroutine, Action onComplete = null) { InitInterpreter(); // Load the Lua script DynValue res = null; try { res = interpreter.LoadString(luaString, null, friendlyName); } catch (InterpreterException ex) { UnityEngine.Debug.LogError(ex.DecoratedMessage + "\n" + luaString); } if (res == null) { if (onComplete != null) { onComplete(null); } return; } // Execute the Lua script if (runAsCoroutine) { StartCoroutine(RunLuaCoroutineInternal(res.Function, luaString, onComplete)); } else { DynValue returnValue = null; try { returnValue = res.Function.Call(); } catch (InterpreterException ex) { UnityEngine.Debug.LogError(ex.DecoratedMessage + "\n" + luaString); } if (onComplete != null) { onComplete(returnValue); } } } /// /// Starts a Unity coroutine which updates a Lua coroutine each frame. /// A MoonSharp closure object representing a function. /// Debug text to display if an exception occurs (usually the Lua code that is being executed). /// A delegate method that is called when the coroutine completes. Includes return parameter. /// public void RunLuaCoroutine(Closure closure, string debugInfo, Action onComplete = null) { StartCoroutine(RunLuaCoroutineInternal(closure, debugInfo, onComplete)); } /// /// A Unity coroutine method which updates a Lua coroutine each frame. /// A MoonSharp closure object representing a function. /// Debug text to display if an exception occurs (usually the Lua code that is being executed). /// A delegate method that is called when the coroutine completes. Includes return parameter. /// protected IEnumerator RunLuaCoroutineInternal(Closure closure, string debugInfo, Action onComplete = null) { DynValue co = interpreter.CreateCoroutine(closure); DynValue returnValue = null; while (co.Coroutine.State != CoroutineState.Dead) { try { returnValue = co.Coroutine.Resume(); } catch (InterpreterException ex) { UnityEngine.Debug.LogError(ex.DecoratedMessage + "\n" + debugInfo); } yield return null; } if (onComplete != null) { onComplete(returnValue); } } /// /// Start a Unity coroutine from a Lua call. /// public Task RunUnityCoroutine(IEnumerator coroutine) { if (coroutine == null) { return null; } // We use the Task class so we can poll the coroutine to check if it has finished. // Standard Unity coroutines don't support this check. return new Task(RunUnityCoroutineImpl(coroutine)); } /// /// Starts a standard Unity coroutine. /// The coroutine is managed by the LuaEnvironment monobehavior, so you can call StopAllCoroutines to /// stop all active coroutines later. /// protected virtual IEnumerator RunUnityCoroutineImpl(IEnumerator coroutine) { if (coroutine == null) { UnityEngine.Debug.LogWarning("Coroutine must not be null"); yield break; } yield return StartCoroutine(coroutine); } protected virtual void ActivateRemoteDebugger(Script script) { if (remoteDebuggerService == null) { remoteDebuggerService = new RemoteDebuggerService(); // the last boolean is to specify if the script is free to run // after attachment, defaults to false remoteDebuggerService.Attach(script, gameObject.name, false); } // start the web-browser at the correct url. Replace this or just // pass the url to the user in some way. Process.Start(remoteDebuggerService.HttpUrlStringLocalHost); } } }