Initial import of IC.Rebuild

This commit is contained in:
2026-10-13 22:55:05 +08:00
commit 16d61f7a5e
87 changed files with 17054 additions and 0 deletions
+216
View File
@@ -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);
}
}
+64
View File
@@ -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();
}
}
+115
View File
@@ -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"; }
}
}
+133
View File
@@ -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); }
}
}
+21
View File
@@ -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;
});
}
+103
View File
@@ -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();
}
}
+49
View File
@@ -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;
}
}
+21
View File
@@ -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";
}
+76
View File
@@ -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();
});
}