using System.Runtime.InteropServices; using Dalamud; using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using ICRebuild.Core; namespace ICRebuild.Runtime; public sealed class StatusBlockModule(GameContext game, Configuration config, ModuleManager modules) { // 2026.09.15: setter RVA 0x8A6BF0; local-player load RVA 0xB37D7A. internal const string SetterSignature = "40 53 56 41 56 48 83 EC 70 45 32 F6 0F 29 74 24 50 41 0F B7 F0 0F 28 F3 48 8B D9 83 FA 3C"; internal const string LocalPlayerSignature = "48 8B 1D ?? ?? ?? ?? 48 8B FA 48 85 DB 74 28 48 8B 03 48 8B CB FF 90 78 02 00 00"; private long blocked; private int lastStatus; public long Blocked => Interlocked.Read(ref blocked); public int LastStatus => Volatile.Read(ref lastStatus); public void Register() => modules.Register(8, scope => { if(!StatusBlockPolicy.IsAuditedVersion(game.GameVersion)) throw new InvalidOperationException($"状态屏蔽仅适配已核对客户端(国服/国际服),当前为 {game.GameVersion}。"); if(Marshal.OffsetOf(nameof(BattleChara.StatusManager)).ToInt32() != StatusBlockPolicy.ManagerOffset || Marshal.OffsetOf(nameof(GameObject.EntityId)).ToInt32() != StatusBlockPolicy.EntityIdOffset) throw new InvalidOperationException("当前 SDK 的角色布局与已核对的状态入口不一致。"); var localPointer = game.StaticAddress(LocalPlayerSignature, 0); if(localPointer == 0) throw new InvalidOperationException("本地角色指针未解析。"); Interlocked.Exchange(ref blocked, 0); Volatile.Write(ref lastStatus, 0); game.Frames.Clear(); // Wait for a fresh Framework snapshot after enabling. void ClearTerritory(uint _) => game.Frames.Clear(); void ClearLogout(int _, int __) => game.Frames.Clear(); void ClearCondition(ConditionFlag flag, bool value) { if(value && flag is ConditionFlag.BetweenAreas or ConditionFlag.BetweenAreas51 or ConditionFlag.DutyRecorderPlayback) game.Frames.Clear(); } game.Client.TerritoryChanged += ClearTerritory; scope.Defer(()=>game.Client.TerritoryChanged -= ClearTerritory); game.Client.Logout += ClearLogout; scope.Defer(()=>game.Client.Logout -= ClearLogout); game.Conditions.ConditionChange += ClearCondition; scope.Defer(()=>game.Conditions.ConditionChange -= ClearCondition); Hook? hook = null; hook = game.Hook(scope, SetterSignature, (manager,index,id,remaining,param,source,refreshFlags) => { var suppress = false; try { var frame = game.Frames.Read(); // Do not dereference manager.Owner: it may legitimately be null. // Only selected local updates incur native identity reads. if(StatusBlockPolicy.IsSelected(index,id,config.Published.BlockedStatuses) && frame.Ready && !frame.IsReplay && manager != 0 && manager == frame.LocalStatusManagerAddress && SafeMemory.Read(localPointer,out var currentPlayer) && currentPlayer == frame.LocalAddress && SafeMemory.Read(currentPlayer + StatusBlockPolicy.EntityIdOffset,out var currentId)) suppress = StatusBlockPolicy.IsLocal(frame,manager,currentPlayer,currentId); } catch(Exception ex) { game.Fault(8,ex); } // Outside the catch: a native invocation must never be retried on an exception. var result = StatusBlockPolicy.Forward(hook!.Original,suppress,manager,index,id,remaining,param,source,refreshFlags); if(suppress) { Interlocked.Increment(ref blocked); Volatile.Write(ref lastStatus,id); } return result; }); hook.Enable(); }); }