using BrewMonster; using BrewMonster.Scripts; using BrewMonster.Scripts.World; using CSNetwork; using CSNetwork.GPDataType; using DG.Tweening; using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Runtime.InteropServices; using BrewMonster.Network; using Unity.VisualScripting; using UnityEngine; public class CECNPCMan : IMsgHandler { private Dictionary m_NPCTab = new Dictionary(512); private Dictionary m_UkNPCTab = new Dictionary(32); List m_aDisappearNPCs = new List(32); public int HandlerId => (int)MANAGER_INDEX.MAN_NPC; // List of NPCs to remove. It's needed in every tick. // Having this as a global variable is more efficient than creating a new list every tick. CECNPC[] aRemove = new CECNPC[64]; int SIZE_REMOVETAB = 64; int iRemoveCnt = 0; List removeFromDisappearTable = new List(32); public CECNPCMan() { } public bool ProcessMessage(ECMSG Msg) { if (Msg.iSubID == 0) { switch (Msg.dwMsg) { case EC_MsgDef.MSG_NM_NPCINFO: OnMsgNPCInfo(Msg); break; case EC_MsgDef.MSG_NM_NPCMOVE: OnMsgNPCMove(Msg); break; case EC_MsgDef.MSG_NM_NPCSTOPMOVE: OnMsgNPCStopMove(Msg); break; case EC_MsgDef.MSG_NM_NPCRUNOUT: OnMsgNPCRunOut(Msg); break; case EC_MsgDef.MSG_NM_NPCOUTOFVIEW: OnMsgNPCOutOfView(Msg); break; case EC_MsgDef.MSG_NM_NPCDIED: OnMsgNPCDied(Msg); break; case EC_MsgDef.MSG_NM_NPCDISAPPEAR: OnMsgNPCDisappear(Msg); break; case EC_MsgDef.MSG_NM_INVALIDOBJECT: OnMsgInvalidObject(Msg); break; case EC_MsgDef.MSG_NM_FORBIDBESELECTED: OnMsgForbidBeSelected(Msg); break; case EC_MsgDef.MSG_NM_NPCATKRESULT: case EC_MsgDef.MSG_NM_NPCEXTSTATE: case EC_MsgDef.MSG_NM_NPCCASTSKILL: case EC_MsgDef.MSG_NM_ENCHANTRESULT: case EC_MsgDef.MSG_NM_NPCROOT: case EC_MsgDef.MSG_NM_NPCSKILLRESULT: case EC_MsgDef.MSG_NM_NPCLEVELUP: case EC_MsgDef.MSG_NM_NPCINVISIBLE: case EC_MsgDef.MSG_NM_NPCSTARTPLAYACTION: case EC_MsgDef.MSG_NM_NPCSTOPPLAYACTION: case EC_MsgDef.MSG_NM_MULTIOBJECT_EFFECT: TransmitMessage(Msg); break; } } return true; } private void OnMsgForbidBeSelected(ECMSG Msg) { cmd_object_forbid_be_selected pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); CECNPC pNPC = GetNPC(pCmd.id); if (pNPC) { pNPC.SetSelectable(pCmd.b == 0); } } private void OnMsgInvalidObject(ECMSG Msg) { int id = (int)Msg.dwParam1; CECNPC pNPC = GetNPC(id); if (pNPC) { NPCLeave(id); } } private void OnMsgNPCOutOfView(ECMSG msg) { NPCLeave((int)msg.dwParam1); } public void Tick() { iRemoveCnt = 0; // Tick all NPCs foreach (var pNPC in m_NPCTab.Values) { if (pNPC.ShouldDisappear()) { if (iRemoveCnt < SIZE_REMOVETAB) aRemove[iRemoveCnt++] = pNPC; } else { } } for (int i = 0; i < iRemoveCnt; i++) NPCLeave(aRemove[i].GetNPCID()); // Tick all NPCs who are in disappear table iRemoveCnt = 0; CECNPC disappearedNPC = null; removeFromDisappearTable.Clear(); for (int i = 0; i < m_aDisappearNPCs.Count; i++) { disappearedNPC = m_aDisappearNPCs[i]; if (disappearedNPC.ShouldDisappear()) { if (iRemoveCnt < SIZE_REMOVETAB) { aRemove[iRemoveCnt++] = disappearedNPC; removeFromDisappearTable.Add(disappearedNPC); } } } for (int i = 0; i < removeFromDisappearTable.Count; i++) { m_aDisappearNPCs.Remove(removeFromDisappearTable[i]); } for (int i = 0; i < iRemoveCnt; i++) { ReleaseNPC(aRemove[i]); } // Update NPCs in various ranges (Active, visible, mini-map etc.) // UpdateNPCInRanges(dwDeltaTime); // Udpate unknown NPC table // UpdateUnknownNPCs(); } private void OnMsgNPCDisappear(ECMSG Msg) { BMLogger.Log("HoangDev : OnMsgNPCDisappear "); var pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); NPCDisappear(pCmd.id); } void NPCDisappear(int nid) { CECNPC pNPC = GetNPC(nid); if (pNPC) { if (!pNPC.IsDead()) { // NPC Ïûʧʱ£¨¿ÉÄÜÉíÉÏ»¹ÓÐÏà¹ØÌØÐ§£¬ÐèÒª´¥·¢£¬Èç×Ô±¬Ê±±¬Õ¨ÌØÐ§£© // ËäÈ»ÔÚ CECNPC::Killed ÖÐÒÑ×ö´¦Àí£¬µ«¿Í»§¶Ë¿ÉÄÜ»áÖ±½ÓÊÕµ½ disappear ÏûÏ¢£¨Èç×Ô±¬¼¼ÄÜ£© // Òò´ËÕâÀïÐèÒªÔö¼Ó´¥·¢µ÷Óà // Èô֮ǰ NPC ÒÑËÀÍö£¬Ôò˵Ã÷Òѵ÷Óùý pNPC.ClearComActFlag(true); } pNPC.Disappear(); // From npc from active table and add it to disappear table NPCLeave(nid, true, false); m_aDisappearNPCs.Add(pNPC); } } void NPCLeave(int nid, bool bUpdateMMArray = true, bool bRelease = true) { // Release NPC CECNPC pNPC = GetNPC(nid); if (!pNPC) return; /*if (bUpdateMMArray) RemoveNPCFromMiniMap(pNPC);*/ pNPC.m_iMMIndex = -1; var hostplayer = CECGameRun.Instance.GetHostPlayer(); // If this NPC is selected by host, cancel the selection if (pNPC.GetNPCID() == hostplayer.GetSelectedTarget()) hostplayer.SelectTarget(0); // Remove it from active NPC table BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.NPCLeave: Removing NPC from m_NPCTab - nid={nid}, table size before={m_NPCTab.Count}"); bool removed = m_NPCTab.Remove(nid); BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.NPCLeave: NPC removed={(removed ? "SUCCESS" : "FAILED (not in table)" )}, table size after={m_NPCTab.Count}"); // Forbid reloading npc's resources //QueueNPCUndoLoad(nid, pNPC->GetBornStamp()); // Release NPC resource if (bRelease) ReleaseNPC(pNPC); else { CECHostPlayer pHost = hostplayer; if (pHost != null) pHost.RemoveObjectFromTabSels(pNPC); } /* CECPlayerWrapper* pWrapper = CECAutoPolicy::GetInstance().GetPlayerWrapper(); if (pWrapper) pWrapper->OnObjectDisappear(nid);*/ } void ReleaseNPC(CECNPC pNPC) { if (pNPC) { // Remove tab-selected array CECHostPlayer pHost = CECGameRun.Instance.GetHostPlayer(); if (pHost) pHost.RemoveObjectFromTabSels(pNPC); pNPC.Release(); pNPC.DestroySelf(); } } private bool TransmitMessage(ECMSG Msg) { int nid = 0; switch (Msg.dwMsg) { case long value when value == EC_MsgDef.MSG_NM_NPCATKRESULT: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).attacker_id; break; case long value when value == EC_MsgDef.MSG_NM_NPCEXTSTATE: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).id; break; case long value when value == EC_MsgDef.MSG_NM_NPCCASTSKILL: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).caster; break; case long value when value == EC_MsgDef.MSG_NM_ENCHANTRESULT: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).caster; break; case long value when value == EC_MsgDef.MSG_NM_NPCROOT: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).id; break; case long value when value == EC_MsgDef.MSG_NM_NPCSKILLRESULT: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).attacker_id; break; case long value when value == EC_MsgDef.MSG_NM_NPCLEVELUP: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).id; break; case long value when value == EC_MsgDef.MSG_NM_NPCINVISIBLE: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).id; break; case long value when value == EC_MsgDef.MSG_NM_NPCSTARTPLAYACTION: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).id; break; case long value when value == EC_MsgDef.MSG_NM_NPCSTOPPLAYACTION: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).id; break; case long value when value == EC_MsgDef.MSG_NM_MULTIOBJECT_EFFECT: nid = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1).id; break; default: return false; } CECNPC pNPC = SeekOutNPC(nid); if (pNPC) pNPC.ProcessMessage(Msg); return true; } private bool OnMsgNPCDied(ECMSG msg) { int nid = 0, idKiller = 0; bool bDelay = false; var stateNPC = Convert.ToInt32(msg.dwParam2); var byteArray = (byte[])msg.dwParam1; if (stateNPC == CommandID.NPC_DIED) { cmd_npc_died pCmd = EC_Utility.ByteArrayToStructure(byteArray); nid = pCmd.id; idKiller = pCmd.idKiller; } else if (stateNPC == CommandID.NPC_DIED2) { cmd_npc_died2 pCmd = EC_Utility.ByteArrayToStructure(byteArray); nid = pCmd.id; idKiller = pCmd.idKiller; bDelay = true; } if (!GPDataTypeHelper.ISNPCID(nid)) return false; CECNPC pNPC = GetNPC(nid); EventBus.Publish(new NPCDiedEvent(nid)); if (pNPC && !pNPC.IsAboutToDie()) { pNPC.Killed(bDelay); // Below codes may case the last damaged bubble number before // npc died couldn't popup // if (!bDelay) // NPCDisappear(nid); } return true; } private bool OnMsgNPCStopMove(ECMSG msg) { cmd_object_stop_move pCmd = EC_Utility.ByteArrayToStructure((byte[])msg.dwParam1); if (-2041571143 == pCmd.id) { BMLogger.Log("HoangDev: OnMsgNPCStopMove NPCID: " + pCmd.id); } CECNPC pNPC = SeekOutNPC(pCmd.id); if (pNPC) pNPC.StopMoveTo(pCmd); return true; } private bool OnMsgNPCRunOut(ECMSG msg) { int id = GPDataTypeHelper.FromBytes((byte[])msg.dwParam1); NPCLeave(id); return true; } private bool OnMsgNPCMove(ECMSG msg) { var buffer = (byte[])msg.dwParam1; cmd_object_move pCmd = MemoryMarshal.Read(buffer); if (pCmd.use_time == 0) return true; CECNPC pNPC = SeekOutNPC(pCmd.id); if (pNPC) pNPC.MoveTo(pCmd); return true; } public CECNPC SeekOutNPC(int nid) { if (!m_NPCTab.TryGetValue(nid, out var npc)) { // Couldn't find this NPC, put it into unknown NPC table m_UkNPCTab[nid] = nid; return null; } return npc; } private bool OnMsgNPCInfo(ECMSG msg) { int commandId = Convert.ToInt32(msg.dwParam2); BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Received NPC info message, commandID={commandId}"); switch (commandId) { case CommandID.NPC_INFO_LIST: { BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Processing NPC_INFO_LIST"); // msg.dwParam1 chính là buffer chứa placeholder data (không có header cmd_npc_info_list) cmd_npc_info_list pCmd = MemoryMarshal.Read(((byte[])msg.dwParam1).AsSpan()); BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: NPC_INFO_LIST contains {pCmd.count} NPC(s)"); int offset = Marshal.OffsetOf("placeholder").ToInt32(); byte[] buffer = (byte[])msg.dwParam1; Span pDataBuf = buffer.AsSpan(offset); for (int i = 0; i < pCmd.count; i++) { // giống const info_npc& Info = *(const info_npc*)pDataBuf; info_npc info = MemoryMarshal.Read(pDataBuf); BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Processing NPC {i + 1}/{pCmd.count} - nid={info.nid}, tid={info.tid}"); int iSize = info_npc.HEADER_SIZE; if ((info.state & PlayerNPCState.GP_STATE_EXTEND_PROPERTY) != 0) iSize += sizeof(uint) * NumberDWORDsPlayerNPC.OBJECT_EXT_STATE_COUNT; if ((info.state & PlayerNPCState.GP_STATE_NPC_PET) != 0) iSize += sizeof(int); if ((info.state & PlayerNPCState.GP_STATE_NPC_NAME) != 0) { byte len = pDataBuf[iSize]; iSize += 1 + len; } if ((info.state & PlayerNPCState.GP_STATE_MULTIOBJ_EFFECT) != 0) { int countEff = BinaryPrimitives.ReadInt32LittleEndian( pDataBuf.Slice(iSize, sizeof(int))); iSize += sizeof(int) + countEff * (sizeof(int) + 1); } if ((info.state & PlayerNPCState.GP_STATE_NPC_MAFIA) != 0) iSize += sizeof(int); NPCEnter(info, false, buffer, offset); // dịch pDataBuf về sau (giống pDataBuf += iSize) pDataBuf = pDataBuf.Slice(iSize); offset += iSize; } break; } case CommandID.NPC_ENTER_SLICE: { BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Processing NPC_ENTER_SLICE"); var buffer = (byte[])msg.dwParam1; info_npc info = MemoryMarshal.Read(buffer.AsSpan(0, info_npc.HEADER_SIZE)); BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: NPC_ENTER_SLICE - nid={info.nid}, tid={info.tid}"); NPCEnter(info, false, buffer, info_npc.HEADER_SIZE); break; } case CommandID.NPC_ENTER_WORLD: { BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Processing NPC_ENTER_WORLD"); var buffer = (byte[])msg.dwParam1; info_npc info = MemoryMarshal.Read(buffer.AsSpan(0, info_npc.HEADER_SIZE)); BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: NPC_ENTER_WORLD - nid={info.nid}, tid={info.tid}"); NPCEnter(info, true, buffer, info_npc.HEADER_SIZE); break; } case CommandID.NPC_INFO_00: { var buffer = (byte[])msg.dwParam1; cmd_npc_info_00 pCmd = GPDataTypeHelper.FromBytes(buffer); CECNPC pNPC = SeekOutNPC(pCmd.idNPC); if (pNPC) { ROLEBASICPROP bp = pNPC.GetBasicProps(); ROLEEXTPROP ep = pNPC.GetExtendProps(); bp.iCurHP = pCmd.iHP; ep.bs.max_hp = pCmd.iMaxHP; pNPC.SetSelectedTarget(pCmd.iTargetID); pNPC.SetWorldHealthImage((float)pCmd.iHP, (float)pCmd.iMaxHP); EventBus.Publish(new CECHostPlayer.NPCINFO(pNPC.GetName(), pCmd.iHP, pCmd.iMaxHP, pCmd.idNPC)); } break; } case CommandID.NPC_VISIBLE_TID_NOTIFY: { cmd_npc_visible_tid_notify pCmd = (cmd_npc_visible_tid_notify)msg.dwParam1; CECNPC pNPC = SeekOutNPC(pCmd.nid); if (pNPC) pNPC.TransformShape(pCmd.vis_tid); break; } } return true; } public bool NPCEnter(in info_npc Info, bool bBornInSight, ReadOnlySpan packet, int infoOffset) { var npc = GetNPC(Info.nid); if (npc != null) { m_NPCTab.Remove(Info.nid); GameObject.Destroy(npc.gameObject); } // Nếu id này có trong bảng unknown thì xóa nó if (m_UkNPCTab.ContainsKey(Info.nid)) { m_UkNPCTab.Remove(Info.nid); } // Tạo NPC mới npc = CreateNPC(Info, bBornInSight, packet, infoOffset); //if (npc != null) //{ // npc.SetUpCECNPC(this); //} if (object.ReferenceEquals(npc, null)) { BrewMonster.BMLogger.LogError($"Failed to create NPC ({Info.tid})"); return false; } // Thêm NPC vào bảng BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.NPCEnter: Adding NPC to m_NPCTab - nid={Info.nid}, tid={Info.tid}, npc={(npc != null ? "created" : "NULL")}"); m_NPCTab[Info.nid] = npc; BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.NPCEnter: NPC added successfully. m_NPCTab[Info.nid] = npc {m_NPCTab[Info.nid].name} NPC(s)"); return true; } // Get NPC by id and optional bornStamp public CECNPC GetNPC(int nid, uint bornStamp = 0) { if (!m_NPCTab.TryGetValue(nid, out var npc)) return null; return npc; } // Find first NPC/Monster by template id (tid). Used by UI auto-move coordinate resolving. // 通过模板ID(tid)查找第一个NPC/怪物。用于UI自动寻路坐标解析。 public CECNPC FindNPCByTemplateID(int tid) { if (tid == 0) return null; foreach (var npc in m_NPCTab.Values) { if (!npc) continue; var info = npc.GetNPCInfo(); if (info.tid == tid || info.vis_tid == tid) { return npc; } } return null; } public CECNPC GetNPCFromAll(int nid) { BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: Looking for NPC nid={nid}, m_NPCTab.Count={m_NPCTab.Count}"); CECNPC pNPC = GetNPC(nid); if (pNPC != null) { BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: GetNPC returned {pNPC.name}"); BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: NPC {nid} FOUND in m_NPCTab"); return pNPC; } BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: NPC {nid} NOT FOUND in m_NPCTab! Available NPC IDs: {string.Join(", ", m_NPCTab.Keys)}"); // Search from disappear array - provides grace period for GFX events (matches C++ behavior) for (int i = 0; i < m_aDisappearNPCs.Count; i++) { CECNPC pDisappearNPC = m_aDisappearNPCs[i]; if (pDisappearNPC != null && pDisappearNPC.gameObject != null && pDisappearNPC.GetNPCID() == nid) { BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: NPC {nid} FOUND in m_aDisappearNPCs"); return pDisappearNPC; // Return NPC even if removed from main table } } return null; } public CECNPC CreateNPC(info_npc Info, bool bBornInSight, ReadOnlySpan packet, int infoOffset) { CECNPC pNPC = null; int tid = Info.tid; bool bPet = (Info.state & PlayerNPCState.GP_STATE_NPC_PET) != 0; // Get data type from database var edm = ElementDataManProvider.GetElementDataMan(); // tương đương g_pGame->GetElementDataMan() DATA_TYPE dataType = edm.get_data_type((uint)tid, ID_SPACE.ID_SPACE_ESSENCE); if (dataType != DATA_TYPE.DT_NPC_ESSENCE && dataType != DATA_TYPE.DT_MONSTER_ESSENCE && dataType != DATA_TYPE.DT_PET_ESSENCE) { // Try default npc tid = 4249; dataType = edm.get_data_type((uint)tid, ID_SPACE.ID_SPACE_ESSENCE); } if (bPet) { pNPC = CECGameRun.Instance.GetPet(); } else { switch (dataType) { case DATA_TYPE.DT_NPC_ESSENCE: pNPC = CECGameRun.Instance.GetNPCServer(); break; case DATA_TYPE.DT_MONSTER_ESSENCE: pNPC = CECGameRun.Instance.GetMonster(); break; case DATA_TYPE.DT_PET_ESSENCE: pNPC = CECGameRun.Instance.GetPet(); break; default: UnityEngine.Debug.Assert(false, "Invalid DATA_TYPE in CreateNPC"); return null; } } // Set born stamp & born-in-sight (giữ nguyên semantics) uint bornStamp = CECWorld.Instance.GetBornStamp(); if (!object.ReferenceEquals(pNPC, null)) { pNPC.SetBornStamp(bornStamp); pNPC.SetBornInSight(bBornInSight); pNPC.SetUpCECNPC(this); } else { return null; } // Init với tid + Info như C++ if (!pNPC.Init(tid, Info, packet, infoOffset)) { // đảm bảo giải phóng nếu bạn có tài nguyên kèm theo //pNPC?.Release(); pNPC = null; // log lỗi tương tự glb_ErrorOutput UnityEngine.Debug.LogError($"CECNPCMan::CreateNPC failed, tid={tid}"); return null; } return pNPC; } // Get npc candidates whom can be auto-selected by 'TAB' key public void TabSelectCandidates(int idCurSel, List aCands) { CECHostPlayer pHost = CECGameRun.Instance.GetHostPlayer(); if (pHost == null) return; // Note: IsSkeletonReady() check is commented out in Unity codebase // if (!pHost.IsSkeletonReady()) // { // // Only when IsSkeletonReady() is true, GetDistToHost() is valid // return; // } // Trace all NPCs foreach (var kvp in m_NPCTab) { CECNPC pNPC = kvp.Value; if (pNPC == null) continue; if (!pNPC.IsSelectable() || !pNPC.IsMonsterNPC() || pNPC.IsDead() || pNPC.GetNPCID() == idCurSel || pHost.AttackableJudge(pNPC.GetNPCID(), false) != 1) continue; float fDist = pNPC.GetDistToHost(); if (fDist > EC_RoleTypes.EC_TABSEL_DIST || !pHost.CanSafelySelectWith(fDist)) { continue; } aCands.Add(pNPC); } } } public struct NPCDiedEvent { public int NPCID; public NPCDiedEvent(int npcID) { NPCID = npcID; } }