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 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() .ToDictionary(a=>a.RowId,a=>(Moves:a.AffectsPosition,Hostile:a.CanTargetHostile)); Hook? range=null; range=game.HookAddress(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() .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? rangeCheck = null; Hook? precheck = null; Hook? ringUpdate=null; Hook? radius=null; rangeCheck=game.HookAddress(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(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(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(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? hook = null; hook = game.Hook(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? hook = null; hook = game.Hook(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((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? read = null; Hook? sync = null; read = game.HookAddress(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(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? read = null; Hook? 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(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(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); } }