// EC_AutoPolicy.cs // Converted from EC_AutoPolicy.h / EC_AutoPolicy.cpp using BrewMonster.Assets.PerfectWorld.Scripts.Players; using BrewMonster.Network; using CSNetwork.GPDataType; using System; using System.Collections.Generic; using System.IO; public sealed class CECAutoPolicy { // policy type public const int POLICY_AUTOKILLMONSTER = 0; // system events public const int EVENT_BEHURT = 1; public const int EVENT_SKILLINTERRUPT = 2; public const int EVENT_RETURNTOWNOK = 3; public const int EVENT_CONFIGCHANGED = 4; public struct CONFIG { public int attack_skill; public bool attack_iscombo; public int assist_skill; public bool assist_iscombo; public int nAssistInterval; public int nTime; public int iAutoPickMode; // 0 none, 1 all, 2 only money public int nPetrolRadius; public CONFIG(bool _ = true) { attack_skill = 0; attack_iscombo = false; assist_skill = 0; assist_iscombo = false; nAssistInterval = 60000; nTime = 3600000; iAutoPickMode = 0; nPetrolRadius = 500; } } private static readonly CECAutoPolicy _instance = new CECAutoPolicy(); public static CECAutoPolicy GetInstance() => _instance; private CONFIG m_Config; private CECPlayerWrapper m_pPlayer; private string m_strCurPolicy; private readonly CECCounter m_cntTick; private uint m_dwKeepingTime; private uint m_dwCurrentTime; private CECAutoPolicy() { m_pPlayer = null; m_cntTick = new CECCounter(); // 100ms => 10 ticks/sec m_cntTick.SetPeriod(100); m_dwKeepingTime = 0; m_dwCurrentTime = 0; m_strCurPolicy = string.Empty; m_Config = new CONFIG(); } public bool Init() { // load lua file list: configs/autopolicy/allfiles.txt // (token-based in C++; here read whitespace tokens) var listPath = Path.Combine("configs", "autopolicy", "allfiles.txt"); if (!File.Exists(listPath)) { // a_LogOutput equivalent: adapt in your project return false; } foreach (var line in File.ReadLines(listPath)) { var s = line.Trim(); if (string.IsNullOrEmpty(s)) continue; if (s.StartsWith("#") || s.StartsWith("//")) continue; // token 0 var parts = s.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) continue; var luaFile = Path.Combine("configs", "autopolicy", parts[0]); LoadLuaFile(luaFile); } // Register player API: _InitPlayerAPI(g_LuaStateMan.GetAIState()); AutoPolicyLuaHooks.InitPlayerAPI?.Invoke(); return true; } public void Release() { m_pPlayer = null; } public void OnEnterWorld() { if (m_pPlayer == null) m_pPlayer = new CECPlayerWrapper(EC_Game.GetGameRun().GetHostPlayer()); LoadConfigData(); } public void OnLeaveWorld() { if (m_pPlayer != null) { SaveConfigData(); m_pPlayer = null; } SetCurPolicy(string.Empty); m_dwCurrentTime = 0; } public void Tick(uint dwDeltaTime) { if (EC_Game.GetGameRun().GetGameState() != (int)GameState.GS_GAME) return; m_pPlayer?.Tick(dwDeltaTime); if (m_cntTick.IncCounter(dwDeltaTime)) { var args = new List { new CScriptValue((double)m_dwKeepingTime) }; var ret = new List(); CallLuaFunc("AIManager", "OnTick", args, ret); m_cntTick.Reset(); m_dwKeepingTime = 0; } m_dwKeepingTime += dwDeltaTime; if (IsAutoPolicyEnabled()) m_dwCurrentTime += dwDeltaTime; } public void Render() { // C++ draws debug text with A3DFont. Implement your own debug UI if needed. // In C++ it uses: m_pPlayer.m_iAttackErrCnt / m_iPickupErrCnt / action queue list } public void StartPolicy(int policyType) { string[] policyMap = { "AutoKillMonster", // POLICY_AUTOKILLMONSTER }; // FIXED: original C++ has a sizeof bug. Correct is policyMap.Length. if (policyType < 0 || policyType >= policyMap.Length) return; string policyName = policyMap[policyType]; SetCurPolicy(policyName); m_dwCurrentTime = 0; if (m_pPlayer != null) { m_pPlayer.GetHostPlayer().ClearComboSkill(); m_pPlayer.SetOrigPos(m_pPlayer.GetHostPlayer().GetPos()); } var pGameUI = EC_Game.GetGameRun().GetUIManager().GetInGameUIMan(); //pGameUI?.AddChatMessage(pGameUI.GetStringFromTable(10920), EC_GPDataType.GP_CHAT_MISC); } // Put this inside CECAutoPolicy class private void SetCurPolicy(string strPolicy) { // normalize null strPolicy ??= string.Empty; // no change if (string.Equals(m_strCurPolicy, strPolicy, StringComparison.Ordinal)) return; // stop old policy (if any) if (!string.IsNullOrEmpty(m_strCurPolicy)) { // Try a few common Lua callbacks (depending on your lua side implementation). // You can keep only the one that exists in your project. try { CallLuaFunc("AIManager", "OnStopPolicy", new List { new CScriptValue(m_strCurPolicy) }, null); } catch { /* ignore if function not exist */ } try { CallLuaFunc("AIManager", "OnChangePolicy", new List { new CScriptValue(m_strCurPolicy), new CScriptValue(strPolicy) }, null); } catch { /* ignore if function not exist */ } } // assign m_strCurPolicy = strPolicy; // start new policy (if any) if (!string.IsNullOrEmpty(m_strCurPolicy)) { try { CallLuaFunc("AIManager", "OnStartPolicy", new List { new CScriptValue(m_strCurPolicy) }, null); } catch { /* ignore if function not exist */ } } } public void StopPolicy() { SetCurPolicy(string.Empty); m_pPlayer?.OnStopPolicy(); m_dwCurrentTime = 0; var pGameUI = EC_Game.GetGameRun().GetUIManager().GetInGameUIMan(); //pGameUI?.AddChatMessage(pGameUI.GetStringFromTable(10921), ChatChannel.GP_CHAT_MISC); } public void SendEvent_BeHurt(int attacker) { var args = new List { new CScriptValue((double)EVENT_BEHURT), new CScriptValue((double)attacker), }; CallLuaFunc("AIManager", "OnEvent", args, null); if (GPDataTypeHelper.ISNPCID(attacker) && m_pPlayer != null) m_pPlayer.OnMonsterAttackMe(attacker); } public void SendEvent_SkillInterrupt(int skill_id) { } public void SendEvent_ReturnTown() { var args = new List { new CScriptValue((double)EVENT_RETURNTOWNOK), }; CallLuaFunc("AIManager", "OnEvent", args, null); } public void SendEvent_ConfigChanged() { var args = new List { new CScriptValue((double)EVENT_CONFIGCHANGED), }; CallLuaFunc("AIManager", "OnEvent", args, null); // C++: m_pPlayer->m_InvalidObj.clear(); // In C# wrapper keeps it protected; clear via StopPolicy-style reset or expose method if you want exact parity. // If you need exact parity, add a method on wrapper to ClearInvalidObj(). } public CECPlayerWrapper GetPlayerWrapper() => m_pPlayer; public bool IsAutoPolicyEnabled() => !string.IsNullOrEmpty(m_strCurPolicy); public string GetCurPolicy() => m_strCurPolicy; public CONFIG GetConfigData() => m_Config; public void SetConfigData(CONFIG data) { m_Config = data; SaveConfigData(); } public uint GetRemainTime() { if (m_Config.nTime > (int)m_dwCurrentTime) return (uint)(m_Config.nTime - (int)m_dwCurrentTime); return 0; } private bool LoadLuaFile(string filename) { // C++ uses LuaState lock + RegisterFile. Provide hooks. return AutoPolicyLuaHooks.RegisterFile?.Invoke(filename) ?? false; } private void CallLuaFunc(string szTable, string szName, List args, List ret) { AutoPolicyLuaHooks.CallLua?.Invoke(szTable, szName, args, ret); } private bool LoadConfigData() { if (m_pPlayer == null) return false; string strFile = Path.Combine("userdata", "autopolicy", $"{m_pPlayer.GetHostPlayer().GetCharacterID()}.ini"); if (!File.Exists(strFile)) { SaveConfigData(); return false; } var ini = SimpleIni.Load(strFile); m_Config.attack_skill = ini.GetInt("config", "attack_skill", 0); m_Config.attack_iscombo = ini.GetBool("config", "attack_iscombo", false); m_Config.assist_skill = ini.GetInt("config", "assist_skill", 0); m_Config.assist_iscombo = ini.GetBool("config", "assist_iscombo", false); m_Config.nAssistInterval = ini.GetInt("config", "assist_interval", 60000); m_Config.nTime = ini.GetInt("config", "keeping_time", 3600000); m_Config.iAutoPickMode = ini.GetInt("config", "autopick", 1); m_Config.nPetrolRadius = ini.GetInt("config", "petrol_radius", 500); return true; } private void SaveConfigData() { Directory.CreateDirectory(Path.Combine("userdata", "autopolicy")); var ini = new SimpleIni(); ini.SetInt("config", "attack_skill", m_Config.attack_skill); ini.SetBool("config", "attack_iscombo", m_Config.attack_iscombo); ini.SetInt("config", "assist_skill", m_Config.assist_skill); ini.SetBool("config", "assist_iscombo", m_Config.assist_iscombo); ini.SetInt("config", "assist_interval", m_Config.nAssistInterval); ini.SetInt("config", "keeping_time", m_Config.nTime); ini.SetInt("config", "autopick", m_Config.iAutoPickMode); ini.SetInt("config", "petrol_radius", m_Config.nPetrolRadius); if (m_pPlayer != null) { string strFile = Path.Combine("userdata", "autopolicy", $"{m_pPlayer.GetHostPlayer().GetCharacterID()}.ini"); ini.Save(strFile); } } } // ===================== Hooks & tiny helpers ===================== // Keep these minimal; replace by your real Lua + script value types. public static class AutoPolicyLuaHooks { // return true if file registered/loaded public static Func RegisterFile; // table, func, args, ret (ret can be null) public static Action, List> CallLua; public static Action InitPlayerAPI; } // Minimal script value public readonly struct CScriptValue { public readonly object Value; public CScriptValue(object v) { Value = v; } } // Very small INI helper (enough for this file) public sealed class SimpleIni { private readonly Dictionary> _data = new(StringComparer.OrdinalIgnoreCase); public static SimpleIni Load(string path) { var ini = new SimpleIni(); string section = ""; foreach (var raw in File.ReadLines(path)) { var line = raw.Trim(); if (line.Length == 0) continue; if (line.StartsWith(";") || line.StartsWith("#") || line.StartsWith("//")) continue; if (line.StartsWith("[") && line.EndsWith("]")) { section = line.Substring(1, line.Length - 2).Trim(); if (!ini._data.ContainsKey(section)) ini._data[section] = new Dictionary(StringComparer.OrdinalIgnoreCase); continue; } int eq = line.IndexOf('='); if (eq <= 0) continue; string key = line.Substring(0, eq).Trim(); string val = line.Substring(eq + 1).Trim(); if (!ini._data.TryGetValue(section, out var dict)) { dict = new Dictionary(StringComparer.OrdinalIgnoreCase); ini._data[section] = dict; } dict[key] = val; } return ini; } public int GetInt(string section, string key, int def) { if (_data.TryGetValue(section, out var s) && s.TryGetValue(key, out var v) && int.TryParse(v, out var r)) return r; return def; } public bool GetBool(string section, string key, bool def) { if (_data.TryGetValue(section, out var s) && s.TryGetValue(key, out var v)) { if (v == "1") return true; if (v == "0") return false; if (bool.TryParse(v, out var r)) return r; } return def; } public void SetInt(string section, string key, int value) => Set(section, key, value.ToString()); public void SetBool(string section, string key, bool value) => Set(section, key, value ? "1" : "0"); private void Set(string section, string key, string value) { if (!_data.TryGetValue(section, out var s)) { s = new Dictionary(StringComparer.OrdinalIgnoreCase); _data[section] = s; } s[key] = value; } public void Save(string path) { using var sw = new StreamWriter(path); foreach (var sect in _data) { sw.WriteLine($"[{sect.Key}]"); foreach (var kv in sect.Value) sw.WriteLine($"{kv.Key}={kv.Value}"); sw.WriteLine(); } } }