77 lines
4.0 KiB
C#
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();
|
|
});
|
|
}
|