using System.Threading; using Ananta.SDK.Network; using Ananta.SDK.Serialization; using Ananta.Server.Protocol.Client4229938; using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene; namespace Ananta.Server.Handlers.Game; /// /// 敌人血条的显示时机 —— SyncAddUnitState / SyncRemoveUnitState 推 /// UnitStateConfig.HideHp。 /// /// ⛑⛑ 与 GameRouter.EnemyWarning.cs(detectionState 那条)是两条完全不同的路, /// 别再混为一谈。客户端判据在 EnemyHUDCtrl.lua: /// · RefreshHpVisible(:551) 决定血条 —— 看 cfg.ShowHPBar(模板配置, /// 服务端改不了)和 shouldShowHp 里的 UnitStateConfig.HideHp(我们能写)。 /// · detectionState 只在 :85 绑到 OnShowDetectionEffect,动的是潜行警戒条, /// 跟血条无关。所以警戒状态机从 Idle 推到 Fight 血条也不会变。 /// /// 结论:想让血条"不打不显示、打了才显示",唯一 per-unit 且服务端可写的开关就是 HideHp。 /// 先例:GameRouter.HackerSummon.cs:341 用同一招关掉机械狗的敌人血条。 /// internal sealed partial class GameRouter { /// /// UnitStateConfig.HideHp = 10266(dump.cs:1448623 / 1449004)。 /// ⛑ 不是 UnitStateAtlasConfig 的图集 id,也不是 DeadS(2)。 /// private const uint LocalEnemyUnitStateHideHp = 10266; /// spawn 批之后补发 HideHp 的延迟(ms)。与 ReEngageSpawnedEnemyAsync 的 1500 对齐。 private const int LocalEnemyHpBarHideReassertMs = 1500; /// /// spawn 之后调用:SyncAddUnitState(68026128) 把血条收起。 /// 幂等 —— 已经挂着 HideHp 的单位不会再发一次。 /// internal static async Task HideLocalEnemyHpBarAsync(TcpSession session, ulong uid) { if (!EnemyHit.HideHpBarUntilHit) return; var runtime = GetLocalEnemyRuntimeState(session); lock (runtime.SyncRoot) { if (!runtime.Enemies.TryGetValue(uid, out var existing) || existing is null) return; // 只有 server-spawned 的走这套。client-spawned 的本地单位客户端自己建好了血条, // 我们第一次知道它存在的时候它已经在挨打了,没机会提前收。 if (!existing.ServerSpawned || existing.Dead || existing.HpBarHidden) return; } try { await session.NotifyAsync(MethodId.SyncAddUnitState, UxSerializer.Serialize(new GameMethods.SyncAddUnitStateArgs4229938 { Id = uid, State = LocalEnemyUnitStateHideHp, EffectFreezeState = 0u, }), CancellationToken.None); bool hidden; lock (runtime.SyncRoot) { hidden = runtime.Enemies.TryGetValue(uid, out var rec) && rec is not null; if (hidden) rec!.HpBarHidden = true; } if (!hidden) return; session.Log.Info($"[ENEMY-HPBAR] hidden uid={uid} state=HideHp({LocalEnemyUnitStateHideHp}) — bar shows on first hit"); // 补一拍。spawn 批是几十个包,AddUnitState 可能比单位在客户端建好更早到, // 那就白发了(engage 那五件套也吃这个亏,ReEngageSpawnedEnemyAsync 同样延 1.5 s)。 // 若这 1.5 s 里玩家已经打中,HpBarHidden 已置 false ⇒ 这里跳过,不会把血条又收回去。 _ = ReassertHiddenHpBarAsync(session, uid); } catch (Exception ex) { session.Log.Warn($"[ENEMY-HPBAR] hide failed uid={uid}: {ex.Message}"); } } private static async Task ReassertHiddenHpBarAsync(TcpSession session, ulong uid) { try { await Task.Delay(LocalEnemyHpBarHideReassertMs, CancellationToken.None); var runtime = GetLocalEnemyRuntimeState(session); lock (runtime.SyncRoot) { if (!runtime.Enemies.TryGetValue(uid, out var rec) || rec is null) return; if (!rec.HpBarHidden || rec.Dead) return; } await session.NotifyAsync(MethodId.SyncAddUnitState, UxSerializer.Serialize(new GameMethods.SyncAddUnitStateArgs4229938 { Id = uid, State = LocalEnemyUnitStateHideHp, EffectFreezeState = 0u, }), CancellationToken.None); session.Log.Info($"[ENEMY-HPBAR] reassert uid={uid} state=HideHp({LocalEnemyUnitStateHideHp})"); } catch (Exception ex) { session.Log.Warn($"[ENEMY-HPBAR] reassert failed uid={uid}: {ex.Message}"); } } /// /// 第一次命中时调用:SyncRemoveUnitState(68566559) 撤掉 HideHp,血条出现。 /// 只在 HpBarHidden 为真时发,之后自动变 no-op(不会每一下都刷包)。 /// internal static async Task RevealLocalEnemyHpBarAsync(TcpSession session, ulong uid) { var runtime = GetLocalEnemyRuntimeState(session); lock (runtime.SyncRoot) { if (!runtime.Enemies.TryGetValue(uid, out var existing) || existing is null) return; if (!existing.HpBarHidden) return; existing.HpBarHidden = false; } try { await session.NotifyAsync(MethodId.SyncRemoveUnitState, UxSerializer.Serialize(new SceneMethods.SyncRemoveUnitState { unitId = uid, state = LocalEnemyUnitStateHideHp, reason = 1, sourceId = 0u, }), CancellationToken.None); session.Log.Info($"[ENEMY-HPBAR] revealed uid={uid} state=HideHp({LocalEnemyUnitStateHideHp}) removed"); } catch (Exception ex) { session.Log.Warn($"[ENEMY-HPBAR] reveal failed uid={uid}: {ex.Message}"); } } }