Compare commits
9
Commits
16d61f7a5e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daded9e405 | ||
|
|
6bae98720f | ||
|
|
7eaa536efb | ||
|
|
c68f5f743a | ||
|
|
a6cccdbc43 | ||
|
|
bddff722dc | ||
|
|
702d770d07 | ||
|
|
2e1f00087a | ||
|
|
8008519d8b |
@@ -20,6 +20,10 @@ 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 FastSelectYesno { get; set; } = false;
|
||||
public bool AutoRetainerReventure { get; set; } = false;
|
||||
public float Value(int id)
|
||||
{
|
||||
return Published.Value(id);
|
||||
|
||||
@@ -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 范围"),
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace ICRebuild.Core;
|
||||
public static class CurrentProtocolEvidence
|
||||
{
|
||||
// Only fill missing names/slots; do not rewrite explicit mappings or the cache file.
|
||||
private static readonly (int Opcode,string Name)[] Downstream =
|
||||
private static readonly (int Opcode,string Name)[] Downstream20260915 =
|
||||
[
|
||||
(338,"DOWN_PingRsp"),
|
||||
(671,"DOWN_UpdateHate"),
|
||||
@@ -14,11 +14,29 @@ public static class CurrentProtocolEvidence
|
||||
(788,"DOWN_UpdateClassInfo"),
|
||||
(214,"DOWN_FirstAttack")
|
||||
];
|
||||
|
||||
private static readonly (int Opcode,string Name)[] Downstream20261006 =
|
||||
[
|
||||
(581,"DOWN_PingRsp"),
|
||||
(673,"DOWN_UpdateHate"),
|
||||
(171,"DOWN_UpdateHater"),
|
||||
(549,"DOWN_EffectResultBasic"),
|
||||
(748,"DOWN_JobGauge"),
|
||||
(592,"DOWN_UpdateClassInfo"),
|
||||
(374,"DOWN_FirstAttack")
|
||||
];
|
||||
|
||||
public static int AddMissing(string version,Dictionary<int,string> down)
|
||||
{
|
||||
if(version!=PacketParser.AuditedClientVersion) return 0;
|
||||
var evidence = version switch
|
||||
{
|
||||
PacketParser.AuditedClientVersion => Downstream20261006,
|
||||
PacketParser.BaselineClientVersion => Downstream20260915,
|
||||
_ => null
|
||||
};
|
||||
if(evidence == null) return 0;
|
||||
var added=0;
|
||||
foreach(var (opcode,name) in Downstream)
|
||||
foreach(var (opcode,name) in evidence)
|
||||
if(!down.ContainsKey(opcode) && !down.ContainsValue(name)) { down[opcode]=name; added++; }
|
||||
return added;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<None Update="ICRebuild.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Update="opcodes.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
<Target Name="ValidateRuntime" BeforeTargets="ResolveReferences">
|
||||
<Error Condition="!Exists('$(HooksDevPath)\Dalamud.dll')" Text="HooksDevPath must contain the Dalamud API 15 runtime DLLs." />
|
||||
|
||||
@@ -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 lastTalkAdvanceTime;
|
||||
private long lastYesnoTime;
|
||||
private string statusFilterText = "";
|
||||
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(()=>
|
||||
@@ -86,19 +97,26 @@ public sealed class Plugin : IDalamudPlugin
|
||||
adaptationReport=game.EvaluateAdaptation(modules);
|
||||
AdaptationManifest.ApplyGate(modules,adaptationReport);
|
||||
});
|
||||
var opcodeFile=Path.Combine(directory,"opcodes.json");
|
||||
if(File.Exists(opcodeFile)) Try(() => opcodes.Load(File.ReadAllText(opcodeFile),gameVersion,"本地缓存"));
|
||||
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;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;yes;no;select 索引;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();
|
||||
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))
|
||||
{
|
||||
@@ -165,11 +183,30 @@ 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;
|
||||
case "yes":
|
||||
ConfirmYesno(true); break;
|
||||
case "no":
|
||||
ConfirmYesno(false); break;
|
||||
case "select" or "choose" when args.Length==2:
|
||||
var optIndex=int.Parse(args[1],CultureInfo.InvariantCulture);
|
||||
SelectDialogOption(optIndex); break;
|
||||
default: Open(); notice="用法:/icr on|off 编号;stop;pos X Y Z;ground 技能ID [target|self];finish [事件ID];retainer;yes;no;select 索引"; break;
|
||||
}
|
||||
});
|
||||
}
|
||||
private void Draw()
|
||||
private unsafe void Draw()
|
||||
{
|
||||
if(!open) return;
|
||||
ImGui.SetNextWindowSize(new(790,650),ImGuiCond.FirstUseEver);
|
||||
@@ -179,7 +216,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
ImGui.TextWrapped($"IC Rebuild 0.1.16 · {region} · API 15 · 客户端 {gameVersion}");
|
||||
if(ImGui.Button("关闭全部模块")) { modules.DisableAll(); config.Requested.Clear(); Try(Save); }
|
||||
ImGui.TextWrapped(notice);
|
||||
DrawAdaptation();
|
||||
foreach(var group in Features.All.GroupBy(x=>x.Group))
|
||||
{
|
||||
if(!ImGui.CollapsingHeader(group.Key)) continue;
|
||||
@@ -196,6 +232,13 @@ public sealed class Plugin : IDalamudPlugin
|
||||
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)
|
||||
{
|
||||
@@ -214,6 +257,23 @@ public sealed class Plugin : IDalamudPlugin
|
||||
: "等待当前目标的原生半径读取。");
|
||||
ImGui.TextWrapped($"目标修改 {combat.TargetRadiusChanges} · 原生圈补偿 {combat.NativeRingBonuses} · 距离检查命中 {combat.RadiusBonuses}");
|
||||
}
|
||||
if(feature.Id==18)
|
||||
{
|
||||
ImGui.TextWrapped("结印技能(天之印、地之印、人之印)复唱归零,结印无需等待 GCD。依赖模块 17。");
|
||||
}
|
||||
if(feature.Id==20)
|
||||
{
|
||||
var pvpState=game.IsPvP ? "是 (生效中)" : "否 (仅PvP区域生效)";
|
||||
var mgr=FFXIVClientStructs.FFXIV.Client.Game.ActionManager.Instance();
|
||||
var currentLock=mgr!=null ? mgr->AnimationLock : 0f;
|
||||
ImGui.Text($"PvP 区域:【{pvpState}】 · 当前动画锁:{currentLock:F3}s");
|
||||
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)
|
||||
@@ -233,49 +293,63 @@ public sealed class Plugin : IDalamudPlugin
|
||||
}
|
||||
ImGui.End();
|
||||
}
|
||||
private void DrawAdaptation()
|
||||
{
|
||||
if(!ImGui.CollapsingHeader("版本适配与诊断 (M0)")) return;
|
||||
if(ImGui.Button("重新评估适配清单")) Try(()=>
|
||||
{
|
||||
adaptationReport=game.EvaluateAdaptation(modules);
|
||||
AdaptationManifest.ApplyGate(modules,adaptationReport);
|
||||
});
|
||||
if(adaptationReport is not {} report)
|
||||
{
|
||||
ImGui.TextWrapped("尚未生成适配诊断报告。");
|
||||
return;
|
||||
}
|
||||
ImGui.TextWrapped($"客户端 {report.ClientVersion}(已审计基线 {report.AuditedVersion})· 可用模块 {report.ReadyCount} · 受阻模块 {report.BlockedCount}");
|
||||
ImGui.TextWrapped(report.ActiveSendSummary);
|
||||
foreach(var entry in report.Entries)
|
||||
{
|
||||
var tag=entry.Issue switch
|
||||
{
|
||||
AdaptationIssue.None=>"就绪",
|
||||
AdaptationIssue.MissingImplementation=>"未实现",
|
||||
AdaptationIssue.VersionMismatch=>"版本不匹配",
|
||||
AdaptationIssue.OffsetMismatch=>"SDK 偏移变化",
|
||||
AdaptationIssue.SignatureMissing=>"签名缺失",
|
||||
AdaptationIssue.MultipleMatches=>"签名不唯一",
|
||||
AdaptationIssue.RuntimeFault=>"运行异常",
|
||||
_=>"受限"
|
||||
};
|
||||
ImGui.TextWrapped($"{entry.FeatureId} · {entry.FeatureName}:{tag} · {entry.Summary}");
|
||||
}
|
||||
}
|
||||
private void DrawStatusBlock()
|
||||
{
|
||||
ImGui.TextWrapped($"已选 {config.BlockedStatuses.Count} 项 · 本次启用拦截 {statusBlock.Blocked} 次 · 最近状态 ID {statusBlock.LastStatus}");
|
||||
ImGui.TextWrapped("仅屏蔽本地角色后续收到的已选状态更新;开启前已有状态须等下一次更新。默认选择 1257 强制移动,空名单不屏蔽。录像回放暂停处理。");
|
||||
ImGui.TextWrapped("关闭后恢复正常接收,已屏蔽状态须等服务端再次同步。本地处理不代表服务端状态被删除;实际移动或机制效果待实测。");
|
||||
if(!ImGui.CollapsingHeader("状态名单(已审计候选,效果待逐项确认)")) return;
|
||||
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<Lumina.Excel.Sheets.Status>();
|
||||
foreach(var id in StatusCatalog.AuditedBlockCandidates.Order())
|
||||
{
|
||||
var selected=config.BlockedStatuses.Contains(id);
|
||||
var name=sheet?.GetRowOrDefault(id)?.Name.ToString() ?? "未知状态";
|
||||
if(ImGui.Checkbox($"{id} · {name}",ref selected))
|
||||
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);
|
||||
@@ -371,7 +445,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
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(()=>opcodes.Load(File.ReadAllText(DataPath("opcodes.json")),gameVersion,"本地文件"));
|
||||
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;
|
||||
@@ -399,39 +473,127 @@ 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}" : "未就绪(需角色在场且非过图/占卜回放)")}");
|
||||
ImGui.InputInt("测试技能 ID (UP_ActionSend)",ref manualSendActionId);
|
||||
ImGui.InputInt("请求序号 RequestId",ref manualSendRequestId);
|
||||
if(ImGui.Button("加入技能发送队列 (当前目标)")) Try(()=>
|
||||
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))
|
||||
{
|
||||
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(()=>
|
||||
{
|
||||
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("支持自动极速快进剧情与NPC对白 Talk、自动确认 Yes/No 提示框,以及手动发包立即完成开箱/门锁事件。");
|
||||
var fastEv = config.FastEventFinish;
|
||||
if (ImGui.Checkbox("自动快进剧情与NPC对话 (Fast Talk)##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})" : "";
|
||||
@@ -439,11 +601,167 @@ 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;
|
||||
}
|
||||
}
|
||||
private unsafe void ExecuteGroundCast(uint actionId,bool atTarget)
|
||||
{
|
||||
if(actionId==0) throw new ArgumentException("请输入有效地面技能 ID");
|
||||
var player=game.Objects.LocalPlayer;
|
||||
if(player==null) throw new InvalidOperationException("本地角色未就绪");
|
||||
Vector3 targetPos=atTarget && game.Targets.Target is { } target ? target.Position : player.Position;
|
||||
var destStr=atTarget && game.Targets.Target!=null ? $"目标【{game.Targets.Target.Name}】" : "自身脚下";
|
||||
|
||||
var am=FFXIVClientStructs.FFXIV.Client.Game.ActionManager.Instance();
|
||||
if(am!=null)
|
||||
{
|
||||
var loc=targetPos;
|
||||
var success=am->UseActionLocation(FFXIVClientStructs.FFXIV.Client.Game.ActionType.Action,actionId,0xE000_0000,&loc);
|
||||
if(success)
|
||||
{
|
||||
notice=$"地面技能 {actionId} 已智能释放至 {destStr} ({targetPos.X:F1}, {targetPos.Y:F1}, {targetPos.Z:F1})";
|
||||
chat.Print($"[IC Rebuild] 地面技能 {actionId} 已释放至 {destStr}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EnsureOpcodes();
|
||||
var session=game.ReadSendSession();
|
||||
if(!session.IsValid) throw new InvalidOperationException("当前角色或区域会话未就绪");
|
||||
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);
|
||||
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>=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 SelectDialogOption(int index)
|
||||
{
|
||||
var mgr=FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkUnitManager.Instance();
|
||||
if(mgr==null) return;
|
||||
var selectString=mgr->GetAddonByName("SelectString",1);
|
||||
if(selectString!=null && selectString->IsVisible && selectString->IsReady)
|
||||
{
|
||||
var val=stackalloc FFXIVClientStructs.FFXIV.Component.GUI.AtkValue[1];
|
||||
val[0].SetInt(index);
|
||||
selectString->FireCallback(1,val,true);
|
||||
chat.Print($"[IC Rebuild] 已选择【SelectString】第 {index + 1} 项 (索引 {index})");
|
||||
return;
|
||||
}
|
||||
var selectIconString=mgr->GetAddonByName("SelectIconString",1);
|
||||
if(selectIconString!=null && selectIconString->IsVisible && selectIconString->IsReady)
|
||||
{
|
||||
var val=stackalloc FFXIVClientStructs.FFXIV.Component.GUI.AtkValue[1];
|
||||
val[0].SetInt(index);
|
||||
selectIconString->FireCallback(1,val,true);
|
||||
chat.Print($"[IC Rebuild] 已选择【SelectIconString】第 {index + 1} 项 (索引 {index})");
|
||||
return;
|
||||
}
|
||||
chat.Print("[IC Rebuild] 当前无可见的选择列表 (SelectString / SelectIconString)");
|
||||
}
|
||||
private unsafe void ProcessRetainerAutomation()
|
||||
{
|
||||
var mgr=FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkUnitManager.Instance();
|
||||
if(mgr==null) return;
|
||||
var now=Environment.TickCount64;
|
||||
if(now-lastRetainerActionTime<750) 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)
|
||||
@@ -488,6 +806,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed unsafe class CombatModules(GameContext game, Configuration config,
|
||||
modules.Register(13,AcquireRange);
|
||||
modules.Register(14, scope =>
|
||||
{
|
||||
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("原生目标圈入口仅核对了 2026.09.15 客户端");
|
||||
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;
|
||||
@@ -137,7 +137,7 @@ public sealed unsafe class CombatModules(GameContext game, Configuration config,
|
||||
});
|
||||
modules.Register(16, scope =>
|
||||
{
|
||||
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("技能位移结构仅核对了 2026.09.15 客户端");
|
||||
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) =>
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed unsafe class MovementModules(GameContext game, Configuration confi
|
||||
});
|
||||
modules.Register(2, scope =>
|
||||
{
|
||||
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("最大加速度仅完成 2026.09.15 客户端的字段核对。");
|
||||
if(!AccelerationPolicy.IsAuditedVersion(game.GameVersion)) throw new InvalidOperationException($"最大加速度仅完成已核对客户端的字段核对,当前为 {game.GameVersion}。");
|
||||
Hook<Acceleration>? hook = null;
|
||||
hook = game.Hook<Acceleration>(scope, "40 ?? 48 ?? ?? ?? 80 79 ?? ?? 48 ?? ?? 0f 84 ?? ?? ?? ?? 48 89 7c 24 ?? 48 ?? ?? ??", actor =>
|
||||
{
|
||||
@@ -85,7 +85,7 @@ public sealed unsafe class MovementModules(GameContext game, Configuration confi
|
||||
});
|
||||
modules.Register(7, scope =>
|
||||
{
|
||||
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("强制移动入口参数仅核对了 2026.09.15 客户端");
|
||||
if(!AccelerationPolicy.IsAuditedVersion(game.GameVersion)) throw new InvalidOperationException($"强制移动入口参数仅核对了已审计客户端,当前为 {game.GameVersion}。");
|
||||
Hook<Forced>? hook = null;
|
||||
hook = game.Hook<Forced>(scope, Signatures.FuckBewithAction, (actor,x,y,z,movementParameter,options) =>
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class StatusBlockModule(GameContext game, Configuration config, Mo
|
||||
public void Register() => modules.Register(8, scope =>
|
||||
{
|
||||
if(!StatusBlockPolicy.IsAuditedVersion(game.GameVersion))
|
||||
throw new InvalidOperationException($"状态屏蔽仅适配国际服已审计版本,当前为 {game.GameVersion}。");
|
||||
throw new InvalidOperationException($"状态屏蔽仅适配已核对客户端(国服/国际服),当前为 {game.GameVersion}。");
|
||||
if(Marshal.OffsetOf<BattleChara>(nameof(BattleChara.StatusManager)).ToInt32() != StatusBlockPolicy.ManagerOffset ||
|
||||
Marshal.OffsetOf<GameObject>(nameof(GameObject.EntityId)).ToInt32() != StatusBlockPolicy.EntityIdOffset)
|
||||
throw new InvalidOperationException("当前 SDK 的角色布局与已核对的状态入口不一致。");
|
||||
|
||||
@@ -30,29 +30,29 @@
|
||||
| 编号 | 功能 | 当前结果 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 移速调整 | 用户确认实测有效;保留减速状态过滤 |
|
||||
| 2 | 最大加速度 | 用户确认实测有效;已修正原生结果被旧值覆盖的问题并限定本地对象 |
|
||||
| 2 | 最大加速度 | 用户确认实测有效;已修正原生结果被旧值覆盖的问题并限定本地对象;双版本门控核验通过 |
|
||||
| 3 | 强制移动 | 用户确认验收通过;回退保持原始参数 |
|
||||
| 4 | 击退调整 | 已接入;按本地对象地址过滤,距离和持续时间可配置,待实测 |
|
||||
| 4 | 击退调整 | 已接入;按本地对象地址过滤,距离和持续时间可配置;提供一键完全防击退快捷键,待实测 |
|
||||
| 5 | 掉落无伤 | 已接入历史标志处理,待实测 |
|
||||
| 6 | 无掉落 | 已接入历史检查 Hook,待核实作用范围 |
|
||||
| 7 | 魅惑与恐惧移动处理 | 0.1.3 核实为强制坐标移动入口,第五参数改为 float;不删除状态,待实测 |
|
||||
| 8 | 状态屏蔽 | 0.1.12 接入转换后 SetStatus,本地身份门控及回放暂停;默认仅选 1257,可编辑 38 项候选名单并查看命中。只处理后续更新,待首个状态效果实测 |
|
||||
| 7 | 魅惑与恐惧移动处理 | 0.1.3 核实为强制坐标移动入口,第五参数改为 float;不删除状态;双版本门控核验通过,待实测 |
|
||||
| 8 | 状态屏蔽 | 0.1.12 接入转换后 SetStatus,本地身份门控及回放暂停;新增滑冰/强制移动/混乱分类预设按钮与实时名称/ID搜索过滤;双版本已核对,待实测 |
|
||||
| 9 | 飞天遁地 | 用户确认验收通过;手动生效、回到 0;0.1.6 新增服务栏状态 |
|
||||
| 10 | 深层迷宫专用模式 | 用户确认验收通过;依赖 9,未知和特殊层不应用偏移;0.1.6 新增服务栏状态 |
|
||||
| 11 | 迷宫状态显示 | 用户确认验收通过;0.1.6 新增服务栏楼层及特殊层提示 |
|
||||
| 13 | 技能距离 | 用户确认实测有效;普通技能增量上限 3,保留特殊技能逻辑 |
|
||||
| 14 | 目标圈半径 | 用户确认 16461 落点正常;统一扩大目标原生半径,自身不增加,不额外叠加技能距离 |
|
||||
| 14 | 目标圈半径 | 用户确认 16461 落点正常;统一扩大目标原生半径,自身不增加,不额外叠加技能距离;双版本门控核验通过 |
|
||||
| 15 | 后摇可移动 | 用户确认实测有效 |
|
||||
| 16 | 突进无位移 | 用户确认实测有效;79 个玩家位移技能、58 个编号;0.1.5 名单管理及手动编号默认折叠 |
|
||||
| 16 | 突进无位移 | 用户确认实测有效;79 个玩家位移技能、58 个编号;0.1.5 名单管理及手动编号默认折叠;双版本门控核验通过 |
|
||||
| 17 | 复唱缩减 | 用户确认实测有效 |
|
||||
| 18 | 快速结印 | 已接入,依赖 17;待实测 |
|
||||
| 18 | 快速结印 | 已接入,依赖 17;结印技能复唱归零;待实测 |
|
||||
| 19 | 咏唱缩减 | 用户确认实测有效 |
|
||||
| 20 | PVP 动画锁 | 已接入每帧处理,待实测 |
|
||||
| 20 | PVP 动画锁 | 已接入每帧处理;提供 0.5s/0.6s/0.7s 快捷预设与实时 PvP/动画锁监控;待实测 |
|
||||
| 27 | 坐标移动 | 用户确认实测有效 |
|
||||
| 28 | 面向调整 | 用户确认实测有效 |
|
||||
| 29 | 目标缩放 | 用户确认实测有效;保留参数校验及条件恢复 |
|
||||
| 36 | 网络记录 | 0.1.13 接入共享收发管理器;0.1.15 新增 M4 主动发送事务调度(`ReceiverSendPacket 0x1CFD140` + `ActiveSendScope` 来源标注)与 M0 适配诊断面板;原 891 条样本回放一致,新版启停、主动发送与切图待实测 |
|
||||
| 37 | 包解析与 Opcode | 0.1.14 补齐剩余 212 条已命名记录与 8 档变长事件包校验;0.1.15 新增按包名反查 Opcode 及已审计主动发送编码器;`capture-018.jsonl` 891 条 100% 解析通过,8 组 Ping 与 27 组动作请求响应全部配对;待游戏内 UI 验收 |
|
||||
| 36 | 网络记录 | 0.1.13 接入共享收发管理器;0.1.15 新增 M4 主动发送事务调度(`ReceiverSendPacket 0x1CFD140` + `ActiveSendScope` 来源标注)与双环境适配;待实测 |
|
||||
| 37 | 包解析与 Opcode | 0.1.14 补齐剩余 212 条已命名记录与 8 档变长事件包校验;0.1.15 落地地面技能智能释放、极速交互完成与雇员探险重派三项业务扩展;双版本 Opcode 映射自动装载;待实测 |
|
||||
|
||||
## 已完成的工程工作
|
||||
|
||||
|
||||
+69
-14
@@ -729,12 +729,12 @@ Test("verified PingRsp fills only missing mappings for the audited version",()=>
|
||||
const string v=PacketParser.AuditedClientVersion;
|
||||
var map=new OpcodeMap();
|
||||
map.Load($$$"""{"{{{v}}}":{"UP_PingReq":[838]}}""",v,"test");
|
||||
Check(map.Resolve(v,PacketDirection.Down,338)=="DOWN_PingRsp"); Check(map.Snapshot.Source.Contains("本机核实"));
|
||||
map.Load($$$"""{"{{{v}}}":{"DOWN_Explicit":[338]}}""",v,"test");
|
||||
Check(map.Resolve(v,PacketDirection.Down,338)=="DOWN_Explicit");
|
||||
map.Load($$$"""{"{{{v}}}":{"DOWN_PingRsp":[339]}}""",v,"test");
|
||||
Check(map.Resolve(v,PacketDirection.Down,338)==null);
|
||||
map.Load("""{"other":{"UP_PingReq":[838]}}""","other","test"); Check(map.Resolve("other",PacketDirection.Down,338)==null);
|
||||
Check(map.Resolve(v,PacketDirection.Down,581)=="DOWN_PingRsp"); Check(map.Snapshot.Source.Contains("本机核实"));
|
||||
map.Load($$$"""{"{{{v}}}":{"DOWN_Explicit":[581]}}""",v,"test");
|
||||
Check(map.Resolve(v,PacketDirection.Down,581)=="DOWN_Explicit");
|
||||
map.Load($$$"""{"{{{v}}}":{"DOWN_PingRsp":[582]}}""",v,"test");
|
||||
Check(map.Resolve(v,PacketDirection.Down,581)==null);
|
||||
map.Load("""{"other":{"UP_PingReq":[838]}}""","other","test"); Check(map.Resolve("other",PacketDirection.Down,581)==null);
|
||||
Throws<InvalidDataException>(()=>map.Load($$$"""{"{{{v}}}":{}}""",v,"test"));
|
||||
});
|
||||
Test("current Ping pairs require full exact-length payload and matching echo",()=>
|
||||
@@ -746,7 +746,7 @@ Test("current Ping pairs require full exact-length payload and matching echo",()
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(response.AsSpan(4),333); // not RTT; do not use it as elapsed time
|
||||
var time=DateTimeOffset.UtcNow;
|
||||
var a=new PacketRecord(1,0,time,PacketDirection.Up,838,v,null,null,request);
|
||||
var b=new PacketRecord(2,0,time.AddMilliseconds(83),PacketDirection.Down,338,v,null,null,response);
|
||||
var b=new PacketRecord(2,0,time.AddMilliseconds(83),PacketDirection.Down,581,v,null,null,response);
|
||||
var pairs=PacketCorrelator.Ping([a,b],map); Check(pairs.Length==1 && pairs[0].Elapsed.TotalMilliseconds==83);
|
||||
Check(PacketCorrelator.Ping([a with {Payload=new byte[28]},b],map).Length==0);
|
||||
Check(PacketCorrelator.Ping([a,b with {PayloadComplete=false}],map).Length==0);
|
||||
@@ -756,10 +756,16 @@ Test("audited protocol additions preserve explicit opcode and name assignments",
|
||||
{
|
||||
var down=new Dictionary<int,string>{{671,"DOWN_Explicit"},{100,"DOWN_UpdateHater"}};
|
||||
Check(CurrentProtocolEvidence.AddMissing("other",down)==0);
|
||||
Check(CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,down)==5);
|
||||
Check(CurrentProtocolEvidence.AddMissing(PacketParser.BaselineClientVersion,down)==5);
|
||||
Check(down[671]=="DOWN_Explicit" && !down.ContainsKey(558));
|
||||
Check(down[697]=="DOWN_EffectResultBasic" && down[338]=="DOWN_PingRsp");
|
||||
Check(CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,down)==0);
|
||||
Check(CurrentProtocolEvidence.AddMissing(PacketParser.BaselineClientVersion,down)==0);
|
||||
|
||||
var downAudited=new Dictionary<int,string>{{673,"DOWN_Explicit"},{100,"DOWN_UpdateHater"}};
|
||||
Check(CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,downAudited)==5);
|
||||
Check(downAudited[673]=="DOWN_Explicit" && !downAudited.ContainsKey(171));
|
||||
Check(downAudited[549]=="DOWN_EffectResultBasic" && downAudited[581]=="DOWN_PingRsp");
|
||||
Check(CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,downAudited)==0);
|
||||
});
|
||||
Test("enmity parsers respect capacities and native byte value stride",()=>
|
||||
{
|
||||
@@ -813,10 +819,15 @@ Test("remaining fixed packets decode only proven fields and retain 64-bit tag id
|
||||
});
|
||||
Test("gauge evidence label preserves explicit ActorGauge mapping",()=>
|
||||
{
|
||||
var map=new Dictionary<int,string>{{8,"DOWN_ActorGauge"}};
|
||||
CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,map);
|
||||
Check(map[8]=="DOWN_ActorGauge" && map[614]=="DOWN_JobGauge");
|
||||
Check(map[788]=="DOWN_UpdateClassInfo" && map[214]=="DOWN_FirstAttack");
|
||||
var mapBaseline=new Dictionary<int,string>{{8,"DOWN_ActorGauge"}};
|
||||
CurrentProtocolEvidence.AddMissing(PacketParser.BaselineClientVersion,mapBaseline);
|
||||
Check(mapBaseline[8]=="DOWN_ActorGauge" && mapBaseline[614]=="DOWN_JobGauge");
|
||||
Check(mapBaseline[788]=="DOWN_UpdateClassInfo" && mapBaseline[214]=="DOWN_FirstAttack");
|
||||
|
||||
var mapAudited=new Dictionary<int,string>{{8,"DOWN_ActorGauge"}};
|
||||
CurrentProtocolEvidence.AddMissing(PacketParser.AuditedClientVersion,mapAudited);
|
||||
Check(mapAudited[8]=="DOWN_ActorGauge" && mapAudited[748]=="DOWN_JobGauge");
|
||||
Check(mapAudited[592]=="DOWN_UpdateClassInfo" && mapAudited[374]=="DOWN_FirstAttack");
|
||||
});
|
||||
Test("stage 2 upstream action, trigger and event tier parsers enforce audited sizes and capacities",()=>
|
||||
{
|
||||
@@ -952,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);
|
||||
@@ -967,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<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);
|
||||
Check(evStart.Payload.Length==16 && evStart.ExpectedResponseOpcode==737 && PacketParser.Parse(evStart.Name,evStart.Payload,v).Parsed);
|
||||
|
||||
@@ -1072,5 +1095,37 @@ Test("stage 3 M4 ActivePacketDispatcher enforces rate limit, queue bounds, sessi
|
||||
Check(dispatcher.Pump(session,NativeSender)?.Id==tx5);
|
||||
Check(dispatcher.Snapshot().Single(s=>s.Id==tx5) is {State:SendTransactionState.RejectedNative,NativeResult:0});
|
||||
});
|
||||
Test("opcodes.json loads cleanly without conflicts and resolves dual-version audited opcodes",()=>
|
||||
{
|
||||
var path=Path.Combine(AppContext.BaseDirectory,"opcodes.json");
|
||||
if(!File.Exists(path)) path=Path.Combine(AppContext.BaseDirectory,"..","..","..","opcodes.json");
|
||||
if(!File.Exists(path)) path="opcodes.json";
|
||||
Check(File.Exists(path));
|
||||
var json=File.ReadAllText(path);
|
||||
|
||||
// 2026.10.06.0000.0000 audit verification
|
||||
var mapAudited=new OpcodeMap();
|
||||
mapAudited.Load(json,PacketParser.AuditedClientVersion,"local");
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_ActorControlTarget")==200);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_ActorControlSelf")==486);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_ActorControl")==408);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_Effect")==189);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_StatusEffectList")==718);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_EffectResult")==242);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_InventoryTransaction")==136);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Up,"UP_ActionSend")==176);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Up,"UP_ClientTrigger")==872);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Down,"DOWN_PingRsp")==581);
|
||||
Check(mapAudited.ResolveOpcode(PacketParser.AuditedClientVersion,PacketDirection.Up,"UP_PingReq")==137);
|
||||
|
||||
// 2026.09.15.0000.0000 baseline verification
|
||||
var mapBase=new OpcodeMap();
|
||||
mapBase.Load(json,PacketParser.BaselineClientVersion,"local");
|
||||
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Down,"DOWN_ActorControlTarget")==856);
|
||||
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Down,"DOWN_ActorControlSelf")==516);
|
||||
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Down,"DOWN_ActorControl")==607);
|
||||
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Down,"DOWN_Effect")==787);
|
||||
Check(mapBase.ResolveOpcode(PacketParser.BaselineClientVersion,PacketDirection.Up,"UP_ActionSend")==913);
|
||||
});
|
||||
Console.WriteLine($"{passed} tests passed.");
|
||||
|
||||
|
||||
+19
-1
@@ -1,6 +1,24 @@
|
||||
# IC Rebuild 验证记录
|
||||
|
||||
最新交付:0.1.16,2026 年 10 月 13 日。客户端补丁 `2026.10.06.0000.0000` 完整适配;23 项特征码全部扫描核对成功;支持双版本向后兼容;与阶段 1、阶段 2、阶段 3 合并发布。
|
||||
最新交付:0.1.17,2026 年 10 月 14 日。业务功能扩展(地面技能智能释放、极速剧情/开箱跳过、雇员一键重派、Yes/No确认)、双版本运行时门控统一(模块 2/7/14/16/8)、状态屏蔽分类搜索增强与对话列表快捷选择指令。
|
||||
|
||||
## 0.1.17
|
||||
|
||||
业务扩展与运行时易用性交付:
|
||||
- **主动发送业务落地(M4 扩展)**:
|
||||
- **地面技能智能释放 (Smart Ground Cast)**:实现 `UP_ActionSendPos`(Opcode 891),支持以当前选中目标或自身脚下为圆心瞬间释放地面技能;内置学者罩子(188)、白魔庇护所(3569)、占星地星(7439)、忍者土遁(2260)、铃兰(25862)一键预设与 `/icr ground <actionId> [target|self]` 指令。
|
||||
- **极速交互与开箱 (Fast Event Finish & Dialog Advance)**:下行捕获 `DOWN_EventStart` 并自动对齐 `UP_EventFinish` 回包;`Talk` 窗口 200ms 防抖自动推进;`SelectYesno` 提示框自动确认与 `/icr yes` / `/icr no` 手动一键指令。
|
||||
- **雇员探险一键重派 (Retainer Re-Venture)**:监听 `RetainerTaskResult`(再次派遣)与 `RetainerTaskAsk`(确定派遣)Atk 界面,450ms 安全防抖自动点选与 `/icr retainer` 手动一键触发。
|
||||
- **对话列表快捷选择 (SelectString / SelectIconString)**:增加 `/icr select <index>` / `/icr choose <index>`,一键选择列表指定选项。
|
||||
- **双版本运行时门控修复**:
|
||||
- 修复模块 2(最大加速度)、模块 7(魅惑与恐惧移动处理)、模块 14(目标圈半径)、模块 16(突进无位移)与模块 8(状态屏蔽)的启动版本校验,统一使用 `IsAuditedVersion`,国服客户端不再误报版本异常。
|
||||
- **模块易用性与监控升级**:
|
||||
- 模块 8(状态屏蔽):增加“全选滑冰”、“全选强制移动”、“全选混乱”、“全选全部候选”、“清空已选”快捷预设按钮与实时名称/ID搜索输入框。
|
||||
- 模块 4(防击退):增加“一键完全防击退 (距离与持续时间归零)”按钮。
|
||||
- 模块 20(PVP 动画锁):增加 0.5s/0.6s/0.7s 快捷预设,实时显示是否处于 PvP 区域与当前游戏动画锁数值。
|
||||
- 模块 18(快速结印):明确天/地/人及对应印法复唱归零说明。
|
||||
- **离线测试与构建**:85 项离线测试全部通过;Release 构建 0 警告、0 错误。
|
||||
- **双环境部署**:已同步部署至 XIVLauncher(国际服)与 XIVLauncherCN(国服)dev 目录。
|
||||
|
||||
## 0.1.16
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title Dalamud 国服强制注入
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0inject-cn.ps1"
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,45 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
在官方 Dalamud 尚未解锁新版本前,向已启动的国服客户端手动注入 Dalamud。
|
||||
#>
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Write-Host "=== FFXIV 国服 Dalamud 强制注入工具 ===" -ForegroundColor Cyan
|
||||
|
||||
# 1. 寻找正在运行的 ffxiv_dx11 游戏进程
|
||||
$processes = Get-Process -Name "ffxiv_dx11" -ErrorAction SilentlyContinue
|
||||
if (-not $processes) {
|
||||
Write-Host "[!] 未检测到运行中的 ffxiv_dx11.exe 游戏进程!" -ForegroundColor Yellow
|
||||
Write-Host " 步骤:" -ForegroundColor Gray
|
||||
Write-Host " 1. 请先打开 XIVLauncherCN 正常登录并启动国服游戏。" -ForegroundColor Gray
|
||||
Write-Host " 2. 待游戏进入标题画面或角色选择界面后,再次运行本脚本。" -ForegroundColor Gray
|
||||
return
|
||||
}
|
||||
|
||||
# 2. 定位 Dalamud.Injector
|
||||
$injectorPath = Join-Path $env:APPDATA "XIVLauncherCN\addon\Hooks\15.0.3.6\Dalamud.Injector.exe"
|
||||
if (-not (Test-Path $injectorPath)) {
|
||||
$injectorPath = Join-Path $env:APPDATA "XIVLauncherCN\addon\Hooks\dev\Dalamud.Injector.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $injectorPath)) {
|
||||
Write-Host "[X] 未找到 XIVLauncherCN 的 Dalamud.Injector.exe,请确认启动器环境。" -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[+] 找到 Dalamud 注入器: $injectorPath" -ForegroundColor Green
|
||||
|
||||
foreach ($proc in $processes) {
|
||||
Write-Host "[*] 正在向游戏进程 PID: $($proc.Id) ($($proc.ProcessName)) 注入 Dalamud..." -ForegroundColor Cyan
|
||||
& $injectorPath inject $proc.Id
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "[+] 注入指令执行成功 (ExitCode 0)!" -ForegroundColor Green
|
||||
Write-Host " 请切回游戏窗口:" -ForegroundColor Yellow
|
||||
Write-Host " - Dalamud 将自动初始化并加载 Dev 插件 [IC.Rebuild 0.1.16.0]" -ForegroundColor Gray
|
||||
Write-Host " - 在聊天框输入 /icr 可打开 IC.Rebuild 面板" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "[!] 注入器返回退出码 $exitCode,如提示权限不足,请右键选择『以管理员身份运行』本脚本。" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title Dalamud 国际服强制注入
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0inject-global.ps1"
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,46 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
在官方 Dalamud 尚未解锁新版本前,向已启动的国际服客户端手动注入 Dalamud。
|
||||
#>
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Write-Host "=== FFXIV 国际服 Dalamud 强制注入工具 ===" -ForegroundColor Cyan
|
||||
|
||||
# 1. 寻找正在运行的 ffxiv_dx11 游戏进程
|
||||
$processes = Get-Process -Name "ffxiv_dx11" -ErrorAction SilentlyContinue
|
||||
if (-not $processes) {
|
||||
Write-Host "[!] 未检测到运行中的 ffxiv_dx11.exe 游戏进程!" -ForegroundColor Yellow
|
||||
Write-Host " 步骤:" -ForegroundColor Gray
|
||||
Write-Host " 1. 请先打开 XIVLauncher 正常登录并启动国际服游戏(即使提示插件已锁定也不影响启动客户端)。" -ForegroundColor Gray
|
||||
Write-Host " 2. 待游戏进入标题画面或角色选择界面后,再次运行本脚本。" -ForegroundColor Gray
|
||||
return
|
||||
}
|
||||
|
||||
# 2. 定位 Dalamud.Injector
|
||||
$injectorPath = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\15.0.3.6\Dalamud.Injector.exe"
|
||||
if (-not (Test-Path $injectorPath)) {
|
||||
$injectorPath = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev\Dalamud.Injector.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $injectorPath)) {
|
||||
Write-Host "[X] 未找到 Dalamud.Injector.exe,请确认 XIVLauncher 已正确下载 Dalamud 环境。" -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[+] 找到 Dalamud 注入器: $injectorPath" -ForegroundColor Green
|
||||
|
||||
foreach ($proc in $processes) {
|
||||
Write-Host "[*] 正在向游戏进程 PID: $($proc.Id) ($($proc.ProcessName)) 注入 Dalamud..." -ForegroundColor Cyan
|
||||
& $injectorPath inject $proc.Id
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "[+] 注入指令执行成功 (ExitCode 0)!" -ForegroundColor Green
|
||||
Write-Host " 请切回游戏窗口:" -ForegroundColor Yellow
|
||||
Write-Host " - Dalamud 将自动初始化并加载 Dev 插件 [IC.Rebuild 0.1.16.0]" -ForegroundColor Gray
|
||||
Write-Host " - 在聊天框输入 /icr 可打开 IC.Rebuild 面板" -ForegroundColor Gray
|
||||
Write-Host " - 检查 M0 状态(23/23 关键特征码均已 100% 匹配)" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "[!] 注入器返回退出码 $exitCode,如提示权限不足,请右键选择『以管理员身份运行』本脚本。" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
+4828
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user