920 lines
31 KiB
C#
920 lines
31 KiB
C#
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
|
|
{
|
|
private Dictionary<int, CECNPC> m_NPCTab ;
|
|
private Dictionary<int, int> m_UkNPCTab ;
|
|
|
|
List<CECNPC> 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<CECNPC> removeFromDisappearTable = new List<CECNPC>(32);
|
|
|
|
public CECNPCMan()
|
|
{
|
|
//
|
|
m_NPCTab = new Dictionary<int, CECNPC>(512);
|
|
m_UkNPCTab = new Dictionary<int, int>(32);
|
|
m_aDisappearNPCs = new List<CECNPC>(32);
|
|
// Reset debug counter when new instance is created (play mode starts)
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds or updates an NPC in the m_NPCTab dictionary with comprehensive logging.
|
|
/// </summary>
|
|
/// <param name="nid">NPC ID</param>
|
|
/// <param name="npc">NPC object to add</param>
|
|
/// <param name="reason">Reason for adding (for logging purposes)</param>
|
|
/// <returns>True if added successfully, false if npc is null or invalid</returns>
|
|
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
|
|
m_NPCTab[nid] = npc;
|
|
int 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an NPC from the m_NPCTab dictionary with comprehensive logging.
|
|
/// </summary>
|
|
/// <param name="nid">NPC ID to remove</param>
|
|
/// <param name="reason">Reason for removal (for logging purposes)</param>
|
|
/// <returns>True if removed successfully, false if key didn't exist</returns>
|
|
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 = m_NPCTab.Remove(nid);
|
|
int 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clean up destroyed objects from dictionaries. Call this when play mode starts to remove stale references.
|
|
/// </summary>
|
|
public void CleanupDestroyedObjects()
|
|
{
|
|
// Clean up destroyed NPCs from main table
|
|
var keysToRemove = new List<int>();
|
|
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<cmd_object_forbid_be_selected>((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<cmd_object_disappear>((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<cmd_object_atk_result>((byte[])Msg.dwParam1).attacker_id;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_NPCEXTSTATE:
|
|
nid = GPDataTypeHelper.FromBytes<cmd_update_ext_state>((byte[])Msg.dwParam1).id;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_NPCCASTSKILL:
|
|
|
|
nid = GPDataTypeHelper.FromBytes<cmd_object_cast_skill>((byte[])Msg.dwParam1).caster;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_ENCHANTRESULT:
|
|
|
|
nid = GPDataTypeHelper.FromBytes<cmd_enchant_result>((byte[])Msg.dwParam1).caster;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_NPCROOT:
|
|
|
|
nid = GPDataTypeHelper.FromBytes<cmd_object_root>((byte[])Msg.dwParam1).id;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_NPCSKILLRESULT:
|
|
|
|
nid = GPDataTypeHelper.FromBytes<cmd_object_skill_attack_result>((byte[])Msg.dwParam1).attacker_id;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_NPCLEVELUP:
|
|
|
|
nid = GPDataTypeHelper.FromBytes<cmd_level_up>((byte[])Msg.dwParam1).id;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_NPCINVISIBLE:
|
|
|
|
nid = GPDataTypeHelper.FromBytes<cmd_object_invisible>((byte[])Msg.dwParam1).id;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_NPCSTARTPLAYACTION:
|
|
nid = GPDataTypeHelper.FromBytes<cmd_object_start_play_action>((byte[])Msg.dwParam1).id;
|
|
break;
|
|
|
|
case long value when value == EC_MsgDef.MSG_NM_NPCSTOPPLAYACTION:
|
|
nid = GPDataTypeHelper.FromBytes<cmd_object_stop_play_action>((byte[])Msg.dwParam1).id;
|
|
break;
|
|
case long value when value == EC_MsgDef.MSG_NM_MULTIOBJECT_EFFECT:
|
|
nid = GPDataTypeHelper.FromBytes<cmd_multiobj_effect>((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<cmd_npc_died>(byteArray);
|
|
nid = pCmd.id;
|
|
idKiller = pCmd.idKiller;
|
|
}
|
|
else if (stateNPC == CommandID.NPC_DIED2)
|
|
{
|
|
cmd_npc_died2 pCmd = EC_Utility.ByteArrayToStructure<cmd_npc_died2>(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<cmd_object_stop_move>((byte[])msg.dwParam1);
|
|
CECNPC pNPC = SeekOutNPC(pCmd.id);
|
|
if (pNPC)
|
|
pNPC.StopMoveTo(pCmd);
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool OnMsgNPCRunOut(ECMSG msg)
|
|
{
|
|
int id = GPDataTypeHelper.FromBytes<int>((byte[])msg.dwParam1);
|
|
NPCLeave(id);
|
|
return true;
|
|
}
|
|
|
|
private bool OnMsgNPCMove(ECMSG msg)
|
|
{
|
|
var buffer = (byte[])msg.dwParam1;
|
|
cmd_object_move pCmd = MemoryMarshal.Read<cmd_object_move>(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<cmd_npc_info_list>(((byte[])msg.dwParam1).AsSpan());
|
|
|
|
|
|
int offset = Marshal.OffsetOf<cmd_npc_info_list>("placeholder").ToInt32();
|
|
byte[] buffer = (byte[])msg.dwParam1;
|
|
Span<byte> 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<info_npc>(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<info_npc>(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<info_npc>(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<cmd_npc_info_00>(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<byte> 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;
|
|
}
|
|
// 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;
|
|
}
|
|
|
|
// 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<byte> 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<CECNPC> 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;
|
|
}
|
|
}
|