This commit is contained in:
Tran Hai Nam
2026-05-15 11:06:45 +07:00
8 changed files with 432 additions and 32 deletions
+101
View File
@@ -241,6 +241,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
skillNameText: {fileID: 0}
imageProgress: {fileID: 0}
btnCancel: {fileID: 0}
_debugHostPlayerPos: {x: 0, y: 209.1, z: 0}
_hostPlayerIcon: {fileID: 7794785241219751657}
nRow: 3
@@ -249,6 +250,9 @@ MonoBehaviour:
_imageMiniMapPrefab: {fileID: 561411731206337256}
_listImageMiniMap: []
_transformMiniMapParent: {fileID: 4768066627267225838}
_imageNPCMiniMapPrefab: {fileID: 5617331893333014101}
_npcMiniMapSprite: {fileID: 21300000, guid: 410207327d87441459b814da357c9b54, type: 3}
_npcMiniMapIconSize: {x: 16, y: 16}
_worldMapButton: {fileID: 9145966839271910414}
_spriteAtlas: {fileID: 100100200, guid: b764c7c6d08a20e41a8ebfb3435954db, type: 3}
--- !u!1 &5886515903784990361
@@ -421,6 +425,7 @@ RectTransform:
- {fileID: 4768066627267225838}
- {fileID: 670009488162588346}
- {fileID: 7794785241219751657}
- {fileID: 4868528813881887473}
m_Father: {fileID: 75608124384920614}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
@@ -615,6 +620,102 @@ MonoBehaviour:
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &6750487080347129537
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4868528813881887473}
- component: {fileID: 5425040792438544429}
- component: {fileID: 5617331893333014101}
- component: {fileID: 4760304024276327800}
m_Layer: 0
m_Name: npc
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!224 &4868528813881887473
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6750487080347129537}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5306706511345298923}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5711758, y: 0.4164777}
m_AnchorMax: {x: 0.5711758, y: 0.4164777}
m_AnchoredPosition: {x: -14.804569, y: 17.372637}
m_SizeDelta: {x: 7, y: 7}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5425040792438544429
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6750487080347129537}
m_CullTransparentMesh: 1
--- !u!114 &5617331893333014101
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6750487080347129537}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 410207327d87441459b814da357c9b54, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &4760304024276327800
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6750487080347129537}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 1
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &7056095192285021313
GameObject:
m_ObjectHideFlags: 0
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7bb8ce7140f7f3652a200d0393561498e3ecdd8f4425bd2248e1ff000f225e5e
size 106979
oid sha256:133267e461382e8feb9da7fa610607c52255abb519753fb55dab36ddd758f54d
size 114097
@@ -13,8 +13,30 @@ 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<int, CECNPC> m_NPCTab ;
private Dictionary<int, int> m_UkNPCTab ;
private readonly object m_NPCTabLock = new object();
private readonly List<NPCMiniMapData> m_NPCMiniMapSnapshot = new ();
List<CECNPC> m_aDisappearNPCs ;
public int HandlerId => (int)MANAGER_INDEX.MAN_NPC;
@@ -94,8 +116,13 @@ public class CECNPCMan : IMsgHandler
}
// Add to dictionary
m_NPCTab[nid] = npc;
int countAfter = m_NPCTab.Count;
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);
@@ -162,8 +189,14 @@ public class CECNPCMan : IMsgHandler
// Remove from dictionary
bool removed = m_NPCTab.Remove(nid);
int countAfter = m_NPCTab.Count;
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);
@@ -729,6 +762,9 @@ public class CECNPCMan : IMsgHandler
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)
{
@@ -762,6 +798,75 @@ public class CECNPCMan : IMsgHandler
return npc;
}
/// <summary>
/// Returns a copy of all active NPC objects. Use this on the main thread only.
/// </summary>
public List<CECNPC> GetAllNPCs()
{
lock (m_NPCTabLock)
{
return new List<CECNPC>(m_NPCTab.Values);
}
}
/// <summary>
/// Returns a plain-data snapshot for worker-thread consumers such as the minimap.
/// </summary>
public List<NPCMiniMapData> 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)
@@ -952,7 +1057,11 @@ public class CECNPCMan : IMsgHandler
ReleaseNPC(pNPC);
}
m_NPCTab.Clear();
lock (m_NPCTabLock)
{
m_NPCTab.Clear();
m_NPCMiniMapSnapshot.Clear();
}
// Release all NPCs in disappear table
int i;
@@ -26,6 +26,7 @@ namespace BrewMonster.UI
[SerializeField] private TMP_InputField _passwordInputField;
[SerializeField] private Button _loginButton;
[SerializeField] private SelecScreenCharacter _selectCharacterScreen;
[SerializeField] private CDlgMessageBox _cDlgMessageBox;
private List<RoleInfo> _roleInfos;
private List<RoleInfo> _currentRoles;
@@ -68,6 +69,7 @@ namespace BrewMonster.UI
{
Tech3CSDKWrapper.Instance.RemoveLoginCallback();
Tech3CSDKWrapper.Instance.RemoveLogoutCallback();
_cDlgMessageBox.gameObject.SetActive(false);
}
void Start()
@@ -129,6 +131,12 @@ namespace BrewMonster.UI
public async void OnLoginButtonClicked()
{
if (!UnityGameSession.Instance.GameSession.IsConnectedInternet)
{
_cDlgMessageBox.ShowMessageBoxYes("","Mất Kết Nối",null,null);
_cDlgMessageBox.gameObject.SetActive(true);
return;
}
if (_loginInProgress)
{
BMLogger.LogWarning("[LoginScreenUI] Login already in progress (ignored click).");
@@ -417,7 +425,7 @@ namespace BrewMonster.UI
{
pGameUI.m_pDlgMiniMap.InitializeMiniMap();
}
await Task.Delay(2000);
// Request all known packages: 0=Inventory,1=Equipment,2=Task
UnityGameSession.RequestAllInventoriesAsync(() => { /*BMLogger.Log("Sent Inventory Detail Requests (all packs)");*/ }, 0, 1, 2);
@@ -1,10 +1,14 @@
using System.Collections.Generic;
using System;
using System.Threading;
using BrewMonster;
using BrewMonster.Managers;
using BrewMonster.Network;
using BrewMonster.Scripts;
using BrewMonster.Scripts.Extensions;
using BrewMonster.UI;
using CSNetwork.GPDataType;
using Cysharp.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.U2D;
@@ -39,6 +43,9 @@ namespace PerfectWorld.UI.MiniMap
[SerializeField] private Image _imageMiniMapPrefab;
[SerializeField] private List<Image> _listImageMiniMap = new();
[SerializeField] private RectTransform _transformMiniMapParent;
[SerializeField] private Image _imageNPCMiniMapPrefab;
[SerializeField] private Sprite _npcMiniMapSprite;
[SerializeField] private Vector2 _npcMiniMapIconSize = new Vector2(16f, 16f);
[SerializeField] private Button _worldMapButton;
// reference to unity sprite atlas
@@ -48,6 +55,12 @@ namespace PerfectWorld.UI.MiniMap
private List<MARK> m_vecNPCMark = new();
private Dictionary<string, Sprite> m_TexMap = new();
private List<string> _texToDelete = new(); // list of texture to delete from m_TexMap, use in update functions.
private readonly Dictionary<int, Image> _npcMiniMapImages = new();
private readonly object _npcMiniMapRenderLock = new object();
private List<CECNPCMan.NPCMiniMapData> _pendingNPCMiniMapData = new();
private volatile bool _needRenderNPCMiniMap;
private CancellationTokenSource _npcMiniMapCancellationTokenSource;
private bool _npcMiniMapWatcherStarted;
private float m_fZoom = 1.0f;
private bool m_bShowMark = true;
private bool m_bShowTargetArrow = true;
@@ -69,9 +82,24 @@ namespace PerfectWorld.UI.MiniMap
_worldMapButton.onClick.AddListener(OnMiniMapClicked);
}
private void OnDestroy()
{
_worldMapButton.onClick.RemoveListener(OnMiniMapClicked);
_npcMiniMapCancellationTokenSource?.Cancel();
_npcMiniMapCancellationTokenSource?.Dispose();
_npcMiniMapCancellationTokenSource = null;
}
void Update()
{
UpdateMiniMap();
EnsureNPCMiniMapWatcher();
if (_needRenderNPCMiniMap)
{
_needRenderNPCMiniMap = false;
RenderNPCMiniMap();
}
}
/// <summary>
@@ -104,6 +132,156 @@ namespace PerfectWorld.UI.MiniMap
{
return CECGameRun.Instance.GetHostPlayer();
}
private void EnsureNPCMiniMapWatcher()
{
if (_npcMiniMapWatcherStarted)
return;
CECNPCMan npcMan = EC_ManMessageMono.Instance?.CECNPCMan;
if (npcMan == null)
return;
_npcMiniMapWatcherStarted = true;
_npcMiniMapCancellationTokenSource = new CancellationTokenSource();
CancellationToken token = _npcMiniMapCancellationTokenSource.Token;
UniTask.RunOnThreadPool(async () =>
{
await WatchNPCMiniMapData(npcMan, token);
}, false, cancellationToken: token).Forget();
}
private async UniTask WatchNPCMiniMapData(CECNPCMan npcMan, CancellationToken token)
{
List<CECNPCMan.NPCMiniMapData> lastNPCData = new();
try
{
while (!token.IsCancellationRequested)
{
List<CECNPCMan.NPCMiniMapData> currentNPCData = npcMan.GetAllNPCMiniMapData();
if (!IsSameNPCMiniMapData(lastNPCData, currentNPCData))
{
lock (_npcMiniMapRenderLock)
{
_pendingNPCMiniMapData = currentNPCData;
}
lastNPCData = currentNPCData;
_needRenderNPCMiniMap = true;
}
await UniTask.Delay(1000, cancellationToken: token);
}
}
catch (OperationCanceledException)
{
// Expected when the minimap is destroyed.
}
}
private static bool IsSameNPCMiniMapData(List<CECNPCMan.NPCMiniMapData> previous, List<CECNPCMan.NPCMiniMapData> current)
{
if (previous.Count != current.Count)
return false;
CECNPCMan.NPCMiniMapData oldData;
CECNPCMan.NPCMiniMapData newData;
for (int i = 0; i < current.Count; i++)
{
oldData = previous[i];
newData = current[i];
if (oldData.NPCID != newData.NPCID ||
oldData.TemplateID != newData.TemplateID ||
oldData.MapID != newData.MapID ||
oldData.Name != newData.Name ||
oldData.Position.x != newData.Position.x ||
oldData.Position.y != newData.Position.y ||
oldData.Position.z != newData.Position.z)
{
return false;
}
}
return true;
}
private void RenderNPCMiniMap()
{
List<CECNPCMan.NPCMiniMapData> npcData;
lock (_npcMiniMapRenderLock)
{
BMLogger.Log($"RenderNPCMiniMap: _pendingNPCMiniMapData.Count={_pendingNPCMiniMapData.Count}");
npcData = new List<CECNPCMan.NPCMiniMapData>(_pendingNPCMiniMapData);
}
m_vecNPCMark.Clear();
HashSet<int> activeNPCIds = new();
for (int i = 0; i < npcData.Count; i++)
{
CECNPCMan.NPCMiniMapData data = npcData[i];
// only show NPC_ESSENCE on the minimap
if (data.DataType != DATA_TYPE.DT_NPC_ESSENCE)
continue;
activeNPCIds.Add(data.NPCID);
m_vecNPCMark.Add(new MARK(data.NPCID, data.Name, data.Position, data.MapID));
Image npcImage = GetOrCreateNPCMiniMapImage(data.NPCID);
npcImage.rectTransform.anchoredPosition = new Vector2(data.Position.x * coordinateFactor, data.Position.z * coordinateFactor);
npcImage.gameObject.SetActive(true);
}
RemoveInactiveNPCMiniMapImages(activeNPCIds);
}
private Image GetOrCreateNPCMiniMapImage(int npcID)
{
if (_npcMiniMapImages.TryGetValue(npcID, out Image npcImage) && npcImage != null)
return npcImage;
if (_imageNPCMiniMapPrefab != null)
{
npcImage = Instantiate(_imageNPCMiniMapPrefab, _transformMiniMapParent);
}
else
{
GameObject npcIcon = new GameObject($"NPC_{npcID}", typeof(RectTransform), typeof(Image));
npcIcon.transform.SetParent(_transformMiniMapParent, false);
npcImage = npcIcon.GetComponent<Image>();
}
npcImage.name = $"NPC_{npcID}";
if (_npcMiniMapSprite != null)
npcImage.sprite = _npcMiniMapSprite;
npcImage.raycastTarget = false;
npcImage.rectTransform.sizeDelta = _npcMiniMapIconSize;
_npcMiniMapImages[npcID] = npcImage;
return npcImage;
}
private void RemoveInactiveNPCMiniMapImages(HashSet<int> activeNPCIds)
{
List<int> npcIdsToRemove = new();
foreach (KeyValuePair<int, Image> kvp in _npcMiniMapImages)
{
if (!activeNPCIds.Contains(kvp.Key))
{
if (kvp.Value != null)
Destroy(kvp.Value.gameObject);
npcIdsToRemove.Add(kvp.Key);
}
}
for (int i = 0; i < npcIdsToRemove.Count; i++)
{
_npcMiniMapImages.Remove(npcIdsToRemove[i]);
}
}
// change radar mode
public int GetMode() { return m_nMode; }
@@ -163,6 +341,10 @@ namespace PerfectWorld.UI.MiniMap
Destroy(child.gameObject);
}
_npcMiniMapImages.Clear();
m_vecNPCMark.Clear();
_needRenderNPCMiniMap = true;
Sprite pSprite = null;
for(int r = 0; r < nRow + 3; r++)
{
@@ -6,7 +6,7 @@ TextureImporter:
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,13 +37,13 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -126,7 +126,7 @@ TextureImporter:
customData:
physicsShape: []
bones: []
spriteID:
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
@@ -6,7 +6,7 @@ TextureImporter:
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,13 +37,13 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -126,7 +126,7 @@ TextureImporter:
customData:
physicsShape: []
bones: []
spriteID:
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
@@ -6,7 +6,7 @@ TextureImporter:
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,13 +37,13 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -126,7 +126,7 @@ TextureImporter:
customData:
physicsShape: []
bones: []
spriteID:
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices: