using System.Globalization; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Game.Command; using Dalamud.Game.Gui.Dtr; using Dalamud.Plugin; using Dalamud.Plugin.Services; using ICRebuild.Core; using ICRebuild.Runtime; namespace ICRebuild; public sealed class Plugin : IDalamudPlugin { private readonly IDalamudPluginInterface pi; private readonly ICommandManager commands; private readonly IFramework framework; private readonly IChatGui chat; private readonly IPluginLog log; private readonly Configuration config; private readonly IDtrBarEntry serviceBar; private readonly PrecisionSliders sliders = new(); private ServiceBarStatus? lastServiceStatus; private long lastServiceUpdate; private readonly ModuleManager modules = new(); private readonly GameContext game; private readonly CombatModules combat; private readonly MovementModules movement; private readonly StatusBlockModule statusBlock; private readonly CoordinateTools coordinates; private readonly NetworkCapture capture; private readonly NetworkManager network; private readonly PacketBuffer packets = new(); private readonly OpcodeMap opcodes = new(); private readonly HttpClient http = new() { Timeout = TimeSpan.FromSeconds(20) }; private readonly CancellationTokenSource cancellation = new(); private readonly string gameVersion = GameContext.ReadGameVersion(); private readonly string directory; private bool open, disposed; private Vector3 position; private float rotation; private int timeline; private int actionIdForTimeline; private int packetOpcode = -1; private int packetDirection; private string notice = "参数自动保存;每次重新加载后,所有功能默认关闭。"; private string file = "packets.jsonl"; private Task? pending; private Task? captureSave; private long lastCaptureSave; private readonly string captureSessionFile=$"capture-{DateTime.Now:yyyyMMdd-HHmmss}-{Guid.NewGuid():N}.jsonl"; private string captureSaveStatus="尚未保存"; private DungeonSnapshot dungeon = DungeonSnapshot.Empty; private AdaptationReport? adaptationReport; private int manualSendActionId = 7; 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 long lastYesnoTime; private string statusFilterText = ""; 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, IGameInteropProvider interop,ITargetManager targets,IChatGui chat,IPluginLog log,IDtrBar dtrBar) { pi=pluginInterface; this.commands=commands; this.framework=framework; this.chat=chat; this.log=log; config=pi.GetPluginConfig() as Configuration ?? new(); config.Normalize(); directory=pi.GetPluginConfigDirectory(); Directory.CreateDirectory(directory); game=new(objects,client,conditions,data,scanner,interop,targets); combat=new(game,config,modules); coordinates=new(game,modules); movement=new(game,config,modules); movement.Register(); combat.Register(); coordinates.Register(); statusBlock=new(game,config,modules); statusBlock.Register(); Try(()=> { combat.LoadMovementCatalog(); if(gameVersion!="unknown" && config.MovementListGameVersion!=gameVersion) { combat.AddAllMovementSkills(); Save(); notice=$"已按当前动作表导入 {combat.MovementSkills.Count(MovementSkillCatalog.Mappable)} 个玩家位移技能;所有模块仍默认关闭。"; } }); 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(()=> { adaptationReport=game.EvaluateAdaptation(modules); AdaptationManifest.ApplyGate(modules,adaptationReport); }); Try(() => EnsureOpcodes()); 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;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(); 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)) { try { dungeon=game.ReadDungeon(); } catch(Exception ex) { dungeon=DungeonSnapshot.Empty; game.Fault(11,ex); } } if(Environment.TickCount64-lastServiceUpdate>=200) { lastServiceUpdate=Environment.TickCount64; Try(UpdateServiceBar); } if(config.AutoSaveCapture && modules.IsEnabled(36) && Environment.TickCount64-lastCaptureSave>=5000 && captureSave?.IsCompleted!=false) { lastCaptureSave=Environment.TickCount64; var snapshot=packets.Snapshot(); if(snapshot.Length>0) captureSave=SaveCaptureAsync(DataPath(captureSessionFile),snapshot); } } private void UpdateServiceBar() { var status=ServiceBarPresentation.Build(game.Frames.Read(),modules.Status[9].State,modules.Status[10].State, modules.Status[11].State,config.Value(9),movement.Height.Applied); if(status==lastServiceStatus) return; serviceBar.Text=status.Text; serviceBar.Tooltip=status.Tooltip; serviceBar.Shown=status.Shown; lastServiceStatus=status; } private void Save() { config.Normalize(); pi.SavePluginConfig(config); } private void Toggle(int id,bool enabled) { if(enabled) { config.Requested.Add(id); modules.Enable(id); } else { config.Requested.Remove(id); modules.Disable(id); } foreach(var s in modules.Status.Values) if (!s.Requested) config.Requested.Remove(s.Feature.Id); Save(); } private void Try(Action action) { try { action(); } catch(Exception ex) { notice=ex.Message; log.Warning(ex,"IC operation failed"); } } private static float Number(string value) => float.Parse(value,NumberStyles.Float,CultureInfo.InvariantCulture); private void Command(string _,string arguments) { Try(() => { var args=arguments.Split(' ',StringSplitOptions.RemoveEmptyEntries); if(args.Length==0) { Open(); return; } switch(args[0].ToLowerInvariant()) { case "on" when args.Length==2: case "off" when args.Length==2: var id=int.Parse(args[1],CultureInfo.InvariantCulture); if(!modules.Status.ContainsKey(id)) throw new ArgumentException("编号不在首批范围"); Toggle(id,args[0].Equals("on",StringComparison.OrdinalIgnoreCase)); var state=modules.Status[id]; chat.Print($"IC {id}: {StatusPresentation.Summary(state)}{(StatusPresentation.ShowDetail(state) ? " · "+state.Detail : "")}"); break; case "stop": modules.DisableAll(); config.Requested.Clear(); Save(); break; case "pos" when args.Length==4: coordinates.Position(new(Number(args[1]),Number(args[2]),Number(args[3]))); break; case "face" when args.Length==2: coordinates.Facing(Number(args[1])); break; case "scale" when args.Length==2: coordinates.SetScale(Number(args[1])); break; case "export" when args.Length<=2: if(pending?.IsCompleted==false) throw new InvalidOperationException("已有文件操作正在运行"); var exportPath=DataPath(args.Length==2 ? args[1] : "packets.jsonl"); var exportRecords=packets.Snapshot(); pending=Background(()=>PacketFiles.ExportAtomic(exportPath,exportRecords)); chat.Print($"IC 开始导出 {exportRecords.Length} 条到 {exportPath}"); 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; case "yes": ConfirmYesno(true); break; case "no": ConfirmYesno(false); break; default: Open(); notice="用法:/icr on|off 编号;stop;pos X Y Z;ground 技能ID [target|self];finish [事件ID];retainer;yes;no"; break; } }); } private void Draw() { if(!open) return; ImGui.SetNextWindowSize(new(790,650),ImGuiCond.FirstUseEver); if(ImGui.Begin("IC Rebuild##ICRebuild",ref open)) { var region = (int)game.Client.ClientLanguage == 4 || game.Client.ClientLanguage.ToString().Contains("Chinese") ? "国服" : "国际服"; ImGui.TextWrapped($"IC Rebuild 0.1.16 · {region} · API 15 · 客户端 {gameVersion}"); if(ImGui.Button("关闭全部模块")) { modules.DisableAll(); config.Requested.Clear(); Try(Save); } ImGui.TextWrapped(notice); foreach(var group in Features.All.GroupBy(x=>x.Group)) { if(!ImGui.CollapsingHeader(group.Key)) continue; foreach(var feature in group) { ImGui.PushID(feature.Id); var enabled=modules.IsEnabled(feature.Id); if(ImGui.Checkbox($"{feature.Id} · {feature.Name}",ref enabled)) Try(()=>Toggle(feature.Id,enabled)); var status=modules.Status[feature.Id]; ImGui.TextWrapped(StatusPresentation.Summary(status)); if(StatusPresentation.ShowDetail(status)) ImGui.TextWrapped(status.Detail); if(feature.Min!=feature.Max) { var value=config.Value(feature.Id); if(sliders.Float(feature.Id==9 ? "预设高度偏移" : "参数",ref value,feature.Min,feature.Max)) { config.Values[feature.Id]=value; Try(Save); } } if(feature.Id==4) { if(ImGui.SmallButton("一键完全防击退 (距离与持续时间归零)##antiKnockZero")) { config.Values[4]=0; config.KnockSeconds=0; Try(Save); } } if(feature.Id==8) DrawStatusBlock(); if(feature.Id==9) { ImGui.BeginDisabled(!modules.IsEnabled(9)); if(ImGui.Button(movement.Height.IsApplied ? "回到 0" : "生效")) movement.Height.Toggle(modules.IsEnabled(9),config.Value(9)); ImGui.EndDisabled(); ImGui.SameLine(); ImGui.Text($"已应用:{movement.Height.Applied:F3} · 当前偏移:{movement.EffectiveHeight:F3}"); ImGui.TextWrapped("勾选后先设置参数,再点击生效。修改预设不会自动应用;回到 0 会取消偏移,位置同步后恢复原始高度。"); } if(feature.Id==14) { var frame=game.Frames.Read(); var observed=combat.LastTargetRadius; ImGui.TextWrapped(observed!=null && frame.Ready && observed.EntityId==frame.TargetEntityId ? $"当前目标原生半径 {observed.Original:F3} → {observed.Effective:F3};自身半径不增加。" : "等待当前目标的原生半径读取。"); ImGui.TextWrapped($"目标修改 {combat.TargetRadiusChanges} · 原生圈补偿 {combat.NativeRingBonuses} · 距离检查命中 {combat.RadiusBonuses}"); } if(feature.Id==18) { ImGui.TextWrapped("结印技能(天之印、地之印、人之印)复唱归零,结印无需等待 GCD。依赖模块 17。"); } if(feature.Id==20) { ImGui.Text("快速预设:"); ImGui.SameLine(); if(ImGui.SmallButton("极限 0.5s")) { config.Values[20]=0.5f; Try(Save); } ImGui.SameLine(); if(ImGui.SmallButton("平滑 0.6s")) { config.Values[20]=0.6f; Try(Save); } ImGui.SameLine(); if(ImGui.SmallButton("默认 0.7s")) { config.Values[20]=0.7f; Try(Save); } } ImGui.PopID(); } switch(group.Key) { case "移动": ImGui.TextWrapped("7 拦截本地角色的指定强制移动路径,不删除状态,也不等于解除所有眩晕或禁锢。"); var knock=config.KnockSeconds; if(sliders.Float("击退持续秒数",ref knock,0,5)) { config.KnockSeconds=knock; Try(Save); } break; case "深层迷宫": if(modules.IsEnabled(11)) ImGui.TextWrapped(dungeon.Description); ImGui.TextWrapped("专用模式须先开启飞天遁地。特殊层停止高度偏移;未知迷宫状态不应用专用模式偏移。"); break; case "战斗": DrawTimelines(); break; case "坐标": DrawCoordinates(); break; case "网络": DrawNetwork(); break; } } } ImGui.End(); } private void DrawStatusBlock() { ImGui.TextWrapped($"已选 {config.BlockedStatuses.Count} 项 · 本次启用拦截 {statusBlock.Blocked} 次 · 最近状态 ID {statusBlock.LastStatus}"); ImGui.TextWrapped("仅屏蔽本地角色后续收到的已选状态更新;开启前已有状态须等下一次更新。默认选择 1257 强制移动,空名单不屏蔽。录像回放暂停处理。"); ImGui.TextWrapped("关闭后恢复正常接收,已屏蔽状态须等服务端再次同步。本地处理不代表服务端状态被删除;实际移动或机制效果待实测。"); if(!ImGui.CollapsingHeader("状态名单管理(分类预设与搜索)")) return; ImGui.Text("快捷预设:"); ImGui.SameLine(); if(ImGui.SmallButton("全选滑冰##selSlippery")) { foreach(var id in StatusCatalog.LegacySlipperyCandidates.Where(StatusCatalog.AuditedBlockCandidates.Contains)) config.BlockedStatuses.Add(id); Try(Save); } ImGui.SameLine(); if(ImGui.SmallButton("全选强制移动##selMarch")) { foreach(var id in StatusCatalog.ForcedMarch.Where(StatusCatalog.AuditedBlockCandidates.Contains)) config.BlockedStatuses.Add(id); Try(Save); } ImGui.SameLine(); if(ImGui.SmallButton("全选混乱##selConf")) { foreach(var id in StatusCatalog.Confusion.Where(StatusCatalog.AuditedBlockCandidates.Contains)) config.BlockedStatuses.Add(id); Try(Save); } ImGui.SameLine(); if(ImGui.SmallButton("全选全部候选##selAll")) { foreach(var id in StatusCatalog.AuditedBlockCandidates) config.BlockedStatuses.Add(id); Try(Save); } ImGui.SameLine(); if(ImGui.SmallButton("清空已选##clearStatus")) { config.BlockedStatuses.Clear(); Try(Save); } ImGui.InputText("搜索状态名称或 ID##statusSearch",ref statusFilterText,64); var sheet=game.Data.GetExcelSheet(); foreach(var id in StatusCatalog.AuditedBlockCandidates.Order()) { var name=sheet?.GetRowOrDefault(id)?.Name.ToString() ?? "未知状态"; if(!string.IsNullOrWhiteSpace(statusFilterText)) { if(!id.ToString().Contains(statusFilterText,StringComparison.OrdinalIgnoreCase) && !name.Contains(statusFilterText,StringComparison.OrdinalIgnoreCase)) continue; } var selected=config.BlockedStatuses.Contains(id); if(ImGui.Checkbox($"{id} · {name}##status{id}",ref selected)) { if(selected) config.BlockedStatuses.Add(id); else config.BlockedStatuses.Remove(id); Try(Save); } } } private void DrawTimelines() { ImGui.TextWrapped("14 只扩大当前目标的原生半径,显示、距离与突进落点保持一致。突进应停在扩大后的目标圈边缘附近;自身半径不增加,不叠加为技能射程。"); ImGui.TextWrapped($"14 前置检查 {combat.RadiusPrechecks} · 最终检查 {combat.RadiusFinalChecks} · 距离补偿 {combat.RadiusBonuses}"); ImGui.TextWrapped($"14 原生圈更新 {combat.NativeRingCalls} · 绘制半径补偿 {combat.NativeRingBonuses}"); ImGui.TextWrapped($"14 目标半径修改 {combat.TargetRadiusChanges} · 自身半径保持原值 {combat.SelfRadiusSkips}"); if(!ImGui.CollapsingHeader($"16 · Timeline 名单管理({config.BlockedTimelines.Count} 项)###timeline-manager")) return; var mode=(int)(config.DashMode ?? DashFilterMode.AllLocal); if(ImGui.Combo("16 位移屏蔽模式",ref mode,"全部本地技能位移\0仅屏蔽名单内\0仅放行名单内\0")) { config.DashMode=(DashFilterMode)mode; Try(Save); } ImGui.TextWrapped("已按当前动作表预填玩家位移技能,默认仅屏蔽名单内。也可切换全部本地技能位移。空名单在名单模式下不屏蔽。"); if(ImGui.Button("加入全部位移技能")) Try(()=> { var added=combat.AddAllMovementSkills(); Save(); notice=$"已加入 {added} 个新位移编号,名单共有 {config.BlockedTimelines.Count} 个编号"; }); var unmapped=combat.MovementSkills.Where(s=>!MovementSkillCatalog.Mappable(s)).ToArray(); ImGui.TextWrapped($"动作表:{combat.MovementSkills.Length} 个玩家位移技能,{unmapped.Length} 个没有有效映射;共享位移编号的技能会联动。"); if(ImGui.CollapsingHeader("查看位移技能名单")) { foreach(var skill in combat.MovementSkills) { if(!MovementSkillCatalog.Mappable(skill)) { ImGui.TextWrapped($"未映射:{skill.Name} ({skill.ActionId})"); continue; } var selected=config.BlockedTimelines.Contains(skill.Timeline); if(ImGui.Checkbox($"{skill.Name} · {(skill.PvP ? "PvP" : "PvE")} · ID {skill.ActionId}##movement{skill.ActionId}",ref selected)) { if(selected) config.BlockedTimelines.Add(skill.Timeline); else config.BlockedTimelines.Remove(skill.Timeline); Try(Save); } } } ImGui.InputInt("技能 ID(转换到名单)",ref actionIdForTimeline); if(ImGui.Button("添加技能到名单")) Try(()=> { if(actionIdForTimeline<=0) throw new ArgumentException("请输入有效技能 ID"); var action=game.Data.GetExcelSheet().GetRowOrDefault((uint)actionIdForTimeline); if(action==null || action.Value.AnimationEnd.RowId==0 || action.Value.AnimationEnd.RowId>ushort.MaxValue) throw new ArgumentException("技能没有可用的结束 Timeline"); config.BlockedTimelines.Add(action.Value.AnimationEnd.RowId); Save(); notice=$"已添加 {action.Value.Name} → Timeline {action.Value.AnimationEnd.RowId}"; }); if(ImGui.CollapsingHeader($"手动 Timeline 编号({config.BlockedTimelines.Count} 项)###timeline-raw")) { ImGui.InputInt("Timeline ID",ref timeline); if(ImGui.Button("添加 Timeline") && timeline is >0 and <=ushort.MaxValue) { config.BlockedTimelines.Add((uint)timeline); Try(Save); } foreach(var item in config.BlockedTimelines.Order().ToArray()) { ImGui.Text(item.ToString()); ImGui.SameLine(); if(ImGui.SmallButton($"删除##timeline{item}")) { config.BlockedTimelines.Remove(item); Try(Save); } } } ImGui.Text($"16 本地位移调用 {combat.DashCalls} · 已拦截 {combat.DashBlocks}"); var last=combat.LastDash; if(last!=null) ImGui.TextWrapped($"最近 Timeline {last.Timeline} · 类型 {last.MoveType} · 标志 {last.Flags2:X2}/{last.Flags3:X2} · {(last.Blocked ? "已拦截" : "已放行")}"); } private void DrawCoordinates() { ImGui.InputFloat3("坐标 X Y Z",ref position); if(ImGui.Button("读取自身位置") && game.Objects.LocalPlayer!=null) position=game.Objects.LocalPlayer.Position; ImGui.SameLine(); if(ImGui.Button("应用坐标")) Try(()=>coordinates.Position(position)); ImGui.InputFloat("面向弧度",ref rotation); if(ImGui.Button("应用面向")) Try(()=>coordinates.Facing(rotation)); if(ImGui.Button("应用目标缩放")) Try(()=>coordinates.SetScale(config.Value(29))); ImGui.SameLine(); if(ImGui.Button("恢复修改过的缩放")) Try(coordinates.RestoreAll); ImGui.SameLine(); if(ImGui.Button("目标缩放设为 1")) Try(()=>coordinates.SetScale(1)); ImGui.TextWrapped("坐标和面向是一次性操作,关闭模块不会把角色移回原地。缩放设为 1 是显式操作,不代表所有对象原始缩放均为 1。"); } private void DrawNetwork() { ImGui.TextWrapped($"记录目录:{directory}"); ImGui.TextWrapped($"当前 {packets.Snapshot().Length} 条,丢弃 {packets.Dropped} 条,采集异常 {capture.Errors} 次。"); ImGui.TextWrapped($"缓冲载荷 {packets.PayloadBytes/1024.0:F1} KiB / {packets.PayloadByteLimit/1024/1024} MiB;最多 2048 条,超限移除最旧记录。"); ImGui.TextWrapped($"本次加载累计:完整上行 {capture.CompleteUp} · 完整下行 {capture.CompleteDown} · 仅元数据 {capture.MetadataOnly}。"); ImGui.TextWrapped("按消息长度复制游戏载荷(不含 IPC 头),单包上限 64 KiB。当前版本首轮样本已验证完整上下行;读取失败不标记完整,不采集聊天/登录通道。上行入口记录不代表服务端已收到。"); ImGui.TextWrapped($"最近采集问题:{capture.LastIssue}"); var manager=network.Pipeline; ImGui.TextWrapped($"收发管理器:订阅 {manager.SubscriberCount} · 收包 {manager.Received} · 发送入口 {manager.BeforeSend} · 发送返回 {manager.AfterSend} · 订阅异常 {manager.ObserverErrors}"); ImGui.TextWrapped($"最近原生发送返回码:{(manager.LastSendResult<0 ? "无" : manager.LastSendResult.ToString())}(不代表服务端确认)。暂停与过滤仅影响日志,管理器观察继续。"); DrawActiveSendSection(); var autoSave=config.AutoSaveCapture; if(ImGui.Checkbox("每 5 秒自动保存最近 2048 条记录",ref autoSave)) { config.AutoSaveCapture=autoSave; Try(Save); } ImGui.TextWrapped($"自动保存:{captureSaveStatus} · {captureSessionFile}"); var paused=packets.Paused; if(ImGui.Checkbox("暂停记录",ref paused)) packets.Paused=paused; ImGui.InputText("文件名",ref file,128); if(ImGui.Button("导入离线记录")) Try(()=> { var imported=PacketFiles.Import(DataPath(file)); packets.Replace(imported); notice=$"已导入 {imported.Length} 条,保留原时间、方向、版本及序号"; }); ImGui.SameLine(); if(ImGui.Button("导出记录") && pending?.IsCompleted!=false) { var exportRecords=packets.Snapshot(); Try(()=>{ var path=DataPath(file); pending=Background(()=>PacketFiles.ExportAtomic(path,exportRecords)); }); } ImGui.SameLine(); if(ImGui.Button("清空记录")) packets.Clear(); if(ImGui.Button("载入本地 opcodes.json")) Try(()=>{ EnsureOpcodes(true); notice=$"已成功载入 {gameVersion} Opcode 映射({opcodes.Snapshot.Source})"; }); ImGui.SameLine(); if(ImGui.Button("更新在线 Opcode") && pending?.IsCompleted!=false) pending=UpdateOpcodes(); ImGui.TextWrapped($"映射版本:{opcodes.Snapshot.GameVersion},来源:{opcodes.Snapshot.Source}"); var url=config.OpcodeUrl; if(ImGui.InputText("Opcode HTTPS 地址",ref url,1024)) { config.OpcodeUrl=url; Try(Save); } ImGui.Combo("方向过滤",ref packetDirection,"全部\0上行\0下行\0"); ImGui.InputInt("Opcode 过滤(-1 为全部)",ref packetOpcode); packets.DirectionFilter=packetDirection==0 ? null : (PacketDirection)(packetDirection-1); packets.OpcodeFilter=packetOpcode<0 ? null : packetOpcode; var snapshot=packets.Snapshot(); if(modules.IsEnabled(37)) { foreach(var pair in PacketCorrelator.Ping(snapshot,opcodes).TakeLast(5)) ImGui.Text($"Ping {pair.Request} → {pair.Response}: {pair.Elapsed.TotalMilliseconds:F1} ms"); foreach(var pair in PacketCorrelator.Actions(snapshot,opcodes).TakeLast(5)) ImGui.Text($"动作 {pair.Request} → {pair.Response}: {pair.Elapsed.TotalMilliseconds:F1} ms"); } foreach(var p in snapshot.Where(p=>(packetOpcode<0 || p.Opcode==packetOpcode) && (packetDirection==0 || p.Direction==(PacketDirection)(packetDirection-1))).TakeLast(50)) { var name=modules.IsEnabled(37) ? opcodes.Resolve(p.GameVersion,p.Direction,p.Opcode) : null; var summary=p.PayloadComplete ? PacketParser.Parse(name,p.Payload,p.GameVersion).Summary : $"{name ?? "未知 Opcode"} · 仅元数据:{p.CaptureIssue ?? "载荷长度未知"}"; var origin=p.Context?.Origin==PacketOrigin.PluginActiveSend ? "插件主动发送" : "Zone 原始载荷"; var context=p.Context is {} c ? $"{origin} · {c.Phase} · 观察 {c.ObservationId}" : "旧记录,处理阶段未知"; ImGui.TextWrapped($"{p.Sequence} {p.Time:HH:mm:ss.fff} {p.Direction} 0x{p.Opcode:X4} ({p.Payload.Length} 字节已采集) {context} · {summary}"); } } private void DrawActiveSendSection() { 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});发送时将自动载入"); if(ImGui.CollapsingHeader("1. 地面技能智能释放 (Smart Ground Cast)", ImGuiTreeNodeFlags.DefaultOpen)) { 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); }); } 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); } ImGui.SameLine(); var fastYes = config.FastSelectYesno; if (ImGui.Checkbox("自动确认 Yes/No 提示框 (FastSelectYesno)##fastYesno", ref fastYes)) { config.FastSelectYesno = fastYes; 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})" : ""; var obs=tx.NativeResult is {} nr ? $" · 原生返回 {nr}" : ""; 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>=200) { 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; } } if(config.FastSelectYesno && now-lastYesnoTime>=300) { var yesno=mgr->GetAddonByName("SelectYesno",1); if(yesno!=null && yesno->IsVisible && yesno->IsReady) { var val=stackalloc FFXIVClientStructs.FFXIV.Component.GUI.AtkValue[1]; val[0].SetInt(0); yesno->FireCallback(1,val,true); lastYesnoTime=now; notice="已自动确认【SelectYesno】提示框"; } } } private unsafe void ConfirmYesno(bool yes) { var mgr=FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkUnitManager.Instance(); if(mgr==null) return; var yesno=mgr->GetAddonByName("SelectYesno",1); if(yesno!=null && yesno->IsVisible && yesno->IsReady) { var val=stackalloc FFXIVClientStructs.FFXIV.Component.GUI.AtkValue[1]; val[0].SetInt(yes ? 0 : 1); yesno->FireCallback(1,val,true); chat.Print($"[IC Rebuild] 已手动{(yes ? "确认(Yes)" : "取消(No)")}当前提示框"); } else chat.Print("[IC Rebuild] 当前无可见的 Yes/No 提示框"); } 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("请输入当前记录目录中的文件名"); return Path.Combine(directory,name); } private void EnsureOpcodes(bool force=false) { if(!force && opcodes.Snapshot.GameVersion==gameVersion) return; var path=DataPath("opcodes.json"); if(!File.Exists(path) && pi.AssemblyLocation.DirectoryName is { } devDir) path=Path.Combine(devDir,"opcodes.json"); if(File.Exists(path)) opcodes.Load(File.ReadAllText(path),gameVersion,"本地文件"); } private async Task Background(Action action) { try { await Task.Run(action,cancellation.Token); if(!disposed) notice="文件操作完成"; } catch(Exception ex) { if(!disposed) notice=ex.Message; } } private async Task SaveCaptureAsync(string path,PacketRecord[] records) { try { await Task.Run(()=>PacketFiles.ExportAtomic(path,records)); if(!disposed) captureSaveStatus=$"{DateTime.Now:HH:mm:ss} 已保存 {records.Length} 条"; } catch(Exception ex) { if(!disposed) captureSaveStatus=$"保存失败:{ex.Message}"; } } private async Task UpdateOpcodes() { try { var uri=new Uri(config.OpcodeUrl); if(uri.Scheme!=Uri.UriSchemeHttps) throw new InvalidDataException("Opcode 地址必须使用 HTTPS"); using var response=await http.GetAsync(uri,HttpCompletionOption.ResponseHeadersRead,cancellation.Token); response.EnsureSuccessStatusCode(); if(response.Content.Headers.ContentLength>4*1024*1024) throw new InvalidDataException("Opcode 文件超过 4 MiB"); await using var stream=await response.Content.ReadAsStreamAsync(cancellation.Token); using var content=new MemoryStream(); var buffer=new byte[8192]; int read; while((read=await stream.ReadAsync(buffer,cancellation.Token))>0) { if(content.Length+read>4*1024*1024) throw new InvalidDataException("Opcode 文件超过 4 MiB"); content.Write(buffer,0,read); } var json=System.Text.Encoding.UTF8.GetString(content.ToArray()); if(disposed) return; var validated=new OpcodeMap(); validated.Load(json,gameVersion,uri.Host); var temporary=DataPath("opcodes.json.tmp"); await File.WriteAllTextAsync(temporary,json,cancellation.Token); cancellation.Token.ThrowIfCancellationRequested(); File.Move(temporary,DataPath("opcodes.json"),true); opcodes.Load(json,gameVersion,uri.Host); if(!disposed) notice="Opcode 映射已更新"; } catch(Exception ex) { if(!disposed) notice=$"Opcode 更新失败:{ex.Message}"; } } public void Dispose() { if(disposed) return; disposed=true; 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(); } }