Initial import of IC.Rebuild
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using ICRebuild.Core;
|
||||
|
||||
namespace ICRebuild.Runtime;
|
||||
|
||||
public sealed unsafe class CombatModules(GameContext game, Configuration config, ModuleManager modules)
|
||||
{
|
||||
private delegate long Backswing(long a1);
|
||||
private delegate void Dash(nint controller, byte moveType, nint target, nint origin, float facing, nint data);
|
||||
private delegate nint SyncRecast(nint manager, int recastGroup, uint actionId, float maximum);
|
||||
private delegate uint CastUpdate(nint data, uint id, float total, float start);
|
||||
private delegate nint TargetRingUpdate(nint renderer,ulong targetId);
|
||||
private static readonly HashSet<uint> RangeSkip = [34675,3573,2262,29513];
|
||||
private long dashCalls,dashBlocks;
|
||||
private DashObservation? lastDash;
|
||||
public long DashCalls => Interlocked.Read(ref dashCalls);
|
||||
public long DashBlocks => Interlocked.Read(ref dashBlocks);
|
||||
public DashObservation? LastDash => Volatile.Read(ref lastDash);
|
||||
public MovementSkill[] MovementSkills { get; private set; } = [];
|
||||
private SharedModuleResource? sharedRange;
|
||||
private long radiusPrechecks,radiusFinalChecks,radiusBonuses;
|
||||
public long RadiusPrechecks => Interlocked.Read(ref radiusPrechecks);
|
||||
public long RadiusFinalChecks => Interlocked.Read(ref radiusFinalChecks);
|
||||
public long RadiusBonuses => Interlocked.Read(ref radiusBonuses);
|
||||
private long nativeRingCalls,nativeRingBonuses;
|
||||
public long NativeRingCalls => Interlocked.Read(ref nativeRingCalls);
|
||||
public long NativeRingBonuses => Interlocked.Read(ref nativeRingBonuses);
|
||||
private long targetRadiusChanges,selfRadiusSkips;
|
||||
public long TargetRadiusChanges => Interlocked.Read(ref targetRadiusChanges);
|
||||
public long SelfRadiusSkips => Interlocked.Read(ref selfRadiusSkips);
|
||||
private TargetRadiusObservation? lastTargetRadius;
|
||||
public TargetRadiusObservation? LastTargetRadius => Volatile.Read(ref lastTargetRadius);
|
||||
|
||||
private void AcquireRange(ResourceScope owner)
|
||||
{
|
||||
sharedRange ??= new SharedModuleResource(scope=>
|
||||
{
|
||||
var actions=game.Data.GetExcelSheet<Lumina.Excel.Sheets.Action>()
|
||||
.ToDictionary(a=>a.RowId,a=>(Moves:a.AffectsPosition,Hostile:a.CanTargetHostile));
|
||||
Hook<ActionManager.Delegates.GetActionRange>? range=null;
|
||||
range=game.HookAddress<ActionManager.Delegates.GetActionRange>(scope,(nint)ActionManager.MemberFunctionPointers.GetActionRange,id=>
|
||||
{
|
||||
var original=range!.Original(id);
|
||||
try
|
||||
{
|
||||
if(!game.Ready || !float.IsFinite(original) || original<0 || !actions.TryGetValue(id,out var action)) return original;
|
||||
var result=original;
|
||||
if(modules.IsEnabled(13) && !RangeSkip.Contains(id))
|
||||
{
|
||||
var extra=config.Value(13);
|
||||
result+=id==29066 || (action.Moves && action.Hostile) ? extra : Math.Min(extra,3);
|
||||
}
|
||||
// Module 14 modifies the target radius itself. Never also add it to skill range.
|
||||
return result;
|
||||
}
|
||||
catch(Exception ex) { game.Fault(14,ex); return original; }
|
||||
});
|
||||
range.Enable();
|
||||
});
|
||||
sharedRange.Acquire(owner);
|
||||
}
|
||||
|
||||
public void LoadMovementCatalog()
|
||||
{
|
||||
MovementSkills=MovementSkillCatalog.Select(game.Data.GetExcelSheet<Lumina.Excel.Sheets.Action>()
|
||||
.Where(a=>a.IsPlayerAction && a.AffectsPosition)
|
||||
.Select(a=>new MovementSkill(a.RowId,a.Name.ToString(),a.AnimationEnd.RowId,a.IsPlayerAction,a.AffectsPosition,a.IsPvP)));
|
||||
}
|
||||
public int AddAllMovementSkills()
|
||||
{
|
||||
if(MovementSkills.Length==0) LoadMovementCatalog();
|
||||
var before=config.BlockedTimelines.Count;
|
||||
foreach(var skill in MovementSkills.Where(MovementSkillCatalog.Mappable)) config.BlockedTimelines.Add(skill.Timeline);
|
||||
config.DashMode=DashFilterMode.BlockListed;
|
||||
config.MovementListGameVersion=game.GameVersion;
|
||||
config.Normalize();
|
||||
return config.BlockedTimelines.Count-before;
|
||||
}
|
||||
|
||||
public void Register()
|
||||
{
|
||||
modules.Register(13,AcquireRange);
|
||||
modules.Register(14, scope =>
|
||||
{
|
||||
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("原生目标圈入口仅核对了 2026.09.15 客户端");
|
||||
Hook<ActionManager.Delegates.GetActionInRangeOrLoS>? rangeCheck = null;
|
||||
Hook<ActionManager.Delegates.IsActionTargetInRange>? precheck = null;
|
||||
Hook<TargetRingUpdate>? ringUpdate=null;
|
||||
Hook<FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject.Delegates.GetRadius>? radius=null;
|
||||
rangeCheck=game.HookAddress<ActionManager.Delegates.GetActionInRangeOrLoS>(scope,(nint)ActionManager.MemberFunctionPointers.GetActionInRangeOrLoS,(id,source,target)=>
|
||||
{
|
||||
var frame=game.Frames.Read();
|
||||
var local=frame.Ready && (nint)source==frame.LocalAddress && source!=null && target!=null && source!=target;
|
||||
if(local) Interlocked.Increment(ref radiusFinalChecks);
|
||||
using var rangeScope=ActionRangeCheckScope.Enter(local);
|
||||
return rangeCheck!.Original(id,source,target);
|
||||
});
|
||||
precheck=game.HookAddress<ActionManager.Delegates.IsActionTargetInRange>(scope,(nint)ActionManager.MemberFunctionPointers.IsActionTargetInRange,(manager,type,id)=>
|
||||
{
|
||||
var frame=game.Frames.Read();
|
||||
var local=frame.Ready && manager!=null && (nint)manager==frame.ActionManagerAddress && type==ActionType.Action;
|
||||
if(local) Interlocked.Increment(ref radiusPrechecks);
|
||||
using var rangeScope=ActionRangeCheckScope.Enter(local);
|
||||
return precheck!.Original(manager,type,id);
|
||||
});
|
||||
radius=game.Hook<FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject.Delegates.GetRadius>(scope,Signatures.ActorRadius,(actor,adjust)=>
|
||||
{
|
||||
var original=radius!.Original(actor,adjust);
|
||||
var frame=game.Frames.Read();
|
||||
if((nint)actor==frame.LocalAddress) Interlocked.Increment(ref selfRadiusSkips);
|
||||
var result=TargetRadiusPolicy.Adjust(frame,(nint)actor,actor==null ? 0 : actor->EntityId,original,config.Value(14));
|
||||
if(result!=original && float.IsFinite(result))
|
||||
{
|
||||
Interlocked.Increment(ref targetRadiusChanges);
|
||||
var old=Volatile.Read(ref lastTargetRadius);
|
||||
if(old==null || old.EntityId!=frame.TargetEntityId || old.Original!=original || old.Effective!=result)
|
||||
Volatile.Write(ref lastTargetRadius,new(frame.TargetEntityId,original,result));
|
||||
if(NativeTargetRingScope.Active) Interlocked.Increment(ref nativeRingBonuses);
|
||||
if(ActionRangeCheckScope.Active) Interlocked.Increment(ref radiusBonuses);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
ringUpdate=game.Hook<TargetRingUpdate>(scope,Signatures.NativeTargetRingUpdate,(renderer,targetId)=>
|
||||
{
|
||||
Interlocked.Increment(ref nativeRingCalls);
|
||||
using var ringScope=NativeTargetRingScope.Enter(game.Ready && modules.IsEnabled(14));
|
||||
return ringUpdate!.Original(renderer,targetId);
|
||||
});
|
||||
rangeCheck.Enable(); precheck.Enable(); radius.Enable(); ringUpdate.Enable();
|
||||
});
|
||||
modules.Register(15, scope =>
|
||||
{
|
||||
Hook<Backswing>? hook = null;
|
||||
hook = game.Hook<Backswing>(scope,Signatures.NoBackswing,a1 => game.Ready ? a1 : hook!.Original(a1));
|
||||
hook.Enable();
|
||||
});
|
||||
modules.Register(16, scope =>
|
||||
{
|
||||
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("技能位移结构仅核对了 2026.09.15 客户端");
|
||||
Hook<Dash>? hook = null;
|
||||
hook = game.Hook<Dash>(scope,Signatures.ActionMovementStart,(controller,moveType,target,origin,facing,data) =>
|
||||
{
|
||||
var frame=game.Frames.Read();
|
||||
if(controller!=0 && data!=0 && frame.Ready)
|
||||
{
|
||||
var owner=*(nint*)(controller+0x3E0);
|
||||
if(owner==frame.LocalAddress && owner!=0)
|
||||
{
|
||||
var settings=config.Published;
|
||||
var bytes=new ReadOnlySpan<byte>((void*)data,4);
|
||||
var timeline=DashPolicy.Timeline(bytes);
|
||||
var block=DashPolicy.Block(frame.Ready,owner,frame.LocalAddress,timeline,settings.DashMode,settings.Timelines);
|
||||
Interlocked.Increment(ref dashCalls);
|
||||
Volatile.Write(ref lastDash,new(timeline,bytes[2],bytes[3],moveType,block));
|
||||
if(block) { Interlocked.Increment(ref dashBlocks); return; }
|
||||
}
|
||||
}
|
||||
hook!.Original(controller,moveType,target,origin,facing,data);
|
||||
});
|
||||
hook.Enable();
|
||||
});
|
||||
modules.Register(17, StartRecast);
|
||||
modules.Register(18, _ => { },17);
|
||||
modules.Register(19, StartCast);
|
||||
modules.Register(20, _ => { });
|
||||
}
|
||||
|
||||
private void StartRecast(ResourceScope scope)
|
||||
{
|
||||
Hook<ActionManager.Delegates.GetAdjustedRecastTime>? read = null;
|
||||
Hook<SyncRecast>? sync = null;
|
||||
read = game.HookAddress<ActionManager.Delegates.GetAdjustedRecastTime>(scope,(nint)ActionManager.MemberFunctionPointers.GetAdjustedRecastTime,(type,action,extra) =>
|
||||
{
|
||||
var value = read!.Original(type,action,extra);
|
||||
if (!game.Ready || type != ActionType.Action || value <= 0) return value;
|
||||
if (modules.IsEnabled(18) && Policies.IsMudra((int)action)) return 0;
|
||||
return Math.Max(0,value - (int)(config.Value(17)*1000));
|
||||
});
|
||||
sync = game.Hook<SyncRecast>(scope,"40 ?? 48 ?? ?? ?? 0f 29 74 24 ?? 41 ?? ?? 0f ?? ?? e8 ?? ?? ?? ??",(manager,recastGroup,actionId,maximum) =>
|
||||
{
|
||||
if (game.Ready && recastGroup == NativeIds.GlobalCooldownGroup && float.IsFinite(maximum) && maximum > 0)
|
||||
maximum = modules.IsEnabled(18) && NativeIds.MudraActions.Contains(actionId) ? 0 : Math.Max(0,maximum-config.Value(17));
|
||||
return sync!.Original(manager,recastGroup,actionId,maximum);
|
||||
});
|
||||
read.Enable(); sync.Enable();
|
||||
}
|
||||
|
||||
private void StartCast(ResourceScope scope)
|
||||
{
|
||||
Hook<ActionManager.Delegates.GetAdjustedCastTime>? read = null;
|
||||
Hook<CastUpdate>? update = null;
|
||||
var current = game.StaticAddress("F3 44 0F 2C C0 BA ?? ?? ?? ?? 48 8B CB E8 ?? ?? ?? ?? F3 44 0F 10 1D",0x12);
|
||||
if (current == 0) throw new InvalidOperationException("咏唱同步地址未解析");
|
||||
read = game.HookAddress<ActionManager.Delegates.GetAdjustedCastTime>(scope,(nint)ActionManager.MemberFunctionPointers.GetAdjustedCastTime,(type,id,apply,proc) =>
|
||||
{
|
||||
var value = read!.Original(type,id,apply,proc);
|
||||
return game.Ready && type == ActionType.Action ? Math.Max(0,value-(int)(config.Value(19)*1000)) : value;
|
||||
});
|
||||
update = game.Hook<CastUpdate>(scope,"48 89 5C 24 ?? 57 48 83 EC ?? 48 8B F9 0F 29 74 24 ?? 0F B6 49",(data,id,total,start) =>
|
||||
{
|
||||
if (game.Ready && data != 0 && *(uint*)((byte*)data+4) == id && float.IsFinite(total))
|
||||
{ total = Math.Max(0,total-config.Value(19)); *(float*)current = total; }
|
||||
return update!.Original(data,id,total,start);
|
||||
});
|
||||
read.Enable(); update.Enable();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (!modules.IsEnabled(20) || !game.Ready || !game.IsPvP) return;
|
||||
var manager = ActionManager.Instance();
|
||||
if (manager != null && float.IsFinite(manager->AnimationLock) && manager->AnimationLock > config.Value(20))
|
||||
manager->AnimationLock = config.Value(20);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using ICRebuild.Core;
|
||||
|
||||
namespace ICRebuild.Runtime;
|
||||
|
||||
public sealed unsafe class CoordinateTools(GameContext game, ModuleManager modules)
|
||||
{
|
||||
private delegate void Scale(nint actor, float scale);
|
||||
private Scale? scale;
|
||||
private readonly Dictionary<ulong,(nint Address,uint Territory,float Original,float Applied)> resized=[];
|
||||
public void Register()
|
||||
{
|
||||
modules.Register(27, _ => { }); modules.Register(28, _ => { });
|
||||
modules.Register(29, scope =>
|
||||
{
|
||||
scale = Marshal.GetDelegateForFunctionPointer<Scale>(game.Scan("F3 0F 11 89 ?? ?? ?? ?? E9 ?? ?? ?? ?? CC CC CC CC CC CC CC CC CC CC CC CC"));
|
||||
scope.Defer(() => { try { RestoreAll(); } finally { scale=null; } });
|
||||
});
|
||||
}
|
||||
private GameObject* Player(int feature)
|
||||
{
|
||||
if (!modules.IsEnabled(feature)) throw new InvalidOperationException($"先启用模块 {feature}");
|
||||
if (!game.Ready) throw new InvalidOperationException("角色尚未就绪");
|
||||
return (GameObject*)game.LocalAddress;
|
||||
}
|
||||
public void Position(Vector3 position)
|
||||
{
|
||||
if (!float.IsFinite(position.X) || !float.IsFinite(position.Y) || !float.IsFinite(position.Z) ||
|
||||
Math.Abs(position.X)>100000 || Math.Abs(position.Y)>100000 || Math.Abs(position.Z)>100000)
|
||||
throw new ArgumentException("坐标必须是 ±100000 范围内的有限数");
|
||||
Player(27)->SetPosition(position.X,position.Y,position.Z);
|
||||
}
|
||||
public void Facing(float rotation)
|
||||
{
|
||||
if (!float.IsFinite(rotation)) throw new ArgumentException("面向必须是有限数");
|
||||
Player(28)->SetRotation(MathF.IEEERemainder(rotation,2*MathF.PI));
|
||||
}
|
||||
public void SetScale(float value)
|
||||
{
|
||||
if (!modules.IsEnabled(29) || scale == null) throw new InvalidOperationException("先启用模块 29");
|
||||
if (!game.Ready || game.Targets.Target == null) throw new InvalidOperationException("请选中有效目标");
|
||||
if (!float.IsFinite(value) || value < 0.1f || value > 5) throw new ArgumentException("缩放范围为 0.1—5");
|
||||
var target=game.Targets.Target;
|
||||
var obj=(GameObject*)target.Address;
|
||||
var original=obj->Scale;
|
||||
if(!float.IsFinite(original) || original<=0) throw new InvalidOperationException("目标原始缩放无效");
|
||||
if(resized.TryGetValue(target.GameObjectId,out var old) && old.Address==target.Address && old.Territory==game.Client.TerritoryType)
|
||||
original=old.Original;
|
||||
if(resized.Count>=256 && !resized.ContainsKey(target.GameObjectId)) throw new InvalidOperationException("先恢复已有缩放对象");
|
||||
scale(target.Address,value);
|
||||
resized[target.GameObjectId]=(target.Address,game.Client.TerritoryType,original,value);
|
||||
}
|
||||
public void RestoreAll()
|
||||
{
|
||||
if(scale!=null && game.Ready)
|
||||
foreach(var obj in game.Objects)
|
||||
if(resized.TryGetValue(obj.GameObjectId,out var old) && old.Address==obj.Address && old.Territory==game.Client.TerritoryType &&
|
||||
Math.Abs(((GameObject*)obj.Address)->Scale-old.Applied)<0.0001f)
|
||||
scale(obj.Address,old.Original);
|
||||
resized.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Event;
|
||||
using ICRebuild.Core;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ICRebuild.Runtime;
|
||||
|
||||
public sealed unsafe class GameContext(IObjectTable objects, IClientState client, ICondition condition,
|
||||
IDataManager data, ISigScanner scanner, IGameInteropProvider interop, ITargetManager targets)
|
||||
{
|
||||
public IObjectTable Objects { get; } = objects;
|
||||
public IClientState Client { get; } = client;
|
||||
public ICondition Conditions { get; } = condition;
|
||||
public IDataManager Data { get; } = data;
|
||||
public ITargetManager Targets { get; } = targets;
|
||||
public FrameStore Frames { get; } = new();
|
||||
public string GameVersion { get; } = ReadGameVersion();
|
||||
private readonly ConcurrentQueue<(int Id,string Error)> faults = new();
|
||||
public void Fault(int id,Exception error) { if(faults.Count<32) faults.Enqueue((id,error.Message)); }
|
||||
public void DrainFaults(ModuleManager modules)
|
||||
{
|
||||
while(faults.TryDequeue(out var fault))
|
||||
{
|
||||
modules.Disable(fault.Id);
|
||||
modules.Status[fault.Id].State=ModuleState.Failed;
|
||||
modules.Status[fault.Id].Detail=$"运行错误,已停用:{fault.Error}";
|
||||
}
|
||||
}
|
||||
public bool Ready => Frames.Read().Ready;
|
||||
public nint LocalAddress => Frames.Read().LocalAddress;
|
||||
public bool IsPvP => Frames.Read().IsPvP;
|
||||
public bool IsSlowed => Frames.Read().IsSlowed;
|
||||
|
||||
public DungeonSnapshot ReadDungeon() => Frames.Read().Dungeon;
|
||||
|
||||
// Called exclusively from Framework.Update, never from a native hook or worker thread.
|
||||
public void Refresh(bool sampleDungeon)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player=Objects.LocalPlayer;
|
||||
if(!Client.IsLoggedIn || player==null || Conditions[ConditionFlag.BetweenAreas] || Conditions[ConditionFlag.BetweenAreas51])
|
||||
{ Frames.Clear(); return; }
|
||||
var dungeon=sampleDungeon ? SampleDungeon() : DungeonSnapshot.Empty;
|
||||
var target=Targets.Target;
|
||||
Frames.Publish(new(true,player.Address,Client.IsPvP,player.StatusList.Any(s=>StatusCatalog.AuditedSlow.Contains(s.StatusId)),dungeon,
|
||||
(nint)FFXIVClientStructs.FFXIV.Client.Game.ActionManager.Instance(),target?.Address ?? 0,target?.EntityId ?? 0,
|
||||
(nint)(&((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)player.Address)->StatusManager),
|
||||
player.EntityId,Conditions[ConditionFlag.DutyRecorderPlayback]));
|
||||
}
|
||||
catch { Frames.Clear(); throw; }
|
||||
}
|
||||
private DungeonSnapshot SampleDungeon()
|
||||
{
|
||||
if (!Conditions[ConditionFlag.InDeepDungeon]) return DungeonSnapshot.Empty;
|
||||
var framework = EventFramework.Instance();
|
||||
if (framework == null) return DungeonSnapshot.Empty;
|
||||
var dd = framework->GetInstanceContentDeepDungeon();
|
||||
return dd == null || dd->Floor == 0 ? DungeonSnapshot.Empty : new(true, Client.TerritoryType, dd->DeepDungeonId, dd->Floor);
|
||||
}
|
||||
|
||||
public Hook<T> Hook<T>(ResourceScope scope, string signature, T detour) where T : Delegate
|
||||
{
|
||||
if (!scanner.TryScanText(signature, out var address) || address == 0)
|
||||
throw new InvalidOperationException("签名未匹配,此模块未适配当前客户端。");
|
||||
return scope.Own(interop.HookFromAddress(address, detour));
|
||||
}
|
||||
|
||||
public nint Scan(string signature)
|
||||
{
|
||||
if (!scanner.TryScanText(signature, out var address) || address == 0)
|
||||
throw new InvalidOperationException("签名未匹配。");
|
||||
return address;
|
||||
}
|
||||
public nint StaticAddress(string signature, int offset) => scanner.GetStaticAddressFromSig(signature, offset);
|
||||
public Hook<T> HookAddress<T>(ResourceScope scope,nint address,T detour) where T:Delegate
|
||||
{
|
||||
if(address==0) throw new InvalidOperationException("原生函数地址未解析");
|
||||
return scope.Own(interop.HookFromAddress(address,detour));
|
||||
}
|
||||
|
||||
public SendSession ReadSendSession()
|
||||
{
|
||||
var frame = Frames.Read();
|
||||
return frame.Ready && !frame.IsReplay
|
||||
? new(true, Client.TerritoryType, frame.LocalAddress, frame.LocalEntityId)
|
||||
: SendSession.Empty;
|
||||
}
|
||||
|
||||
public AdaptationReport EvaluateAdaptation(ModuleManager modules)
|
||||
{
|
||||
var offsetsValid =
|
||||
System.Runtime.InteropServices.Marshal.OffsetOf<FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara>(
|
||||
nameof(FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara.StatusManager)).ToInt32() == StatusBlockPolicy.ManagerOffset &&
|
||||
System.Runtime.InteropServices.Marshal.OffsetOf<FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject>(
|
||||
nameof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject.EntityId)).ToInt32() == StatusBlockPolicy.EntityIdOffset;
|
||||
return AdaptationManifest.Evaluate(modules, GameVersion,
|
||||
sig => scanner.TryScanText(sig, out var addr) && addr != 0 ? 1 : 0,
|
||||
offsetsValid);
|
||||
}
|
||||
|
||||
public static string ReadGameVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
var file = Path.Combine(dir ?? "", "ffxivgame.ver");
|
||||
var version = File.Exists(file) ? File.ReadAllText(file).Trim() : "unknown";
|
||||
return version.Length is > 0 and < 128 ? version : "unknown";
|
||||
}
|
||||
catch { return "unknown"; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using Dalamud.Hooking;
|
||||
using ICRebuild.Core;
|
||||
|
||||
namespace ICRebuild.Runtime;
|
||||
|
||||
public sealed unsafe class MovementModules(GameContext game, Configuration config, ModuleManager modules)
|
||||
{
|
||||
private delegate nint Move(nuint actor, nint data, uint arg);
|
||||
private delegate float Speed(long actor);
|
||||
private delegate void Acceleration(long actor);
|
||||
private delegate long Permission(long a1, uint a2, int a3, int a4);
|
||||
private delegate long Knock(long actor, float angle, float distance, float seconds, byte a5, long a6);
|
||||
private delegate nint Fall(nuint a1, uint flags);
|
||||
private delegate long FallCheck(long a1, long a2, long a3);
|
||||
private delegate nint Forced(nint actor, float x, float y, float z, float movementParameter, nint options);
|
||||
[ThreadStatic] private static HashSet<nint>? adjusted;
|
||||
public HeightActivation Height { get; } = new();
|
||||
public float EffectiveHeight => !modules.IsEnabled(9) ? 0 : Policies.HeightDelta(game.Ready,modules.IsEnabled(10),game.ReadDungeon(),Height.Applied);
|
||||
|
||||
public void Register()
|
||||
{
|
||||
modules.Register(11, _ => { });
|
||||
modules.Register(10, _ => { }, 9);
|
||||
modules.Register(9, StartHeight);
|
||||
modules.Register(1, scope =>
|
||||
{
|
||||
Hook<Speed>? hook = null;
|
||||
hook = game.Hook<Speed>(scope, Signatures.SpeedAddress, actor =>
|
||||
{
|
||||
var original = hook!.Original(actor);
|
||||
try
|
||||
{
|
||||
if (!game.Ready || game.IsSlowed) return original;
|
||||
return float.IsFinite(original) ? Math.Max(0, original + config.Value(1)) : original;
|
||||
}
|
||||
catch(Exception ex) { game.Fault(1,ex); return original; }
|
||||
});
|
||||
hook.Enable();
|
||||
});
|
||||
modules.Register(2, scope =>
|
||||
{
|
||||
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("最大加速度仅完成 2026.09.15 客户端的字段核对。");
|
||||
Hook<Acceleration>? hook = null;
|
||||
hook = game.Hook<Acceleration>(scope, "40 ?? 48 ?? ?? ?? 80 79 ?? ?? 48 ?? ?? 0f 84 ?? ?? ?? ?? 48 89 7c 24 ?? 48 ?? ?? ??", actor =>
|
||||
{
|
||||
var frame=game.Frames.Read();
|
||||
if(actor==0 || !frame.Ready || *(byte*)((nint)actor+0x3C)==0) { hook!.Original(actor); return; }
|
||||
var controller=*(nint*)((nint)actor+0x20);
|
||||
var owner=controller==0 ? 0 : *(nint*)(controller+0x3E0);
|
||||
ref var field=ref *(float*)((nint)actor+0x44);
|
||||
var prime=AccelerationPolicy.ShouldPrime(frame,owner,true,field);
|
||||
AccelerationPolicy.Run(ref field,prime,()=>hook!.Original(actor));
|
||||
});
|
||||
hook.Enable();
|
||||
});
|
||||
modules.Register(3, scope =>
|
||||
{
|
||||
Hook<Permission>? hook = null;
|
||||
hook = game.Hook<Permission>(scope, Signatures.MovePermissionAddress, (a1,a2,a3,a4) =>
|
||||
game.Ready && NativeIds.MovementPermissionChecks.Contains(a2) ? 1 : hook!.Original(a1,a2,a3,a4));
|
||||
hook.Enable();
|
||||
});
|
||||
modules.Register(4, scope =>
|
||||
{
|
||||
Hook<Knock>? hook = null;
|
||||
hook = game.Hook<Knock>(scope, Signatures.AntiKnockAddress, (actor,angle,distance,seconds,a5,a6) =>
|
||||
{
|
||||
if (actor != 0 && actor == game.LocalAddress)
|
||||
{ distance = config.Value(4); seconds = config.Published.KnockSeconds; }
|
||||
return hook!.Original(actor,angle,distance,seconds,a5,a6);
|
||||
});
|
||||
hook.Enable();
|
||||
});
|
||||
modules.Register(5, scope =>
|
||||
{
|
||||
Hook<Fall>? hook = null;
|
||||
hook = game.Hook<Fall>(scope, Signatures.OnSendFlag, (a1,flags) => hook!.Original(a1,game.Ready ? Policies.FallFlags(flags) : flags));
|
||||
hook.Enable();
|
||||
});
|
||||
modules.Register(6, scope =>
|
||||
{
|
||||
Hook<FallCheck>? hook = null;
|
||||
hook = game.Hook<FallCheck>(scope, Signatures.FallCheckPatch, (a1,a2,a3) => game.Ready ? -1 : hook!.Original(a1,a2,a3));
|
||||
hook.Enable();
|
||||
});
|
||||
modules.Register(7, scope =>
|
||||
{
|
||||
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("强制移动入口参数仅核对了 2026.09.15 客户端");
|
||||
Hook<Forced>? hook = null;
|
||||
hook = game.Hook<Forced>(scope, Signatures.FuckBewithAction, (actor,x,y,z,movementParameter,options) =>
|
||||
{
|
||||
// This intercepts a forced-coordinate movement path; it does not remove a status.
|
||||
if(actor!=0 && actor==game.LocalAddress) return 0;
|
||||
return hook!.Original(actor,x,y,z,movementParameter,options);
|
||||
});
|
||||
hook.Enable();
|
||||
});
|
||||
}
|
||||
|
||||
private void StartHeight(ResourceScope scope)
|
||||
{
|
||||
Height.Reset();
|
||||
Hook<Move>? normal = null, combat = null;
|
||||
normal = game.Hook<Move>(scope, Signatures.OnSendNormalMove, (actor,data,arg) => ApplyHeight(normal!.Original,actor,data,arg,false));
|
||||
combat = game.Hook<Move>(scope, Signatures.OnSendCombatMove, (actor,data,arg) => ApplyHeight(combat!.Original,actor,data,arg,true));
|
||||
scope.Defer(Height.Reset);
|
||||
normal.Enable(); combat.Enable();
|
||||
}
|
||||
private nint ApplyHeight(Move original, nuint actor, nint data, uint arg, bool combat)
|
||||
{
|
||||
float delta;
|
||||
try
|
||||
{
|
||||
delta=EffectiveHeight;
|
||||
}
|
||||
catch(Exception ex) { game.Fault(9,ex); return original(actor,data,arg); }
|
||||
if (data == 0 || delta == 0) return original(actor,data,arg);
|
||||
adjusted ??= [];
|
||||
if (!adjusted.Add(data)) return original(actor,data,arg);
|
||||
var values = (float*)data;
|
||||
var first = combat ? 4 : 3;
|
||||
float old = values[first], old2 = combat ? values[7] : 0;
|
||||
try
|
||||
{
|
||||
if (!float.IsFinite(old) || (combat && !float.IsFinite(old2))) return original(actor,data,arg);
|
||||
values[first] = old + delta;
|
||||
if (combat) values[7] = old2 + delta;
|
||||
try { return original(actor,data,arg); }
|
||||
finally { values[first] = old; if (combat) values[7] = old2; }
|
||||
}
|
||||
finally { adjusted.Remove(data); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using ICRebuild.Core;
|
||||
|
||||
namespace ICRebuild.Runtime;
|
||||
|
||||
// Feature 36 owns subscriptions; native hooks may also serve independent consumers.
|
||||
public sealed class NetworkCapture(NetworkManager network,ModuleManager modules,PacketBuffer buffer)
|
||||
{
|
||||
private readonly PacketRecorder recorder=new(buffer);
|
||||
public long Errors => network.Pipeline.ReadErrors;
|
||||
public long CompleteUp => recorder.CompleteUp;
|
||||
public long CompleteDown => recorder.CompleteDown;
|
||||
public long MetadataOnly => recorder.MetadataOnly;
|
||||
public string LastIssue => network.Pipeline.LastIssue;
|
||||
public void Register() => modules.Register(36,scope =>
|
||||
{
|
||||
network.Observe(scope,NetworkPhase.Received,recorder.Record);
|
||||
network.Observe(scope,NetworkPhase.BeforeSend,recorder.Record);
|
||||
scope.Defer(()=>buffer.Paused=true);
|
||||
buffer.Paused=false;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud;
|
||||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using ICRebuild.Core;
|
||||
|
||||
namespace ICRebuild.Runtime;
|
||||
|
||||
// Shared Zone hooks; acquire/release from module lifecycle code, never from callbacks.
|
||||
public sealed unsafe class NetworkManager : IDisposable
|
||||
{
|
||||
public const string UpSignature="48 89 5C 24 ?? 48 89 74 24 ?? 4C 89 64 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 70";
|
||||
// Stack-save offset 18 is intentional: wildcarding it also matches chat receive.
|
||||
public const string DownSignature="48 89 5C 24 18 57 48 83 EC 20 48 83 B9 88 00 00 00 00 48 8B FA 48 8B D9";
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
private delegate byte ZoneUp(nint connection,nint data,nint arg,byte flag);
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
private delegate byte ZoneReceive(nint connection,nint holder);
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
private delegate byte ReceiverSendPacket(nint receiver,nint packet,uint a3,uint a4);
|
||||
private readonly SharedModuleResource hooks;
|
||||
private readonly GameContext game;
|
||||
private readonly string version;
|
||||
private readonly PacketMemoryReader memory=SafeMemory.ReadBytes;
|
||||
private ReceiverSendPacket? receiverSend;
|
||||
private bool disposed;
|
||||
public NetworkPipeline Pipeline { get; }
|
||||
public ActivePacketDispatcher ActiveSender { get; }
|
||||
public bool CanSendNative => receiverSend != null;
|
||||
|
||||
public NetworkManager(GameContext game,string version)
|
||||
{
|
||||
this.game=game; this.version=version;
|
||||
Pipeline=new(version); ActiveSender=new(version); hooks=new(Start);
|
||||
}
|
||||
public void Observe(ResourceScope owner,NetworkPhase phase,Action<NetworkObservation> callback,int? opcode=null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed,this);
|
||||
var subscription=new ResourceScope();
|
||||
try
|
||||
{
|
||||
hooks.Acquire(subscription);
|
||||
subscription.Own(Pipeline.Subscribe(phase,callback,opcode));
|
||||
owner.Own(subscription);
|
||||
}
|
||||
catch { subscription.Dispose(); throw; }
|
||||
}
|
||||
public SendTransactionSnapshot EnqueueSend(EncodedGamePacket packet)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed,this);
|
||||
if(receiverSend==null)
|
||||
throw new InvalidOperationException("请先开启 36(网络记录/管线)并确认主动发送入口已就绪");
|
||||
return ActiveSender.Enqueue(packet,game.ReadSendSession());
|
||||
}
|
||||
public SendTransactionSnapshot? PumpActiveSends()
|
||||
{
|
||||
if(disposed) return null;
|
||||
return ActiveSender.Pump(game.ReadSendSession(),SendNative);
|
||||
}
|
||||
private byte SendNative(ReadOnlySpan<byte> nativeBuffer)
|
||||
{
|
||||
var sender=receiverSend;
|
||||
if(sender==null || nativeBuffer.Length<NativeSendPacket.NativePrefixSize) return 0;
|
||||
var fw=Framework.Instance();
|
||||
if(fw==null || fw->NetworkModuleProxy==null) return 0;
|
||||
var receiver=*(nint*)((nint)fw->NetworkModuleProxy+8);
|
||||
if(receiver==0) return 0;
|
||||
var padded=new byte[Math.Max(0xF60,nativeBuffer.Length)];
|
||||
nativeBuffer.CopyTo(padded);
|
||||
fixed(byte* ptr=padded)
|
||||
{
|
||||
return sender(receiver,(nint)ptr,0,0);
|
||||
}
|
||||
}
|
||||
private void Start(ResourceScope scope)
|
||||
{
|
||||
if(!NativePacketCapture.IsSupportedVersion(version))
|
||||
throw new InvalidOperationException($"收发包管理器仅适配国际服已审计版本,当前为 {version}。");
|
||||
Hook<ZoneReceive>? down=null;
|
||||
Hook<ZoneUp>? up=null;
|
||||
down=game.Hook<ZoneReceive>(scope,DownSignature,(connection,holder)=>Pipeline.Receive(
|
||||
()=>down!.Original(connection,holder),accepts=>NativePacketCapture.Down(memory,holder,accepts)));
|
||||
up=game.Hook<ZoneUp>(scope,UpSignature,(connection,data,arg,flag)=>Pipeline.Send(
|
||||
()=>up!.Original(connection,data,arg,flag),accepts=>NativePacketCapture.Up(memory,data,accepts)));
|
||||
scope.Own(ActiveSender.Attach(Pipeline));
|
||||
try { receiverSend=Marshal.GetDelegateForFunctionPointer<ReceiverSendPacket>(game.Scan(AdaptationManifest.ActiveSendReceiverSignature)); }
|
||||
catch { receiverSend=null; }
|
||||
scope.Defer(()=>
|
||||
{
|
||||
receiverSend=null;
|
||||
ActiveSender.CancelAllQueued("收发包管线已关闭,取消待发队列");
|
||||
});
|
||||
down.Enable(); up.Enable();
|
||||
}
|
||||
// Module owners are released before the manager during Plugin.Dispose.
|
||||
public void Dispose()
|
||||
{
|
||||
if(disposed) return;
|
||||
disposed=true;
|
||||
ActiveSender.CancelAllQueued("插件卸载,取消待发队列");
|
||||
Pipeline.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace ICRebuild.Runtime;
|
||||
|
||||
// All slider call sites use this helper so precise input has the same limits as dragging.
|
||||
public sealed class PrecisionSliders
|
||||
{
|
||||
private float editing,firstClickValue;
|
||||
private uint firstClickId;
|
||||
private bool focus;
|
||||
public bool Float(string label,ref float value,float min,float max)
|
||||
{
|
||||
ImGui.PushID(label);
|
||||
var before=value;
|
||||
var id=ImGui.GetID("slider");
|
||||
var changed=ImGui.SliderFloat(label,ref value,min,max,"%.3f",ImGuiSliderFlags.AlwaysClamp|ImGuiSliderFlags.NoRoundToFormat);
|
||||
if(ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
value=firstClickId==id ? firstClickValue : before;
|
||||
changed=value!=before;
|
||||
editing=value; focus=true;
|
||||
ImGui.OpenPopup("输入精确数值");
|
||||
}
|
||||
else if(ImGui.IsItemClicked(ImGuiMouseButton.Left)) { firstClickId=id; firstClickValue=before; }
|
||||
if(ImGui.IsItemHovered()) ImGui.SetTooltip($"双击输入精确数值,或 Ctrl+单击直接输入。当前值:{value:G9}");
|
||||
if(ImGui.BeginPopup("输入精确数值"))
|
||||
{
|
||||
ImGui.TextUnformatted(label);
|
||||
ImGui.Text($"范围:{min:G9} ~ {max:G9}");
|
||||
if(focus) { ImGui.SetKeyboardFocusHere(); focus=false; }
|
||||
ImGui.SetNextItemWidth(210);
|
||||
var enter=ImGui.InputFloat("数值",ref editing,0,0,"%.6f",ImGuiInputTextFlags.EnterReturnsTrue|ImGuiInputTextFlags.AutoSelectAll);
|
||||
if(!float.IsFinite(editing)) ImGui.TextUnformatted("请输入有限数值。");
|
||||
ImGui.BeginDisabled(!float.IsFinite(editing));
|
||||
var apply=ImGui.Button("确定");
|
||||
ImGui.EndDisabled();
|
||||
if((enter || apply) && float.IsFinite(editing))
|
||||
{
|
||||
value=Math.Clamp(editing,min,max); changed=true; ImGui.CloseCurrentPopup();
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if(ImGui.Button("取消")) ImGui.CloseCurrentPopup();
|
||||
ImGui.TextUnformatted("超出范围的数值按上下限保存。");
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
ImGui.PopID();
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ICRebuild.Runtime;
|
||||
// Historical signatures supplied in IC. Successful scanning does not verify the native ABI.
|
||||
internal static class Signatures
|
||||
{
|
||||
public const string AntiKnockAddress = "48 ?? ?? 57 48 ?? ?? ?? ?? ?? ?? 0f 29 70 ?? 0f ?? ??";
|
||||
public const string MovePermissionAddress = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 54 41 56 41 57 48 83 EC ?? 33 DB 41 8B E9 48 39 1D";
|
||||
public const string SpeedAddress = "40 57 48 83 EC ?? 48 8B F9 48 8B 49 ?? 48 8B 01 FF 90 ?? ?? ?? ?? 48 85 C0 75";
|
||||
public const string OnSendFlag = "48 89 5c 24 ?? 57 48 ?? ?? ?? 8b ?? 48 ?? ?? 33 ?? e8 ?? ?? ?? ?? 84 ??";
|
||||
public const string FallCheckPatch = "e8 ?? ?? ?? ?? 8b ?? 85 ?? 0f 88 ?? ?? ?? ?? 48 ?? ?? ?? f3 ?? ?? ?? ?? ?? ?? ??";
|
||||
public const string ActionRange = "48 89 5C 24 ?? 57 48 ?? ?? ?? 48 ?? ?? ?? ?? ?? ?? 8B ?? 0F 29 74 24 20";
|
||||
public const string ActorRadius = "e8 ?? ?? ?? ?? f3 ?? ?? ?? 48 ?? ?? ?? ?? f3 ?? ?? ?? ?? f3 ?? ?? ?? f3 ?? ?? ?? ?? f3 0f 11 74 24 ?? ";
|
||||
// 2026.09.15 native gl_target VFX update, RVA 0x7B5430. Does not calculate movement.
|
||||
public const string NativeTargetRingUpdate = "4C 8B DC 55 57 41 57 49 8D AB ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 80 B9 ?? ?? ?? ?? ?? 4C 8B FA 48 8B F9 0F 85 ?? ?? ?? ?? 48 8B 41 30";
|
||||
public const string NoBackswing = "48 ?? ?? ?? 48 ?? ?? ?? 45 ?? ?? 33 ?? e8 ?? ?? ?? ?? 84 ?? 74 ?? ";
|
||||
// 2026.09.15: common movement initializer, called by the original NoActionMove wrapper.
|
||||
public const string ActionMovementStart = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 48 8B D9 0F B6 FA 48 81 C1 ?? ?? ?? ?? 4D 8B F1 4D 8B F8 E8 ?? ?? ?? ?? 48 8B B3 ?? ?? ?? ??";
|
||||
public const string FuckBewithAction = "e8 ?? ?? ???? b0 ?? c7 43 ?? ?? ?? ?? ?? eb ??";
|
||||
public const string OnSendNormalMove = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B F9 41 8B D8";
|
||||
public const string OnSendCombatMove = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B F9 41 8B F0";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user