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; using UnityEngine.UI; namespace PerfectWorld.UI.MiniMap { public class CDlgMiniMap : AUIDialog { public struct MARK { public int nNPC; public string strName; public A3DVECTOR3 vecPos; public int mapID; // 地图ID // map ID (exposed for USER_LAYOUT save) public MARK(int nNPC, string strName, A3DVECTOR3 vecPos, int mapID) { this.nNPC = nNPC; this.strName = strName; this.vecPos = vecPos; this.mapID = mapID; } } [SerializeField] private Vector3 _debugHostPlayerPos; [SerializeField] private RectTransform _hostPlayerIcon; [SerializeField] private byte nRow, nCol; // number of rows and cols in the current map instances.txt [SerializeField] private TMP_Text txtHostPos; [SerializeField] private Image _imageMiniMapPrefab; [SerializeField] private List _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 [SerializeField] private SpriteAtlas _spriteAtlas; private List m_vecMark = new(); private List m_vecNPCMark = new(); private Dictionary m_TexMap = new(); private List _texToDelete = new(); // list of texture to delete from m_TexMap, use in update functions. private readonly Dictionary _npcMiniMapImages = new(); private readonly object _npcMiniMapRenderLock = new object(); private List _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; // map state private bool isShowMiniMap = true; CECHostPlayer m_pHostPlayer; private float coordinateFactor = 0.5f; // the factor to convert the world coordinates to the mini map coordinates. Vector3Int _lastIntHostPos = Vector3Int.zero; private int m_nMode; // TODO: currently, there is only get logic, not set logic // PC DlgMiniMap.cpp: GetStringFromTable(604), GetStringFromTable(1330 + nTimeIndex), FixFrame(nTimeItem) private const int StrIdSystemTimeFormat = 604; private const int StrIdShichenBase = 1330; private enum MinimapTimeSprite { TIME_DAY = 0, TIME_MORNING, TIME_DUSK, TIME_NIGHT, } /// 十二时辰名(表缺失时回退,与 PC 1330+i 顺序一致:子丑寅…) // Fallback 12 double-hours if string table missing (same order as PC 1330+i) static readonly string[] s_FallbackShichenNames = { "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥", // Zi, Chou, ... }; [Header("Set data show time in mini map")] [SerializeField] private TMP_Text _txtSystemTime; [SerializeField] private Image _imgSystemTime; [Tooltip("PC order: TIME_DAY, TIME_MORNING, TIME_DUSK, TIME_NIGHT (DlgMiniMap enum)")] [SerializeField] private Sprite[] _systemTimeSprites; private void Awake() { // LoadAllMiniMapTextures(); _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(); } UpdateSystemClockFromPcMiniMapLogic(); } /// /// We keep the player icon at the center of the minimap. Then we update the position of the map itself. /// TODO: We have to keep track of the NPC icons on the map also. /// private void UpdateMiniMap() { m_pHostPlayer = GetHostPlayer(); if (m_pHostPlayer == null) return; Transform hostTransform = m_pHostPlayer.transform; Vector3 vecPosHost = hostTransform.position; Vector3Int currentIntHostPos = new Vector3Int(Mathf.RoundToInt(vecPosHost.x) / 10 + 400, Mathf.RoundToInt(vecPosHost.y) / 10, Mathf.RoundToInt(vecPosHost.z) / 10 + 550); if (currentIntHostPos != _lastIntHostPos) { txtHostPos.text = $"{currentIntHostPos.x}, {currentIntHostPos.z}, ↑{currentIntHostPos.y}"; _lastIntHostPos = currentIntHostPos; } Vector2 hostPlayerPos = new Vector2(vecPosHost.x * coordinateFactor, vecPosHost.z * coordinateFactor); _transformMiniMapParent.anchoredPosition = -hostPlayerPos; _hostPlayerIcon.localRotation = Quaternion.Euler(0, 0, -hostTransform.localRotation.eulerAngles.y); } /// /// PC DlgMiniMap.cpp Render: nTimeIndex, Format(604,...), int(v*24), FixFrame(nTimeItem). /// void UpdateSystemClockFromPcMiniMapLogic() { var sun = CECSunMoon.Instance; if (sun == null) return; var inGame = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan(); float fDNFactor = sun.GetDNFactor(); float fDNFactorDest = sun.GetDNFactorDest(); float v = sun.GetTimeOfTheDay(); int nTimeIndex = (int)(12.0f * v + 0.5f) % 12; MinimapTimeSprite nTimeItem; if (fDNFactor == 0.0f) nTimeItem = MinimapTimeSprite.TIME_DAY; else if (fDNFactor == 1.0f) nTimeItem = MinimapTimeSprite.TIME_NIGHT; else nTimeItem = fDNFactorDest == 1.0f ? MinimapTimeSprite.TIME_DUSK : MinimapTimeSprite.TIME_MORNING; if (_imgSystemTime != null && _systemTimeSprites != null && _systemTimeSprites.Length > (int)nTimeItem && _systemTimeSprites[(int)nTimeItem] != null) _imgSystemTime.sprite = _systemTimeSprites[(int)nTimeItem]; string shichen = inGame != null ? GetStringFromTable(StrIdShichenBase + nTimeIndex) : null; if (string.IsNullOrEmpty(shichen)) shichen = s_FallbackShichenNames[nTimeIndex]; int hour = (int)(v * 24.0f); string fmt = inGame != null ? GetStringFromTable(StrIdSystemTimeFormat) : null; if (string.IsNullOrEmpty(fmt)) fmt = "{0}({1}时)"; // 与常见 PC 客户端格式一致 // Typical PC client style: name (hour) string strText = FormatPrintf(fmt, shichen, hour); if (_txtSystemTime != null) _txtSystemTime.text = strText; // BMLogger.Log($"[Cuong] {strText}"); } /// /// Get the Host Player instance. /// /// private CECHostPlayer GetHostPlayer() { 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 UpdateNPCMiniMapDataTask(npcMan, token); }, false, cancellationToken: token).Forget(); } private async UniTask UpdateNPCMiniMapDataTask(CECNPCMan npcMan, CancellationToken token) { List lastNPCData = new(); try { while (!token.IsCancellationRequested) { List currentNPCData = npcMan.GetAllNPCMiniMapData(); if (!IsSameNPCMiniMapData(lastNPCData, currentNPCData)) { lock (_npcMiniMapRenderLock) { _pendingNPCMiniMapData = currentNPCData; } lastNPCData.Clear(); lastNPCData.AddRange(currentNPCData); _needRenderNPCMiniMap = true; } await UniTask.Delay(1000, cancellationToken: token); } } catch (OperationCanceledException) { // Expected when the minimap is destroyed. } } private static bool IsSameNPCMiniMapData(List previous, List 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; } /// /// Render the following objects on the minimap:
/// - NPC_ESSENCE
///
private void RenderNPCMiniMap() { List npcData; lock (_npcMiniMapRenderLock) { BMLogger.Log($"RenderNPCMiniMap: _pendingNPCMiniMapData.Count={_pendingNPCMiniMapData.Count}"); npcData = new List(_pendingNPCMiniMapData); } m_vecNPCMark.Clear(); HashSet 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(); } 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 activeNPCIds) { List npcIdsToRemove = new(); foreach (KeyValuePair 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; } /// Returns the list of user-placed marks on the minimap (for layout save/load). public List GetMarks() => m_vecMark; /// User click on the minimap, we'll show the world map. public void OnMiniMapClicked() { var dlg = CECUIManager.Instance.ShowUI("Win_WorldMap") as DlgWorldMap; // dlg?.Show(true); // dlg?.OnInitDialog(); } /// /// Call this function when user enter the game world (after select role or when use GOTO to jump to a new instance). /// This function will get the world instance data and setup the mini map. /// public async void InitializeMiniMap() { // get current world instance var idInstance = CECGameRun.Instance?.GetWorld()?.GetInstanceID() ?? 161; var worldInstance = EC_Game.GetGameRun()?.GetInstance(idInstance); if (worldInstance == null) { BMLogger.LogError("InitializeMiniMap: worldInstance is null"); return; } // set the number of rows and columns of the mini map nRow = (byte)worldInstance.GetRowNum(); nCol = (byte)worldInstance.GetColNum(); // use Addressable to load all the textures of the mini map _spriteAtlas = await AddressableManager.Instance.LoadSpriteAtlasAsync($"minimaps/{idInstance}"); if (_spriteAtlas == null) { BMLogger.LogError("InitializeMiniMap: sprite atlas is null"); return; } LoadAllMiniMapTextures(); } // keep this so we can load all textures of other map also. [ContextMenu("LoadAllMiniMapTextures")] public void LoadAllMiniMapTextures() { // delete all images in parent foreach(Transform child in _transformMiniMapParent) { Destroy(child.gameObject); } _npcMiniMapImages.Clear(); m_vecNPCMark.Clear(); _needRenderNPCMiniMap = true; Sprite pSprite = null; for(int r = 0; r < nRow + 3; r++) { for(int c = 0; c < nCol + 3; c++) { string strIndex = $"{r:D2}{c:D2}"; pSprite = _spriteAtlas.GetSprite(strIndex); var image = Instantiate(_imageMiniMapPrefab, _transformMiniMapParent); image.sprite = pSprite; image.name = strIndex; image.gameObject.SetActive(true); } } } #if UNITY_EDITOR // this is for debuging/testing while this feature was in development [ContextMenu("MoveHostPlayerIconToPos")] public void MoveHostPlayerIconToPos() { Vector2 hostPlayerPos = new Vector2(_debugHostPlayerPos.x / 2, _debugHostPlayerPos.z / 2); _transformMiniMapParent.anchoredPosition = -hostPlayerPos; } #endif } }