// SPDX-FileCopyrightText: 2023 Matheus Izvekov // SPDX-License-Identifier: ISC using Barotrauma; using Barotrauma.Items.Components; using Barotrauma.Networking; using MoonSharp.Interpreter.Interop; using MoonSharp.Interpreter; using System; using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; namespace miz { partial class MicroLua : ItemComponent, IServerSerializable { private readonly static Regex inpRegex = new Regex(@"^signal_in(\d+)$"); private readonly static Regex outRegex = new Regex(@"^signal_out(\d+)$"); private List outConns = new(); private Script script; private readonly struct EventData : IEventData { public readonly string data; public EventData(string data) { this.data = data; } } private void handleException(string source, InterpreterException e) { LuaCsLogger.LogError($"[{Item}] [{source}] {e.DecoratedMessage}", LuaCsMessageOrigin.LuaMod); } private bool toNumber(string s, out double value) { return double.TryParse(s, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out value); } private string source; [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The Lua source code.", alwaysUseInstanceValues: true)] public string Source { get => source; set { if (value == null) { return; } source = value; script = null; IsActive = false; if (source.IsNullOrWhiteSpace()) { return; } Script newscript = new(CoreModules.Preset_HardSandbox | CoreModules.Metatables | CoreModules.ErrorHandling | CoreModules.Coroutine); newscript.Options.DebugPrint = (string o) => LuaCsLogger.LogMessage($"[{Item}] {o}"); newscript.Options.CheckThreadAccess = false; newscript.Globals.Get("table").Table["clear"] = (Table t) => t.Clear(); // MoonSharp's tonumber is too permissive. However, ours does not support base. newscript.Globals["tonumber"] = double? (string s) => toNumber(s, out double value) ? value : null; newscript.Globals["mode"] = GameMain.NetworkMember?.IsClient switch { null => "single", false => "server", true => "client" }; newscript.Globals["out"] = UserData.Create(this, new OutDesc()); newscript.Globals["time"] = () => GameMain.GameScreen.GameTime; if (GameMain.NetworkMember is not null) { #if SERVER newscript.Globals["send"] = (string data) => item.CreateServerEvent(this, new EventData(data)); #endif } try { newscript.DoString(source, codeFriendlyName: "source"); } catch (SyntaxErrorException e) { handleException("syntax", e); return; } catch (ScriptRuntimeException e) { handleException("runtime", e); return; } IsActive = true; script = newscript; } } public MicroLua(Item item, ContentXElement element) : base(item, element) {} class OutDesc : IUserDataDescriptor { public string Name { get { return "out"; } } public Type Type { get { return typeof(OutDesc); } } public DynValue Index(Script script, object obj, DynValue index, bool isDirectIndexing) { throw new ScriptRuntimeException("__index metamethod not implemented"); } public bool SetIndex(Script script, object obj, DynValue index, DynValue value, bool isDirectIndexing) { var parent = (MicroLua)obj; if (index.Type != DataType.Number) { throw new ScriptRuntimeException("pin must be an integer"); } var indexNumber = index.Number; var pin = (int)indexNumber; if ((double)pin != indexNumber) { throw new ScriptRuntimeException("pin must be an integer"); } if (pin < 0 || pin > parent.outConns.Count) { throw new ScriptRuntimeException($"invalid pin {pin}"); } var connection = parent.outConns[pin - 1] ?? throw new ScriptRuntimeException($"invalid pin {pin}"); parent.item.SendSignal(new Signal(value.Type switch { DataType.Number => value.Number.ToString(CultureInfo.InvariantCulture), DataType.String => value.String, _ => throw new ScriptRuntimeException($"invalid value type {value.Type}") }), connection); return true; } public string AsString(object obj) { throw new ScriptRuntimeException("__tostring metamethod not implemented"); } public DynValue MetaIndex(Script script, object obj, string metaname) { throw new ScriptRuntimeException($"{metaname} metamethod not implemented"); } public bool IsTypeCompatible(Type type, object obj) { return type.IsInstanceOfType(obj); } }; public override void OnItemLoaded() { base.OnItemLoaded(); foreach(var connection in item.Connections) { var match = outRegex.Match(connection.Name); if (!match.Success) { continue; } var index = int.Parse(match.Groups[1].Value); outConns.Insert(index - 1, connection); } } private void Call(string field, params object[] objs) { DynValue cb = script.Globals.Get(field); if (cb.Type == DataType.Nil) { return; } try { script.Call(cb, objs); } catch (ScriptRuntimeException e) { handleException(field, e); } } public override void Update(float deltaTime, Camera cam) { Call("upd", deltaTime); } public override void ReceiveSignal(Signal signal, Connection connection) { if (script == null) { return; } var match = inpRegex.Match(connection.Name); if (!match.Success) { return; } var index = int.Parse(match.Groups[1].Value); var value = toNumber(signal.value, out double dynvalue) ? DynValue.NewNumber(dynvalue) : DynValue.NewString(signal.value); DynValue inp = script.Globals.Get("inp"); switch(inp.Type) { case DataType.Nil: break; case DataType.Table: inp.Table.Set(index, value); break; default: try { script.Call(inp, index, value); } catch (ScriptRuntimeException e) { handleException("inp", e); } break; } } #if CLIENT public void ClientEventRead(IReadMessage msg, float sendingTime) { Call("recv", msg.ReadString()); } #endif #if SERVER public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData) { msg.WriteString(ExtractEventData(extraData).data); } #endif } }