Files
test/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs
T
2026-02-24 18:45:24 +07:00

686 lines
24 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 Unity.VisualScripting;
using UnityEngine;
public class CECNPCMan : IMsgHandler
{
private Dictionary<int, CECNPC> m_NPCTab = new Dictionary<int, CECNPC>(512);
private Dictionary<int, int> m_UkNPCTab = new Dictionary<int, int>(32);
List<CECNPC> m_aDisappearNPCs = new List<CECNPC>(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<CECNPC> removeFromDisappearTable = new List<CECNPC>(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<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.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<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);
}
}
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<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);
}
return true;
}
private bool OnMsgNPCStopMove(ECMSG msg)
{
cmd_object_stop_move pCmd = EC_Utility.ByteArrayToStructure<cmd_object_stop_move>((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<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);
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<cmd_npc_info_list>(((byte[])msg.dwParam1).AsSpan());
BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: NPC_INFO_LIST contains {pCmd.count} NPC(s)");
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);
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<info_npc>(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<info_npc>(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<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)
{
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<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;
}
}