Files
2026-01-23 18:01:15 +07:00

455 lines
14 KiB
C#

// 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<CScriptValue> { new CScriptValue((double)m_dwKeepingTime) };
var ret = new List<CScriptValue>();
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<CScriptValue> { new CScriptValue(m_strCurPolicy) }, null);
}
catch { /* ignore if function not exist */ }
try
{
CallLuaFunc("AIManager", "OnChangePolicy",
new List<CScriptValue> { 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<CScriptValue> { 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<CScriptValue>
{
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<CScriptValue>
{
new CScriptValue((double)EVENT_RETURNTOWNOK),
};
CallLuaFunc("AIManager", "OnEvent", args, null);
}
public void SendEvent_ConfigChanged()
{
var args = new List<CScriptValue>
{
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<CScriptValue> args, List<CScriptValue> 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<string, bool> RegisterFile;
// table, func, args, ret (ret can be null)
public static Action<string, string, List<CScriptValue>, List<CScriptValue>> 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<string, Dictionary<string, string>> _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<string, string>(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<string, string>(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<string, string>(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();
}
}
}