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 UnityEngine; public class CECNPCMan : IMsgHandler { public struct NPCMiniMapData { public int NPCID; public int TemplateID; public string Name; public A3DVECTOR3 Position; public int MapID; public DATA_TYPE DataType; // what type of NPC is this? public NPCMiniMapData(int npcID, int templateID, string name, A3DVECTOR3 position, int mapID, DATA_TYPE dataType) { NPCID = npcID; TemplateID = templateID; Name = name; Position = position; MapID = mapID; DataType = dataType; } } private Dictionary m_NPCTab ; private Dictionary m_UkNPCTab ; private readonly object m_NPCTabLock = new object(); private readonly List m_NPCMiniMapSnapshot = new (); List m_aDisappearNPCs ; public int HandlerId => (int)MANAGER_INDEX.MAN_NPC; // Static counter to track calls - resets to 0 when new instance is created (play mode starts) // 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() { // m_NPCTab = new Dictionary(512); m_UkNPCTab = new Dictionary(32); m_aDisappearNPCs = new List(32); // Reset debug counter when new instance is created (play mode starts) } /// /// Adds or updates an NPC in the m_NPCTab dictionary with comprehensive logging. /// /// NPC ID /// NPC object to add /// Reason for adding (for logging purposes) /// True if added successfully, false if npc is null or invalid private bool AddNPCToTable(int nid, CECNPC npc, string reason = "Unknown") { string stackTrace = System.Environment.StackTrace.Split('\n')[1].Trim(); int countBefore = m_NPCTab.Count; bool keyExists = m_NPCTab.ContainsKey(nid); CECNPC oldValue = keyExists ? m_NPCTab[nid] : null; // Validate input if (npc == null) { BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: FAILED - npc is NULL for nid={nid}, reason={reason}"); return false; } // Check if old value exists and is different if (keyExists && oldValue != null) { bool oldIsDestroyed = false; try { oldIsDestroyed = oldValue.gameObject == null; } catch (System.Exception) { oldIsDestroyed = true; } if (oldValue != npc) { BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: REPLACING existing NPC - nid={nid}, oldValue={(oldValue != null ? oldValue.name : "NULL")}, oldIsDestroyed={oldIsDestroyed}, newValue={npc.name}"); } } // Check new value state bool newIsNull = npc == null; bool newGameObjectIsNull = false; string newNPCName = "NULL"; try { newGameObjectIsNull = npc.gameObject == null; newNPCName = npc.name; } catch (System.Exception ex) { newGameObjectIsNull = true; BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: Exception checking new npc.gameObject for nid={nid}: {ex.Message}"); } // Add to dictionary int countAfter; lock (m_NPCTabLock) { m_NPCTab[nid] = npc; UpsertNPCMiniMapSnapshot(nid, npc); countAfter = m_NPCTab.Count; } // Verify the value was set correctly bool verifySuccess = m_NPCTab.TryGetValue(nid, out var verifyNPC); bool verifyIsNull = verifyNPC == null; bool verifyGameObjectIsNull = false; try { if (verifyNPC != null) verifyGameObjectIsNull = verifyNPC.gameObject == null; } catch (System.Exception ex) { verifyGameObjectIsNull = true; BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: Exception verifying npc.gameObject for nid={nid}: {ex.Message}"); } if (verifyIsNull || verifyGameObjectIsNull) { BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: WARNING - Value is NULL or destroyed immediately after setting! nid={nid}, reason={reason}"); } return true; } /// /// Removes an NPC from the m_NPCTab dictionary with comprehensive logging. /// /// NPC ID to remove /// Reason for removal (for logging purposes) /// True if removed successfully, false if key didn't exist private bool RemoveNPCFromTable(int nid, string reason = "Unknown") { string stackTrace = System.Environment.StackTrace.Split('\n')[1].Trim(); int countBefore = m_NPCTab.Count; bool keyExists = m_NPCTab.ContainsKey(nid); CECNPC valueBeforeRemove = null; bool valueIsNull = false; bool gameObjectIsNull = false; string npcName = "NULL"; if (!keyExists) { BMLogger.LogError($"[DICT_TRACE] RemoveNPCFromTable: KEY NOT FOUND - nid={nid}, reason={reason}"); return false; } // Check value state before removal valueBeforeRemove = m_NPCTab[nid]; valueIsNull = valueBeforeRemove == null; if (!valueIsNull) { try { gameObjectIsNull = valueBeforeRemove.gameObject == null; npcName = valueBeforeRemove.name; } catch (System.Exception ex) { gameObjectIsNull = true; BMLogger.LogError($"[DICT_TRACE] RemoveNPCFromTable: Exception accessing value before remove for nid={nid}: {ex.Message}"); } } // Remove from dictionary bool removed; int countAfter; lock (m_NPCTabLock) { removed = m_NPCTab.Remove(nid); RemoveNPCMiniMapSnapshot(nid); countAfter = m_NPCTab.Count; } // Verify removal bool keyStillExists = m_NPCTab.ContainsKey(nid); if (keyStillExists) { BMLogger.LogError($"[DICT_TRACE] RemoveNPCFromTable: ERROR - Key still exists after removal! nid={nid}, reason={reason}"); } if (valueIsNull || gameObjectIsNull) { BMLogger.LogError($"[DICT_TRACE] RemoveNPCFromTable: REMOVED NULL/DESTROYED VALUE - nid={nid}, valueIsNull={valueIsNull}, gameObjectIsNull={gameObjectIsNull}, reason={reason}. This explains why key existed but value was null!"); } return removed; } /// /// Clean up destroyed objects from dictionaries. Call this when play mode starts to remove stale references. /// public void CleanupDestroyedObjects() { // Clean up destroyed NPCs from main table var keysToRemove = new List(); int nullCount = 0; foreach (var kvp in m_NPCTab) { if (kvp.Value == null) { nullCount++; keysToRemove.Add(kvp.Key); } else if (kvp.Value.gameObject == null) { nullCount++; keysToRemove.Add(kvp.Key); } } foreach (var key in keysToRemove) { RemoveNPCFromTable(key, "CleanupDestroyedObjects - null/destroyed entry"); } // Clean up destroyed NPCs from disappear table int beforeDisappearCount = m_aDisappearNPCs.Count; m_aDisappearNPCs.RemoveAll(npc => npc == null || npc.gameObject == null); } 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 == null || pNPC.gameObject == null) continue; // Skip destroyed objects if (pNPC.ShouldDisappear()) { if (iRemoveCnt < SIZE_REMOVETAB) aRemove[iRemoveCnt++] = pNPC; } else { } } for (int i = 0; i < iRemoveCnt; i++) { if (aRemove[i] != null) { int nid = aRemove[i].GetNPCID(); NPCLeave(nid); } } // 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++) { if (aRemove[i] != null) { int nid = aRemove[i].GetNPCID(); 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) { 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); } else { BMLogger.LogError($"[NPC_REMOVAL_TRACE] NPCDisappear: NPC {nid} NOT FOUND in table"); } } void NPCLeave(int nid, bool bUpdateMMArray = true, bool bRelease = true) { // Release NPC CECNPC pNPC = GetNPC(nid); if (!pNPC) { BMLogger.LogError($"[NPC_REMOVAL_TRACE] NPCLeave: NPC {nid} NOT FOUND in table, cannot remove"); 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 bool removed = RemoveNPCFromTable(nid, $"NPCLeave - bRelease={bRelease}"); // 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) { int nid = pNPC.GetNPCID(); // Remove tab-selected array CECHostPlayer pHost = CECGameRun.Instance.GetHostPlayer(); if (pHost) pHost.RemoveObjectFromTabSels(pNPC); pNPC.Release(); pNPC.DestroySelf(); } else { BMLogger.LogError($"[NPC_REMOVAL_TRACE] ReleaseNPC: pNPC is NULL, cannot release"); } } 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: { int cmdType = Convert.ToInt32(Msg.dwParam2); byte[] buf = (byte[])Msg.dwParam1; if (cmdType == CommandID.UPDATE_EXT_STATE) nid = GPDataTypeHelper.FromBytes(buf).id; else if (cmdType == CommandID.ICON_STATE_NOTIFY) { var iconCmd = new cmd_icon_state_notify(); if (!iconCmd.Initialize(buf)) return false; nid = iconCmd.id; } else return false; 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); } else { if (pNPC == null) BMLogger.LogError($"[NPC_REMOVAL_TRACE] OnMsgNPCDied: NPC {nid} NOT FOUND in table"); else if (pNPC.IsAboutToDie()) BMLogger.LogError($"[NPC_REMOVAL_TRACE] OnMsgNPCDied: NPC {nid} already about to die, skipping"); } return true; } private bool OnMsgNPCStopMove(ECMSG msg) { cmd_object_stop_move pCmd = EC_Utility.ByteArrayToStructure((byte[])msg.dwParam1); 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); switch (commandId) { case CommandID.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()); 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); 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: { var buffer = (byte[])msg.dwParam1; info_npc info = MemoryMarshal.Read(buffer.AsSpan(0, info_npc.HEADER_SIZE)); NPCEnter(info, false, buffer, info_npc.HEADER_SIZE); break; } case CommandID.NPC_ENTER_WORLD: { var buffer = (byte[])msg.dwParam1; info_npc info = MemoryMarshal.Read(buffer.AsSpan(0, info_npc.HEADER_SIZE)); 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) { RemoveNPCFromTable(Info.nid, "NPCEnter - replacing existing NPC"); 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 AddNPCToTable(Info.nid, npc, $"NPCEnter - nid={Info.nid}, tid={Info.tid}, bBornInSight={bBornInSight}"); return true; } public bool NPCEnterTest(int nid, CECNPC npc, string reason = "Unknown") { AddNPCToTable(nid, npc, $"NPCEnterTest - nid={nid}, tid={npc.GetNPCInfo().tid}, bBornInSight={true}"); 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; // Validate that the NPC object is not destroyed (Unity destroyed objects pass != null but throw on access) if (npc == null) { // Clean up destroyed object from dictionary RemoveNPCFromTable(nid, "GetNPC - null value detected"); return null; } try { if (npc.gameObject == null) { // Clean up destroyed object from dictionary RemoveNPCFromTable(nid, "GetNPC - destroyed GameObject detected"); return null; } } catch (System.Exception ex) { BMLogger.LogError($"[DICT_TRACE] GetNPC: Exception accessing npc.gameObject for nid={nid}: {ex.Message}, removing from dictionary"); // Clean up destroyed object from dictionary RemoveNPCFromTable(nid, $"GetNPC - exception accessing GameObject: {ex.Message}"); return null; } return npc; } /// /// Returns a copy of all active NPC objects. Use this on the main thread only. /// public List GetAllNPCs() { lock (m_NPCTabLock) { return new List(m_NPCTab.Values); } } /// /// Returns a plain-data snapshot for worker-thread consumers such as the minimap. /// public List GetAllNPCMiniMapData() { lock (m_NPCTabLock) { return m_NPCMiniMapSnapshot; } } private void UpsertNPCMiniMapSnapshot(int nid, CECNPC npc) { NPCMiniMapData data = CreateNPCMiniMapData(nid, npc); for (int i = 0; i < m_NPCMiniMapSnapshot.Count; i++) { if (m_NPCMiniMapSnapshot[i].NPCID == nid) { m_NPCMiniMapSnapshot[i] = data; return; } } m_NPCMiniMapSnapshot.Add(data); } private void RemoveNPCMiniMapSnapshot(int nid) { for (int i = m_NPCMiniMapSnapshot.Count - 1; i >= 0; i--) { if (m_NPCMiniMapSnapshot[i].NPCID == nid) { m_NPCMiniMapSnapshot.RemoveAt(i); return; } } } private NPCMiniMapData CreateNPCMiniMapData(int nid, CECNPC npc) { var info = npc.GetNPCInfo(); int mapID = CECGameRun.Instance?.GetWorld()?.GetInstanceID() ?? 0; DATA_TYPE dataType = DATA_TYPE.DT_INVALID; if (npc is CECMonster) { dataType = DATA_TYPE.DT_MONSTER_ESSENCE; } else if (npc is CECPet) { dataType = DATA_TYPE.DT_PET_ESSENCE; } else if (npc is CECNPCServer) { dataType = DATA_TYPE.DT_NPC_ESSENCE; } return new NPCMiniMapData(nid, info.tid, npc.GetName() ?? string.Empty, npc.GetServerPos(), mapID, dataType); } // 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) { // Get stack trace to see who's calling this method CECNPC pNPC = GetNPC(nid); // Check for null/destroyed object BEFORE accessing properties (Unity destroyed objects pass != null but throw on access) if (pNPC != null && pNPC.gameObject != null) { return pNPC; } for (int i = 0; i < m_aDisappearNPCs.Count; i++) { CECNPC pDisappearNPC = m_aDisappearNPCs[i]; // Use Unity's == null check which properly handles destroyed objects if (pDisappearNPC == null) continue; if (pDisappearNPC.gameObject == null) continue; try { if (pDisappearNPC.GetNPCID() == nid) { return pDisappearNPC; } } catch (System.Exception) { // Object was destroyed between null check and access - skip it continue; } } 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(); if (pNPC != null) { pNPC.name = $"NPC Server {tid}_{Info.nid}"; } break; case DATA_TYPE.DT_MONSTER_ESSENCE: pNPC = CECGameRun.Instance.GetMonster(); if (pNPC != null) { pNPC.name = $"Monster {tid}_{Info.nid}"; } break; case DATA_TYPE.DT_PET_ESSENCE: pNPC = CECGameRun.Instance.GetPet(); if (pNPC != null) { pNPC.name = $"Pet {tid}_{Info.nid}"; } break; default: BMLogger.LogError($"CECNPCMan::CreateNPC failed, Invalid DATA_TYPE in CreateNPC, tid={tid} dataType={dataType}"); return null; } } // Set born stamp & born-in-sight (giữ nguyên semantics) uint bornStamp = CECGameRun.Instance.GetWorld().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); } } // Release manager public void Release() { OnLeaveGameWorld(); } // On leaving game world bool OnLeaveGameWorld() { // Release all NPCs in active table // NPCTable::iterator it = m_NPCTab.begin(); // for (; it != m_NPCTab.end(); ++it) foreach (var pNPC in m_NPCTab.Values) { ReleaseNPC(pNPC); } lock (m_NPCTabLock) { m_NPCTab.Clear(); m_NPCMiniMapSnapshot.Clear(); } // Release all NPCs in disappear table int i; foreach (var pNPC in m_aDisappearNPCs) { ReleaseNPC(pNPC); } m_aDisappearNPCs.Clear(); // Release all loaded models // ACSWrapper csa(&m_csLoad); // // for (i=0; i < m_aLoadedModels.GetSize(); i++) // { // NPCMODEL* pInfo = m_aLoadedModels[i]; // CECNPC::ReleaseNPCModel(pInfo->Ret); // delete pInfo; // } // // m_aLoadedModels.RemoveAll(); // m_aMMNPCs.RemoveAll(false); // m_aTabSels.RemoveAll(false); return true; } } public struct NPCDiedEvent { public int NPCID; public NPCDiedEvent(int npcID) { NPCID = npcID; } }