feat: 实现地面技能智能释放、极速交互完成与雇员探险一键重派三项业务扩展

This commit is contained in:
2026-10-14 01:10:58 +08:00
parent bddff722dc
commit a6cccdbc43
4 changed files with 279 additions and 33 deletions
+3
View File
@@ -20,6 +20,9 @@ public sealed class Configuration : IPluginConfiguration
[Newtonsoft.Json.JsonIgnore] [Newtonsoft.Json.JsonIgnore]
public SettingsSnapshot Published => Volatile.Read(ref published); public SettingsSnapshot Published => Volatile.Read(ref published);
public string OpcodeUrl { get; set; } = "https://raw.githubusercontent.com/extrant/FFXIV.EXE/main/Opcode/all_opcodes.json"; public string OpcodeUrl { get; set; } = "https://raw.githubusercontent.com/extrant/FFXIV.EXE/main/Opcode/all_opcodes.json";
public bool SmartGroundCastAtTarget { get; set; } = true;
public bool FastEventFinish { get; set; } = false;
public bool AutoRetainerReventure { get; set; } = true;
public float Value(int id) public float Value(int id)
{ {
return Published.Value(id); return Published.Value(id);
+25
View File
@@ -160,6 +160,31 @@ public static class AuditedPacketEncoder
: null); : null);
} }
public static EncodedGamePacket EncodeActionSendPos(OpcodeMap map, string version, uint actionId, byte actionKind,
ushort requestId, System.Numerics.Vector3 position, ushort facing = 0, ushort targetFacing = 0)
{
RequireAuditedVersion(version);
if (actionId == 0) throw new ArgumentException("技能 ID 不能为 0", nameof(actionId));
if (requestId == 0) throw new ArgumentException("请求序号 RequestId 必须大于 0", nameof(requestId));
if (!float.IsFinite(position.X) || !float.IsFinite(position.Y) || !float.IsFinite(position.Z))
throw new ArgumentException("地面目标坐标包含无效浮点数", nameof(position));
var opcode = RequireOpcode(map, version, PacketDirection.Up, "UP_ActionSendPos");
var responseOpcode = map.ResolveOpcode(version, PacketDirection.Down, "DOWN_Effect");
var payload = new byte[0x18];
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0, 4), actionId);
payload[5] = actionKind;
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(6, 2), requestId);
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(8, 2), facing);
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(10, 2), targetFacing);
BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(12, 4), position.X);
BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(16, 4), position.Y);
BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(20, 4), position.Z);
return new("UP_ActionSendPos", opcode, payload, NativeSendPacket.BuildBuffer(opcode, payload), responseOpcode,
responseOpcode.HasValue
? p => p.Length == 0x7C && BinaryPrimitives.ReadUInt16LittleEndian(p.Slice(0x18, 2)) == requestId
: null);
}
private static (string Name, int Capacity, int Length) SelectEventTier(string prefix, int argCount) => argCount switch private static (string Name, int Capacity, int Length) SelectEventTier(string prefix, int argCount) => argCount switch
{ {
< 0 or > 255 => throw new ArgumentOutOfRangeException(nameof(argCount), "事件参数数量超出 0..255 范围"), < 0 or > 255 => throw new ArgumentOutOfRangeException(nameof(argCount), "事件参数数量超出 0..255 范围"),
+238 -32
View File
@@ -56,6 +56,16 @@ public sealed class Plugin : IDalamudPlugin
private int manualSendRequestId = 1; private int manualSendRequestId = 1;
private int manualTriggerCommand = 3; private int manualTriggerCommand = 3;
private int manualTriggerParam1 = 1; private int manualTriggerParam1 = 1;
private int smartGroundActionId = 188;
private uint lastEventId;
private ulong lastEventTargetId;
private uint lastEventParam;
private long lastEventTime;
private long lastRetainerActionTime;
private long lastAutoEventFinishTime;
private long lastTalkAdvanceTime;
private uint pendingEventFinishId;
private readonly IDisposable eventSubscription;
public Plugin(IDalamudPluginInterface pluginInterface,ICommandManager commands,IFramework framework, public Plugin(IDalamudPluginInterface pluginInterface,ICommandManager commands,IFramework framework,
IObjectTable objects,IClientState client,ICondition conditions,IDataManager data,ISigScanner scanner, IObjectTable objects,IClientState client,ICondition conditions,IDataManager data,ISigScanner scanner,
@@ -79,6 +89,7 @@ public sealed class Plugin : IDalamudPlugin
}); });
network=new(game,gameVersion); network=new(game,gameVersion);
capture=new(network,modules,packets); capture.Register(); capture=new(network,modules,packets); capture.Register();
eventSubscription=network.Pipeline.Subscribe(NetworkPhase.Received,OnNetworkReceivedForEvents);
modules.Register(37,_ => { }); modules.Register(37,_ => { });
ValidationCatalog.Apply(modules,gameVersion); ValidationCatalog.Apply(modules,gameVersion);
Try(()=> Try(()=>
@@ -90,14 +101,33 @@ public sealed class Plugin : IDalamudPlugin
serviceBar=dtrBar.Get("IC Rebuild · 高度与迷宫"); serviceBar=dtrBar.Get("IC Rebuild · 高度与迷宫");
serviceBar.Shown=false; serviceBar.Shown=false;
serviceBar.OnClick=_=>Open(); serviceBar.OnClick=_=>Open();
commands.AddHandler("/icr",new CommandInfo(Command) { HelpMessage="IC Rebuild 设置。on/off 编号;stop;pos X Y Z;face 弧度;scale 倍率;export [文件名]。" }); commands.AddHandler("/icr",new CommandInfo(Command) { HelpMessage="IC Rebuild 设置。on/off 编号;stop;pos X Y Z;ground 技能ID [target|self];finish [事件ID];retainer;face 弧度;scale 倍率;export [文件名]。" });
pi.UiBuilder.Draw+=Draw; pi.UiBuilder.OpenConfigUi+=Open; pi.UiBuilder.OpenMainUi+=Open; pi.UiBuilder.Draw+=Draw; pi.UiBuilder.OpenConfigUi+=Open; pi.UiBuilder.OpenMainUi+=Open;
framework.Update+=Update; framework.Update+=Update;
} }
private void Open() => open=true; private void Open() => open=true;
private void Update(IFramework _) private void Update(IFramework _)
{ {
try { game.Refresh(modules.IsEnabled(10)||modules.IsEnabled(11)); game.DrainFaults(modules); combat.Update(); network.PumpActiveSends(); } try
{
game.Refresh(modules.IsEnabled(10)||modules.IsEnabled(11));
game.DrainFaults(modules);
combat.Update();
network.PumpActiveSends();
var finishId=Interlocked.Exchange(ref pendingEventFinishId,0);
if(finishId!=0 && config.FastEventFinish)
{
Try(()=>
{
EnsureOpcodes();
var packet=AuditedPacketEncoder.EncodeEventFinish(opcodes,gameVersion,finishId,0,0,ReadOnlySpan<uint>.Empty);
var tx=network.EnqueueSend(packet);
notice=$"已自动结束事件 0x{finishId:X8} (事务 #{tx.Id})";
});
}
if(config.FastEventFinish) Try(ProcessFastDialogAdvance);
if(config.AutoRetainerReventure) Try(ProcessRetainerAutomation);
}
catch(Exception ex) { modules.Disable(20); notice=ex.Message; log.Error(ex,"IC frame update"); } catch(Exception ex) { modules.Disable(20); notice=ex.Message; log.Error(ex,"IC frame update"); }
if(modules.IsEnabled(11)) if(modules.IsEnabled(11))
{ {
@@ -164,7 +194,19 @@ public sealed class Plugin : IDalamudPlugin
var exportRecords=packets.Snapshot(); var exportRecords=packets.Snapshot();
pending=Background(()=>PacketFiles.ExportAtomic(exportPath,exportRecords)); pending=Background(()=>PacketFiles.ExportAtomic(exportPath,exportRecords));
chat.Print($"IC 开始导出 {exportRecords.Length} 条到 {exportPath}"); break; chat.Print($"IC 开始导出 {exportRecords.Length} 条到 {exportPath}"); break;
default: Open(); notice="用法:/icr on|off 编号;stop;pos X Y Z;face 弧度;scale 倍率"; break; case "ground" or "castpos" when args.Length>=2:
var actId=uint.Parse(args[1],CultureInfo.InvariantCulture);
var useTarget=args.Length<3 || !args[2].Equals("self",StringComparison.OrdinalIgnoreCase);
ExecuteGroundCast(actId,useTarget); break;
case "finish":
uint? evId=args.Length>=2 ? (args[1].StartsWith("0x",StringComparison.OrdinalIgnoreCase)
? uint.Parse(args[1][2..],NumberStyles.HexNumber)
: uint.Parse(args[1],CultureInfo.InvariantCulture)) : null;
ExecuteEventFinish(evId); break;
case "retainer" or "reventure":
ProcessRetainerAutomation();
chat.Print("[IC Rebuild] 已尝试一键处理当前雇员探险窗口"); break;
default: Open(); notice="用法:/icr on|off 编号;stop;pos X Y Z;ground 技能ID [target|self];finish [事件ID];retainer"; break;
} }
}); });
} }
@@ -366,43 +408,120 @@ public sealed class Plugin : IDalamudPlugin
} }
private void DrawActiveSendSection() private void DrawActiveSendSection()
{ {
if(!ImGui.CollapsingHeader("主动发送与请求响应事务 (M4)")) return; if(!ImGui.CollapsingHeader("主动发送与业务扩展 (M4)")) return;
var session=game.ReadSendSession(); var session=game.ReadSendSession();
var dispatcher=network.ActiveSender; var dispatcher=network.ActiveSender;
ImGui.TextWrapped($"原生发送通道:{(network.CanSendNative ? "已就绪 (ReceiverSendPacket + ZoneUp)" : "未就绪(需开启 36 或完成绑定)")} · 队列 {dispatcher.PendingQueueCount} · 等待响应 {dispatcher.InFlightCount}"); ImGui.TextWrapped($"原生发送通道:{(network.CanSendNative ? "已就绪 (ReceiverSendPacket + ZoneUp)" : "未就绪(需开启 36 或完成绑定)")} · 队列 {dispatcher.PendingQueueCount} · 等待响应 {dispatcher.InFlightCount}");
ImGui.TextWrapped($"发送会话:{(session.IsValid ? $"就绪 · 区域 {session.Territory} · Entity 0x{session.LocalEntityId:X8}" : "未就绪(需角色在场且非过图/占卜回放)")}"); ImGui.TextWrapped($"发送会话:{(session.IsValid ? $"就绪 · 区域 {session.Territory} · Entity 0x{session.LocalEntityId:X8}" : "未就绪(需角色在场且非过图/占卜回放)")}");
if(opcodes.Snapshot.GameVersion!=gameVersion) if(opcodes.Snapshot.GameVersion!=gameVersion)
ImGui.TextColored(new Vector4(1,0.4f,0.4f,1),$"提示:Opcode 映射尚未就绪(当前为 {opcodes.Snapshot.GameVersion});发送时将自动载入"); ImGui.TextColored(new Vector4(1,0.4f,0.4f,1),$"提示:Opcode 映射尚未就绪(当前为 {opcodes.Snapshot.GameVersion});发送时将自动载入");
ImGui.InputInt("测试技能 ID (UP_ActionSend)",ref manualSendActionId);
ImGui.InputInt("请求序号 RequestId",ref manualSendRequestId); if(ImGui.CollapsingHeader("1. 地面技能智能释放 (Smart Ground Cast)", ImGuiTreeNodeFlags.DefaultOpen))
if(ImGui.Button("加入技能发送队列 (当前目标)")) Try(()=>
{ {
EnsureOpcodes(); ImGui.TextWrapped("无需鼠标框选瞄准,通过 UP_ActionSendPos 瞬间以当前目标或自身坐标为圆心释放地面区域技能。");
if(manualSendActionId<=0) throw new ArgumentException("请输入有效技能 ID"); var smartTarget = config.SmartGroundCastAtTarget;
if(manualSendRequestId is <=0 or >ushort.MaxValue) manualSendRequestId=1; if (ImGui.Checkbox("优先释放在当前目标位置(若无目标则释放在脚下)##smartGroundTarget", ref smartTarget))
var targetId=game.Targets.Target?.EntityId is >0 and var tid ? (ulong)tid : session.LocalEntityId; {
var packet=AuditedPacketEncoder.EncodeActionSend(opcodes,gameVersion,(uint)manualSendActionId,1,(ushort)manualSendRequestId,targetId); config.SmartGroundCastAtTarget = smartTarget;
var tx=network.EnqueueSend(packet); Try(Save);
manualSendRequestId=manualSendRequestId>=ushort.MaxValue ? 1 : manualSendRequestId+1; }
notice=$"已加入发送事务 #{tx.Id}:{packet.Name} (0x{packet.Opcode:X4})"; ImGui.InputInt("地面技能 ID##smartGroundAction", ref smartGroundActionId);
}); if (ImGui.Button("释放地面技能 (UP_ActionSendPos)")) Try(() =>
ImGui.SameLine(); {
if(ImGui.Button("加入 ClientTrigger 队列")) Try(()=> if (smartGroundActionId <= 0) throw new ArgumentException("请输入有效技能 ID");
{ ExecuteGroundCast((uint)smartGroundActionId, config.SmartGroundCastAtTarget);
EnsureOpcodes(); });
if(manualTriggerCommand<=0 || manualTriggerCommand>ushort.MaxValue) throw new ArgumentException("指令编号超出范围"); ImGui.SameLine();
var packet=AuditedPacketEncoder.EncodeClientTrigger(opcodes,gameVersion,(uint)manualTriggerCommand,(uint)Math.Max(0,manualTriggerParam1)); ImGui.Text("常用预设:");
var tx=network.EnqueueSend(packet); ImGui.SameLine();
notice=$"已加入发送事务 #{tx.Id}:{packet.Name} (0x{packet.Opcode:X4})"; if (ImGui.SmallButton("野战治疗阵 (188)")) Try(() => { smartGroundActionId = 188; ExecuteGroundCast(188, config.SmartGroundCastAtTarget); });
}); ImGui.SameLine();
ImGui.SameLine(); if (ImGui.SmallButton("庇护所 (3569)")) Try(() => { smartGroundActionId = 3569; ExecuteGroundCast(3569, config.SmartGroundCastAtTarget); });
if(ImGui.Button("清空待发队列")) ImGui.SameLine();
{ if (ImGui.SmallButton("地星 (7439)")) Try(() => { smartGroundActionId = 7439; ExecuteGroundCast(7439, config.SmartGroundCastAtTarget); });
var canceled=dispatcher.CancelAllQueued("用户手动清空队列"); ImGui.SameLine();
notice=$"已取消 {canceled} 条待发事务"; if (ImGui.SmallButton("土遁 (2260)")) Try(() => { smartGroundActionId = 2260; ExecuteGroundCast(2260, config.SmartGroundCastAtTarget); });
ImGui.SameLine();
if (ImGui.SmallButton("铃兰 (25862)")) Try(() => { smartGroundActionId = 25862; ExecuteGroundCast(25862, config.SmartGroundCastAtTarget); });
} }
ImGui.InputInt("Trigger 指令 (ushort)",ref manualTriggerCommand);
ImGui.InputInt("Trigger 参数 1 (uint)",ref manualTriggerParam1); if(ImGui.CollapsingHeader("2. 交互/剧情极速跳过与开箱 (Fast Event Finish / Interact)", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.TextWrapped("监听 DOWN_EventStart,直接发送 UP_EventFinish 完成 NPC 对话、开宝箱、副本传送门与剧情过渡。");
var fastEv = config.FastEventFinish;
if (ImGui.Checkbox("自动极速结束交互与对话 (FastEventFinish)##fastEventFinish", ref fastEv))
{
config.FastEventFinish = fastEv;
Try(Save);
}
var evTimeStr = lastEventTime > 0 ? $"{(Environment.TickCount64 - lastEventTime) / 1000} 秒前" : "无";
ImGui.TextWrapped($"最近捕获事件:ID 0x{lastEventId:X8} · 目标 0x{lastEventTargetId:X8} · 参数 0x{lastEventParam:X8} · 时间 {evTimeStr}");
if (ImGui.Button("立即完成当前事件 (UP_EventFinish)")) Try(() => ExecuteEventFinish());
ImGui.SameLine();
if (ImGui.Button("与当前目标发起事件 (UP_EventStart)")) Try(() =>
{
EnsureOpcodes();
var targetId = game.Targets.Target?.EntityId is > 0 and var tid ? (ulong)tid : 0xE0000000;
if (targetId == 0xE0000000) throw new ArgumentException("请先选中一个可交互的目标");
var evId = lastEventId != 0 ? lastEventId : 1;
var packet = AuditedPacketEncoder.EncodeEventStart(opcodes, gameVersion, targetId, evId);
var tx = network.EnqueueSend(packet);
notice = $"已发送事件发起 #{tx.Id}:目标 0x{targetId:X8} 事件 0x{evId:X8}";
});
}
if(ImGui.CollapsingHeader("3. 生活自动化:雇员一键重派探险 (Retainer Re-Venture)", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.TextWrapped("打开雇员探险结算界面时,自动或一键触发【再次派遣】与【确认】,省去重复繁琐的鼠标点击。");
var autoRet = config.AutoRetainerReventure;
if (ImGui.Checkbox("自动重派雇员探险 (AutoRetainerReventure)##autoRetainer", ref autoRet))
{
config.AutoRetainerReventure = autoRet;
Try(Save);
}
if (ImGui.Button("一键重派当前雇员探险窗口")) Try(() =>
{
ProcessRetainerAutomation();
notice = "已尝试处理当前雇员探险窗口";
});
ImGui.SameLine();
ImGui.TextColored(new Vector4(0.4f, 1, 0.4f, 1), "状态:安全防抖 450ms 原生调用");
}
if(ImGui.CollapsingHeader("基础指令与发包诊断"))
{
ImGui.InputInt("测试技能 ID (UP_ActionSend)",ref manualSendActionId);
ImGui.InputInt("请求序号 RequestId",ref manualSendRequestId);
if(ImGui.Button("加入技能发送队列 (当前目标)")) Try(()=>
{
EnsureOpcodes();
if(manualSendActionId<=0) throw new ArgumentException("请输入有效技能 ID");
if(manualSendRequestId is <=0 or >ushort.MaxValue) manualSendRequestId=1;
var targetId=game.Targets.Target?.EntityId is >0 and var tid ? (ulong)tid : session.LocalEntityId;
var packet=AuditedPacketEncoder.EncodeActionSend(opcodes,gameVersion,(uint)manualSendActionId,1,(ushort)manualSendRequestId,targetId);
var tx=network.EnqueueSend(packet);
manualSendRequestId=manualSendRequestId>=ushort.MaxValue ? 1 : manualSendRequestId+1;
notice=$"已加入发送事务 #{tx.Id}:{packet.Name} (0x{packet.Opcode:X4})";
});
ImGui.SameLine();
if(ImGui.Button("加入 ClientTrigger 队列")) Try(()=>
{
EnsureOpcodes();
if(manualTriggerCommand<=0 || manualTriggerCommand>ushort.MaxValue) throw new ArgumentException("指令编号超出范围");
var packet=AuditedPacketEncoder.EncodeClientTrigger(opcodes,gameVersion,(uint)manualTriggerCommand,(uint)Math.Max(0,manualTriggerParam1));
var tx=network.EnqueueSend(packet);
notice=$"已加入发送事务 #{tx.Id}:{packet.Name} (0x{packet.Opcode:X4})";
});
ImGui.SameLine();
if(ImGui.Button("清空待发队列"))
{
var canceled=dispatcher.CancelAllQueued("用户手动清空队列");
notice=$"已取消 {canceled} 条待发事务";
}
ImGui.InputInt("Trigger 指令 (ushort)",ref manualTriggerCommand);
ImGui.InputInt("Trigger 参数 1 (uint)",ref manualTriggerParam1);
}
foreach(var tx in dispatcher.Snapshot().TakeLast(10)) foreach(var tx in dispatcher.Snapshot().TakeLast(10))
{ {
var rtt=tx.RoundTripMs is {} d ? $" · RTT {d} ms (下行 #{tx.ResponseObservationId})" : ""; var rtt=tx.RoundTripMs is {} d ? $" · RTT {d} ms (下行 #{tx.ResponseObservationId})" : "";
@@ -410,6 +529,92 @@ public sealed class Plugin : IDalamudPlugin
ImGui.TextWrapped($"事务 #{tx.Id} {tx.PacketName} (0x{tx.Opcode:X4}) [{tx.State}]{obs}{rtt} · {tx.Detail}"); ImGui.TextWrapped($"事务 #{tx.Id} {tx.PacketName} (0x{tx.Opcode:X4}) [{tx.State}]{obs}{rtt} · {tx.Detail}");
} }
} }
private void OnNetworkReceivedForEvents(NetworkObservation obs)
{
var msg=obs.Message;
if(!msg.Complete || msg.Direction!=PacketDirection.Down) return;
var downEventStartOpcode=opcodes.ResolveOpcode(gameVersion,PacketDirection.Down,"DOWN_EventStart");
if(downEventStartOpcode.HasValue && msg.Opcode==downEventStartOpcode.Value && msg.Payload.Length>=0x10)
{
lastEventTargetId=System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(msg.Payload[..8]);
lastEventId=System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(msg.Payload.Slice(8,4));
lastEventParam=System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(msg.Payload.Slice(12,4));
lastEventTime=Environment.TickCount64;
if(config.FastEventFinish && lastEventId!=0 && Environment.TickCount64-lastAutoEventFinishTime>=800)
{
lastAutoEventFinishTime=Environment.TickCount64;
Volatile.Write(ref pendingEventFinishId,lastEventId);
}
}
}
private void ExecuteGroundCast(uint actionId,bool atTarget)
{
EnsureOpcodes();
if(actionId==0) throw new ArgumentException("请输入有效地面技能 ID");
var session=game.ReadSendSession();
if(!session.IsValid) throw new InvalidOperationException("当前角色或区域会话未就绪");
var player=game.Objects.LocalPlayer;
if(player==null) throw new InvalidOperationException("本地角色未就绪");
Vector3 targetPos=atTarget && game.Targets.Target is { } target ? target.Position : player.Position;
ushort facing=(ushort)((player.Rotation+MathF.PI)/(MathF.PI*2f)*ushort.MaxValue);
manualSendRequestId=manualSendRequestId>=ushort.MaxValue ? 1 : manualSendRequestId+1;
var packet=AuditedPacketEncoder.EncodeActionSendPos(opcodes,gameVersion,actionId,1,(ushort)manualSendRequestId,targetPos,facing);
var tx=network.EnqueueSend(packet);
var destStr=atTarget && game.Targets.Target!=null ? $"目标【{game.Targets.Target.Name}】" : "自身脚下";
notice=$"已加入地面技能事务 #{tx.Id}:技能 {actionId} -> {destStr} ({targetPos.X:F1}, {targetPos.Y:F1}, {targetPos.Z:F1})";
chat.Print($"[IC Rebuild] 地面技能 {actionId} 已排队发送至 {destStr}");
}
private void ExecuteEventFinish(uint? explicitEventId=null)
{
EnsureOpcodes();
var targetEventId=explicitEventId ?? lastEventId;
if(targetEventId==0) throw new InvalidOperationException("当前无可用事件 ID,请先触发交互或传入目标事件 ID");
var packet=AuditedPacketEncoder.EncodeEventFinish(opcodes,gameVersion,targetEventId,0,0,ReadOnlySpan<uint>.Empty);
var tx=network.EnqueueSend(packet);
notice=$"已加入事件完成事务 #{tx.Id}:事件 ID 0x{targetEventId:X8}";
chat.Print($"[IC Rebuild] 立即完成事件 0x{targetEventId:X8} 已排队发送");
}
private unsafe void ProcessFastDialogAdvance()
{
var mgr=FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkUnitManager.Instance();
if(mgr==null) return;
var now=Environment.TickCount64;
if(now-lastTalkAdvanceTime<250) return;
var talk=mgr->GetAddonByName("Talk",1);
if(talk!=null && talk->IsVisible && talk->IsReady)
{
var val=stackalloc FFXIVClientStructs.FFXIV.Component.GUI.AtkValue[1];
val[0].SetInt(0);
talk->FireCallback(1,val,true);
lastTalkAdvanceTime=now;
}
}
private unsafe void ProcessRetainerAutomation()
{
var mgr=FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkUnitManager.Instance();
if(mgr==null) return;
var now=Environment.TickCount64;
if(now-lastRetainerActionTime<450) return;
var resultAddon=mgr->GetAddonByName("RetainerTaskResult",1);
if(resultAddon!=null && resultAddon->IsVisible && resultAddon->IsReady)
{
var val=stackalloc FFXIVClientStructs.FFXIV.Component.GUI.AtkValue[1];
val[0].SetInt(0);
resultAddon->FireCallback(1,val,true);
lastRetainerActionTime=now;
notice="已自动执行雇员探险【再次派遣】";
return;
}
var askAddon=mgr->GetAddonByName("RetainerTaskAsk",1);
if(askAddon!=null && askAddon->IsVisible && askAddon->IsReady)
{
var val=stackalloc FFXIVClientStructs.FFXIV.Component.GUI.AtkValue[1];
val[0].SetInt(0);
askAddon->FireCallback(1,val,true);
lastRetainerActionTime=now;
notice="已自动执行雇员探险【确定派遣】";
}
}
private string DataPath(string name) private string DataPath(string name)
{ {
if(string.IsNullOrWhiteSpace(name) || Path.GetFileName(name)!=name || name.IndexOfAny(Path.GetInvalidFileNameChars())>=0) throw new ArgumentException("请输入当前记录目录中的文件名"); if(string.IsNullOrWhiteSpace(name) || Path.GetFileName(name)!=name || name.IndexOfAny(Path.GetInvalidFileNameChars())>=0) throw new ArgumentException("请输入当前记录目录中的文件名");
@@ -468,6 +673,7 @@ public sealed class Plugin : IDalamudPlugin
framework.Update-=Update; commands.RemoveHandler("/icr"); framework.Update-=Update; commands.RemoveHandler("/icr");
pi.UiBuilder.Draw-=Draw; pi.UiBuilder.OpenConfigUi-=Open; pi.UiBuilder.OpenMainUi-=Open; pi.UiBuilder.Draw-=Draw; pi.UiBuilder.OpenConfigUi-=Open; pi.UiBuilder.OpenMainUi-=Open;
serviceBar.OnClick=null; serviceBar.Dispose(); serviceBar.OnClick=null; serviceBar.Dispose();
eventSubscription.Dispose();
modules.Dispose(); network.Dispose(); game.Frames.Clear(); cancellation.Cancel(); http.Dispose(); cancellation.Dispose(); modules.Dispose(); network.Dispose(); game.Frames.Clear(); cancellation.Cancel(); http.Dispose(); cancellation.Dispose();
} }
} }
+13 -1
View File
@@ -963,7 +963,7 @@ Test("stage 3 M4 native send buffer layout and audited encoders roundtrip throug
{ {
const string v=PacketParser.AuditedClientVersion; const string v=PacketParser.AuditedClientVersion;
var map=new OpcodeMap(); var map=new OpcodeMap();
map.Load($$$"""{"{{{v}}}":{"UP_ActionSend":[913],"UP_EventStart":[720],"UP_ClientTrigger":[963],"UP_EventFinish":[501],"UP_EventFinish4":[502],"UP_EventFinish8":[503],"UP_EventFinish16":[504],"UP_EventFinish32":[505],"UP_EventFinish64":[506],"UP_EventFinish128":[507],"UP_EventFinish255":[508],"UP_EventAction":[601],"UP_EventAction4":[602],"UP_EventAction8":[603],"UP_EventAction16":[604],"UP_EventAction32":[605],"UP_EventAction64":[606],"UP_EventAction128":[607],"UP_EventAction255":[608],"DOWN_Effect":[787],"DOWN_EventStart":[737],"DOWN_EventFinish":[217]}}""",v,"test"); map.Load($$$"""{"{{{v}}}":{"UP_ActionSend":[913],"UP_ActionSendPos":[891],"UP_EventStart":[720],"UP_ClientTrigger":[963],"UP_EventFinish":[501],"UP_EventFinish4":[502],"UP_EventFinish8":[503],"UP_EventFinish16":[504],"UP_EventFinish32":[505],"UP_EventFinish64":[506],"UP_EventFinish128":[507],"UP_EventFinish255":[508],"UP_EventAction":[601],"UP_EventAction4":[602],"UP_EventAction8":[603],"UP_EventAction16":[604],"UP_EventAction32":[605],"UP_EventAction64":[606],"UP_EventAction128":[607],"UP_EventAction255":[608],"DOWN_Effect":[787],"DOWN_EventStart":[737],"DOWN_EventFinish":[217]}}""",v,"test");
var act=AuditedPacketEncoder.EncodeActionSend(map,v,7535,1,321,0x10001234); var act=AuditedPacketEncoder.EncodeActionSend(map,v,7535,1,321,0x10001234);
Check(act.Name=="UP_ActionSend" && act.Opcode==913 && act.Payload.Length==32 && act.ExpectedResponseOpcode==787); Check(act.Name=="UP_ActionSend" && act.Opcode==913 && act.Payload.Length==32 && act.ExpectedResponseOpcode==787);
@@ -978,6 +978,18 @@ Test("stage 3 M4 native send buffer layout and audited encoders roundtrip throug
Check(captured.Error==null && captured.Packet is {Complete:true,Opcode:913}); Check(captured.Error==null && captured.Packet is {Complete:true,Opcode:913});
Check(captured.Packet!.Payload.SequenceEqual(act.Payload)); Check(captured.Packet!.Payload.SequenceEqual(act.Payload));
var pos=AuditedPacketEncoder.EncodeActionSendPos(map,v,188,1,322,new System.Numerics.Vector3(100.5f,20.0f,-50.25f),512,256);
Check(pos.Name=="UP_ActionSendPos" && pos.Opcode==891 && pos.Payload.Length==24 && pos.ExpectedResponseOpcode==787);
Check(PacketParser.Parse(pos.Name,pos.Payload,v).Summary.Contains("请求序号=322") && PacketParser.Parse(pos.Name,pos.Payload,v).Summary.Contains("X=100.500"));
var rawPos=pos.NativeBuffer;
Check(rawPos.Length==NativeSendPacket.NativePrefixSize+pos.Payload.Length);
mem.Blocks[0x5000]=rawPos[..16];
mem.Blocks[0x5020]=rawPos[0x20..(0x20+pos.Payload.Length)];
var capturedPos=NativePacketCapture.Up(mem.Read,0x5000,_=>true);
Check(capturedPos.Error==null && capturedPos.Packet is {Complete:true,Opcode:891});
Check(capturedPos.Packet!.Payload.SequenceEqual(pos.Payload));
Throws<ArgumentException>(()=>AuditedPacketEncoder.EncodeActionSendPos(map,v,188,1,322,new System.Numerics.Vector3(float.NaN,0,0)));
var evStart=AuditedPacketEncoder.EncodeEventStart(map,v,0x1000200030004000,0x00050001,7); var evStart=AuditedPacketEncoder.EncodeEventStart(map,v,0x1000200030004000,0x00050001,7);
Check(evStart.Payload.Length==16 && evStart.ExpectedResponseOpcode==737 && PacketParser.Parse(evStart.Name,evStart.Payload,v).Parsed); Check(evStart.Payload.Length==16 && evStart.ExpectedResponseOpcode==737 && PacketParser.Parse(evStart.Name,evStart.Payload,v).Parsed);