using System; using System.Collections.Generic; using System.Linq; using System.Text; using Newtonsoft.Json.Linq; using NLua; using OBSWebsocketDotNet; namespace LuaScriptEngine; public class LuaState: IDisposable { private readonly Lua state = new(); private readonly List PostDataLoadedFuncs = []; private readonly List PreUpdateFuncs = []; private readonly List PostUpdateFuncs = []; private readonly List PreGameBeginFuncs = []; private readonly List PostGameBeginFuncs = []; private readonly List PreGameEndFuncs = []; private readonly List PostGameEndFuncs = []; private readonly HashSet Timers = []; private readonly List TimersToRemove = []; private readonly OBSWebsocket _obs = new(); private readonly Dictionary _scheduledText = []; private long _lastSeedKey = 0L; private string _lastClusterString = ""; public LuaState() { var assemblyPath = System.IO.Path.Combine( System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!, "scripts" ); state.State.Encoding = Encoding.UTF8; state.LoadCLRPackage(); state.DoString("import('Assembly-CSharp')"); state.DoString($"package.path = '{assemblyPath.Replace('\\', '/')}/?.lua'"); RegisterFunctions(); foreach (var file in System.IO.Directory.GetFiles(assemblyPath, "*.lua")) { LuaScriptEngine.Logger.LogInfo($"Loading Lua script: {file}"); state.DoFile(file); } } public void Dispose() { state.Dispose(); if (_obs.IsConnected) { _obs.Disconnect(); } _scheduledText.Clear(); Timers.Clear(); PreUpdateFuncs.Clear(); PostUpdateFuncs.Clear(); PreGameBeginFuncs.Clear(); PostGameBeginFuncs.Clear(); PreGameEndFuncs.Clear(); PostGameEndFuncs.Clear(); } private void RegisterFunctions() { #region Callback Functions state["register_callback"] = (string tp, LuaFunction action) => { switch (tp) { case "data_loaded": PostDataLoadedFuncs.Add(action); break; case "pre_update": PreUpdateFuncs.Add(action); break; case "post_update": PostUpdateFuncs.Add(action); break; case "pre_game_begin": PreGameBeginFuncs.Add(action); break; case "post_game_begin": PostGameBeginFuncs.Add(action); break; case "pre_game_end": PreGameEndFuncs.Add(action); break; case "post_game_end": PostGameEndFuncs.Add(action); break; } }; state["add_timer"] = Timer (LuaFunction func, long firstInterval, long repeatInterval) => { var timer = new Timer(func, firstInterval, repeatInterval); Timers.Add(timer); return timer; }; state["remove_timer"] = void (Timer timer) => { Timers.Remove(timer); }; #endregion #region OBS Functions state["obs_connect"] = void (string server, string password) => { if (_obs.IsConnected) { LuaScriptEngine.Logger.LogInfo("Already connected to OBS"); return; } _obs.Connected += (_, _) => { LuaScriptEngine.Logger.LogInfo($"Connected to OBS at {server}"); foreach (var (sourceName, text) in _scheduledText) { try { _obs.SetInputSettings(sourceName, new JObject { { "text", text } }); } catch (Exception e) { LuaScriptEngine.Logger.LogError($"Error setting {sourceName}'s text to `{text}`: {e}"); } } _scheduledText.Clear(); }; _obs.Disconnected += (_, _) => { _obs.ConnectAsync(server, password); }; _obs.ConnectAsync(server, password); }; state["obs_disconnect"] = void () => { _obs.Disconnect(); }; state["obs_set_source_text"] = void (string sourceName, string text) => { if (_obs.IsConnected) { try { _obs.SetInputSettings(sourceName, new JObject { { "text", text } }); } catch (Exception e) { LuaScriptEngine.Logger.LogError($"Error setting source text: {e}"); _obs.Disconnect(); _scheduledText[sourceName] = text; } } else { _scheduledText[sourceName] = text; } }; #endregion #region Common Data Retrieval Functions state["get_current_cluster"] = string () => { var data = GameMain.data; if (data == null) return ""; var seedKey = data.GetClusterSeedKey(); if (seedKey == _lastSeedKey) return _lastClusterString; _lastSeedKey = seedKey; _lastClusterString = data.gameDesc.clusterString; return _lastClusterString; }; var getCurrentResearch = () => { var history = GameMain.history; if (history == null) return (0, 0, 0L, 0L); var current = history.currentTech; if (current == 0) return (0, 0, 0L, 0L); var techStates = history.techStates; if (!techStates.TryGetValue(current, out var value)) return (0, 0, 0L, 0L); return (current, value.curLevel, value.hashUploaded, value.hashNeeded); }; state["get_current_research"] = getCurrentResearch; state["get_current_research_str"] = string (string format) => { var (current, level, hashUploaded, hashNeeded) = getCurrentResearch(); if (current == 0) return ""; return string.Format(format, LDB.techs.Select(current).name, level, JournalUtility.TranslateKMGValue(hashUploaded), JournalUtility.TranslateKMGValue(hashNeeded)); }; var getTechLevel = (NLua.LuaTable techIds) => { var techStates = GameMain.history?.techStates; if (techStates == null) return 0; int level = 0; foreach (var techId in techIds.Values.Cast()) { if (!techStates.TryGetValue((int)techId, out var value)) { return level; } var newLevel = value.unlocked ? value.curLevel : value.curLevel - 1; if (newLevel > level) { level = newLevel; } if (!value.unlocked) { return level; } } return level; }; state["get_tech_level"] = getTechLevel; state["get_tech_level_str"] = string (string format, NLua.LuaTable techIds) => { var level = getTechLevel(techIds); if (level <= 0) return ""; return string.Format(format, level); }; var getFactoryStat = (int itemId) => { var gameData = GameMain.data; if (gameData == null) return (0, 0); var statPool = GameMain.statistics?.production.factoryStatPool; if (statPool == null) return (0, 0); long productTotal = 0L, consumeTotal = 0L; for (var i = gameData.factoryCount - 1; i >= 0; i--) { var stat = statPool[i]; if (stat == null) continue; var index = stat.productIndices[itemId]; var ppool = stat.productPool[index]; if (ppool == null) continue; var cursor = ppool.cursor[4]; if (cursor > 0) { productTotal += ppool.count[cursor - 1]; } cursor = ppool.cursor[4 + 6]; if (cursor > 0) { consumeTotal += ppool.count[cursor - 1]; } } return (productTotal, consumeTotal); }; state["get_factory_stat"] = getFactoryStat; state["get_factory_stat_str"] = string (string format, int itemId) => { var (productTotal, consumeTotal) = getFactoryStat(itemId); return string.Format(format, JournalUtility.TranslateKMGValue(productTotal), JournalUtility.TranslateKMGValue(consumeTotal)); }; var getDysonSphereTotalGen = () => { var data = GameMain.data; if (data == null) return 0; return data.GetDysonSphereTotalGen(); }; state["get_dyson_sphere_power_gen"] = getDysonSphereTotalGen; state["get_dyson_sphere_power_gen_str"] = string (string format) => { var totalGen = getDysonSphereTotalGen(); if (totalGen <= 0) return ""; return string.Format(format, JournalUtility.TranslateKMGValue(totalGen * 60L)); }; #endregion } public void PostDataLoaded() { LoopCall(PostDataLoadedFuncs); } public void PreUpdate() { if (Timers.Count > 0) { var gameTick = GameMain.gameTick; foreach (var timer in Timers) { if (timer == null || !timer.Check(gameTick)) continue; TimersToRemove.Add(timer); } if (TimersToRemove.Count > 0) { foreach (var timer in TimersToRemove) { Timers.Remove(timer); } TimersToRemove.Clear(); } } LoopCall(PreUpdateFuncs); } public void PostUpdate() { LoopCall(PostUpdateFuncs); } public void PreGameBegin() { var tick = GameMain.gameTick; foreach (var timer in Timers) { if (timer.Reset(tick)) { TimersToRemove.Add(timer); } } if (TimersToRemove.Count > 0) { foreach (var timer in TimersToRemove) { Timers.Remove(timer); } TimersToRemove.Clear(); } LoopCall(PreGameBeginFuncs); } public void PostGameBegin() { LoopCall(PostGameBeginFuncs); } public void PreGameEnd() { LoopCall(PreGameEndFuncs); } public void PostGameEnd() { LoopCall(PostGameEndFuncs); } private void LoopCall(List funcs) { foreach (var func in funcs) { try { func.Call(); } catch (Exception e) { LuaScriptEngine.Logger.LogError($"Error in Lua script: {e}"); } } } private class Timer(LuaFunction func, long startInterval, long repeatInterval = 0L) { public bool Check(long gameTick) { if (gameTick < _nextTick) return false; try { _func.Call(); } catch (Exception e) { LuaScriptEngine.Logger.LogError($"Error in Lua script: {e}"); } if (_repeatInterval <= 0L) return true; _nextTick += _repeatInterval; if (_nextTick < gameTick) _nextTick = gameTick + 1; return false; } public bool Reset(long gameTick) { if (_repeatInterval <= 0L) return true; _nextTick = gameTick + _repeatInterval; return false; } private readonly LuaFunction _func = func; private readonly long _repeatInterval = repeatInterval; private long _nextTick = GameMain.gameTick + startInterval; } }