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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user