450 lines
17 KiB
C#
450 lines
17 KiB
C#
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<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
|
||
[SerializeField] private SpriteAtlas _spriteAtlas;
|
||
|
||
private List<MARK> m_vecMark = new();
|
||
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;
|
||
|
||
// 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,
|
||
}
|
||
|
||
/// <summary>十二时辰名(表缺失时回退,与 PC 1330+i 顺序一致:子丑寅…) // Fallback 12 double-hours if string table missing (same order as PC 1330+i)</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// PC DlgMiniMap.cpp Render: nTimeIndex, Format(604,...), int(v*24), FixFrame(nTimeItem).
|
||
/// </summary>
|
||
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}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Get the Host Player instance.
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
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<CECNPCMan.NPCMiniMapData> lastNPCData = new();
|
||
|
||
try
|
||
{
|
||
while (!token.IsCancellationRequested)
|
||
{
|
||
List<CECNPCMan.NPCMiniMapData> 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<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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Render the following objects on the minimap: <br/>
|
||
/// - NPC_ESSENCE <br/>
|
||
/// </summary>
|
||
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; }
|
||
|
||
/// <summary>Returns the list of user-placed marks on the minimap (for layout save/load).</summary>
|
||
public List<MARK> GetMarks() => m_vecMark;
|
||
|
||
/// <summary>User click on the minimap, we'll show the world map.</summary>
|
||
public void OnMiniMapClicked()
|
||
{
|
||
var dlg = CECUIManager.Instance.ShowUI("Win_WorldMap") as DlgWorldMap;
|
||
// dlg?.Show(true);
|
||
// dlg?.OnInitDialog();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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
|
||
}
|
||
}
|