diff --git a/Configuration.cs b/Configuration.cs index 68ba536..630337e 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -20,6 +20,9 @@ public sealed class Configuration : IPluginConfiguration [Newtonsoft.Json.JsonIgnore] 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 bool SmartGroundCastAtTarget { get; set; } = true; + public bool FastEventFinish { get; set; } = false; + public bool AutoRetainerReventure { get; set; } = true; public float Value(int id) { return Published.Value(id); diff --git a/Core/ActivePacketSender.cs b/Core/ActivePacketSender.cs index 7f0d320..b9b883e 100644 --- a/Core/ActivePacketSender.cs +++ b/Core/ActivePacketSender.cs @@ -160,6 +160,31 @@ public static class AuditedPacketEncoder : 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 { < 0 or > 255 => throw new ArgumentOutOfRangeException(nameof(argCount), "事件参数数量超出 0..255 范围"), diff --git a/Plugin.cs b/Plugin.cs index bb16f92..6806d45 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -56,6 +56,16 @@ public sealed class Plugin : IDalamudPlugin private int manualSendRequestId = 1; private int manualTriggerCommand = 3; 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, IObjectTable objects,IClientState client,ICondition conditions,IDataManager data,ISigScanner scanner, @@ -79,6 +89,7 @@ public sealed class Plugin : IDalamudPlugin }); network=new(game,gameVersion); capture=new(network,modules,packets); capture.Register(); + eventSubscription=network.Pipeline.Subscribe(NetworkPhase.Received,OnNetworkReceivedForEvents); modules.Register(37,_ => { }); ValidationCatalog.Apply(modules,gameVersion); Try(()=> @@ -90,14 +101,33 @@ public sealed class Plugin : IDalamudPlugin serviceBar=dtrBar.Get("IC Rebuild · 高度与迷宫"); serviceBar.Shown=false; 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; framework.Update+=Update; } private void Open() => open=true; 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.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"); } if(modules.IsEnabled(11)) { @@ -164,7 +194,19 @@ public sealed class Plugin : IDalamudPlugin var exportRecords=packets.Snapshot(); pending=Background(()=>PacketFiles.ExportAtomic(exportPath,exportRecords)); 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() { - if(!ImGui.CollapsingHeader("主动发送与请求响应事务 (M4)")) return; + if(!ImGui.CollapsingHeader("主动发送与业务扩展 (M4)")) return; var session=game.ReadSendSession(); var dispatcher=network.ActiveSender; ImGui.TextWrapped($"原生发送通道:{(network.CanSendNative ? "已就绪 (ReceiverSendPacket + ZoneUp)" : "未就绪(需开启 36 或完成绑定)")} · 队列 {dispatcher.PendingQueueCount} · 等待响应 {dispatcher.InFlightCount}"); ImGui.TextWrapped($"发送会话:{(session.IsValid ? $"就绪 · 区域 {session.Territory} · Entity 0x{session.LocalEntityId:X8}" : "未就绪(需角色在场且非过图/占卜回放)")}"); if(opcodes.Snapshot.GameVersion!=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.Button("加入技能发送队列 (当前目标)")) Try(()=> + + if(ImGui.CollapsingHeader("1. 地面技能智能释放 (Smart Ground Cast)", ImGuiTreeNodeFlags.DefaultOpen)) { - 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.TextWrapped("无需鼠标框选瞄准,通过 UP_ActionSendPos 瞬间以当前目标或自身坐标为圆心释放地面区域技能。"); + var smartTarget = config.SmartGroundCastAtTarget; + if (ImGui.Checkbox("优先释放在当前目标位置(若无目标则释放在脚下)##smartGroundTarget", ref smartTarget)) + { + config.SmartGroundCastAtTarget = smartTarget; + Try(Save); + } + ImGui.InputInt("地面技能 ID##smartGroundAction", ref smartGroundActionId); + if (ImGui.Button("释放地面技能 (UP_ActionSendPos)")) Try(() => + { + if (smartGroundActionId <= 0) throw new ArgumentException("请输入有效技能 ID"); + ExecuteGroundCast((uint)smartGroundActionId, config.SmartGroundCastAtTarget); + }); + ImGui.SameLine(); + ImGui.Text("常用预设:"); + ImGui.SameLine(); + if (ImGui.SmallButton("野战治疗阵 (188)")) Try(() => { smartGroundActionId = 188; ExecuteGroundCast(188, config.SmartGroundCastAtTarget); }); + ImGui.SameLine(); + if (ImGui.SmallButton("庇护所 (3569)")) Try(() => { smartGroundActionId = 3569; ExecuteGroundCast(3569, config.SmartGroundCastAtTarget); }); + ImGui.SameLine(); + if (ImGui.SmallButton("地星 (7439)")) Try(() => { smartGroundActionId = 7439; ExecuteGroundCast(7439, config.SmartGroundCastAtTarget); }); + ImGui.SameLine(); + 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)) { 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}"); } } + 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.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) { 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"); pi.UiBuilder.Draw-=Draw; pi.UiBuilder.OpenConfigUi-=Open; pi.UiBuilder.OpenMainUi-=Open; serviceBar.OnClick=null; serviceBar.Dispose(); + eventSubscription.Dispose(); modules.Dispose(); network.Dispose(); game.Frames.Clear(); cancellation.Cancel(); http.Dispose(); cancellation.Dispose(); } } diff --git a/Tests/Program.cs b/Tests/Program.cs index b7e4848..aed7f8a 100644 --- a/Tests/Program.cs +++ b/Tests/Program.cs @@ -963,7 +963,7 @@ Test("stage 3 M4 native send buffer layout and audited encoders roundtrip throug { const string v=PacketParser.AuditedClientVersion; 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); 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.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(()=>AuditedPacketEncoder.EncodeActionSendPos(map,v,188,1,322,new System.Numerics.Vector3(float.NaN,0,0))); + 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);