diff --git a/.codex/skills/th1-crashsight-daily/SKILL.md b/.codex/skills/th1-crashsight-daily/SKILL.md new file mode 100644 index 000000000..dc92b633c --- /dev/null +++ b/.codex/skills/th1-crashsight-daily/SKILL.md @@ -0,0 +1,107 @@ +--- +name: th1-crashsight-daily +description: TH1 project-specific daily CrashSight triage workflow for using the logged-in Chrome session to scan recent versions, inspect every CrashSight error, decode obfuscated Unity C# stacks, classify only direct exceptions or try/catch captured exceptions as blocking, and write Markdown blocking/debug reports under MD/. Use when the user asks for CrashSight daily reports, 最近一天异常扫描, version-scoped crash/error triage, blocking/debug report generation, or recurring production error review. +--- + +# TH1 CrashSight Daily + +## Core Rule + +Do not over-classify business `LogSystem.LogError` telemetry as blocking. + +Classify an issue as `blocking` only when one of these is true: + +- CrashSight issue type is a real exception, such as `NullReferenceException`, `KeyNotFoundException`, `InvalidOperationException`, `ArgumentNullException`, `MemoryPackSerializationException`, `DllNotFoundException`, etc. +- The visible message or detail page contains a try/catch captured exception object or stack, such as `System.*Exception`, `异常类型`, `异常信息`, `调用堆栈`, `error: System...`, `failed: System...`, `ex: Object reference`, or `at Namespace.Type.Method(...)`. +- A `UnityLogError` is clearly wrapping an exception caught by code, for example `OnMessageReceived 处理失败, error: System.NullReferenceException...`, `Timer任务执行异常: 异常类型: ...`, or `EventManager Publish<...> listener failed: System...`. + +Classify as `debug` when the issue is only a plain project log or diagnostic state, even if it sounds serious: + +- Player/map/action mismatch logs without an exception object, such as `CompleteExecute Player 不一致`, `Map不一致`, `OnReceivedActionExcute Player 不一致`. +- AI/action diagnostics such as `存在相似action`, `不应该出现在...`, `CheckCan No`, `ActionConfirm send failed`, unless a concrete exception stack is present. +- Networking/environment/send telemetry such as STS/OSS failures, P2P send/connect failures, lobby failures, ForceUpdate/request logs, player-net mapping failure logs, Steam not logged in. +- UI/prefab guard logs such as `CityInfoMono.SetCulture: ... is null` or `FragmentDie: UnitRenderer 为空` when they are plain guard logs, not thrown/caught exceptions. +- Save/file/Workshop/local environment logs when they do not include an exception stack. + +When uncertain after quick preview, open the detail page. If the detail still does not contain a concrete exception object/stack, keep it in `debug` and record the code location only. + +## Workflow + +1. Use the Chrome skill, not the in-app browser, because CrashSight requires the user's logged-in Chrome session. +2. Open the CrashSight errors URL from the user or the previous daily URL. +3. Filter scope: + - status: open/processing, usually `status=0,2`. + - exception category: `ERROR`. + - date: last 1 day. + - versions: use the two user-specified recent versions; if unspecified, inspect the version dropdown and choose the newest two target release versions only. Avoid broad wildcard ranges unless the user explicitly asks. +4. Capture all list pages, increasing `rows` when possible, and dedupe by Issue ID. Store raw captured rows under `Temp/CrashSight/Daily_/`. +5. Inspect issues one by one: + - Use quick preview when it shows full message and stack. + - Open issue detail when preview is truncated, message is `-`, the stack lacks symbols, or classification depends on whether a real exception is present. +6. Decode obfuscated online stacks before code search: + - Use `Tools/DecodeOnlineError.ps1` or `Tools/ObfuscatedExceptionDecoder.ps1`. + - Decode all blocking candidates and any debug rows that need code location from an obfuscated stack. +7. Locate code with `rg` and the decoded symbols. Prefer exact method/class names first, then stable message strings. +8. Generate Markdown under `MD/CrashSight___1day/`. + +## Output Layout + +Create this structure: + +```text +MD/CrashSight____1day/ +├── index.md +├── debug_summary.md +├── report_manifest.json +└── blocking/ + ├── 001_issue_.md + └── ... +``` + +Use a filesystem-safe version suffix, for example `0.7.1k_0.7.1j`. + +`index.md` must include: + +- filter scope and capture time. +- CrashSight total seen and deduped rows. +- blocking issue count/occurrence count. +- debug issue count/occurrence count. +- blocking family table sorted by occurrence count. +- top blocking issues with links to per-issue reports. + +`debug_summary.md` must include: + +- debug category summary with counts, occurrences, code locations, and example Issue IDs. +- debug detail table for every debug Issue. +- no trigger-cause analysis and no business fix explanation. + +Each `blocking/*.md` must include: + +- Issue ID, CrashSight URL, type, versions, first/last seen, count. +- raw message and key stack. +- decoded stack or decoded log text. +- code location with file paths and line numbers when possible. +- trigger reason and why it is blocking. +- focused recommendation. + +`report_manifest.json` must mirror the final classification and counts so a later run can audit changes. + +## Classification Audit + +Before finalizing, run a text audit over the generated results: + +- Verify every `blocking` issue either has a non-`UnityLogError` exception type or contains a concrete caught exception/stack in the message/detail. +- Search debug rows for `System.*Exception`, `异常类型`, `调用堆栈`, `Object reference`, `KeyNotFoundException`, `ArgumentNullException`; promote only those with real exception context. +- Search blocking rows for plain telemetry strings like `Player 不一致`, `Map不一致`, `存在相似action`, `ForceUpdate 玩家网络映射失败`, `安全写入失败`, `P2P message send failed`; demote them unless they also include a real exception object/stack. +- Confirm `blocking/*.md` count equals `report_manifest.json.blockingReports.length`. + +## Reporting Back + +In the final response, provide: + +- link to `index.md`. +- final blocking/debug counts. +- a short note that plain `LogSystem.LogError` diagnostics were kept in debug unless they wrapped an actual exception. +- any limitations, such as rows that required detail pages but still had no full stack. + +At the end of Chrome automation, close/finalize browser tabs according to the Chrome skill instructions. diff --git a/.codex/skills/th1-crashsight-daily/agents/openai.yaml b/.codex/skills/th1-crashsight-daily/agents/openai.yaml new file mode 100644 index 000000000..87c0d230b --- /dev/null +++ b/.codex/skills/th1-crashsight-daily/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "TH1 CrashSight Daily" + short_description: "Daily CrashSight triage for TH1 reports" + default_prompt: "Use $th1-crashsight-daily to scan recent CrashSight errors and write blocking/debug reports." diff --git a/Unity/Assets/Scripts/TH1_Data/MapData.cs b/Unity/Assets/Scripts/TH1_Data/MapData.cs index 1696fb81f..3dab3545f 100644 --- a/Unity/Assets/Scripts/TH1_Data/MapData.cs +++ b/Unity/Assets/Scripts/TH1_Data/MapData.cs @@ -114,15 +114,22 @@ namespace RuntimeData // 旧存档兼容:WaterType 字段不存在时默认为 Pangea if (!System.Enum.IsDefined(typeof(Logic.MapWaterType), WaterType)) WaterType = Logic.MapWaterType.Pangea; + MultiCivs ??= new List(); + PlayerSettlements ??= new List(); + MatchLimits ??= new List(); + RefreshMultiCivsDict(); } // 根据房间成员信息更新 mapconfig 信息 - public void UpdateLobbyMember(Dictionary memberInfos) + public bool UpdateLobbyMember(Dictionary memberInfos) { + if (memberInfos == null) return false; + MultiCivs ??= new List(); + var changed = false; // 先剔除已离开 lobby 的成员,避免 MultiCivs 留下幽灵占位: // 旧版只 Add 不 Remove,会导致大厅 nP 跳号(1P/4P/5P/6P), // 以及开战时给离线幽灵创 PlayerData、占用 PlayerCount 名额、AI 补位变少。 - MultiCivs.RemoveAll(mc => !memberInfos.ContainsKey(mc.MemberId)); + changed |= MultiCivs.RemoveAll(mc => mc == null || !memberInfos.ContainsKey(mc.MemberId)) > 0; RefreshMultiCivsDict(); foreach (var kv in memberInfos) { @@ -132,22 +139,31 @@ namespace RuntimeData civ.CivId = 0; civ.ForceId = 0; MultiCivs.Add(civ); + changed = true; } RefreshMultiCivsDict(); + return changed; } // 内部刷新 private void RefreshMultiCivsDict() { - if (_memberCivs.Count == MultiCivs.Count) return; + _memberCivs ??= new Dictionary(); + MultiCivs ??= new List(); _memberCivs.Clear(); - foreach (var memberCiv in MultiCivs) _memberCivs[memberCiv.MemberId] = memberCiv; + foreach (var memberCiv in MultiCivs) + { + if (memberCiv == null) continue; + _memberCivs[memberCiv.MemberId] = memberCiv; + } } public MemberCiv GetMemberCiv(ulong memberId) { + MultiCivs ??= new List(); foreach (var memberCiv in MultiCivs) { + if (memberCiv == null) continue; if (memberCiv.MemberId == memberId) return memberCiv; } @@ -155,22 +171,73 @@ namespace RuntimeData } // 主从端一致的更新某一个成员信息 - public void UpdateMemberCiv(MemberCiv civ) + public bool UpdateMemberCiv(MemberCiv civ) { + if (civ == null) return false; if (LobbyManager.Instance.Lobby.IsInLobby() && !LobbyManager.Instance.Lobby.IsLobbyOwner()) { - GameNetSender.Instance.ChangeCiv(civ); - return; + var selfMemberId = LobbyManager.Instance.Lobby.GetSelfMemberId(); + if (civ.MemberId != selfMemberId) + { + LogSystem.LogError($"客户端只能修改自己的阵营: self={selfMemberId}, target={civ.MemberId}"); + return false; + } + + if (!GameNetSender.Instance.ChangeCiv(civ)) return false; + ApplyMemberCivLocal(civ); + return true; } + if (LobbyManager.Instance.Lobby.IsInLobby() && !LobbyManager.Instance.Lobby.IsMemberInLobby(civ.MemberId)) + { + LogSystem.LogError($"不能修改不在房间内的成员阵营: target={civ.MemberId}"); + return false; + } + + return ApplyMemberCivLocal(civ); + } + + private bool ApplyMemberCivLocal(MemberCiv civ) + { + MultiCivs ??= new List(); foreach (var memberCiv in MultiCivs) { + if (memberCiv == null) continue; if (memberCiv.MemberId != civ.MemberId) continue; + if (memberCiv.CivId == civ.CivId && memberCiv.ForceId == civ.ForceId) return false; memberCiv.CivId = civ.CivId; memberCiv.ForceId = civ.ForceId; - return; + memberCiv.PlayerId = civ.PlayerId; + RefreshMultiCivsDict(); + return true; } - MultiCivs.Add(civ); + + MultiCivs.Add(new MemberCiv + { + MemberId = civ.MemberId, + CivId = civ.CivId, + ForceId = civ.ForceId, + PlayerId = civ.PlayerId + }); + RefreshMultiCivsDict(); + return true; + } + + public bool HasSameLobbyMembers(Dictionary memberInfos) + { + if (memberInfos == null) return false; + MultiCivs ??= new List(); + if (MultiCivs.Count != memberInfos.Count) return false; + + var seen = new HashSet(); + foreach (var memberCiv in MultiCivs) + { + if (memberCiv == null) return false; + if (!memberInfos.ContainsKey(memberCiv.MemberId)) return false; + if (!seen.Add(memberCiv.MemberId)) return false; + } + + return seen.Count == memberInfos.Count; } // 主从端一致的本地数据检测 diff --git a/Unity/Assets/Scripts/TH1_Logic/Core/GameLogic.cs b/Unity/Assets/Scripts/TH1_Logic/Core/GameLogic.cs index bfa6b8e60..5da4ac3dc 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Core/GameLogic.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Core/GameLogic.cs @@ -377,12 +377,18 @@ namespace Logic { if (LobbyManager.Instance.Lobby.IsLobbyOwner()) { - Main.Instance.MapConfig.UpdateLobbyMember(LobbyManager.Instance.Lobby.GetAllMemberInfo()); + if (Main.Instance.MapConfig.UpdateLobbyMember(LobbyManager.Instance.Lobby.GetAllMemberInfo())) + Main.Instance.MapConfig.CheckMapConfigChanged(); } else { - if (LobbyManager.Instance.Lobby.GetAllMemberInfo().Count != Main.Instance.MapConfig.MultiCivs.Count) + var memberInfos = LobbyManager.Instance.Lobby.GetAllMemberInfo(); + if (GameNetSender.Instance.NeedsLobbyDataFromHost() + || !Main.Instance.MapConfig.HasSameLobbyMembers(memberInfos)) + { + GameNetSender.Instance.MarkLobbyDataSyncRequired(); GameNetSender.Instance.RequestLobbyData(); + } } if (_recordTime > 1f) diff --git a/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs b/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs index c56cf2e64..0a1549dd3 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs @@ -350,8 +350,8 @@ namespace TH1_Logic.Steam } if (!LobbyManager.Instance.Lobby.IsLobbyOwner()) return; if (message.Civ == null) return; - Main.Instance.MapConfig.UpdateMemberCiv(message.Civ); - Main.Instance.MapConfig.CheckMapConfigChanged(); + if (Main.Instance.MapConfig.UpdateMemberCiv(message.Civ)) + Main.Instance.MapConfig.CheckMapConfigChanged(); } // 只有玩家会收到 @@ -365,6 +365,15 @@ namespace TH1_Logic.Steam if (LobbyManager.Instance.Lobby.IsLobbyOwner()) return; if (message.Config == null) return; Main.Instance.MapConfig = message.Config; + if (Main.Instance.MapConfig.HasSameLobbyMembers(LobbyManager.Instance.Lobby.GetAllMemberInfo())) + { + GameNetSender.Instance.MarkLobbyDataSyncedFromHost(); + } + else + { + GameNetSender.Instance.MarkLobbyDataSyncRequired(); + GameNetSender.Instance.RequestLobbyData(); + } EventManager.Publish(new UpdateUIOutsideMultiplayRoomSetting()); } @@ -377,6 +386,8 @@ namespace TH1_Logic.Steam return; } if (!LobbyManager.Instance.Lobby.IsLobbyOwner()) return; + if (Main.Instance.MapConfig.UpdateLobbyMember(LobbyManager.Instance.Lobby.GetAllMemberInfo())) + Main.Instance.MapConfig.CheckMapConfigChanged(); GameNetSender.Instance.SendLobbyData(Main.Instance.MapConfig, message.MemberId); } diff --git a/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs b/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs index 8518a9cc1..fa6856caa 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs @@ -11,6 +11,7 @@ using Logic.Action; using Logic.AI; using Logic.CrashSight; using RuntimeData; +using Steamworks; using TH1_Logic.Chat; using TH1_Logic.Core; using TH1_Logic.Net; @@ -26,6 +27,7 @@ namespace TH1_Logic.Steam private const float RequestForceUpdateCooldown = 5f; private float _lastRequestLobbyDataTime = -RequestLobbyDataCooldown; private float _lastRequestForceUpdateTime = -RequestForceUpdateCooldown; + private bool _needLobbyDataFromHost; // 发送消息给房主 public bool SendMessage(BaseMessage message) @@ -194,17 +196,19 @@ namespace TH1_Logic.Steam } // 修改阵营 (成员 => 房主) - public void ChangeCiv(MemberCiv memberCiv) + public bool ChangeCiv(MemberCiv memberCiv) { if (memberCiv == null) { LogSystem.LogError($"Get Self MemberCiv Error "); - return; + return false; } var data = new ChangeCivMessage(); data.Civ = memberCiv; - SendMessage(data); + if (!SendMessage(data)) return false; + MarkLobbyDataSyncRequired(); + return true; } // 更新房间配置 (房主 => 所有成员) @@ -217,15 +221,45 @@ namespace TH1_Logic.Steam } // 请求更新房间配置 (单成员 => 房主) - public void RequestLobbyData() + public bool RequestLobbyData(bool force = false) { + if (LobbyManager.Instance.Lobby.IsLobbyOwner()) return false; + if (!LobbyManager.Instance.Lobby.IsInLobby()) return false; + var hostId = LobbyManager.Instance.Lobby.GetLobbyOwnerId(); + if (hostId == 0 || !SimpleP2P.Instance.IsConnectedTo(new CSteamID(hostId))) return false; + var now = UnityEngine.Time.time; - if (now - _lastRequestLobbyDataTime < RequestLobbyDataCooldown) return; + if (!force && now - _lastRequestLobbyDataTime < RequestLobbyDataCooldown) return false; _lastRequestLobbyDataTime = now; var data = new RequestLobbyDataMessage(); data.MemberId = LobbyManager.Instance.Lobby.GetSelfMemberId(); - SendMessage(data); + if (SendMessage(data)) return true; + + _lastRequestLobbyDataTime = now - RequestLobbyDataCooldown + 0.2f; + return false; + } + + public void MarkLobbyDataSyncRequired() + { + _needLobbyDataFromHost = true; + _lastRequestLobbyDataTime = -RequestLobbyDataCooldown; + } + + public void MarkLobbyDataSyncedFromHost() + { + _needLobbyDataFromHost = false; + } + + public void ClearLobbyDataSyncState() + { + _needLobbyDataFromHost = false; + _lastRequestLobbyDataTime = -RequestLobbyDataCooldown; + } + + public bool NeedsLobbyDataFromHost() + { + return _needLobbyDataFromHost; } // 更新房间配置 (房主 => 单成员) diff --git a/Unity/Assets/Scripts/TH1_Logic/Steam/SteamLobbyManager.cs b/Unity/Assets/Scripts/TH1_Logic/Steam/SteamLobbyManager.cs index aa1b51dde..487d9d7d2 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Steam/SteamLobbyManager.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Steam/SteamLobbyManager.cs @@ -510,6 +510,7 @@ namespace TH1_Logic.Steam // 断开所有P2P连接 SimpleP2P.Instance.DisconnectAll(); SteamMatchmaking.LeaveLobby(CurrentLobby); + GameNetSender.Instance.ClearLobbyDataSyncState(); ResetLobbyState(); OnLobbyLeftEvent?.Invoke(null); } @@ -1019,6 +1020,15 @@ namespace TH1_Logic.Steam CheckIfKicked(); OnLobbyReadyInternal(); OnLobbyMembersChangedInternal(); + if (IsLobbyOwner()) + { + GameNetSender.Instance.ClearLobbyDataSyncState(); + } + else + { + GameNetSender.Instance.MarkLobbyDataSyncRequired(); + GameNetSender.Instance.RequestLobbyData(true); + } // 触发加入成功事件 OnLobbyEnteredEvent?.Invoke(CurrentLobby); @@ -1147,7 +1157,18 @@ namespace TH1_Logic.Steam // P2P事件处理 private void OnP2PPeerConnected(CSteamID steamID) { - if (IsLobbyOwner()) Main.Instance.GameLogic.OnConnectToOtherPlayer(steamID.m_SteamID); + if (IsLobbyOwner()) + { + Main.Instance.GameLogic.OnConnectToOtherPlayer(steamID.m_SteamID); + if (Main.Instance.GameLogic.GetCurState() == GameState.Menu && Main.Instance.MapConfig != null) + { + if (Main.Instance.MapConfig.UpdateLobbyMember(GetAllMemberInfo())) + Main.Instance.MapConfig.CheckMapConfigChanged(); + GameNetSender.Instance.SendLobbyData(Main.Instance.MapConfig, steamID.m_SteamID); + } + } + else if (steamID.m_SteamID == GetLobbyOwnerId()) + GameNetSender.Instance.RequestLobbyData(true); LogSystem.LogInfo($"P2P connection established with: {steamID}"); } diff --git a/Unity/Assets/Scripts/TH1_Renderer/MapRenderer.cs b/Unity/Assets/Scripts/TH1_Renderer/MapRenderer.cs index 8a2d897e5..d5c1ede45 100644 --- a/Unity/Assets/Scripts/TH1_Renderer/MapRenderer.cs +++ b/Unity/Assets/Scripts/TH1_Renderer/MapRenderer.cs @@ -435,13 +435,20 @@ namespace TH1Renderer public void RenderUpdateUnitMap() { foreach (var unitData in Main.MapData.UnitMap.UnitList) + { + if (unitData == null) + continue; if(!ROUnitMap.ContainsKey(unitData.Id)) { //生成单位图像 - ROUnitMap[unitData.Id] = new UnitRenderer(_unitPrefab,_unitRenderMap,unitData.Id); + var unitRenderer = new UnitRenderer(_unitPrefab,_unitRenderMap,unitData.Id); + if (!unitRenderer.IsValid) + continue; + ROUnitMap[unitData.Id] = unitRenderer; //立刻更新每个unit的视觉 - ROUnitMap[unitData.Id].InstantUpdateUnit(true); + unitRenderer.InstantUpdateUnit(true); } + } } //当projectileMap出现新的对象时,新建对象 @@ -486,9 +493,13 @@ namespace TH1Renderer // 2) 补齐缺失:数据层有但 ROUnitMap 没有的 foreach (var unitData in mapData.UnitMap.UnitList) { + if (unitData == null) + continue; if (!ROUnitMap.ContainsKey(unitData.Id)) { - ROUnitMap[unitData.Id] = new UnitRenderer(_unitPrefab, _unitRenderMap, unitData.Id); + var unitRenderer = new UnitRenderer(_unitPrefab, _unitRenderMap, unitData.Id); + if (unitRenderer.IsValid) + ROUnitMap[unitData.Id] = unitRenderer; } } diff --git a/Unity/Assets/Scripts/TH1_Renderer/Prefab/UnitMono.cs b/Unity/Assets/Scripts/TH1_Renderer/Prefab/UnitMono.cs index 3d4faa64a..c15199c4c 100644 --- a/Unity/Assets/Scripts/TH1_Renderer/Prefab/UnitMono.cs +++ b/Unity/Assets/Scripts/TH1_Renderer/Prefab/UnitMono.cs @@ -62,6 +62,7 @@ public class UnitMono : MonoBehaviour public void UpdateUnitDefense(float defenseBonus) { + if (Defense == null || SuperDefense == null || InfoGroup == null) return; bool defense = Defense.activeSelf; bool superdefense = SuperDefense.activeSelf; bool noDefense = !defense && !superdefense; diff --git a/Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs b/Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs index 90589cc42..578a33a84 100644 --- a/Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs +++ b/Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs @@ -23,6 +23,42 @@ namespace TH1_Renderer private GameObject _ROUnit; private UnitMono _unitMono; + public bool IsValid => _ROUnit != null && _unitMono != null && _unitMono.SpriteRenderer != null && _unitData != null; + + private bool TryRefreshUnitRefs(bool requirePlayer = false, bool requireGrid = false) + { + var mapData = Main.MapData; + if (mapData == null || mapData.UnitMap == null) + { + _unitData = null; + _playerData = null; + _gridData = null; + return false; + } + + if (!mapData.UnitMap.GetUnitDataByUnitId(_unitId, out _unitData) || _unitData == null) + { + _unitData = null; + _playerData = null; + _gridData = null; + return false; + } + + if (requirePlayer && (!mapData.GetPlayerDataByUnitId(_unitId, out _playerData) || _playerData == null)) + { + _playerData = null; + return false; + } + + if (requireGrid && (!mapData.GetGridDataByUnitId(_unitId, out _gridData) || _gridData == null)) + { + _gridData = null; + return false; + } + + return true; + } + //------- 表现层RenderData ---------// public bool IsAttackHighlight = false; public bool IsSelectHighlight = false; @@ -90,15 +126,32 @@ namespace TH1_Renderer public UnitRenderer(GameObject prefab,Transform father, uint uid) { _unitId = uid; - Main.MapData.UnitMap.GetUnitDataByUnitId(uid,out _unitData); - Main.MapData.GetPlayerDataByUnitId(uid, out _playerData); - Main.MapData.GetGridDataByUnitId(uid,out _gridData); AnimManager = new UnitAnimManager(); - Vector3 tpos = Table.Instance.GridToWorld(_gridData,"isUnit"); + var table = Table.Instance; + if (!TryRefreshUnitRefs(requirePlayer: true, requireGrid: true) || prefab == null || father == null || table == null) + { + _unitData = null; + _playerData = null; + _gridData = null; + return; + } + + Vector3 tpos = table.GridToWorld(_gridData,"isUnit"); _ROUnit = GameObject.Instantiate(prefab, tpos, Quaternion.identity, father); - _unitMono = _ROUnit?.GetComponent(); + _unitMono = _ROUnit != null ? _ROUnit.GetComponent() : null; + if (_unitMono == null || _unitMono.SpriteRenderer == null) + { + if (_ROUnit != null) + GameObject.Destroy(_ROUnit); + _ROUnit = null; + _unitMono = null; + _unitData = null; + _playerData = null; + _gridData = null; + return; + } // 初始化 StatusArea if (_unitMono?.StatusAreaContainer != null && _unitMono?.StatusIconPrefab != null) @@ -121,11 +174,17 @@ namespace TH1_Renderer // 清理状态区域 _statusArea?.ClearAllStatus(); - GameObject.Destroy(_ROUnit.gameObject); + if (_ROUnit != null) + GameObject.Destroy(_ROUnit.gameObject); - if(MapRenderer.Instance.ROUnitMap.TryGetValue(_unitId,out var _)) - MapRenderer.Instance.ROUnitMap.Remove(_unitId); + var mapRenderer = MapRenderer.Instance; + if (mapRenderer?.ROUnitMap != null) + mapRenderer.ROUnitMap.Remove(_unitId); _unitData = null; + _playerData = null; + _gridData = null; + _unitMono = null; + _statusArea = null; } #region [-------------------- Status Area Management --------------------] @@ -190,17 +249,20 @@ namespace TH1_Renderer public void InstantDisappear() { + if (_ROUnit == null) return; _ROUnit.SetActive(false); } public void InstantShow() { + if (_ROUnit == null) return; _ROUnit.SetActive(true); } public void Update() { - AnimManager?.Update(_unitMono); + if (_unitMono != null) + AnimManager?.Update(_unitMono); UpdateShenlanTint(); } @@ -208,7 +270,7 @@ namespace TH1_Renderer // 只改 RGB,保留 alpha(HideState 用 alpha 控制半透明)。 private void UpdateShenlanTint() { - if (_unitMono?.SpriteRenderer == null) return; + if (_unitMono == null || _unitMono.SpriteRenderer == null) return; if (_unitData == null || !_unitData.IsAlive()) { if (_shenlanTinted) @@ -240,13 +302,14 @@ namespace TH1_Renderer public void RenderUpdateUnitDefense() { - if (_unitData == null || !_unitData.IsAlive()) return; - _unitMono.UpdateUnitDefense(_unitData.GetDefenseMultiplicationParamOnlyForDefenseShow(Main.MapData)); + var mapData = Main.MapData; + if (_unitMono == null || mapData == null || !TryRefreshUnitRefs() || !_unitData.IsAlive()) return; + _unitMono.UpdateUnitDefense(_unitData.GetDefenseMultiplicationParamOnlyForDefenseShow(mapData)); } public void RenderUpdateUnitImage() { - if (_unitData == null ) return; + if (_unitMono == null || Main.MapData == null || !TryRefreshUnitRefs()) return; RenderUpdateUnitInfo(); @@ -269,6 +332,7 @@ namespace TH1_Renderer //如果unit死了,不能直接die!!要等动画那边主动凋起才可以die public bool InstantUpdateUnit(bool showoff) { + if (_unitMono == null || !TryRefreshUnitRefs()) return false; //如果要做显隐更新,先判断显隐,显的情况下,再更新image //如果不做显隐更新,直接更新image if ((showoff && RenderUpdateUnitShowOff()) @@ -281,7 +345,7 @@ namespace TH1_Renderer //瞬间更新unit的 die的情况 public bool InstantUpdateTryDie() { - if (_unitData == null || !_unitData.IsAlive()) + if (!TryRefreshUnitRefs() || !_unitData.IsAlive()) { Die(); return true; @@ -298,6 +362,7 @@ namespace TH1_Renderer public void RenderUpdateUnitInfo() { + if (_unitMono == null || !TryRefreshUnitRefs() || Table.Instance?.UnitTypeDataAssets == null) return; if (!_unitMono?.HealthText || !_unitMono?.UnitInfoBG) return; _unitMono.HealthText.text = _unitData.Health.ToString(); //处理血量的颜色。如果血量<一半且<5,那么赋予红色,否则白色 @@ -306,30 +371,33 @@ namespace TH1_Renderer else _unitMono.HealthText.color = Color.white; - Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(_unitData.UnitFullType, out var unitInfo); + if (!Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(_unitData.UnitFullType, out var unitInfo)) + return; var chessType = unitInfo.ChessType; if (chessType == ChessType.None) { if(Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(_unitData.CarryUnitFullType, out var carryUnitInfo)) chessType = carryUnitInfo.ChessType; } - if (chessType != ChessType.None) + if (chessType != ChessType.None && _unitMono.ChessImg != null) { - Table.Instance.UnitTypeDataAssets.GetChessTypeInfo(chessType ,out var chessInfo); - _unitMono.ChessImg.sprite = chessInfo.ChessSprite; + if (Table.Instance.UnitTypeDataAssets.GetChessTypeInfo(chessType ,out var chessInfo)) + _unitMono.ChessImg.sprite = chessInfo.ChessSprite; } //根据敌我情况更新infoBG的颜色 //_unitInfoBGImg.sprite = (Main.MapData.SameUnion(_playerData.Id , Main.MapData.PlayerMap.SelfPlayerId)) ? ResourceCache.Instance.SpriteCache.UnitInfoSelf : if (Main.MapData == null) return; + if (Main.MapData.PlayerMap == null) return; if (!Main.MapData.GetPlayerDataByUnitId(_unitData.Id, out var playerData)) return; var col = _unitMono.UnitBGBlue; if (playerData.Id != Main.MapData.PlayerMap.SelfPlayerId) col =(Main.MapData.SameUnion(playerData.Id, Main.MapData.PlayerMap.SelfPlayerId)) ? _unitMono.UnitBGGreen : _unitMono.UnitBGRed; - _unitMono.ChessBG.color = col; + if (_unitMono.ChessBG != null) + _unitMono.ChessBG.color = col; _unitMono.UnitInfoBG.color = col; @@ -358,7 +426,9 @@ namespace TH1_Renderer } //更改兵种显示文字 - if(Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(_unitData.UnitType,_unitData.GiantType,_unitData.UnitLevel,out var info)) + if(_unitMono.UnitInfoName != null + && MultilingualManager.Instance != null + && Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(_unitData.UnitType,_unitData.GiantType,_unitData.UnitLevel,out var info)) MultilingualManager.Instance.SetUIText(_unitMono.UnitInfoName,info.Name); SyncStatusWithUnitSkills(); @@ -366,6 +436,7 @@ namespace TH1_Renderer public void RenderUpdateDebug() { + if (_unitMono == null || !TryRefreshUnitRefs(requirePlayer: true) || _unitMono.RODebugText == null || _unitMono.DebugText == null || Main.MapData?.PlayerMap?.SelfPlayerData == null) return; if (DebugCenter.Instance.DebugMode) { if(!_unitMono.RODebugText.activeSelf) @@ -383,20 +454,24 @@ namespace TH1_Renderer //如果不是我方单位,显示军团及unit的战略 _unitMono.DebugText.text += $"Lid={_unitData.LegionId}\n"; if(!Main.MapData.CheckUnitIdBelongPlayerId(_unitData.Id,Main.MapData.PlayerMap.SelfPlayerData.Id)) - if (MainEditor.Instance.Data != null) + { + var mainEditor = MainEditor.Instance; + if (mainEditor?.Data != null) { - MainEditor.Instance.GetUnitStrategy(_unitId, _unitData.LegionId, _playerData.Id, out var st, + mainEditor.GetUnitStrategy(_unitId, _unitData.LegionId, _playerData.Id, out var st, out var tar, out var type); _unitMono.DebugText.text += $"ST:{st} TAR:{tar} TYPE:{type}"; } + } } public bool RenderUpdateUnitShowOff() { - if (_unitData == null || _unitMono == null) + var mapData = Main.MapData; + if (_unitMono == null || mapData == null || mapData.PlayerMap == null || mapData.PlayerMap.SelfPlayerData == null || !TryRefreshUnitRefs()) return false; bool ret = _unitData.InMainSight(); //如果在视野内但是敌方隐身单位,对当前玩家不可见 - if (ret && _unitData.IsHideAndCantSee(Main.MapData, Main.MapData.PlayerMap.SelfPlayerData)) + if (ret && _unitData.IsHideAndCantSee(mapData, mapData.PlayerMap.SelfPlayerData)) ret = false; //由隐转显时,先把 transform 同步到当前 grid,避免显示在过期位置 //(敌方隐身单位全程 SetActive(false),RenderUpdateUnitPosition 没机会跑) @@ -407,10 +482,14 @@ namespace TH1_Renderer } public void RenderUpdateUnitGlow() { - var player = _unitData.Player(Main.MapData); + var mapData = Main.MapData; + var table = Table.Instance; + if (_unitMono == null || _unitMono.SpriteRenderer == null || mapData == null || table == null || table.UnitTypeDataAssets == null || ResourceCache.Instance?.MatCache == null || !TryRefreshUnitRefs()) + return; + var player = _unitData.Player(mapData); if (player == null) return; Sprite sprite; - if (!Table.Instance.UnitTypeDataAssets.GetUnitSprite(Main.MapData, _unitData, out sprite)) + if (!table.UnitTypeDataAssets.GetUnitSprite(mapData, _unitData, out sprite)) return; _unitMono.SpriteRenderer.sprite = sprite; _unitMono.SpriteRenderer.material = ResourceCache.Instance.MatCache.TH1URPShaders_Default; @@ -419,10 +498,11 @@ namespace TH1_Renderer //首先处理玩家(判断是否置灰或者高亮) if (player.IsSelfPlayer()) { + var mapRenderer = MapRenderer.Instance; //如果MP>0 或者周围有可以攻击的目标,或者可以移动的目标,或者说可以占领城市 if (_unitData.GetActionPoint(ActionPointType.Move) > 0 - || MapRenderer.Instance.CheckUnitHasMoveAttackTarget(_unitId) - || MapRenderer.Instance.CheckUnitHasSpecialUnitActionTarget(_unitId)) + || (mapRenderer != null && mapRenderer.CheckUnitHasMoveAttackTarget(_unitId)) + || (mapRenderer != null && mapRenderer.CheckUnitHasSpecialUnitActionTarget(_unitId))) { _unitMono.SpriteRenderer.material = ResourceCache.Instance.MatCache.TH1URPShaders_Sprite_Glow; _isGlow = true; @@ -435,14 +515,14 @@ namespace TH1_Renderer else { //如果是正在行动的AI - if (Main.MapData.CurPlayer == player) + if (mapData.CurPlayer == player) { if(_unitData.GetActionPoint(ActionPointType.Move) == 0 && _unitData.GetActionPoint(ActionPointType.Capture) == 0 && _unitData.GetActionPoint(ActionPointType.Attack) == 0 && _unitData.GetActionPoint(ActionPointType.Move) == 0) _unitMono.SpriteRenderer.material = ResourceCache.Instance.MatCache.TH1URPShaders_Sprite_WhiteOverlay; } else //如果是还没行动的AI - if(Main.MapData.GetPlayerHasActedInBigTurn(player)) + if(mapData.GetPlayerHasActedInBigTurn(player)) { //啥都不做 } @@ -459,8 +539,12 @@ namespace TH1_Renderer public void RenderUpdateHideState() { - bool hideState = _unitData.IsHideState(Main.MapData); - bool isSelfOrAlly = Main.MapData.SameUnion(_playerData.Id, Main.MapData.PlayerMap.SelfPlayerId); + var mapData = Main.MapData; + if (_unitMono == null || _unitMono.SpriteRenderer == null || mapData == null || mapData.PlayerMap == null || !TryRefreshUnitRefs(requirePlayer: true)) + return; + + bool hideState = _unitData.IsHideState(mapData); + bool isSelfOrAlly = mapData.SameUnion(_playerData.Id, mapData.PlayerMap.SelfPlayerId); // 状态未变时的处理:确保显示状态正确 if (hideState == _isHideState) @@ -516,22 +600,23 @@ namespace TH1_Renderer public void RenderUpdateHideAround() { - if (_unitMono.HideAround == null) return; + var mapData = Main.MapData; + if (_unitMono == null || _unitMono.HideAround == null || mapData?.GridMap == null || !TryRefreshUnitRefs(requirePlayer: true)) return; bool show = false; //只对当前玩家自己的单位显示 - var curGrid = _unitData.Grid(Main.MapData); + var curGrid = _unitData.Grid(mapData); if (_playerData != null && _playerData.IsSelfPlayer() && curGrid != null) { _aroundBuf ??= new List(); _aroundBuf.Clear(); - Main.MapData.GridMap.GetAroundGridData(1, 1, curGrid, _aroundBuf); + mapData.GridMap.GetAroundGridData(1, 1, curGrid, _aroundBuf); foreach (var around in _aroundBuf) { if (around == curGrid) continue; - if (!around.RealUnit(Main.MapData, out var nearUnit)) continue; - if (!nearUnit.IsHideState(Main.MapData)) continue; + if (!around.RealUnit(mapData, out var nearUnit)) continue; + if (!nearUnit.IsHideState(mapData)) continue; //排除自己的单位和同盟单位 - if (Main.MapData.SameUnionByUnitId(_unitData.Id, nearUnit.Id)) continue; + if (mapData.SameUnionByUnitId(_unitData.Id, nearUnit.Id)) continue; show = true; break; } @@ -559,9 +644,13 @@ namespace TH1_Renderer public void RenderUpdataHighlight() { - _unitMono.AttackHighlight.SetActive(IsAttackHighlight); - _unitMono.SelectHighlight.SetActive(IsSelectHighlight); - _unitMono.AllyHighlight.SetActive(IsAllyHighlight); + if (_unitMono == null) return; + if (_unitMono.AttackHighlight != null) + _unitMono.AttackHighlight.SetActive(IsAttackHighlight); + if (_unitMono.SelectHighlight != null) + _unitMono.SelectHighlight.SetActive(IsSelectHighlight); + if (_unitMono.AllyHighlight != null) + _unitMono.AllyHighlight.SetActive(IsAllyHighlight); } public void SetSelectHighlight(bool v) @@ -569,8 +658,10 @@ namespace TH1_Renderer if (IsSelectHighlight == v) return; IsSelectHighlight = v; - MapRenderer.Instance.HighlightUnitIdSet.Add(_unitId); - MapRenderer.Instance.HighlightUnitIdSetRenderMark = true; + var mapRenderer = MapRenderer.Instance; + if (mapRenderer?.HighlightUnitIdSet == null) return; + mapRenderer.HighlightUnitIdSet.Add(_unitId); + mapRenderer.HighlightUnitIdSetRenderMark = true; } public void SetAttackHighlight(bool v) @@ -578,8 +669,10 @@ namespace TH1_Renderer if (IsAttackHighlight == v) return; IsAttackHighlight = v; - MapRenderer.Instance.HighlightUnitIdSet.Add(_unitId); - MapRenderer.Instance.HighlightUnitIdSetRenderMark = true; + var mapRenderer = MapRenderer.Instance; + if (mapRenderer?.HighlightUnitIdSet == null) return; + mapRenderer.HighlightUnitIdSet.Add(_unitId); + mapRenderer.HighlightUnitIdSetRenderMark = true; } public void SetAllyHighlight(bool v) @@ -587,13 +680,20 @@ namespace TH1_Renderer if (IsAllyHighlight == v) return; IsAllyHighlight = v; - MapRenderer.Instance.HighlightUnitIdSet.Add(_unitId); - MapRenderer.Instance.HighlightUnitIdSetRenderMark = true; + var mapRenderer = MapRenderer.Instance; + if (mapRenderer?.HighlightUnitIdSet == null) return; + mapRenderer.HighlightUnitIdSet.Add(_unitId); + mapRenderer.HighlightUnitIdSetRenderMark = true; } public void RenderUpdateUnitSprite() { - if (!Table.Instance.UnitTypeDataAssets.GetUnitSprite(Main.MapData, _unitData, out var sprite)) + var mapData = Main.MapData; + var table = Table.Instance; + if (_unitMono == null || _unitMono.SpriteRenderer == null || mapData == null || table == null || table.UnitTypeDataAssets == null || !TryRefreshUnitRefs()) + return; + + if (!table.UnitTypeDataAssets.GetUnitSprite(mapData, _unitData, out var sprite) || sprite == null) return; _unitMono.SpriteRenderer.sprite = sprite; //RenderUpdateUnitSpecialSprite(); @@ -603,14 +703,19 @@ namespace TH1_Renderer public void RenderUpdateUnitPosition() { - var t = _unitData.Grid(Main.MapData)?.Pos; + var mapData = Main.MapData; + var table = Table.Instance; + if (_unitMono == null || mapData == null || table == null || !TryRefreshUnitRefs(requireGrid: true)) + return; + + var t = _unitData.Grid(mapData)?.Pos; if (t == null) return; - _unitMono.transform.position = Table.Instance.GridPosToWorld(new Vector2Int(t.X,t.Y),"isUnit"); + _unitMono.transform.position = table.GridPosToWorld(new Vector2Int(t.X,t.Y),"isUnit"); } public Vector3 GetPosition() { - return _ROUnit.transform.position; + return _ROUnit != null ? _ROUnit.transform.position : Vector3.zero; } public bool isGlow() @@ -619,4 +724,4 @@ namespace TH1_Renderer } } -} \ No newline at end of file +} diff --git a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMultiplayMemberRowMono.cs b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMultiplayMemberRowMono.cs index 8a91aeb33..f00808d1f 100644 --- a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMultiplayMemberRowMono.cs +++ b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMultiplayMemberRowMono.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using Logic.Multilingual; +using RuntimeData; using TH1_Logic.Core; using TH1_Logic.Net; using TH1_Logic.Steam; @@ -113,16 +114,22 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour } - public void UpdatePlayerInfoData(CivEnum civ,ForceEnum force) + public bool UpdatePlayerInfoData(CivEnum civ,ForceEnum force) { - //修改mapConfig - var t = Main.Instance.MapConfig.GetMemberCiv(_lobby.GetSelfMemberId()); - if(t != null){ - t.CivId = Table.Instance.TransCivEnumToCivId(civ); - t.ForceId = Table.Instance.TransForceEnumToForceId(force); - Main.Instance.MapConfig.UpdateMemberCiv(t); - Main.Instance.MapConfig.CheckMapConfigChanged(); - } + var selfMemberId = _lobby.GetSelfMemberId(); + var t = Main.Instance.MapConfig.GetMemberCiv(selfMemberId); + if (t == null) return false; + + var next = new MemberCiv + { + MemberId = selfMemberId, + CivId = Table.Instance.TransCivEnumToCivId(civ), + ForceId = Table.Instance.TransForceEnumToForceId(force) + }; + + var accepted = Main.Instance.MapConfig.UpdateMemberCiv(next); + if (accepted) Main.Instance.MapConfig.CheckMapConfigChanged(); + return accepted; } public void OnClickForces() @@ -132,23 +139,23 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour _forceNameOverride = null; if (_civ == CivEnum.Egyptian) { - UpdatePlayerInfoData(CivEnum.French,ForceEnum.Kaguya); - UpdatePlayerInfoView(CivEnum.French,ForceEnum.Kaguya); + if (UpdatePlayerInfoData(CivEnum.French,ForceEnum.Kaguya)) + UpdatePlayerInfoView(CivEnum.French,ForceEnum.Kaguya); } else if (_civ == CivEnum.French) { - UpdatePlayerInfoData(CivEnum.Germany,ForceEnum.Kanako); - UpdatePlayerInfoView(CivEnum.Germany,ForceEnum.Kanako); + if (UpdatePlayerInfoData(CivEnum.Germany,ForceEnum.Kanako)) + UpdatePlayerInfoView(CivEnum.Germany,ForceEnum.Kanako); } else if (_civ == CivEnum.Germany) { - UpdatePlayerInfoData(CivEnum.Indian,ForceEnum.Satori); - UpdatePlayerInfoView(CivEnum.Indian,ForceEnum.Satori); + if (UpdatePlayerInfoData(CivEnum.Indian,ForceEnum.Satori)) + UpdatePlayerInfoView(CivEnum.Indian,ForceEnum.Satori); } else if (_civ == CivEnum.Indian) { - UpdatePlayerInfoData(CivEnum.Egyptian,ForceEnum.Remilia); - UpdatePlayerInfoView(CivEnum.Egyptian,ForceEnum.Remilia); + if (UpdatePlayerInfoData(CivEnum.Egyptian,ForceEnum.Remilia)) + UpdatePlayerInfoView(CivEnum.Egyptian,ForceEnum.Remilia); } } void Start()