Files
IC.Rebuild/Runtime/StatusBlockModule.cs
T

77 lines
4.0 KiB
C#

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<BattleChara>(nameof(BattleChara.StatusManager)).ToInt32() != StatusBlockPolicy.ManagerOffset ||
Marshal.OffsetOf<GameObject>(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<StatusSetter>? hook = null;
hook = game.Hook<StatusSetter>(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<nint>(localPointer,out var currentPlayer) && currentPlayer == frame.LocalAddress &&
SafeMemory.Read<uint>(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();
});
}