Files
IC.Rebuild/Runtime/CombatModules.cs
T

217 lines
12 KiB
C#

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(!AccelerationPolicy.IsAuditedVersion(game.GameVersion)) throw new InvalidOperationException($"原生目标圈入口仅核对了已审计客户端,当前为 {game.GameVersion}。");
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(!AccelerationPolicy.IsAuditedVersion(game.GameVersion)) throw new InvalidOperationException($"技能位移结构仅核对了已审计客户端,当前为 {game.GameVersion}。");
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);
}
}