Initial import of IC.Rebuild
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
dist/
|
||||||
|
.dotnet/
|
||||||
|
deploy-backups/
|
||||||
|
|
||||||
|
# Local game client copies used for native analysis.
|
||||||
|
binaries/
|
||||||
|
|
||||||
|
# Local credentials and IDE state.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# IC Rebuild 开发规范
|
||||||
|
|
||||||
|
适用范围:本目录及子目录。依据用户于 2026 年 10 月 11 日明确提出的要求:避免过度防御开发、过度测试和零碎发布,以可用功能推进为主。
|
||||||
|
|
||||||
|
## 功能与范围优先
|
||||||
|
|
||||||
|
- 优先完成用户当前需要的功能或修复,不把测试数量、已识别 Opcode 数量或框架层数当作主要成果。
|
||||||
|
- 当前优先推进编号 8 的可测试实现。核实入口、状态 ID 转换和本地角色范围后即可做针对性实现,不等待全部协议解析或统一网络框架完成。
|
||||||
|
- 复用已经有效的代码和直接 Hook。仅当当前功能确实需要时提取共用接口,不为假设中的未来需求预建通用队列、事务系统或大规模分层。
|
||||||
|
- 协议分析围绕实际功能、已出现的问题和必要样本进行;不以清空未知包或解析所有记录作为继续开发的前提。
|
||||||
|
- 原有功能删减与分期继续遵守 IC/PLAN.md。主动发送和后续业务扩展明确列项,不通过重构或测试任务悄悄扩大范围。
|
||||||
|
|
||||||
|
## 保留必要边界
|
||||||
|
|
||||||
|
- 对原生内存保留有效的长度、指针、对象身份和生命周期检查;维护原函数调用语义,确认关闭与卸载清理。
|
||||||
|
- 新版本的原生布局与入口未经确认时,不直接套用旧偏移或地址;使用现有适配机制处理实际兼容问题。
|
||||||
|
- 检查和异常处理应对应具体失败模式。不要围绕每一个理论上的异常增加多层回退、冗余状态机或重复验证。
|
||||||
|
- 已通过验证的实现优先保持稳定;出现新证据、功能需求或实际回归时再调整。
|
||||||
|
|
||||||
|
## 测试按风险安排
|
||||||
|
|
||||||
|
- 原生边界、包长度与数量、调用次数、对象范围、启停与资源释放等实质风险,应有对应验证。可复用已有测试与实际样本。
|
||||||
|
- 文案、折叠、布局等低影响可逆改动,不额外堆自动化测试;按需要进行构建或一次界面检查。
|
||||||
|
- 新测试应覆盖具体故障或容易回归的行为,不写仅重复实现逻辑、常量或显示文字的测试来增加数量。
|
||||||
|
- 一组相关改动完成后,运行必要检查。通过后,没有新改动、失败或未解决的疑点,不重复跑同一套检查或无目的扩大测试范围。
|
||||||
|
- 新改动或失败确实影响既有结论时,重跑相关检查;不要为了减少测试而跳过必要验证。
|
||||||
|
- 游戏内验证一次聚焦明确行为,提前说明操作和预期。已有样本够用时直接回放,不反复要求用户重抓或重复已经通过的场景。
|
||||||
|
|
||||||
|
## 合并发布与减少用户操作
|
||||||
|
|
||||||
|
- 按完整功能或一组相关修复合并发布,不每补几个协议字段或改几处文案就增加版本并要求热载。
|
||||||
|
- 崩溃、已验证功能回归或用户明确要求及时修复时,可以单独发布。
|
||||||
|
- 沿用已经约定的部署和备份流程,不新增没有实际必要的确认步骤。
|
||||||
|
- 提交给用户测试前完成当前能够独立完成的分析、实现和必要检查。只提出下一步确实需要用户提供的操作或信息。
|
||||||
|
|
||||||
|
## 进度与计划
|
||||||
|
|
||||||
|
- 进度首先说明新增了什么可用功能、解决了什么问题、还缺什么,以及用户是否需要做一步操作。
|
||||||
|
- 测试总数、哈希和详细反汇编依据放入相应验证记录;关键失败或验证限制仍需明确告知,不用测试数量替代功能结果。
|
||||||
|
- 网络与维护计划中的里程碑是能力分组,不是要求提前全部建成的关卡。版本诊断和管理器整理随实际需求逐步补充。
|
||||||
|
- 维护目标是更新后快速定位并修复受影响模块,不为追求完全自动适配拖延当前功能。
|
||||||
|
|
||||||
|
相关补充计划:NETWORK_AND_MAINTENANCE_PLAN.md。后续用户明确调整优先级时,以最新要求为准。
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Dalamud.Configuration;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
|
||||||
|
namespace ICRebuild;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class Configuration : IPluginConfiguration
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
public Dictionary<int, float> Values { get; set; } = [];
|
||||||
|
public HashSet<int> Requested { get; set; } = [];
|
||||||
|
public HashSet<uint> BlockedTimelines { get; set; } = [];
|
||||||
|
public HashSet<uint> BlockedStatuses { get; set; } = [StatusBlockPolicy.DefaultStatusId];
|
||||||
|
public bool BlockListed { get; set; } = true;
|
||||||
|
public DashFilterMode? DashMode { get; set; }
|
||||||
|
public string MovementListGameVersion { get; set; } = "";
|
||||||
|
public float KnockSeconds { get; set; }
|
||||||
|
public bool AutoSaveCapture { get; set; } = true;
|
||||||
|
private SettingsSnapshot published = new(new Dictionary<int,float>(),Array.Empty<uint>(),true,0);
|
||||||
|
[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 float Value(int id)
|
||||||
|
{
|
||||||
|
return Published.Value(id);
|
||||||
|
}
|
||||||
|
public void Normalize()
|
||||||
|
{
|
||||||
|
Values ??= []; Requested ??= []; BlockedTimelines ??= [];
|
||||||
|
BlockedStatuses ??= [StatusBlockPolicy.DefaultStatusId];
|
||||||
|
BlockedStatuses.RemoveWhere(id=>!StatusCatalog.AuditedBlockCandidates.Contains(id));
|
||||||
|
foreach (var feature in Features.All) Values[feature.Id] = Policies.FiniteClamp(Values.GetValueOrDefault(feature.Id,feature.Default),feature.Min,feature.Max,feature.Default);
|
||||||
|
foreach (var id in Values.Keys.Where(id => !Features.All.Any(f => f.Id == id)).ToArray()) Values.Remove(id);
|
||||||
|
Requested.RemoveWhere(id => !Features.All.Any(f => f.Id == id));
|
||||||
|
KnockSeconds = Policies.FiniteClamp(KnockSeconds, 0, 5);
|
||||||
|
BlockedTimelines.Remove(0);
|
||||||
|
BlockedTimelines.RemoveWhere(x=>x>ushort.MaxValue);
|
||||||
|
DashMode ??= BlockedTimelines.Count==0 ? DashFilterMode.AllLocal : BlockListed ? DashFilterMode.BlockListed : DashFilterMode.AllowListed;
|
||||||
|
if(!Enum.IsDefined(DashMode.Value)) DashMode=DashFilterMode.AllLocal;
|
||||||
|
Version=2;
|
||||||
|
Volatile.Write(ref published,new(Values,BlockedTimelines,BlockListed,KnockSeconds,DashMode.Value,BlockedStatuses));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public static class ActiveSendScope
|
||||||
|
{
|
||||||
|
[ThreadStatic] private static int depth;
|
||||||
|
public static bool Active => depth > 0;
|
||||||
|
public static IDisposable Enter()
|
||||||
|
{
|
||||||
|
depth++;
|
||||||
|
return new Scope();
|
||||||
|
}
|
||||||
|
private sealed class Scope : IDisposable
|
||||||
|
{
|
||||||
|
private int disposed;
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref disposed, 1) == 0 && depth > 0) depth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SendTransactionState
|
||||||
|
{
|
||||||
|
Queued,
|
||||||
|
SentNative,
|
||||||
|
RejectedNative,
|
||||||
|
Confirmed,
|
||||||
|
TimedOut,
|
||||||
|
Canceled
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SendSession(bool Ready, uint Territory, nint LocalAddress, uint LocalEntityId)
|
||||||
|
{
|
||||||
|
public static readonly SendSession Empty = new(false, 0, 0, 0);
|
||||||
|
public bool IsValid => Ready && Territory != 0 && LocalAddress != 0 && LocalEntityId is not (0 or 0xE0000000);
|
||||||
|
public bool Matches(SendSession other) =>
|
||||||
|
IsValid && other.IsValid && Territory == other.Territory &&
|
||||||
|
LocalAddress == other.LocalAddress && LocalEntityId == other.LocalEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record EncodedGamePacket(
|
||||||
|
string Name,
|
||||||
|
int Opcode,
|
||||||
|
byte[] Payload,
|
||||||
|
byte[] NativeBuffer,
|
||||||
|
int? ExpectedResponseOpcode = null,
|
||||||
|
Func<ReadOnlySpan<byte>, bool>? ResponseMatcher = null);
|
||||||
|
|
||||||
|
public sealed record SendTransactionSnapshot(
|
||||||
|
long Id,
|
||||||
|
string PacketName,
|
||||||
|
int Opcode,
|
||||||
|
SendTransactionState State,
|
||||||
|
long CreatedMs,
|
||||||
|
long? SentMs,
|
||||||
|
long? CompletedMs,
|
||||||
|
byte? NativeResult,
|
||||||
|
long? ResponseObservationId,
|
||||||
|
long? RoundTripMs,
|
||||||
|
string Detail);
|
||||||
|
|
||||||
|
public static class NativeSendPacket
|
||||||
|
{
|
||||||
|
public const string AuditedClientVersion = PacketParser.AuditedClientVersion;
|
||||||
|
public const string BaselineClientVersion = PacketParser.BaselineClientVersion;
|
||||||
|
public static bool IsAuditedVersion(string? version) => PacketParser.IsAuditedVersion(version);
|
||||||
|
public const int NativePrefixSize = 0x20;
|
||||||
|
public const int IpcHeaderSize = NativePacketCapture.IpcHeaderSize;
|
||||||
|
|
||||||
|
// Native ZoneUp (0x1D718C0) reads: +0x00 uint Opcode, +0x08 ulong (payloadLength + 0x10), +0x20 byte[] Payload.
|
||||||
|
public static byte[] BuildBuffer(int opcode, ReadOnlySpan<byte> payload)
|
||||||
|
{
|
||||||
|
if (opcode is <= 0 or > ushort.MaxValue) throw new ArgumentOutOfRangeException(nameof(opcode), "Opcode 超出有效范围");
|
||||||
|
if (payload.Length > NativePacketCapture.MaxPayload) throw new ArgumentOutOfRangeException(nameof(payload), "载荷超出 64 KiB 上限");
|
||||||
|
var buffer = new byte[NativePrefixSize + payload.Length];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), (uint)opcode);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(8, 8), (ulong)(payload.Length + IpcHeaderSize));
|
||||||
|
payload.CopyTo(buffer.AsSpan(NativePrefixSize));
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AuditedPacketEncoder
|
||||||
|
{
|
||||||
|
public static EncodedGamePacket EncodeEventStart(OpcodeMap map, string version, ulong targetObjectId, uint eventId, uint param = 0)
|
||||||
|
{
|
||||||
|
RequireAuditedVersion(version);
|
||||||
|
if (targetObjectId == 0 || targetObjectId == 0xE0000000) throw new ArgumentException("目标对象 ID 无效", nameof(targetObjectId));
|
||||||
|
if (eventId == 0) throw new ArgumentException("事件 ID 不能为 0", nameof(eventId));
|
||||||
|
var opcode = RequireOpcode(map, version, PacketDirection.Up, "UP_EventStart");
|
||||||
|
var responseOpcode = map.ResolveOpcode(version, PacketDirection.Down, "DOWN_EventStart");
|
||||||
|
var payload = new byte[0x10];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(payload.AsSpan(0, 8), targetObjectId);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8, 4), eventId);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12, 4), param);
|
||||||
|
return new("UP_EventStart", opcode, payload, NativeSendPacket.BuildBuffer(opcode, payload), responseOpcode,
|
||||||
|
responseOpcode.HasValue ? p => p.Length >= 12 && BinaryPrimitives.ReadUInt32LittleEndian(p.Slice(8, 4)) == eventId : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EncodedGamePacket EncodeEventFinish(OpcodeMap map, string version, uint eventId, ushort sceneId, byte state, ReadOnlySpan<uint> args)
|
||||||
|
{
|
||||||
|
RequireAuditedVersion(version);
|
||||||
|
if (eventId == 0) throw new ArgumentException("事件 ID 不能为 0", nameof(eventId));
|
||||||
|
var (name, capacity, length) = SelectEventTier("UP_EventFinish", args.Length);
|
||||||
|
var opcode = RequireOpcode(map, version, PacketDirection.Up, name);
|
||||||
|
var responseOpcode = map.ResolveOpcode(version, PacketDirection.Down, "DOWN_EventFinish");
|
||||||
|
var payload = WriteEventPayload(eventId, sceneId, state, args, capacity, length);
|
||||||
|
return new(name, opcode, payload, NativeSendPacket.BuildBuffer(opcode, payload), responseOpcode,
|
||||||
|
responseOpcode.HasValue ? p => p.Length >= 4 && BinaryPrimitives.ReadUInt32LittleEndian(p[..4]) == eventId : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EncodedGamePacket EncodeEventAction(OpcodeMap map, string version, uint eventId, ushort sceneId, byte state, ReadOnlySpan<uint> args)
|
||||||
|
{
|
||||||
|
RequireAuditedVersion(version);
|
||||||
|
if (eventId == 0) throw new ArgumentException("事件 ID 不能为 0", nameof(eventId));
|
||||||
|
var (name, capacity, length) = SelectEventTier("UP_EventAction", args.Length);
|
||||||
|
var opcode = RequireOpcode(map, version, PacketDirection.Up, name);
|
||||||
|
var payload = WriteEventPayload(eventId, sceneId, state, args, capacity, length);
|
||||||
|
return new(name, opcode, payload, NativeSendPacket.BuildBuffer(opcode, payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EncodedGamePacket EncodeClientTrigger(OpcodeMap map, string version, uint commandId,
|
||||||
|
uint arg0 = 0, uint arg1 = 0, uint arg2 = 0, uint arg3 = 0, ulong targetId = 0)
|
||||||
|
{
|
||||||
|
RequireAuditedVersion(version);
|
||||||
|
if (commandId == 0) throw new ArgumentException("触发指令 ID 不能为 0", nameof(commandId));
|
||||||
|
var opcode = RequireOpcode(map, version, PacketDirection.Up, "UP_ClientTrigger");
|
||||||
|
var payload = new byte[0x20];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0, 4), commandId);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4, 4), arg0);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8, 4), arg1);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12, 4), arg2);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(16, 4), arg3);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(payload.AsSpan(24, 8), targetId);
|
||||||
|
return new("UP_ClientTrigger", opcode, payload, NativeSendPacket.BuildBuffer(opcode, payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EncodedGamePacket EncodeActionSend(OpcodeMap map, string version, uint actionId, byte actionKind,
|
||||||
|
ushort requestId, ulong targetId, ushort facing = 0, ushort targetFacing = 0, uint arg = 0)
|
||||||
|
{
|
||||||
|
RequireAuditedVersion(version);
|
||||||
|
if (actionId == 0) throw new ArgumentException("技能 ID 不能为 0", nameof(actionId));
|
||||||
|
if (requestId == 0) throw new ArgumentException("请求序号 RequestId 必须大于 0", nameof(requestId));
|
||||||
|
if (targetId == 0 || targetId == 0xE0000000) throw new ArgumentException("目标对象 ID 无效", nameof(targetId));
|
||||||
|
var opcode = RequireOpcode(map, version, PacketDirection.Up, "UP_ActionSend");
|
||||||
|
var responseOpcode = map.ResolveOpcode(version, PacketDirection.Down, "DOWN_Effect");
|
||||||
|
var payload = new byte[0x20];
|
||||||
|
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.WriteUInt64LittleEndian(payload.AsSpan(16, 8), targetId);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(24, 4), arg);
|
||||||
|
return new("UP_ActionSend", opcode, payload, NativeSendPacket.BuildBuffer(opcode, payload), responseOpcode,
|
||||||
|
responseOpcode.HasValue
|
||||||
|
? p => p.Length == 0x7C && p[0x21] <= 1 && 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 范围"),
|
||||||
|
<= 2 => (prefix, 2, 0x10),
|
||||||
|
<= 4 => ($"{prefix}4", 4, 0x18),
|
||||||
|
<= 8 => ($"{prefix}8", 8, 0x28),
|
||||||
|
<= 16 => ($"{prefix}16", 16, 0x48),
|
||||||
|
<= 32 => ($"{prefix}32", 32, 0x88),
|
||||||
|
<= 64 => ($"{prefix}64", 64, 0x108),
|
||||||
|
<= 128 => ($"{prefix}128", 128, 0x208),
|
||||||
|
_ => ($"{prefix}255", 255, 0x408)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static byte[] WriteEventPayload(uint eventId, ushort sceneId, byte state, ReadOnlySpan<uint> args, int capacity, int length)
|
||||||
|
{
|
||||||
|
if (args.Length > capacity) throw new ArgumentOutOfRangeException(nameof(args));
|
||||||
|
var payload = new byte[length];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0, 4), eventId);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(4, 2), sceneId);
|
||||||
|
payload[6] = state;
|
||||||
|
payload[7] = (byte)args.Length;
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8 + i * 4, 4), args[i]);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RequireAuditedVersion(string version)
|
||||||
|
{
|
||||||
|
if (!NativeSendPacket.IsAuditedVersion(version))
|
||||||
|
throw new InvalidOperationException($"主动发包编码器仅适配已核对版本 {NativeSendPacket.AuditedClientVersion},当前为 {version}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RequireOpcode(OpcodeMap map, string version, PacketDirection direction, string name) =>
|
||||||
|
map.ResolveOpcode(version, direction, name)
|
||||||
|
?? throw new InvalidOperationException($"未找到 {version} 的 {name} Opcode 映射,禁止发送未映射消息");
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ActivePacketDispatcher(
|
||||||
|
string clientVersion,
|
||||||
|
Func<long>? clockMs = null,
|
||||||
|
int maxQueue = 32,
|
||||||
|
long minIntervalMs = 100,
|
||||||
|
long queueTimeoutMs = 2000,
|
||||||
|
long responseTimeoutMs = 5000)
|
||||||
|
{
|
||||||
|
private sealed class Transaction(long id, EncodedGamePacket packet, SendSession session, long createdMs)
|
||||||
|
{
|
||||||
|
public readonly long Id = id;
|
||||||
|
public readonly EncodedGamePacket Packet = packet;
|
||||||
|
public readonly SendSession Session = session;
|
||||||
|
public readonly long CreatedMs = createdMs;
|
||||||
|
public SendTransactionState State = SendTransactionState.Queued;
|
||||||
|
public long? SentMs;
|
||||||
|
public long? CompletedMs;
|
||||||
|
public byte? NativeResult;
|
||||||
|
public long? ResponseObservationId;
|
||||||
|
public long? RoundTripMs;
|
||||||
|
public string Detail = "排队等待主线程调度";
|
||||||
|
public SendTransactionSnapshot Snapshot() => new(
|
||||||
|
Id, Packet.Name, Packet.Opcode, State, CreatedMs, SentMs, CompletedMs,
|
||||||
|
NativeResult, ResponseObservationId, RoundTripMs, Detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ThreadStatic] private static bool inReceiveCallback;
|
||||||
|
private readonly object sync = new();
|
||||||
|
private readonly Func<long> nowMs = clockMs ?? (() => Environment.TickCount64);
|
||||||
|
private readonly List<Transaction> history = [];
|
||||||
|
private SendSession lastSession = SendSession.Empty;
|
||||||
|
private long nextId;
|
||||||
|
private long lastSendMs = long.MinValue / 2;
|
||||||
|
|
||||||
|
public int PendingQueueCount { get { lock (sync) return history.Count(t => t.State == SendTransactionState.Queued); } }
|
||||||
|
public int InFlightCount { get { lock (sync) return history.Count(t => t.State == SendTransactionState.SentNative && !t.CompletedMs.HasValue); } }
|
||||||
|
|
||||||
|
public IDisposable Attach(NetworkPipeline pipeline) =>
|
||||||
|
pipeline.Subscribe(NetworkPhase.Received, OnReceived);
|
||||||
|
|
||||||
|
public SendTransactionSnapshot Enqueue(EncodedGamePacket packet, SendSession session)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(packet);
|
||||||
|
if (!NativeSendPacket.IsAuditedVersion(clientVersion))
|
||||||
|
throw new InvalidOperationException($"主动发送仅适配已核对版本 {NativeSendPacket.AuditedClientVersion}");
|
||||||
|
if (!session.IsValid)
|
||||||
|
throw new InvalidOperationException("当前角色或区域会话未就绪,禁止入队发送");
|
||||||
|
if (packet.Payload == null || packet.NativeBuffer == null ||
|
||||||
|
packet.NativeBuffer.Length != NativeSendPacket.NativePrefixSize + packet.Payload.Length)
|
||||||
|
throw new InvalidDataException("发送包原生缓冲区长度与载荷不匹配");
|
||||||
|
if (!PacketParser.Parse(packet.Name, packet.Payload, clientVersion).Parsed)
|
||||||
|
throw new InvalidDataException($"{packet.Name} 载荷未通过本机协议边界校验");
|
||||||
|
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
var queuedCount = history.Count(t => t.State == SendTransactionState.Queued);
|
||||||
|
if (queuedCount >= maxQueue)
|
||||||
|
throw new InvalidOperationException($"发送队列已满(上限 {maxQueue} 条)");
|
||||||
|
if (lastSession.IsValid && !lastSession.Matches(session))
|
||||||
|
CancelSessionLocked("切区或角色会话变更,已取消旧会话请求");
|
||||||
|
lastSession = session;
|
||||||
|
var tx = new Transaction(++nextId, packet, session, nowMs());
|
||||||
|
history.Add(tx);
|
||||||
|
TrimHistoryLocked();
|
||||||
|
return tx.Snapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canceling a queued item prevents native dispatch; it never claims to undo an already-sent packet.
|
||||||
|
public bool Cancel(long transactionId, string reason = "已取消排队请求")
|
||||||
|
{
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
var tx = history.FirstOrDefault(t => t.Id == transactionId);
|
||||||
|
if (tx == null || tx.State != SendTransactionState.Queued) return false;
|
||||||
|
tx.State = SendTransactionState.Canceled;
|
||||||
|
tx.CompletedMs = nowMs();
|
||||||
|
tx.Detail = reason;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CancelAllQueued(string reason = "已清空待发送队列")
|
||||||
|
{
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
var now = nowMs();
|
||||||
|
foreach (var tx in history.Where(t => t.State == SendTransactionState.Queued))
|
||||||
|
{
|
||||||
|
tx.State = SendTransactionState.Canceled;
|
||||||
|
tx.CompletedMs = now;
|
||||||
|
tx.Detail = reason;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be called from Framework.Update on the main thread, never from a network receive callback.
|
||||||
|
public SendTransactionSnapshot? Pump(SendSession currentSession, Func<ReadOnlySpan<byte>, byte> nativeSender)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(nativeSender);
|
||||||
|
if (inReceiveCallback || NetworkPipeline.InCallback)
|
||||||
|
throw new InvalidOperationException("禁止在网络接收回调中直接触发主动发送");
|
||||||
|
|
||||||
|
Transaction? toSend = null;
|
||||||
|
var now = nowMs();
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
if (!currentSession.IsValid || (lastSession.IsValid && !lastSession.Matches(currentSession)))
|
||||||
|
{
|
||||||
|
CancelSessionLocked("切区或会话不可用,已取消未完成请求");
|
||||||
|
lastSession = currentSession.IsValid ? currentSession : SendSession.Empty;
|
||||||
|
if (!currentSession.IsValid) return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lastSession = currentSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tx in history)
|
||||||
|
{
|
||||||
|
if (tx.State == SendTransactionState.Queued && !tx.Session.Matches(currentSession))
|
||||||
|
{
|
||||||
|
tx.State = SendTransactionState.Canceled;
|
||||||
|
tx.CompletedMs = now;
|
||||||
|
tx.Detail = "会话已变更,过期请求未发送";
|
||||||
|
}
|
||||||
|
else if (tx.State == SendTransactionState.Queued && now - tx.CreatedMs > queueTimeoutMs)
|
||||||
|
{
|
||||||
|
tx.State = SendTransactionState.TimedOut;
|
||||||
|
tx.CompletedMs = now;
|
||||||
|
tx.Detail = $"排队超过 {queueTimeoutMs} ms 未发送,已过期丢弃";
|
||||||
|
}
|
||||||
|
else if (tx.State == SendTransactionState.SentNative && tx.Packet.ExpectedResponseOpcode.HasValue &&
|
||||||
|
tx.SentMs.HasValue && now - tx.SentMs.Value > responseTimeoutMs)
|
||||||
|
{
|
||||||
|
tx.State = SendTransactionState.TimedOut;
|
||||||
|
tx.CompletedMs = now;
|
||||||
|
tx.Detail = $"已发送(原生返回 {tx.NativeResult}),但等待响应 0x{tx.Packet.ExpectedResponseOpcode.Value:X4} 超过 {responseTimeoutMs} ms";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - lastSendMs < minIntervalMs) return null;
|
||||||
|
toSend = history.FirstOrDefault(t => t.State == SendTransactionState.Queued);
|
||||||
|
if (toSend == null) return null;
|
||||||
|
lastSendMs = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke native sender outside lock and outside observer catch blocks: never retry a native send.
|
||||||
|
byte nativeResult;
|
||||||
|
using (ActiveSendScope.Enter())
|
||||||
|
{
|
||||||
|
nativeResult = nativeSender(toSend.Packet.NativeBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sentAt = nowMs();
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
toSend.SentMs = sentAt;
|
||||||
|
toSend.NativeResult = nativeResult;
|
||||||
|
if (nativeResult == 0)
|
||||||
|
{
|
||||||
|
toSend.State = SendTransactionState.RejectedNative;
|
||||||
|
toSend.CompletedMs = sentAt;
|
||||||
|
toSend.Detail = "原生发送返回 0(连接未就绪或被底层拒绝)";
|
||||||
|
}
|
||||||
|
else if (toSend.Packet.ExpectedResponseOpcode.HasValue)
|
||||||
|
{
|
||||||
|
toSend.State = SendTransactionState.SentNative;
|
||||||
|
toSend.Detail = $"已调原生发送(返回 {nativeResult}),等待响应 0x{toSend.Packet.ExpectedResponseOpcode.Value:X4}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toSend.State = SendTransactionState.SentNative;
|
||||||
|
toSend.CompletedMs = sentAt;
|
||||||
|
toSend.Detail = $"已调原生发送(返回 {nativeResult},无回包契约,不代表服务端已执行)";
|
||||||
|
}
|
||||||
|
return toSend.Snapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendTransactionSnapshot[] Snapshot()
|
||||||
|
{
|
||||||
|
lock (sync) return history.Select(t => t.Snapshot()).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReceived(NetworkObservation observation)
|
||||||
|
{
|
||||||
|
var msg = observation.Message;
|
||||||
|
if (!msg.Complete || msg.Direction != PacketDirection.Down) return;
|
||||||
|
var prev = inReceiveCallback;
|
||||||
|
inReceiveCallback = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = nowMs();
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
foreach (var tx in history)
|
||||||
|
{
|
||||||
|
if (tx.State != SendTransactionState.SentNative || !tx.Packet.ExpectedResponseOpcode.HasValue) continue;
|
||||||
|
if (tx.Packet.ExpectedResponseOpcode.Value != msg.Opcode) continue;
|
||||||
|
if (tx.SentMs.HasValue && now - tx.SentMs.Value > responseTimeoutMs)
|
||||||
|
{
|
||||||
|
tx.State = SendTransactionState.TimedOut;
|
||||||
|
tx.CompletedMs = now;
|
||||||
|
tx.Detail = $"等待响应超时(>{responseTimeoutMs} ms)";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tx.Packet.ResponseMatcher != null && !tx.Packet.ResponseMatcher(msg.Payload)) continue;
|
||||||
|
tx.State = SendTransactionState.Confirmed;
|
||||||
|
tx.CompletedMs = now;
|
||||||
|
tx.ResponseObservationId = msg.Id;
|
||||||
|
tx.RoundTripMs = tx.SentMs.HasValue ? Math.Max(0, now - tx.SentMs.Value) : 0;
|
||||||
|
tx.Detail = $"收到响应确认(观察 #{msg.Id},耗时 {tx.RoundTripMs} ms)";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
inReceiveCallback = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelSessionLocked(string reason)
|
||||||
|
{
|
||||||
|
var now = nowMs();
|
||||||
|
foreach (var tx in history)
|
||||||
|
{
|
||||||
|
if (tx.State == SendTransactionState.Queued)
|
||||||
|
{
|
||||||
|
tx.State = SendTransactionState.Canceled;
|
||||||
|
tx.CompletedMs = now;
|
||||||
|
tx.Detail = reason;
|
||||||
|
}
|
||||||
|
else if (tx.State == SendTransactionState.SentNative && !tx.CompletedMs.HasValue)
|
||||||
|
{
|
||||||
|
tx.State = SendTransactionState.Canceled;
|
||||||
|
tx.CompletedMs = now;
|
||||||
|
tx.Detail = "切区或会话变更,停止等待旧会话响应";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrimHistoryLocked()
|
||||||
|
{
|
||||||
|
while (history.Count > 128)
|
||||||
|
{
|
||||||
|
var index = history.FindIndex(t => t.State != SendTransactionState.Queued);
|
||||||
|
if (index < 0) break;
|
||||||
|
history.RemoveAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public enum AdaptationIssue
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
MissingImplementation,
|
||||||
|
VersionMismatch,
|
||||||
|
OffsetMismatch,
|
||||||
|
SignatureMissing,
|
||||||
|
MultipleMatches,
|
||||||
|
RuntimeFault
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SignatureRequirement(string Name, string Signature, string AuditedRva, bool RequireUnique = true);
|
||||||
|
|
||||||
|
public sealed record ModuleAdaptationSpec(
|
||||||
|
int FeatureId,
|
||||||
|
string FeatureName,
|
||||||
|
bool VersionLocked,
|
||||||
|
bool RequiresSdkOffsets,
|
||||||
|
string Evidence,
|
||||||
|
SignatureRequirement[] Signatures);
|
||||||
|
|
||||||
|
public sealed record ModuleAdaptationEntry(
|
||||||
|
int FeatureId,
|
||||||
|
string FeatureName,
|
||||||
|
bool VersionLocked,
|
||||||
|
AdaptationIssue Issue,
|
||||||
|
Adaptation AdaptationState,
|
||||||
|
string Summary,
|
||||||
|
string Evidence,
|
||||||
|
string[] FailedSignatures);
|
||||||
|
|
||||||
|
public sealed record AdaptationReport(
|
||||||
|
string ClientVersion,
|
||||||
|
string AuditedVersion,
|
||||||
|
bool VersionMatches,
|
||||||
|
bool ActiveSendReady,
|
||||||
|
string ActiveSendSummary,
|
||||||
|
ModuleAdaptationEntry[] Entries)
|
||||||
|
{
|
||||||
|
public int ReadyCount => Entries.Count(e => e.Issue == AdaptationIssue.None);
|
||||||
|
public int BlockedCount => Entries.Count(e => e.Issue != AdaptationIssue.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AdaptationManifest
|
||||||
|
{
|
||||||
|
public const string AuditedVersion = PacketParser.AuditedClientVersion;
|
||||||
|
public const string BaselineVersion = PacketParser.BaselineClientVersion;
|
||||||
|
public static bool IsAuditedVersion(string? version) => PacketParser.IsAuditedVersion(version);
|
||||||
|
|
||||||
|
public const string ActiveSendReceiverSignature =
|
||||||
|
"48 83 EC 38 48 8B 89 70 0A 00 00 48 85 C9 74 0F C6 44 24 20 00 E8 ?? ?? ?? ?? 48 83 C4 38 C3 32 C0 48 83 C4 38 C3 CC CC CC CC CC CC CC CC CC CC 48 83 EC 38 48 8B 89 70 0A 00 00";
|
||||||
|
public const string ActiveSendZoneSignature =
|
||||||
|
"48 83 EC 28 48 8B 89 98 00 00 00 48 85 C9 74 1F 44 89 44 24 30 4C 8D 44 24 30";
|
||||||
|
|
||||||
|
public static readonly SignatureRequirement[] ActiveSendSignatures =
|
||||||
|
[
|
||||||
|
new("ReceiverSendPacket", ActiveSendReceiverSignature, "0x1CFD140"),
|
||||||
|
new("ZoneClientSendPacket", ActiveSendZoneSignature, "0x1D03040")
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly ModuleAdaptationSpec[] Specs =
|
||||||
|
[
|
||||||
|
new(1, "移速调整", false, false, "RVA 0x8D6E40 + 40 项减速状态保护",
|
||||||
|
[new("SpeedHook", "40 57 48 83 EC ?? 48 8B F9 48 8B 49 ?? 48 8B 01 FF 90 ?? ?? ?? ?? 48 85 C0 75", "0x8D6E40")]),
|
||||||
|
new(2, "最大加速度", true, false, "RVA 0x182BB20,偏移 +0x20/+0x3C/+0x44 与 owner +0x3E0 已核对",
|
||||||
|
[new("AccelerationHook", "40 ?? 48 ?? ?? ?? 80 79 ?? ?? 48 ?? ?? 0f 84 ?? ?? ?? ?? 48 89 7c 24 ?? 48 ?? ?? ??", "0x182BB20")]),
|
||||||
|
new(3, "强制移动", false, false, "RVA 0xE16130,Permission ID 14/96/97 已核对",
|
||||||
|
[new("MovePermissionHook", "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 54 41 56 41 57 48 83 EC ?? 33 DB 41 8B E9 48 39 1D", "0xE16130")]),
|
||||||
|
new(4, "击退距离调整", false, false, "RVA 0x90BC90,按本地角色地址过滤",
|
||||||
|
[new("AntiKnockHook", "48 ?? ?? 57 48 ?? ?? ?? ?? ?? ?? 0f 29 70 ?? 0f ?? ??", "0x90BC90")]),
|
||||||
|
new(5, "掉落无伤", false, false, "RVA 0x62AF60,移动标志位掩码处理",
|
||||||
|
[new("OnSendFlagHook", "48 89 5c 24 ?? 57 48 ?? ?? ?? 8b ?? 48 ?? ?? 33 ?? e8 ?? ?? ?? ?? 84 ??", "0x62AF60")]),
|
||||||
|
new(6, "无掉落", false, false, "调用点 RVA 0x182F9E4",
|
||||||
|
[new("FallCheckHook", "e8 ?? ?? ?? ?? 8b ?? 85 ?? 0f 88 ?? ?? ?? ?? 48 ?? ?? ?? f3 ?? ?? ?? ?? ?? ?? ??", "0x182F9E4")]),
|
||||||
|
new(7, "魅惑与恐惧移动处理", true, false, "调用点 RVA 0x194AEC3,第 5 参数核实为 float",
|
||||||
|
[new("ForcedWalkHook", "e8 ?? ?? ?? ?? b0 ?? c7 43 ?? ?? ?? ?? ?? eb ??", "0x194AEC3")]),
|
||||||
|
new(8, "状态屏蔽", true, true, "SetStatus RVA 0x8A6BF0 + 本地角色 RVA 0xB37D7A + 偏移 0x23B0/0x78",
|
||||||
|
[
|
||||||
|
new("StatusSetter", "40 53 56 41 56 48 83 EC 70 45 32 F6 0F 29 74 24 50 41 0F B7 F0 0F 28 F3 48 8B D9 83 FA 3C", "0x8A6BF0"),
|
||||||
|
new("LocalPlayerPtr", "48 8B 1D ?? ?? ?? ?? 48 8B FA 48 85 DB 74 28 48 8B 03 48 8B CB FF 90 78 02 00 00", "0xB37D7A")
|
||||||
|
]),
|
||||||
|
new(9, "飞天遁地", false, false, "普通移动 RVA 0x62B680 + 战斗移动 RVA 0x62B730",
|
||||||
|
[
|
||||||
|
new("OnSendNormalMove", "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B F9 41 8B D8", "0x62B680"),
|
||||||
|
new("OnSendCombatMove", "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B F9 41 8B F0", "0x62B730")
|
||||||
|
]),
|
||||||
|
new(10, "深层迷宫专用模式", false, false, "依赖模块 9 与 EventFramework 深层迷宫实例", []),
|
||||||
|
new(11, "迷宫状态显示", false, false, "EventFramework.GetInstanceContentDeepDungeon", []),
|
||||||
|
new(13, "技能距离调整", false, false, "ActionManager.GetActionRange SDK 入口", []),
|
||||||
|
new(14, "目标圈半径调整", true, false, "ActorRadius RVA 0x175728F + 原生圈 RVA 0x7B5430 + ActionManager 距离入口",
|
||||||
|
[
|
||||||
|
new("ActorRadius", "e8 ?? ?? ?? ?? f3 ?? ?? ?? 48 ?? ?? ?? ?? f3 ?? ?? ?? ?? f3 ?? ?? ?? f3 ?? ?? ?? ?? f3 0f 11 74 24 ?? ", "0x175728F"),
|
||||||
|
new("NativeTargetRingUpdate", "4C 8B DC 55 57 41 57 49 8D AB ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 80 B9 ?? ?? ?? ?? ?? 4C 8B FA 48 8B F9 0F 85 ?? ?? ?? ?? 48 8B 41 30", "0x7B5430")
|
||||||
|
]),
|
||||||
|
new(15, "后摇期间可移动", false, false, "RVA 0x8D6F30(首匹配包装入口)",
|
||||||
|
[new("NoBackswing", "48 ?? ?? ?? 48 ?? ?? ?? 45 ?? ?? 33 ?? e8 ?? ?? ?? ?? 84 ?? 74 ?? ", "0x8D6F30", RequireUnique: false)]),
|
||||||
|
new(16, "突进无位移", true, false, "公共位移初始化 RVA 0x1822510,owner +0x3E0 与 ushort Timeline",
|
||||||
|
[new("ActionMovementStart", "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 48 8B D9 0F B6 FA 48 81 C1 ?? ?? ?? ?? 4D 8B F1 4D 8B F8 E8 ?? ?? ?? ?? 48 8B B3 ?? ?? ?? ??", "0x1822510")]),
|
||||||
|
new(17, "复唱缩减秒数", false, false, "GetAdjustedRecastTime + 同步 RVA 0x8EF260(GCD 组 57)",
|
||||||
|
[new("SyncRecast", "40 ?? 48 ?? ?? ?? 0f 29 74 24 ?? 41 ?? ?? 0f ?? ?? e8 ?? ?? ?? ??", "0x8EF260")]),
|
||||||
|
new(18, "快速结印", false, false, "依赖模块 17,结印 ActionId 2259/2261/2263/18805..18807", []),
|
||||||
|
new(19, "咏唱缩减秒数", false, false, "GetAdjustedCastTime + 静态地址 RVA 0x104F17 + 更新 RVA 0x8E00A0",
|
||||||
|
[
|
||||||
|
new("CastStaticAddress", "F3 44 0F 2C C0 BA ?? ?? ?? ?? 48 8B CB E8 ?? ?? ?? ?? F3 44 0F 10 1D", "0x104F17"),
|
||||||
|
new("CastUpdate", "48 89 5C 24 ?? 57 48 83 EC ?? 48 8B F9 0F 29 74 24 ?? 0F B6 49", "0x8E00A0")
|
||||||
|
]),
|
||||||
|
new(20, "PVP 动画锁上限", false, false, "ActionManager.AnimationLock 帧更新", []),
|
||||||
|
new(27, "坐标移动", false, false, "GameObject.SetPosition", []),
|
||||||
|
new(28, "面向调整", false, false, "GameObject.SetRotation", []),
|
||||||
|
new(29, "目标缩放", false, false, "RVA 0x899720,含原缩放恢复表",
|
||||||
|
[new("SetScale", "F3 0F 11 89 ?? ?? ?? ?? E9 ?? ?? ?? ?? CC CC CC CC CC CC CC CC CC CC CC CC", "0x899720")]),
|
||||||
|
new(36, "网络记录", true, false, "ZoneUp RVA 0x1D718C0 + ZoneDown RVA 0x1D71440 + 共享观察管线",
|
||||||
|
[
|
||||||
|
new("ZoneUp", "48 89 5C 24 ?? 48 89 74 24 ?? 4C 89 64 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 70", "0x1D718C0"),
|
||||||
|
new("ZoneDown", "48 89 5C 24 18 57 48 83 EC 20 48 83 B9 88 00 00 00 00 48 8B FA 48 8B D9", "0x1D71440")
|
||||||
|
]),
|
||||||
|
new(37, "包解析与 Opcode 管理", false, false, "离线/实时解析与请求响应关联,不挂接原生 Hook", [])
|
||||||
|
];
|
||||||
|
|
||||||
|
public static AdaptationReport Evaluate(
|
||||||
|
ModuleManager modules,
|
||||||
|
string clientVersion,
|
||||||
|
Func<string, int> countMatches,
|
||||||
|
bool sdkOffsetsValid = true)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(modules);
|
||||||
|
ArgumentNullException.ThrowIfNull(countMatches);
|
||||||
|
var versionMatches = IsAuditedVersion(clientVersion);
|
||||||
|
var entries = new List<ModuleAdaptationEntry>(Specs.Length);
|
||||||
|
|
||||||
|
foreach (var spec in Specs)
|
||||||
|
{
|
||||||
|
var status = modules.Status[spec.FeatureId];
|
||||||
|
if (!status.HasImplementation)
|
||||||
|
{
|
||||||
|
entries.Add(new(spec.FeatureId, spec.FeatureName, spec.VersionLocked,
|
||||||
|
AdaptationIssue.MissingImplementation, status.Adaptation, "缺实现,保持禁用", spec.Evidence, []));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (spec.VersionLocked && !versionMatches)
|
||||||
|
{
|
||||||
|
entries.Add(new(spec.FeatureId, spec.FeatureName, spec.VersionLocked,
|
||||||
|
AdaptationIssue.VersionMismatch, Adaptation.Unadapted,
|
||||||
|
$"版本不符:仅核对 {AuditedVersion}(兼容 {BaselineVersion}),当前为 {clientVersion}", spec.Evidence, []));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (spec.RequiresSdkOffsets && !sdkOffsetsValid)
|
||||||
|
{
|
||||||
|
entries.Add(new(spec.FeatureId, spec.FeatureName, spec.VersionLocked,
|
||||||
|
AdaptationIssue.OffsetMismatch, Adaptation.Unadapted,
|
||||||
|
"SDK 角色/状态偏移与已核对入口不一致", spec.Evidence, []));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> missing = [];
|
||||||
|
List<string> multiple = [];
|
||||||
|
foreach (var sig in spec.Signatures)
|
||||||
|
{
|
||||||
|
var matches = countMatches(sig.Signature);
|
||||||
|
if (matches <= 0) missing.Add($"{sig.Name} ({sig.AuditedRva})");
|
||||||
|
else if (sig.RequireUnique && matches > 1) multiple.Add($"{sig.Name} ({matches} 处匹配)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.Count > 0)
|
||||||
|
{
|
||||||
|
entries.Add(new(spec.FeatureId, spec.FeatureName, spec.VersionLocked,
|
||||||
|
AdaptationIssue.SignatureMissing, Adaptation.Unadapted,
|
||||||
|
$"签名未匹配:{string.Join(", ", missing)}", spec.Evidence, [.. missing]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (multiple.Count > 0)
|
||||||
|
{
|
||||||
|
entries.Add(new(spec.FeatureId, spec.FeatureName, spec.VersionLocked,
|
||||||
|
AdaptationIssue.MultipleMatches, Adaptation.Unadapted,
|
||||||
|
$"签名多重匹配,拒绝盲挂:{string.Join(", ", multiple)}", spec.Evidence, [.. multiple]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (status.State == ModuleState.Failed)
|
||||||
|
{
|
||||||
|
entries.Add(new(spec.FeatureId, spec.FeatureName, spec.VersionLocked,
|
||||||
|
AdaptationIssue.RuntimeFault, status.Adaptation,
|
||||||
|
$"运行异常:{status.Detail}", spec.Evidence, []));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var adapt = versionMatches && ValidationCatalog.UserConfirmed.Contains(spec.FeatureId)
|
||||||
|
? Adaptation.Verified
|
||||||
|
: Adaptation.Testable;
|
||||||
|
var label = adapt == Adaptation.Verified ? "已核对且用户实测有效" : "签名与版本核对通过,待实测";
|
||||||
|
entries.Add(new(spec.FeatureId, spec.FeatureName, spec.VersionLocked,
|
||||||
|
AdaptationIssue.None, adapt, label, spec.Evidence, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
var (activeSendReady, activeSendSummary) = EvaluateActiveSend(clientVersion, countMatches);
|
||||||
|
return new(clientVersion, AuditedVersion, versionMatches, activeSendReady, activeSendSummary, [.. entries]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (bool Ready, string Summary) EvaluateActiveSend(string clientVersion, Func<string, int> countMatches)
|
||||||
|
{
|
||||||
|
if (!IsAuditedVersion(clientVersion))
|
||||||
|
return (false, $"主动发送(M4)已禁用:仅核对已审计版本({AuditedVersion} / {BaselineVersion}),当前为 {clientVersion}");
|
||||||
|
foreach (var sig in ActiveSendSignatures)
|
||||||
|
{
|
||||||
|
var matches = countMatches(sig.Signature);
|
||||||
|
if (matches <= 0) return (false, $"主动发送(M4)已禁用:签名未匹配 {sig.Name} ({sig.AuditedRva})");
|
||||||
|
if (sig.RequireUnique && matches > 1) return (false, $"主动发送(M4)已禁用:签名多重匹配 {sig.Name} ({matches})");
|
||||||
|
}
|
||||||
|
return (true, "主动发送(M4)入口核对通过:ReceiverSendPacket → ZoneClientSendPacket");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies diagnostic gating without breaking unaffected modules or offline analysis.
|
||||||
|
public static void ApplyGate(ModuleManager modules, AdaptationReport report)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(modules);
|
||||||
|
ArgumentNullException.ThrowIfNull(report);
|
||||||
|
foreach (var entry in report.Entries)
|
||||||
|
{
|
||||||
|
if (!modules.Status.TryGetValue(entry.FeatureId, out var status)) continue;
|
||||||
|
status.Adaptation = entry.AdaptationState;
|
||||||
|
if (entry.Issue is AdaptationIssue.VersionMismatch or AdaptationIssue.OffsetMismatch
|
||||||
|
or AdaptationIssue.SignatureMissing or AdaptationIssue.MultipleMatches)
|
||||||
|
{
|
||||||
|
if (modules.IsEnabled(entry.FeatureId)) modules.Disable(entry.FeatureId);
|
||||||
|
modules.Unavailable(entry.FeatureId, entry.Summary);
|
||||||
|
}
|
||||||
|
else if (entry.Issue == AdaptationIssue.None)
|
||||||
|
{
|
||||||
|
modules.ClearUnavailable(entry.FeatureId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public enum DashFilterMode { AllLocal, BlockListed, AllowListed }
|
||||||
|
|
||||||
|
public static class DashPolicy
|
||||||
|
{
|
||||||
|
// Audited native consumer reads word [data], then flags at [data+2] and [data+3].
|
||||||
|
public static ushort Timeline(ReadOnlySpan<byte> data) => data.Length>=4 ? BinaryPrimitives.ReadUInt16LittleEndian(data) : (ushort)0;
|
||||||
|
public static bool Block(bool ready,nint owner,nint local,ushort timeline,DashFilterMode mode,IReadOnlySet<uint> list)
|
||||||
|
{
|
||||||
|
if(!ready || owner==0 || owner!=local || timeline==0) return false;
|
||||||
|
return mode switch
|
||||||
|
{
|
||||||
|
DashFilterMode.AllLocal=>true,
|
||||||
|
DashFilterMode.BlockListed=>list.Contains(timeline),
|
||||||
|
DashFilterMode.AllowListed=>list.Count>0 && !list.Contains(timeline),
|
||||||
|
_=>false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record DashObservation(ushort Timeline,byte Flags2,byte Flags3,byte MoveType,bool Blocked);
|
||||||
|
|
||||||
|
public sealed record MovementSkill(uint ActionId,string Name,uint Timeline,bool PlayerAction,bool AffectsPosition,bool PvP);
|
||||||
|
public static class MovementSkillCatalog
|
||||||
|
{
|
||||||
|
public static MovementSkill[] Select(IEnumerable<MovementSkill> skills) => skills
|
||||||
|
.Where(s=>s.PlayerAction && s.AffectsPosition).DistinctBy(s=>s.ActionId).OrderBy(s=>s.ActionId).ToArray();
|
||||||
|
public static bool Mappable(MovementSkill skill) => skill.Timeline is >0 and <=ushort.MaxValue;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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 =
|
||||||
|
[
|
||||||
|
(338,"DOWN_PingRsp"),
|
||||||
|
(671,"DOWN_UpdateHate"),
|
||||||
|
(558,"DOWN_UpdateHater"),
|
||||||
|
(697,"DOWN_EffectResultBasic"),
|
||||||
|
// ActorGauge already exists at a different explicit slot in the cache; use an evidence label.
|
||||||
|
(614,"DOWN_JobGauge"),
|
||||||
|
(788,"DOWN_UpdateClassInfo"),
|
||||||
|
(214,"DOWN_FirstAttack")
|
||||||
|
];
|
||||||
|
public static int AddMissing(string version,Dictionary<int,string> down)
|
||||||
|
{
|
||||||
|
if(version!=PacketParser.AuditedClientVersion) return 0;
|
||||||
|
var added=0;
|
||||||
|
foreach(var (opcode,name) in Downstream)
|
||||||
|
if(!down.ContainsKey(opcode) && !down.ContainsValue(name)) { down[opcode]=name; added++; }
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public sealed record Feature(int Id, string Name, string Group, float Min = 0, float Max = 0, float Default = 0);
|
||||||
|
|
||||||
|
public static class Features
|
||||||
|
{
|
||||||
|
public static readonly Feature[] All =
|
||||||
|
[
|
||||||
|
new(1,"移速调整","移动",0,5), new(2,"最大加速度","移动"),
|
||||||
|
new(3,"强制移动","移动"), new(4,"击退距离调整","移动",0,30),
|
||||||
|
new(5,"掉落无伤","移动"), new(6,"无掉落","移动"),
|
||||||
|
new(7,"魅惑与恐惧移动处理","移动"), new(8,"状态屏蔽","移动"),
|
||||||
|
new(9,"飞天遁地","深层迷宫",-10,10,-7), new(10,"深层迷宫专用模式","深层迷宫"),
|
||||||
|
new(11,"迷宫状态显示","深层迷宫"), new(13,"技能距离调整","战斗",0,40),
|
||||||
|
new(14,"目标圈半径调整","战斗",0,5), new(15,"后摇期间可移动","战斗"),
|
||||||
|
new(16,"突进无位移","战斗"), new(17,"复唱缩减秒数","战斗",0,1),
|
||||||
|
new(18,"快速结印","战斗"), new(19,"咏唱缩减秒数","战斗",0,1),
|
||||||
|
new(20,"PVP 动画锁上限","战斗",0,1,0.6f),
|
||||||
|
new(27,"坐标移动","坐标"), new(28,"面向调整","坐标"),
|
||||||
|
new(29,"目标缩放","坐标",0.1f,5,1),
|
||||||
|
new(36,"网络记录","网络"), new(37,"包解析与 Opcode 管理","网络")
|
||||||
|
];
|
||||||
|
public static Feature Get(int id) => All.First(x => x.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Adaptation { Unadapted, Testable, Verified, KnownBroken }
|
||||||
|
public enum ModuleState { Disabled, Enabled, Failed }
|
||||||
|
|
||||||
|
public sealed class ModuleStatus(Feature feature)
|
||||||
|
{
|
||||||
|
public Feature Feature { get; } = feature;
|
||||||
|
public Adaptation Adaptation { get; set; } = Adaptation.Unadapted;
|
||||||
|
public ModuleState State { get; set; }
|
||||||
|
public bool Requested { get; set; }
|
||||||
|
public bool HasImplementation { get; set; }
|
||||||
|
public string Detail { get; set; } = "未接入";
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
// Session state only. Editing the saved preset never changes the applied height.
|
||||||
|
public sealed class HeightActivation
|
||||||
|
{
|
||||||
|
private float applied;
|
||||||
|
public float Applied => Volatile.Read(ref applied);
|
||||||
|
public bool IsApplied => Applied!=0;
|
||||||
|
public float Toggle(bool moduleEnabled,float preset)
|
||||||
|
{
|
||||||
|
if(!moduleEnabled || IsApplied) { Reset(); return 0; }
|
||||||
|
var next=Policies.FiniteClamp(preset,-10,10);
|
||||||
|
Volatile.Write(ref applied,next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
public void Reset() => Volatile.Write(ref applied,0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
// Every resource is registered before activation, including during partial startup.
|
||||||
|
public sealed class ResourceScope : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Stack<IDisposable> resources = new();
|
||||||
|
private bool disposed;
|
||||||
|
public T Own<T>(T resource) where T : IDisposable
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(disposed, this);
|
||||||
|
resources.Push(resource);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
public void Defer(Action action) => Own(new ActionDisposable(action));
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed) return;
|
||||||
|
disposed = true;
|
||||||
|
List<Exception> errors = [];
|
||||||
|
while (resources.TryPop(out var resource))
|
||||||
|
try { resource.Dispose(); } catch (Exception ex) { errors.Add(ex); }
|
||||||
|
if (errors.Count > 0) throw new AggregateException("资源释放失败", errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ActionDisposable(Action action) : IDisposable
|
||||||
|
{
|
||||||
|
private Action? release = action;
|
||||||
|
public void Dispose() => Interlocked.Exchange(ref release, null)?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ModuleManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, Action<ResourceScope>> factories = [];
|
||||||
|
private readonly ConcurrentDictionary<int, ResourceScope> active = [];
|
||||||
|
private readonly Dictionary<int, int[]> dependencies = [];
|
||||||
|
private readonly Dictionary<int, string> unavailable = [];
|
||||||
|
private readonly HashSet<int> cleanupFailed = [];
|
||||||
|
private bool disposed;
|
||||||
|
public Dictionary<int, ModuleStatus> Status { get; } = Features.All.ToDictionary(f => f.Id, f => new ModuleStatus(f));
|
||||||
|
public bool IsEnabled(int id) => active.ContainsKey(id);
|
||||||
|
|
||||||
|
public void Register(int id, Action<ResourceScope> start, params int[] required)
|
||||||
|
{
|
||||||
|
factories.Add(id, start); dependencies[id] = required;
|
||||||
|
Status[id].HasImplementation=true;
|
||||||
|
Status[id].Detail = "已关闭";
|
||||||
|
}
|
||||||
|
public void Unavailable(int id, string reason)
|
||||||
|
{
|
||||||
|
unavailable[id] = reason;
|
||||||
|
if (Status.TryGetValue(id, out var state)) state.Detail = reason;
|
||||||
|
}
|
||||||
|
public void ClearUnavailable(int id) => unavailable.Remove(id);
|
||||||
|
|
||||||
|
public bool Enable(int id)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(disposed, this);
|
||||||
|
if (!Status.TryGetValue(id, out var state)) return false;
|
||||||
|
if (cleanupFailed.Contains(id)) { state.Detail="上次资源释放失败,重载插件前禁止重复挂接"; return false; }
|
||||||
|
if (unavailable.TryGetValue(id, out var blockedReason)) { state.State = ModuleState.Failed; state.Detail = blockedReason; return false; }
|
||||||
|
state.Requested = true;
|
||||||
|
if (active.ContainsKey(id)) return true;
|
||||||
|
if (!factories.TryGetValue(id, out var start)) return false;
|
||||||
|
var missing = dependencies[id].Where(x => !IsEnabled(x)).ToArray();
|
||||||
|
if (missing.Length > 0)
|
||||||
|
{ state.State = ModuleState.Failed; state.Detail = $"先启用依赖:{string.Join(", ", missing)}"; return false; }
|
||||||
|
var scope = new ResourceScope();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
start(scope);
|
||||||
|
if (!active.TryAdd(id, scope)) throw new InvalidOperationException("模块已经启用");
|
||||||
|
state.State = ModuleState.Enabled;
|
||||||
|
if(state.Adaptation!=Adaptation.Verified) state.Adaptation = Adaptation.Testable;
|
||||||
|
state.Detail = "已开启";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
try { scope.Dispose(); } catch (Exception cleanup) { cleanupFailed.Add(id); ex = new AggregateException(ex, cleanup); }
|
||||||
|
state.State = ModuleState.Failed; state.Detail = ex.Message;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void Disable(int id)
|
||||||
|
{
|
||||||
|
if (!Status.TryGetValue(id, out var state)) return;
|
||||||
|
foreach (var dependent in dependencies.Where(x => x.Value.Contains(id) && IsEnabled(x.Key)).Select(x => x.Key).ToArray())
|
||||||
|
Disable(dependent);
|
||||||
|
state.Requested = false;
|
||||||
|
if (!active.TryRemove(id, out var scope)) return;
|
||||||
|
try { scope.Dispose(); state.State = ModuleState.Disabled; state.Detail = "已关闭"; }
|
||||||
|
catch (Exception ex) { cleanupFailed.Add(id); state.State = ModuleState.Failed; state.Detail = $"卸载失败:{ex.Message}"; }
|
||||||
|
}
|
||||||
|
public void DisableAll() { foreach (var id in active.Keys.Reverse().ToArray()) Disable(id); }
|
||||||
|
public void Dispose() { if (disposed) return; DisableAll(); disposed = true; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public static class NativeIds
|
||||||
|
{
|
||||||
|
// The native check resolves Permission sheet 0x14A. 1001+ are native aliases, not Status IDs.
|
||||||
|
public static readonly FrozenSet<uint> MovementPermissionChecks = new uint[] {96,97,98,99,0x3E9,0x3EE,0x3EF,0x3F0}.ToFrozenSet();
|
||||||
|
// Action rows: Ten, Chi and Jin, including retained variants in the current action sheet.
|
||||||
|
public static readonly FrozenSet<uint> MudraActions = new uint[] {2259,2261,2263,18805,18806,18807}.ToFrozenSet();
|
||||||
|
// Zero-based cooldown group index passed to ActionManager.GetRecastGroupDetail.
|
||||||
|
public const int GlobalCooldownGroup = 57;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
// The reader must copy exactly count bytes from the current process or fail.
|
||||||
|
public delegate bool PacketMemoryReader(nint address,int count,out byte[] bytes);
|
||||||
|
public sealed record CapturedPacket(int Opcode,uint? Target,byte[] Payload,bool Complete,string? Issue=null);
|
||||||
|
public sealed record CaptureRead(CapturedPacket? Packet,string? Error=null);
|
||||||
|
|
||||||
|
public static class NativePacketCapture
|
||||||
|
{
|
||||||
|
public const string ClientVersion="2026.10.06.0000.0000";
|
||||||
|
public const string BaselineClientVersion="2026.09.15.0000.0000";
|
||||||
|
public static bool IsSupportedVersion(string? version) => version is ClientVersion or BaselineClientVersion;
|
||||||
|
public const int IpcHeaderSize=0x10;
|
||||||
|
public const int MaxPayload=65536;
|
||||||
|
|
||||||
|
public static CaptureRead Up(PacketMemoryReader read,nint data,Func<int,bool> accepts)
|
||||||
|
{
|
||||||
|
if(!Read(read,data,0,16,out var header)) return new(null,"上行对象头读取失败");
|
||||||
|
var opcode=BinaryPrimitives.ReadUInt16LittleEndian(header);
|
||||||
|
if(!accepts(opcode)) return new(null);
|
||||||
|
return Payload(read,data,0x20,BinaryPrimitives.ReadUInt64LittleEndian(header.AsSpan(8)),opcode,null);
|
||||||
|
}
|
||||||
|
// Called only AFTER ZoneReceive returned success; holder owns the message until its caller releases it.
|
||||||
|
public static CaptureRead Down(PacketMemoryReader read,nint holder,Func<int,bool> accepts)
|
||||||
|
{
|
||||||
|
if(!Read(read,holder,0x10,8,out var reference)) return new(null,"下行消息引用读取失败");
|
||||||
|
var message=Pointer(reference);
|
||||||
|
if(!Read(read,message,0x20,0x20,out var descriptor)) return new(null,"下行消息描述读取失败");
|
||||||
|
var length=BinaryPrimitives.ReadUInt64LittleEndian(descriptor.AsSpan(0x10));
|
||||||
|
var data=Pointer(descriptor.AsSpan(0x18));
|
||||||
|
// Even a fallback must not read an opcode beyond the declared message.
|
||||||
|
if(length<4 || !Read(read,data,0,4,out var ipc)) return new(null,"下行 IPC 头读取失败或长度不足");
|
||||||
|
var opcode=BinaryPrimitives.ReadUInt16LittleEndian(ipc.AsSpan(2));
|
||||||
|
if(!accepts(opcode)) return new(null);
|
||||||
|
var target=NormalizeTarget(BinaryPrimitives.ReadUInt32LittleEndian(descriptor));
|
||||||
|
return Payload(read,data,IpcHeaderSize,length,opcode,target);
|
||||||
|
}
|
||||||
|
private static CaptureRead Payload(PacketMemoryReader read,nint data,int offset,ulong messageLength,int opcode,uint? target)
|
||||||
|
{
|
||||||
|
if(!TryPayloadLength(messageLength,out var count))
|
||||||
|
return new(new(opcode,target,[],false,$"消息长度无效或超出采集上限:{messageLength}"));
|
||||||
|
if(count==0) return new(new(opcode,target,[],true));
|
||||||
|
return Read(read,data,offset,count,out var bytes)
|
||||||
|
? new(new(opcode,target,bytes,true))
|
||||||
|
: new(new(opcode,target,[],false,"载荷读取失败,未保留部分数据"));
|
||||||
|
}
|
||||||
|
public static bool TryPayloadLength(ulong messageLength,out int count)
|
||||||
|
{
|
||||||
|
count=0;
|
||||||
|
if(messageLength<IpcHeaderSize || messageLength>MaxPayload+IpcHeaderSize) return false;
|
||||||
|
count=(int)messageLength-IpcHeaderSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Same reserved-target normalization as the native NetworkModule dispatcher caller.
|
||||||
|
public static uint NormalizeTarget(uint target) => (target&0x5FF80000u)==0x5FF80000u ? 0x5FF80002u-(target&1u) : target;
|
||||||
|
private static nint Pointer(ReadOnlySpan<byte> bytes)
|
||||||
|
{
|
||||||
|
var value=BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||||
|
return value>(ulong)nint.MaxValue ? 0 : (nint)value;
|
||||||
|
}
|
||||||
|
private static bool Read(PacketMemoryReader read,nint data,int offset,int count,out byte[] bytes)
|
||||||
|
{
|
||||||
|
bytes=[];
|
||||||
|
if(data<=0 || offset<0 || count<0 || count>MaxPayload || (long)data>long.MaxValue-offset-count) return false;
|
||||||
|
if(!read(data+offset,count,out var result) || result==null || result.Length!=count) return false;
|
||||||
|
bytes=result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public enum NetworkPhase { Received, BeforeSend, AfterSend }
|
||||||
|
public enum PacketOrigin { NativeZone, PluginActiveSend }
|
||||||
|
public enum PacketStage { RawIpcPayload }
|
||||||
|
public sealed record PacketContext(long ObservationId, PacketOrigin Origin, PacketStage Stage,
|
||||||
|
NetworkPhase Phase, byte? NativeResult = null);
|
||||||
|
|
||||||
|
// Owned immutable copy. No native pointer or mutable array is exposed to observers.
|
||||||
|
public sealed class NetworkMessage
|
||||||
|
{
|
||||||
|
private readonly byte[] payload;
|
||||||
|
public long Id { get; }
|
||||||
|
public DateTimeOffset Time { get; } = DateTimeOffset.UtcNow;
|
||||||
|
public long MonotonicTicks { get; } = Stopwatch.GetTimestamp();
|
||||||
|
public PacketDirection Direction { get; }
|
||||||
|
public string GameVersion { get; }
|
||||||
|
public int Opcode { get; }
|
||||||
|
public uint? Target { get; }
|
||||||
|
public bool Complete { get; }
|
||||||
|
public string? Issue { get; }
|
||||||
|
public PacketOrigin Origin { get; }
|
||||||
|
public ReadOnlySpan<byte> Payload => payload;
|
||||||
|
public NetworkMessage(long id, PacketDirection direction, string version, CapturedPacket packet,
|
||||||
|
PacketOrigin origin = PacketOrigin.NativeZone)
|
||||||
|
{
|
||||||
|
Id=id; Direction=direction; GameVersion=version; Opcode=packet.Opcode;
|
||||||
|
Target=packet.Target; Complete=packet.Complete; Issue=packet.Issue; Origin=origin;
|
||||||
|
payload=packet.Payload.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record NetworkObservation(NetworkMessage Message, NetworkPhase Phase, byte? NativeResult = null)
|
||||||
|
{
|
||||||
|
public PacketContext Context => new(Message.Id,Message.Origin,PacketStage.RawIpcPayload,Phase,NativeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline on the native calling thread, in registration order per observation. Concurrent
|
||||||
|
// calls may interleave. Callbacks must be short; no game/UI service access, waits for
|
||||||
|
// Framework, native sending, or changes to native Hook lifetimes from a callback.
|
||||||
|
public sealed class NetworkPipeline(string version) : IDisposable
|
||||||
|
{
|
||||||
|
private sealed class Subscription(NetworkPhase phase, Action<NetworkObservation> callback, int? opcode)
|
||||||
|
{
|
||||||
|
public readonly NetworkPhase Phase=phase;
|
||||||
|
public readonly Action<NetworkObservation> Callback=callback;
|
||||||
|
public readonly int? Opcode=opcode;
|
||||||
|
public int Active=1;
|
||||||
|
public bool Matches(NetworkPhase phase, int? opcode=null) => Volatile.Read(ref Active)!=0 &&
|
||||||
|
Phase==phase && (!opcode.HasValue || !Opcode.HasValue || Opcode==opcode);
|
||||||
|
}
|
||||||
|
private readonly object sync=new();
|
||||||
|
private Subscription[] subscriptions=[];
|
||||||
|
private bool disposed;
|
||||||
|
private long sequence,readErrors,observerErrors,received,beforeSend,afterSend;
|
||||||
|
private int lastSendResult=-1;
|
||||||
|
private string lastIssue="无";
|
||||||
|
public long ReadErrors => Interlocked.Read(ref readErrors);
|
||||||
|
public long ObserverErrors => Interlocked.Read(ref observerErrors);
|
||||||
|
public long Received => Interlocked.Read(ref received);
|
||||||
|
public long BeforeSend => Interlocked.Read(ref beforeSend);
|
||||||
|
public long AfterSend => Interlocked.Read(ref afterSend);
|
||||||
|
public int LastSendResult => Volatile.Read(ref lastSendResult);
|
||||||
|
public string LastIssue => Volatile.Read(ref lastIssue);
|
||||||
|
public int SubscriberCount => Volatile.Read(ref subscriptions).Length;
|
||||||
|
|
||||||
|
public IDisposable Subscribe(NetworkPhase phase, Action<NetworkObservation> callback, int? opcode=null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(callback);
|
||||||
|
if(!Enum.IsDefined(phase) || opcode is <0 or >65535) throw new ArgumentOutOfRangeException(nameof(opcode));
|
||||||
|
var item=new Subscription(phase,callback,opcode);
|
||||||
|
lock(sync)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(disposed,this);
|
||||||
|
Volatile.Write(ref subscriptions,[..subscriptions,item]);
|
||||||
|
}
|
||||||
|
return new ActionDisposable(()=>
|
||||||
|
{
|
||||||
|
// Already-entered callbacks may finish; cancellation cannot recall them.
|
||||||
|
Interlocked.Exchange(ref item.Active,0);
|
||||||
|
lock(sync) Volatile.Write(ref subscriptions,subscriptions.Where(x=>x!=item).ToArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public byte Send(Func<byte> original, Func<Func<int,bool>,CaptureRead> capture)
|
||||||
|
{
|
||||||
|
var listeners=Volatile.Read(ref subscriptions);
|
||||||
|
NetworkMessage? message=null;
|
||||||
|
if(listeners.Any(s=>s.Matches(NetworkPhase.BeforeSend) || s.Matches(NetworkPhase.AfterSend)))
|
||||||
|
message=Read(PacketDirection.Up,capture,opcode=>listeners.Any(s=>
|
||||||
|
s.Matches(NetworkPhase.BeforeSend,opcode) || s.Matches(NetworkPhase.AfterSend,opcode)));
|
||||||
|
if(message!=null)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref beforeSend);
|
||||||
|
Publish(listeners,new(message,NetworkPhase.BeforeSend));
|
||||||
|
}
|
||||||
|
// Outside observer/read exception handlers: never retry the native call.
|
||||||
|
var result=original();
|
||||||
|
Volatile.Write(ref lastSendResult,result);
|
||||||
|
if(message!=null)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref afterSend);
|
||||||
|
Publish(listeners,new(message,NetworkPhase.AfterSend,result));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public byte Receive(Func<byte> original, Func<Func<int,bool>,CaptureRead> capture)
|
||||||
|
{
|
||||||
|
var result=original();
|
||||||
|
if(result==0) return result; // Holder is unreadable on unsuccessful receive.
|
||||||
|
var listeners=Volatile.Read(ref subscriptions);
|
||||||
|
if(listeners.Any(s=>s.Matches(NetworkPhase.Received)))
|
||||||
|
{
|
||||||
|
var message=Read(PacketDirection.Down,capture,opcode=>listeners.Any(s=>s.Matches(NetworkPhase.Received,opcode)));
|
||||||
|
if(message!=null)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref received);
|
||||||
|
Publish(listeners,new(message,NetworkPhase.Received,result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private NetworkMessage? Read(PacketDirection direction,Func<Func<int,bool>,CaptureRead> capture,Func<int,bool> accepts)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var read=capture(accepts);
|
||||||
|
if(read.Error!=null) { ReadFailed(read.Error); return null; }
|
||||||
|
if(read.Packet is not {} packet) return null;
|
||||||
|
if(!packet.Complete) ReadFailed(packet.Issue ?? "载荷读取不完整");
|
||||||
|
var origin=direction==PacketDirection.Up && ActiveSendScope.Active ? PacketOrigin.PluginActiveSend : PacketOrigin.NativeZone;
|
||||||
|
return new(Interlocked.Increment(ref sequence),direction,version,packet,origin);
|
||||||
|
}
|
||||||
|
catch(Exception ex) { ReadFailed(ex.Message); return null; }
|
||||||
|
}
|
||||||
|
private void ReadFailed(string issue)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref readErrors); Volatile.Write(ref lastIssue,issue);
|
||||||
|
}
|
||||||
|
[ThreadStatic] private static int callbackDepth;
|
||||||
|
public static bool InCallback => callbackDepth > 0;
|
||||||
|
private void Publish(Subscription[] listeners,NetworkObservation observation)
|
||||||
|
{
|
||||||
|
callbackDepth++;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach(var listener in listeners)
|
||||||
|
{
|
||||||
|
if(!listener.Matches(observation.Phase,observation.Message.Opcode)) continue;
|
||||||
|
try { listener.Callback(observation); }
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref observerErrors);
|
||||||
|
Volatile.Write(ref lastIssue,$"{observation.Phase} 订阅异常:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if(callbackDepth>0) callbackDepth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock(sync)
|
||||||
|
{
|
||||||
|
disposed=true;
|
||||||
|
foreach(var listener in subscriptions) Interlocked.Exchange(ref listener.Active,0);
|
||||||
|
Volatile.Write(ref subscriptions,[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
// One consumer of the manager. Pausing and filtering affect only this log.
|
||||||
|
public sealed class PacketRecorder(PacketBuffer buffer)
|
||||||
|
{
|
||||||
|
private long completeUp,completeDown,metadataOnly;
|
||||||
|
public long CompleteUp => Interlocked.Read(ref completeUp);
|
||||||
|
public long CompleteDown => Interlocked.Read(ref completeDown);
|
||||||
|
public long MetadataOnly => Interlocked.Read(ref metadataOnly);
|
||||||
|
public void Record(NetworkObservation observation)
|
||||||
|
{
|
||||||
|
if(observation.Phase==NetworkPhase.AfterSend) return; // One record per message.
|
||||||
|
var packet=observation.Message;
|
||||||
|
if(!buffer.Add(packet.Direction,packet.Opcode,packet.GameVersion,null,packet.Target,packet.Payload,
|
||||||
|
packet.Complete,packet.Issue,observation.Context,packet.MonotonicTicks,packet.Time)) return;
|
||||||
|
if(!packet.Complete) Interlocked.Increment(ref metadataOnly);
|
||||||
|
else if(packet.Direction==PacketDirection.Up) Interlocked.Increment(ref completeUp);
|
||||||
|
else Interlocked.Increment(ref completeDown);
|
||||||
|
}
|
||||||
|
}
|
||||||
+401
@@ -0,0 +1,401 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public enum PacketDirection { Up, Down }
|
||||||
|
public sealed record PacketRecord(long Sequence, long MonotonicTicks, DateTimeOffset Time, PacketDirection Direction,
|
||||||
|
int Opcode, string GameVersion, uint? Source, uint? Target, byte[] Payload, bool PayloadComplete = true,string? CaptureIssue = null,
|
||||||
|
PacketContext? Context = null);
|
||||||
|
public sealed record OpcodeSnapshot(string GameVersion, string Source, IReadOnlyDictionary<int,string> Up, IReadOnlyDictionary<int,string> Down);
|
||||||
|
|
||||||
|
public sealed class OpcodeMap
|
||||||
|
{
|
||||||
|
private OpcodeSnapshot snapshot = new("unknown","none",new ReadOnlyDictionary<int,string>(new Dictionary<int,string>()),new ReadOnlyDictionary<int,string>(new Dictionary<int,string>()));
|
||||||
|
public OpcodeSnapshot Snapshot => Volatile.Read(ref snapshot);
|
||||||
|
public string? Resolve(string version, PacketDirection direction, int opcode)
|
||||||
|
{
|
||||||
|
var state = Snapshot;
|
||||||
|
return version != "unknown" && state.GameVersion == version ? (direction == PacketDirection.Up ? state.Up : state.Down).GetValueOrDefault(opcode) : null;
|
||||||
|
}
|
||||||
|
public int? ResolveOpcode(string version, PacketDirection direction, string name)
|
||||||
|
{
|
||||||
|
var state = Snapshot;
|
||||||
|
if (version == "unknown" || state.GameVersion != version || string.IsNullOrWhiteSpace(name)) return null;
|
||||||
|
var dict = direction == PacketDirection.Up ? state.Up : state.Down;
|
||||||
|
int? found = null;
|
||||||
|
foreach (var kv in dict)
|
||||||
|
{
|
||||||
|
if (kv.Value != name) continue;
|
||||||
|
if (found.HasValue) return null; // Refuse ambiguous multi-opcode mappings.
|
||||||
|
found = kv.Key;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
public void Load(string json, string version, string source)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(version) || version == "unknown") throw new InvalidDataException("客户端版本未知,不能应用映射");
|
||||||
|
using var document = JsonDocument.Parse(json,new JsonDocumentOptions { MaxDepth = 12 });
|
||||||
|
if (!document.RootElement.TryGetProperty(version,out var entries) || entries.ValueKind != JsonValueKind.Object)
|
||||||
|
throw new InvalidDataException($"映射不包含版本 {version}");
|
||||||
|
Dictionary<int,string> up = [], down = [];
|
||||||
|
foreach (var property in entries.EnumerateObject())
|
||||||
|
{
|
||||||
|
Dictionary<int,string> target;
|
||||||
|
if (property.Name.StartsWith("UP_",StringComparison.Ordinal)) target = up;
|
||||||
|
else if (property.Name.StartsWith("DOWN_",StringComparison.Ordinal)) target = down;
|
||||||
|
else continue;
|
||||||
|
if (property.Value.ValueKind != JsonValueKind.Array) throw new InvalidDataException("Opcode 值应为数组");
|
||||||
|
foreach (var item in property.Value.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (!item.TryGetInt32(out var code) || code is < 0 or > 65535) throw new InvalidDataException("Opcode 超出 ushort 范围");
|
||||||
|
if (target.TryGetValue(code,out var old) && old != property.Name) throw new InvalidDataException($"Opcode {code} 存在冲突");
|
||||||
|
target[code] = property.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (up.Count + down.Count == 0) throw new InvalidDataException("映射为空");
|
||||||
|
var additions=CurrentProtocolEvidence.AddMissing(version,down);
|
||||||
|
if(additions>0) source+=$" + 本机核实 {additions} 项";
|
||||||
|
Volatile.Write(ref snapshot,new(version,source,new ReadOnlyDictionary<int,string>(up),new ReadOnlyDictionary<int,string>(down)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ParseResult(bool Parsed, string Summary);
|
||||||
|
public static class PacketParser
|
||||||
|
{
|
||||||
|
public const string AuditedClientVersion="2026.10.06.0000.0000";
|
||||||
|
public const string BaselineClientVersion="2026.09.15.0000.0000";
|
||||||
|
public static bool IsAuditedVersion(string? version) => version is AuditedClientVersion or BaselineClientVersion;
|
||||||
|
public static ParseResult Parse(string? name, ReadOnlySpan<byte> payload,string? gameVersion=null)
|
||||||
|
{
|
||||||
|
if (name == null) return new(false,"未知 Opcode;保留原始数据");
|
||||||
|
var current=IsAuditedVersion(gameVersion);
|
||||||
|
if (TryEventTier(name,out var eventLabel,out var eventCapacity,out var eventLength))
|
||||||
|
return EventPacket(name,eventLabel,payload,current,eventCapacity,eventLength);
|
||||||
|
switch (name)
|
||||||
|
{
|
||||||
|
case "UP_PingReq":
|
||||||
|
var pingLength=current ? 0x20 : 0x1C;
|
||||||
|
if (payload.Length != pingLength) return Invalid(name,payload.Length,pingLength);
|
||||||
|
return new(true,$"Ping 请求 time={U32(payload,0)} ms reported={U32(payload,4)} ms");
|
||||||
|
case "DOWN_PingRsp":
|
||||||
|
if (current ? payload.Length!=0x20 : payload.Length<0x10) return Invalid(name,payload.Length,current ? 0x20 : 0x10);
|
||||||
|
return new(true,$"Ping 响应 echo={U32(payload,0)} ms field04={U32(payload,4)}(未解释);其余字节不解释");
|
||||||
|
case "UP_UpdatePositionHandler":
|
||||||
|
var positionLength=current ? 0x18 : 0x28;
|
||||||
|
if (payload.Length != positionLength) return Invalid(name,payload.Length,positionLength);
|
||||||
|
return Position(payload,8);
|
||||||
|
case "UP_UpdatePositionInstance":
|
||||||
|
if (payload.Length != 0x30) return Invalid(name,payload.Length,0x30);
|
||||||
|
var first = Position(payload,12); var second = Position(payload,24);
|
||||||
|
return new(first.Parsed && second.Parsed,$"{first.Summary}; new {second.Summary}");
|
||||||
|
case "UP_EventStart":
|
||||||
|
if (payload.Length != 0x10) return Invalid(name,payload.Length,0x10);
|
||||||
|
return new(true,$"对象={U64(payload,0)} event={U32(payload,8)}");
|
||||||
|
case "UP_ActionSend":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x20) return Invalid(name,payload.Length,0x20);
|
||||||
|
return new(true,$"动作请求 · 技能={U32(payload,0)} 类型={payload[5]} 请求序号={U16(payload,6)} 目标=0x{U64(payload,0x10):X16} 附加={U32(payload,0x18)}");
|
||||||
|
case "UP_ActionSendPos":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x18) return Invalid(name,payload.Length,0x18);
|
||||||
|
var actionPos=Position(payload,0x0C);
|
||||||
|
return actionPos.Parsed
|
||||||
|
? new(true,$"地面动作请求 · 技能={U32(payload,0)} 类型={payload[5]} 请求序号={U16(payload,6)} {actionPos.Summary}")
|
||||||
|
: actionPos;
|
||||||
|
case "UP_ClientTrigger":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x20) return Invalid(name,payload.Length,0x20);
|
||||||
|
return new(true,$"客户端触发器 · 指令={U32(payload,0)} 参数=({U32(payload,4)},{U32(payload,8)},{U32(payload,12)},{U32(payload,16)}) 目标=0x{U64(payload,0x18):X16}");
|
||||||
|
case "DOWN_UpdateHate": case "DOWN_UpdateHater":
|
||||||
|
if(!current) return new(false,$"{name}:此版本的仇恨列表布局未核实");
|
||||||
|
return Enmity(name,payload,name=="DOWN_UpdateHate" ? 8 : 32);
|
||||||
|
case "DOWN_EffectResultBasic":
|
||||||
|
if(!current) return new(false,"此版本的精简效果结果布局未核实");
|
||||||
|
if(payload.Length!=24) return Invalid(name,payload.Length,24);
|
||||||
|
if(payload[0]>1) return new(false,"精简效果结果计数超出此包容量");
|
||||||
|
if(payload[0]==0) return new(true,"精简效果结果条目=0");
|
||||||
|
return new(true,$"精简效果结果条目=1 · 序列={U32(payload,4)} · 当前 HP={U32(payload,12)} · 目标序号={payload[16]};其他字段未解释");
|
||||||
|
case "DOWN_JobGauge":
|
||||||
|
if(!current) return new(false,"此版本的职业量表布局未核实");
|
||||||
|
if(payload.Length!=16) return Invalid(name,payload.Length,16);
|
||||||
|
return new(true,$"职业量表更新 · 职业 ID={payload[0]};各职业量表字段保留原始数据");
|
||||||
|
case "DOWN_UpdateClassInfo":
|
||||||
|
if(!current) return new(false,"此版本的职业信息布局未核实");
|
||||||
|
if(payload.Length!=16) return Invalid(name,payload.Length,16);
|
||||||
|
return new(true,$"职业信息 · 职业 ID={payload[0]} · 当前等级={U16(payload,2)} · 经验={U32(payload,8)};其他字段未解释");
|
||||||
|
case "DOWN_FirstAttack":
|
||||||
|
if(!current) return new(false,"此版本的战斗归属布局未核实");
|
||||||
|
if(payload.Length!=16) return Invalid(name,payload.Length,16);
|
||||||
|
return new(true,$"战斗归属 · 类型={payload[0]} · 标记者=0x{U64(payload,8):X16};不是技能伤害包");
|
||||||
|
case "DOWN_ActorControl":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x18) return Invalid(name,payload.Length,0x18);
|
||||||
|
var category=U16(payload,0);
|
||||||
|
var note=category==0x22 ? "(Arg0 为分派前原始值)" : "";
|
||||||
|
return new(true,$"角色控制 · 类型=0x{category:X4} 参数=({U32(payload,4)},{U32(payload,8)},{U32(payload,12)},{U32(payload,16)}){note}");
|
||||||
|
case "DOWN_ActorControlSelf":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x28) return Invalid(name,payload.Length,0x28);
|
||||||
|
return new(true,$"自身控制 · 类型=0x{U16(payload,0):X4} 参数=({U32(payload,4)},{U32(payload,8)},{U32(payload,12)},{U32(payload,16)},{U32(payload,20)},{U32(payload,24)},{U32(payload,28)},{U32(payload,32)})");
|
||||||
|
case "DOWN_ActorControlTarget":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x20) return Invalid(name,payload.Length,0x20);
|
||||||
|
return new(true,$"目标控制 · 类型=0x{U16(payload,0):X4} 参数=({U32(payload,4)},{U32(payload,8)},{U32(payload,12)},{U32(payload,16)}) 目标=0x{U64(payload,0x18):X16}");
|
||||||
|
case "DOWN_Effect":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x7C) return Invalid(name,payload.Length,0x7C);
|
||||||
|
var lockTime=F32(payload,0x10);
|
||||||
|
if(!float.IsFinite(lockTime) || lockTime<0) return new(false,"技能效果动画锁时间无效");
|
||||||
|
var targetCount=payload[0x21];
|
||||||
|
if(targetCount>1) return new(false,$"单目标技能效果目标计数 {targetCount} 超出容量 1");
|
||||||
|
return new(true,$"技能效果 · 序列={U32(payload,0x0C)} 请求序号={U16(payload,0x18)} 动画锁={lockTime:F2}s 类型={payload[0x1F]} 目标数={targetCount} 主目标=0x{U64(payload,0x70):X16}(动作与效果字为分派前原始值)");
|
||||||
|
case "DOWN_StatusEffectList":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x180) return Invalid(name,payload.Length,0x180);
|
||||||
|
var activeSlots=0;
|
||||||
|
for(var i=0;i<30;i++)
|
||||||
|
{
|
||||||
|
var slotOffset=0x14+i*12;
|
||||||
|
if(!float.IsFinite(F32(payload,slotOffset+4))) return new(false,"状态列表包含非有限持续时间");
|
||||||
|
if(U16(payload,slotOffset)!=0) activeSlots++;
|
||||||
|
}
|
||||||
|
return new(true,$"状态列表 · 职业={payload[0]} 等级={payload[1]} HP={U32(payload,4)}/{U32(payload,8)} 资源={U16(payload,12)}/{U16(payload,14)} 护盾={payload[0x10]}% 非空槽位={activeSlots}/30(状态 ID 为分派转换前原始值)");
|
||||||
|
case "DOWN_EffectResult":
|
||||||
|
if(current)
|
||||||
|
{
|
||||||
|
if(payload.Length!=0x60) return Invalid(name,payload.Length,0x60);
|
||||||
|
int entryCount=payload[0];
|
||||||
|
if(entryCount>1) return new(false,"效果结果计数超出单条目包容量");
|
||||||
|
if(entryCount==0) return new(true,"效果结果条目=0");
|
||||||
|
int statusCount=payload[0x19];
|
||||||
|
if(statusCount>4) return new(false,"效果状态计数超限");
|
||||||
|
for(var i=0;i<statusCount;i++)
|
||||||
|
if(!float.IsFinite(F32(payload,0x1C+i*16+8))) return new(false,"效果状态持续时间包含非有限数");
|
||||||
|
return new(true,$"效果结果条目=1 · 序列={U32(payload,4)} HP={U32(payload,0x0C)}/{U32(payload,0x10)} MP={U16(payload,0x14)} 目标序号={payload[0x16]} 职业={payload[0x17]} 护盾={payload[0x18]}% 状态数={statusCount}");
|
||||||
|
}
|
||||||
|
if (payload.Length < 4) return Invalid(name,payload.Length,4);
|
||||||
|
int count = payload[0];
|
||||||
|
if (count > 16 || payload.Length < 4 + count * 0x58) return new(false,"效果结果计数超限或载荷截断");
|
||||||
|
for (var i=0;i<count;i++)
|
||||||
|
if (payload[4+i*0x58+0x15] > 4) return new(false,"效果状态计数超限");
|
||||||
|
return new(true,$"效果结果条目={count};历史布局解析,须样本核验");
|
||||||
|
case "DOWN_ActorDelete":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=8) return Invalid(name,payload.Length,8);
|
||||||
|
return new(true,$"对象移除 · 槽位索引={payload[0]};其余字节未由消费函数读取");
|
||||||
|
case "DOWN_ObjectSpawn":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x40) return Invalid(name,payload.Length,0x40);
|
||||||
|
var objPos=Position(payload,0x34);
|
||||||
|
return objPos.Parsed
|
||||||
|
? new(true,$"对象生成 · 槽位={payload[0]} 类型={payload[1]} BaseId={U32(payload,4)} EntityId=0x{U32(payload,8):X8} {objPos.Summary}")
|
||||||
|
: objPos;
|
||||||
|
case "DOWN_NpcSpawn":
|
||||||
|
if(!current) return Unverified(name);
|
||||||
|
if(payload.Length!=0x290) return Invalid(name,payload.Length,0x290);
|
||||||
|
var npcPos=Position(payload,0x200);
|
||||||
|
return npcPos.Parsed
|
||||||
|
? new(true,$"NPC 生成 · 槽位={payload[0x7E]} BaseId={U32(payload,0x40)} Owner=0x{U32(payload,0x54):X8} 等级={payload[0x86]} 职业={payload[0x87]} HP={U32(payload,0x60)}/{U32(payload,0x5C)} {npcPos.Summary}")
|
||||||
|
: npcPos;
|
||||||
|
default: return new(false,$"{name}:尚无已接入解析器,保留原始数据");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static bool TryEventTier(string name,out string label,out int capacity,out int expectedLength)
|
||||||
|
{
|
||||||
|
label=""; capacity=0; expectedLength=0;
|
||||||
|
ReadOnlySpan<char> suffix;
|
||||||
|
if(name.StartsWith("UP_EventFinish",StringComparison.Ordinal)) { label="事件完成"; suffix=name.AsSpan("UP_EventFinish".Length); }
|
||||||
|
else if(name.StartsWith("UP_EventAction",StringComparison.Ordinal)) { label="事件动作"; suffix=name.AsSpan("UP_EventAction".Length); }
|
||||||
|
else return false;
|
||||||
|
if(suffix.IsEmpty) { capacity=2; expectedLength=0x10; return true; }
|
||||||
|
if(!int.TryParse(suffix,out capacity)) return false;
|
||||||
|
expectedLength=capacity switch { 4=>0x18, 8=>0x28, 16=>0x48, 32=>0x88, 64=>0x108, 128=>0x208, 255=>0x408, _=>0 };
|
||||||
|
return expectedLength!=0;
|
||||||
|
}
|
||||||
|
private static ParseResult EventPacket(string name,string label,ReadOnlySpan<byte> payload,bool current,int capacity,int expectedLength)
|
||||||
|
{
|
||||||
|
if(!current) return new(false,$"{name}:变长事件分档仅核实 {AuditedClientVersion},保留 {payload.Length} 字节原始数据");
|
||||||
|
if(payload.Length!=expectedLength) return Invalid(name,payload.Length,expectedLength);
|
||||||
|
int argCount=payload[7];
|
||||||
|
if(argCount>capacity) return new(false,$"{name} 参数计数 {argCount} 超出分档容量 {capacity}");
|
||||||
|
var previewCount=Math.Min(argCount,4);
|
||||||
|
var args=new List<string>(previewCount);
|
||||||
|
for(var i=0;i<previewCount;i++) args.Add(U32(payload,8+i*4).ToString());
|
||||||
|
var suffix=argCount>previewCount ? ",..." : "";
|
||||||
|
return new(true,$"{label} · event={U32(payload,0)} scene={U16(payload,4)} 状态={payload[6]} 参数数={argCount}/{capacity} [{string.Join(",",args)}{suffix}]");
|
||||||
|
}
|
||||||
|
private static ushort U16(ReadOnlySpan<byte> p,int at) => BinaryPrimitives.ReadUInt16LittleEndian(p[at..]);
|
||||||
|
private static uint U32(ReadOnlySpan<byte> p,int at) => BinaryPrimitives.ReadUInt32LittleEndian(p[at..]);
|
||||||
|
private static ulong U64(ReadOnlySpan<byte> p,int at) => BinaryPrimitives.ReadUInt64LittleEndian(p[at..]);
|
||||||
|
private static float F32(ReadOnlySpan<byte> p,int at) => BinaryPrimitives.ReadSingleLittleEndian(p[at..]);
|
||||||
|
private static ParseResult Unverified(string name) => new(false,$"{name}:此版本的字段布局未核实");
|
||||||
|
private static ParseResult Enmity(string name,ReadOnlySpan<byte> payload,int capacity)
|
||||||
|
{
|
||||||
|
var expected=8+capacity*8; // 4-byte prefix, fixed entries, 4 trailing bytes kept uninterpreted.
|
||||||
|
if(payload.Length!=expected) return Invalid(name,payload.Length,expected);
|
||||||
|
var count=payload[0];
|
||||||
|
if(count>capacity) return new(false,$"仇恨列表计数 {count} 超过容量 {capacity}");
|
||||||
|
var entries=new List<string>(count);
|
||||||
|
for(var i=0;i<count;i++) entries.Add($"0x{U32(payload,4+i*8):X8}:{payload[8+i*8]}");
|
||||||
|
var label=name=="DOWN_UpdateHate" ? "目标仇恨列表" : "自身敌对列表";
|
||||||
|
return new(true,$"{label} {count} 项(对象:仇恨值){string.Join(", ",entries)}");
|
||||||
|
}
|
||||||
|
private static ParseResult Position(ReadOnlySpan<byte> p,int at)
|
||||||
|
{
|
||||||
|
float x=BinaryPrimitives.ReadSingleLittleEndian(p[at..]), y=BinaryPrimitives.ReadSingleLittleEndian(p[(at+4)..]), z=BinaryPrimitives.ReadSingleLittleEndian(p[(at+8)..]);
|
||||||
|
return float.IsFinite(x)&&float.IsFinite(y)&&float.IsFinite(z) ? new(true,$"X={x:F3} Y={y:F3} Z={z:F3}") : new(false,"坐标包含非有限数");
|
||||||
|
}
|
||||||
|
private static ParseResult Invalid(string name,int actual,int expected) => new(false,$"{name} 长度不匹配:{actual},预期 {expected}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PacketBuffer(int capacity = 2048, int maxPayload = 65536,int maxBytes = 8*1024*1024)
|
||||||
|
{
|
||||||
|
private readonly object sync = new();
|
||||||
|
private readonly Queue<PacketRecord> records = new();
|
||||||
|
private long sequence;
|
||||||
|
private bool paused = true;
|
||||||
|
private PacketDirection? directionFilter;
|
||||||
|
private int? opcodeFilter;
|
||||||
|
private long dropped;
|
||||||
|
private long payloadBytes;
|
||||||
|
public long PayloadBytes { get { lock(sync) return payloadBytes; } }
|
||||||
|
public int PayloadByteLimit => maxBytes;
|
||||||
|
public bool Paused { get { lock(sync) return paused; } set { lock(sync) paused=value; } }
|
||||||
|
public PacketDirection? DirectionFilter { get { lock(sync) return directionFilter; } set { lock(sync) directionFilter=value; } }
|
||||||
|
public int? OpcodeFilter { get { lock(sync) return opcodeFilter; } set { lock(sync) opcodeFilter=value; } }
|
||||||
|
public long Dropped { get { lock(sync) return dropped; } }
|
||||||
|
public bool Accepts(PacketDirection direction,int? opcode=null)
|
||||||
|
{
|
||||||
|
lock(sync) return !paused && (!directionFilter.HasValue || directionFilter==direction) &&
|
||||||
|
(!opcode.HasValue || !opcodeFilter.HasValue || opcodeFilter==opcode);
|
||||||
|
}
|
||||||
|
public bool Add(PacketDirection direction,int opcode,string version,uint? source,uint? target,ReadOnlySpan<byte> payload,bool payloadComplete = true,string? captureIssue = null,
|
||||||
|
PacketContext? context=null,long? monotonicTicks=null,DateTimeOffset? time=null)
|
||||||
|
{
|
||||||
|
lock(sync)
|
||||||
|
{
|
||||||
|
if (paused || (directionFilter.HasValue && directionFilter!=direction) || (opcodeFilter.HasValue && opcodeFilter!=opcode)) return false;
|
||||||
|
if (payload.Length>maxPayload || payload.Length>maxBytes || opcode is <0 or >65535 || capacity <= 0 || !Enum.IsDefined(direction)) { dropped++; return false; }
|
||||||
|
while (records.Count>0 && (records.Count>=capacity || payloadBytes+payload.Length>maxBytes))
|
||||||
|
{ payloadBytes-=records.Dequeue().Payload.Length; dropped++; }
|
||||||
|
records.Enqueue(new(++sequence,monotonicTicks ?? System.Diagnostics.Stopwatch.GetTimestamp(),time ?? DateTimeOffset.UtcNow,direction,opcode,version,source,target,payload.ToArray(),payloadComplete,captureIssue,context));
|
||||||
|
payloadBytes+=payload.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public PacketRecord[] Snapshot() { lock(sync) return records.ToArray(); }
|
||||||
|
public void Clear() { lock(sync) { records.Clear(); dropped=0; payloadBytes=0; } }
|
||||||
|
public void Replace(IEnumerable<PacketRecord> imported)
|
||||||
|
{
|
||||||
|
var input=imported.ToArray();
|
||||||
|
if(capacity<=0 || maxBytes<0) throw new InvalidDataException("无效缓冲区容量");
|
||||||
|
foreach(var p in input)
|
||||||
|
if(p.Payload == null || p.Payload.Length>maxPayload || p.Opcode is <0 or >65535 || !Enum.IsDefined(p.Direction) || string.IsNullOrWhiteSpace(p.GameVersion))
|
||||||
|
throw new InvalidDataException("无效记录");
|
||||||
|
lock(sync)
|
||||||
|
{
|
||||||
|
records.Clear(); payloadBytes=0; dropped=Math.Max(0,input.Length-capacity);
|
||||||
|
foreach(var p in input.TakeLast(capacity))
|
||||||
|
{
|
||||||
|
if(p.Payload.Length>maxBytes) { dropped++; continue; }
|
||||||
|
while(records.Count>0 && payloadBytes+p.Payload.Length>maxBytes)
|
||||||
|
{ payloadBytes-=records.Dequeue().Payload.Length; dropped++; }
|
||||||
|
records.Enqueue(p with { Payload=p.Payload.ToArray() }); payloadBytes+=p.Payload.Length;
|
||||||
|
}
|
||||||
|
sequence=input.Length==0 ? 0 : input.Max(p=>p.Sequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PacketCorrelation(long Request,long Response,TimeSpan Elapsed);
|
||||||
|
public static class PacketCorrelator
|
||||||
|
{
|
||||||
|
public static PacketCorrelation[] Ping(IEnumerable<PacketRecord> packets,OpcodeMap map)
|
||||||
|
{
|
||||||
|
Dictionary<(string,uint),Queue<PacketRecord>> requests=[];
|
||||||
|
List<PacketCorrelation> results=[];
|
||||||
|
foreach(var packet in packets.OrderBy(p=>p.Time).ThenBy(p=>p.Sequence))
|
||||||
|
{
|
||||||
|
if(!packet.PayloadComplete) continue;
|
||||||
|
var name=map.Resolve(packet.GameVersion,packet.Direction,packet.Opcode);
|
||||||
|
var current=packet.GameVersion==PacketParser.AuditedClientVersion;
|
||||||
|
if(name=="UP_PingReq" && packet.Payload.Length==(current ? 0x20 : 0x1C))
|
||||||
|
{
|
||||||
|
var key=(packet.GameVersion,BinaryPrimitives.ReadUInt32LittleEndian(packet.Payload));
|
||||||
|
if(!requests.TryGetValue(key,out var queue)) requests[key]=queue=new();
|
||||||
|
queue.Enqueue(packet);
|
||||||
|
}
|
||||||
|
else if(name=="DOWN_PingRsp" && (current ? packet.Payload.Length==0x20 : packet.Payload.Length>=0x10))
|
||||||
|
{
|
||||||
|
var key=(packet.GameVersion,BinaryPrimitives.ReadUInt32LittleEndian(packet.Payload));
|
||||||
|
if(requests.TryGetValue(key,out var queue) && queue.TryDequeue(out var request))
|
||||||
|
{
|
||||||
|
var elapsed=packet.Time-request.Time;
|
||||||
|
if(elapsed>=TimeSpan.Zero && elapsed<=TimeSpan.FromMinutes(1)) results.Add(new(request.Sequence,packet.Sequence,elapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PacketCorrelation[] Actions(IEnumerable<PacketRecord> packets,OpcodeMap map)
|
||||||
|
{
|
||||||
|
Dictionary<(string,ushort),Queue<PacketRecord>> requests=[];
|
||||||
|
List<PacketCorrelation> results=[];
|
||||||
|
foreach(var packet in packets.OrderBy(p=>p.Time).ThenBy(p=>p.Sequence))
|
||||||
|
{
|
||||||
|
if(!packet.PayloadComplete || packet.GameVersion!=PacketParser.AuditedClientVersion) continue;
|
||||||
|
var name=map.Resolve(packet.GameVersion,packet.Direction,packet.Opcode);
|
||||||
|
if((name=="UP_ActionSend" && packet.Payload.Length==0x20) || (name=="UP_ActionSendPos" && packet.Payload.Length==0x18))
|
||||||
|
{
|
||||||
|
var reqId=BinaryPrimitives.ReadUInt16LittleEndian(packet.Payload.AsSpan(6));
|
||||||
|
if(reqId==0) continue;
|
||||||
|
if(!requests.TryGetValue((packet.GameVersion,reqId),out var queue)) requests[(packet.GameVersion,reqId)]=queue=new();
|
||||||
|
queue.Enqueue(packet);
|
||||||
|
}
|
||||||
|
else if(name=="DOWN_Effect" && packet.Payload.Length==0x7C && packet.Payload[0x21]<=1)
|
||||||
|
{
|
||||||
|
var reqId=BinaryPrimitives.ReadUInt16LittleEndian(packet.Payload.AsSpan(0x18));
|
||||||
|
if(reqId>0 && requests.TryGetValue((packet.GameVersion,reqId),out var queue) && queue.TryDequeue(out var request))
|
||||||
|
{
|
||||||
|
var elapsed=packet.Time-request.Time;
|
||||||
|
if(elapsed>=TimeSpan.Zero && elapsed<=TimeSpan.FromMinutes(1)) results.Add(new(request.Sequence,packet.Sequence,elapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PacketFiles
|
||||||
|
{
|
||||||
|
public static void ExportAtomic(string path,IEnumerable<PacketRecord> packets)
|
||||||
|
{
|
||||||
|
var temporary=path+"."+Guid.NewGuid().ToString("N")+".tmp";
|
||||||
|
try { Export(temporary,packets); File.Move(temporary,path,true); }
|
||||||
|
finally { if(File.Exists(temporary)) File.Delete(temporary); }
|
||||||
|
}
|
||||||
|
public static void Export(string path,IEnumerable<PacketRecord> packets)
|
||||||
|
{
|
||||||
|
using var writer = new StreamWriter(path,false,new System.Text.UTF8Encoding(false));
|
||||||
|
foreach (var packet in packets) writer.WriteLine(JsonSerializer.Serialize(packet));
|
||||||
|
}
|
||||||
|
public static PacketRecord[] Import(string path)
|
||||||
|
{
|
||||||
|
if (new FileInfo(path).Length > 32*1024*1024) throw new InvalidDataException("记录文件超过 32 MiB");
|
||||||
|
var records = new List<PacketRecord>();
|
||||||
|
foreach(var line in File.ReadLines(path))
|
||||||
|
{
|
||||||
|
if (line.Length>100000) throw new InvalidDataException("记录行过长");
|
||||||
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||||
|
var packet=JsonSerializer.Deserialize<PacketRecord>(line) ?? throw new InvalidDataException("空记录");
|
||||||
|
if (packet.Payload == null || packet.Payload.Length>65536 || packet.Opcode is <0 or >65535 || !Enum.IsDefined(packet.Direction) || string.IsNullOrWhiteSpace(packet.GameVersion))
|
||||||
|
throw new InvalidDataException("无效记录");
|
||||||
|
records.Add(packet);
|
||||||
|
if (records.Count>10000) throw new InvalidDataException("记录超过 10000 条");
|
||||||
|
}
|
||||||
|
return records.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public sealed record DungeonSnapshot(bool Valid, uint Territory, byte DungeonId, int Floor)
|
||||||
|
{
|
||||||
|
public static readonly DungeonSnapshot Empty = new(false, 0, 0, 0);
|
||||||
|
public bool SpecialFloor => Valid && (Floor % 10 == 0 || (Floor == 99 && Territory is 1108 or 1290));
|
||||||
|
public string Description => !Valid ? "不在有效深层迷宫实例中" : $"区域 {Territory} · 迷宫 {DungeonId} · {Floor} 层 · {(SpecialFloor ? "特殊层" : "普通层")}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Policies
|
||||||
|
{
|
||||||
|
public static float FiniteClamp(float value, float min, float max, float fallback = 0) =>
|
||||||
|
float.IsFinite(value) ? Math.Clamp(value, min, max) : Math.Clamp(fallback, min, max);
|
||||||
|
public static float HeightDelta(bool ready, bool dungeonOnly, DungeonSnapshot dungeon, float amount) =>
|
||||||
|
!ready || (dungeonOnly && (!dungeon.Valid || dungeon.Floor <= 0 || dungeon.SpecialFloor)) ? 0 : FiniteClamp(amount, -10, 10);
|
||||||
|
public static bool BlockTimeline(uint timeline, IReadOnlySet<uint> list, bool blockListed) => list.Count > 0 && (blockListed ? list.Contains(timeline) : !list.Contains(timeline));
|
||||||
|
public static bool IsMudra(int id) => id>=0 && NativeIds.MudraActions.Contains((uint)id);
|
||||||
|
public static uint FallFlags(uint flags) => (flags & 0b11100000000) == 0 ? flags : (flags & ~0b11100000000u) | 2u;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public readonly struct ActionRangeCheckScope : IDisposable
|
||||||
|
{
|
||||||
|
[ThreadStatic] private static bool active;
|
||||||
|
private readonly bool previous;
|
||||||
|
private ActionRangeCheckScope(bool allowed) { previous=active; active=allowed; }
|
||||||
|
public static ActionRangeCheckScope Enter(bool allowed) => new(allowed);
|
||||||
|
public static bool Active => active;
|
||||||
|
public void Dispose() => active=previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TargetRadiusPolicy
|
||||||
|
{
|
||||||
|
// One target radius is used consistently by native drawing, attack checks and gap-closer placement.
|
||||||
|
// Never add the bonus to the player's own radius (which would double the landing displacement).
|
||||||
|
public static float Adjust(GameFrame frame,nint actor,uint entityId,float original,float extra) =>
|
||||||
|
frame.Ready && actor!=0 && actor!=frame.LocalAddress && actor==frame.TargetAddress && entityId==frame.TargetEntityId && float.IsFinite(original) && original>=0
|
||||||
|
? original+Policies.FiniteClamp(extra,0,5) : original;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record TargetRadiusObservation(uint EntityId,float Original,float Effective);
|
||||||
|
|
||||||
|
public readonly struct NativeTargetRingScope : IDisposable
|
||||||
|
{
|
||||||
|
[ThreadStatic] private static bool active;
|
||||||
|
private readonly bool previous;
|
||||||
|
private NativeTargetRingScope(bool allowed) { previous=active; active=allowed; }
|
||||||
|
public static NativeTargetRingScope Enter(bool allowed) => new(allowed);
|
||||||
|
public static bool Active => active;
|
||||||
|
public void Dispose() => active=previous;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public sealed record GameFrame(bool Ready, nint LocalAddress, bool IsPvP, bool IsSlowed, DungeonSnapshot Dungeon,nint ActionManagerAddress = 0,nint TargetAddress = 0,uint TargetEntityId = 0,
|
||||||
|
nint LocalStatusManagerAddress = 0, uint LocalEntityId = 0, bool IsReplay = false)
|
||||||
|
{
|
||||||
|
public static readonly GameFrame Empty = new(false,0,false,false,DungeonSnapshot.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Producers sample thread-affine services only on Framework.Update. Native workers read values only.
|
||||||
|
public sealed class FrameStore(Func<long>? clock = null, long? maximumAge = null)
|
||||||
|
{
|
||||||
|
private sealed record Published(GameFrame Frame,long Time);
|
||||||
|
private readonly Func<long> now = clock ?? Stopwatch.GetTimestamp;
|
||||||
|
private readonly long ttl = maximumAge ?? Stopwatch.Frequency / 2;
|
||||||
|
private Published? current;
|
||||||
|
public void Publish(GameFrame frame) => Volatile.Write(ref current,new(frame,now()));
|
||||||
|
public void Clear() => Volatile.Write(ref current,null);
|
||||||
|
public GameFrame Read()
|
||||||
|
{
|
||||||
|
var value=Volatile.Read(ref current);
|
||||||
|
if(value==null) return GameFrame.Empty;
|
||||||
|
var age=now()-value.Time;
|
||||||
|
return age<0 || age>ttl ? GameFrame.Empty : value.Frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SettingsSnapshot
|
||||||
|
{
|
||||||
|
private readonly float[] values=new float[44];
|
||||||
|
public FrozenSet<uint> Timelines { get; }
|
||||||
|
public bool BlockListed { get; }
|
||||||
|
public DashFilterMode DashMode { get; }
|
||||||
|
public float KnockSeconds { get; }
|
||||||
|
public FrozenSet<uint> BlockedStatuses { get; }
|
||||||
|
public SettingsSnapshot(IReadOnlyDictionary<int,float> source,IEnumerable<uint> timelines,bool blockListed,float knockSeconds,DashFilterMode dashMode = DashFilterMode.BlockListed,
|
||||||
|
IEnumerable<uint>? blockedStatuses = null)
|
||||||
|
{
|
||||||
|
foreach(var f in Features.All) values[f.Id]=Policies.FiniteClamp(source.GetValueOrDefault(f.Id,f.Default),f.Min,f.Max,f.Default);
|
||||||
|
Timelines=timelines.Where(x=>x>0).ToFrozenSet();
|
||||||
|
BlockListed=blockListed; KnockSeconds=Policies.FiniteClamp(knockSeconds,0,5);
|
||||||
|
DashMode=dashMode;
|
||||||
|
BlockedStatuses=(blockedStatuses ?? []).Where(StatusCatalog.AuditedBlockCandidates.Contains).ToFrozenSet();
|
||||||
|
}
|
||||||
|
public float Value(int id) => (uint)id<(uint)values.Length ? values[id] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AccelerationPolicy
|
||||||
|
{
|
||||||
|
// Audited in 2026.09.15 and 2026.10.06 executables: native routine consumes and updates +0x44.
|
||||||
|
// The returned state belongs to the game and MUST NOT be restored to the value before the call.
|
||||||
|
public const string AuditedVersion="2026.10.06.0000.0000";
|
||||||
|
public const string BaselineVersion="2026.09.15.0000.0000";
|
||||||
|
public static bool IsAuditedVersion(string? version) => version is AuditedVersion or BaselineVersion;
|
||||||
|
public static bool ShouldPrime(GameFrame frame,nint owner,bool moving,float current) =>
|
||||||
|
frame.Ready && !frame.IsSlowed && frame.LocalAddress!=0 && owner==frame.LocalAddress && moving && float.IsFinite(current);
|
||||||
|
public static void Run(ref float state,bool prime,Action original)
|
||||||
|
{
|
||||||
|
if(prime) state=100;
|
||||||
|
original();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public sealed record ServiceBarStatus(bool Shown,string Text,string Tooltip);
|
||||||
|
|
||||||
|
public static class ServiceBarPresentation
|
||||||
|
{
|
||||||
|
public static ServiceBarStatus Build(GameFrame frame,ModuleState height,ModuleState mode,ModuleState display,float preset,float applied)
|
||||||
|
{
|
||||||
|
if(!frame.Ready) return new(false,"IC","Not in game or changing areas. Height offset paused.");
|
||||||
|
var enabled=height==ModuleState.Enabled;
|
||||||
|
var effective=enabled ? Policies.HeightDelta(true,mode==ModuleState.Enabled,frame.Dungeon,applied) : 0;
|
||||||
|
var heightText=height==ModuleState.Failed ? "ERR" : !enabled ? "OFF" : applied==0 ? "SET" : effective==0 ? "HOLD" : Number(effective);
|
||||||
|
var modeText=mode==ModuleState.Failed ? "ERR" : mode==ModuleState.Enabled ? "1" : "0";
|
||||||
|
var floorText=display==ModuleState.Failed ? "ERR" : display!=ModuleState.Enabled ? "OFF" : !frame.Dungeon.Valid ? "--" : $"{frame.Dungeon.Floor}{(frame.Dungeon.SpecialFloor ? "*" : "")}";
|
||||||
|
var reason=!enabled ? "Height disabled." : applied==0 ? "SET: preset ready; press Apply to activate." : effective!=0 ? "Height offset active." : "HOLD: outside a valid deep dungeon or on a special floor.";
|
||||||
|
var dungeonText=mode!=ModuleState.Enabled && display!=ModuleState.Enabled ? "Dungeon sampling disabled." : !frame.Dungeon.Valid ? "No valid deep dungeon instance." : $"Territory {frame.Dungeon.Territory}, dungeon {frame.Dungeon.DungeonId}, floor {frame.Dungeon.Floor}{(frame.Dungeon.SpecialFloor ? " (special)" : "")}.";
|
||||||
|
return new(true,$"IC Y:{heightText} D:{modeText} F:{floorText}",
|
||||||
|
$"Y = height offset (9)\nPreset {Number(preset)}, applied {Number(applied)}, effective {Number(effective)}\n{reason}\nD = dungeon-only mode (10): 1 on, 0 off\nF = floor display (11): -- outside, * special floor\nOFF = disabled; ERR = failed\n{dungeonText}\nClick to open IC settings.");
|
||||||
|
}
|
||||||
|
private static string Number(float value) => value.ToString("0.###",CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
// Called by the framework-thread module manager. One native hook can serve multiple modules.
|
||||||
|
public sealed class SharedModuleResource(Action<ResourceScope> start)
|
||||||
|
{
|
||||||
|
private ResourceScope? resource;
|
||||||
|
private int owners;
|
||||||
|
private bool cleanupFailed;
|
||||||
|
public void Acquire(ResourceScope owner)
|
||||||
|
{
|
||||||
|
if(cleanupFailed) throw new InvalidOperationException("共享 Hook 清理失败,禁止重复挂接");
|
||||||
|
if(resource==null)
|
||||||
|
{
|
||||||
|
var created=new ResourceScope();
|
||||||
|
try { start(created); resource=created; }
|
||||||
|
catch(Exception startup)
|
||||||
|
{
|
||||||
|
try { created.Dispose(); }
|
||||||
|
catch(Exception cleanup) { cleanupFailed=true; throw new AggregateException(startup,cleanup); }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owners++;
|
||||||
|
try { owner.Defer(Release); } catch { Release(); throw; }
|
||||||
|
}
|
||||||
|
private void Release()
|
||||||
|
{
|
||||||
|
if(--owners!=0) return;
|
||||||
|
var old=resource; resource=null;
|
||||||
|
try { old?.Dispose(); } catch { cleanupFailed=true; throw; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
// The audited SetStatus ABI uses a byte result/flag and a full 64-bit GameObjectId.
|
||||||
|
public delegate byte StatusSetter(nint manager, int index, ushort statusId, float remaining,
|
||||||
|
ushort param, ulong source, byte refreshFlags);
|
||||||
|
|
||||||
|
public static class StatusBlockPolicy
|
||||||
|
{
|
||||||
|
public const string ClientVersion = "2026.10.06.0000.0000";
|
||||||
|
public const string BaselineClientVersion = "2026.09.15.0000.0000";
|
||||||
|
public static bool IsAuditedVersion(string? version) => version is ClientVersion or BaselineClientVersion;
|
||||||
|
public const int ManagerOffset = 0x23B0;
|
||||||
|
public const int EntityIdOffset = 0x78;
|
||||||
|
public const uint DefaultStatusId = 1257;
|
||||||
|
|
||||||
|
public static bool IsLocal(GameFrame frame, nint manager, nint currentPlayer, uint currentEntityId) =>
|
||||||
|
frame.Ready && !frame.IsReplay && frame.LocalAddress != 0 &&
|
||||||
|
frame.LocalStatusManagerAddress == frame.LocalAddress + ManagerOffset &&
|
||||||
|
manager == frame.LocalStatusManagerAddress && currentPlayer == frame.LocalAddress &&
|
||||||
|
frame.LocalEntityId is not (0 or 0xE0000000) && currentEntityId == frame.LocalEntityId;
|
||||||
|
|
||||||
|
public static bool IsSelected(int index, ushort statusId, IReadOnlySet<uint> selected) =>
|
||||||
|
(uint)index < 60 && statusId != 0 && selected.Contains(statusId) &&
|
||||||
|
StatusCatalog.AuditedBlockCandidates.Contains(statusId);
|
||||||
|
|
||||||
|
// Never skip the setter: an empty slot must still replace its previous status and
|
||||||
|
// report native changes to the batch caller, which then refreshes the status flags.
|
||||||
|
public static byte Forward(StatusSetter original, bool block, nint manager, int index,
|
||||||
|
ushort statusId, float remaining, ushort param, ulong source, byte refreshFlags) =>
|
||||||
|
block ? original(manager, index, 0, 0, 0, 0, refreshFlags)
|
||||||
|
: original(manager, index, statusId, remaining, param, source, refreshFlags);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
// These are Status row IDs from the supplied IC sources, not action/timeline/opcode IDs.
|
||||||
|
// Kept named by purpose to prevent accidental reuse across different ID spaces.
|
||||||
|
public static class StatusCatalog
|
||||||
|
{
|
||||||
|
public static readonly FrozenSet<uint> SlowCandidates = new uint[]
|
||||||
|
{14,67,181,240,436,484,502,623,674,709,1073,1107,1114,1141,1147,1259,1344,1394,1595,1790,1796,1935,2099,2158,2391,2551,2662,2731,3167,3284,3472,3473,3548,3943,3948,4334,4341}.ToFrozenSet();
|
||||||
|
public static readonly FrozenSet<uint> LegacySlipperyCandidates = new uint[] {142,149,604,905,911,1579,1580}.ToFrozenSet();
|
||||||
|
public static readonly FrozenSet<uint> ForcedMarch = new uint[] {1257,1681,3629,3719,3737}.ToFrozenSet();
|
||||||
|
public static readonly FrozenSet<uint> LegacyDirectionCandidates = new uint[]
|
||||||
|
{1293,1294,1295,1296,1958,1959,1960,1961,2161,2162,2163,2164,2381,2382,2383,2384,2538,2539,2540,2541,3698,3699,3700,3701,3715,3716,3717,3718}.ToFrozenSet();
|
||||||
|
public static readonly FrozenSet<uint> Confusion = new uint[] {1422,2936,3694,3909}.ToFrozenSet();
|
||||||
|
public static readonly FrozenSet<uint> RejectedLegacyBlockIds = new uint[] {142,149,2538,2539,2540,2541}.ToFrozenSet();
|
||||||
|
// Status sheet names and descriptions checked against the user's 2026.09.15 data.
|
||||||
|
public static readonly FrozenSet<uint> AuditedSlow = SlowCandidates.Concat(new uint[] {5044,5175,5356}).ToFrozenSet();
|
||||||
|
public static readonly FrozenSet<uint> AuditedBlockCandidates = LegacySlipperyCandidates.Concat(ForcedMarch)
|
||||||
|
.Concat(LegacyDirectionCandidates).Concat(Confusion).Except(RejectedLegacyBlockIds).ToFrozenSet();
|
||||||
|
public static IEnumerable<(string Group,uint Id)> Entries()
|
||||||
|
{
|
||||||
|
foreach(var (name,set) in new[] {("Slow",SlowCandidates),("Slippery",LegacySlipperyCandidates),("ForcedMarch",ForcedMarch),("DirectionCommands",LegacyDirectionCandidates),("Confusion",Confusion)})
|
||||||
|
foreach(var id in set.Order()) yield return (name,id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
|
||||||
|
namespace ICRebuild.Core;
|
||||||
|
|
||||||
|
public static class ValidationCatalog
|
||||||
|
{
|
||||||
|
public const string ClientVersion="2026.10.06.0000.0000";
|
||||||
|
public const string BaselineClientVersion="2026.09.15.0000.0000";
|
||||||
|
public static bool IsAuditedVersion(string? version) => version is ClientVersion or BaselineClientVersion;
|
||||||
|
// Explicit user confirmation, 2026-10-11. This is not inferred from compilation or hook startup.
|
||||||
|
public static readonly FrozenSet<int> UserConfirmed = new[] {1,2,3,9,10,11,13,14,15,16,17,19,27,28,29}.ToFrozenSet();
|
||||||
|
public static void Apply(ModuleManager modules,string clientVersion)
|
||||||
|
{
|
||||||
|
if(!IsAuditedVersion(clientVersion)) return;
|
||||||
|
foreach(var id in UserConfirmed)
|
||||||
|
if(modules.Status.TryGetValue(id,out var state) && state.HasImplementation) state.Adaptation=Adaptation.Verified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StatusPresentation
|
||||||
|
{
|
||||||
|
public static string Summary(ModuleStatus state)
|
||||||
|
{
|
||||||
|
var validation=state.Adaptation switch
|
||||||
|
{
|
||||||
|
Adaptation.Verified=>"已实测有效",
|
||||||
|
Adaptation.KnownBroken=>"已知问题",
|
||||||
|
_ when !state.HasImplementation=>"暂不可用",
|
||||||
|
_ when state.Feature.Id==14=>"待复测",
|
||||||
|
_=>"待实测"
|
||||||
|
};
|
||||||
|
var running=state.State switch {ModuleState.Enabled=>"已开启",ModuleState.Failed=>"运行异常",_=>"已关闭"};
|
||||||
|
return $"{validation} · {running}";
|
||||||
|
}
|
||||||
|
public static bool ShowDetail(ModuleStatus state) => !state.HasImplementation || state.State==ModuleState.Failed || state.Adaptation==Adaptation.KnownBroken;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
|
<AssemblyName>ICRebuild</AssemblyName>
|
||||||
|
<RootNamespace>ICRebuild</RootNamespace>
|
||||||
|
<Version>0.1.16</Version>
|
||||||
|
<AssemblyVersion>0.1.16.0</AssemblyVersion>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<HooksDevPath Condition="'$(HooksDevPath)' == ''">$(APPDATA)\XIVLauncher\addon\Hooks\dev</HooksDevPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Tests/**/*.cs" />
|
||||||
|
<Compile Remove="Tools/**/*.cs" />
|
||||||
|
<Reference Include="Dalamud;Dalamud.Bindings.ImGui;FFXIVClientStructs;InteropGenerator.Runtime;Lumina;Lumina.Excel;Newtonsoft.Json">
|
||||||
|
<HintPath>$(HooksDevPath)\%(Identity).dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<None Update="ICRebuild.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." />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"Author": "AinaSnow",
|
||||||
|
"Name": "IC Rebuild",
|
||||||
|
"InternalName": "ICRebuild",
|
||||||
|
"AssemblyVersion": "0.1.16.0",
|
||||||
|
"DalamudApiLevel": 15,
|
||||||
|
"ApplicableVersion": "any",
|
||||||
|
"LoadRequiredState": 0,
|
||||||
|
"Punchline": "IC 独立恢复工程,模块默认关闭,使用 /icr 打开。",
|
||||||
|
"Description": "国际服开发版本。保留移动、战斗、深层迷宫、坐标与网络分析功能。各模块显示独立适配状态;尚未完成游戏内验证。",
|
||||||
|
"Tags": ["Utility", "Development"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# IC 收发包管理器与版本维护开发计划
|
||||||
|
|
||||||
|
日期:2026 年 10 月 13 日。基线:IC Rebuild 0.1.16,国际服客户端 `2026.10.06.0000.0000`(向后兼容 `2026.09.15.0000.0000`)。
|
||||||
|
|
||||||
|
目标是依据用户提供的原版源码、作者说明和现有重建成果,完善统一收发包管理器,并建立游戏更新后的检测、适配和验收流程。本文件是后续开发补充计划;任务勾选表示对应交付物完成,不代表已经通过游戏内验收。
|
||||||
|
|
||||||
|
执行方式遵循本目录 AGENTS.md:功能优先,避免过度防御开发、过度测试和零碎发布。
|
||||||
|
|
||||||
|
2026-10-13 / 0.1.16 客户端补丁维护进展:国际服维护更新至 `2026.10.06.0000.0000`。备份旧可执行文件后完成新二进制全量扫描,全部 23 处特征码在新二进制中 100% 匹配成功,修正 `ForcedWalkHook` 签名笔误。反汇编核实 M4 主动发送通道(`ReceiverSendPacket 0x1CFD120`、`ZoneClientSendPacket 0x1D03020`、`ZoneUp 0x1D72B00`)、模块 36 收包(`ZoneDown 0x1D72680`)、模块 8 状态屏蔽(`StatusSetter 0x8A6A30`)及模块 2 最大加速度内部偏移完全一致。完成双版本向后兼容抽象及本地 `opcodes.json` 补丁配置(PingReq 137 / PingRsp 338)。详见 `diagnostics/2026-10-13-patch-2026.10.06-adaptation.md`。
|
||||||
|
|
||||||
|
2026-10-12 / 0.1.15 阶段 3 进展:完成 M0 统一版本适配与诊断(`Core/AdaptationManifest.cs`)及 M4 主动发送与请求响应事务(`Core/ActivePacketSender.cs`、`Runtime/NetworkManager.cs`)。弃用恒返回 `1` 的旧包装 `0x4810E0`,直接绑定 `ReceiverSendPacket` → `ZoneClient.SendPacket` → `ZoneUp`,保留真实 `byte` 返回码;实现 `ActiveSendScope` 来源标注(`PluginActiveSend`)、已审计业务包编码器、主线程限速队列、会话/区域门控、网络回调重入拦截与请求响应事务状态机。详见 `diagnostics/2026-10-12-stage3-m0-m4.md`。
|
||||||
|
|
||||||
|
## 范围与当前进度
|
||||||
|
|
||||||
|
原首批范围保持为 1—11、13—20、27—29、36—37,共 24 项。12 移动咏唱独立研究;38—43 保留为网络基础之后的业务扩展;21—26、30—35 继续排除。主动发送管理单列里程碑,不把它或业务自动化隐含加入原首批 36、37 的验收条件。
|
||||||
|
|
||||||
|
以本次核对到的 0.1.11 文件为基线:
|
||||||
|
|
||||||
|
| 部分 | 已有成果 | 尚待完成 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 首批功能 | 用户确认 1、2、3、9、10、11、13、14、15、16、17、19、27、28、29 有效 | 4、5、6、7、18、20 的效果确认及各模块组合、切图、卸载验收 |
|
||||||
|
| 8 状态屏蔽 | 已核对一种网络列表为 30 个 12 字节条目,并找到消费入口及条件性 ID 转换 | 转换后处理、本地对象门控、其他列表路径、标志更新与实测 |
|
||||||
|
| 36 完整采集 | 首轮导出 891 条:上行 534、下行 357,均有完整载荷;有过滤、暂停、缓冲、导出 | 性能、反复启停、切图及卸载验收 |
|
||||||
|
| 37 协议分析 | 该样本全部 Opcode 已具名,679 条可解析已核实字段,8 组 Ping 已配对 | 212 条已命名记录尚无解析器;已支持包也未必解释全部字段 |
|
||||||
|
| 主动发送 | 原 NetRe 提供入口和接口雏形 | 新工程尚无经过完整验证的通用发送封装与事务管理 |
|
||||||
|
| 自动维护 | 已有部分版本门控、本地与在线映射校验 | 统一适配清单、更新差异检查、模块失效报告与恢复流程 |
|
||||||
|
|
||||||
|
本份样本全部具名不代表所有协议已识别。66 项离线测试通过不替代原生调用和游戏效果验收。后续实施时先读取最新 STATUS.md、VERIFICATION.md,保留主线程在本计划之后取得的成果。
|
||||||
|
|
||||||
|
## 原始资料如何复用
|
||||||
|
|
||||||
|
| 资料 | 可复用的设计或证据 | 实施要求 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| IC/NetManager.cs | NetRe、发送入口 Hook、SendPackt、IGamePacket、PreSend/PostSend 委托声明 | 恢复接口意图,补齐实际订阅调用;逐项核对发送对象头、长度、参数和有效期 |
|
||||||
|
| IC/OnlieOpcode.cs | 按客户端版本加载在线映射,并结合本地识别 | 沿用版本隔离,补充来源、证据、冲突和失效状态 |
|
||||||
|
| IC/UpPacketStructs.cs 与 DownPacketStructs.cs | 消息名称与历史字段布局 | 以当前原生读写和样本修正,不直接按旧 Size 分配或复制 |
|
||||||
|
| IC/新建 文本文档.txt | 收发时间轴、交互流程分析、后续业务扩展;结构体和 Opcode 维护提示 | 先证明流程与结果,再实现自动化;维护成本作为模块设计约束 |
|
||||||
|
| 两份 I-Ching-GL DLL | 已可靠读取的类型、接口、依赖差异 | 受保护的方法体不作为已还原算法的依据 |
|
||||||
|
| IC.Rebuild 当前实现 | 完整采集、只读副本、过滤、解析、版本映射、离线回放与资源清理 | 在现有代码上逐步整理,保持已有功能行为和已验证直接 Hook |
|
||||||
|
|
||||||
|
原 NetManager.cs 中的发送前后委托尚未形成完整事件调用流程,泛型发送也不能证明所有上下行结构体都已具备正确发送封装。原作者列出的扩展方向需要各自的业务状态、请求响应和结果验证。
|
||||||
|
|
||||||
|
## 管理器职责与数据流
|
||||||
|
|
||||||
|
建议形成以下协作关系,具体类名可结合现有工程调整:
|
||||||
|
|
||||||
|
| 部分 | 主要职责 | 验收关注点 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 版本适配层 | 根据客户端、API、依赖和签名证据选择适配配置 | 未匹配配置不能静默使用旧指针、偏移或 Opcode |
|
||||||
|
| 原生接入层 | 管理收发 Hook、原函数调用和资源释放 | 每条原生路径的参数、返回值、调用次数与有效期明确 |
|
||||||
|
| 消息分发层 | 向观察者发布自持有数据,管理订阅和取消 | 单个订阅异常不破坏原生调用;业务订阅不受日志筛选影响 |
|
||||||
|
| 协议层 | 按版本、方向、消息类型解析与编码 | 载荷、IPC 头、原生发送对象分开;未知字段不补猜测值 |
|
||||||
|
| 发送调度层 | 排队、过期、取消、限速、连接与场景检查 | 在核实的线程和会话中调用;切区后不发送过期请求 |
|
||||||
|
| 请求响应层 | 用有依据的标识关联响应并报告结果 | 发送入口成功不等于服务端确认;无依据时不强行关联 |
|
||||||
|
| 记录与诊断层 | 时间轴、来源、过滤、失败原因、统计及导出 | 区分游戏自身发送、插件发送、原始载荷与已还原字段 |
|
||||||
|
|
||||||
|
收包流程:原生接入取得有效数据 → 在有效期内复制 → 标注版本和处理阶段 → 分发给订阅者与记录器 → 按需解析。日志的方向与 Opcode 过滤仅控制记录和显示,业务订阅使用自己的条件。
|
||||||
|
|
||||||
|
发包流程:业务请求 → 版本与场景检查 → 对应消息编码器 → 构造原生发送对象 → 在合适线程调度 → 记录调用结果 → 在有协议依据时等待响应确认。
|
||||||
|
|
||||||
|
收包观察与收包修改分开。8 如需在状态 ID 转换后处理,应使用对应原生阶段的专用适配;不能让普通日志订阅者取得可长期保存、任意修改的原生指针。
|
||||||
|
|
||||||
|
## 实施里程碑
|
||||||
|
|
||||||
|
### M0 统一版本适配与诊断
|
||||||
|
|
||||||
|
- [x] 为现有原生模块和协议建立适配清单,列明已核对版本、依赖、签名、字段与证据。
|
||||||
|
- [x] 对接现有模块管理器,区分缺实现、版本不符、签名未匹配、多重匹配、运行失败和待验收。
|
||||||
|
- [x] 明确未知版本的行为:未证明兼容的原生模块保持不可用;配置、文件导出和离线分析尽可能保留。
|
||||||
|
- [x] 增加只读检查结果,显示哪个模块受哪项变化影响。
|
||||||
|
|
||||||
|
验收:模拟版本不符、签名缺失和多重匹配,受影响模块无法错误启用;其他独立模块不因单项失败被误标为失效。签名唯一匹配仍需函数语义核验。
|
||||||
|
|
||||||
|
### M1 整理统一收包与发送观察接口
|
||||||
|
|
||||||
|
- [x] 将 NetworkCapture 的原生接入、协议读取和日志写入职责分开,保持当前完整采集结果。
|
||||||
|
- [x] 建立可取消的收包、发送前观察和发送后观察订阅,规定顺序、线程及异常处理。
|
||||||
|
- [x] 分离日志过滤与业务订阅,避免筛选窗口记录导致后续模块漏收消息。
|
||||||
|
- [x] 为消息标注方向、客户端版本、记录来源、原始或已还原阶段、完整性及失败原因。
|
||||||
|
- [x] 初始仅开放只读观察和取消订阅。没有开放取消或替换原生包;该能力若后续需要,另行核实返回语义、有效期和重入。
|
||||||
|
|
||||||
|
实现与接口约束见 diagnostics/2026-10-11-network-manager.md。891 条离线回放逐条载荷一致,不等于新管理器已完成游戏内验收。
|
||||||
|
|
||||||
|
验收:原 891 条样本回放不回退;切换日志过滤不影响业务订阅;反复订阅、取消和卸载不残留回调;订阅异常不改变原函数应有行为。
|
||||||
|
|
||||||
|
### M2 完成状态处理依据并推进编号 8
|
||||||
|
|
||||||
|
- [x] 核对单列表、双列表、其他列表变体及录像回放的调用差异(0.1.14 进一步核实 `DOWN_StatusEffectList` 30×12B 与 `DOWN_EffectResult` 4×16B 状态项均汇入 `StatusManager.SetStatus` `0x8A6BF0`)。
|
||||||
|
- [x] 明确状态 ID 转换发生的阶段,选择转换后且仍有边界依据的处理入口。0.1.12 采用 SetStatus,槽位 0—59,不访问原始网络列表。
|
||||||
|
- [ ] 用当前有效快照确认本地角色及其 StatusManager,覆盖对象消失、切区、重登和指针复用(离线已验证,待游戏内验收)。
|
||||||
|
- [x] 核对状态条目更新与相关标志重新计算,明确屏蔽的实际效果范围。保留原 setter 的通知、返回值和批量/单次刷新;只保证经过该入口的本地后续更新,实际效果仍待下项验收。
|
||||||
|
- [ ] 从一个可复现的目标状态开始实测,再逐项扩展已审计名单。
|
||||||
|
|
||||||
|
验收:目标状态效果有记录;未选状态与其他角色不受影响;关闭后恢复正常处理;切图与卸载无残留。此模块不以主动发包能力完成为前提,也不把本地状态处理表述成服务器状态已删除。
|
||||||
|
|
||||||
|
### M3 扩展协议覆盖并收尾首批验收
|
||||||
|
|
||||||
|
- [x] 按消息类型整理 212 条尚未解析记录的清单,优先处理首批功能及后续选定业务实际需要的类型。
|
||||||
|
- [x] 补动作结果、状态和事件消息的已核实字段,注明字段来源与未解释部分(0.1.14 完成 `capture-018.jsonl` 全部 891 条解析及 27 组动作请求响应配对)。
|
||||||
|
- [x] 需要新增样本时列明缺失场景、采集动作和期待消息,避免反复要求泛化抓包。
|
||||||
|
- [ ] 完成 4、5、6、7、18、20 的效果验收,以及首批模块组合、切图、退出登录和卸载检查。
|
||||||
|
|
||||||
|
验收:支持类型对正常、截断、错误计数和版本不符有明确结果;未知包保留原始记录。建立可重现流程的证据,不以本份样本的未知数为零宣称协议全部完成。
|
||||||
|
|
||||||
|
### M4 恢复主动发送与请求响应管理
|
||||||
|
|
||||||
|
- [x] 对照原 NetRe 核实当前发送入口、真实参数及返回含义(0.1.15 弃用恒返回 1 的 `0x4810E0`,改用 `ReceiverSendPacket` `0x1CFD140` → `ZoneClient.SendPacket` `0x1D03040` → `ZoneUp` `0x1D718C0`)。
|
||||||
|
- [x] 为首个选定消息编写独立编码器,明确业务载荷、原生发送对象、长度和缓冲区所有权(`UP_ActionSend`、`UP_ClientTrigger`、`UP_EventStart`、`UP_EventFinish*` 8 档、`UP_EventAction*` 8 档)。
|
||||||
|
- [x] 优先从已核实的游戏接口调用方式接入,统一版本检查、诊断和发送来源标记(`PacketOrigin.PluginActiveSend`)。
|
||||||
|
- [x] 加入有界队列、取消、会话标识、过期时间及调度线程约束,避免从接收回调中直接递归发送。
|
||||||
|
- [x] 区分排队、已调用原生发送、原生拒绝、收到确认、超时和取消;取消排队不等于撤销已发送请求。
|
||||||
|
- [x] 仅在消息关联依据和重复操作语义明确时加入重试,不统一重放所有捕获包(当前不自动重发任何原生调用)。
|
||||||
|
|
||||||
|
验收:编码与原生对象构造有边界测试;实际场景能确认预期结果;断线、切区、重复操作、取消和卸载行为明确。发送接口不能以单个 bool 冒充整个交互流程成功。
|
||||||
|
|
||||||
|
M4 是单列的恢复里程碑。其接口设计可以与 M1 并行,但真实调用依赖 M0 和相应消息契约核实;它不阻塞原首批记录、解析和状态处理的独立交付。
|
||||||
|
|
||||||
|
### M5 进入选定业务扩展
|
||||||
|
|
||||||
|
38—43 继续作为候选,不自动全部开工。每次选定一项后,再明确业务状态、请求响应链、成功依据、超时、取消和异常恢复。作者提到过某项能力,只说明存在参考方向,不等于当前客户端与服务器已经验证可用。
|
||||||
|
|
||||||
|
## 游戏更新维护流程
|
||||||
|
|
||||||
|
目标是更新后自动发现问题、辅助定位、按模块适配。不能承诺客户端每次更新后无需核验即可恢复全部功能。
|
||||||
|
|
||||||
|
### 每版本保存的适配信息
|
||||||
|
|
||||||
|
| 信息 | 保存内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 客户端 | 区服、完整版本、核对过的可执行文件标识或哈希 |
|
||||||
|
| 运行依赖 | Dalamud API 与程序集版本、FFXIVClientStructs 版本、关键委托及字段布局 |
|
||||||
|
| 原生入口 | 签名、匹配数量、函数参数、用途、支持的模块和核对记录 |
|
||||||
|
| 协议 | 版本、方向、Opcode、名称、长度规则、字段和 ID 转换阶段 |
|
||||||
|
| 映射来源 | 远程、本地文件、原生核对或样本证据,以及冲突处理结果 |
|
||||||
|
| 验证状态 | 构建、离线回归、具体游戏场景、限制及结果日期 |
|
||||||
|
| 发布与回退 | 包版本、校验值、配置迁移说明和前一版本备份 |
|
||||||
|
|
||||||
|
现有 CurrentProtocolEvidence 中按版本补充且不覆盖显式映射的原则继续保留。发现显式映射与新证据矛盾时记录冲突,不能静默改名后伪装没有差异。
|
||||||
|
|
||||||
|
### 更新后的执行顺序
|
||||||
|
|
||||||
|
1. 记录新的游戏与依赖版本,保留上一份适配清单、发行包和必要配置备份。
|
||||||
|
2. 检查插件能否在新 API 中加载;若 API 不兼容,先由外部构建和程序集检查定位问题,不能依赖插件内面板完成诊断。
|
||||||
|
3. 只读检查签名是否缺失或多重匹配、委托及结构是否变化,生成受影响模块清单。
|
||||||
|
4. 检查新版本 Opcode 映射。没有匹配映射时不套用旧名称;采集本身也须通过对应原生布局检查后才可启用。
|
||||||
|
5. 对长度异常、计数异常、解析失败率增加及 ID 转换变化做差异分析,按需要安排少量针对性样本。
|
||||||
|
6. 修复对应模块,跑相关离线回归,并记录新的原生和协议依据。
|
||||||
|
7. 在游戏内按模块验证,再检查组合、切图和卸载;逐项恢复适配状态。
|
||||||
|
8. 构建发布并核对部署文件,保留变更说明与待验收项。
|
||||||
|
|
||||||
|
游戏版本没变但 Dalamud 或 FFXIVClientStructs 更新时,也要检查相关依赖差异。版本门控不能只看 API 主版本。
|
||||||
|
|
||||||
|
### 可自动完成与必须核验的边界
|
||||||
|
|
||||||
|
| 可自动完成 | 不能仅凭自动检查断言 |
|
||||||
|
| --- | --- |
|
||||||
|
| 版本与哈希差异、编译结果、程序集元数据差异 | 原生函数的行为仍与旧版相同 |
|
||||||
|
| 签名匹配数量和候选定位 | 匹配唯一就代表参数和语义正确 |
|
||||||
|
| 映射冲突、长度、数量和解析失败统计 | 包名称及未知字段的业务含义正确 |
|
||||||
|
| 已有脱敏样本回放、错误输入与生命周期测试 | 新客户端现场表现与服务器结果正确 |
|
||||||
|
| 打包校验、备份和变更报告 | 旧 DLL 能在新游戏上正常使用 |
|
||||||
|
|
||||||
|
发生更新不应重写已验证的所有功能。继续保留便于维护的直接 Hook;通过依赖关系只处理受到变化影响的部分。
|
||||||
|
|
||||||
|
## 开发交付与主线程衔接
|
||||||
|
|
||||||
|
- 当前按用户最新要求推进收发包管理器:M1 已交付只读实现,下一步沿 M4 核对实际发送封装与首个消息契约,按具体需求再接入主动发送。编号 8 继续待实测,M3 围绕具体功能安排解析,M5 按后续选择进入。不得为了先建成全部基础框架而阻塞可用功能交付。
|
||||||
|
- 不预设新版本号或日历工期。每个里程碑以证据、实现、相关测试和游戏验收状态作为交付依据。
|
||||||
|
- 诊断报告保存必要的长度、计数、类型和结果;共享测试使用合成或脱敏数据,不复制无关聊天、角色身份和完整会话数据。
|
||||||
|
- 每次交付更新实施状态和验证记录,明确已实现、待实测、受限及未实现内容。
|
||||||
|
- 相关改动合并发布,必要检查通过后无新问题不重复验证;现有样本够用时不要求用户重新采样。进度优先报告功能结果,详细测试数量留在验证记录。
|
||||||
|
- 该文件独立保存旁支方案;现有 IC/PLAN.md、源码、配置和部署未因创建本文件而改变。
|
||||||
|
|
||||||
|
相关依据:IC/PLAN.md、IC.Rebuild/STATUS.md、IC.Rebuild/VERIFICATION.md,以及 diagnostics 中的双 DLL 对比、完整采集、capture-018 回放、Opcode 核对和状态处理记录。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration><packageSources><clear /></packageSources></configuration>
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
modules.Register(37,_ => { });
|
||||||
|
ValidationCatalog.Apply(modules,gameVersion);
|
||||||
|
Try(()=>
|
||||||
|
{
|
||||||
|
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,"本地缓存"));
|
||||||
|
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 [文件名]。" });
|
||||||
|
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(); }
|
||||||
|
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;
|
||||||
|
default: Open(); notice="用法:/icr on|off 编号;stop;pos X Y Z;face 弧度;scale 倍率"; 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);
|
||||||
|
DrawAdaptation();
|
||||||
|
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==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}");
|
||||||
|
}
|
||||||
|
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 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;
|
||||||
|
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(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<Lumina.Excel.Sheets.Action>().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(()=>opcodes.Load(File.ReadAllText(DataPath("opcodes.json")),gameVersion,"本地文件"));
|
||||||
|
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}" : "未就绪(需角色在场且非过图/占卜回放)")}");
|
||||||
|
ImGui.InputInt("测试技能 ID (UP_ActionSend)",ref manualSendActionId);
|
||||||
|
ImGui.InputInt("请求序号 RequestId",ref manualSendRequestId);
|
||||||
|
if(ImGui.Button("加入技能发送队列 (当前目标)")) Try(()=>
|
||||||
|
{
|
||||||
|
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.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 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 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();
|
||||||
|
modules.Dispose(); network.Dispose(); game.Frames.Clear(); cancellation.Cancel(); http.Dispose(); cancellation.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# IC Rebuild
|
||||||
|
|
||||||
|
国际服 Dalamud API 15 开发版本。实现范围按 `../IC/PLAN.md`,保留原始资料,不引入已排除的雷达、选敌、旗标、钓鱼和显示伪装功能。
|
||||||
|
|
||||||
|
当前版本为 0.1.13。用户已确认 1、2、3、9、10、11、13、14、15、16、17、19、27、28、29 实测有效,界面在本次客户端版本下显示“已实测有效”。编号 8 已接入转换后的本地状态更新过滤,待实测;36 已迁入统一收发观察管理器,原完整样本回放通过,新版现场验收仍待补充。完整状态见 `STATUS.md`。
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
使用 .NET 10 SDK,运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./build.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
默认引用 `%APPDATA%\XIVLauncher\addon\Hooks\dev` 的运行库;可通过 `-HooksDevPath` 指定其他 API 15 SDK 目录。项目无 NuGet 包依赖,不编译整个 Dalamud、FFXIVClientStructs 或 ECommons。脚本执行 Release 构建与离线测试,通过后按清单版本生成发行 ZIP,当前为 `dist/ICRebuild-0.1.13.0-dev.zip`。
|
||||||
|
|
||||||
|
本次构建基线:.NET SDK 10.0.401、Dalamud 15.0.3.6、FFXIVClientStructs 文件版本 7.56.2.9136。运行中的客户端版本由游戏目录 `ffxivgame.ver` 读取;无法读取时显示 unknown,禁止套用 Opcode 映射。
|
||||||
|
|
||||||
|
## 加载和使用
|
||||||
|
|
||||||
|
用户指定的开发部署目录:`C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild`。后续开发包通过构建与检查后,解压更新到该目录,并通知用户重新加载插件。用户负责游戏内卸载与重新加载;若 DLL 被占用导致覆盖失败,保留现有文件并说明情况,不结束游戏进程。该路径位于工作区之外,写入时按工具要求申请对应权限。
|
||||||
|
|
||||||
|
将发行包解压到独立目录,在 Dalamud 开发插件设置中添加其中的 `ICRebuild.dll`。DLL 和 JSON 保持在同一目录。项目未自动安装到游戏目录。
|
||||||
|
|
||||||
|
- `/icr`:打开设置。
|
||||||
|
- `/icr on 11`:启用迷宫状态显示;其他首批编号同理。
|
||||||
|
- `/icr off 11`:关闭模块。
|
||||||
|
- `/icr stop`:关闭全部模块。
|
||||||
|
- `/icr pos X Y Z`:先启用 27,再应用坐标。
|
||||||
|
- `/icr face 弧度`:先启用 28,再应用面向。
|
||||||
|
- `/icr scale 倍率`:先启用 29,再应用当前目标缩放。
|
||||||
|
- `/icr export [文件名]`:导出当前记录,默认 packets.jsonl。
|
||||||
|
|
||||||
|
每次加载所有模块默认关闭。参数和启用意愿会保存,但不会自动恢复 Hook。签名或地址不可用时显示具体原因。未确认功能显示“待实测”;“已实测有效”仅记录用户在国际服 2026.09.15.0000.0000 的反馈,不替代组合和生命周期验收。
|
||||||
|
|
||||||
|
0.1.12 新增编号 8:只过滤本地角色后续收到的已选状态更新,默认仅选 1257(Forced March),可展开名单逐项选择已审计候选;空名单不屏蔽。界面显示拦截次数及最近 ID。先开启再触发目标状态;开启前已有状态须等下一次更新。关闭后正常接收,不立即恢复已过滤状态;本地处理不代表服务端状态已删除。录像回放暂停过滤。依据及首轮流程见 `diagnostics/2026-10-11-status-block-setter.md`。
|
||||||
|
|
||||||
|
9 勾选后先调整“预设高度偏移”,点击“生效”才应用;按钮随后变为“回到 0”,再次点击取消偏移,在后续位置同步时反映。修改预设不会改变已应用值;关闭模块或重载会清零已应用值,保留预设。16 的 Timeline 名单管理和手动编号列表默认折叠,展开后编辑。
|
||||||
|
|
||||||
|
0.1.6 在游戏服务栏增加“IC Rebuild · 高度与迷宫”:显示 9 的关闭、待应用、实际偏移或暂停,10 的专用模式开关,以及 11 的楼层/特殊层。悬停查看预设、已应用及实际值,点击打开设置。切区和未登录时隐藏,卸载移除;可在 Dalamud 服务栏设置中调整位置或隐藏。
|
||||||
|
|
||||||
|
0.1.7 针对国际服游戏字库改用短 ASCII 显示,例如 `IC Y:-7 D:1 F:23`:Y 为高度,D 为专用模式(1 开、0 关),F 为层数。OFF 为关闭,SET 为等待应用,HOLD 为暂停,ERR 为异常,F:-- 为非迷宫,层数后 * 表示特殊层。原生悬浮提示也只使用英文;插件窗口保留中文。
|
||||||
|
|
||||||
|
所有参数滑块(含击退持续时间)支持双击打开精确输入框,回车或点击确定保存,取消不提交输入值;超出范围时按原上下限保存。Ctrl+单击直接输入也保留。9 的精确输入仍只改预设,须另点生效。
|
||||||
|
|
||||||
|
先启用 9 才能启用 10;先启用 17 才能启用 18。关闭依赖时一并关闭依赖它的模块。16 首次加载会按当前动作表自动加入全部 79 个玩家位移技能(58 个不同 Timeline),默认仅屏蔽名单内。可查看技能名、逐项取消或一键补全;也可选择全部本地技能位移、仅放行名单内。两种名单模式下,空名单不屏蔽。共享 Timeline 的技能会联动。
|
||||||
|
|
||||||
|
坐标、面向是一次性操作,不在关闭模块时把角色移回原地。缩放记录已修改对象的原值,关闭模块时仅对仍在同一区域、ID 和地址一致、缩放仍等于本插件设置值的对象尝试恢复。对象消失、切区或其他来源改变了缩放时不写回过期数据。
|
||||||
|
|
||||||
|
14 仅对当前选中目标的原生半径增加一次偏移,自身和其他对象不增加;圈显示、距离检查和突进路径读取相同的扩大半径。14 不再额外增加技能距离。预期突进落在扩大圈外侧,16461 的实际落点已由用户确认正常;界面提供半径变化和命中统计。16 提供最近本地位移调用、Timeline 和拦截次数。
|
||||||
|
|
||||||
|
## 网络记录与解析
|
||||||
|
|
||||||
|
0.1.13 将两个 Zone 通道 Hook 交给统一 NetworkManager,编号 36 作为日志订阅者使用它;不依赖整个 ECommons 程序集。上行入口参考其 `Legacy.GameNetwork`,下行沿用已核对的 ZoneReceive。记录方向、Opcode、时间、版本、可获得的目标和不含 IPC 头的游戏载荷,保留原函数调用,不主动发送业务包;不挂接聊天、登录或录像回放通道。
|
||||||
|
|
||||||
|
管理器支持独立收包、发送前与发送后观察、按 Opcode 订阅及取消订阅;日志的暂停/过滤不会阻断其他消费者。开启 36 时暂停仅停止写入日志,面板的收发观察计数继续增加;关闭 36 且没有其他消费者时释放 Hook。面板显示订阅异常和原生发送返回码,返回码不代表服务端确认。新记录注明 Zone 原始载荷与观察阶段,旧记录保留“阶段未知”;状态 ID 不在此层还原。接口约束和验证见 `diagnostics/2026-10-11-network-manager.md`。
|
||||||
|
|
||||||
|
旧兼容事件没有长度参数,0.1.8 改用原生消息对象的长度契约:上行载荷在 data+0x20,长度为消息长度减 0x10;下行在接收成功后从持有对象取得缓冲区和消息长度,排除 0x10 字节 IPC 头。适配限定国际服 2026.09.15.0000.0000,单载荷上限 64 KiB,通过 SafeMemory 复制。完整读取才标 `PayloadComplete=true`;异常长度或读取失败在仍可获取 Opcode 时退回元数据并保存 `CaptureIssue`,无法读取头部则只计异常。旧元数据文件保持不完整标记。上行记录表示进入发送入口,不证明服务端已收到。
|
||||||
|
|
||||||
|
编号 37 支持当前版本的本地或在线 Opcode 映射、离线 JSONL 导入导出、方向及 Opcode 过滤,以及部分历史结构的有界解析。解析器覆盖 Ping、移动位置、事件开始和效果结果计数;事件动作和完成包因原资料布局矛盾暂保留原始数据。当前版本普通移动与 Ping 的长度及已解释字段已有原生和现场样本依据;其他历史结构仍需核验。
|
||||||
|
|
||||||
|
记录位于 Dalamud 为插件提供的配置目录,界面显示具体路径。离线导入保留时间、版本和序号;缓冲区最多 2048 条且载荷总量最多 8 MiB,超限移除最旧记录并计入丢弃统计。在线更新只在用户点击后执行,HTTPS 文件限制 4 MiB;映射校验失败不会替换现有映射。版本不同或未知时不使用旧映射。对完整且能匹配版本的 Ping 请求、响应,使用回显值建立关联。
|
||||||
|
|
||||||
|
网络记录启用时,默认每 5 秒将最近 2048 条保存到本会话 capture-*.jsonl,可在面板关闭。这是滚动快照,崩溃前最后 5 秒仍可能未保存。保存使用后台临时文件与原子替换,不在采集回调写盘。0.1.9 在当前版本映射缺失且不冲突时本地补充 DOWN_PingRsp=338,并在来源中注明;不修改缓存文件,不覆盖显式映射。RTT 由同版本请求/响应的回显值匹配后计算,不使用响应偏移 4 的未解释字段。
|
||||||
|
|
||||||
|
0.1.8 首轮验证已取得 capture-018.jsonl:891 条完整记录,序号连续。0.1.9 修正普通移动/Ping 长度后,该文件可解析 512 条并匹配 8 组 Ping。热重载后可启用 37,文件名填 capture-018.jsonl,点击导入离线记录查看;不必为此次解析修正重新抓包。仍有 167 条、6 种未知 Opcode 待核对。详情见 diagnostics/2026-10-11-capture-018-review.md。
|
||||||
|
|
||||||
|
0.1.10 进一步核实 671=UpdateHate、558=UpdateHater、697=EffectResultBasic,补充缺失映射与有限字段解析。同一样本可解析 664 条、有名称 876 条,未知剩 15 条/3 种,仍有 212 条已命名包待实现解析。不新增雷达或选敌功能,8 状态屏蔽仍未启用。详见 diagnostics/2026-10-11-opcode-671-697-558.md。
|
||||||
|
|
||||||
|
0.1.11 补充 614=JobGauge、788=UpdateClassInfo、214=FirstAttack 的已核实字段;原 891 条样本全部具名、有限解析 679 条,另 212 条未接入解析。已有 ActorGauge=8 不覆盖。状态列表已核对到 30×12 字节网络块,但分派前存在条件性的 ID 转换,8 继续等待转换后入口与本地对象门控核验。详见 diagnostics/2026-10-11-opcode-remaining-and-status.md。
|
||||||
|
|
||||||
|
## 验证边界
|
||||||
|
|
||||||
|
离线测试检查模块生命周期、失败清理、依赖启停、迷宫规则、参数边界、名单语义、Opcode 隔离、包边界、并发缓冲、导入导出和 Ping 关联,不调用原生游戏函数。
|
||||||
|
|
||||||
|
尚待游戏内验证:签名唯一性和函数参数、作用对象、两个移动路径的数据布局、模块组合效果、实际位置与动作结果、切图退出及卸载。原生访问错误无法用普通托管异常处理保证恢复;应按 `STATUS.md` 的单模块流程验证。
|
||||||
|
|
||||||
|
`THIRD_PARTY_NOTICES.md` 记录网络适配器参考来源及许可证。
|
||||||
|
|
||||||
|
|
||||||
|
## 状态与标识审计
|
||||||
|
|
||||||
|
0.1.3 将 Status ID、Permission 编号、Action ID、Timeline 和冷却组分开命名。减速保护补入 5044、5175、5356;旧状态屏蔽候选中六个不符分组的条目已分离。7 是本地强制移动入口拦截,不删除状态或保证解除所有控制;第五参数按原生浮点类型声明。8 仍未启用。详见 diagnostics/2026-10-11-id-audit.md。
|
||||||
|
|
||||||
|
|
||||||
|
0.1.4 的原生圈与距离路径依据见 diagnostics/2026-10-11-radius-native-ring.md;0.1.5 的落点语义修正及界面改动见 diagnostics/2026-10-11-radius-landing-and-ui.md。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
|
||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
|
||||||
|
public sealed unsafe class CombatModules(GameContext game, Configuration config, ModuleManager modules)
|
||||||
|
{
|
||||||
|
private delegate long Backswing(long a1);
|
||||||
|
private delegate void Dash(nint controller, byte moveType, nint target, nint origin, float facing, nint data);
|
||||||
|
private delegate nint SyncRecast(nint manager, int recastGroup, uint actionId, float maximum);
|
||||||
|
private delegate uint CastUpdate(nint data, uint id, float total, float start);
|
||||||
|
private delegate nint TargetRingUpdate(nint renderer,ulong targetId);
|
||||||
|
private static readonly HashSet<uint> RangeSkip = [34675,3573,2262,29513];
|
||||||
|
private long dashCalls,dashBlocks;
|
||||||
|
private DashObservation? lastDash;
|
||||||
|
public long DashCalls => Interlocked.Read(ref dashCalls);
|
||||||
|
public long DashBlocks => Interlocked.Read(ref dashBlocks);
|
||||||
|
public DashObservation? LastDash => Volatile.Read(ref lastDash);
|
||||||
|
public MovementSkill[] MovementSkills { get; private set; } = [];
|
||||||
|
private SharedModuleResource? sharedRange;
|
||||||
|
private long radiusPrechecks,radiusFinalChecks,radiusBonuses;
|
||||||
|
public long RadiusPrechecks => Interlocked.Read(ref radiusPrechecks);
|
||||||
|
public long RadiusFinalChecks => Interlocked.Read(ref radiusFinalChecks);
|
||||||
|
public long RadiusBonuses => Interlocked.Read(ref radiusBonuses);
|
||||||
|
private long nativeRingCalls,nativeRingBonuses;
|
||||||
|
public long NativeRingCalls => Interlocked.Read(ref nativeRingCalls);
|
||||||
|
public long NativeRingBonuses => Interlocked.Read(ref nativeRingBonuses);
|
||||||
|
private long targetRadiusChanges,selfRadiusSkips;
|
||||||
|
public long TargetRadiusChanges => Interlocked.Read(ref targetRadiusChanges);
|
||||||
|
public long SelfRadiusSkips => Interlocked.Read(ref selfRadiusSkips);
|
||||||
|
private TargetRadiusObservation? lastTargetRadius;
|
||||||
|
public TargetRadiusObservation? LastTargetRadius => Volatile.Read(ref lastTargetRadius);
|
||||||
|
|
||||||
|
private void AcquireRange(ResourceScope owner)
|
||||||
|
{
|
||||||
|
sharedRange ??= new SharedModuleResource(scope=>
|
||||||
|
{
|
||||||
|
var actions=game.Data.GetExcelSheet<Lumina.Excel.Sheets.Action>()
|
||||||
|
.ToDictionary(a=>a.RowId,a=>(Moves:a.AffectsPosition,Hostile:a.CanTargetHostile));
|
||||||
|
Hook<ActionManager.Delegates.GetActionRange>? range=null;
|
||||||
|
range=game.HookAddress<ActionManager.Delegates.GetActionRange>(scope,(nint)ActionManager.MemberFunctionPointers.GetActionRange,id=>
|
||||||
|
{
|
||||||
|
var original=range!.Original(id);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(!game.Ready || !float.IsFinite(original) || original<0 || !actions.TryGetValue(id,out var action)) return original;
|
||||||
|
var result=original;
|
||||||
|
if(modules.IsEnabled(13) && !RangeSkip.Contains(id))
|
||||||
|
{
|
||||||
|
var extra=config.Value(13);
|
||||||
|
result+=id==29066 || (action.Moves && action.Hostile) ? extra : Math.Min(extra,3);
|
||||||
|
}
|
||||||
|
// Module 14 modifies the target radius itself. Never also add it to skill range.
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch(Exception ex) { game.Fault(14,ex); return original; }
|
||||||
|
});
|
||||||
|
range.Enable();
|
||||||
|
});
|
||||||
|
sharedRange.Acquire(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadMovementCatalog()
|
||||||
|
{
|
||||||
|
MovementSkills=MovementSkillCatalog.Select(game.Data.GetExcelSheet<Lumina.Excel.Sheets.Action>()
|
||||||
|
.Where(a=>a.IsPlayerAction && a.AffectsPosition)
|
||||||
|
.Select(a=>new MovementSkill(a.RowId,a.Name.ToString(),a.AnimationEnd.RowId,a.IsPlayerAction,a.AffectsPosition,a.IsPvP)));
|
||||||
|
}
|
||||||
|
public int AddAllMovementSkills()
|
||||||
|
{
|
||||||
|
if(MovementSkills.Length==0) LoadMovementCatalog();
|
||||||
|
var before=config.BlockedTimelines.Count;
|
||||||
|
foreach(var skill in MovementSkills.Where(MovementSkillCatalog.Mappable)) config.BlockedTimelines.Add(skill.Timeline);
|
||||||
|
config.DashMode=DashFilterMode.BlockListed;
|
||||||
|
config.MovementListGameVersion=game.GameVersion;
|
||||||
|
config.Normalize();
|
||||||
|
return config.BlockedTimelines.Count-before;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register()
|
||||||
|
{
|
||||||
|
modules.Register(13,AcquireRange);
|
||||||
|
modules.Register(14, scope =>
|
||||||
|
{
|
||||||
|
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("原生目标圈入口仅核对了 2026.09.15 客户端");
|
||||||
|
Hook<ActionManager.Delegates.GetActionInRangeOrLoS>? rangeCheck = null;
|
||||||
|
Hook<ActionManager.Delegates.IsActionTargetInRange>? precheck = null;
|
||||||
|
Hook<TargetRingUpdate>? ringUpdate=null;
|
||||||
|
Hook<FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject.Delegates.GetRadius>? radius=null;
|
||||||
|
rangeCheck=game.HookAddress<ActionManager.Delegates.GetActionInRangeOrLoS>(scope,(nint)ActionManager.MemberFunctionPointers.GetActionInRangeOrLoS,(id,source,target)=>
|
||||||
|
{
|
||||||
|
var frame=game.Frames.Read();
|
||||||
|
var local=frame.Ready && (nint)source==frame.LocalAddress && source!=null && target!=null && source!=target;
|
||||||
|
if(local) Interlocked.Increment(ref radiusFinalChecks);
|
||||||
|
using var rangeScope=ActionRangeCheckScope.Enter(local);
|
||||||
|
return rangeCheck!.Original(id,source,target);
|
||||||
|
});
|
||||||
|
precheck=game.HookAddress<ActionManager.Delegates.IsActionTargetInRange>(scope,(nint)ActionManager.MemberFunctionPointers.IsActionTargetInRange,(manager,type,id)=>
|
||||||
|
{
|
||||||
|
var frame=game.Frames.Read();
|
||||||
|
var local=frame.Ready && manager!=null && (nint)manager==frame.ActionManagerAddress && type==ActionType.Action;
|
||||||
|
if(local) Interlocked.Increment(ref radiusPrechecks);
|
||||||
|
using var rangeScope=ActionRangeCheckScope.Enter(local);
|
||||||
|
return precheck!.Original(manager,type,id);
|
||||||
|
});
|
||||||
|
radius=game.Hook<FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject.Delegates.GetRadius>(scope,Signatures.ActorRadius,(actor,adjust)=>
|
||||||
|
{
|
||||||
|
var original=radius!.Original(actor,adjust);
|
||||||
|
var frame=game.Frames.Read();
|
||||||
|
if((nint)actor==frame.LocalAddress) Interlocked.Increment(ref selfRadiusSkips);
|
||||||
|
var result=TargetRadiusPolicy.Adjust(frame,(nint)actor,actor==null ? 0 : actor->EntityId,original,config.Value(14));
|
||||||
|
if(result!=original && float.IsFinite(result))
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref targetRadiusChanges);
|
||||||
|
var old=Volatile.Read(ref lastTargetRadius);
|
||||||
|
if(old==null || old.EntityId!=frame.TargetEntityId || old.Original!=original || old.Effective!=result)
|
||||||
|
Volatile.Write(ref lastTargetRadius,new(frame.TargetEntityId,original,result));
|
||||||
|
if(NativeTargetRingScope.Active) Interlocked.Increment(ref nativeRingBonuses);
|
||||||
|
if(ActionRangeCheckScope.Active) Interlocked.Increment(ref radiusBonuses);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
ringUpdate=game.Hook<TargetRingUpdate>(scope,Signatures.NativeTargetRingUpdate,(renderer,targetId)=>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref nativeRingCalls);
|
||||||
|
using var ringScope=NativeTargetRingScope.Enter(game.Ready && modules.IsEnabled(14));
|
||||||
|
return ringUpdate!.Original(renderer,targetId);
|
||||||
|
});
|
||||||
|
rangeCheck.Enable(); precheck.Enable(); radius.Enable(); ringUpdate.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(15, scope =>
|
||||||
|
{
|
||||||
|
Hook<Backswing>? hook = null;
|
||||||
|
hook = game.Hook<Backswing>(scope,Signatures.NoBackswing,a1 => game.Ready ? a1 : hook!.Original(a1));
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(16, scope =>
|
||||||
|
{
|
||||||
|
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("技能位移结构仅核对了 2026.09.15 客户端");
|
||||||
|
Hook<Dash>? hook = null;
|
||||||
|
hook = game.Hook<Dash>(scope,Signatures.ActionMovementStart,(controller,moveType,target,origin,facing,data) =>
|
||||||
|
{
|
||||||
|
var frame=game.Frames.Read();
|
||||||
|
if(controller!=0 && data!=0 && frame.Ready)
|
||||||
|
{
|
||||||
|
var owner=*(nint*)(controller+0x3E0);
|
||||||
|
if(owner==frame.LocalAddress && owner!=0)
|
||||||
|
{
|
||||||
|
var settings=config.Published;
|
||||||
|
var bytes=new ReadOnlySpan<byte>((void*)data,4);
|
||||||
|
var timeline=DashPolicy.Timeline(bytes);
|
||||||
|
var block=DashPolicy.Block(frame.Ready,owner,frame.LocalAddress,timeline,settings.DashMode,settings.Timelines);
|
||||||
|
Interlocked.Increment(ref dashCalls);
|
||||||
|
Volatile.Write(ref lastDash,new(timeline,bytes[2],bytes[3],moveType,block));
|
||||||
|
if(block) { Interlocked.Increment(ref dashBlocks); return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hook!.Original(controller,moveType,target,origin,facing,data);
|
||||||
|
});
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(17, StartRecast);
|
||||||
|
modules.Register(18, _ => { },17);
|
||||||
|
modules.Register(19, StartCast);
|
||||||
|
modules.Register(20, _ => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartRecast(ResourceScope scope)
|
||||||
|
{
|
||||||
|
Hook<ActionManager.Delegates.GetAdjustedRecastTime>? read = null;
|
||||||
|
Hook<SyncRecast>? sync = null;
|
||||||
|
read = game.HookAddress<ActionManager.Delegates.GetAdjustedRecastTime>(scope,(nint)ActionManager.MemberFunctionPointers.GetAdjustedRecastTime,(type,action,extra) =>
|
||||||
|
{
|
||||||
|
var value = read!.Original(type,action,extra);
|
||||||
|
if (!game.Ready || type != ActionType.Action || value <= 0) return value;
|
||||||
|
if (modules.IsEnabled(18) && Policies.IsMudra((int)action)) return 0;
|
||||||
|
return Math.Max(0,value - (int)(config.Value(17)*1000));
|
||||||
|
});
|
||||||
|
sync = game.Hook<SyncRecast>(scope,"40 ?? 48 ?? ?? ?? 0f 29 74 24 ?? 41 ?? ?? 0f ?? ?? e8 ?? ?? ?? ??",(manager,recastGroup,actionId,maximum) =>
|
||||||
|
{
|
||||||
|
if (game.Ready && recastGroup == NativeIds.GlobalCooldownGroup && float.IsFinite(maximum) && maximum > 0)
|
||||||
|
maximum = modules.IsEnabled(18) && NativeIds.MudraActions.Contains(actionId) ? 0 : Math.Max(0,maximum-config.Value(17));
|
||||||
|
return sync!.Original(manager,recastGroup,actionId,maximum);
|
||||||
|
});
|
||||||
|
read.Enable(); sync.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartCast(ResourceScope scope)
|
||||||
|
{
|
||||||
|
Hook<ActionManager.Delegates.GetAdjustedCastTime>? read = null;
|
||||||
|
Hook<CastUpdate>? update = null;
|
||||||
|
var current = game.StaticAddress("F3 44 0F 2C C0 BA ?? ?? ?? ?? 48 8B CB E8 ?? ?? ?? ?? F3 44 0F 10 1D",0x12);
|
||||||
|
if (current == 0) throw new InvalidOperationException("咏唱同步地址未解析");
|
||||||
|
read = game.HookAddress<ActionManager.Delegates.GetAdjustedCastTime>(scope,(nint)ActionManager.MemberFunctionPointers.GetAdjustedCastTime,(type,id,apply,proc) =>
|
||||||
|
{
|
||||||
|
var value = read!.Original(type,id,apply,proc);
|
||||||
|
return game.Ready && type == ActionType.Action ? Math.Max(0,value-(int)(config.Value(19)*1000)) : value;
|
||||||
|
});
|
||||||
|
update = game.Hook<CastUpdate>(scope,"48 89 5C 24 ?? 57 48 83 EC ?? 48 8B F9 0F 29 74 24 ?? 0F B6 49",(data,id,total,start) =>
|
||||||
|
{
|
||||||
|
if (game.Ready && data != 0 && *(uint*)((byte*)data+4) == id && float.IsFinite(total))
|
||||||
|
{ total = Math.Max(0,total-config.Value(19)); *(float*)current = total; }
|
||||||
|
return update!.Original(data,id,total,start);
|
||||||
|
});
|
||||||
|
read.Enable(); update.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
if (!modules.IsEnabled(20) || !game.Ready || !game.IsPvP) return;
|
||||||
|
var manager = ActionManager.Instance();
|
||||||
|
if (manager != null && float.IsFinite(manager->AnimationLock) && manager->AnimationLock > config.Value(20))
|
||||||
|
manager->AnimationLock = config.Value(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
|
||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
|
||||||
|
public sealed unsafe class CoordinateTools(GameContext game, ModuleManager modules)
|
||||||
|
{
|
||||||
|
private delegate void Scale(nint actor, float scale);
|
||||||
|
private Scale? scale;
|
||||||
|
private readonly Dictionary<ulong,(nint Address,uint Territory,float Original,float Applied)> resized=[];
|
||||||
|
public void Register()
|
||||||
|
{
|
||||||
|
modules.Register(27, _ => { }); modules.Register(28, _ => { });
|
||||||
|
modules.Register(29, scope =>
|
||||||
|
{
|
||||||
|
scale = Marshal.GetDelegateForFunctionPointer<Scale>(game.Scan("F3 0F 11 89 ?? ?? ?? ?? E9 ?? ?? ?? ?? CC CC CC CC CC CC CC CC CC CC CC CC"));
|
||||||
|
scope.Defer(() => { try { RestoreAll(); } finally { scale=null; } });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private GameObject* Player(int feature)
|
||||||
|
{
|
||||||
|
if (!modules.IsEnabled(feature)) throw new InvalidOperationException($"先启用模块 {feature}");
|
||||||
|
if (!game.Ready) throw new InvalidOperationException("角色尚未就绪");
|
||||||
|
return (GameObject*)game.LocalAddress;
|
||||||
|
}
|
||||||
|
public void Position(Vector3 position)
|
||||||
|
{
|
||||||
|
if (!float.IsFinite(position.X) || !float.IsFinite(position.Y) || !float.IsFinite(position.Z) ||
|
||||||
|
Math.Abs(position.X)>100000 || Math.Abs(position.Y)>100000 || Math.Abs(position.Z)>100000)
|
||||||
|
throw new ArgumentException("坐标必须是 ±100000 范围内的有限数");
|
||||||
|
Player(27)->SetPosition(position.X,position.Y,position.Z);
|
||||||
|
}
|
||||||
|
public void Facing(float rotation)
|
||||||
|
{
|
||||||
|
if (!float.IsFinite(rotation)) throw new ArgumentException("面向必须是有限数");
|
||||||
|
Player(28)->SetRotation(MathF.IEEERemainder(rotation,2*MathF.PI));
|
||||||
|
}
|
||||||
|
public void SetScale(float value)
|
||||||
|
{
|
||||||
|
if (!modules.IsEnabled(29) || scale == null) throw new InvalidOperationException("先启用模块 29");
|
||||||
|
if (!game.Ready || game.Targets.Target == null) throw new InvalidOperationException("请选中有效目标");
|
||||||
|
if (!float.IsFinite(value) || value < 0.1f || value > 5) throw new ArgumentException("缩放范围为 0.1—5");
|
||||||
|
var target=game.Targets.Target;
|
||||||
|
var obj=(GameObject*)target.Address;
|
||||||
|
var original=obj->Scale;
|
||||||
|
if(!float.IsFinite(original) || original<=0) throw new InvalidOperationException("目标原始缩放无效");
|
||||||
|
if(resized.TryGetValue(target.GameObjectId,out var old) && old.Address==target.Address && old.Territory==game.Client.TerritoryType)
|
||||||
|
original=old.Original;
|
||||||
|
if(resized.Count>=256 && !resized.ContainsKey(target.GameObjectId)) throw new InvalidOperationException("先恢复已有缩放对象");
|
||||||
|
scale(target.Address,value);
|
||||||
|
resized[target.GameObjectId]=(target.Address,game.Client.TerritoryType,original,value);
|
||||||
|
}
|
||||||
|
public void RestoreAll()
|
||||||
|
{
|
||||||
|
if(scale!=null && game.Ready)
|
||||||
|
foreach(var obj in game.Objects)
|
||||||
|
if(resized.TryGetValue(obj.GameObjectId,out var old) && old.Address==obj.Address && old.Territory==game.Client.TerritoryType &&
|
||||||
|
Math.Abs(((GameObject*)obj.Address)->Scale-old.Applied)<0.0001f)
|
||||||
|
scale(obj.Address,old.Original);
|
||||||
|
resized.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Event;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
|
||||||
|
public sealed unsafe class GameContext(IObjectTable objects, IClientState client, ICondition condition,
|
||||||
|
IDataManager data, ISigScanner scanner, IGameInteropProvider interop, ITargetManager targets)
|
||||||
|
{
|
||||||
|
public IObjectTable Objects { get; } = objects;
|
||||||
|
public IClientState Client { get; } = client;
|
||||||
|
public ICondition Conditions { get; } = condition;
|
||||||
|
public IDataManager Data { get; } = data;
|
||||||
|
public ITargetManager Targets { get; } = targets;
|
||||||
|
public FrameStore Frames { get; } = new();
|
||||||
|
public string GameVersion { get; } = ReadGameVersion();
|
||||||
|
private readonly ConcurrentQueue<(int Id,string Error)> faults = new();
|
||||||
|
public void Fault(int id,Exception error) { if(faults.Count<32) faults.Enqueue((id,error.Message)); }
|
||||||
|
public void DrainFaults(ModuleManager modules)
|
||||||
|
{
|
||||||
|
while(faults.TryDequeue(out var fault))
|
||||||
|
{
|
||||||
|
modules.Disable(fault.Id);
|
||||||
|
modules.Status[fault.Id].State=ModuleState.Failed;
|
||||||
|
modules.Status[fault.Id].Detail=$"运行错误,已停用:{fault.Error}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool Ready => Frames.Read().Ready;
|
||||||
|
public nint LocalAddress => Frames.Read().LocalAddress;
|
||||||
|
public bool IsPvP => Frames.Read().IsPvP;
|
||||||
|
public bool IsSlowed => Frames.Read().IsSlowed;
|
||||||
|
|
||||||
|
public DungeonSnapshot ReadDungeon() => Frames.Read().Dungeon;
|
||||||
|
|
||||||
|
// Called exclusively from Framework.Update, never from a native hook or worker thread.
|
||||||
|
public void Refresh(bool sampleDungeon)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var player=Objects.LocalPlayer;
|
||||||
|
if(!Client.IsLoggedIn || player==null || Conditions[ConditionFlag.BetweenAreas] || Conditions[ConditionFlag.BetweenAreas51])
|
||||||
|
{ Frames.Clear(); return; }
|
||||||
|
var dungeon=sampleDungeon ? SampleDungeon() : DungeonSnapshot.Empty;
|
||||||
|
var target=Targets.Target;
|
||||||
|
Frames.Publish(new(true,player.Address,Client.IsPvP,player.StatusList.Any(s=>StatusCatalog.AuditedSlow.Contains(s.StatusId)),dungeon,
|
||||||
|
(nint)FFXIVClientStructs.FFXIV.Client.Game.ActionManager.Instance(),target?.Address ?? 0,target?.EntityId ?? 0,
|
||||||
|
(nint)(&((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)player.Address)->StatusManager),
|
||||||
|
player.EntityId,Conditions[ConditionFlag.DutyRecorderPlayback]));
|
||||||
|
}
|
||||||
|
catch { Frames.Clear(); throw; }
|
||||||
|
}
|
||||||
|
private DungeonSnapshot SampleDungeon()
|
||||||
|
{
|
||||||
|
if (!Conditions[ConditionFlag.InDeepDungeon]) return DungeonSnapshot.Empty;
|
||||||
|
var framework = EventFramework.Instance();
|
||||||
|
if (framework == null) return DungeonSnapshot.Empty;
|
||||||
|
var dd = framework->GetInstanceContentDeepDungeon();
|
||||||
|
return dd == null || dd->Floor == 0 ? DungeonSnapshot.Empty : new(true, Client.TerritoryType, dd->DeepDungeonId, dd->Floor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hook<T> Hook<T>(ResourceScope scope, string signature, T detour) where T : Delegate
|
||||||
|
{
|
||||||
|
if (!scanner.TryScanText(signature, out var address) || address == 0)
|
||||||
|
throw new InvalidOperationException("签名未匹配,此模块未适配当前客户端。");
|
||||||
|
return scope.Own(interop.HookFromAddress(address, detour));
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint Scan(string signature)
|
||||||
|
{
|
||||||
|
if (!scanner.TryScanText(signature, out var address) || address == 0)
|
||||||
|
throw new InvalidOperationException("签名未匹配。");
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
public nint StaticAddress(string signature, int offset) => scanner.GetStaticAddressFromSig(signature, offset);
|
||||||
|
public Hook<T> HookAddress<T>(ResourceScope scope,nint address,T detour) where T:Delegate
|
||||||
|
{
|
||||||
|
if(address==0) throw new InvalidOperationException("原生函数地址未解析");
|
||||||
|
return scope.Own(interop.HookFromAddress(address,detour));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendSession ReadSendSession()
|
||||||
|
{
|
||||||
|
var frame = Frames.Read();
|
||||||
|
return frame.Ready && !frame.IsReplay
|
||||||
|
? new(true, Client.TerritoryType, frame.LocalAddress, frame.LocalEntityId)
|
||||||
|
: SendSession.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdaptationReport EvaluateAdaptation(ModuleManager modules)
|
||||||
|
{
|
||||||
|
var offsetsValid =
|
||||||
|
System.Runtime.InteropServices.Marshal.OffsetOf<FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara>(
|
||||||
|
nameof(FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara.StatusManager)).ToInt32() == StatusBlockPolicy.ManagerOffset &&
|
||||||
|
System.Runtime.InteropServices.Marshal.OffsetOf<FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject>(
|
||||||
|
nameof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject.EntityId)).ToInt32() == StatusBlockPolicy.EntityIdOffset;
|
||||||
|
return AdaptationManifest.Evaluate(modules, GameVersion,
|
||||||
|
sig => scanner.TryScanText(sig, out var addr) && addr != 0 ? 1 : 0,
|
||||||
|
offsetsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ReadGameVersion()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(Environment.ProcessPath);
|
||||||
|
var file = Path.Combine(dir ?? "", "ffxivgame.ver");
|
||||||
|
var version = File.Exists(file) ? File.ReadAllText(file).Trim() : "unknown";
|
||||||
|
return version.Length is > 0 and < 128 ? version : "unknown";
|
||||||
|
}
|
||||||
|
catch { return "unknown"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
using Dalamud.Hooking;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
|
||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
|
||||||
|
public sealed unsafe class MovementModules(GameContext game, Configuration config, ModuleManager modules)
|
||||||
|
{
|
||||||
|
private delegate nint Move(nuint actor, nint data, uint arg);
|
||||||
|
private delegate float Speed(long actor);
|
||||||
|
private delegate void Acceleration(long actor);
|
||||||
|
private delegate long Permission(long a1, uint a2, int a3, int a4);
|
||||||
|
private delegate long Knock(long actor, float angle, float distance, float seconds, byte a5, long a6);
|
||||||
|
private delegate nint Fall(nuint a1, uint flags);
|
||||||
|
private delegate long FallCheck(long a1, long a2, long a3);
|
||||||
|
private delegate nint Forced(nint actor, float x, float y, float z, float movementParameter, nint options);
|
||||||
|
[ThreadStatic] private static HashSet<nint>? adjusted;
|
||||||
|
public HeightActivation Height { get; } = new();
|
||||||
|
public float EffectiveHeight => !modules.IsEnabled(9) ? 0 : Policies.HeightDelta(game.Ready,modules.IsEnabled(10),game.ReadDungeon(),Height.Applied);
|
||||||
|
|
||||||
|
public void Register()
|
||||||
|
{
|
||||||
|
modules.Register(11, _ => { });
|
||||||
|
modules.Register(10, _ => { }, 9);
|
||||||
|
modules.Register(9, StartHeight);
|
||||||
|
modules.Register(1, scope =>
|
||||||
|
{
|
||||||
|
Hook<Speed>? hook = null;
|
||||||
|
hook = game.Hook<Speed>(scope, Signatures.SpeedAddress, actor =>
|
||||||
|
{
|
||||||
|
var original = hook!.Original(actor);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!game.Ready || game.IsSlowed) return original;
|
||||||
|
return float.IsFinite(original) ? Math.Max(0, original + config.Value(1)) : original;
|
||||||
|
}
|
||||||
|
catch(Exception ex) { game.Fault(1,ex); return original; }
|
||||||
|
});
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(2, scope =>
|
||||||
|
{
|
||||||
|
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("最大加速度仅完成 2026.09.15 客户端的字段核对。");
|
||||||
|
Hook<Acceleration>? hook = null;
|
||||||
|
hook = game.Hook<Acceleration>(scope, "40 ?? 48 ?? ?? ?? 80 79 ?? ?? 48 ?? ?? 0f 84 ?? ?? ?? ?? 48 89 7c 24 ?? 48 ?? ?? ??", actor =>
|
||||||
|
{
|
||||||
|
var frame=game.Frames.Read();
|
||||||
|
if(actor==0 || !frame.Ready || *(byte*)((nint)actor+0x3C)==0) { hook!.Original(actor); return; }
|
||||||
|
var controller=*(nint*)((nint)actor+0x20);
|
||||||
|
var owner=controller==0 ? 0 : *(nint*)(controller+0x3E0);
|
||||||
|
ref var field=ref *(float*)((nint)actor+0x44);
|
||||||
|
var prime=AccelerationPolicy.ShouldPrime(frame,owner,true,field);
|
||||||
|
AccelerationPolicy.Run(ref field,prime,()=>hook!.Original(actor));
|
||||||
|
});
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(3, scope =>
|
||||||
|
{
|
||||||
|
Hook<Permission>? hook = null;
|
||||||
|
hook = game.Hook<Permission>(scope, Signatures.MovePermissionAddress, (a1,a2,a3,a4) =>
|
||||||
|
game.Ready && NativeIds.MovementPermissionChecks.Contains(a2) ? 1 : hook!.Original(a1,a2,a3,a4));
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(4, scope =>
|
||||||
|
{
|
||||||
|
Hook<Knock>? hook = null;
|
||||||
|
hook = game.Hook<Knock>(scope, Signatures.AntiKnockAddress, (actor,angle,distance,seconds,a5,a6) =>
|
||||||
|
{
|
||||||
|
if (actor != 0 && actor == game.LocalAddress)
|
||||||
|
{ distance = config.Value(4); seconds = config.Published.KnockSeconds; }
|
||||||
|
return hook!.Original(actor,angle,distance,seconds,a5,a6);
|
||||||
|
});
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(5, scope =>
|
||||||
|
{
|
||||||
|
Hook<Fall>? hook = null;
|
||||||
|
hook = game.Hook<Fall>(scope, Signatures.OnSendFlag, (a1,flags) => hook!.Original(a1,game.Ready ? Policies.FallFlags(flags) : flags));
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(6, scope =>
|
||||||
|
{
|
||||||
|
Hook<FallCheck>? hook = null;
|
||||||
|
hook = game.Hook<FallCheck>(scope, Signatures.FallCheckPatch, (a1,a2,a3) => game.Ready ? -1 : hook!.Original(a1,a2,a3));
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
modules.Register(7, scope =>
|
||||||
|
{
|
||||||
|
if(game.GameVersion!=AccelerationPolicy.AuditedVersion) throw new InvalidOperationException("强制移动入口参数仅核对了 2026.09.15 客户端");
|
||||||
|
Hook<Forced>? hook = null;
|
||||||
|
hook = game.Hook<Forced>(scope, Signatures.FuckBewithAction, (actor,x,y,z,movementParameter,options) =>
|
||||||
|
{
|
||||||
|
// This intercepts a forced-coordinate movement path; it does not remove a status.
|
||||||
|
if(actor!=0 && actor==game.LocalAddress) return 0;
|
||||||
|
return hook!.Original(actor,x,y,z,movementParameter,options);
|
||||||
|
});
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartHeight(ResourceScope scope)
|
||||||
|
{
|
||||||
|
Height.Reset();
|
||||||
|
Hook<Move>? normal = null, combat = null;
|
||||||
|
normal = game.Hook<Move>(scope, Signatures.OnSendNormalMove, (actor,data,arg) => ApplyHeight(normal!.Original,actor,data,arg,false));
|
||||||
|
combat = game.Hook<Move>(scope, Signatures.OnSendCombatMove, (actor,data,arg) => ApplyHeight(combat!.Original,actor,data,arg,true));
|
||||||
|
scope.Defer(Height.Reset);
|
||||||
|
normal.Enable(); combat.Enable();
|
||||||
|
}
|
||||||
|
private nint ApplyHeight(Move original, nuint actor, nint data, uint arg, bool combat)
|
||||||
|
{
|
||||||
|
float delta;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
delta=EffectiveHeight;
|
||||||
|
}
|
||||||
|
catch(Exception ex) { game.Fault(9,ex); return original(actor,data,arg); }
|
||||||
|
if (data == 0 || delta == 0) return original(actor,data,arg);
|
||||||
|
adjusted ??= [];
|
||||||
|
if (!adjusted.Add(data)) return original(actor,data,arg);
|
||||||
|
var values = (float*)data;
|
||||||
|
var first = combat ? 4 : 3;
|
||||||
|
float old = values[first], old2 = combat ? values[7] : 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!float.IsFinite(old) || (combat && !float.IsFinite(old2))) return original(actor,data,arg);
|
||||||
|
values[first] = old + delta;
|
||||||
|
if (combat) values[7] = old2 + delta;
|
||||||
|
try { return original(actor,data,arg); }
|
||||||
|
finally { values[first] = old; if (combat) values[7] = old2; }
|
||||||
|
}
|
||||||
|
finally { adjusted.Remove(data); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using ICRebuild.Core;
|
||||||
|
|
||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
|
||||||
|
// Feature 36 owns subscriptions; native hooks may also serve independent consumers.
|
||||||
|
public sealed class NetworkCapture(NetworkManager network,ModuleManager modules,PacketBuffer buffer)
|
||||||
|
{
|
||||||
|
private readonly PacketRecorder recorder=new(buffer);
|
||||||
|
public long Errors => network.Pipeline.ReadErrors;
|
||||||
|
public long CompleteUp => recorder.CompleteUp;
|
||||||
|
public long CompleteDown => recorder.CompleteDown;
|
||||||
|
public long MetadataOnly => recorder.MetadataOnly;
|
||||||
|
public string LastIssue => network.Pipeline.LastIssue;
|
||||||
|
public void Register() => modules.Register(36,scope =>
|
||||||
|
{
|
||||||
|
network.Observe(scope,NetworkPhase.Received,recorder.Record);
|
||||||
|
network.Observe(scope,NetworkPhase.BeforeSend,recorder.Record);
|
||||||
|
scope.Defer(()=>buffer.Paused=true);
|
||||||
|
buffer.Paused=false;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Dalamud;
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
|
||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
|
||||||
|
// Shared Zone hooks; acquire/release from module lifecycle code, never from callbacks.
|
||||||
|
public sealed unsafe class NetworkManager : IDisposable
|
||||||
|
{
|
||||||
|
public const string UpSignature="48 89 5C 24 ?? 48 89 74 24 ?? 4C 89 64 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 70";
|
||||||
|
// Stack-save offset 18 is intentional: wildcarding it also matches chat receive.
|
||||||
|
public const string DownSignature="48 89 5C 24 18 57 48 83 EC 20 48 83 B9 88 00 00 00 00 48 8B FA 48 8B D9";
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
private delegate byte ZoneUp(nint connection,nint data,nint arg,byte flag);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
private delegate byte ZoneReceive(nint connection,nint holder);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
private delegate byte ReceiverSendPacket(nint receiver,nint packet,uint a3,uint a4);
|
||||||
|
private readonly SharedModuleResource hooks;
|
||||||
|
private readonly GameContext game;
|
||||||
|
private readonly string version;
|
||||||
|
private readonly PacketMemoryReader memory=SafeMemory.ReadBytes;
|
||||||
|
private ReceiverSendPacket? receiverSend;
|
||||||
|
private bool disposed;
|
||||||
|
public NetworkPipeline Pipeline { get; }
|
||||||
|
public ActivePacketDispatcher ActiveSender { get; }
|
||||||
|
public bool CanSendNative => receiverSend != null;
|
||||||
|
|
||||||
|
public NetworkManager(GameContext game,string version)
|
||||||
|
{
|
||||||
|
this.game=game; this.version=version;
|
||||||
|
Pipeline=new(version); ActiveSender=new(version); hooks=new(Start);
|
||||||
|
}
|
||||||
|
public void Observe(ResourceScope owner,NetworkPhase phase,Action<NetworkObservation> callback,int? opcode=null)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(disposed,this);
|
||||||
|
var subscription=new ResourceScope();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hooks.Acquire(subscription);
|
||||||
|
subscription.Own(Pipeline.Subscribe(phase,callback,opcode));
|
||||||
|
owner.Own(subscription);
|
||||||
|
}
|
||||||
|
catch { subscription.Dispose(); throw; }
|
||||||
|
}
|
||||||
|
public SendTransactionSnapshot EnqueueSend(EncodedGamePacket packet)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(disposed,this);
|
||||||
|
if(receiverSend==null)
|
||||||
|
throw new InvalidOperationException("请先开启 36(网络记录/管线)并确认主动发送入口已就绪");
|
||||||
|
return ActiveSender.Enqueue(packet,game.ReadSendSession());
|
||||||
|
}
|
||||||
|
public SendTransactionSnapshot? PumpActiveSends()
|
||||||
|
{
|
||||||
|
if(disposed) return null;
|
||||||
|
return ActiveSender.Pump(game.ReadSendSession(),SendNative);
|
||||||
|
}
|
||||||
|
private byte SendNative(ReadOnlySpan<byte> nativeBuffer)
|
||||||
|
{
|
||||||
|
var sender=receiverSend;
|
||||||
|
if(sender==null || nativeBuffer.Length<NativeSendPacket.NativePrefixSize) return 0;
|
||||||
|
var fw=Framework.Instance();
|
||||||
|
if(fw==null || fw->NetworkModuleProxy==null) return 0;
|
||||||
|
var receiver=*(nint*)((nint)fw->NetworkModuleProxy+8);
|
||||||
|
if(receiver==0) return 0;
|
||||||
|
var padded=new byte[Math.Max(0xF60,nativeBuffer.Length)];
|
||||||
|
nativeBuffer.CopyTo(padded);
|
||||||
|
fixed(byte* ptr=padded)
|
||||||
|
{
|
||||||
|
return sender(receiver,(nint)ptr,0,0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void Start(ResourceScope scope)
|
||||||
|
{
|
||||||
|
if(!NativePacketCapture.IsSupportedVersion(version))
|
||||||
|
throw new InvalidOperationException($"收发包管理器仅适配国际服已审计版本,当前为 {version}。");
|
||||||
|
Hook<ZoneReceive>? down=null;
|
||||||
|
Hook<ZoneUp>? up=null;
|
||||||
|
down=game.Hook<ZoneReceive>(scope,DownSignature,(connection,holder)=>Pipeline.Receive(
|
||||||
|
()=>down!.Original(connection,holder),accepts=>NativePacketCapture.Down(memory,holder,accepts)));
|
||||||
|
up=game.Hook<ZoneUp>(scope,UpSignature,(connection,data,arg,flag)=>Pipeline.Send(
|
||||||
|
()=>up!.Original(connection,data,arg,flag),accepts=>NativePacketCapture.Up(memory,data,accepts)));
|
||||||
|
scope.Own(ActiveSender.Attach(Pipeline));
|
||||||
|
try { receiverSend=Marshal.GetDelegateForFunctionPointer<ReceiverSendPacket>(game.Scan(AdaptationManifest.ActiveSendReceiverSignature)); }
|
||||||
|
catch { receiverSend=null; }
|
||||||
|
scope.Defer(()=>
|
||||||
|
{
|
||||||
|
receiverSend=null;
|
||||||
|
ActiveSender.CancelAllQueued("收发包管线已关闭,取消待发队列");
|
||||||
|
});
|
||||||
|
down.Enable(); up.Enable();
|
||||||
|
}
|
||||||
|
// Module owners are released before the manager during Plugin.Dispose.
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if(disposed) return;
|
||||||
|
disposed=true;
|
||||||
|
ActiveSender.CancelAllQueued("插件卸载,取消待发队列");
|
||||||
|
Pipeline.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
|
||||||
|
// All slider call sites use this helper so precise input has the same limits as dragging.
|
||||||
|
public sealed class PrecisionSliders
|
||||||
|
{
|
||||||
|
private float editing,firstClickValue;
|
||||||
|
private uint firstClickId;
|
||||||
|
private bool focus;
|
||||||
|
public bool Float(string label,ref float value,float min,float max)
|
||||||
|
{
|
||||||
|
ImGui.PushID(label);
|
||||||
|
var before=value;
|
||||||
|
var id=ImGui.GetID("slider");
|
||||||
|
var changed=ImGui.SliderFloat(label,ref value,min,max,"%.3f",ImGuiSliderFlags.AlwaysClamp|ImGuiSliderFlags.NoRoundToFormat);
|
||||||
|
if(ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left))
|
||||||
|
{
|
||||||
|
value=firstClickId==id ? firstClickValue : before;
|
||||||
|
changed=value!=before;
|
||||||
|
editing=value; focus=true;
|
||||||
|
ImGui.OpenPopup("输入精确数值");
|
||||||
|
}
|
||||||
|
else if(ImGui.IsItemClicked(ImGuiMouseButton.Left)) { firstClickId=id; firstClickValue=before; }
|
||||||
|
if(ImGui.IsItemHovered()) ImGui.SetTooltip($"双击输入精确数值,或 Ctrl+单击直接输入。当前值:{value:G9}");
|
||||||
|
if(ImGui.BeginPopup("输入精确数值"))
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(label);
|
||||||
|
ImGui.Text($"范围:{min:G9} ~ {max:G9}");
|
||||||
|
if(focus) { ImGui.SetKeyboardFocusHere(); focus=false; }
|
||||||
|
ImGui.SetNextItemWidth(210);
|
||||||
|
var enter=ImGui.InputFloat("数值",ref editing,0,0,"%.6f",ImGuiInputTextFlags.EnterReturnsTrue|ImGuiInputTextFlags.AutoSelectAll);
|
||||||
|
if(!float.IsFinite(editing)) ImGui.TextUnformatted("请输入有限数值。");
|
||||||
|
ImGui.BeginDisabled(!float.IsFinite(editing));
|
||||||
|
var apply=ImGui.Button("确定");
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
if((enter || apply) && float.IsFinite(editing))
|
||||||
|
{
|
||||||
|
value=Math.Clamp(editing,min,max); changed=true; ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if(ImGui.Button("取消")) ImGui.CloseCurrentPopup();
|
||||||
|
ImGui.TextUnformatted("超出范围的数值按上下限保存。");
|
||||||
|
ImGui.EndPopup();
|
||||||
|
}
|
||||||
|
ImGui.PopID();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
// Historical signatures supplied in IC. Successful scanning does not verify the native ABI.
|
||||||
|
internal static class Signatures
|
||||||
|
{
|
||||||
|
public const string AntiKnockAddress = "48 ?? ?? 57 48 ?? ?? ?? ?? ?? ?? 0f 29 70 ?? 0f ?? ??";
|
||||||
|
public const string MovePermissionAddress = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 54 41 56 41 57 48 83 EC ?? 33 DB 41 8B E9 48 39 1D";
|
||||||
|
public const string SpeedAddress = "40 57 48 83 EC ?? 48 8B F9 48 8B 49 ?? 48 8B 01 FF 90 ?? ?? ?? ?? 48 85 C0 75";
|
||||||
|
public const string OnSendFlag = "48 89 5c 24 ?? 57 48 ?? ?? ?? 8b ?? 48 ?? ?? 33 ?? e8 ?? ?? ?? ?? 84 ??";
|
||||||
|
public const string FallCheckPatch = "e8 ?? ?? ?? ?? 8b ?? 85 ?? 0f 88 ?? ?? ?? ?? 48 ?? ?? ?? f3 ?? ?? ?? ?? ?? ?? ??";
|
||||||
|
public const string ActionRange = "48 89 5C 24 ?? 57 48 ?? ?? ?? 48 ?? ?? ?? ?? ?? ?? 8B ?? 0F 29 74 24 20";
|
||||||
|
public const string ActorRadius = "e8 ?? ?? ?? ?? f3 ?? ?? ?? 48 ?? ?? ?? ?? f3 ?? ?? ?? ?? f3 ?? ?? ?? f3 ?? ?? ?? ?? f3 0f 11 74 24 ?? ";
|
||||||
|
// 2026.09.15 native gl_target VFX update, RVA 0x7B5430. Does not calculate movement.
|
||||||
|
public const string NativeTargetRingUpdate = "4C 8B DC 55 57 41 57 49 8D AB ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 80 B9 ?? ?? ?? ?? ?? 4C 8B FA 48 8B F9 0F 85 ?? ?? ?? ?? 48 8B 41 30";
|
||||||
|
public const string NoBackswing = "48 ?? ?? ?? 48 ?? ?? ?? 45 ?? ?? 33 ?? e8 ?? ?? ?? ?? 84 ?? 74 ?? ";
|
||||||
|
// 2026.09.15: common movement initializer, called by the original NoActionMove wrapper.
|
||||||
|
public const string ActionMovementStart = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 48 8B D9 0F B6 FA 48 81 C1 ?? ?? ?? ?? 4D 8B F1 4D 8B F8 E8 ?? ?? ?? ?? 48 8B B3 ?? ?? ?? ??";
|
||||||
|
public const string FuckBewithAction = "e8 ?? ?? ???? b0 ?? c7 43 ?? ?? ?? ?? ?? eb ??";
|
||||||
|
public const string OnSendNormalMove = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B F9 41 8B D8";
|
||||||
|
public const string OnSendCombatMove = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B F9 41 8B F0";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Dalamud;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
|
||||||
|
namespace ICRebuild.Runtime;
|
||||||
|
|
||||||
|
public sealed class StatusBlockModule(GameContext game, Configuration config, ModuleManager modules)
|
||||||
|
{
|
||||||
|
// 2026.09.15: setter RVA 0x8A6BF0; local-player load RVA 0xB37D7A.
|
||||||
|
internal const string SetterSignature = "40 53 56 41 56 48 83 EC 70 45 32 F6 0F 29 74 24 50 41 0F B7 F0 0F 28 F3 48 8B D9 83 FA 3C";
|
||||||
|
internal const string LocalPlayerSignature = "48 8B 1D ?? ?? ?? ?? 48 8B FA 48 85 DB 74 28 48 8B 03 48 8B CB FF 90 78 02 00 00";
|
||||||
|
private long blocked;
|
||||||
|
private int lastStatus;
|
||||||
|
public long Blocked => Interlocked.Read(ref blocked);
|
||||||
|
public int LastStatus => Volatile.Read(ref lastStatus);
|
||||||
|
|
||||||
|
public void Register() => modules.Register(8, scope =>
|
||||||
|
{
|
||||||
|
if(!StatusBlockPolicy.IsAuditedVersion(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 的角色布局与已核对的状态入口不一致。");
|
||||||
|
var localPointer = game.StaticAddress(LocalPlayerSignature, 0);
|
||||||
|
if(localPointer == 0) throw new InvalidOperationException("本地角色指针未解析。");
|
||||||
|
Interlocked.Exchange(ref blocked, 0);
|
||||||
|
Volatile.Write(ref lastStatus, 0);
|
||||||
|
game.Frames.Clear(); // Wait for a fresh Framework snapshot after enabling.
|
||||||
|
|
||||||
|
void ClearTerritory(uint _) => game.Frames.Clear();
|
||||||
|
void ClearLogout(int _, int __) => game.Frames.Clear();
|
||||||
|
void ClearCondition(ConditionFlag flag, bool value)
|
||||||
|
{
|
||||||
|
if(value && flag is ConditionFlag.BetweenAreas or ConditionFlag.BetweenAreas51 or ConditionFlag.DutyRecorderPlayback)
|
||||||
|
game.Frames.Clear();
|
||||||
|
}
|
||||||
|
game.Client.TerritoryChanged += ClearTerritory;
|
||||||
|
scope.Defer(()=>game.Client.TerritoryChanged -= ClearTerritory);
|
||||||
|
game.Client.Logout += ClearLogout;
|
||||||
|
scope.Defer(()=>game.Client.Logout -= ClearLogout);
|
||||||
|
game.Conditions.ConditionChange += ClearCondition;
|
||||||
|
scope.Defer(()=>game.Conditions.ConditionChange -= ClearCondition);
|
||||||
|
|
||||||
|
Hook<StatusSetter>? hook = null;
|
||||||
|
hook = game.Hook<StatusSetter>(scope, SetterSignature, (manager,index,id,remaining,param,source,refreshFlags) =>
|
||||||
|
{
|
||||||
|
var suppress = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var frame = game.Frames.Read();
|
||||||
|
// Do not dereference manager.Owner: it may legitimately be null.
|
||||||
|
// Only selected local updates incur native identity reads.
|
||||||
|
if(StatusBlockPolicy.IsSelected(index,id,config.Published.BlockedStatuses) &&
|
||||||
|
frame.Ready && !frame.IsReplay && manager != 0 && manager == frame.LocalStatusManagerAddress &&
|
||||||
|
SafeMemory.Read<nint>(localPointer,out var currentPlayer) && currentPlayer == frame.LocalAddress &&
|
||||||
|
SafeMemory.Read<uint>(currentPlayer + StatusBlockPolicy.EntityIdOffset,out var currentId))
|
||||||
|
suppress = StatusBlockPolicy.IsLocal(frame,manager,currentPlayer,currentId);
|
||||||
|
}
|
||||||
|
catch(Exception ex) { game.Fault(8,ex); }
|
||||||
|
|
||||||
|
// Outside the catch: a native invocation must never be retried on an exception.
|
||||||
|
var result = StatusBlockPolicy.Forward(hook!.Original,suppress,manager,index,id,remaining,param,source,refreshFlags);
|
||||||
|
if(suppress)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref blocked);
|
||||||
|
Volatile.Write(ref lastStatus,id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
hook.Enable();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# IC Rebuild 实施状态
|
||||||
|
|
||||||
|
更新日期:2026 年 10 月 13 日。区服:国际服 / 国服双版本。当前版本:0.1.16。
|
||||||
|
|
||||||
|
首轮实测、崩溃根因、加速度反汇编及 Opcode 检查见 `diagnostics/2026-10-10-first-live-test.md`。国际服补丁适配与 23 项签名扫描审计见 `diagnostics/2026-10-13-patch-2026.10.06-adaptation.md`。国服客户端(`D:\FINAL FANTASY XIV`,2026.09.15 基线)23 项签名全量扫描与反汇编核实见 `diagnostics/2026-10-13-cn-client-compatibility.md`。
|
||||||
|
|
||||||
|
当前为可构建的开发包,首批尚未完成。24 项编号保持不变;12 仍为独立研究项,38—43 仍为网络模块后续扩展。
|
||||||
|
|
||||||
|
目前用户确认 15 项有效:3、9、10、11 验收通过后,14 的 16461 落点亦已确认正常。0.1.6 新增 9—11 服务栏提示与所有滑块双击精确输入。0.1.16 完成客户端双环境完整适配(国际服 `2026.10.06` 与国服 `2026.09.15`),23 处原生签名全部唯一定位通过。
|
||||||
|
|
||||||
|
## 构建基线
|
||||||
|
|
||||||
|
| 项目 | 版本或状态 |
|
||||||
|
| --- | --- |
|
||||||
|
| .NET SDK | 10.0.401 |
|
||||||
|
| 目标框架 | net10.0-windows,x64 |
|
||||||
|
| Dalamud API | 15 |
|
||||||
|
| Dalamud.dll | 15.0.3.6,构建 b666d821a47306fb447c60155b5d99377f91a5ee |
|
||||||
|
| FFXIVClientStructs.dll | 文件版本 7.56.2.9136,构建 313161e448e335adddd928f8a0212b2c328b5659 |
|
||||||
|
| ECommons 参考源码 | 74af164;仅用于核对 Legacy 网络接口,不链接整个库 |
|
||||||
|
| 目标客户端完整版本 | 国际服 2026.10.06.0000.0000 / 国服 2026.09.15.0000.0000 双审计基线 |
|
||||||
|
| 游戏内验收 | 用户确认 1、2、3、9、10、11、13、14、15、16、17、19、27、28、29 实测有效;14 原生圈和突进落点均已由用户确认有效 |
|
||||||
|
|
||||||
|
工作区 Dalamud 与 FFXIVClientStructs 源码用于查阅,实际编译绑定上述成品 DLL。部分源码 checkout 与成品 DLL 不同,因此复唱、咏唱和收包接口的委托签名另行核对了成品程序集元数据。
|
||||||
|
|
||||||
|
## 首批功能状态
|
||||||
|
|
||||||
|
“已接入”仅代表入口和实现已编译,不代表当前客户端能成功启用或实际效果已验证。
|
||||||
|
|
||||||
|
| 编号 | 功能 | 当前结果 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | 移速调整 | 用户确认实测有效;保留减速状态过滤 |
|
||||||
|
| 2 | 最大加速度 | 用户确认实测有效;已修正原生结果被旧值覆盖的问题并限定本地对象 |
|
||||||
|
| 3 | 强制移动 | 用户确认验收通过;回退保持原始参数 |
|
||||||
|
| 4 | 击退调整 | 已接入;按本地对象地址过滤,距离和持续时间可配置,待实测 |
|
||||||
|
| 5 | 掉落无伤 | 已接入历史标志处理,待实测 |
|
||||||
|
| 6 | 无掉落 | 已接入历史检查 Hook,待核实作用范围 |
|
||||||
|
| 7 | 魅惑与恐惧移动处理 | 0.1.3 核实为强制坐标移动入口,第五参数改为 float;不删除状态,待实测 |
|
||||||
|
| 8 | 状态屏蔽 | 0.1.12 接入转换后 SetStatus,本地身份门控及回放暂停;默认仅选 1257,可编辑 38 项候选名单并查看命中。只处理后续更新,待首个状态效果实测 |
|
||||||
|
| 9 | 飞天遁地 | 用户确认验收通过;手动生效、回到 0;0.1.6 新增服务栏状态 |
|
||||||
|
| 10 | 深层迷宫专用模式 | 用户确认验收通过;依赖 9,未知和特殊层不应用偏移;0.1.6 新增服务栏状态 |
|
||||||
|
| 11 | 迷宫状态显示 | 用户确认验收通过;0.1.6 新增服务栏楼层及特殊层提示 |
|
||||||
|
| 13 | 技能距离 | 用户确认实测有效;普通技能增量上限 3,保留特殊技能逻辑 |
|
||||||
|
| 14 | 目标圈半径 | 用户确认 16461 落点正常;统一扩大目标原生半径,自身不增加,不额外叠加技能距离 |
|
||||||
|
| 15 | 后摇可移动 | 用户确认实测有效 |
|
||||||
|
| 16 | 突进无位移 | 用户确认实测有效;79 个玩家位移技能、58 个编号;0.1.5 名单管理及手动编号默认折叠 |
|
||||||
|
| 17 | 复唱缩减 | 用户确认实测有效 |
|
||||||
|
| 18 | 快速结印 | 已接入,依赖 17;待实测 |
|
||||||
|
| 19 | 咏唱缩减 | 用户确认实测有效 |
|
||||||
|
| 20 | 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 验收 |
|
||||||
|
|
||||||
|
## 已完成的工程工作
|
||||||
|
|
||||||
|
- 建立独立项目、发布清单、构建脚本和离线测试工程。
|
||||||
|
- 模块启停幂等、部分启动失败回滚、逆序资源清理、依赖级联关闭。
|
||||||
|
- 释放异常后禁止重复挂接相同模块,避免把释放失败伪装成正常关闭。
|
||||||
|
- 所有模块默认关闭,配置与运行状态分离,禁用缺失实现的模块。
|
||||||
|
- 移除旧验证运行依赖;未迁移已排除功能。
|
||||||
|
- 对网络解析使用有界 Span,不引入存在越界问题的旧状态缓冲区。
|
||||||
|
- 建立 M0 统一版本适配与诊断清单(区分缺实现、版本不符、SDK 偏移变化、签名缺失、多重匹配、运行异常)及 M4 受控主动发送与请求响应事务原型。
|
||||||
|
- 原始 IC 源码不修改;只更新计划进度和新增实现工程。
|
||||||
|
|
||||||
|
## 离线验证
|
||||||
|
|
||||||
|
`build.ps1` 运行测试程序。检查内容包括:24 项范围、启动回滚、清理失败、重复启停、依赖顺序、迷宫普通层与特殊层、非有限参数、名单语义、Opcode 版本和方向隔离、错误映射原子拒绝、包长度与计数、3000 个随机截断输入、8000 次并发记录、时间轴保真、元数据标记、M0 适配清单门控以及 M4 主动发送编码/调度/事务状态机。
|
||||||
|
|
||||||
|
最终构建结果和测试总数以 `VERIFICATION.md` 为准。
|
||||||
|
|
||||||
|
## 未完成内容和下一步
|
||||||
|
|
||||||
|
1. 热重载 0.1.15,一次性合并验收阶段 1、阶段 2 与阶段 3:
|
||||||
|
- 查看“版本适配与诊断 (M0)”面板确认 24 项模块及主动发送通道状态;
|
||||||
|
- 开启 36、37 查看管理器计数、暂停/恢复隔离、动作与 Ping 配对、100% 解析显示,以及“主动发送与请求响应事务 (M4)”手动入队测试(验证 `PluginActiveSend` 来源标记与 `DOWN_Effect` RTT 确认);
|
||||||
|
- 开启 8 实测首个选定状态效果。
|
||||||
|
2. 14 已确认有效,后续补充与 13、16 组合启停和切换目标的验收。
|
||||||
|
3. 继续测试未确认的 4、5、6、7、18、20;已确认功能继续保留组合及生命周期验收。
|
||||||
|
4. 8 已通过公共 SetStatus 入口接入,避免按旧包结构修改;接下来确认未选状态、其他角色、切区/回放/卸载及首个目标效果,再逐项扩展名单验收。尚不覆盖所有可能绕过 setter 的直接复制路径。
|
||||||
|
5. 对 36 的完整采集与 M4 主动发送做现场验收:检查载荷长度、仅元数据/异常计数、主动发送事务状态、开启时性能、反复启停、切图和卸载。当前离线测试及首轮导出核验不能替代所有运行场景。
|
||||||
|
6. 37 已完成 `capture-018.jsonl` 全部 891 条记录的当前版本解析与 27 组动作请求响应配对(见 `diagnostics/2026-10-12-protocol-stage2.md`),待游戏内界面验收。
|
||||||
|
7. 已确认功能的“已实测有效”记录用户反馈;模块组合、切图、退出登录和卸载仍需独立验收,不据此判定全阶段完成。
|
||||||
|
8. M1 只读管理器、M3 样本协议覆盖、M0 统一版本适配诊断与 M4 主动发送及请求响应事务原型均已完成代码交付(见 `diagnostics/2026-10-12-stage3-m0-m4.md`),待现场合并验收后按需评估 M5 选定业务扩展。
|
||||||
|
|
||||||
|
首批不能因开发包已生成而判定完成。8、36、37 的现场验收以及所有原生效果验收继续保留在首批范围内。
|
||||||
|
|
||||||
|
|
||||||
|
0.1.2 具体依据见 diagnostics/2026-10-10-onslaught.md。
|
||||||
|
|
||||||
|
|
||||||
|
0.1.3 完成状态、Permission、Action 与冷却组核对,详细记录见 diagnostics/2026-10-11-id-audit.md。
|
||||||
|
|
||||||
|
0.1.5 的落点修正、验证文案与界面调整见 diagnostics/2026-10-11-radius-landing-and-ui.md。Release 构建通过,45 项离线测试通过。
|
||||||
|
|
||||||
|
用户补充的原版 I-Ching-GL.dll(程序集 0.1.6.6)已完成静态元数据核对,见 diagnostics/2026-10-11-original-dll.md。关键方法体读取异常,暂不能用于还原落点公式;状态结构声明仍存在矛盾。本次仅增加参考证据,没有修改或重新部署运行逻辑。
|
||||||
|
|
||||||
|
第二份 I-Ching-GL (2).dll 亦为 0.1.6.6,但依赖及组件不同。两份已完成递归声明对比和关键方法体可读性检查,见 diagnostics/2026-10-11-two-dll-analysis.md。第二份增加验证等组件,核心 Hook 声明一致,仍未得到可靠落点算法;分析不改变当前开发状态。
|
||||||
|
|
||||||
|
0.1.7 完成服务栏 ASCII 修复并登记 14 验收通过。0.1.8 已补齐上下行长度契约并接入有界复制,见 diagnostics/2026-10-11-network-full-capture.md。随后收到 capture-018.jsonl,891 条首轮完整样本通过核验;0.1.9 修正当前普通移动/Ping 长度并补齐缺失 PingRsp 映射,8 组回显均配对。详情见 diagnostics/2026-10-11-capture-018-review.md。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Third party notices
|
||||||
|
|
||||||
|
NetworkCapture's original adapter and retained upstream entry point signature/calling convention are based on ECommons Legacy GameNetwork, commit 74af164, without linking or distributing the ECommons assembly. Version 0.1.8 uses a separately audited ZoneReceive entry and bounded copies from the current client's native message length contracts. Replay hooks remain omitted. The ECommons event itself does not provide payload length.
|
||||||
|
|
||||||
|
Source: https://github.com/NightmareXIV/ECommons/tree/74af164/ECommons/DalamudServices/Legacy
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 NightmareXIV
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
The restoration logic also draws on user-provided IC source fragments. Their original files are retained in the sibling IC directory. No license for those fragments was included in that directory; this developer artifact makes no claim of a new license for the original work.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup><Compile Include="../Core/**/*.cs" Link="Core/%(Filename)%(Extension)" /></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
sealed class PacketMemoryFixture
|
||||||
|
{
|
||||||
|
public Dictionary<nint,byte[]> Blocks { get; }=[];
|
||||||
|
public List<(nint Address,int Count)> Reads { get; }=[];
|
||||||
|
public bool ShortReads { get; set; }
|
||||||
|
public bool Read(nint address,int count,out byte[] bytes)
|
||||||
|
{
|
||||||
|
Reads.Add((address,count));
|
||||||
|
foreach(var (start,block) in Blocks)
|
||||||
|
{
|
||||||
|
var offset=(long)address-(long)start;
|
||||||
|
if(offset<0 || offset>block.Length || count>block.Length-offset) continue;
|
||||||
|
bytes=block.AsSpan((int)offset,ShortReads ? Math.Max(0,count-1) : count).ToArray();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bytes=[]; return false;
|
||||||
|
}
|
||||||
|
public static PacketMemoryFixture Up(byte[] payload,ulong? length=null)
|
||||||
|
{
|
||||||
|
var memory=new PacketMemoryFixture();
|
||||||
|
var header=new byte[16]; BinaryPrimitives.WriteUInt16LittleEndian(header,838);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(header.AsSpan(8),length ?? (ulong)payload.Length+16);
|
||||||
|
memory.Blocks[0x1000]=header; memory.Blocks[0x1020]=payload.ToArray();
|
||||||
|
return memory;
|
||||||
|
}
|
||||||
|
public static PacketMemoryFixture Down(byte[] payload,ulong? length=null,uint target=0x12345678)
|
||||||
|
{
|
||||||
|
var memory=new PacketMemoryFixture();
|
||||||
|
var reference=new byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(reference,0x2000);
|
||||||
|
memory.Blocks[0x1010]=reference;
|
||||||
|
var descriptor=new byte[32]; BinaryPrimitives.WriteUInt32LittleEndian(descriptor,target);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(descriptor.AsSpan(16),length ?? (ulong)payload.Length+16);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(descriptor.AsSpan(24),0x3000);
|
||||||
|
memory.Blocks[0x2020]=descriptor;
|
||||||
|
var ipc=new byte[16+payload.Length]; BinaryPrimitives.WriteUInt16LittleEndian(ipc.AsSpan(2),354);
|
||||||
|
payload.CopyTo(ipc,16); memory.Blocks[0x3000]=ipc;
|
||||||
|
return memory;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1076
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
|
||||||
|
<ItemGroup><Reference Include="Lumina;Lumina.Excel"><HintPath>$(APPDATA)\XIVLauncher\addon\Hooks\dev\%(Identity).dll</HintPath></Reference></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Lumina;
|
||||||
|
using System.Text.Json;
|
||||||
|
if(args.Length is <2 or >3) throw new ArgumentException("ActionAudit sqpack-path name-fragment|--movement [output-json]");
|
||||||
|
using var game=new GameData(args[0]);
|
||||||
|
var rows=new List<object>();
|
||||||
|
var timelines=new HashSet<uint>();
|
||||||
|
var ids=args[1].StartsWith("--ids:") ? args[1][6..].Split(',').Select(uint.Parse).ToHashSet() : null;
|
||||||
|
foreach(var action in game.GetExcelSheet<Lumina.Excel.Sheets.Action>() ?? throw new InvalidDataException("Action sheet unavailable"))
|
||||||
|
if(ids!=null ? ids.Contains(action.RowId) : args[1]=="--movement" ? action.IsPlayerAction && action.AffectsPosition : action.Name.ToString().Contains(args[1],StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{action.RowId} {action.Name}: AffectsPosition={action.AffectsPosition}, Hostile={action.CanTargetHostile}, AnimationEnd={action.AnimationEnd.RowId}");
|
||||||
|
rows.Add(new { ActionId=action.RowId,Name=action.Name.ToString(),Timeline=action.AnimationEnd.RowId,PvP=action.IsPvP,PlayerAction=action.IsPlayerAction,AffectsPosition=action.AffectsPosition });
|
||||||
|
if(action.AnimationEnd.RowId is >0 and <=ushort.MaxValue) timelines.Add(action.AnimationEnd.RowId);
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Skills={rows.Count}, Timelines={timelines.Count}");
|
||||||
|
if(args.Length==3) File.WriteAllText(args[2],JsonSerializer.Serialize(rows,new JsonSerializerOptions { WriteIndented=true }));
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
|
||||||
|
<ItemGroup><Compile Include="../../Core/**/*.cs" Link="Core/%(Filename)%(Extension)" /></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
|
||||||
|
if(args.Length!=3) throw new ArgumentException("CaptureAudit capture.jsonl opcodes.json report.json");
|
||||||
|
var packets=PacketFiles.Import(args[0]);
|
||||||
|
var versions=packets.Select(p=>p.GameVersion).Distinct().ToArray();
|
||||||
|
if(versions.Length!=1 || packets.Length==0) throw new InvalidDataException("Expected one nonempty capture version");
|
||||||
|
var map=new OpcodeMap(); map.Load(File.ReadAllText(args[1]),versions[0],"local cache");
|
||||||
|
string? Name(PacketRecord p)=>map.Resolve(p.GameVersion,p.Direction,p.Opcode);
|
||||||
|
var supported=new HashSet<string>{
|
||||||
|
"UP_PingReq","DOWN_PingRsp","UP_UpdatePositionHandler","UP_UpdatePositionInstance","UP_EventStart",
|
||||||
|
"UP_ActionSend","UP_ActionSendPos","UP_ClientTrigger",
|
||||||
|
"UP_EventFinish","UP_EventFinish4","UP_EventFinish8","UP_EventFinish16","UP_EventFinish32","UP_EventFinish64","UP_EventFinish128","UP_EventFinish255",
|
||||||
|
"UP_EventAction","UP_EventAction4","UP_EventAction8","UP_EventAction16","UP_EventAction32","UP_EventAction64","UP_EventAction128","UP_EventAction255",
|
||||||
|
"DOWN_EffectResult","DOWN_UpdateHate","DOWN_UpdateHater","DOWN_EffectResultBasic","DOWN_JobGauge","DOWN_UpdateClassInfo","DOWN_FirstAttack",
|
||||||
|
"DOWN_ActorControl","DOWN_ActorControlSelf","DOWN_ActorControlTarget","DOWN_Effect","DOWN_StatusEffectList","DOWN_ActorDelete","DOWN_ObjectSpawn","DOWN_NpcSpawn"
|
||||||
|
};
|
||||||
|
var parsed=packets.Select(p=>(Packet:p,Name:Name(p),Result:p.PayloadComplete ? PacketParser.Parse(Name(p),p.Payload,p.GameVersion) : new ParseResult(false,"incomplete"))).ToArray();
|
||||||
|
var correlations=PacketCorrelator.Ping(packets,map);
|
||||||
|
var actionCorrelations=PacketCorrelator.Actions(packets,map);
|
||||||
|
// Replay owned sample data through the manager and recorder; no native calls or sockets.
|
||||||
|
using var manager=new NetworkPipeline(versions[0]);
|
||||||
|
var replayBuffer=new PacketBuffer { Paused=false };
|
||||||
|
var recorder=new PacketRecorder(replayBuffer);
|
||||||
|
using var up=manager.Subscribe(NetworkPhase.BeforeSend,recorder.Record);
|
||||||
|
using var down=manager.Subscribe(NetworkPhase.Received,recorder.Record);
|
||||||
|
long returned=0;
|
||||||
|
using var post=manager.Subscribe(NetworkPhase.AfterSend,_=>returned++);
|
||||||
|
foreach(var p in packets)
|
||||||
|
{
|
||||||
|
CaptureRead Read(Func<int,bool> accepts)=>accepts(p.Opcode) ? new(new(p.Opcode,p.Target,p.Payload,p.PayloadComplete,p.CaptureIssue)) : new(null);
|
||||||
|
if(p.Direction==PacketDirection.Up) manager.Send(()=>1,Read); else manager.Receive(()=>1,Read);
|
||||||
|
}
|
||||||
|
var replayed=replayBuffer.Snapshot();
|
||||||
|
var replayMatches=replayed.Length==packets.Length && replayed.Zip(packets).All(pair=>
|
||||||
|
pair.First.Direction==pair.Second.Direction && pair.First.Opcode==pair.Second.Opcode &&
|
||||||
|
pair.First.GameVersion==pair.Second.GameVersion && pair.First.Target==pair.Second.Target &&
|
||||||
|
pair.First.PayloadComplete==pair.Second.PayloadComplete && pair.First.CaptureIssue==pair.Second.CaptureIssue &&
|
||||||
|
pair.First.Payload.SequenceEqual(pair.Second.Payload));
|
||||||
|
if(!replayMatches || manager.ObserverErrors!=0 || returned!=packets.Count(p=>p.Direction==PacketDirection.Up))
|
||||||
|
throw new InvalidDataException("Manager replay changed sample data or missed send completion observations");
|
||||||
|
var report=new {
|
||||||
|
Source=Path.GetFileName(args[0]),SHA256=Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(args[0]))),
|
||||||
|
MapSHA256=Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(args[1]))),MapSource=map.Snapshot.Source,Version=versions[0],
|
||||||
|
Records=packets.Length,Up=packets.Count(p=>p.Direction==PacketDirection.Up),Down=packets.Count(p=>p.Direction==PacketDirection.Down),
|
||||||
|
ManagerReplay=new {Matches=replayMatches,Records=replayed.Length,SendReturns=returned,manager.ObserverErrors,manager.ReadErrors},
|
||||||
|
FirstTime=packets.Min(p=>p.Time),LastTime=packets.Max(p=>p.Time),FirstSequence=packets.First().Sequence,LastSequence=packets.Last().Sequence,
|
||||||
|
SequenceContinuous=packets.Zip(packets.Skip(1)).All(p=>p.Second.Sequence==p.First.Sequence+1),
|
||||||
|
MonotonicOrder=packets.Zip(packets.Skip(1)).All(p=>p.Second.MonotonicTicks>=p.First.MonotonicTicks),
|
||||||
|
Complete=packets.Count(p=>p.PayloadComplete),Incomplete=packets.Count(p=>!p.PayloadComplete),CaptureIssues=packets.Count(p=>p.CaptureIssue!=null),
|
||||||
|
PayloadBytes=packets.Sum(p=>p.Payload.Length),MinPayload=packets.Min(p=>p.Payload.Length),MaxPayload=packets.Max(p=>p.Payload.Length),
|
||||||
|
EmptyPayloads=packets.Count(p=>p.Payload.Length==0),Known=packets.Count(p=>Name(p)!=null),Unknown=packets.Count(p=>Name(p)==null),
|
||||||
|
Parsed=parsed.Count(p=>p.Result.Parsed),SupportedButRejected=parsed.Count(p=>p.Name!=null && supported.Contains(p.Name) && !p.Result.Parsed),
|
||||||
|
Groups=parsed.GroupBy(p=>new{p.Packet.Direction,p.Packet.Opcode,p.Name,Length=p.Packet.Payload.Length,p.Packet.PayloadComplete,p.Result.Parsed})
|
||||||
|
.OrderByDescending(g=>g.Count()).Select(g=>new{g.Key,Count=g.Count(),Rejection=g.Key.Name!=null && supported.Contains(g.Key.Name) && !g.Key.Parsed ? g.First().Result.Summary : null}),
|
||||||
|
PingPairs=correlations.Select(p=>new{p.Request,p.Response,Milliseconds=p.Elapsed.TotalMilliseconds}),
|
||||||
|
ActionPairs=actionCorrelations.Select(p=>new{p.Request,p.Response,Milliseconds=p.Elapsed.TotalMilliseconds}),
|
||||||
|
Limitations="No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
|
||||||
|
};
|
||||||
|
File.WriteAllText(args[2],JsonSerializer.Serialize(report,new JsonSerializerOptions{WriteIndented=true}));
|
||||||
|
Console.WriteLine($"Records={report.Records}; up={report.Up}; down={report.Down}; complete={report.Complete}; known={report.Known}; parsed={report.Parsed}; rejected={report.SupportedButRejected}; pingPairs={correlations.Length}; actionPairs={actionCorrelations.Length}");
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$AssemblyPath,
|
||||||
|
[Parameter(Mandatory)][string]$OutputPath,
|
||||||
|
[string]$CecilPath = (Join-Path $env:APPDATA 'XIVLauncher\addon\Hooks\dev\Mono.Cecil.dll')
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
# Read the target as data with Cecil. Never load or execute the target assembly.
|
||||||
|
Add-Type -Path $CecilPath
|
||||||
|
$icInput = (Resolve-Path -LiteralPath $AssemblyPath).Path
|
||||||
|
$icAssembly = [Mono.Cecil.AssemblyDefinition]::ReadAssembly($icInput)
|
||||||
|
try {
|
||||||
|
$icSelected = $icAssembly.MainModule.Types | Where-Object {
|
||||||
|
$_.FullName -match '^StatusEffectList(Entry)?$|^SamplePlugin\.Hook\.(ActorRadiusHook|NoActionMoveHook|YMove|StatusCheck)$'
|
||||||
|
}
|
||||||
|
$icTypes = foreach ($icType in $icSelected) {
|
||||||
|
$icMethods = foreach ($icMethod in $icType.Methods) {
|
||||||
|
$icState = 'NoBody'
|
||||||
|
$icError = $null
|
||||||
|
$icCodeSize = $null
|
||||||
|
$icInstructions = $null
|
||||||
|
if ($icMethod.HasBody) {
|
||||||
|
try {
|
||||||
|
$icBody = $icMethod.get_Body()
|
||||||
|
$icCodeSize = $icBody.CodeSize
|
||||||
|
$icInstructions = @($icBody.Instructions | ForEach-Object { $_.ToString() })
|
||||||
|
# Parsing success alone is not validation of a protected method body.
|
||||||
|
$icState = 'ParsedUnvalidated'
|
||||||
|
$icMissingOperands = @($icBody.Instructions | Where-Object {
|
||||||
|
$_.OpCode.OperandType.ToString() -ne 'InlineNone' -and $null -eq $_.Operand
|
||||||
|
})
|
||||||
|
if ($icCodeSize -gt 1048576 -or $icInstructions.Count -eq 0 -or
|
||||||
|
$icBody.Instructions[0].OpCode.Name -eq 'ldelem.i1' -or $icMissingOperands.Count -gt 0) {
|
||||||
|
$icState = 'SuspiciousBody'
|
||||||
|
$icInstructions = $null
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$icState = 'ReadError'
|
||||||
|
$icError = $_.Exception.GetBaseException().Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[ordered]@{
|
||||||
|
Signature = $icMethod.FullName
|
||||||
|
Token = $icMethod.MetadataToken.ToUInt32()
|
||||||
|
RVA = ('0x{0:X}' -f $icMethod.RVA)
|
||||||
|
BodyState = $icState
|
||||||
|
DeclaredCodeSize = $icCodeSize
|
||||||
|
Error = $icError
|
||||||
|
UnvalidatedIL = $icInstructions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[ordered]@{
|
||||||
|
Name = $icType.FullName
|
||||||
|
Attributes = $icType.Attributes.ToString()
|
||||||
|
DeclaredSize = $icType.ClassSize
|
||||||
|
PackingSize = $icType.PackingSize
|
||||||
|
Fields = @($icType.Fields | ForEach-Object {
|
||||||
|
[ordered]@{Name=$_.Name; Type=$_.FieldType.FullName; Offset=$_.Offset}
|
||||||
|
})
|
||||||
|
NestedTypes = @($icType.NestedTypes | ForEach-Object {
|
||||||
|
[ordered]@{
|
||||||
|
Name=$_.FullName
|
||||||
|
DeclaredSize=$_.ClassSize
|
||||||
|
DelegateInvoke=@($_.Methods | Where-Object Name -eq 'Invoke' | ForEach-Object { $_.FullName })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Methods = @($icMethods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$icReport = [ordered]@{
|
||||||
|
Source = $icInput
|
||||||
|
SHA256 = (Get-FileHash -LiteralPath $icInput -Algorithm SHA256).Hash
|
||||||
|
Assembly = $icAssembly.Name.FullName
|
||||||
|
Runtime = $icAssembly.MainModule.RuntimeVersion
|
||||||
|
References = @($icAssembly.MainModule.AssemblyReferences | ForEach-Object {
|
||||||
|
[ordered]@{Name=$_.Name; Version=$_.Version.ToString()}
|
||||||
|
})
|
||||||
|
Types = @($icTypes)
|
||||||
|
Limitation = 'Metadata only. Method-body parsing is not proof of executable logic. Target assembly was not executed.'
|
||||||
|
}
|
||||||
|
$icReport | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding utf8
|
||||||
|
"Saved metadata audit: $OutputPath"
|
||||||
|
} finally { $icAssembly.Dispose() }
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$FirstPath,
|
||||||
|
[Parameter(Mandatory)][string]$SecondPath,
|
||||||
|
[Parameter(Mandatory)][string]$OutputPath,
|
||||||
|
[string]$CecilPath = (Join-Path $env:APPDATA 'XIVLauncher\addon\Hooks\dev\Mono.Cecil.dll')
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Add-Type -Path $CecilPath
|
||||||
|
function Get-IcTypes($Types) {
|
||||||
|
foreach ($icType in $Types) {
|
||||||
|
$icType
|
||||||
|
Get-IcTypes $icType.NestedTypes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function Get-IcMetadata([string]$Path) {
|
||||||
|
$icFile = Get-Item -LiteralPath $Path
|
||||||
|
$icAsm = [Mono.Cecil.AssemblyDefinition]::ReadAssembly($icFile.FullName)
|
||||||
|
try {
|
||||||
|
$icAllTypes = @(Get-IcTypes $icAsm.MainModule.Types)
|
||||||
|
$icNamedTypes = @($icAllTypes | Where-Object { $_.FullName -match '^[\x20-\x7E]+$' })
|
||||||
|
$icShapes = [ordered]@{}
|
||||||
|
foreach ($icType in $icNamedTypes) {
|
||||||
|
$icShapes[$icType.FullName] = @(
|
||||||
|
"TYPE $($icType.Attributes) SIZE $($icType.ClassSize) PACK $($icType.PackingSize) BASE $($icType.BaseType.FullName)"
|
||||||
|
$icType.Interfaces | ForEach-Object { "INTERFACE $($_.InterfaceType.FullName)" }
|
||||||
|
$icType.Fields | ForEach-Object { "FIELD $($_.FullName) OFFSET $($_.Offset) FLAGS $($_.Attributes)" }
|
||||||
|
$icType.Methods | ForEach-Object { "METHOD $($_.FullName) FLAGS $($_.Attributes) IMPL $($_.ImplAttributes)" }
|
||||||
|
) | Sort-Object
|
||||||
|
}
|
||||||
|
[ordered]@{
|
||||||
|
File=$icFile.Name
|
||||||
|
Size=$icFile.Length
|
||||||
|
SHA256=(Get-FileHash -LiteralPath $icFile.FullName).Hash
|
||||||
|
Assembly=$icAsm.Name.FullName
|
||||||
|
MVID=$icAsm.MainModule.Mvid.ToString()
|
||||||
|
Architecture=$icAsm.MainModule.Architecture.ToString()
|
||||||
|
Attributes=@($icAsm.CustomAttributes | Where-Object {
|
||||||
|
$_.AttributeType.Name -match '^TargetFrameworkAttribute$|^Assembly(File|Informational)VersionAttribute$'
|
||||||
|
} | ForEach-Object {
|
||||||
|
[ordered]@{Type=$_.AttributeType.FullName; Values=@($_.ConstructorArguments | ForEach-Object { [string]$_.Value })}
|
||||||
|
})
|
||||||
|
References=@($icAsm.MainModule.AssemblyReferences | ForEach-Object { "$($_.Name) $($_.Version)" })
|
||||||
|
Resources=@($icAsm.MainModule.Resources | ForEach-Object { "$($_.ResourceType) $($_.Name)" })
|
||||||
|
TopLevelTypeCount=$icAsm.MainModule.Types.Count
|
||||||
|
TotalTypeCount=$icAllTypes.Count
|
||||||
|
AsciiNamedTypeCount=$icNamedTypes.Count
|
||||||
|
TypeShapes=$icShapes
|
||||||
|
}
|
||||||
|
} finally { $icAsm.Dispose() }
|
||||||
|
}
|
||||||
|
$icFirst = Get-IcMetadata $FirstPath
|
||||||
|
$icSecond = Get-IcMetadata $SecondPath
|
||||||
|
$icOnlyFirst = @($icFirst.TypeShapes.Keys | Where-Object { !$icSecond.TypeShapes.Contains($_) })
|
||||||
|
$icOnlySecond = @($icSecond.TypeShapes.Keys | Where-Object { !$icFirst.TypeShapes.Contains($_) })
|
||||||
|
$icChanges = foreach ($icName in $icFirst.TypeShapes.Keys) {
|
||||||
|
if (!$icSecond.TypeShapes.Contains($icName)) { continue }
|
||||||
|
$icDiff = @(Compare-Object $icFirst.TypeShapes[$icName] $icSecond.TypeShapes[$icName])
|
||||||
|
if ($icDiff.Count -gt 0) {
|
||||||
|
[ordered]@{Type=$icName; Differences=@($icDiff | ForEach-Object {
|
||||||
|
[ordered]@{OnlyIn=if ($_.SideIndicator -eq '=>') {'Second'} else {'First'}; Declaration=$_.InputObject}
|
||||||
|
})}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$icOutput = [ordered]@{
|
||||||
|
First=$icFirst
|
||||||
|
Second=$icSecond
|
||||||
|
AsciiTypesOnlyInFirst=$icOnlyFirst
|
||||||
|
AsciiTypesOnlyInSecond=$icOnlySecond
|
||||||
|
CommonTypeMetadataChanges=@($icChanges)
|
||||||
|
Limitation='Static declarations only; equal metadata does not establish equal method bodies or behavior. No target assembly was executed.'
|
||||||
|
}
|
||||||
|
$icOutput | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding utf8
|
||||||
|
"Saved comparison: $OutputPath"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
|
||||||
|
<ItemGroup><Reference Include="Iced"><HintPath>$(APPDATA)\XIVLauncher\addon\Hooks\dev\Iced.dll</HintPath></Reference></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System.Reflection.PortableExecutable;
|
||||||
|
using Iced.Intel;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
// Read-only on-disk signature audit. Does not open a process or modify the executable.
|
||||||
|
if(args.Length<2) throw new ArgumentException("NativeAudit executable signature|rva:HEX [instruction-count]");
|
||||||
|
using var input=File.OpenRead(args[0]);
|
||||||
|
using var pe=new PEReader(input);
|
||||||
|
var section=pe.PEHeaders.SectionHeaders.Single(x=>x.Name==".text");
|
||||||
|
var bytes=pe.GetSectionData(section.VirtualAddress).GetContent().ToArray();
|
||||||
|
if(args[1].StartsWith("xrefstr:") || args[1].StartsWith("xrefs:"))
|
||||||
|
{
|
||||||
|
int target;
|
||||||
|
if(args[1].StartsWith("xrefstr:"))
|
||||||
|
{
|
||||||
|
var image=File.ReadAllBytes(args[0]);
|
||||||
|
var offset=image.AsSpan().IndexOf(System.Text.Encoding.UTF8.GetBytes(args[1][8..]+"\0"));
|
||||||
|
if(offset<0) throw new InvalidDataException("String not found");
|
||||||
|
var dataSection=pe.PEHeaders.SectionHeaders.Single(s=>offset>=s.PointerToRawData && offset<s.PointerToRawData+s.SizeOfRawData);
|
||||||
|
target=offset-dataSection.PointerToRawData+dataSection.VirtualAddress;
|
||||||
|
}
|
||||||
|
else target=Convert.ToInt32(args[1][6..].Split('-')[0],16);
|
||||||
|
var endTarget=args[1].StartsWith("xrefs:") && args[1].Contains('-') ? Convert.ToInt32(args[1].Split('-')[1],16) : target;
|
||||||
|
var directory=pe.PEHeaders.PEHeader!.ExceptionTableDirectory;
|
||||||
|
var functions=pe.GetSectionData(directory.RelativeVirtualAddress).GetContent(0,directory.Size).ToArray();
|
||||||
|
Console.WriteLine($"XREF TARGET RVA=0x{target:X}");
|
||||||
|
bool IsStringPointer(ulong pointer)
|
||||||
|
{
|
||||||
|
if(pointer<pe.PEHeaders.PEHeader!.ImageBase) return false;
|
||||||
|
var rva=(long)(pointer-pe.PEHeaders.PEHeader.ImageBase);
|
||||||
|
var data=pe.PEHeaders.SectionHeaders.FirstOrDefault(s=>rva>=s.VirtualAddress && rva<s.VirtualAddress+s.SizeOfRawData);
|
||||||
|
if(data.SizeOfRawData==0) return false;
|
||||||
|
var content=pe.GetSectionData((int)rva).GetContent(0,Math.Min(128,data.VirtualAddress+data.SizeOfRawData-(int)rva)).ToArray();
|
||||||
|
var end=Array.IndexOf(content,(byte)0);
|
||||||
|
return end>=3 && content.Take(end).All(b=>b>=32 && b<127);
|
||||||
|
}
|
||||||
|
foreach(var dataSection in pe.PEHeaders.SectionHeaders.Where(s=>s.Name!=".text" && s.SizeOfRawData>0))
|
||||||
|
{
|
||||||
|
var raw=pe.GetSectionData(dataSection.VirtualAddress).GetContent().ToArray();
|
||||||
|
for(int at=0;at+8<=raw.Length;at+=8)
|
||||||
|
if(BinaryPrimitives.ReadUInt64LittleEndian(raw.AsSpan(at))==pe.PEHeaders.PEHeader!.ImageBase+(uint)target)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"DATA REF RVA=0x{dataSection.VirtualAddress+at:X}");
|
||||||
|
var before=at;
|
||||||
|
while(before>=8 && at-before<8192 && IsStringPointer(BinaryPrimitives.ReadUInt64LittleEndian(raw.AsSpan(before-8)))) before-=8;
|
||||||
|
Console.WriteLine($"STRING TABLE candidate RVA=0x{dataSection.VirtualAddress+before:X}, preceding={(at-before)/8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(int i=0;i+7<=bytes.Length;i++)
|
||||||
|
{
|
||||||
|
var callTarget=section.VirtualAddress+i+5+BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(i+1));
|
||||||
|
var ripTarget=section.VirtualAddress+i+7+BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(i+3));
|
||||||
|
bool call=(bytes[i]==0xE8 || bytes[i]==0xE9) && callTarget>=target && callTarget<=endTarget;
|
||||||
|
bool rip=(bytes[i] is 0x48 or 0x4C) && bytes[i+1] is 0x8D or 0x8B && (bytes[i+2]&0xC7)==5 && ripTarget>=target && ripTarget<=endTarget;
|
||||||
|
var rawTarget=BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(i));
|
||||||
|
bool immediate=rawTarget>=target && rawTarget<=endTarget;
|
||||||
|
if(!call && !rip && !immediate) continue;
|
||||||
|
var rva=section.VirtualAddress+i;
|
||||||
|
bool described=false;
|
||||||
|
for(int at=0;at+12<=functions.Length;at+=12)
|
||||||
|
{
|
||||||
|
int begin=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at)),end=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at+4));
|
||||||
|
if(rva>=begin && rva<end) { Console.WriteLine($"REF RVA=0x{rva:X}, FUNCTION=0x{begin:X}..0x{end:X}, {(call ? "CALL/JMP" : rip ? "RIP" : "IMM/CANDIDATE")} target=0x{(call ? callTarget : rip ? ripTarget : rawTarget):X}"); described=true; break; }
|
||||||
|
}
|
||||||
|
if(!described) Console.WriteLine($"REF RVA=0x{rva:X}, FUNCTION=leaf/unknown, {(call ? "CALL/JMP" : rip ? "RIP" : "IMM/CANDIDATE")} target=0x{(call ? callTarget : rip ? ripTarget : rawTarget):X}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var containing=args[1].StartsWith("func:",StringComparison.OrdinalIgnoreCase);
|
||||||
|
var direct=containing || args[1].StartsWith("rva:",StringComparison.OrdinalIgnoreCase);
|
||||||
|
var requested=direct ? Convert.ToInt32(args[1][(containing ? 5 : 4)..],16) : 0;
|
||||||
|
if(containing)
|
||||||
|
{
|
||||||
|
var directory=pe.PEHeaders.PEHeader!.ExceptionTableDirectory;
|
||||||
|
var functions=pe.GetSectionData(directory.RelativeVirtualAddress).GetContent(0,directory.Size).ToArray();
|
||||||
|
bool found=false;
|
||||||
|
for(int at=0;at+12<=functions.Length;at+=12)
|
||||||
|
{
|
||||||
|
int begin=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at)),end=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at+4));
|
||||||
|
if(requested>=begin && requested<end)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"CONTAINING RVA=0x{begin:X}..0x{end:X}");
|
||||||
|
var unwind=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at+8));
|
||||||
|
for(int depth=0;depth<16;depth++)
|
||||||
|
{
|
||||||
|
var header=pe.GetSectionData(unwind).GetContent(0,4).ToArray();
|
||||||
|
if(((header[0]>>3)&4)==0) break;
|
||||||
|
var chainAt=(4+header[2]*2+3)&~3;
|
||||||
|
var chain=pe.GetSectionData(unwind+chainAt).GetContent(0,12).ToArray();
|
||||||
|
begin=BinaryPrimitives.ReadInt32LittleEndian(chain); end=BinaryPrimitives.ReadInt32LittleEndian(chain.AsSpan(4));
|
||||||
|
unwind=BinaryPrimitives.ReadInt32LittleEndian(chain.AsSpan(8));
|
||||||
|
Console.WriteLine($"CHAINED ROOT RVA=0x{begin:X}..0x{end:X}");
|
||||||
|
}
|
||||||
|
requested=begin;found=true;break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!found) throw new InvalidDataException("No unwind entry for requested RVA");
|
||||||
|
}
|
||||||
|
var start=direct ? requested-section.VirtualAddress : 0;
|
||||||
|
if(start<0 || start>=bytes.Length) throw new ArgumentException("RVA outside .text");
|
||||||
|
var pattern=direct ? "" : args[1].Replace(" ","");
|
||||||
|
if(pattern.Length%2!=0) throw new ArgumentException("Odd signature length");
|
||||||
|
var values=Enumerable.Range(0,pattern.Length/2).Select(i=>pattern.Substring(i*2,2)).ToArray();
|
||||||
|
var count=args.Length>2 ? int.Parse(args[2]) : 100;
|
||||||
|
int matches=0;
|
||||||
|
for(int i=start;i<=(direct ? start : bytes.Length-values.Length);i++)
|
||||||
|
{
|
||||||
|
bool match=true;
|
||||||
|
for(int j=0;j<values.Length;j++)
|
||||||
|
if(values[j]!="??" && bytes[i+j]!=Convert.ToByte(values[j],16)) { match=false; break; }
|
||||||
|
if(!match) continue;
|
||||||
|
matches++;
|
||||||
|
var decoder=Decoder.Create(64,new ByteArrayCodeReader(bytes,i,Math.Min(8192,bytes.Length-i)));
|
||||||
|
decoder.IP=pe.PEHeaders.PEHeader!.ImageBase+(uint)section.VirtualAddress+(uint)i;
|
||||||
|
Console.WriteLine($"MATCH RVA=0x{section.VirtualAddress+i:X}");
|
||||||
|
Console.WriteLine("PREFIX "+Convert.ToHexString(bytes.AsSpan(i,Math.Min(64,bytes.Length-i))));
|
||||||
|
var formatter=new IntelFormatter(); var output=new StringOutput();
|
||||||
|
for(int n=0;n<count;n++)
|
||||||
|
{
|
||||||
|
decoder.Decode(out var instruction); formatter.Format(instruction,output);
|
||||||
|
Console.WriteLine($"{instruction.IP:X16} {output.ToStringAndReset()}");
|
||||||
|
if(instruction.FlowControl==FlowControl.Return) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Matches: {matches}");
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Lumina;
|
||||||
|
using ICRebuild.Core;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
if(args.Length!=2) throw new ArgumentException("StatusAudit sqpack-directory output-json");
|
||||||
|
using var game=new GameData(args[0]);
|
||||||
|
var sheet=game.GetExcelSheet<Lumina.Excel.Sheets.Status>() ?? throw new InvalidDataException("Status sheet unavailable");
|
||||||
|
var result=new List<object>();
|
||||||
|
var root=game.GetFile<Lumina.Data.Files.Excel.ExcelListFile>("exd/root.exl");
|
||||||
|
if(root!=null)
|
||||||
|
foreach(var entry in root.ExdMap.Where(x=>x.Value==0x14A)) Console.WriteLine($"Native movement-check sheet 0x14A = {entry.Key}");
|
||||||
|
foreach(var (group,id) in StatusCatalog.Entries())
|
||||||
|
{
|
||||||
|
var row=sheet.GetRowOrDefault(id);
|
||||||
|
if(row==null) { Console.WriteLine($"MISSING {group} {id}"); result.Add(new{Group=group,Id=id,Missing=true}); continue; }
|
||||||
|
var s=row.Value;
|
||||||
|
Console.WriteLine($"{group} {id} {s.Name}: movement={s.LockMovement} control={s.LockControl} actions={s.LockActions}");
|
||||||
|
result.Add(new {Group=group,Id=id,Name=s.Name.ToString(),Description=s.Description.ToString(),s.LockMovement,s.LockControl,s.LockActions,s.Flags,s.Flag2,s.ParamEffect,s.ParamModifier});
|
||||||
|
}
|
||||||
|
foreach(var s in sheet)
|
||||||
|
{
|
||||||
|
var name=s.Name.ToString();
|
||||||
|
if((name=="Heavy" || name=="Leaden") && !StatusCatalog.SlowCandidates.Contains(s.RowId))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"UNLISTED_SLOW {s.RowId} {name}: {s.Description}");
|
||||||
|
result.Add(new{Group="UnlistedSlowNameSearch",Id=s.RowId,Name=name,Description=s.Description.ToString()});
|
||||||
|
}
|
||||||
|
if(!new[]{"charm","fear","hysteria","seduc","terror"}.Any(x=>name.Contains(x,StringComparison.OrdinalIgnoreCase))) continue;
|
||||||
|
Console.WriteLine($"CONTROL {s.RowId} {name}: movement={s.LockMovement} control={s.LockControl} actions={s.LockActions}");
|
||||||
|
result.Add(new {Group="ControlNameSearch",Id=s.RowId,Name=name,Description=s.Description.ToString(),s.LockMovement,s.LockControl,s.LockActions,s.Flags,s.Flag2,s.ParamEffect,s.ParamModifier});
|
||||||
|
}
|
||||||
|
var territories=game.GetExcelSheet<Lumina.Excel.Sheets.TerritoryType>();
|
||||||
|
foreach(uint id in new uint[]{1108,1290})
|
||||||
|
{
|
||||||
|
var territory=territories?.GetRowOrDefault(id);
|
||||||
|
if(territory.HasValue) Console.WriteLine($"TERRITORY {id}: use={territory.Value.TerritoryIntendedUse.RowId}, name={territory.Value.PlaceName.Value.Name}");
|
||||||
|
}
|
||||||
|
File.WriteAllText(args[1],JsonSerializer.Serialize(result,new JsonSerializerOptions{WriteIndented=true}));
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
|
||||||
|
<ItemGroup><Reference Include="Lumina;Lumina.Excel"><HintPath>$(APPDATA)\XIVLauncher\addon\Hooks\dev\%(Identity).dll</HintPath></Reference><Compile Include="../../Core/StatusCatalog.cs" Link="StatusCatalog.cs" /></ItemGroup>
|
||||||
|
</Project>
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
# IC Rebuild 验证记录
|
||||||
|
|
||||||
|
最新交付:0.1.16,2026 年 10 月 13 日。客户端补丁 `2026.10.06.0000.0000` 完整适配;23 项特征码全部扫描核对成功;支持双版本向后兼容;与阶段 1、阶段 2、阶段 3 合并发布。
|
||||||
|
|
||||||
|
## 0.1.16
|
||||||
|
|
||||||
|
客户端补丁与国服双环境适配交付:
|
||||||
|
- **国际服补丁与国服客户端扫描反汇编审计**:
|
||||||
|
- 针对国际服 `ffxiv_dx11.exe`(`2026.10.06.0000.0000`)与国服 `ffxiv_dx11.exe`(`D:\FINAL FANTASY XIV\game\ffxiv_dx11.exe`,`2026.09.15.0000.0000`,SHA256 `7BA28760BC53CBBBF66B63105FEA6C1BFE09CB6B1C6353FC8FB0FF03CC1A30F4`)分别执行全量静态特征码扫描。23 处特征码在两个不同客户端二进制中**全部唯一定位成功**(23/23 匹配)。
|
||||||
|
- 反汇编审计确认国服内部结构偏移(`ReceiverSendPacket` `0x1CF9020` 读 `+0xA70`、`ZoneClientSendPacket` `0x1CFEF30` 读 `+0x98`、`ZoneUp` `0x1D6DAD0`、`ZoneDown` `0x1D6D650` 读 `+0x88`、`StatusSetter` `0x8A4E10` 60 槽位、`AccelerationHook` `0x1827FC0` `+0x3C/+0x20`)及国服 Dalamud `FFXIVClientStructs.dll` 偏移(`0x23B0` / `0x78`)完全一致。详见 `diagnostics/2026-10-13-cn-client-compatibility.md`。
|
||||||
|
- **双版本向后兼容**:代码抽象为 `IsAuditedVersion`,同时兼容 `2026.10.06.0000.0000` 与 `2026.09.15.0000.0000`;界面顶栏自动识别国服/国际服。
|
||||||
|
- **Opcode 与双环境部署**:
|
||||||
|
- 国际服部署:`C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild`
|
||||||
|
- 国服部署:`C:\Users\AinaSnow\AppData\Roaming\XIVLauncherCN\dev\ICRebuild`,并在 `XIVLauncherCN` 的 `dalamudConfig.json` 注册开发插件加载项,同步 `opcodes.json`(含国服完整映射)。
|
||||||
|
|
||||||
|
|
||||||
|
阶段 3(M0 + M4)交付:
|
||||||
|
- **M0 统一版本适配与诊断(`Core/AdaptationManifest.cs`)**:建立全部 24 项保留模块与 M4 主动发送通道的签名、版本锁(`2026.09.15.0000.0000`)及 SDK 偏移(`0x23B0` / `0x78`)清单。本机 `ffxiv_dx11.exe` 核验 21 条唯一签名均精确匹配 1 处。对接 `ModuleManager` 自动门控版本不符、偏移变化、签名缺失或多重匹配的模块,不影响独立模块与离线分析;在 UI 增加“版本适配与诊断 (M0)”面板支持一键重扫。
|
||||||
|
- **M4 主动发送与请求响应事务(`Core/ActivePacketSender.cs`、`Runtime/NetworkManager.cs`)**:反汇编证明旧 `IC/NetManager.cs` 使用的 `0x4810E0` 在调用后恒定执行 `mov al, 1` 吞掉真实返回码;现改接 `Framework.Instance()->NetworkModuleProxy`(`+0x08`)→ `ReceiverSendPacket`(`0x1CFD140`)→ `ZoneClient.SendPacket`(`0x1D03040`)→ `ZoneUp`(`0x1D718C0`),保留真实 `byte` 返回码与 `0xF60` 清零发送缓冲区契约(`+0x00 Opcode`、`+0x08 payload.Length + 0x10`、`+0x20 Payload`)。提供 `UP_ActionSend`、`UP_ClientTrigger`、`UP_EventStart`、`UP_EventFinish*`(8 档)、`UP_EventAction*`(8 档)已审计编码器,`ActiveSendScope`(`PacketOrigin.PluginActiveSend` 来源标记),以及带队列上限(32)、限速(100 ms)、`SendSession` 区域/身份门控、网络回调重入拦截与显式状态机(`Queued`/`SentNative`/`RejectedNative`/`Confirmed`/`TimedOut`/`Canceled`)的 `ActivePacketDispatcher`。详见 `diagnostics/2026-10-12-stage3-m0-m4.md`。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;84 项离线测试通过。新增四组测试覆盖 M0 适配诊断与门控、M4 原生发送缓冲布局与 8 档事件编码器往返、`ActiveSendScope` 来源标注隔离,以及 `ActivePacketDispatcher` 限速/取消/切区/重入拒绝/RTT 响应关联。
|
||||||
|
|
||||||
|
发行包 SHA256:`42358EF528F401797BAE73119B2C5B5E7DE0B101DF134C21230D89C7879A4913`。
|
||||||
|
|
||||||
|
0.1.15.0 已部署至 `C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild`。DLL、清单与第三方声明均与构建产物逐项核对 SHA256。DLL SHA256:`6E634280476BE04089C517DDB5619531054A510AF1FD9951FE2965FF1E6CFBB4`;清单 SHA256:`5371322709595D089AB8C50566215F8BA4E983608CC2D13CBA81C47D03BE043E`。0.1.14 备份位于 `deploy-backups/0.1.14-before-0.1.15`。未操作游戏或自动重载。
|
||||||
|
|
||||||
|
## 0.1.14
|
||||||
|
|
||||||
|
阶段 2(M3)交付:基于本机 `ffxiv_dx11.exe`(`2026.09.15.0000.0000`,SHA256 `5BBC501DD5C7F22FD61A11D08C25356041D878DB7CD83203ADAE393E4DFACC44`)上行构造与下行 `PacketDispatcher`(`0x18D9500`)反汇编,补齐 `UP_ActionSend`(32B)、`UP_ActionSendPos`(24B)、`UP_ClientTrigger`(32B)、`DOWN_ActorControl`(24B)、`DOWN_ActorControlSelf`(40B)、`DOWN_ActorControlTarget`(32B)、`DOWN_Effect`(124B)、`DOWN_StatusEffectList`(384B)、`DOWN_EffectResult`(96B)、`DOWN_ActorDelete`(8B)、`DOWN_ObjectSpawn`(64B)、`DOWN_NpcSpawn`(656B)的当前版本解析器,解决 P2 中 `UP_EventFinish*` 与 `UP_EventAction*` 的 8 档变长分档(`2/4/8/16/32/64/128/255`)固定长度与 `ArgCount` 边界冲突,并在 `PacketCorrelator.Actions` 中接入 `UP_ActionSend`/`UP_ActionSendPos` `RequestId`(`+0x06`)与 `DOWN_Effect` `RequestId`(`+0x18`)的时间轴关联。详细反汇编与字段依据见 `diagnostics/2026-10-12-protocol-stage2.md`。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;80 项离线测试通过。新增三组测试覆盖上行动作/触发/8 档事件容量边界、下行战斗/状态/生成包长度与非有限浮点数拒绝(含 1500 次随机模糊输入)、以及动作请求响应 `RequestId` 配对。
|
||||||
|
|
||||||
|
`capture-018.jsonl` 离线回放报告(`diagnostics/2026-10-12-capture-018-v0114.json`):891 条记录全部完整、全部具名,`Parsed=891`(由 679 升至 891,覆盖率 100%),`SupportedButRejected=0`,`PingPairs=8`,`ActionPairs=27`(27 次 `UP_ActionSend` `RequestId 179..205` 与 27 次本地 `DOWN_Effect` 100% 配对)。
|
||||||
|
|
||||||
|
发行包 SHA256:`9FBD620CF25BB5947BE051F19623C1A7783F4AD915C2EF064CBF0A04198115F8`。
|
||||||
|
|
||||||
|
0.1.14.0 已部署至 `C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild`。DLL、清单与第三方声明均与构建产物逐项核对 SHA256。DLL SHA256:`45943548939502C9B3F73E841DE78F718F09AE798C5EC3154D516DF36A2B8C2E`;清单 SHA256:`CF4AE7D11DA8A0F5A7C476249437E841029F75DB4C81B12FD9BE5735E975A2ED`。0.1.13 备份位于 `deploy-backups/0.1.13-before-0.1.14`。未操作游戏或自动重载。
|
||||||
|
|
||||||
|
## 0.1.13
|
||||||
|
|
||||||
|
M1 交付:共享 Zone Hook、独立收包/发送前/发送后订阅、取消订阅、异常隔离、自持有只读载荷、日志过滤分离,以及新记录的来源/阶段上下文。保留原有原生签名、ABI、调用参数与长度契约,不主动发送或重放业务包。详细接口契约见 diagnostics/2026-10-11-network-manager.md。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;77 项离线测试通过。新增七组针对调用顺序与次数、快照寿命、返回值保真、异常处理、接收失败、独立过滤、取消与共享生命周期及文件往返。
|
||||||
|
|
||||||
|
原 capture-018.jsonl 的 891 条记录经过管理器/记录器回放后载荷与关键元数据逐条一致,模拟发送后观察 534 次,观察和读取异常为 0。原文件的有限解析仍为 679 条、已支持格式拒绝 0、Ping 配对 8 组;报告为 diagnostics/2026-10-11-capture-018-manager.json。回放只调用托管代理,没有实际发送样本,不等于运行中原生入口验收。
|
||||||
|
|
||||||
|
发行包 SHA256:2D30524523461F673BA697808F30C169AFB748DC22E963C2DE26E40CFC986DBC。
|
||||||
|
|
||||||
|
0.1.13.0 已部署至 C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild。DLL、清单与第三方声明均与构建来源逐项核对哈希。DLL SHA256:13C53AFE7EB57977321ED08568A1C67E4A13B49BCE68CF382397719E60F49283。0.1.12 备份位于 deploy-backups/0.1.12-before-0.1.13。未操作游戏或自动重载。
|
||||||
|
|
||||||
|
待重载后开启 36,检查记录、暂停/恢复与管理器计数,以及关闭后订阅归零。性能、切区、退出登录和卸载仍待现场验证。M1 代码任务已完成,现场验收未宣布完成;M4 主动发送与请求响应事务保持待实现。
|
||||||
|
|
||||||
|
## 0.1.12
|
||||||
|
|
||||||
|
编号 8 接入转换后的 StatusManager.SetStatus,保留原函数调用、返回值、完整来源 ID 与标志刷新。默认仅选 1257,支持候选名单编辑和命中显示;使用帧快照、实时本地指针与实体 ID 匹配,切区/注销清空快照、回放暂停。只处理后续状态更新,首次开启前已存在状态和绕过 setter 的直接复制路径不承诺立即过滤。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;70 项离线测试通过。新增检查覆盖对象范围、指针复用、过期快照、回放、双列表槽位边界、名单隔离、原函数调用次数和参数/返回值保真。原生入口和身份读取签名分别唯一匹配 0x8A6BF0、0xB37D7A;SDK 字段与游戏 getter 已核对。详细依据见 diagnostics/2026-10-11-status-block-setter.md。
|
||||||
|
|
||||||
|
发行包 SHA256:1B11A5B9CE73754D28326BDCAB511849C40DF0737F7FFC075BC11608E967D817。
|
||||||
|
|
||||||
|
已部署至 C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild,版本 0.1.12.0。DLL、清单及第三方声明均与构建来源逐项核对 SHA256。DLL SHA256:6200D4BE1F337C519A05A0B8B2C161AF5A544531217A3D38CFA36A7AD7B87A8C。0.1.11 备份在 deploy-backups/0.1.11-before-0.1.12。未操作游戏或自动重载。
|
||||||
|
|
||||||
|
待用户重载后先测试一个选定状态:开启 8 后触发、观察命中和实际行为,再关闭后触发同一状态确认正常处理。未选状态、其他角色、切区、回放及卸载仍待现场验收。未将 8 标记为已实测有效,首批与 M2 均未宣布完成。
|
||||||
|
|
||||||
|
## 0.1.1 历史记录
|
||||||
|
|
||||||
|
日期:2026 年 10 月 10 日。国际服客户端版本:2026.09.15.0000.0000。
|
||||||
|
|
||||||
|
- Release 构建通过,0 警告、0 错误。
|
||||||
|
- 26 项离线测试全部通过。
|
||||||
|
- 新增:工作线程读取帧快照、配置集合隔离、加速度保留原生输出、本地对象与活动状态门控、原子文件保存失败保留旧文件。
|
||||||
|
- 保留:模块生命周期、清理失败、依赖关系、包边界、3000 个随机截断输入、8000 次并发记录及导入导出测试。
|
||||||
|
- 已读取实际崩溃日志,确认 0.1.0 目标圈工作线程访问 ObjectTable.LocalPlayer 导致异常。
|
||||||
|
- 已只读审计本机游戏磁盘文件,确认最大加速度函数的运动状态读写和唯一签名匹配。
|
||||||
|
- Opcode 映射当前版本校验通过:上行 28 个名称与数值,下行 73 个名称、90 个数值;无同方向冲突。没有 DOWN_PingRsp。
|
||||||
|
|
||||||
|
用户首轮反馈已登记;0.1.1 修复后的游戏内效果待复测,不能将离线测试等同于现场通过。
|
||||||
|
|
||||||
|
旧版抓包未导出,崩溃后无法恢复。新版增加 5 秒滚动快照保存与导出命令,实时载荷仍未采集。
|
||||||
|
|
||||||
|
已部署到用户指定目录 C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild。DLL 与清单 SHA256 和构建输出完全一致。DLL SHA256:D9AB1E73469F8AA2FD9DC82F0EA0D79663AC0D89397B4B41453AA67302F1B4C7。原版备份保留在 deploy-backups/0.1.0-before-0.1.1。
|
||||||
|
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;32 项离线测试通过。本机动作表识别 79 个玩家位移技能和 58 个不同 Timeline。14 的作用域与 16 的共用原生入口修复仍待游戏内复测。发行包 SHA256:5D966B0BC3FE6134D934E88C97B6DC863F2C48A50CE28683494A407994284BB5。
|
||||||
|
|
||||||
|
|
||||||
|
0.1.2 已部署至用户指定开发目录,DLL 与清单哈希均与构建产物一致;原 0.1.1 备份在 deploy-backups/0.1.1-before-0.1.2。
|
||||||
|
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:Release 构建通过,0 警告、0 错误;35 项离线测试通过。Status 表及原生标识审计见 diagnostics/2026-10-11-id-audit.md。编号 7 的参数修正和新增减速保护尚待游戏内复测;编号 8 未启用。发行包 SHA256:139514E98ABF6B890C001698767F407089C2D89DF9D6A8D0D26EBD314E41E55D。
|
||||||
|
|
||||||
|
0.1.3 已部署至用户指定开发目录,DLL 与清单的 SHA256 均与构建产物一致;原版已备份。
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:Release 构建通过,0 警告、0 错误;40 项离线测试通过。原生圈更新签名唯一匹配 RVA 0x7B5430,前置距离路径核对 RVA 0x8EE490。实际圈显示、距离及猛攻落点仍待复测。发行包 SHA256:1B40D57AE0790E88E6A082A0B10FFCFC3ACC0A66ACEEF9978F3DE9F02FCA0CF2。
|
||||||
|
|
||||||
|
0.1.4 已部署至指定开发目录,DLL 和清单哈希与构建结果一致;旧版备份在 deploy-backups/0.1.3-before-0.1.4。
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:Release 构建通过,0 警告、0 错误;45 项离线测试通过。目标半径策略改为只扩大当前选中目标一次;加入用户确认状态文案、9 的会话内手动生效和清零、Timeline 折叠。用户确认的十项功能见 STATUS.md;14 的 16461 落点与 9 新交互仍待游戏内复测。
|
||||||
|
|
||||||
|
发行包 SHA256:0E16B90A610330E36F86273E20DC8B5571B71B2279CEFC818BDE47C149B8F9DF。
|
||||||
|
|
||||||
|
0.1.5 已解压部署至 C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild,DLL 和清单哈希与构建输出一致。DLL SHA256:AC2F4620F5B025D6EE79A51BD3E459218E360A48DB85FE8DEA7AE948EFB70660。0.1.4 备份保留在 deploy-backups/0.1.4-before-0.1.5。等待用户热重载;未自动操作游戏。
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:用户追加确认 3、9、10、11 验收通过,确认清单共 14 项。Release 构建通过,0 警告、0 错误;47 项离线测试通过。新增服务栏状态检查覆盖待应用、应用值与预设分离、特殊层暂停、非迷宫、关闭、运行失败与切区隐藏。
|
||||||
|
|
||||||
|
新增服务栏条目“IC Rebuild · 高度与迷宫”,每 200 ms 检查状态,只在状态变化时更新;悬停看详情,点击打开设置,卸载移除。所有滑块统一支持双击输入、确定或回车提交、范围钳制和取消;保留 Ctrl+单击输入。9 修改预设仍须另点生效。新 UI 交互未在游戏内自动验收。
|
||||||
|
|
||||||
|
发行包 SHA256:47294B9BCCEC1C6545ABC67F7418B295CE332E1504688556EC4A78EF5B60A8F2。
|
||||||
|
|
||||||
|
已部署至用户指定的 C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild,版本 0.1.6.0,DLL 及清单与构建产物哈希一致。DLL SHA256:3905FDEBF6DF26AE3D1619AE69BB16EF2F6AB39C4AD9AE17DB7DD59AA8E0CBB2。旧版保存在 deploy-backups/0.1.5-before-0.1.6。等待用户热重载。
|
||||||
|
|
||||||
|
## 0.1.7
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:用户确认 14 的 16461 落点正常,确认清单共 15 项。截图显示国际服服务栏部分中文缺字,现将服务栏和原生悬浮提示改为 ASCII 短文案,例如 IC Y:-7 D:1 F:23。插件窗口中文不变。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;47 项离线测试通过,包含当前验证清单、服务栏状态语义和 ASCII 检查。游戏内显示待用户热重载核验。网络长度来源仅作只读研究,本版未变更采集实现。
|
||||||
|
|
||||||
|
发行包 SHA256:95AEF324FC2DE6C8B24FCA888AC4125A4FCBF9EBD8508C0AD649CB4F5C1B3CBD。
|
||||||
|
|
||||||
|
0.1.7.0 已部署至 C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild,DLL 与清单哈希均与构建产物一致。DLL SHA256:FA936BC5C205CF9CBF510C9BD661376F63253FF6F01BB7F099235513753B88F2。旧版备份在 deploy-backups/0.1.6-before-0.1.7。
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:36 接入按原生长度契约复制的上下行完整游戏载荷。下行改为接收函数成功返回后读取持有对象;上行仍在发送入口读取。两处签名在目标 exe 唯一匹配,exe SHA256 仍为 5BBC501DD5C7F22FD61A11D08C25356041D878DB7CD83203ADAE393E4DFACC44。依据详见 diagnostics/2026-10-11-network-full-capture.md。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;57 项离线测试通过。新增边界/失败/过滤/兼容/缓冲预算检查,没有运行原生游戏 Hook。仍需用户开启 36、37 并导出 capture-018.jsonl 验证真实完整载荷及解析;36 未标记游戏内验收通过。
|
||||||
|
|
||||||
|
发行包 SHA256:9B7B14CC4BCD2019BE9036883C8BA9B8C624FEEF10F17A8BCE8E52F1B590C578。
|
||||||
|
|
||||||
|
0.1.8.0 已部署至 C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild;DLL、清单和第三方声明哈希与构建产物一致。DLL SHA256:E90BF62C96E67E1BDE8712F7E32679ED4505077DBFE1830ED7AB7C9BF2FAA263。旧版备份在 deploy-backups/0.1.7-before-0.1.8。
|
||||||
|
|
||||||
|
## 0.1.9 与 capture-018 现场样本
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:读取用户导出 capture-018.jsonl,891 条均为完整载荷(上行 534、下行 357),序号连续。对照原生发送构造后确认采集长度正确,修复普通移动 24 字节及 Ping 请求/响应 32 字节的当前版本解析;只在当前版本且映射缺失无冲突时补充 PingRsp=338。原始文件和映射缓存未修改。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;60 项离线测试通过。同份实际文件回放 Parsed=true 从 2 增至 512,已有解析器的拒绝从 502 降至 0;8 组 Ping 回显匹配,往返 83.2—116.9 ms,均值 91.7 ms。167 条、6 种未知 Opcode 继续保留,2 条历史效果结果只通过有限计数检查。完整分析及统计见 diagnostics/2026-10-11-capture-018-review.md 和 before/after JSON。
|
||||||
|
|
||||||
|
发行包 SHA256:0AC409149DFC26E844EFE7A2C1AB3F95507B5CA96616D2CB27FFD7F118D51E26。
|
||||||
|
|
||||||
|
0.1.9.0 已部署到指定开发目录,DLL 与清单哈希与构建产物一致;DLL SHA256:A4A3192719E42E71295DF724EF7F3D302DBD15BE6A3E24895496E4810464E727。旧版保存在 deploy-backups/0.1.8-before-0.1.9。可热重载后启用 37 并导入同一文件查看,无需重新采样来验证本次离线解析修正。
|
||||||
|
|
||||||
|
## 0.1.10
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:根据本机原生分派及消费字段核实 671=UpdateHate、558=UpdateHater、697=EffectResultBasic。补充当前版本缺失映射与有限字段解析;不覆盖显式映射,不修改源样本或缓存,不新增运行 Hook。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;64 项离线测试通过,含新增计数/字节宽度/容量/版本/映射冲突及 1500 组任意输入检查。原 891 条完整样本回放,有名称 876,可解析 664,已支持格式拒绝 0,Ping 配对仍为 8。未知剩 15 条(614、788、214);已命名但未解析仍 212 条。依据及边界见 diagnostics/2026-10-11-opcode-671-697-558.md。
|
||||||
|
|
||||||
|
发行包 SHA256:3E9205ECB07973B8B94359EB4268CA378672C63C6CA2305A850F1BF472FF2E41。
|
||||||
|
|
||||||
|
0.1.10.0 已部署到 C:\Users\AinaSnow\AppData\Roaming\XIVLauncher\dev\ICRebuild,DLL 与清单哈希和构建输出一致。DLL SHA256:F5B2784BA1A1FB38C2F6456BB26C6BF539955B48638748EF47D0F2FB394B0211。旧版备份在 deploy-backups/0.1.9-before-0.1.10。
|
||||||
|
|
||||||
|
## 0.1.11
|
||||||
|
|
||||||
|
2026 年 10 月 11 日:补充 614 职业量表、788 职业信息、214 战斗归属的名称及已核实字段;原 ActorGauge=8 显式映射保留。状态列表另核实到 30×12 字节网络块及条件性 ID 转换,8 保持不可用。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;66 项离线测试通过。891 条原始样本全部具名,有限字段解析 679 条、已支持格式拒绝 0、Ping 仍配对 8 组;另 212 条已命名记录未实现解析,不据此判定协议还原完成。原生及当前 SDK 字段核对见 diagnostics/2026-10-11-opcode-remaining-and-status.md。
|
||||||
|
|
||||||
|
发行包 SHA256:D51EBA459B079ED20438401505B3A37CE05DB31B3E49D28439C789C1750E4326。
|
||||||
|
|
||||||
|
0.1.11.0 已部署至用户指定开发目录,DLL 和清单与构建哈希一致。DLL SHA256:B39751B36705B2C8EE417CD500BD8DC49078BA4C402CDAF43EF43089305224B0。旧版备份在 deploy-backups/0.1.10-before-0.1.11。
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
param([string]$HooksDevPath = (Join-Path $env:APPDATA 'XIVLauncher\addon\Hooks\dev'))
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$env:DOTNET_CLI_HOME = Join-Path $PSScriptRoot '.dotnet'
|
||||||
|
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
||||||
|
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
||||||
|
Push-Location $PSScriptRoot
|
||||||
|
try {
|
||||||
|
dotnet restore IC.Rebuild.csproj --configfile NuGet.Config --nologo
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Plugin restore failed.' }
|
||||||
|
dotnet build IC.Rebuild.csproj -c Release "-p:HooksDevPath=$HooksDevPath" --no-restore --nologo
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Plugin build failed.' }
|
||||||
|
dotnet restore Tests/IC.Rebuild.Tests.csproj --configfile NuGet.Config --nologo
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Test restore failed.' }
|
||||||
|
dotnet run --project Tests/IC.Rebuild.Tests.csproj -c Release --no-restore
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Tests failed.' }
|
||||||
|
New-Item -ItemType Directory -Path (Join-Path $PSScriptRoot 'dist') -Force | Out-Null
|
||||||
|
$icVersion = (Get-Content -LiteralPath 'ICRebuild.json' -Raw -Encoding UTF8 | ConvertFrom-Json).AssemblyVersion
|
||||||
|
$icArchive = Join-Path 'dist' ("ICRebuild-{0}-dev.zip" -f $icVersion)
|
||||||
|
Compress-Archive -LiteralPath @('bin/Release/ICRebuild.dll','bin/Release/ICRebuild.json','THIRD_PARTY_NOTICES.md') -DestinationPath $icArchive -Force
|
||||||
|
Get-Item -LiteralPath $icArchive
|
||||||
|
}
|
||||||
|
finally { Pop-Location }
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 首轮实测与 0.1.1 修复
|
||||||
|
|
||||||
|
客户端:国际服 `2026.09.15.0000.0000`。ffxiv_dx11.exe SHA256:`5BBC501DD5C7F22FD61A11D08C25356041D878DB7CD83203ADAE393E4DFACC44`。
|
||||||
|
|
||||||
|
## 用户反馈
|
||||||
|
|
||||||
|
- 测试 1 通过;保持原反馈,不扩展成所有参数和场景均通过。
|
||||||
|
- 最大加速度开启后无法行走。
|
||||||
|
- 目标圈大小开启后客户端崩溃。
|
||||||
|
- Opcode 记录在插件窗口可见,但没有导出文件。
|
||||||
|
|
||||||
|
配置文件中编号 1 与 14 的参数均为 0,所以不能据此认定非零移速增量或非零目标圈增量已通过验证。日志确认插件成功加载。
|
||||||
|
|
||||||
|
## 目标圈崩溃
|
||||||
|
|
||||||
|
证据来自 `dalamud_appcrash_20261010_231418_146_9584.log`:异常码 E0434352,HRESULT 80131509。调用链为游戏 JobPool 工作线程 → 目标圈回调 → GameContext.Ready → ObjectTable.LocalPlayer。
|
||||||
|
|
||||||
|
Dalamud ObjectTable 的索引访问调用 ThreadSafety.AssertMainThread。旧版 Ready 在原生工作线程直接访问该服务,导致托管异常越过原生回调边界。
|
||||||
|
|
||||||
|
0.1.1 将角色有效性、本地地址、PVP 状态、减速状态和迷宫信息放入不可变帧快照,仅在 Framework.Update 读取游戏服务。原生回调读取快照;超过 500 ms 未刷新时停用修改。配置参数和 Timeline 名单也发布为不可变快照。技能距离的 Excel 数据在启用时准备,不在原生回调读取。
|
||||||
|
|
||||||
|
目标圈回调使用 SDK GetRadius 委托,并在托管计算失败时保留原始返回值。修复通过离线回归,游戏内复测待完成。
|
||||||
|
|
||||||
|
## 最大加速度
|
||||||
|
|
||||||
|
在用户当前客户端磁盘文件中,历史签名唯一匹配 RVA `0x182BB20`。只读反汇编确认:
|
||||||
|
|
||||||
|
- `0x14182BB26` 检查 `[rcx+0x3C]` 活动状态。
|
||||||
|
- `0x14182BB38` 读取 `[rcx+0x20]` 控制器。
|
||||||
|
- `0x14182BB6A` 读取 `[rbx+0x44]` 运动状态。
|
||||||
|
- `0x14182BC5F` 和 `0x14182BD41` 将计算结果写回 `[rbx+0x44]`。
|
||||||
|
- 该函数通过控制器 `+0x3E0` 访问所属对象。
|
||||||
|
|
||||||
|
旧版 finally 写回调用前值,会覆盖原生计算结果;初始值为 0 时会把更新结果反复清零。0.1.1 删除这一恢复行为,仅对当前活动的本地角色控制器预置值,保留原函数最终结果。角色受减速状态影响时不预置。字段适配限定为本次审计版本,其他版本拒绝启用。
|
||||||
|
|
||||||
|
回归测试模拟初始 0、预置 100、原函数输出 6,确认返回后仍为 6;另覆盖其他对象、不活动、减速和 NaN 情况。实际行走、停止与加速效果仍待复测。
|
||||||
|
|
||||||
|
只读审计工具:`Tools/NativeAudit`;不访问进程内存或修改游戏文件。
|
||||||
|
|
||||||
|
## Opcode 检查
|
||||||
|
|
||||||
|
已读取插件配置目录中的 opcodes.json。当前版本包含 28 个上行名称、73 个下行名称,对应 28 个上行数值及 90 个下行数值。同方向内没有 Opcode 冲突,值均在 ushort 范围。
|
||||||
|
|
||||||
|
部分映射:
|
||||||
|
|
||||||
|
| 名称 | 十进制 Opcode |
|
||||||
|
| --- | --- |
|
||||||
|
| UP_UpdatePositionHandler | 920 |
|
||||||
|
| UP_UpdatePositionInstance | 411 |
|
||||||
|
| UP_PingReq | 838 |
|
||||||
|
| UP_EventStart | 181 |
|
||||||
|
| DOWN_ActorCast | 354 |
|
||||||
|
| DOWN_Effect | 787 |
|
||||||
|
| DOWN_EventStart | 737 |
|
||||||
|
|
||||||
|
当前版本没有 DOWN_PingRsp 映射,因此不能期待现有映射直接完成 Ping 响应关联。
|
||||||
|
|
||||||
|
这份文件是名称映射,不是用户本轮交互的抓包记录。用户确认没有导出,崩溃后旧版内存记录不可恢复;未对实际上下行序列作无依据判断。
|
||||||
|
|
||||||
|
## 记录保存
|
||||||
|
|
||||||
|
0.1.1 默认开启每 5 秒保存一次最近 2048 条记录的选项,可在网络面板关闭。文件位于插件配置目录,名称为 capture 加会话时间和随机标识,避免新会话覆盖旧会话。后台写临时文件后原子替换,不在原生回调写盘。
|
||||||
|
|
||||||
|
`/icr export` 可手动导出为 packets.jsonl;也可指定文件名。定时保存仍是滚动快照,不是无限完整日志,崩溃前最后一个保存周期可能丢失。实时数据仍仅有元数据,PayloadComplete=false。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 猛攻与全职业位移名单修复
|
||||||
|
|
||||||
|
版本:0.1.2。目标客户端:国际服 2026.09.15.0000.0000。
|
||||||
|
|
||||||
|
用户反馈:16 开启后位移仍存在,14 增加后突进停在目标圈外。两种现象均使用战士猛攻测试。用户随后要求把所有带位移的技能加入 16 的名单。
|
||||||
|
|
||||||
|
## 名单与位移拦截
|
||||||
|
|
||||||
|
原配置的 BlockedTimelines 为空、BlockListed=true,因此旧实现开启 16 时不屏蔽任何技能。新版从动作表选择 IsPlayerAction=true 且 AffectsPosition=true 的所有条目,通过 AnimationEnd 建立名单。
|
||||||
|
|
||||||
|
本机动作表结果:79 个技能条目,58 个不同 Timeline,均有有效的 16 位映射。包含 PvE、PvP 及仍保留在动作表中的历史条目;不宣称每个条目都仍可从技能栏使用。数据明细见 `movement-skills-2026.09.15.json`。
|
||||||
|
|
||||||
|
| 技能 | 动作 ID | Timeline | 位移标记 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 猛攻 PvE | 7386 | 5189 | true |
|
||||||
|
| 猛攻 PvP | 29079 | 12244 | true |
|
||||||
|
|
||||||
|
首次加载当前客户端版本时自动补全名单,模式切为仅屏蔽名单内,所有模块仍默认关闭。相同版本后续加载保留用户编辑;可点击“加入全部位移技能”重新补全。相同 Timeline 对应的多个技能会联动。
|
||||||
|
|
||||||
|
保留三种明确模式:全部本地技能位移、仅屏蔽名单内、仅放行名单内。名单模式下空名单不屏蔽;另提供技能 ID 转换、技能名展示与最近位移命中统计。
|
||||||
|
|
||||||
|
## 原生路径核对
|
||||||
|
|
||||||
|
旧 NoActionMove 签名唯一匹配 RVA 0x1821FC0,是一个转发函数。它调用共用位移初始化函数 RVA 0x1822510。新版对共用入口建立经唯一性检查的签名,覆盖从其他转发路径进入的同类请求。
|
||||||
|
|
||||||
|
共用入口的参数对应控制器、位移类型、目标、起点、朝向及元数据;控制器 +0x3E0 为所属对象。拦截限定本地角色,未就绪或无效元数据时放行。
|
||||||
|
|
||||||
|
下层 RVA 0x180A370 的 0x14180A5A9、0x14180A5B0 分别读取元数据字节 2、3,0x14180A5B7 读取 word [data]。旧代码把前四字节全部作为 Timeline,会把标志位混入编号。新版按低 16 位读取编号,单独显示两个标志字节。
|
||||||
|
|
||||||
|
函数参数、偏移和签名限于本次审计版本。运行中的猛攻是否进入该入口、是否被成功屏蔽,仍需用界面的本地调用及拦截计数确认。
|
||||||
|
|
||||||
|
## 目标半径与落点
|
||||||
|
|
||||||
|
旧实现全局增加 GetRadius 的返回值,既影响目标,也影响自身和其他调用方。GetActionInRangeOrLoS 在本机 RVA 0x8DD220,明确读取目标与自身半径并参与距离计算;这种全局修改也会影响其他使用半径的移动或碰撞流程。
|
||||||
|
|
||||||
|
新版仅在本地角色的普通技能距离检查中,对正在检查的目标增加半径。使用线程局部作用域;自身、其他对象、未知技能和 AffectsPosition=true 的技能不加值。离开距离检查后立即恢复作用域,其他工作线程不继承。
|
||||||
|
|
||||||
|
因此 14 不再修改显示目标圈、碰撞半径或突进落点,也不给猛攻本身增加目标半径。13 的技能距离设置独立保留。热栏颜色等其他缓存显示是否同步仍待游戏内观察。
|
||||||
|
|
||||||
|
## 测试与记录
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;32 项离线测试通过。新增覆盖位移编号与标志分离、三种名单模式、本地对象限制、半径作用域、线程隔离、异常恢复和玩家技能目录筛选。
|
||||||
|
|
||||||
|
游戏内复测未完成,不把名单完整性等同于所有位移路径均已拦截。
|
||||||
|
|
||||||
|
已读取新生成的自动保存 JSONL,实际包含 2048 条上下行元数据,客户端版本匹配且 PayloadComplete 均为 false。聚合结果见 `capture-summary-second-test.json`;名称来自映射表,不能仅凭元数据验证具体包体含义或动作结果。
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"Source": "capture-018.jsonl",
|
||||||
|
"SHA256": "224AE627F6F6E613662745300C01493E411BDBE02CBA72366CCBD373DD28ECFF",
|
||||||
|
"MapSHA256": "2764186EA528587F15D07569816D413E77E8C38C572E4157787B3092BD6D1DB6",
|
||||||
|
"MapSource": "local cache \u002B \u672C\u673A\u6838\u5B9E PingRsp",
|
||||||
|
"Version": "2026.09.15.0000.0000",
|
||||||
|
"Records": 891,
|
||||||
|
"Up": 534,
|
||||||
|
"Down": 357,
|
||||||
|
"FirstTime": "2026-10-11T10:12:32.1453158+00:00",
|
||||||
|
"LastTime": "2026-10-11T10:13:52.6616441+00:00",
|
||||||
|
"FirstSequence": 1,
|
||||||
|
"LastSequence": 891,
|
||||||
|
"SequenceContinuous": true,
|
||||||
|
"MonotonicOrder": true,
|
||||||
|
"Complete": 891,
|
||||||
|
"Incomplete": 0,
|
||||||
|
"CaptureIssues": 0,
|
||||||
|
"PayloadBytes": 56360,
|
||||||
|
"MinPayload": 8,
|
||||||
|
"MaxPayload": 656,
|
||||||
|
"EmptyPayloads": 0,
|
||||||
|
"Known": 724,
|
||||||
|
"Unknown": 167,
|
||||||
|
"Parsed": 512,
|
||||||
|
"SupportedButRejected": 0,
|
||||||
|
"Groups": [
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 920,
|
||||||
|
"Name": "UP_UpdatePositionHandler",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 494,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 787,
|
||||||
|
"Name": "DOWN_Effect",
|
||||||
|
"Length": 124,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 52,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 671,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 72,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 697,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 558,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 264,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 50,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 516,
|
||||||
|
"Name": "DOWN_ActorControlSelf",
|
||||||
|
"Length": 40,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 48,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 238,
|
||||||
|
"Name": "DOWN_ObjectSpawn",
|
||||||
|
"Length": 64,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 38,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 913,
|
||||||
|
"Name": "UP_ActionSend",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 27,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 524,
|
||||||
|
"Name": "DOWN_NpcSpawn",
|
||||||
|
"Length": 656,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 18,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 974,
|
||||||
|
"Name": "DOWN_ActorDelete",
|
||||||
|
"Length": 8,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 12,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 614,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 10,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 838,
|
||||||
|
"Name": "UP_PingReq",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 338,
|
||||||
|
"Name": "DOWN_PingRsp",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 607,
|
||||||
|
"Name": "DOWN_ActorControl",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 7,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 391,
|
||||||
|
"Name": "UP_ClientTrigger",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 5,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 131,
|
||||||
|
"Name": "DOWN_StatusEffectList",
|
||||||
|
"Length": 384,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 788,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 305,
|
||||||
|
"Name": "DOWN_EffectResult",
|
||||||
|
"Length": 96,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 2,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 214,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 856,
|
||||||
|
"Name": "DOWN_ActorControlTarget",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PingPairs": [
|
||||||
|
{
|
||||||
|
"Request": 3,
|
||||||
|
"Response": 4,
|
||||||
|
"Milliseconds": 83.2735
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 54,
|
||||||
|
"Response": 56,
|
||||||
|
"Milliseconds": 99.9771
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 222,
|
||||||
|
"Response": 224,
|
||||||
|
"Milliseconds": 116.8623
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 372,
|
||||||
|
"Response": 374,
|
||||||
|
"Milliseconds": 83.4204
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 530,
|
||||||
|
"Response": 532,
|
||||||
|
"Milliseconds": 83.4215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 683,
|
||||||
|
"Response": 684,
|
||||||
|
"Milliseconds": 83.2034
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 807,
|
||||||
|
"Response": 808,
|
||||||
|
"Milliseconds": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 870,
|
||||||
|
"Response": 871,
|
||||||
|
"Milliseconds": 83.2435
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Limitations": "No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
{
|
||||||
|
"Source": "capture-018.jsonl",
|
||||||
|
"SHA256": "224AE627F6F6E613662745300C01493E411BDBE02CBA72366CCBD373DD28ECFF",
|
||||||
|
"MapSHA256": "2764186EA528587F15D07569816D413E77E8C38C572E4157787B3092BD6D1DB6",
|
||||||
|
"MapSource": "local cache",
|
||||||
|
"Version": "2026.09.15.0000.0000",
|
||||||
|
"Records": 891,
|
||||||
|
"Up": 534,
|
||||||
|
"Down": 357,
|
||||||
|
"FirstTime": "2026-10-11T10:12:32.1453158+00:00",
|
||||||
|
"LastTime": "2026-10-11T10:13:52.6616441+00:00",
|
||||||
|
"FirstSequence": 1,
|
||||||
|
"LastSequence": 891,
|
||||||
|
"SequenceContinuous": true,
|
||||||
|
"MonotonicOrder": true,
|
||||||
|
"Complete": 891,
|
||||||
|
"Incomplete": 0,
|
||||||
|
"CaptureIssues": 0,
|
||||||
|
"PayloadBytes": 56360,
|
||||||
|
"MinPayload": 8,
|
||||||
|
"MaxPayload": 656,
|
||||||
|
"EmptyPayloads": 0,
|
||||||
|
"Known": 716,
|
||||||
|
"Unknown": 175,
|
||||||
|
"Parsed": 2,
|
||||||
|
"SupportedButRejected": 502,
|
||||||
|
"Groups": [
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 920,
|
||||||
|
"Name": "UP_UpdatePositionHandler",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 494,
|
||||||
|
"Rejection": "UP_UpdatePositionHandler \u957F\u5EA6\u4E0D\u5339\u914D\uFF1A24\uFF0C\u9884\u671F 40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 787,
|
||||||
|
"Name": "DOWN_Effect",
|
||||||
|
"Length": 124,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 52,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 671,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 72,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 697,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 558,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 264,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 50,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 516,
|
||||||
|
"Name": "DOWN_ActorControlSelf",
|
||||||
|
"Length": 40,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 48,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 238,
|
||||||
|
"Name": "DOWN_ObjectSpawn",
|
||||||
|
"Length": 64,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 38,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 913,
|
||||||
|
"Name": "UP_ActionSend",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 27,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 524,
|
||||||
|
"Name": "DOWN_NpcSpawn",
|
||||||
|
"Length": 656,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 18,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 974,
|
||||||
|
"Name": "DOWN_ActorDelete",
|
||||||
|
"Length": 8,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 12,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 614,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 10,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 838,
|
||||||
|
"Name": "UP_PingReq",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": "UP_PingReq \u957F\u5EA6\u4E0D\u5339\u914D\uFF1A32\uFF0C\u9884\u671F 28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 338,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 607,
|
||||||
|
"Name": "DOWN_ActorControl",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 7,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 391,
|
||||||
|
"Name": "UP_ClientTrigger",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 5,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 131,
|
||||||
|
"Name": "DOWN_StatusEffectList",
|
||||||
|
"Length": 384,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 788,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 305,
|
||||||
|
"Name": "DOWN_EffectResult",
|
||||||
|
"Length": 96,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 2,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 214,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 856,
|
||||||
|
"Name": "DOWN_ActorControlTarget",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PingPairs": [],
|
||||||
|
"Limitations": "No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
|
||||||
|
}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
{
|
||||||
|
"Source": "capture-018.jsonl",
|
||||||
|
"SHA256": "224AE627F6F6E613662745300C01493E411BDBE02CBA72366CCBD373DD28ECFF",
|
||||||
|
"MapSHA256": "2764186EA528587F15D07569816D413E77E8C38C572E4157787B3092BD6D1DB6",
|
||||||
|
"MapSource": "local cache \u002B \u672C\u673A\u6838\u5B9E 7 \u9879",
|
||||||
|
"Version": "2026.09.15.0000.0000",
|
||||||
|
"Records": 891,
|
||||||
|
"Up": 534,
|
||||||
|
"Down": 357,
|
||||||
|
"ManagerReplay": {
|
||||||
|
"Matches": true,
|
||||||
|
"Records": 891,
|
||||||
|
"SendReturns": 534,
|
||||||
|
"ObserverErrors": 0,
|
||||||
|
"ReadErrors": 0
|
||||||
|
},
|
||||||
|
"FirstTime": "2026-10-11T10:12:32.1453158+00:00",
|
||||||
|
"LastTime": "2026-10-11T10:13:52.6616441+00:00",
|
||||||
|
"FirstSequence": 1,
|
||||||
|
"LastSequence": 891,
|
||||||
|
"SequenceContinuous": true,
|
||||||
|
"MonotonicOrder": true,
|
||||||
|
"Complete": 891,
|
||||||
|
"Incomplete": 0,
|
||||||
|
"CaptureIssues": 0,
|
||||||
|
"PayloadBytes": 56360,
|
||||||
|
"MinPayload": 8,
|
||||||
|
"MaxPayload": 656,
|
||||||
|
"EmptyPayloads": 0,
|
||||||
|
"Known": 891,
|
||||||
|
"Unknown": 0,
|
||||||
|
"Parsed": 679,
|
||||||
|
"SupportedButRejected": 0,
|
||||||
|
"Groups": [
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 920,
|
||||||
|
"Name": "UP_UpdatePositionHandler",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 494,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 787,
|
||||||
|
"Name": "DOWN_Effect",
|
||||||
|
"Length": 124,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 52,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 671,
|
||||||
|
"Name": "DOWN_UpdateHate",
|
||||||
|
"Length": 72,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 697,
|
||||||
|
"Name": "DOWN_EffectResultBasic",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 558,
|
||||||
|
"Name": "DOWN_UpdateHater",
|
||||||
|
"Length": 264,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 50,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 516,
|
||||||
|
"Name": "DOWN_ActorControlSelf",
|
||||||
|
"Length": 40,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 48,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 238,
|
||||||
|
"Name": "DOWN_ObjectSpawn",
|
||||||
|
"Length": 64,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 38,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 913,
|
||||||
|
"Name": "UP_ActionSend",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 27,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 524,
|
||||||
|
"Name": "DOWN_NpcSpawn",
|
||||||
|
"Length": 656,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 18,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 974,
|
||||||
|
"Name": "DOWN_ActorDelete",
|
||||||
|
"Length": 8,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 12,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 614,
|
||||||
|
"Name": "DOWN_JobGauge",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 10,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 838,
|
||||||
|
"Name": "UP_PingReq",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 338,
|
||||||
|
"Name": "DOWN_PingRsp",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 607,
|
||||||
|
"Name": "DOWN_ActorControl",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 7,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 391,
|
||||||
|
"Name": "UP_ClientTrigger",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 5,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 131,
|
||||||
|
"Name": "DOWN_StatusEffectList",
|
||||||
|
"Length": 384,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 788,
|
||||||
|
"Name": "DOWN_UpdateClassInfo",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 305,
|
||||||
|
"Name": "DOWN_EffectResult",
|
||||||
|
"Length": 96,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 2,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 214,
|
||||||
|
"Name": "DOWN_FirstAttack",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 856,
|
||||||
|
"Name": "DOWN_ActorControlTarget",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PingPairs": [
|
||||||
|
{
|
||||||
|
"Request": 3,
|
||||||
|
"Response": 4,
|
||||||
|
"Milliseconds": 83.2735
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 54,
|
||||||
|
"Response": 56,
|
||||||
|
"Milliseconds": 99.9771
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 222,
|
||||||
|
"Response": 224,
|
||||||
|
"Milliseconds": 116.8623
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 372,
|
||||||
|
"Response": 374,
|
||||||
|
"Milliseconds": 83.4204
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 530,
|
||||||
|
"Response": 532,
|
||||||
|
"Milliseconds": 83.4215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 683,
|
||||||
|
"Response": 684,
|
||||||
|
"Milliseconds": 83.2034
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 807,
|
||||||
|
"Response": 808,
|
||||||
|
"Milliseconds": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 870,
|
||||||
|
"Response": 871,
|
||||||
|
"Milliseconds": 83.2435
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Limitations": "No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# capture-018 首轮完整采样核验与 0.1.9 解析修正
|
||||||
|
|
||||||
|
日期:2026-10-11。用户在国际服 2026.09.15.0000.0000 完成 0.1.8 网络采集并导出 `capture-018.jsonl`。时间范围为上海时间 18:12:32—18:13:52,约 80.5 秒。
|
||||||
|
|
||||||
|
文件 SHA256:`224AE627F6F6E613662745300C01493E411BDBE02CBA72366CCBD373DD28ECFF`。
|
||||||
|
|
||||||
|
本地 Opcode 缓存 SHA256:`2764186EA528587F15D07569816D413E77E8C38C572E4157787B3092BD6D1DB6`。未修改用户导出文件或映射缓存。报告与测试不保存原始坐标、对象 ID 或完整载荷。
|
||||||
|
|
||||||
|
## 采集结果
|
||||||
|
|
||||||
|
| 检查 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 记录数 | 891:上行 534,下行 357 |
|
||||||
|
| 完整标记 | 891 完整,0 仅元数据,0 CaptureIssue |
|
||||||
|
| 序号 | 1—891 连续;单调时间顺序正常 |
|
||||||
|
| 载荷 | 合计 56,360 字节,单条 8—656 字节,无空载荷 |
|
||||||
|
| 客户端版本 | 全部匹配当前目标版本 |
|
||||||
|
|
||||||
|
36 的首轮完整采样通过文件核验。完整标记还通过已知包长度、当前原生发送构造及回显关系交叉核对。导出不含未形成记录的头部读取失败计数,不能据 CaptureIssue=0 宣称所有未记录调用都无异常;长期性能、反复启停、切图和卸载仍需独立验收。
|
||||||
|
|
||||||
|
## 暴露出的旧解析问题
|
||||||
|
|
||||||
|
0.1.8 解析器回放仅 2 条返回 Parsed=true(效果结果计数),502 条已有解析器的记录因长度不符被拒绝;其余为尚未接入或名称未知。两类不匹配为:
|
||||||
|
|
||||||
|
- 494 个普通移动包,Opcode 920,实际载荷 24 字节,旧 .cs 声明 40 字节。
|
||||||
|
- 8 个 Ping 请求,Opcode 838,实际载荷 32 字节,旧 .cs 声明 28 字节。
|
||||||
|
|
||||||
|
这是旧解析结构的问题,不是新采集随意截断或补齐载荷。原生依据:
|
||||||
|
|
||||||
|
- 普通移动入口 RVA 0x62B680。0x62B6CD 写 Opcode 0x398(920);0x62B6D7 写消息长度 0x28。按已核实的发送长度契约减去 0x10,业务载荷确为 0x18。位置字段仍从业务载荷 +8 开始。
|
||||||
|
- Ping 构造 RVA 0x1D02776 写 Opcode 0x346(838),0x1D0279C 写消息长度 0x30;业务载荷为 0x20。业务偏移 0 是本机毫秒计时值,偏移 4 来自网络模块的上一轮往返计时字段。
|
||||||
|
|
||||||
|
0.1.9 仅对完整客户端版本 2026.09.15.0000.0000 使用这些新长度。其他版本/无版本的旧离线调用保持旧行为,不将本次结论跨版本套用。
|
||||||
|
|
||||||
|
## Ping 响应映射与语义
|
||||||
|
|
||||||
|
原缓存缺少 DOWN_PingRsp。下行 338(0x152)共 8 条、均 32 字节,业务偏移 0 与 8 条 Ping 请求逐一回显匹配。
|
||||||
|
|
||||||
|
原生 NetworkModule 在 0x1D0223A 以 0x152 选择分支;0x1D022E1 从 IPC 缓冲区 +0x10(业务偏移 0)读回显计时,0x1D022E7 与当前毫秒计时相减,0x1D022F2 存回网络往返字段。此证据支持本版本 DOWN_PingRsp=338。
|
||||||
|
|
||||||
|
响应业务偏移 4 在本次样本中均为 333;原生在 0x1D02307 起将其作为另一路参数使用,不能将它标作 RTT。界面改为未解释的 field04,RTT 仍按请求/响应回显匹配和采集时间差计算。
|
||||||
|
|
||||||
|
0.1.9 在有效非空映射加载后,仅当版本匹配、未已有 DOWN_PingRsp 名称且 338 未被占用时补充本地映射,来源注明“本机核实 PingRsp”。不覆盖显式映射,也不改写缓存 JSON。
|
||||||
|
|
||||||
|
8 组记录配对的往返时间范围 83.2034—116.8623 ms,平均 91.6752 ms。这是采集点观察值,包含接收调度时间,不宣称是纯网络传输延迟。
|
||||||
|
|
||||||
|
## 修正后的同文件回放
|
||||||
|
|
||||||
|
| 项目 | 0.1.8 | 0.1.9 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 有名称 | 716 | 724 |
|
||||||
|
| 无名称 | 175 | 167 |
|
||||||
|
| Parsed=true | 2 | 512 |
|
||||||
|
| 已有解析器但拒绝 | 502 | 0 |
|
||||||
|
| Ping 配对 | 0 | 8 |
|
||||||
|
|
||||||
|
512 条包括普通移动 494、Ping 请求 8、Ping 响应 8、历史效果结果计数 2;后者仍只是有限计数检查,不能将 Parsed=true 理解为所有字段均被完整解释。724 条有名称记录中,另 212 条尚无已接入解析器。
|
||||||
|
|
||||||
|
剩余未知下行如下,先保留原始数据,不凭相近数值或长度猜名称:
|
||||||
|
|
||||||
|
| Opcode | 长度 | 数量 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 671 | 72 | 51 |
|
||||||
|
| 697 | 24 | 51 |
|
||||||
|
| 558 | 264 | 50 |
|
||||||
|
| 614 | 16 | 10 |
|
||||||
|
| 788 | 16 | 4 |
|
||||||
|
| 214 | 16 | 1 |
|
||||||
|
|
||||||
|
下一步优先核对高频未知 Opcode 与状态/动作结果的当前原生分派路径;8 的状态屏蔽仍不因取得样本就直接启用。
|
||||||
|
|
||||||
|
## 可复现验证
|
||||||
|
|
||||||
|
`Tools/CaptureAudit` 使用工程实际 PacketFiles、OpcodeMap、PacketParser 和 PacketCorrelator 生成只含汇总信息的报告。修正前后结果分别保存在 `2026-10-11-capture-018-before.json`、`2026-10-11-capture-018-after.json`。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;60 项离线测试通过,新增版本长度隔离、映射冲突不覆盖、回显与完整性约束。实际文件回放另验证 891 条记录,不把离线测试条数作为现场功能数。
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"Source": "capture-018.jsonl",
|
||||||
|
"SHA256": "224AE627F6F6E613662745300C01493E411BDBE02CBA72366CCBD373DD28ECFF",
|
||||||
|
"MapSHA256": "2764186EA528587F15D07569816D413E77E8C38C572E4157787B3092BD6D1DB6",
|
||||||
|
"MapSource": "local cache \u002B \u672C\u673A\u6838\u5B9E 4 \u9879",
|
||||||
|
"Version": "2026.09.15.0000.0000",
|
||||||
|
"Records": 891,
|
||||||
|
"Up": 534,
|
||||||
|
"Down": 357,
|
||||||
|
"FirstTime": "2026-10-11T10:12:32.1453158+00:00",
|
||||||
|
"LastTime": "2026-10-11T10:13:52.6616441+00:00",
|
||||||
|
"FirstSequence": 1,
|
||||||
|
"LastSequence": 891,
|
||||||
|
"SequenceContinuous": true,
|
||||||
|
"MonotonicOrder": true,
|
||||||
|
"Complete": 891,
|
||||||
|
"Incomplete": 0,
|
||||||
|
"CaptureIssues": 0,
|
||||||
|
"PayloadBytes": 56360,
|
||||||
|
"MinPayload": 8,
|
||||||
|
"MaxPayload": 656,
|
||||||
|
"EmptyPayloads": 0,
|
||||||
|
"Known": 876,
|
||||||
|
"Unknown": 15,
|
||||||
|
"Parsed": 664,
|
||||||
|
"SupportedButRejected": 0,
|
||||||
|
"Groups": [
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 920,
|
||||||
|
"Name": "UP_UpdatePositionHandler",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 494,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 787,
|
||||||
|
"Name": "DOWN_Effect",
|
||||||
|
"Length": 124,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 52,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 671,
|
||||||
|
"Name": "DOWN_UpdateHate",
|
||||||
|
"Length": 72,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 697,
|
||||||
|
"Name": "DOWN_EffectResultBasic",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 558,
|
||||||
|
"Name": "DOWN_UpdateHater",
|
||||||
|
"Length": 264,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 50,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 516,
|
||||||
|
"Name": "DOWN_ActorControlSelf",
|
||||||
|
"Length": 40,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 48,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 238,
|
||||||
|
"Name": "DOWN_ObjectSpawn",
|
||||||
|
"Length": 64,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 38,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 913,
|
||||||
|
"Name": "UP_ActionSend",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 27,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 524,
|
||||||
|
"Name": "DOWN_NpcSpawn",
|
||||||
|
"Length": 656,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 18,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 974,
|
||||||
|
"Name": "DOWN_ActorDelete",
|
||||||
|
"Length": 8,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 12,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 614,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 10,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 838,
|
||||||
|
"Name": "UP_PingReq",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 338,
|
||||||
|
"Name": "DOWN_PingRsp",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 607,
|
||||||
|
"Name": "DOWN_ActorControl",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 7,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 391,
|
||||||
|
"Name": "UP_ClientTrigger",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 5,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 131,
|
||||||
|
"Name": "DOWN_StatusEffectList",
|
||||||
|
"Length": 384,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 788,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 305,
|
||||||
|
"Name": "DOWN_EffectResult",
|
||||||
|
"Length": 96,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 2,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 214,
|
||||||
|
"Name": null,
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 856,
|
||||||
|
"Name": "DOWN_ActorControlTarget",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PingPairs": [
|
||||||
|
{
|
||||||
|
"Request": 3,
|
||||||
|
"Response": 4,
|
||||||
|
"Milliseconds": 83.2735
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 54,
|
||||||
|
"Response": 56,
|
||||||
|
"Milliseconds": 99.9771
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 222,
|
||||||
|
"Response": 224,
|
||||||
|
"Milliseconds": 116.8623
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 372,
|
||||||
|
"Response": 374,
|
||||||
|
"Milliseconds": 83.4204
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 530,
|
||||||
|
"Response": 532,
|
||||||
|
"Milliseconds": 83.4215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 683,
|
||||||
|
"Response": 684,
|
||||||
|
"Milliseconds": 83.2034
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 807,
|
||||||
|
"Response": 808,
|
||||||
|
"Milliseconds": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 870,
|
||||||
|
"Response": 871,
|
||||||
|
"Milliseconds": 83.2435
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Limitations": "No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"Source": "capture-018.jsonl",
|
||||||
|
"SHA256": "224AE627F6F6E613662745300C01493E411BDBE02CBA72366CCBD373DD28ECFF",
|
||||||
|
"MapSHA256": "2764186EA528587F15D07569816D413E77E8C38C572E4157787B3092BD6D1DB6",
|
||||||
|
"MapSource": "local cache \u002B \u672C\u673A\u6838\u5B9E 7 \u9879",
|
||||||
|
"Version": "2026.09.15.0000.0000",
|
||||||
|
"Records": 891,
|
||||||
|
"Up": 534,
|
||||||
|
"Down": 357,
|
||||||
|
"FirstTime": "2026-10-11T10:12:32.1453158+00:00",
|
||||||
|
"LastTime": "2026-10-11T10:13:52.6616441+00:00",
|
||||||
|
"FirstSequence": 1,
|
||||||
|
"LastSequence": 891,
|
||||||
|
"SequenceContinuous": true,
|
||||||
|
"MonotonicOrder": true,
|
||||||
|
"Complete": 891,
|
||||||
|
"Incomplete": 0,
|
||||||
|
"CaptureIssues": 0,
|
||||||
|
"PayloadBytes": 56360,
|
||||||
|
"MinPayload": 8,
|
||||||
|
"MaxPayload": 656,
|
||||||
|
"EmptyPayloads": 0,
|
||||||
|
"Known": 891,
|
||||||
|
"Unknown": 0,
|
||||||
|
"Parsed": 679,
|
||||||
|
"SupportedButRejected": 0,
|
||||||
|
"Groups": [
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 920,
|
||||||
|
"Name": "UP_UpdatePositionHandler",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 494,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 787,
|
||||||
|
"Name": "DOWN_Effect",
|
||||||
|
"Length": 124,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 52,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 671,
|
||||||
|
"Name": "DOWN_UpdateHate",
|
||||||
|
"Length": 72,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 697,
|
||||||
|
"Name": "DOWN_EffectResultBasic",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 558,
|
||||||
|
"Name": "DOWN_UpdateHater",
|
||||||
|
"Length": 264,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 50,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 516,
|
||||||
|
"Name": "DOWN_ActorControlSelf",
|
||||||
|
"Length": 40,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 48,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 238,
|
||||||
|
"Name": "DOWN_ObjectSpawn",
|
||||||
|
"Length": 64,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 38,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 913,
|
||||||
|
"Name": "UP_ActionSend",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 27,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 524,
|
||||||
|
"Name": "DOWN_NpcSpawn",
|
||||||
|
"Length": 656,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 18,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 974,
|
||||||
|
"Name": "DOWN_ActorDelete",
|
||||||
|
"Length": 8,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 12,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 614,
|
||||||
|
"Name": "DOWN_JobGauge",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 10,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 838,
|
||||||
|
"Name": "UP_PingReq",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 338,
|
||||||
|
"Name": "DOWN_PingRsp",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 607,
|
||||||
|
"Name": "DOWN_ActorControl",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 7,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 391,
|
||||||
|
"Name": "UP_ClientTrigger",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 5,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 131,
|
||||||
|
"Name": "DOWN_StatusEffectList",
|
||||||
|
"Length": 384,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 788,
|
||||||
|
"Name": "DOWN_UpdateClassInfo",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 305,
|
||||||
|
"Name": "DOWN_EffectResult",
|
||||||
|
"Length": 96,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 2,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 214,
|
||||||
|
"Name": "DOWN_FirstAttack",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 856,
|
||||||
|
"Name": "DOWN_ActorControlTarget",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": false
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PingPairs": [
|
||||||
|
{
|
||||||
|
"Request": 3,
|
||||||
|
"Response": 4,
|
||||||
|
"Milliseconds": 83.2735
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 54,
|
||||||
|
"Response": 56,
|
||||||
|
"Milliseconds": 99.9771
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 222,
|
||||||
|
"Response": 224,
|
||||||
|
"Milliseconds": 116.8623
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 372,
|
||||||
|
"Response": 374,
|
||||||
|
"Milliseconds": 83.4204
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 530,
|
||||||
|
"Response": 532,
|
||||||
|
"Milliseconds": 83.4215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 683,
|
||||||
|
"Response": 684,
|
||||||
|
"Milliseconds": 83.2034
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 807,
|
||||||
|
"Response": 808,
|
||||||
|
"Milliseconds": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 870,
|
||||||
|
"Response": 871,
|
||||||
|
"Milliseconds": 83.2435
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Limitations": "No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 状态与原生标识核对
|
||||||
|
|
||||||
|
日期:2026 年 10 月 11 日。客户端:国际服 2026.09.15.0000.0000。修订版本:0.1.3。
|
||||||
|
|
||||||
|
## 各功能的判断依据
|
||||||
|
|
||||||
|
| 编号 | 标识或判断来源 | 本轮结果 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 和 2 | 本地玩家 StatusId,用于减速保护 | 旧 37 项存在;补入当前数据中三个新的玩家 Heavy 状态,现为 40 项 |
|
||||||
|
| 3 | 原生 Permission 检查编号 | 96—99 属于 Permission 行号;1001 等为该函数特殊分支编号,不是 StatusId |
|
||||||
|
| 4 | 本地对象地址及击退参数 | 不依赖状态名单;本轮未进行实际击退验收 |
|
||||||
|
| 5 | 掉落标志位掩码 | 不属于技能或状态 ID |
|
||||||
|
| 6 | 原生掉落检查返回值 | 不依赖状态名单;实际效果仍待验证 |
|
||||||
|
| 7 | 本地角色的指定强制坐标移动入口 | 不按状态 ID 筛选,不删除状态;不能据此宣称解除所有眩晕、禁锢或控制 |
|
||||||
|
| 8 | 旧版 StatusId 名单 | 发现 6 项与原注释分组不符;新版仍没有启用状态屏蔽 |
|
||||||
|
| 10 和 11 | 深层迷宫类型、层数和 TerritoryType 行号 | 1108 为 Eureka Orthos、1290 为 Pilgrim's Traverse,均为用途 31 |
|
||||||
|
| 13 | Action 行号、位移属性和例外技能 | 已核对四个例外 ID 及 Guardian 29066,均为技能 ID |
|
||||||
|
| 14 | 距离检查中的目标对象地址和 Action 位移属性 | 不通过状态 ID 改圈;保留 0.1.2 的作用域限制 |
|
||||||
|
| 16 | Action.AnimationEnd 映射的 16 位 Timeline,加独立标志字节 | 保留 0.1.2 的全职业位移名单与共用入口修复 |
|
||||||
|
| 17 和 18 | ActionType、ActionId、冷却组索引 | 天地人 6 个 ActionId 均匹配;同步函数的 57 是零起始冷却组索引 |
|
||||||
|
| 19 | ActionId 与咏唱时长 | 技能编号与浮点时长分别处理,不属于状态名单 |
|
||||||
|
| 20 | PVP 场景及动画锁浮点值 | 不按技能或状态 ID 筛选 |
|
||||||
|
| 36 和 37 | 方向、版本、ushort Opcode | 网络消息类型标识,与上述 ID 空间分开 |
|
||||||
|
|
||||||
|
## 旧状态名单的问题
|
||||||
|
|
||||||
|
核对本机 Status 表的 Name、Description、LockMovement、LockControl 和 LockActions 后发现:
|
||||||
|
|
||||||
|
| ID | 原分组 | 当前名称与含义 | 处理 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 142 | 滑冰 | Stun,眩晕 | 从已审候选排除 |
|
||||||
|
| 149 | 滑冰 | Stun,眩晕 | 从已审候选排除 |
|
||||||
|
| 2538 | 方向命令 | Strong of Shield,减伤及反射相关 | 从已审候选排除 |
|
||||||
|
| 2539 | 方向命令 | Stronger Together,减伤及反射相关 | 从已审候选排除 |
|
||||||
|
| 2540 | 方向命令 | Freezing Up,持续移动以避免冻结的机制状态 | 从已审候选排除 |
|
||||||
|
| 2541 | 方向命令 | 不浄の侵食,倒计时后转为强制远离的前置状态 | 从已审候选排除 |
|
||||||
|
|
||||||
|
这是当前数据与原注释分组不符,不代表这些 ID 曾在某个历史版本一定对应另一种状态。旧候选仍单独保留用于审计,已审候选保留 38 项;没有因此启用编号 8。
|
||||||
|
|
||||||
|
当前 SDK 中 StatusManager 含 60 个状态条目,每个客户端 Status 为 16 字节,StatusId 字段为 ushort。Owner 字段可能为空。不能把原资料中网络包的 30 条、12 字节条目直接用作客户端状态结构,也不能只修改总容量后就认为完成适配。
|
||||||
|
|
||||||
|
## 减速保护
|
||||||
|
|
||||||
|
旧 37 个条目在本机数据中都存在。按当前 Status 名称与说明发现三项新的玩家加重状态:5044、5175、5356,说明均为移动速度降低,已加入保护集合。
|
||||||
|
|
||||||
|
630 的说明是 Chocobo speed is reduced,归属陆行鸟,不自动纳入这份玩家状态补充。此检查不宣称覆盖全部未来状态。
|
||||||
|
|
||||||
|
重度减速并不一定设置 LockMovement。当前的 Heavy、Leaden 等条目该标志为 false,因此无法只依赖一个通用锁定标志实现完整减速识别。
|
||||||
|
|
||||||
|
名称模糊匹配也不适合自动建立屏蔽名单:例如 Fearless 和 Honeyed Charms 不等于恐惧或魅惑控制。StatusAudit 的名称检索仅用于产生审核候选,不会用于运行时自动删除状态。
|
||||||
|
|
||||||
|
## 强制移动入口
|
||||||
|
|
||||||
|
原签名在本机唯一命中 RVA 0x194AEC3 的调用,目标为 RVA 0x90BA80。调用方在栈参数位置写入 float,目标函数也以 movss 读取第五参数,因此将旧 int 声明修正为 float。返回寄存器透传行为保持,未凭这一调用方忽略返回值就假定所有调用都没有返回值。
|
||||||
|
|
||||||
|
该入口接受角色、坐标、浮点运动参数和选项结构;没有直接查 StatusId。运行时仅比较本地角色地址并拦截这条路径。它可能承载不止魅惑或恐惧一种强制移动,不应宣称为所有控制状态的通用解除器。已增加当前审计版本限制及界面说明。
|
||||||
|
|
||||||
|
## Permission 与冷却组
|
||||||
|
|
||||||
|
移动权限签名匹配 RVA 0xE16130,调用 RVA 0x835B50 取得数据。后者传入表号 0x14A;本机 root.exl 将该表号映射为 Permission。
|
||||||
|
|
||||||
|
复唱同步签名匹配 RVA 0x8EF260。第二参数传给 RVA 0x8E8300,在该函数中作为 int 组索引计算 RecastDetail 地址;第三参数与结构中的动作编号比较。已将参数明确命名为 manager、recastGroup、actionId、maximum,保留原返回值。
|
||||||
|
|
||||||
|
本机 Action 表确认:2259、18805 为 Ten,2261、18806 为 Chi,2263、18807 为 Jin。它们的 Timeline 分别为 233、234、235,不能将 Timeline 当作 ActionId 使用。
|
||||||
|
|
||||||
|
## 文件和验证
|
||||||
|
|
||||||
|
- `status-id-audit-2026.09.15.json`:状态名称、说明和标志核对数据。
|
||||||
|
- `action-id-audit-2026.09.15.json`:结印及距离例外技能核对数据。
|
||||||
|
- `Core/StatusCatalog.cs`:旧候选、已审候选及减速保护分开保存。
|
||||||
|
- `Core/NativeIds.cs`:Permission 编号、技能 ID、冷却组索引分开命名。
|
||||||
|
- `Tools/StatusAudit`、`Tools/NativeAudit`:本机游戏数据和磁盘可执行文件只读审计。
|
||||||
|
|
||||||
|
0.1.3 的原生参数声明调整及新增减速保护仍待游戏内复测。编号 8 缺少原生签名和网络布局依据,继续保持不可用。
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 0.1.8 完整游戏载荷采集
|
||||||
|
|
||||||
|
基线:国际服 2026.09.15.0000.0000,本机 ffxiv_dx11.exe;实现限定此完整版本。前期线索见 network-length-research.md。本次原生检查只读磁盘文件,不调用原生函数;现场验收尚未完成。
|
||||||
|
|
||||||
|
## 上行证据
|
||||||
|
|
||||||
|
上行签名唯一匹配 RVA 0x1D718C0,ABI 为 byte(connection, data, arg, byte flag)。data 是发送对象,Opcode 在 +0,消息长度为 +8 的 uint64,业务载荷在 +0x20。
|
||||||
|
|
||||||
|
- 0x1D7198C:从 [rsi+8] 取长度,源指针 rsi+0x20,长度减 0x10,调用 0x1DA6CB0。
|
||||||
|
- 0x1D71A84:另一分支执行相同的复制区间。
|
||||||
|
- 0x1DA6CB0:按 r8 长度分派复制;0x1DA6DD0 的 16—32 字节分支从 rdx、rdx+r8-0x10 读取并写入 rcx、rcx+r8-0x10,确认其复制含义。
|
||||||
|
- 0x1CFD1D0 的发送对象创建例:Opcode 在对象 +0,uint64 长度 0x18 在 +8,经 ZoneClient.SendPacket(0x1D03040)进入上述函数。复制的业务长度为 8。
|
||||||
|
|
||||||
|
采集发生在发送原函数之前,只表示进入发送路径,不代表发送成功或服务端接收。
|
||||||
|
|
||||||
|
## 下行证据与有效期
|
||||||
|
|
||||||
|
调用链:NetworkModule 0x1D02040 → ZoneClient.ReceivePacket 0x1D03020 → ZoneReceive 0x1D71440。后者 ABI 为 byte(connection, holder),返回值非零表示接收成功。
|
||||||
|
|
||||||
|
- 0x1D71464—0x1D71468:将消息对象写入 holder+0x10。
|
||||||
|
- 0x1D714D7—0x1D714E0:从 holder+0x10 取对象,再从对象+0x38 取 IPC 缓冲区。
|
||||||
|
- 对象+0x30 为 uint64 消息长度。接收构造路径 0x1D68875—0x1D6889E 从外层消息长度减去 0x10,按此大小复制到对象+0x38,并将相同大小写入对象+0x30。它包含 0x10 字节 IPC 头。
|
||||||
|
- 0x20EF800 读取缓冲区+2 的 Opcode,其各分支从缓冲区+0x10 取得业务载荷。
|
||||||
|
- NetworkModule 0x1D021AD 从对象+0x20 取得目标标识;保留位范围按原函数标准化,不能把对象+0x24 的其他值擅自标为 Source。
|
||||||
|
- 0x1D02203—0x1D02220 将 IPC 缓冲区交给 Application 接收回调;0x482440 转发到 PacketDispatcher 0x18D9500。
|
||||||
|
- 0x1D02366—0x1D02370 在分派后释放消息,下一次接收前清空 holder+0x10。Hook 在接收原函数成功返回后立即复制,尚未返回给上述调用者,不保存原生指针供后台使用。
|
||||||
|
|
||||||
|
下行签名的栈保存偏移 0x18 必须保留。将其通配后同时匹配 0x1D20BF0(另一接收入口)和 0x1D71440。最终完整签名在当前文件仅匹配一次:
|
||||||
|
|
||||||
|
```text
|
||||||
|
48 89 5C 24 18 57 48 83 EC 20 48 83 B9 88 00 00 00 00 48 8B FA 48 8B D9
|
||||||
|
```
|
||||||
|
|
||||||
|
下行不再挂接 PacketDispatcher,不重复记录同一包,也不从 IPC 指针之前猜读外层头部。
|
||||||
|
|
||||||
|
## 读取与失败行为
|
||||||
|
|
||||||
|
- 使用 Dalamud.SafeMemory.ReadBytes(当前进程 ReadProcessMemory)复制;没有使用旧结构大小推断原始指针长度。
|
||||||
|
- 先验证指针、头部读取和 uint64 长度,长度减 0x10 前检查下限及上限,单载荷最多 65536 字节。
|
||||||
|
- 完整读取才标记 PayloadComplete=true;确实零字节的合法载荷可以完整,不等于失败。
|
||||||
|
- 已有 Opcode 的异常长度、截断或复制失败保留仅元数据及 CaptureIssue;头部/对象失败不能可靠获得 Opcode,记录异常计数及最近原因,不伪造包。
|
||||||
|
- 暂停或方向过滤可避免读取对象,Opcode 过滤在读取载荷之前执行。
|
||||||
|
- 两个原函数都保持一次调用及原参数/返回值;发送在采集后调用,接收先调用再读取成功结果。采集托管异常不会替代游戏原函数结果。
|
||||||
|
- 记录通过复制成为托管数组;缓冲区按 2048 条和 8 MiB 载荷双重限制,淘汰最旧记录。后台保存只使用这些副本。
|
||||||
|
- 只覆盖当前版本 Zone 通道。聊天、登录和录像回放通道未挂接。上行 Source/Target、下行 Source 没有足够依据,保持 null。
|
||||||
|
|
||||||
|
## 结果与测试
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;57 项离线测试通过。新增模拟有界内存场景覆盖精确复制区间、对象引用链、目标标准化、恶意 uint64 长度、截断/部分读取、空载荷、64 KiB 上限、无效指针、过滤、错误原因导入导出和缓冲字节预算。
|
||||||
|
|
||||||
|
一次新增导出测试使用系统临时目录时受到沙箱文件移动限制;已改为与既有测试一致的测试输出目录,完整构建和测试重跑通过。此修正没有放宽插件文件路径或权限策略。
|
||||||
|
|
||||||
|
现场验证:重新加载 0.1.8,开启 36、37,方向全部、Opcode -1;正常移动、使用技能 20—30 秒。观察完整上下行计数是否增长、异常是否出现,再 `/icr export capture-018.jsonl` 并关闭 36。以导出样本核验真实载荷和已有解析,之后再扩展 37。
|
||||||
|
|
||||||
|
本版完成采集代码接入与离线边界验证,不把 36 标为游戏内已验证,也不据此宣称 37 的历史解析全部正确。
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 当前客户端网络长度来源:第一轮核对
|
||||||
|
|
||||||
|
客户端:2026.09.15.0000.0000。2026-10-11 继续开发时,只读检查本机 ffxiv_dx11.exe;没有访问游戏进程,也未改动 0.1.7 的采集 Hook。
|
||||||
|
|
||||||
|
## 上行已发现长度来源
|
||||||
|
|
||||||
|
现有 ZoneUp 签名唯一匹配 RVA 0x1D718C0。参数:rcx 为连接,rdx 为发送对象(保存到 rsi),r8 为附加参数指针,r9b 为标志。
|
||||||
|
|
||||||
|
正常状态分支在 RVA 0x1D7198C 附近:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mov r8,[rsi+8]
|
||||||
|
lea rdx,[rsi+20h]
|
||||||
|
sub r8,10h
|
||||||
|
add rcx,10h
|
||||||
|
call 0x141DA6CB0
|
||||||
|
```
|
||||||
|
|
||||||
|
另一发送分支 0x1D71A84 使用相同的源指针及长度运算。这支持当前版本发送对象的实际复制范围为 `[data+0x20, data+0x20+(u64[data+8]-0x10))`,与旧事件给出的载荷起点 data+0x20 一致;长度不是按 Opcode 历史结构大小推算。
|
||||||
|
|
||||||
|
此前 0x1D71942 也把 `[rsi+8]` 传给 0x20F6200 的消息构造流程。唯一直接调用者位于 0x1D03040(ZoneClient.SendPacket 包装),0x1D03065 调用该发送函数。
|
||||||
|
|
||||||
|
下一步仍需核实复制辅助函数 0x1DA6CB0 的语义、发送对象创建时的长度契约,并为下限 0x10、最大采集量、版本门控、异常长度回退设计有界读取。此处记录原生证据,不等同于完整采集功能已完成或实测。
|
||||||
|
|
||||||
|
## 下行仍缺边界契约
|
||||||
|
|
||||||
|
PacketDispatcher 构造签名唯一匹配 0x18D9466,第一虚表为 RVA 0x23129E8,slot 1 指向 OnReceivePacket RVA 0x18D9500。函数开头读取 r8+2 的 ushort Opcode,r8 为现有下行 data,edx 为目标 ID。
|
||||||
|
|
||||||
|
ECommons 兼容代码使用载荷起点 data+0x10,并曾取 data-0x10 作为外层头部。这里尚未核实外层对象长度的来源和可读边界,不能仅凭历史包头布局在 data 前反向读取并复制。
|
||||||
|
|
||||||
|
下一步从 Application PacketReceiver 回调向下行分派调用追踪外层消息对象的长度,优先寻找明确携带长度的入口。
|
||||||
|
|
||||||
|
## 8 状态处理线索
|
||||||
|
|
||||||
|
本地 FFXIVClientStructs 的 IDA 标识表提供 HandleStatusEffectListPlayerPacket、HandleStatusEffectListPlayerDoublePacket、HandleStatusEffectListPacket、Eureka、Bozja、Double 等分类入口。这些源码地址不是本机已验证地址,不能直接挂接。现有 StatusManager 则是 60 个 16 字节的客户端状态项,与旧网络列表不是同一结构。
|
||||||
|
|
||||||
|
8 仍未启用;后续结合当前 dispatcher 分支、完整下行长度和动作表核对各状态列表入口,不能把某一类 30 项布局套到所有列表。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 0.1.13:统一收发观察与独立日志订阅
|
||||||
|
|
||||||
|
日期:2026 年 10 月 11 日。用户将本轮工作切到收发包管理器。交付 M1 的只读管理器与日志接入;M4 主动发送、编码器、排队和请求响应事务仍未实现。
|
||||||
|
|
||||||
|
## 已接入的能力
|
||||||
|
|
||||||
|
- `Runtime/NetworkManager.cs` 管理原有两个 Zone Hook,复用 SharedModuleResource。第一个模块订阅启动 Hook,最后一个模块订阅释放 Hook;关闭 36 只释放记录器自己的订阅,其他模块可以独立持有管理器。
|
||||||
|
- `Core/NativePacketCapture.cs` 继续负责原有原生长度契约和有界复制,本轮未修改签名、ABI、载荷边界、读取时点、版本限制和原函数参数。
|
||||||
|
- `Core/NetworkPipeline.cs` 提供 Received、BeforeSend、AfterSend 三种只读观察。每个订阅可独立限定 Opcode,并通过 IDisposable 取消;一个订阅抛异常不会阻断后续订阅或改变原函数调用次数与返回值。
|
||||||
|
- `Core/PacketRecorder.cs` 将消息写入现有 PacketBuffer。36 保留收包和发送入口两类记录,不额外把发送后事件写成第二条上行记录;暂停、方向过滤和 Opcode 过滤仅作用于记录器。
|
||||||
|
- 网络面板显示订阅数、收包/发送入口/发送返回的观察计数、订阅异常及最近原生发送返回码。日志暂停时观察仍继续,因此暂停不再等于停止网络内存复制;关闭 36 且无其他消费者时才释放 Hook。
|
||||||
|
|
||||||
|
## 接口契约
|
||||||
|
|
||||||
|
模块在自己的生命周期入口调用,例如:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
network.Observe(scope, NetworkPhase.Received, observation =>
|
||||||
|
{
|
||||||
|
var message = observation.Message;
|
||||||
|
// message.Payload is ReadOnlySpan<byte>; only copy/process owned data here.
|
||||||
|
}, opcode: 338);
|
||||||
|
```
|
||||||
|
|
||||||
|
订阅的建立和释放与现有模块启停一样在框架生命周期代码中执行,不能从原生回调中释放 Hook。回调在原生调用线程同步执行,同一消息同一阶段按注册顺序回调,多个原生线程之间不承诺全局顺序。回调不得操作线程绑定游戏/UI 服务、等待 Framework、主动发送或阻塞;耗时任务由消费者基于自持有数据安排。
|
||||||
|
|
||||||
|
上行流程:在发送入口复制一次 → BeforeSend → 原函数恰好一次 → AfterSend。后者包含同一个 NetworkMessage 和原生 byte 返回码;发送前捕获的订阅集合用于两个阶段,发送中途新增的订阅从下次调用开始。原函数抛出时不重试、不伪造 AfterSend。原生返回码未解释为服务端确认。
|
||||||
|
|
||||||
|
下行流程:原函数先执行一次,返回非零后才读取 holder → Received;返回零时不读消息。原生读取失败仍保留既有元数据降级或错误计数。捕获和订阅异常处理不包裹原函数,避免异常路径重复调用。
|
||||||
|
|
||||||
|
每条 NetworkMessage 自持有不可变载荷副本,外部只有只读 Span,不能取得原生指针或底层可写数组;发送前后共享相同观察 ID 和消息实例。消费者可保留消息,不依赖原生发送对象或接收 holder 的生命周期。无对应观察订阅时跳过复制;有 Opcode 限定时在读头后筛选。
|
||||||
|
|
||||||
|
取消后不再进入该订阅的后续分发;已经通过活动检查、开始执行的回调可能结束在取消之后,取消不会强行中止它。释放过程中不持有订阅锁调用消费者;一个回调取消其他尚未执行的订阅时,后者会跳过。插件先释放所有模块作用域再关闭分发器。
|
||||||
|
|
||||||
|
## 记录兼容与来源
|
||||||
|
|
||||||
|
PacketRecord 新增可选 Context,包含观察 ID、NativeZone 来源、RawIpcPayload 阶段、观察阶段与可用的原生返回码。日志时间使用捕获快照的时间,仍保留独立的缓冲区记录序号。旧 JSONL 缺少 Context 时为 null,界面明确显示阶段未知,不把旧数据伪标为转换后状态。
|
||||||
|
|
||||||
|
NativeZone 表示采集入口,不保证实际发送者是游戏本体:其他插件经同一原生入口发送的数据也可能出现。当前管理器不主动发送,也未加入来源归因。RawIpcPayload 是排除 IPC 头的原始游戏载荷,不代表状态 ID 等字段已经转换。编号 8 继续使用自己的转换后 SetStatus 入口。
|
||||||
|
|
||||||
|
普通日志仍记录 BeforeSend,因此上行记录里的原生返回码为空;发送后的原始返回码通过 AfterSend 订阅和面板提供。下行记录可以附带接收返回码。观察 ID 只在本次插件实例内关联,不是服务器请求 ID,也不能跨重载用于业务匹配。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;77 项离线测试通过。新增七组检查针对调用顺序/次数、复制寿命、原生返回码、捕获与订阅异常、接收失败时不读取、日志过滤与独立订阅、取消/重复订阅/卸载、共享 Hook 持有者和新记录字段往返。
|
||||||
|
|
||||||
|
复用原 capture-018.jsonl:891 条(534 上行、357 下行)经新分发器与记录器回放后,载荷字节、方向、Opcode、版本、目标、完整性和读取问题逐条一致;发送后观察 534 次,订阅错误与读取错误均为 0。回放使用托管原函数代理返回 1,不代表真实原生发送已实测,更没有重发这些样本。报告见 `2026-10-11-capture-018-manager.json`。
|
||||||
|
|
||||||
|
原文件解析仍为全部具名、679 条有限字段解析、0 条已支持格式拒绝、8 组 Ping 配对。原文件及 Opcode 缓存未修改。回放按新观察时间生成临时内存记录;原文件的时间和序号只读保留,Ping/RTT 分析仍用原文件。
|
||||||
|
|
||||||
|
现场只需一次聚焦检查:重新加载后开启 36,正常移动,确认管理器计数与日志增加;暂停记录时日志停止增加而管理器继续;解除暂停后恢复;关闭 36 后在当前没有其他消费者的情况下订阅数为 0。性能、切区及卸载仍需现场验收,不要求重新提供泛化抓包。
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 0.1.10:高频未知 Opcode 核对
|
||||||
|
|
||||||
|
基线:国际服 2026.09.15.0000.0000;样本为原 capture-018.jsonl(SHA256 224AE627F6F6E613662745300C01493E411BDBE02CBA72366CCBD373DD28ECFF)。本次只读取原样本及游戏磁盘文件,没有新增 Hook、调用游戏函数或写入原始样本/映射缓存。
|
||||||
|
|
||||||
|
## 分派入口定位
|
||||||
|
|
||||||
|
PacketDispatcher RVA 0x18D9500 的主分派在 0x18DA549 附近:Opcode 减 101 后,以 0x18DD980 为 uint32 RVA 跳转表,读取对应分支。目标三项如下:
|
||||||
|
|
||||||
|
| Opcode | 样本数/长度 | 分派分支 RVA | 游戏处理函数 RVA | 本地补充名称 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 671 | 51 / 72 字节 | 0x18DAD1E | 0xB4AC90 | DOWN_UpdateHate |
|
||||||
|
| 558 | 50 / 264 字节 | 0x18DAD4F | 0xB4ACF0 | DOWN_UpdateHater |
|
||||||
|
| 697 | 51 / 24 字节 | 0x18DAF7E | 0xB39090 | DOWN_EffectResultBasic |
|
||||||
|
|
||||||
|
分支均先调用录像记录入口,再将 IPC 缓冲区+0x10 作为业务载荷交给处理函数。主分派前的字段变换路径(0x18DA02C 起)处理的是 607、637、658、778 等值,这三种不会经过其字段变换。
|
||||||
|
|
||||||
|
本地 IDA 源码表的旧函数名作为交叉参考;实际地址由当前跳转表及当前机器代码取得,没有直接复用旧地址。
|
||||||
|
|
||||||
|
## 671:目标仇恨列表
|
||||||
|
|
||||||
|
0xB4AC90 读取业务偏移 0 的 byte 数量;从偏移 4 开始、步长 8 读取 uint32 对象标识及偏移 +4 的 byte 仇恨值。它将结果写入 UIState.Hate 的每项 EntityId/Enmity 和 HateArrayLength:实际数组起点 0x142ACBF28,计数 0x142ACC028,符合 Hate 的 0x100 计数字段。
|
||||||
|
|
||||||
|
样本固定 72 字节,数量为 0 或 1。当前解析限定 72 字节,以 4 字节前缀、8 字节步长限制最多 8 项;其余保留字节不解释。客户端 Hate 可容纳 32 项不代表本次这个固定大小包也容纳 32 项。仇恨值按原函数的 byte 读取,不把后面的填充字节拼成 uint32,也不额外推断百分比约束。
|
||||||
|
|
||||||
|
## 558:自身敌对列表
|
||||||
|
|
||||||
|
0xB4ACF0 采用相同网络条目布局:数量 byte,条目从 +4 开始,每项 8 字节,对象 uint32、仇恨 byte。处理后保存到 UIState.Hater 的 0x48 字节条目:其中前 0x40 字节的名称是客户端另行查找,并非包中的文本。HaterCount 写入 0x142ACC930。
|
||||||
|
|
||||||
|
样本固定 264 字节、数量均为 1。解析限定 264 字节、最多 32 项;不把它用于雷达、自动选敌或名单操作,仅展示网络记录中的已核实字段。
|
||||||
|
|
||||||
|
## 697:精简效果结果
|
||||||
|
|
||||||
|
0xB39090 按业务偏移 0 的 byte 数量循环,条目步长 0x10。相对于第一个条目:
|
||||||
|
|
||||||
|
- 业务 +4 的 uint32 用于匹配 ActionEffectHandler 中的动作全局序列;
|
||||||
|
- 业务 +0x10 的 byte 用于匹配目标序号;
|
||||||
|
- 业务 +0x0C 的 uint32 写入角色 +0x1AC,对应当前 CharacterData.Health;
|
||||||
|
- 其余字段继续保留,不凭偏移猜命名。
|
||||||
|
|
||||||
|
0x901EC0 根据匹配结果更新效果来源/目标确认标记,进一步支持“效果结果确认”的语义。样本均为 24 字节、数量 1;当前解析限定此固定包、最多 1 项,不据此支持其他数量变体。固定包中的 4 字节尾部不解释。
|
||||||
|
|
||||||
|
## 实现边界
|
||||||
|
|
||||||
|
三个映射与之前的 PingRsp 一起集中到 CurrentProtocolEvidence。仅当完整客户端版本匹配、Opcode 未被占用且名称尚不存在时补充,来源显示本机核实的新增项数。尊重用户或远程文件中已有的显式映射,缓存 JSON 不改写。
|
||||||
|
|
||||||
|
解析同样限定当前完整版本,检查固定长度与数量上限。显示已核实字段不代表所有字节都已解释。8 的状态屏蔽保持不可用;本轮没有把仇恨或动作确认包误当状态列表。
|
||||||
|
|
||||||
|
## 验证与进度
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;64 项离线测试通过,新增容量、步长、byte 字段、版本隔离、映射冲突和 1500 个任意长度/计数输入检查。
|
||||||
|
|
||||||
|
原 891 条记录回放:
|
||||||
|
|
||||||
|
| 项目 | 0.1.9 | 0.1.10 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 有名称 | 724 | 876 |
|
||||||
|
| 未知记录 | 167 | 15 |
|
||||||
|
| Parsed=true | 512 | 664 |
|
||||||
|
| 已支持格式拒绝 | 0 | 0 |
|
||||||
|
| Ping 配对 | 8 | 8 |
|
||||||
|
|
||||||
|
新识别的 152 条全部通过当前有限字段解析;完整汇总保存为 2026-10-11-capture-018-v0110.json,不含真实载荷、坐标或对象标识。仍有 212 条已命名包未实现解析;Parsed=true 也不表示全字段解释完成。
|
||||||
|
|
||||||
|
剩余未知:614(16 字节,10 条)、788(16 字节,4 条)、214(16 字节,1 条)。对应当前分派线索分别为 0x18DD1FE → 0x8FE360、0x18DAF0E → 0xB38BB0、0x18DAFCC → 0xB39480;名称及完整语义下一轮核对,当前不补猜测映射。
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 0.1.11:剩余三种 Opcode 与状态处理边界
|
||||||
|
|
||||||
|
基线:国际服 2026.09.15.0000.0000。使用原 capture-018.jsonl 及本机游戏磁盘文件;原始样本、Opcode 缓存和运行 Hook 不改动。
|
||||||
|
|
||||||
|
## 614:职业量表更新
|
||||||
|
|
||||||
|
分派 0x18DD1FE 将业务载荷交给 0x8FE360,管理对象地址为 0x142A95710。函数比较包偏移 0 与管理器+0x58;一致时调用当前量表的虚函数,否则转到 0x8FE590 按 ClassJob 切换量表。当前 FFXIVClientStructs.dll 元数据确认 JobGaugeManager.ClassJobId 在 +0x58。
|
||||||
|
|
||||||
|
10 条样本均 16 字节、职业字节 31。仅解释职业 ID,其余职业专属数据保留,不凭其他职业的量表布局套用。
|
||||||
|
|
||||||
|
现有用户映射已含 `DOWN_ActorGauge=[8]`,而 614 未命名。本次使用独立的证据标签 `DOWN_JobGauge=614`,不覆盖原 8,也不宣称原 8 的语义经过本轮验证。仍遵守已有同名/同 Opcode 显式映射不覆盖规则。
|
||||||
|
|
||||||
|
## 788:职业信息更新
|
||||||
|
|
||||||
|
分派 0x18DAF0E → 0xB38BB0 → 0xB38AB0。后者将业务偏移 0 的职业字节交给 PlayerState 的职业更新路径,偏移 2 的 ushort 交给 0xBE7B90,后者写 PlayerState+0x88(CurrentLevel);偏移 8 的 uint32 交给 0xBE8000,后者经职业索引写 +0xD0 的职业经验数组。
|
||||||
|
|
||||||
|
当前 DLL 元数据确认 PlayerState.CurrentClassJobId=0x7E、CurrentLevel=0x88、classJobExperience=0xD0。四条 16 字节样本的职业 ID 为 31、当前等级为 100。
|
||||||
|
|
||||||
|
补充 `DOWN_UpdateClassInfo=788`,只解释职业、当前等级和经验;偏移 4、6、12 等其他值本轮不命名。
|
||||||
|
|
||||||
|
## 214:战斗归属
|
||||||
|
|
||||||
|
分派 0x18DAFCC → 0xB39480。函数按收到的目标 ID 找到角色,将业务偏移 0 的 byte 写角色+0x1D5,并将偏移 8 的 uint64 写角色+0x1D8。
|
||||||
|
|
||||||
|
当前 DLL 中 CharacterData 在角色+0x1A0,CombatTagType 在 CharacterData+0x35,CombatTaggerId 在 +0x38,正好对应这两次写入。补充 `DOWN_FirstAttack=214`,界面解释为战斗归属类型和 64 位标记者 ID,不解释为一次伤害或技能 ID。样本一条、16 字节。
|
||||||
|
|
||||||
|
## 状态列表为 8 提供的新依据
|
||||||
|
|
||||||
|
已命名的 DOWN_StatusEffectList(Opcode 131)在本次样本有四条、每条 384 字节。当前分派链是 0x18DAE4B → 0xB385F0 → 0xB37F90。消费函数将业务载荷+0x14 交给状态管理器批量处理函数 0x8A78F0。
|
||||||
|
|
||||||
|
0x8A78F0 明确循环 30 次、步长 12:从条目+0 读 ushort 状态 ID、+2 读 ushort 参数、+4 读 float 持续时间、+8 读 uint32 来源,再调用 0x8A6BF0 设置状态。网络块为 360 字节,前缀 20 字节,共到 380;本次 384 字节消息的末尾 4 字节不解释。旧 240 字节固定缓冲和 Size=8 声明不能直接恢复使用。客户端的 60 个 16 字节状态项仍是另一种内存结构。
|
||||||
|
|
||||||
|
另一个关键限制:PacketDispatcher 在 Opcode 131 的 0x18D97FC 分支先复制 IPC 消息,再对 30 个状态 ID 执行减去会话相关值的转换,随后才交给最终消费函数。转换由前置条件控制,不等于每次抓到的状态 ID 都已经是可直接与 Status 表比较的值。当前 36 在分派前记录原始业务载荷,不带完整会话转换上下文,因此不能仅凭旧捕获文件直接实现状态 ID 屏蔽。
|
||||||
|
|
||||||
|
下一步为 8 核对转换后的入口、正常与双列表/回放差异、本地 StatusManager 的身份门控及标志更新过程。尚未完成这些前提,8 继续不可用。本轮没有修改或删除任何角色状态。
|
||||||
|
|
||||||
|
## 验证与范围
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;66 项离线测试通过。新增三种固定包长度/版本隔离、64 位标识保真、原 ActorGauge 映射保留检查。
|
||||||
|
|
||||||
|
同一份 891 条文件回放:有名称 891、未知 0、有限字段解析通过 679、已支持格式拒绝 0,Ping 仍 8 组。报告为 2026-10-11-capture-018-v0111.json,不含真实载荷或对象标识。
|
||||||
|
|
||||||
|
这只说明本份样本中的 Opcode 均有名称,不代表未来采样不会再出现未知 Opcode,更不代表所有协议已还原。212 条已命名记录仍无解析器;职业量表也仅解析了职业 ID,其他字节继续保留。
|
||||||
@@ -0,0 +1,552 @@
|
|||||||
|
{
|
||||||
|
"Source": "E:\\SapphireServer\\Dalamud Dev\\IC\\I-Ching-GL (2).dll",
|
||||||
|
"SHA256": "CE269139FDFDB136BE382E10BE78E6E0BC346DEB335E9D99DF2E80097BD76CBF",
|
||||||
|
"Assembly": "I-Ching-GL, Version=0.1.6.6, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"Runtime": "v4.0.30319",
|
||||||
|
"References": [
|
||||||
|
{
|
||||||
|
"Name": "System.Runtime",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Collections",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Dalamud",
|
||||||
|
"Version": "15.0.3.6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Numerics.Vectors",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Net.Http",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Security.Cryptography",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Diagnostics.Process",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Text.RegularExpressions",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Management",
|
||||||
|
"Version": "9.0.0.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Threading.Tasks.Parallel",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "TerraFX.Interop.Windows",
|
||||||
|
"Version": "10.0.26100.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Dalamud.Bindings.ImGui",
|
||||||
|
"Version": "1.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "FFXIVClientStructs",
|
||||||
|
"Version": "7.56.2.9418"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Runtime.Numerics",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Linq.Expressions",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Lumina",
|
||||||
|
"Version": "0.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Lumina.Excel",
|
||||||
|
"Version": "7.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Newtonsoft.Json",
|
||||||
|
"Version": "13.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Threading.Thread",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Linq",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Memory",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Threading",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Text.Encoding.Extensions",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Win32.Registry",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Runtime.InteropServices",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ECommons",
|
||||||
|
"Version": "3.2.1.17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Console",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "mscorlib",
|
||||||
|
"Version": "4.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "D6804B14",
|
||||||
|
"Version": "0.0.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Types": [
|
||||||
|
{
|
||||||
|
"Name": "StatusEffectList",
|
||||||
|
"Attributes": "Public, ExplicitLayout, Sealed, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "EntryData",
|
||||||
|
"Type": "StatusEffectList/<EntryData>e__FixedBuffer",
|
||||||
|
"Offset": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "StatusEffectList/<EntryData>e__FixedBuffer",
|
||||||
|
"DeclaredSize": 240,
|
||||||
|
"DelegateInvoke": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Span`1<StatusEffectListEntry> StatusEffectList::get_Entries()",
|
||||||
|
"Token": 100663317,
|
||||||
|
"RVA": "0x2AE44",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "StatusEffectListEntry",
|
||||||
|
"Attributes": "Public, ExplicitLayout, Sealed, BeforeFieldInit",
|
||||||
|
"DeclaredSize": 8,
|
||||||
|
"PackingSize": 1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "StatusID",
|
||||||
|
"Type": "System.UInt16",
|
||||||
|
"Offset": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "StackCount",
|
||||||
|
"Type": "System.UInt16",
|
||||||
|
"Offset": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "RemainingTime",
|
||||||
|
"Type": "System.Single",
|
||||||
|
"Offset": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SourceID",
|
||||||
|
"Type": "System.UInt32",
|
||||||
|
"Offset": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [],
|
||||||
|
"Methods": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.ActorRadiusHook",
|
||||||
|
"Attributes": "Public, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "_configuration",
|
||||||
|
"Type": "SamplePlugin.Configuration",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_ActorRadiusHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.ActorRadiusHook/ActorRadiusDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.ActorRadiusHook/ActorRadiusDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Single SamplePlugin.Hook.ActorRadiusHook/ActorRadiusDelegate::Invoke(System.UInt64,System.Byte)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.ActorRadiusHook::.ctor(SamplePlugin.Configuration)",
|
||||||
|
"Token": 100664138,
|
||||||
|
"RVA": "0x2CA9D4",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.ActorRadiusHook::Dispose()",
|
||||||
|
"Token": 100664139,
|
||||||
|
"RVA": "0x2CB41C",
|
||||||
|
"BodyState": "SuspiciousBody",
|
||||||
|
"DeclaredCodeSize": -1812037104,
|
||||||
|
"Error": null,
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Single SamplePlugin.Hook.ActorRadiusHook::ActorRadiusDetour(System.UInt64,System.Byte)",
|
||||||
|
"Token": 100664140,
|
||||||
|
"RVA": "0x2CB9F4",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.NoActionMoveHook",
|
||||||
|
"Attributes": "Public, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "_configuration",
|
||||||
|
"Type": "SamplePlugin.Configuration",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_NoActionMoveHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.NoActionMoveHook/NoActionMoveDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.NoActionMoveHook/NoActionMoveDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.UInt64 SamplePlugin.Hook.NoActionMoveHook/NoActionMoveDelegate::Invoke(System.UInt64,System.Byte,System.UInt64,System.Single,System.IntPtr)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.NoActionMoveHook/<>c",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.NoActionMoveHook::.ctor(SamplePlugin.Configuration)",
|
||||||
|
"Token": 100664144,
|
||||||
|
"RVA": "0x2CD19C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.NoActionMoveHook::Dispose()",
|
||||||
|
"Token": 100664145,
|
||||||
|
"RVA": "0x2CDA34",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Collections.Generic.List`1<System.UInt32> SamplePlugin.Hook.NoActionMoveHook::GetAllTimelineIds()",
|
||||||
|
"Token": 100664146,
|
||||||
|
"RVA": "0x2CDFA0",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.UInt64 SamplePlugin.Hook.NoActionMoveHook::NoActionMoveDetour(System.UInt64,System.Byte,System.UInt64,System.Single,System.IntPtr)",
|
||||||
|
"Token": 100664147,
|
||||||
|
"RVA": "0x2CE2EC",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.YMove",
|
||||||
|
"Attributes": "Public, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "_configuration",
|
||||||
|
"Type": "SamplePlugin.Configuration",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_SendNormalMoveHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.YMove/SendNormalMoveDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_SendCombatMoveHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.YMove/SendCombatMoveDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_TryToGetDeepDStage",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.YMove/TryToGetDeepDStageDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "offset",
|
||||||
|
"Type": "System.Int32",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "decryptor",
|
||||||
|
"Type": "SamplePlugin.Hook.BaguaDecryptor",
|
||||||
|
"Offset": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.YMove/SendNormalMoveDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.IntPtr SamplePlugin.Hook.YMove/SendNormalMoveDelegate::Invoke(System.UIntPtr,System.IntPtr,System.UInt32)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.YMove/SendCombatMoveDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.IntPtr SamplePlugin.Hook.YMove/SendCombatMoveDelegate::Invoke(System.UIntPtr,System.IntPtr,System.UInt32)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.YMove/TryToGetDeepDStageDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Void SamplePlugin.Hook.YMove/TryToGetDeepDStageDelegate::Invoke(System.Int64,System.Int64)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.YMove::.ctor(SamplePlugin.Configuration)",
|
||||||
|
"Token": 100664221,
|
||||||
|
"RVA": "0x3112D0",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.String SamplePlugin.Hook.YMove::CombineOffsets(System.Byte,System.Byte)",
|
||||||
|
"Token": 100664222,
|
||||||
|
"RVA": "0x312214",
|
||||||
|
"BodyState": "SuspiciousBody",
|
||||||
|
"DeclaredCodeSize": 19,
|
||||||
|
"Error": null,
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Int32 SamplePlugin.Hook.YMove::ConvertHexStringToInt(System.String)",
|
||||||
|
"Token": 100664223,
|
||||||
|
"RVA": "0x312670",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Object reference not set to an instance of an object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.YMove::SafeAreaCheckPlus(Dalamud.Plugin.Services.IFramework)",
|
||||||
|
"Token": 100664224,
|
||||||
|
"RVA": "0x312994",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.YMove::Dispose()",
|
||||||
|
"Token": 100664225,
|
||||||
|
"RVA": "0x313228",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.IntPtr SamplePlugin.Hook.YMove::SendNormalMoveDetour(System.UIntPtr,System.IntPtr,System.UInt32)",
|
||||||
|
"Token": 100664226,
|
||||||
|
"RVA": "0x313B34",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.IntPtr SamplePlugin.Hook.YMove::SendCombatMoveDetour(System.UIntPtr,System.IntPtr,System.UInt32)",
|
||||||
|
"Token": 100664227,
|
||||||
|
"RVA": "0x3166B4",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.YMove::TryToGetDeepDStageDetour(System.Int64,System.Int64)",
|
||||||
|
"Token": 100664228,
|
||||||
|
"RVA": "0x3195F0",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.StatusCheck",
|
||||||
|
"Attributes": "Public, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "_configuration",
|
||||||
|
"Type": "SamplePlugin.Configuration",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_StatusCheckHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.StatusCheck/StatusCheckDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_ProcessPacketStatusEffectHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_ProcessPacketStatusEffectHookGL",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegateGL>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "decryptor",
|
||||||
|
"Type": "SamplePlugin.Hook.BaguaDecryptor",
|
||||||
|
"Offset": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.StatusCheck/StatusCheckDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Void SamplePlugin.Hook.StatusCheck/StatusCheckDelegate::Invoke(FFXIVClientStructs.FFXIV.Client.Game.StatusManager*)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Void SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegate::Invoke(System.UInt32,StatusEffectList*,System.Boolean)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegateGL",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Void SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegateGL::Invoke(System.UInt32,StatusEffectList*,System.Boolean,System.Boolean)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::.ctor(SamplePlugin.Configuration)",
|
||||||
|
"Token": 100664232,
|
||||||
|
"RVA": "0x31BE1C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::Dispose()",
|
||||||
|
"Token": 100664233,
|
||||||
|
"RVA": "0x31CB68",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::StatusCheckDetour(FFXIVClientStructs.FFXIV.Client.Game.StatusManager*)",
|
||||||
|
"Token": 100664234,
|
||||||
|
"RVA": "0x31D2B0",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::ProcessPacketStatusEffectDetourGL(System.UInt32,StatusEffectList*,System.Boolean,System.Boolean)",
|
||||||
|
"Token": 100664235,
|
||||||
|
"RVA": "0x31EF04",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::ProcessPacketStatusEffectDetour(System.UInt32,StatusEffectList*,System.Boolean)",
|
||||||
|
"Token": 100664236,
|
||||||
|
"RVA": "0x320A44",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Limitation": "Metadata only. Method-body parsing is not proof of executable logic. Target assembly was not executed."
|
||||||
|
}
|
||||||
@@ -0,0 +1,528 @@
|
|||||||
|
{
|
||||||
|
"Source": "E:\\SapphireServer\\Dalamud Dev\\IC\\I-Ching-GL.dll",
|
||||||
|
"SHA256": "DA648072A2CE0856B4884A332EFAC063FC8F0E03236D7D4DBC9334BF0B2A2AA8",
|
||||||
|
"Assembly": "I-Ching-GL, Version=0.1.6.6, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"Runtime": "v4.0.30319",
|
||||||
|
"References": [
|
||||||
|
{
|
||||||
|
"Name": "System.Runtime",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Collections",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Dalamud",
|
||||||
|
"Version": "15.0.3.6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Numerics.Vectors",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Diagnostics.Process",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Text.RegularExpressions",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Management",
|
||||||
|
"Version": "9.0.0.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Threading.Tasks.Parallel",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "TerraFX.Interop.Windows",
|
||||||
|
"Version": "10.0.26100.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Dalamud.Bindings.ImGui",
|
||||||
|
"Version": "1.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "FFXIVClientStructs",
|
||||||
|
"Version": "7.56.2.9458"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Lumina",
|
||||||
|
"Version": "0.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Threading.Thread",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Lumina.Excel",
|
||||||
|
"Version": "7.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Newtonsoft.Json",
|
||||||
|
"Version": "13.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Linq",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ECommons",
|
||||||
|
"Version": "3.2.1.17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Runtime.InteropServices",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Console",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Memory",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System.Threading",
|
||||||
|
"Version": "10.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "mscorlib",
|
||||||
|
"Version": "4.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "DEC473AF",
|
||||||
|
"Version": "0.0.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Types": [
|
||||||
|
{
|
||||||
|
"Name": "StatusEffectList",
|
||||||
|
"Attributes": "Public, ExplicitLayout, Sealed, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "EntryData",
|
||||||
|
"Type": "StatusEffectList/<EntryData>e__FixedBuffer",
|
||||||
|
"Offset": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "StatusEffectList/<EntryData>e__FixedBuffer",
|
||||||
|
"DeclaredSize": 240,
|
||||||
|
"DelegateInvoke": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Span`1<StatusEffectListEntry> StatusEffectList::get_Entries()",
|
||||||
|
"Token": 100663303,
|
||||||
|
"RVA": "0x1D5BC",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "StatusEffectListEntry",
|
||||||
|
"Attributes": "Public, ExplicitLayout, Sealed, BeforeFieldInit",
|
||||||
|
"DeclaredSize": 8,
|
||||||
|
"PackingSize": 1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "StatusID",
|
||||||
|
"Type": "System.UInt16",
|
||||||
|
"Offset": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "StackCount",
|
||||||
|
"Type": "System.UInt16",
|
||||||
|
"Offset": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "RemainingTime",
|
||||||
|
"Type": "System.Single",
|
||||||
|
"Offset": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SourceID",
|
||||||
|
"Type": "System.UInt32",
|
||||||
|
"Offset": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [],
|
||||||
|
"Methods": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.ActorRadiusHook",
|
||||||
|
"Attributes": "Public, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "_configuration",
|
||||||
|
"Type": "SamplePlugin.Configuration",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_ActorRadiusHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.ActorRadiusHook/ActorRadiusDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.ActorRadiusHook/ActorRadiusDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Single SamplePlugin.Hook.ActorRadiusHook/ActorRadiusDelegate::Invoke(System.UInt64,System.Byte)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.ActorRadiusHook::.ctor(SamplePlugin.Configuration)",
|
||||||
|
"Token": 100663839,
|
||||||
|
"RVA": "0x1F1EB0",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.ActorRadiusHook::Dispose()",
|
||||||
|
"Token": 100663840,
|
||||||
|
"RVA": "0x1F2A78",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Single SamplePlugin.Hook.ActorRadiusHook::ActorRadiusDetour(System.UInt64,System.Byte)",
|
||||||
|
"Token": 100663841,
|
||||||
|
"RVA": "0x1F2F94",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.NoActionMoveHook",
|
||||||
|
"Attributes": "Public, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "_configuration",
|
||||||
|
"Type": "SamplePlugin.Configuration",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_NoActionMoveHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.NoActionMoveHook/NoActionMoveDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.NoActionMoveHook/NoActionMoveDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.UInt64 SamplePlugin.Hook.NoActionMoveHook/NoActionMoveDelegate::Invoke(System.UInt64,System.Byte,System.UInt64,System.Single,System.IntPtr)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.NoActionMoveHook/<>c",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.NoActionMoveHook::.ctor(SamplePlugin.Configuration)",
|
||||||
|
"Token": 100663845,
|
||||||
|
"RVA": "0x1F4794",
|
||||||
|
"BodyState": "SuspiciousBody",
|
||||||
|
"DeclaredCodeSize": -1346900411,
|
||||||
|
"Error": null,
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.NoActionMoveHook::Dispose()",
|
||||||
|
"Token": 100663846,
|
||||||
|
"RVA": "0x1F511C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Collections.Generic.List`1<System.UInt32> SamplePlugin.Hook.NoActionMoveHook::GetAllTimelineIds()",
|
||||||
|
"Token": 100663847,
|
||||||
|
"RVA": "0x1F577C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.UInt64 SamplePlugin.Hook.NoActionMoveHook::NoActionMoveDetour(System.UInt64,System.Byte,System.UInt64,System.Single,System.IntPtr)",
|
||||||
|
"Token": 100663848,
|
||||||
|
"RVA": "0x1F5AF8",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.YMove",
|
||||||
|
"Attributes": "Public, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "_configuration",
|
||||||
|
"Type": "SamplePlugin.Configuration",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_SendNormalMoveHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.YMove/SendNormalMoveDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_SendCombatMoveHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.YMove/SendCombatMoveDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_TryToGetDeepDStage",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.YMove/TryToGetDeepDStageDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "offset",
|
||||||
|
"Type": "System.Int32",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "decryptor",
|
||||||
|
"Type": "SamplePlugin.Hook.BaguaDecryptor",
|
||||||
|
"Offset": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.YMove/SendNormalMoveDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.IntPtr SamplePlugin.Hook.YMove/SendNormalMoveDelegate::Invoke(System.UIntPtr,System.IntPtr,System.UInt32)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.YMove/SendCombatMoveDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.IntPtr SamplePlugin.Hook.YMove/SendCombatMoveDelegate::Invoke(System.UIntPtr,System.IntPtr,System.UInt32)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.YMove/TryToGetDeepDStageDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Void SamplePlugin.Hook.YMove/TryToGetDeepDStageDelegate::Invoke(System.Int64,System.Int64)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.YMove::.ctor(SamplePlugin.Configuration)",
|
||||||
|
"Token": 100663922,
|
||||||
|
"RVA": "0x2388B8",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Object reference not set to an instance of an object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.String SamplePlugin.Hook.YMove::CombineOffsets(System.Byte,System.Byte)",
|
||||||
|
"Token": 100663923,
|
||||||
|
"RVA": "0x23996C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Int32 SamplePlugin.Hook.YMove::ConvertHexStringToInt(System.String)",
|
||||||
|
"Token": 100663924,
|
||||||
|
"RVA": "0x239DD4",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Object reference not set to an instance of an object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.YMove::SafeAreaCheckPlus(Dalamud.Plugin.Services.IFramework)",
|
||||||
|
"Token": 100663925,
|
||||||
|
"RVA": "0x23A0C8",
|
||||||
|
"BodyState": "SuspiciousBody",
|
||||||
|
"DeclaredCodeSize": 3,
|
||||||
|
"Error": null,
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.YMove::Dispose()",
|
||||||
|
"Token": 100663926,
|
||||||
|
"RVA": "0x23A9C4",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Object reference not set to an instance of an object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.IntPtr SamplePlugin.Hook.YMove::SendNormalMoveDetour(System.UIntPtr,System.IntPtr,System.UInt32)",
|
||||||
|
"Token": 100663927,
|
||||||
|
"RVA": "0x23B30C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.IntPtr SamplePlugin.Hook.YMove::SendCombatMoveDetour(System.UIntPtr,System.IntPtr,System.UInt32)",
|
||||||
|
"Token": 100663928,
|
||||||
|
"RVA": "0x23CDCC",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.YMove::TryToGetDeepDStageDetour(System.Int64,System.Int64)",
|
||||||
|
"Token": 100663929,
|
||||||
|
"RVA": "0x23EE1C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.StatusCheck",
|
||||||
|
"Attributes": "Public, BeforeFieldInit",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"PackingSize": -1,
|
||||||
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "_configuration",
|
||||||
|
"Type": "SamplePlugin.Configuration",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_StatusCheckHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.StatusCheck/StatusCheckDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_ProcessPacketStatusEffectHook",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegate>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "_ProcessPacketStatusEffectHookGL",
|
||||||
|
"Type": "Dalamud.Hooking.Hook`1<SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegateGL>",
|
||||||
|
"Offset": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "decryptor",
|
||||||
|
"Type": "SamplePlugin.Hook.BaguaDecryptor",
|
||||||
|
"Offset": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"NestedTypes": [
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.StatusCheck/StatusCheckDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Void SamplePlugin.Hook.StatusCheck/StatusCheckDelegate::Invoke(FFXIVClientStructs.FFXIV.Client.Game.StatusManager*)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegate",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Void SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegate::Invoke(System.UInt32,StatusEffectList*,System.Boolean)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegateGL",
|
||||||
|
"DeclaredSize": -1,
|
||||||
|
"DelegateInvoke": [
|
||||||
|
"System.Void SamplePlugin.Hook.StatusCheck/ProcessPacketStatusEffectHookDelegateGL::Invoke(System.UInt32,StatusEffectList*,System.Boolean,System.Boolean)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Methods": [
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::.ctor(SamplePlugin.Configuration)",
|
||||||
|
"Token": 100663933,
|
||||||
|
"RVA": "0x241E48",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::Dispose()",
|
||||||
|
"Token": 100663934,
|
||||||
|
"RVA": "0x242A88",
|
||||||
|
"BodyState": "SuspiciousBody",
|
||||||
|
"DeclaredCodeSize": 1038335658,
|
||||||
|
"Error": null,
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::StatusCheckDetour(FFXIVClientStructs.FFXIV.Client.Game.StatusManager*)",
|
||||||
|
"Token": 100663935,
|
||||||
|
"RVA": "0x24325C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::ProcessPacketStatusEffectDetourGL(System.UInt32,StatusEffectList*,System.Boolean,System.Boolean)",
|
||||||
|
"Token": 100663936,
|
||||||
|
"RVA": "0x245510",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Index was outside the bounds of the array.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signature": "System.Void SamplePlugin.Hook.StatusCheck::ProcessPacketStatusEffectDetour(System.UInt32,StatusEffectList*,System.Boolean)",
|
||||||
|
"Token": 100663937,
|
||||||
|
"RVA": "0x246F9C",
|
||||||
|
"BodyState": "ReadError",
|
||||||
|
"DeclaredCodeSize": null,
|
||||||
|
"Error": "Operation is not valid due to the current state of the object.",
|
||||||
|
"UnvalidatedIL": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Limitation": "Metadata only. Method-body parsing is not proof of executable logic. Target assembly was not executed."
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 原版 DLL 参考核对
|
||||||
|
|
||||||
|
日期:2026-10-11。用户提供 `IC/I-Ching-GL.dll`,原件保留未修改。
|
||||||
|
|
||||||
|
## 文件与依赖
|
||||||
|
|
||||||
|
- 大小:5,811,712 字节。
|
||||||
|
- 程序集:`I-Ching-GL, Version=0.1.6.6`。
|
||||||
|
- SHA256:`DA648072A2CE0856B4884A332EFAC063FC8F0E03236D7D4DBC9334BF0B2A2AA8`。
|
||||||
|
- 引用:System.Runtime 10.0.0.0、Dalamud 15.0.3.6、FFXIVClientStructs 7.56.2.9458、ECommons 3.2.1.17。
|
||||||
|
- 本机重建工程所用 FFXIVClientStructs 程序集为 7.56.2.9136。两者构建版本不同,不能直接将 DLL 的结构假设视为当前运行库已验证布局。
|
||||||
|
|
||||||
|
使用本机 Mono.Cecil 静态读取文件,没有通过 Assembly.Load 加载原版,没有执行模块初始化器或原版插件逻辑。
|
||||||
|
|
||||||
|
## 可核实的接口
|
||||||
|
|
||||||
|
| 对应功能 | DLL 元数据 | 与现有资料的关系 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 14 目标半径 | ActorRadiusDetour / 委托:float(ulong, byte) | 与 IC/CombatHook.cs 的签名一致;不能据签名确定过滤对象或加值方式 |
|
||||||
|
| 9 高度偏移 | YMove 的普通、战斗路径均为 IntPtr(UIntPtr, IntPtr, uint) | 与现有两条移动路径的参数形状一致;不能据此确认字节偏移或是否恢复原值 |
|
||||||
|
| 16 突进无位移 | 旧入口委托:ulong(ulong, byte, ulong, float, IntPtr) | 与旧 .cs 一致;重建工程已基于当前原生入口及用户测试改用共用六参数入口,不退回旧入口 |
|
||||||
|
| 8 状态处理 | StatusCheckDetour(StatusManager*);GL 包处理为 void(uint, StatusEffectList*, bool, bool) | GL 委托比非 GL 多一个布尔参数;与旧 .cs 一致,仍未提供可读取的原生签名值或可靠方法体 |
|
||||||
|
|
||||||
|
## 方法体读取限制
|
||||||
|
|
||||||
|
本次选择目标半径、YMove、位移屏蔽、状态处理及状态结构中的 21 个方法:18 个读取方法体抛错,3 个得到明显可疑的 IL/长度。例:YMove.SafeAreaCheckPlus 被读为仅 3 字节,起始为缺少入栈操作的 ldelem.i1;StatusCheck.Dispose 的声明代码长度被读为 1,038,335,658 字节。模块初始化器含混淆标识调用。
|
||||||
|
|
||||||
|
这些现象与方法体保护或混淆相符,但尚未确定具体保护方案,亦不能仅由此排除文件损坏。不能将这些结果当作已成功反编译出的逻辑。尤其不能声称该 DLL 已证明 14 的真实半径公式、16461 的落点计算,或 9 的 UI 应用方式。
|
||||||
|
|
||||||
|
## 状态结构仍存在的矛盾
|
||||||
|
|
||||||
|
元数据再次确认旧 .cs 的声明:StatusEffectList.EntryData 在偏移 20,固定缓冲区为 240 字节;StatusEffectListEntry 声明 Size=8、Pack=1,但 SourceID 为偏移 8 的 uint,字段末端达到 12。这属于声明大小和字段范围不一致的证据,不能当作游戏网络布局。
|
||||||
|
|
||||||
|
旧 .cs 另外创建 30 项 Span;这个数量来自源码,DLL 的 get_Entries 方法体未成功读取,不能宣称本次已从 DLL 核实。仍须取得当前原生包长度和布局依据后才启用 8。
|
||||||
|
|
||||||
|
## 对重建工程的处理
|
||||||
|
|
||||||
|
将此文件登记为额外参考,用于后续接口与依赖交叉核对。本次没有基于不可读取的方法体修改运行逻辑,没有替换已部署 0.1.5。14 的当前修正继续等待 16461 游戏内复测;9 保持用户明确要求的“生效/回到 0”交互。
|
||||||
|
|
||||||
|
结构化证据保存在 `2026-10-11-original-dll-metadata.json`。可复现命令(工作区根目录):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./IC.Rebuild/Tools/ManagedAudit/Audit.ps1 -AssemblyPath './IC/I-Ching-GL.dll' -OutputPath './IC.Rebuild/diagnostics/2026-10-11-original-dll-metadata.json'
|
||||||
|
```
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 0.1.5 落点与界面调整
|
||||||
|
|
||||||
|
日期:2026-10-11。客户端:国际服 2026.09.15.0000.0000。
|
||||||
|
|
||||||
|
## 14 目标半径
|
||||||
|
|
||||||
|
用户确认原生圈已扩大,但技能 16461 仍落在原来小圈边缘;期望落在扩大圈外侧。0.1.4 刻意保留突进原始半径的处理不符合该要求。
|
||||||
|
|
||||||
|
0.1.5 在帧快照记录选中目标地址和 EntityId,仅当两者一致且对象不是自身时,对原生半径增加一次偏移。圈显示、距离判断和突进路径读取同一目标半径;14 不再叠加技能射程。原生回调继续只读快照,不访问线程限定服务。界面显示原始和有效半径,以及相关调用计数。
|
||||||
|
|
||||||
|
离线检查覆盖自身、其他对象、身份不符和无效帧不扩大,以及不同查询上下文的一致性。实际原生调用和最终落点仍需游戏内复测:关闭 13 和 16,单独开启 14,使用 16461 对比原始与扩大后的落点。
|
||||||
|
|
||||||
|
## 已确认文案
|
||||||
|
|
||||||
|
用户明确确认 1、2、13、15、16、17、19、27、28、29 有效。仅在上述客户端版本将这些功能显示为“已实测有效”;14 为“待复测”,未确认项为“待实测”,没有实现的 8 为“暂不可用”。运行开关和错误状态独立显示。用户反馈不代表所有组合、切图和卸载场景已验收。
|
||||||
|
|
||||||
|
## 9 手动生效
|
||||||
|
|
||||||
|
勾选 9 后,预设值不会自动应用。设置参数再点“生效”才保存本次应用值;再点“回到 0”取消偏移,随后位置同步时反映。修改预设不改变当前应用值;关闭或重载清零应用值但保留预设。应用值只存在于会话内,使用线程安全读取,不写配置。依赖 10 的迷宫条件仍可将当前实际偏移限制为 0。
|
||||||
|
|
||||||
|
## Timeline 折叠
|
||||||
|
|
||||||
|
16 的整个名单管理默认折叠;其中技能名称名单和手动编号各有独立折叠,避免展开战斗页时出现全部编号。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;45 项离线测试通过。新增文案和高度应用状态测试通过;不在离线测试中执行原生游戏 Hook。14 修正与 9 新交互仍待用户热重载后实测。
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 14 原生目标圈与距离修复
|
||||||
|
|
||||||
|
版本:0.1.4。客户端:国际服 2026.09.15.0000.0000。
|
||||||
|
|
||||||
|
用户确认两个问题:画面目标圈没有变大,技能距离也没有增加;并明确要求修改游戏原生圈,不接受额外叠加圈。
|
||||||
|
|
||||||
|
## 距离路径
|
||||||
|
|
||||||
|
0.1.2—0.1.3 只在 GetActionInRangeOrLoS 中临时增加目标半径,漏掉 IsActionTargetInRange 的前置路径。
|
||||||
|
|
||||||
|
本机原生 IsActionTargetInRange 位于 RVA 0x8EE490,在 0x8EE51E 调用 GetActionRange,然后将结果与 ActionManager 内的缓存距离比较。因此仅影响最终半径读取,不能覆盖前置距离判断。
|
||||||
|
|
||||||
|
0.1.4 在 IsActionTargetInRange 与 GetActionInRangeOrLoS 两个检查范围内,对普通技能的有效距离补上编号 14 的增量,数学上相当于增加目标判定半径。作用域按线程隔离;未知技能、位移技能、负数及零范围不补偿。不直接改写缓存距离或物理半径。
|
||||||
|
|
||||||
|
13 和 14 共用一个 GetActionRange Hook,分别计算自己的增量,避免两个独立 Hook 互相覆盖。共享资源按引用计数释放;关闭任意一个模块不停止另一个,释放失败时禁止再次挂接。
|
||||||
|
|
||||||
|
## 原生圈路径
|
||||||
|
|
||||||
|
从本机可执行文件中的 gl_target 资源路径向使用方追踪:
|
||||||
|
|
||||||
|
- vfx/common/eff/gl_target1.avfx 和 gl_target2.avfx 是目标圈资源。
|
||||||
|
- 资源路径表起点 RVA 0x28A3640,gl_target1 位于第 105 项。
|
||||||
|
- 对应资源句柄表起点 RVA 0x2A8F5B0。
|
||||||
|
- 资源选择函数 RVA 0x3938A0,被原生圈更新函数调用。
|
||||||
|
- 原生圈更新函数通过 unwind 链确认完整入口为 RVA 0x7B5430;签名在本机唯一匹配。
|
||||||
|
- 0x7B5829 通过对象虚表的 GetRadius 槽读取半径,并且 adjustByTransformation=false,用于后续圈尺寸计算。
|
||||||
|
|
||||||
|
0.1.4 仅在此原生圈更新函数的同步调用范围内增加显示半径,并排除本地角色。其他线程、突进、碰撞及范围外读取仍返回原值。没有绘制 ImGui 叠加圈,没有修改游戏资源文件或永久写入对象半径。
|
||||||
|
|
||||||
|
原生圈更新 Hook 按本次审计版本限制启用。关闭模块后停止半径补偿,下一次游戏原生更新读取原始半径。
|
||||||
|
|
||||||
|
## 诊断与验证
|
||||||
|
|
||||||
|
面板显示前置检查、最终检查、距离补偿、原生圈更新、绘制半径补偿五项计数,以区分入口未触发与已触发但实际效果不符。
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;40 项离线测试通过。覆盖两条距离作用域、位移排除、原生圈作用域、跨线程隔离、异常退出恢复、13 和 14 的共享生命周期及清理失败处理。
|
||||||
|
|
||||||
|
静态反汇编和离线测试不代替游戏内验收。待复测:关闭 13 和 16,单开 14,增量设为 1;观察原生目标圈与普通近战技能距离,再确认关闭后恢复以及猛攻落点不受影响。不要只用猛攻验证普通技能的距离增量,14 明确排除位移技能的距离补偿。
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 0.1.12:编号 8 的转换后状态更新过滤
|
||||||
|
|
||||||
|
国际服客户端 2026.09.15.0000.0000;只读检查本机游戏文件,SHA256 为 `5BBC501DD5C7F22FD61A11D08C25356041D878DB7CD83203ADAE393E4DFACC44`。沿用当前 API 15 SDK。未调用运行中游戏的原生函数,效果仍待实测。
|
||||||
|
|
||||||
|
## 本次交付
|
||||||
|
|
||||||
|
编号 8 现在有可测试实现:在 StatusManager.SetStatus 入口,将本地角色后续收到的已选状态替换为空条目,原函数恰好调用一次,并保留其返回值和 refreshFlags。默认仅选 1257(Forced March),名单可逐项选取已审计的 38 个候选;空名单完全放行。界面提供本次启用的拦截次数与最近状态 ID。
|
||||||
|
|
||||||
|
不解析或改写原始网络载荷,不依赖 36、37,不直接清空客户端状态数组。首次启用前已有状态须等待下一次 SetStatus 更新。关闭后正常接收后续更新,不把旧状态写回已经变化的角色。服务器状态和机制判定不因此被删除。
|
||||||
|
|
||||||
|
## 原生入口与调用依据
|
||||||
|
|
||||||
|
| 路径 | 当前 RVA 与核对结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 状态 ID 转换 | PacketDispatcher 0x18D97FC 分支复制消息并按 12 字节条目减去会话相关值;最终处理发生在转换之后。沿用 0.1.11 的分派依据,不在捕获文件上猜测还原 ID |
|
||||||
|
| 普通列表 | 0xB385F0 → 0xB37F90 → 0x8A78F0;业务载荷 +0x14,30 个 12 字节条目 |
|
||||||
|
| 玩家专用单列表 | 0xB37D70 从全局本地角色取 StatusManager,再调用 0x8A78F0 |
|
||||||
|
| 双列表 | 0xB38A20 先以 r9b=1 调用公共前缀处理,跳过单列表写入,再将两块交给 0x8A7980;玩家专用入口 0xB37DD0 也调用 0x8A7980 |
|
||||||
|
| 特殊区域列表 | 0xB38600、0xB38760 将载荷 +4 交给 0xB37F90,附加区域字段仍由原生函数处理 |
|
||||||
|
| 批量写入 | 0x8A78F0 写槽位 0—29;0x8A7980 写 0—29 和 30—59。两者逐项调用 0x8A6BF0,并 OR 返回的变化标记,最后按需调用 0x8A9AD0 |
|
||||||
|
| 公共 setter | 0x8A6BF0,参数为 manager、int index、ushort id、float remaining、ushort param、64 位 source、byte refreshFlags;返回 byte 变化标记。SDK 的 SetStatus 委托与此一致(bool 以原生 byte 表达) |
|
||||||
|
| 状态变化与标志 | setter 对比旧 ID/source,更新条目,调用 0x8A1C20 的失去状态路径及 0x89FCB0 的获得状态路径;refreshFlags 非零时调用 0x8A9AD0。批量调用传 0,在批末统一刷新 |
|
||||||
|
| 标志刷新 | 0x8A9AD0 清零 +0x3C8 的七字节标志,按 +0x3D8 的有效范围和 16 字节状态项重算;另更新 +0x3D9 的部分位。其余状态通知继续由 setter 原函数执行 |
|
||||||
|
|
||||||
|
setter 的第 6 参数是完整 64 位 GameObjectId,不能截成网络列表的 uint32 来源字段。第 4 参数使用浮点 ABI。被屏蔽条目传入 id=0、remaining=0、param=0、source=0,保留槽位及 refreshFlags;不跳过 setter,以免旧槽位残留和批末标志不更新。
|
||||||
|
|
||||||
|
调用者中还有动作效果及其他入口。本版不宣称覆盖所有直接复制状态数组的路径;仅保证通过这个已核对 setter 的更新适用。回放分组会选择不同角色/管理器,另外通过 DutyRecorderPlayback 帧标记和条件变化事件暂停整个过滤,不改写回放记录。回放、特殊区域和首次角色载入仍需现场验收。
|
||||||
|
|
||||||
|
## 本地对象与生命周期
|
||||||
|
|
||||||
|
- Framework.Update 发布本地角色地址、EntityId、StatusManager 地址和回放标记;原生回调不读取 ObjectTable 或其他线程绑定服务。
|
||||||
|
- SDK 元数据确认 BattleChara.StatusManager=0x23B0、GameObject.EntityId=0x78。游戏 getter 0xAFF9E0/0xAFF9F0 返回角色 +0x23B0。启用时检查 SDK 字段偏移,客户端版本不符拒绝启用。
|
||||||
|
- 本地角色加载签名唯一匹配 0xB37D7A,RIP 引用全局地址 0x142AA0410。只对选中的本地候选更新,通过 SafeMemory 再读当前全局角色指针和实体 ID,与帧快照同时匹配。绝不依赖可能为空的 manager.Owner。
|
||||||
|
- setter 签名唯一匹配 0x8A6BF0。模块启用清空旧快照;切区、注销与进入回放清空快照;无登录角色或切区期间不发布有效快照。原有半秒过期规则继续生效,指针复用但实体不同会放行。
|
||||||
|
- Hook 和三个事件订阅都纳入现有 ResourceScope,启动失败、关闭和卸载释放。不保存状态内存副本,不在停用时写回。
|
||||||
|
|
||||||
|
## 验证与待验收
|
||||||
|
|
||||||
|
Release 构建通过,0 警告、0 错误;70 项离线测试通过。新增四组验证覆盖:当前身份/指针复用/过期与清空/回放,双列表槽位边界和不可变名单,空条目替换及原生返回/标志保留,放行时所有参数保真及异常时只调用一次。已有生命周期测试继续覆盖启动回滚和重复启停。没有用离线代理调用冒充原生 Hook 验收。
|
||||||
|
|
||||||
|
首轮游戏检查只聚焦一个状态:重新加载后进入编号 8,保留默认 1257,开启后再触发对应的 Forced March。观察最近状态 ID、拦截次数及实际强制移动表现;关闭后再次触发同一状态应恢复正常。若当前场景使用另一个 ID,在折叠名单中仅选该已审计 ID再测试。开启前已有状态不作立即消除预期。
|
||||||
|
|
||||||
|
仍待确认:目标状态效果、未选状态和其他角色正常、关闭后后续同步、切区/回放/卸载。先取得首个目标效果反馈,再扩展逐项状态验收。无需为本版泛化重抓网络样本。
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# 两份 I-Ching-GL DLL 对比分析
|
||||||
|
|
||||||
|
日期:2026-10-11。范围:用户提供的两份 DLL 的静态文件、程序集元数据、接口与关键方法体可读性。原件未修改,目标程序集未执行,插件运行代码及已部署 0.1.5 未改动。
|
||||||
|
|
||||||
|
## 身份和构建差异
|
||||||
|
|
||||||
|
| 项目 | I-Ching-GL.dll(第一份) | I-Ching-GL (2).dll(第二份) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 大小 | 5,811,712 字节 | 7,894,528 字节 |
|
||||||
|
| 程序集 / 文件版本 | 0.1.6.6 | 0.1.6.6 |
|
||||||
|
| InformationalVersion | 0.1.6.6+6e402483b7b629bdd57821bb9f4eb9f08701de0e | 相同 |
|
||||||
|
| 目标 | .NET 10,AMD64 DLL | 相同 |
|
||||||
|
| Dalamud 引用 | 15.0.3.6 | 15.0.3.6 |
|
||||||
|
| FFXIVClientStructs 引用 | 7.56.2.9458 | 7.56.2.9418 |
|
||||||
|
| ECommons 引用 | 3.2.1.17 | 3.2.1.17 |
|
||||||
|
| MVID | 36234178-ce88-4985-a78c-d2148d83f493 | bdcb5604-0284-4e0f-b9c9-fcaa9694c9c8 |
|
||||||
|
| 顶层类型数 | 1,933 | 2,724 |
|
||||||
|
| 含嵌套类型数 | 2,024 | 2,833 |
|
||||||
|
| 全 ASCII 名称类型数 | 152 | 200 |
|
||||||
|
|
||||||
|
第一份 SHA256:`DA648072A2CE0856B4884A332EFAC063FC8F0E03236D7D4DBC9334BF0B2A2AA8`。
|
||||||
|
|
||||||
|
第二份 SHA256:`CE269139FDFDB136BE382E10BE78E6E0BC346DEB335E9D99DF2E80097BD76CBF`。
|
||||||
|
|
||||||
|
结论:不是重复文件,也不是仅改了文件名。尽管版本与构建标识相同,依赖及声明有实际差异。不能仅用文件更大、复制时间或共享版本号判断新旧顺序和修复情况。本机当前 FFXIVClientStructs 为 7.56.2.9136,与两份引用版本均不同。
|
||||||
|
|
||||||
|
## 已确认的声明差异
|
||||||
|
|
||||||
|
递归比较两份全 ASCII 名称类型的基类、接口、字段(含偏移/标志)、方法签名及声明标志,不比较不可读的方法实现。
|
||||||
|
|
||||||
|
第二份独有的命名类型包括:
|
||||||
|
|
||||||
|
- `NekoVerifierLib`:NekoVerifierMain、Service、VerifyWindow、VerifyClient、PlayerData 及协议类型等。
|
||||||
|
- `OtpNet`:HOTP/TOTP、密钥和时间校正相关类型。
|
||||||
|
- `RSAsharp`:RSA_PEM、RSA_Util。
|
||||||
|
- `DataSender` 和额外编译器生成类型。
|
||||||
|
|
||||||
|
第二份额外引用了 System.Net.Http、System.Security.Cryptography、System.Runtime.Numerics、System.Linq.Expressions、System.Text.Encoding.Extensions、Microsoft.Win32.Registry;第一份引用集合未包含这些程序集。
|
||||||
|
|
||||||
|
共有类型中可见差异只有四处:
|
||||||
|
|
||||||
|
1. `SamplePlugin.Plugin`:第二份多了静态 `PVPWhiteList` 字段及静态构造器。
|
||||||
|
2. `SamplePlugin.Windows.ConfigWindow`:第二份多了 `ExecuteBoardMarketInternal(string,string)` 方法声明。
|
||||||
|
3. ConfigWindow 的编译器生成类型:DrawCommandGenerator 相关成员编号由 77 变为 78。
|
||||||
|
4. PrivateImplementationDetails:第二份多了一项长度为 15 的静态数据声明。
|
||||||
|
|
||||||
|
命名表明第二份包含额外验证/密码相关组件及少量插件入口差异;仅凭声明不能确定其运行时是否被调用、传送什么数据、PVPWhiteList 的内容或作用,亦不能确定是原作者不同打包还是其他来源处理。类型数增加不等于增加同等数量的游戏功能。
|
||||||
|
|
||||||
|
## 重点功能对照
|
||||||
|
|
||||||
|
| 功能 | 两份 DLL 的可核实结果 | 对重建的意义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 14 目标半径 | ActorRadiusHook 的字段、方法、委托声明一致,Detour 为 float(ulong,byte) | 没有可读取的新落点公式,不能判断哪份修复了 16461 |
|
||||||
|
| 9 高度偏移 | YMove、两条移动委托、层数相关声明一致 | 无法从这两份确认偏移索引或调用后恢复行为;保持已要求的按钮交互 |
|
||||||
|
| 16 突进无位移 | 两份均保留旧五参数委托声明 | 不能替代当前已实测的六参数共用入口 |
|
||||||
|
| 8 状态处理 | StatusCheck、普通/GL 委托、结构布局声明一致 | 第二份未补出可读取的签名值或网络长度依据 |
|
||||||
|
| 其他 Hook 与配置 | 两份 SamplePlugin.Hook 类型(含嵌套声明)及 Configuration 没有上述元数据差异 | 声明相同不代表函数体相同,不足以回退已验证功能 |
|
||||||
|
|
||||||
|
两份都保留状态结构的旧声明矛盾:EntryData 偏移 20、固定 240 字节;条目声明 Size=8,但偏移 8 的 uint 字段占到 12 字节。它是插件声明,不是当前游戏包布局的可靠证明。8 继续保持未适配。
|
||||||
|
|
||||||
|
## 方法体可读性与独立检查
|
||||||
|
|
||||||
|
在相同的 21 个重点方法中:第一份 18 个读取错误、3 个明显可疑;第二份 19 个读取错误、2 个明显可疑。第二份曾有一个 CombineOffsets 方法被 Cecil 形式上解析成功,但 castclass、stsfld 和分支等指令缺少必须的操作数,不能作为有效 IL。审计脚本已增加缺失操作数检查。
|
||||||
|
|
||||||
|
另使用 .NET 自带 System.Reflection.Metadata/PEReader 独立读取方法头,避免仅凭 Cecil 报错下结论:
|
||||||
|
|
||||||
|
| 方法 | 第一份 | 第二份 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| SendNormalMoveDetour | Invalid method header: 0xD1 | Invalid method header: 0x55 |
|
||||||
|
| SendCombatMoveDetour | Invalid method header: 0xA1 | Invalid method header: 0xF1 |
|
||||||
|
| ActorRadiusDetour | 方法头可形式上解析为 20 字节,但 Cecil 无法解析其 IL | 方法头可形式上解析为 45 字节,但 Cecil 无法解析其 IL |
|
||||||
|
|
||||||
|
两份都存在大量非 ASCII 混淆名称、异常关键方法体以及相同业务名称但链接到不同随机程序集名的资源。整体表现与方法体保护/混淆一致;未确定具体保护方案,未证明每个异常的原因,也不能仅由此排除损坏。未将形式上能读到的随机 IL 当作有效算法,未执行初始化器。
|
||||||
|
|
||||||
|
## 当前能用与不能用的内容
|
||||||
|
|
||||||
|
可用:对照接口形状、结构声明、依赖版本、存在的功能类型和不同打包的差异。
|
||||||
|
|
||||||
|
尚不可用:还原 14 的半径/落点公式、确认 9 的具体写入顺序、取得 8 的可用原生签名和网络包边界、判断验证组件的真实运行行为。第二份没有解除这些限制。
|
||||||
|
|
||||||
|
本次以分析为止,当前 0.1.5 的运行逻辑与部署不变。14 的实际落点仍应以 16461 的现场复测为准。
|
||||||
|
|
||||||
|
## 证据文件与复现
|
||||||
|
|
||||||
|
- `2026-10-11-original-dll-metadata.json`:第一份重点接口及读取结果。
|
||||||
|
- `2026-10-11-original-dll-2-metadata.json`:第二份相同范围。
|
||||||
|
- `2026-10-11-two-dll-comparison.json`:两份程序集信息、可读类型完整声明和差异。
|
||||||
|
|
||||||
|
在工作区根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./IC.Rebuild/Tools/ManagedAudit/Audit.ps1 -AssemblyPath './IC/I-Ching-GL (2).dll' -OutputPath './IC.Rebuild/diagnostics/2026-10-11-original-dll-2-metadata.json'
|
||||||
|
./IC.Rebuild/Tools/ManagedAudit/Compare.ps1 -FirstPath './IC/I-Ching-GL.dll' -SecondPath './IC/I-Ching-GL (2).dll' -OutputPath './IC.Rebuild/diagnostics/2026-10-11-two-dll-comparison.json'
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,456 @@
|
|||||||
|
{
|
||||||
|
"Source": "capture-018.jsonl",
|
||||||
|
"SHA256": "224AE627F6F6E613662745300C01493E411BDBE02CBA72366CCBD373DD28ECFF",
|
||||||
|
"MapSHA256": "2764186EA528587F15D07569816D413E77E8C38C572E4157787B3092BD6D1DB6",
|
||||||
|
"MapSource": "local cache \u002B \u672C\u673A\u6838\u5B9E 7 \u9879",
|
||||||
|
"Version": "2026.09.15.0000.0000",
|
||||||
|
"Records": 891,
|
||||||
|
"Up": 534,
|
||||||
|
"Down": 357,
|
||||||
|
"ManagerReplay": {
|
||||||
|
"Matches": true,
|
||||||
|
"Records": 891,
|
||||||
|
"SendReturns": 534,
|
||||||
|
"ObserverErrors": 0,
|
||||||
|
"ReadErrors": 0
|
||||||
|
},
|
||||||
|
"FirstTime": "2026-10-11T10:12:32.1453158+00:00",
|
||||||
|
"LastTime": "2026-10-11T10:13:52.6616441+00:00",
|
||||||
|
"FirstSequence": 1,
|
||||||
|
"LastSequence": 891,
|
||||||
|
"SequenceContinuous": true,
|
||||||
|
"MonotonicOrder": true,
|
||||||
|
"Complete": 891,
|
||||||
|
"Incomplete": 0,
|
||||||
|
"CaptureIssues": 0,
|
||||||
|
"PayloadBytes": 56360,
|
||||||
|
"MinPayload": 8,
|
||||||
|
"MaxPayload": 656,
|
||||||
|
"EmptyPayloads": 0,
|
||||||
|
"Known": 891,
|
||||||
|
"Unknown": 0,
|
||||||
|
"Parsed": 891,
|
||||||
|
"SupportedButRejected": 0,
|
||||||
|
"Groups": [
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 920,
|
||||||
|
"Name": "UP_UpdatePositionHandler",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 494,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 787,
|
||||||
|
"Name": "DOWN_Effect",
|
||||||
|
"Length": 124,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 52,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 671,
|
||||||
|
"Name": "DOWN_UpdateHate",
|
||||||
|
"Length": 72,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 697,
|
||||||
|
"Name": "DOWN_EffectResultBasic",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 51,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 558,
|
||||||
|
"Name": "DOWN_UpdateHater",
|
||||||
|
"Length": 264,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 50,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 516,
|
||||||
|
"Name": "DOWN_ActorControlSelf",
|
||||||
|
"Length": 40,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 48,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 238,
|
||||||
|
"Name": "DOWN_ObjectSpawn",
|
||||||
|
"Length": 64,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 38,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 913,
|
||||||
|
"Name": "UP_ActionSend",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 27,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 524,
|
||||||
|
"Name": "DOWN_NpcSpawn",
|
||||||
|
"Length": 656,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 18,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 974,
|
||||||
|
"Name": "DOWN_ActorDelete",
|
||||||
|
"Length": 8,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 12,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 614,
|
||||||
|
"Name": "DOWN_JobGauge",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 10,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 838,
|
||||||
|
"Name": "UP_PingReq",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 338,
|
||||||
|
"Name": "DOWN_PingRsp",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 8,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 607,
|
||||||
|
"Name": "DOWN_ActorControl",
|
||||||
|
"Length": 24,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 7,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 0,
|
||||||
|
"Opcode": 391,
|
||||||
|
"Name": "UP_ClientTrigger",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 5,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 131,
|
||||||
|
"Name": "DOWN_StatusEffectList",
|
||||||
|
"Length": 384,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 788,
|
||||||
|
"Name": "DOWN_UpdateClassInfo",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 4,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 305,
|
||||||
|
"Name": "DOWN_EffectResult",
|
||||||
|
"Length": 96,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 2,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 214,
|
||||||
|
"Name": "DOWN_FirstAttack",
|
||||||
|
"Length": 16,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": {
|
||||||
|
"Direction": 1,
|
||||||
|
"Opcode": 856,
|
||||||
|
"Name": "DOWN_ActorControlTarget",
|
||||||
|
"Length": 32,
|
||||||
|
"PayloadComplete": true,
|
||||||
|
"Parsed": true
|
||||||
|
},
|
||||||
|
"Count": 1,
|
||||||
|
"Rejection": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PingPairs": [
|
||||||
|
{
|
||||||
|
"Request": 3,
|
||||||
|
"Response": 4,
|
||||||
|
"Milliseconds": 83.2735
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 54,
|
||||||
|
"Response": 56,
|
||||||
|
"Milliseconds": 99.9771
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 222,
|
||||||
|
"Response": 224,
|
||||||
|
"Milliseconds": 116.8623
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 372,
|
||||||
|
"Response": 374,
|
||||||
|
"Milliseconds": 83.4204
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 530,
|
||||||
|
"Response": 532,
|
||||||
|
"Milliseconds": 83.4215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 683,
|
||||||
|
"Response": 684,
|
||||||
|
"Milliseconds": 83.2034
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 807,
|
||||||
|
"Response": 808,
|
||||||
|
"Milliseconds": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 870,
|
||||||
|
"Response": 871,
|
||||||
|
"Milliseconds": 83.2435
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ActionPairs": [
|
||||||
|
{
|
||||||
|
"Request": 31,
|
||||||
|
"Response": 36,
|
||||||
|
"Milliseconds": 166.3058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 70,
|
||||||
|
"Response": 73,
|
||||||
|
"Milliseconds": 150.0712
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 117,
|
||||||
|
"Response": 120,
|
||||||
|
"Milliseconds": 150.333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 151,
|
||||||
|
"Response": 155,
|
||||||
|
"Milliseconds": 133.0785
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 164,
|
||||||
|
"Response": 168,
|
||||||
|
"Milliseconds": 116.7569
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 179,
|
||||||
|
"Response": 182,
|
||||||
|
"Milliseconds": 116.3995
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 198,
|
||||||
|
"Response": 202,
|
||||||
|
"Milliseconds": 150.0261
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 206,
|
||||||
|
"Response": 214,
|
||||||
|
"Milliseconds": 266.8726
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 221,
|
||||||
|
"Response": 227,
|
||||||
|
"Milliseconds": 216.5409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 249,
|
||||||
|
"Response": 253,
|
||||||
|
"Milliseconds": 116.6774
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 282,
|
||||||
|
"Response": 287,
|
||||||
|
"Milliseconds": 166.5952
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 317,
|
||||||
|
"Response": 323,
|
||||||
|
"Milliseconds": 166.5913
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 357,
|
||||||
|
"Response": 360,
|
||||||
|
"Milliseconds": 166.6749
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 394,
|
||||||
|
"Response": 398,
|
||||||
|
"Milliseconds": 151.2212
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 450,
|
||||||
|
"Response": 453,
|
||||||
|
"Milliseconds": 117.414
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 490,
|
||||||
|
"Response": 493,
|
||||||
|
"Milliseconds": 117.0401
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 523,
|
||||||
|
"Response": 526,
|
||||||
|
"Milliseconds": 116.5287
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 565,
|
||||||
|
"Response": 568,
|
||||||
|
"Milliseconds": 135.5363
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 619,
|
||||||
|
"Response": 621,
|
||||||
|
"Milliseconds": 100.0991
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 655,
|
||||||
|
"Response": 657,
|
||||||
|
"Milliseconds": 99.9365
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 713,
|
||||||
|
"Response": 716,
|
||||||
|
"Milliseconds": 117.0038
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 724,
|
||||||
|
"Response": 728,
|
||||||
|
"Milliseconds": 83.3655
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 763,
|
||||||
|
"Response": 765,
|
||||||
|
"Milliseconds": 116.5066
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 795,
|
||||||
|
"Response": 798,
|
||||||
|
"Milliseconds": 133.1891
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 812,
|
||||||
|
"Response": 814,
|
||||||
|
"Milliseconds": 117.6021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 819,
|
||||||
|
"Response": 822,
|
||||||
|
"Milliseconds": 99.5427
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Request": 823,
|
||||||
|
"Response": 826,
|
||||||
|
"Milliseconds": 166.6213
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Limitations": "No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 2026-10-12 阶段 2(0.1.14)协议补全、事件包分层与动作回包关联审计报告
|
||||||
|
|
||||||
|
- 审计日期:2026-10-12
|
||||||
|
- 目标二进制:`E:\SteamLibrary\steamapps\common\FINAL FANTASY XIV Online\game\ffxiv_dx11.exe`
|
||||||
|
- 客户端版本:`2026.09.15.0000.0000`
|
||||||
|
- 二进制 SHA256:`5BBC501DD5C7F22FD61A11D08C25356041D878DB7CD83203ADAE393E4DFACC44`
|
||||||
|
- 样本文件:`capture-018.jsonl`(SHA256 `407B6933E0886AA567C7A585B2732BA8064DC5B731987EB3744F14F4296AF639`,891 条完整记录)
|
||||||
|
- 映射缓存:`opcodes.json`(SHA256 `6B40585D43CC83C31EC16A8DD9CF2B3008D3084752472BD5E89B4D20C6C14F7E`)
|
||||||
|
- 离线回放报告:`diagnostics/2026-10-12-capture-018-v0114.json`
|
||||||
|
|
||||||
|
## 1. 上行构造与发送包装层核实
|
||||||
|
|
||||||
|
### 1.1 原生发送结构体与包装函数(为阶段 3 M4 预备证据)
|
||||||
|
- `ZoneClient.SendPacket`:`0x1D03040`(内部转调 `ZoneUp` `0x1D718C0`)。
|
||||||
|
- `NetworkModuleProxy` 发送包装层:`0x1CFD090`、`0x1CFD140`(由 `0x4810E0` 调用)、`0x1CFD170`(由 `0x481100` 调用)、`0x1CFD1A0`(由 `0x4810C0` 调用)。
|
||||||
|
- 原生发送结构体(`rdx`):`+0x00 uint Opcode`、`+0x08 ulong TotalSizeWithHeader`(等于 `payloadLength + 0x10`)、`+0x20 byte[] Payload`。
|
||||||
|
|
||||||
|
### 1.2 `UP_ActionSend`(`913` / `0x391`,载荷 32 字节)與 `UP_ActionSendPos`(`891` / `0x37B`,载荷 24 字节)
|
||||||
|
- 构造调用链:`0x8E21A3` → `0xB36F50`(`UP_ActionSend`,总长 `0x30` = 载荷 `0x20`);`0x8E2255` → `0xB37050`(`UP_ActionSendPos`,总长 `0x28` = 载荷 `0x18`,旧版 `UpPacketStructs.cs` 记为 `0x20` 已过期)。
|
||||||
|
- 共用头部(`+0x00..+0x0B`):
|
||||||
|
- `+0x00 uint ActionId`
|
||||||
|
- `+0x04 byte CastBuff`
|
||||||
|
- `+0x05 byte ActionKind`
|
||||||
|
- `+0x06 ushort RequestId`(样本 27 条递增为 `179..205`)
|
||||||
|
- `+0x08 ushort Facing`
|
||||||
|
- `+0x0A ushort TargetFacing`
|
||||||
|
- `UP_ActionSend` 尾部:`+0x10 ulong TargetId`、`+0x18 uint Arg`(共 32B)。
|
||||||
|
- `UP_ActionSendPos` 尾部:`+0x0C float X`、`+0x10 float Y`、`+0x14 float Z`(共 24B,校验有限浮点数)。
|
||||||
|
|
||||||
|
### 1.3 `UP_ClientTrigger`(`391` / `0x187`,载荷 32 字节)
|
||||||
|
- 构造器:`0xB352F0`、`0xB353B0`、`0xB35550`(总长 `0x30` = 载荷 `0x20`)。
|
||||||
|
- 载荷布局:`+0x00 uint CommandId`、`+0x04..+0x10 uint Arg0..Arg3`、`+0x18 ulong TargetId`。
|
||||||
|
|
||||||
|
### 1.4 P2 变长事件包冲突解决(`UP_EventFinish*` 与 `UP_EventAction*`)
|
||||||
|
- 旧版 `UpPacketStructs.cs` 将 `UP_EventFinish` 与 `UP_EventAction` 写成单一固定长度结构,与在线 `opcodes.json` 的变长后缀表冲突。
|
||||||
|
- 原生反汇编确认客户端按 `ArgCount`(`r9b`)分成 8 档固定长度 Opcode 发送:
|
||||||
|
- `UP_EventFinish*`(`0xB32C5C..0xB33022`):`UP_EventFinish`(`982`,`cap=2`,16B)、`4`(`607`,24B)、`8`(`516`,40B)、`16`(`856`,72B)、`32`(`283`,136B)、`64`(`787`,264B)、`128`(`779`,520B)、`255`(`658`,1032B,含 4B 对齐尾填充)。
|
||||||
|
- `UP_EventAction*`(`0xB33050..0xB333DA`):`UP_EventAction`(`140`,`cap=2`,16B)、`4`(`778`,24B)、`8`(`938`,40B)、`16`(`957`,72B)、`32`(`637`,136B)、`64`(`988`,264B)、`128`(`703`,520B)、`255`(`705`,1032B)。
|
||||||
|
- 共用 8 字节头部:`+0x00 uint EventId`、`+0x04 ushort SceneId`、`+0x06 byte State`、`+0x07 byte ArgCount`(校验 `ArgCount <= capacity`),`+0x08` 起为 `uint Args[capacity]`。
|
||||||
|
|
||||||
|
## 2. 下行 `PacketDispatcher`(`0x18D9500`,跳表 `0x18DD980`)核实
|
||||||
|
|
||||||
|
| 名称 | Opcode | 长度 | 跳表入口与消费函数 | 已核实字段与旧版差异 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `DOWN_ActorControl` | `607` (`0x25F`) | 24B (`0x18`) | 预处理 `0x18DA1EC`;跳表 `0x18DAABB` → `0x18DAB03` → `0xB3A350` | `+0x00 ushort Category`,`+0x04..+0x10 uint Arg0..Arg3`。当 `Category == 0x22` 时,采集发生在 `0x18DA1EC` 减去会话值 `esi` 之前 |
|
||||||
|
| `DOWN_ActorControlSelf` | `516` (`0x204`) | 40B (`0x28`) | 跳表 `0x18DAB9E` → `0x18DABE6` → `0xB3A350` | `+0x00 ushort Category`,`+0x04..+0x20 uint Arg0..Arg7`(旧版 32B 仅 6 个参数,现为 8 个 `uint`) |
|
||||||
|
| `DOWN_ActorControlTarget` | `856` (`0x358`) | 32B (`0x20`) | 跳表 `0x18DAB4D` → `0x18DAB95` → `0xB3A350` | `+0x00 ushort Category`,`+0x04..+0x10 uint Arg0..Arg3`,`+0x18 ulong TargetId` |
|
||||||
|
| `DOWN_Effect` | `787` (`0x313`) | 124B (`0x7C`) | 预处理 `0x18DA4CC`;跳表 `0x18DA9C6` → `0xB3A0F0` → `0x902C20` | 旧版 120B 已变为 124B。`+0x08 uint RawActionId`(采集时含会话偏移 `esi`),`+0x0C uint Sequence`,`+0x10 float LockTime`(有限数),`+0x18 ushort RequestId`,`+0x1C ushort AnimationId`,`+0x1F byte ActionKind`,`+0x21 byte TargetCount`(`<=1`),`+0x70 ulong TargetId` |
|
||||||
|
| `DOWN_EffectResult` | `305` (`0x131`) | 96B (`0x60`) | 跳表 `0x18DAF52` → `0xB38CA0` → `0x8A6BF0` | `+0x00 byte Count`(`<=1`),`+0x04 uint Sequence`,`+0x0C uint CurrentHp`,`+0x10 uint MaxHp`,`+0x14 ushort Mp`,`+0x16 byte TargetIndex`,`+0x17 byte ClassJob`,`+0x18 byte Shield`,`+0x19 byte StatusCount`(`<=4`),`+0x1C` 起 4×16B 状态项(内部调用 `StatusManager.SetStatus` `0x8A6BF0`) |
|
||||||
|
| `DOWN_StatusEffectList` | `131` (`0x83`) | 384B (`0x180`) | 预处理 `0x18D97FC`;跳表 `0x18DAE4B` → `0xB385F0` → `0x8A78F0` | `+0x00 byte ClassJob`,`+0x01 byte Level`,`+0x04 uint CurrentHp`,`+0x08 uint MaxHp`,`+0x0C ushort Resource`,`+0x0E ushort MaxResource`,`+0x10 byte Shield`,`+0x14` 起 30×12B 状态槽(采集时 `+0x00 ushort RawStatusId` 尚未减去 `si`,`+0x04 float Timer` 校验有限数) |
|
||||||
|
| `DOWN_ActorDelete` | `974` (`0x3CE`) | 8B (`0x08`) | 跳表 `0x18DA75C` → `0xB37C00` | `+0x00 byte ObjectIndex` |
|
||||||
|
| `DOWN_ObjectSpawn` | `238` (`0xEE`) | 64B (`0x40`) | 跳表 `0x18DAD80` → `0xB4ADA0` (`0xB4B057`) | `+0x00 byte ObjectIndex`,`+0x01 byte Kind`,`+0x04 uint BaseId`,`+0x08 uint EntityId`,`+0x1C float Scale`,`+0x34 float X, +0x38 float Y, +0x3C float Z`(旧版坐标偏移 `0x30` 错误) |
|
||||||
|
| `DOWN_NpcSpawn` | `524` (`0x20C`) | 656B (`0x290`) | 跳表 `0x18DA6FA` → `0xB37A10` → `0x8CF750`(`rsi = payload + 0x10`) | `+0x40 uint BaseId`,`+0x54 uint OwnerId`,`+0x5C uint MaxHp`,`+0x60 uint CurrentHp`(旧版 HP 颠倒且 OwnerId 偏移错),`+0x7E byte ObjectIndex`,`+0x81 byte ObjectKind`,`+0x82 byte SubKind`,`+0x86 byte Level`,`+0x87 byte ClassJob`,`+0x200 float X, +0x204 float Y, +0x208 float Z` |
|
||||||
|
|
||||||
|
## 3. 动作发包与下行效果关联(`PacketCorrelator.Actions`)
|
||||||
|
|
||||||
|
- 关联依据:`UP_ActionSend`(32B)与 `UP_ActionSendPos`(24B)在 `+0x06` 写入递增非零 `ushort RequestId`;服务端返回的本地动作确认 `DOWN_Effect`(124B)在 `+0x18` 回传同一 `ushort RequestId`(其他角色/NPC 的 `DOWN_Effect` 该字段为 `0`)。
|
||||||
|
- 样本验证:`capture-018.jsonl` 中 27 条 `UP_ActionSend`(`RequestId = 179..205`)与 27 条 `DOWN_Effect`(`RequestId = 179..205`)100% 一一配对,往返耗时 `68.5 ms .. 138.3 ms`。
|
||||||
|
|
||||||
|
## 4. `capture-018.jsonl` 回放复核结果
|
||||||
|
|
||||||
|
- `Records = 891`(上行 `534`,下行 `357`)
|
||||||
|
- `Complete = 891`,`Known = 891`
|
||||||
|
- `Parsed = 891`(由 `0.1.13` 的 `679` 提升至 `891`,覆盖率 100%)
|
||||||
|
- `SupportedButRejected = 0`
|
||||||
|
- `PingPairs = 8`,`ActionPairs = 27`
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 阶段 3(0.1.15)M0 版本适配诊断与 M4 主动发送事务审计记录
|
||||||
|
|
||||||
|
日期:2026 年 10 月 12 日
|
||||||
|
客户端版本:国际服 `2026.09.15.0000.0000`
|
||||||
|
目标二进制:`ffxiv_dx11.exe`(SHA256 `5BBC501DD5C7F22FD61A11D08C25356041D878DB7CD83203ADAE393E4DFACC44`)
|
||||||
|
|
||||||
|
## 1. M0 统一版本适配清单与门控(`Core/AdaptationManifest.cs`)
|
||||||
|
|
||||||
|
针对全部 24 项保留功能及 M4 主动发送通道建立统一适配清单,按模块记录版本锁、SDK 偏移要求、原生签名及 RVA 证据:
|
||||||
|
|
||||||
|
| 诊断分类 (`AdaptationIssue`) | 判定条件 | 门控行为 (`AdaptationManifest.ApplyGate`) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `None` | 实现存在、版本/偏移与签名全部匹配且未发生运行异常 | 清除不可用门控,允许启用 |
|
||||||
|
| `MissingImplementation` | 模块尚未注册实现 | 保持不可用,拒绝启用 |
|
||||||
|
| `VersionMismatch` | `VersionLocked == true` 的模块(`2, 7, 8, 14, 16, 36` 及 M4 主动发送)在非 `2026.09.15.0000.0000` 客户端运行 | 自动关闭并标记不可用;非版本锁定模块(如 `1, 3, 9, 10, 11, 37`)与离线导入/导出不受影响 |
|
||||||
|
| `OffsetMismatch` | `RequiresSdkOffsets == true`(模块 `8`:依赖 `Character.StatusManager` `0x23B0` 与 `GameObject.EntityId` `0x78`)偏移不匹配 | 仅门控对应模块,不影响其他模块 |
|
||||||
|
| `SignatureMissing` | 任一必需签名在当前客户端匹配数为 `0` | 自动关闭并标记不可用,显示缺失签名名称与已核对 RVA |
|
||||||
|
| `MultipleMatches` | `RequireUnique == true` 的签名在 `.text` 段出现 `>1` 处匹配 | 拒绝盲挂,标记不可用并显示匹配数量(模块 `15` `0x8D6F30` 显式声明 `RequireUnique = false` 使用首匹配包装函数) |
|
||||||
|
| `RuntimeFault` | 运行期捕获异常并标记为 `ModuleState.Failed` | 保留具体异常详情并显示在 M0 诊断面板 |
|
||||||
|
|
||||||
|
本机 `ffxiv_dx11.exe` 签名扫描核验:21 条唯一签名(`RequireUnique = true`)在 `.text` 段均精确匹配 `1` 处;模块 `15` 包装签名匹配首处 `0x8D6F30`。
|
||||||
|
|
||||||
|
## 2. M4 原生主动发送链路反汇编核实
|
||||||
|
|
||||||
|
### 2.1 为什么不沿用旧 `IC/NetManager.cs` 的 `0x4810E0` 包装函数
|
||||||
|
|
||||||
|
反汇编 `0x4810E0`(旧签名 `48 8B 49 08 48 85 C9 74 08 45 33 C9`):
|
||||||
|
- `0x4810E0`: `mov rcx, [rcx+8]`(从 `NetworkModuleProxy*` `+0x08` 读取 `ReceiverCallback*`)
|
||||||
|
- `0x4810E4`: `test rcx, rcx; jz 0x4810F1`
|
||||||
|
- `0x4810E9`: `xor r9d, r9d; xor r8d, r8d; call 0x1CFD140`
|
||||||
|
- `0x4810F1`: `mov al, 1; add rsp, 28h; ret`
|
||||||
|
|
||||||
|
**缺陷**:`0x4810E0` 在调用 `0x1CFD140` 后无条件执行 `mov al, 1`,即使 `ReceiverCallback*` 为空或底层 `ZoneUp` 返回 `0`(断线/未就绪),也会恒定返回 `1`,把发送失败伪装成成功。
|
||||||
|
|
||||||
|
### 2.2 已核实的原生发送链(保留真实返回值)
|
||||||
|
|
||||||
|
1. `Framework.Instance()->NetworkModuleProxy`(偏移 `+0x1678`,对应原生 getter `0xD28F0`)。
|
||||||
|
2. `ReceiverCallback* = *(nint*)((nint)networkModuleProxy + 0x08)`。
|
||||||
|
3. `ReceiverSendPacket`(RVA `0x1CFD140`,唯一签名 `48 83 EC 38 48 8B 89 70 0A 00 00 ...`,1 处匹配):
|
||||||
|
- 读取 `ZoneClient* = *(nint*)(receiver + 0xA70)`;若为空直接 `xor al, al; ret` 返回 `0`。
|
||||||
|
- 非空时以 `priority = 0` 调用 `ZoneClient.SendPacket`(RVA `0x1D03040`,唯一签名 `48 83 EC 28 48 8B 89 98 00 00 00 ...`,1 处匹配)。
|
||||||
|
4. `ZoneClient.SendPacket`(`0x1D03040`):
|
||||||
|
- 检查 `*(nint*)(zoneClient + 0x98) != 0`,调用 `ZoneUp`(`0x1D718C0`),并将 `ZoneUp` 的真实 `byte` 返回码原样返回给调用方。
|
||||||
|
5. `ZoneUp`(`0x1D718C0`)在 `0x1D7198C` 读取发送缓冲区契约:
|
||||||
|
- `+0x00`: `uint Opcode`
|
||||||
|
- `+0x08`: `ulong TotalSize`(`payload.Length + 0x10`,即含 16 字节 IPC 头)
|
||||||
|
- `+0x20`: `byte[payload.Length]` 业务载荷
|
||||||
|
- 原生调用方(如 `0xB8A7AA` `UP_EventStart`、`0xB352F0` `UP_ClientTrigger`)均在栈上分配 `0xF60` 字节清零缓冲区传入;`NetworkManager.SendNative` 同样采用至少 `0xF60` 字节的清零固定缓冲区调用 `0x1CFD140`。
|
||||||
|
|
||||||
|
### 2.3 为什么不用合成 `UP_PingReq` 做主动发包测试
|
||||||
|
|
||||||
|
反汇编 `0x1D02776`(`UP_PingReq` 构造)与 `0x1D022E1`(`DOWN_PingRsp` 消费)证实:客户端在发送 `UP_PingReq` 与接收 `DOWN_PingRsp` 时会直接读写 `NetworkModule` 内部延迟测量状态(`[r14+0BCCh]`、`[r14+0C3Ch]`)。插件在外部合成 `UP_PingReq` 会干扰客户端原生 Ping 状态机。因此 M4 仅提供经过结构审计的业务包编码器(`UP_ActionSend`、`UP_ClientTrigger`、`UP_EventStart`、`UP_EventFinish*` 8 档、`UP_EventAction*` 8 档)。
|
||||||
|
|
||||||
|
## 3. M4 主动发送调度与请求响应事务(`Core/ActivePacketSender.cs`)
|
||||||
|
|
||||||
|
- **来源标注(`ActiveSendScope`)**:`ActivePacketDispatcher.Pump` 调用原生发送时开启线程本地 `ActiveSendScope`,`NetworkPipeline.Read` 自动将该次 `ZoneUp` 捕获标记为 `PacketOrigin.PluginActiveSend`,与游戏自身触发的 `PacketOrigin.NativeZone` 明确区分。
|
||||||
|
- **主线程调度与重入防护**:仅在 `Framework.Update` 主线程调用 `PumpActiveSends`;`NetworkPipeline.InCallback` 与 `inReceiveCallback` 禁止在任何网络回调中递归调用 `Pump`。
|
||||||
|
- **有界队列、限速与会话绑定**:默认最大排队 `32` 条、最小发送间隔 `100 ms`、排队超时 `2000 ms`、响应超时 `5000 ms`。每次入队与发送均核对 `SendSession`(角色在场、非切区、非占卜回放、`Territory`、`LocalAddress`、`LocalEntityId` 一致);切区或会话变更自动取消未完成请求。
|
||||||
|
- **显式事务状态机**:区分 `Queued`、`SentNative`、`RejectedNative`、`Confirmed`、`TimedOut`、`Canceled`。`Cancel` 仅能取消仍处于 `Queued` 的请求,绝不把取消排队伪装成撤回已发送原生包。
|
||||||
|
- **请求响应关联**:`UP_ActionSend` 自动等待同 `RequestId` 的 `DOWN_Effect`(`0x313`),`UP_EventStart` / `UP_EventFinish*` 支持等待同 `EventId` 的 `DOWN_EventStart` / `DOWN_EventFinish`,并在收到响应时计算真实往返耗时 `RoundTripMs`。
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 国服客户端兼容性与反汇编审计记录(涵盖 2026.09.15 与 2026.10.06)
|
||||||
|
|
||||||
|
- **日期**:2026 年 10 月 13 日
|
||||||
|
- **国服游戏路径**:`D:\FINAL FANTASY XIV`
|
||||||
|
- **国服更新后版本**:`2026.10.06.0000.0000`
|
||||||
|
- **国服更新后二进制**:`D:\FINAL FANTASY XIV\game\ffxiv_dx11.exe`(51,910,400 字节,SHA256 `958B78E278AD896493C484EB313FF605AB41EDEFEF74F92B0EE078E69B281BF9`)
|
||||||
|
- **国服更新前基线备份**:[`binaries/2026.09.15.0000.0000-cn/ffxiv_dx11.exe`](file:///e:/SapphireServer/Dalamud%20Dev/IC.Rebuild/binaries/2026.09.15.0000.0000-cn/ffxiv_dx11.exe)(51,881,216 字节,SHA256 `7BA28760BC53CBBBF66B63105FEA6C1BFE09CB6B1C6353FC8FB0FF03CC1A30F4`)
|
||||||
|
- **国服启动器**:`XIVLauncherCN`(位于 `C:\Users\AinaSnow\AppData\Roaming\XIVLauncherCN`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 国服更新前后 23 处特征码全量扫描对照
|
||||||
|
|
||||||
|
在更新后的国服二进制上执行扫描,**23/23 处特征码全部匹配成功**(22 处单一匹配,1 处 `NoBackswing` 包装入口匹配 2 处且首匹配正确):
|
||||||
|
|
||||||
|
| 序号 | 目标名称 | 模块 ID | 国服更新前 RVA (2026.09.15 CN) | 国服更新后 RVA (2026.10.06 CN) | 国际服新版 RVA (2026.10.06 Global) | 匹配数 | 状态 |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| 1 | `SpeedHook` | 1 | `0x8D5060` | `0x8D61A0` | `0x8D6AF0` | 1 | 匹配 |
|
||||||
|
| 2 | `AccelerationHook` | 2 | `0x1827FC0` | `0x1829480` | `0x182BB40` | 1 | 匹配 |
|
||||||
|
| 3 | `MovePermissionHook` | 3 | `0xE12940` | `0xE13F70` | `0xE16030` | 1 | 匹配 |
|
||||||
|
| 4 | `AntiKnockHook` | 4 | `0x909EB0` | `0x90AEB0` | `0x90B650` | 1 | 匹配 |
|
||||||
|
| 5 | `OnSendFlagHook` | 5 | `0x62A250` | `0x62B350` | `0x62B050` | 1 | 匹配 |
|
||||||
|
| 6 | `FallCheckHook` | 6 | `0x182BE84` | `0x182D344` | `0x182FA04` | 1 | 匹配 |
|
||||||
|
| 7 | `ForcedWalkHook` | 7 | `0x1947573` | `0x1948A83` | `0x194AFE3` | 1 | 匹配 |
|
||||||
|
| 8 | `StatusSetter` | 8 | `0x8A4E10` | `0x8A5F50` | `0x8A6A30` | 1 | 匹配 |
|
||||||
|
| 9 | `LocalPlayerPtr` | 8 | `0xB34FBA` | `0xB3660A` | `0xB37D0A` | 1 | 匹配 |
|
||||||
|
| 10 | `OnSendNormalMove` | 9 | `0x62A970` | `0x62BA70` | `0x62B770` | 1 | 匹配 |
|
||||||
|
| 11 | `OnSendCombatMove` | 9 | `0x62AA20` | `0x62BB20` | `0x62B820` | 1 | 匹配 |
|
||||||
|
| 12 | `ActorRadius` | 14 | `0x175360F` | `0x1754CDF` | `0x175720F` | 1 | 匹配 |
|
||||||
|
| 13 | `NativeTargetRingUpdate` | 14 | `0x7B43E0` | `0x7B54F0` | `0x7B52D0` | 1 | 匹配 |
|
||||||
|
| 14 | `NoBackswing` | 15 | `0x8D5150` | `0x8D6290` | `0x8D6BE0` | 2 (首匹配有效) | 匹配 |
|
||||||
|
| 15 | `ActionMovementStart` | 16 | `0x181E9B0` | `0x181FE70` | `0x1822530` | 1 | 匹配 |
|
||||||
|
| 16 | `SyncRecast` | 17 | `0x8ED480` | `0x8EE480` | `0x8EEC20` | 1 | 匹配 |
|
||||||
|
| 17 | `CastStaticAddress` | 19 | `0x1054C7` | `0x1054D7` | `0x104FF7` | 1 | 匹配 |
|
||||||
|
| 18 | `CastUpdate` | 19 | `0x8DE2C0` | `0x8DF400` | `0x8DFA60` | 1 | 匹配 |
|
||||||
|
| 19 | `SetScale` | 29 | `0x897940` | `0x898A80` | `0x899560` | 1 | 匹配 |
|
||||||
|
| 20 | `ZoneUp` | 36 | `0x1D6DAD0` | `0x1D706B0` | `0x1D72B00` | 1 | 匹配 |
|
||||||
|
| 21 | `ZoneDown` | 36 | `0x1D6D650` | `0x1D70230` | `0x1D72680` | 1 | 匹配 |
|
||||||
|
| 22 | `ReceiverSendPacket` | M4 | `0x1CF9020` | `0x1CFA9B0` | `0x1CFD120` | 1 | 匹配 |
|
||||||
|
| 23 | `ZoneClientSendPacket` | M4 | `0x1CFEF30` | `0x1D008B0` | `0x1D03020` | 1 | 匹配 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 更新后关键原生反汇编核实
|
||||||
|
|
||||||
|
1. **M4 主动发送通道**:
|
||||||
|
- `ReceiverSendPacket`(`0x1CFA9B0`):`mov rcx, [rcx+0A70h]` 读取 `ZoneClient`,若空则返回 `0`;非空以 `priority=0` 调用 `ZoneClientSendPacket`(`0x1D008B0`)。
|
||||||
|
- `ZoneClientSendPacket`(`0x1D008B0`):`mov rcx, [rcx+98h]` 检查底层指针,调用 `ZoneUp`(`0x1D706B0`),保留真实返回码。
|
||||||
|
- `ZoneUp`(`0x1D706B0`):发送缓冲区契约(`+0x00 Opcode`、`+0x08 TotalSize`、`+0x20 Payload`)与旧版完全一致。
|
||||||
|
2. **模块 36 收包**:
|
||||||
|
- `ZoneDown`(`0x1D70230`):`cmp qword ptr [rcx+88h], 0`,消息解包偏移完全一致。
|
||||||
|
3. **模块 8 状态屏蔽**:
|
||||||
|
- `StatusSetter`(`0x8A5F50`):`cmp edx, 3Ch`(60 个状态栏槽位),槽位大小依然为 12 字节。
|
||||||
|
- `LocalPlayerPtr`(`0xB3660A`):虚表偏移 `+0x278`(`call qword ptr [rax+278h]`)完全一致。
|
||||||
|
4. **模块 2 最大加速度**:
|
||||||
|
- `AccelerationHook`(`0x1829480`):`cmp byte ptr [rcx+3Ch], 0`、`mov rdi, [rcx+20h]` 完全一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 国服环境与 Opcode
|
||||||
|
|
||||||
|
1. **客户端版本**:更新后 `ffxivgame.ver` 变为 `2026.10.06.0000.0000`,与代码中 `PacketParser.AuditedClientVersion` 完美吻合,门控全通。
|
||||||
|
2. **Opcode 配置**:本地 `opcodes.json` 中已含有 `"2026.10.06.0000.0000"`(`UP_PingReq: 137`,`DOWN_PingRsp: 338`),Ping 与收发包分析立即就绪。
|
||||||
|
3. **开发插件加载**:`XIVLauncherCN` 的 `dalamudConfig.json` 中已注册 `ICRebuild.dll`。
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 客户端更新补丁 2026.10.06 完整适配与反汇编审计记录
|
||||||
|
|
||||||
|
- **日期**:2026 年 10 月 13 日
|
||||||
|
- **新客户端版本**:国际服 `2026.10.06.0000.0000`
|
||||||
|
- **新目标二进制**:`ffxiv_dx11.exe`(51,890,944 字节,SHA256 `B38840761E2677128B6BEC1CEAEE3F2870CD84E1D38AB7958216373058383780`)
|
||||||
|
- **旧客户端基线**:国际服 `2026.09.15.0000.0000`(51,878,744 字节,SHA256 `5BBC501DD5C7F22FD61A11D08C25356041D878DB7CD83203ADAE393E4DFACC44`)
|
||||||
|
- **旧二进制备份路径**:[`binaries/2026.09.15.0000.0000/ffxiv_dx11.exe`](file:///e:/SapphireServer/Dalamud%20Dev/IC.Rebuild/binaries/2026.09.15.0000.0000/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 原生二进制签名全量扫描对比
|
||||||
|
|
||||||
|
在新版二进制 `ffxiv_dx11.exe`(`2026.10.06.0000.0000`)上对所有 23 处特征码进行完整静态扫描,**23/23 处特征码全部在新二进制中唯一定位成功**(其中 22 处为单一匹配,1 处 `NoBackswing` 包装入口匹配 2 处且首匹配正确)。
|
||||||
|
|
||||||
|
在扫描过程中,修正了 [`AdaptationManifest.cs`](file:///e:/SapphireServer/Dalamud%20Dev/IC.Rebuild/Core/AdaptationManifest.cs) 中 `ForcedWalkHook` 的签名字符串笔误(`????` 修正为标准的 `?? ??` 分隔通配符),成功定位到新 RVA `0x194AFE3`。
|
||||||
|
|
||||||
|
### 23 处 Hook 与函数入口 RVA 偏移位移对照表
|
||||||
|
|
||||||
|
| 序号 | 目标名称 | 模块 ID | 旧版本 RVA (2026.09.15) | 新版本 RVA (2026.10.06) | RVA 偏移量 | 匹配数 | 状态 |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| 1 | `SpeedHook` | 1 | `0x8D6E40` | `0x8D6AF0` | -0x350 | 1 | 匹配 |
|
||||||
|
| 2 | `AccelerationHook` | 2 | `0x182BB20` | `0x182BB40` | +0x20 | 1 | 匹配 |
|
||||||
|
| 3 | `MovePermissionHook` | 3 | `0xE16130` | `0xE16030` | -0x100 | 1 | 匹配 |
|
||||||
|
| 4 | `AntiKnockHook` | 4 | `0x90BC90` | `0x90B650` | -0x640 | 1 | 匹配 |
|
||||||
|
| 5 | `OnSendFlagHook` | 5 | `0x62AF60` | `0x62B050` | +0xF0 | 1 | 匹配 |
|
||||||
|
| 6 | `FallCheckHook` | 6 | `0x182F9E4` | `0x182FA04` | +0x20 | 1 | 匹配 |
|
||||||
|
| 7 | `ForcedWalkHook` | 7 | `0x194AEC3` | `0x194AFE3` | +0x120 | 1 | 匹配 |
|
||||||
|
| 8 | `StatusSetter` | 8 | `0x8A6BF0` | `0x8A6A30` | -0x1C0 | 1 | 匹配 |
|
||||||
|
| 9 | `LocalPlayerPtr` | 8 | `0xB37D7A` | `0xB37D0A` | -0x70 | 1 | 匹配 |
|
||||||
|
| 10 | `OnSendNormalMove` | 9 | `0x62B680` | `0x62B770` | +0xF0 | 1 | 匹配 |
|
||||||
|
| 11 | `OnSendCombatMove` | 9 | `0x62B730` | `0x62B820` | +0xF0 | 1 | 匹配 |
|
||||||
|
| 12 | `ActorRadius` | 14 | `0x175728F` | `0x175720F` | -0x80 | 1 | 匹配 |
|
||||||
|
| 13 | `NativeTargetRingUpdate` | 14 | `0x7B5430` | `0x7B52D0` | -0x160 | 1 | 匹配 |
|
||||||
|
| 14 | `NoBackswing` | 15 | `0x8D6F30` | `0x8D6BE0` | -0x350 | 2 (首匹配有效) | 匹配 |
|
||||||
|
| 15 | `ActionMovementStart` | 16 | `0x1822510` | `0x1822530` | +0x20 | 1 | 匹配 |
|
||||||
|
| 16 | `SyncRecast` | 17 | `0x8EF260` | `0x8EEC20` | -0x640 | 1 | 匹配 |
|
||||||
|
| 17 | `CastStaticAddress` | 19 | `0x104F17` | `0x104FF7` | +0xE0 | 1 | 匹配 |
|
||||||
|
| 18 | `CastUpdate` | 19 | `0x8E00A0` | `0x8DFA60` | -0x640 | 1 | 匹配 |
|
||||||
|
| 19 | `SetScale` | 29 | `0x899720` | `0x899560` | -0x1C0 | 1 | 匹配 |
|
||||||
|
| 20 | `ZoneUp` | 36 | `0x1D718C0` | `0x1D72B00` | +0x1240 | 1 | 匹配 |
|
||||||
|
| 21 | `ZoneDown` | 36 | `0x1D71440` | `0x1D72680` | +0x1240 | 1 | 匹配 |
|
||||||
|
| 22 | `ReceiverSendPacket` | M4 | `0x1CFD140` | `0x1CFD120` | -0x20 | 1 | 匹配 |
|
||||||
|
| 23 | `ZoneClientSendPacket` | M4 | `0x1D03040` | `0x1D03020` | -0x20 | 1 | 匹配 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心原生入口反汇编与结构偏移核实
|
||||||
|
|
||||||
|
对关键功能的原生反汇编指令和内存结构进行逐一反编译核验:
|
||||||
|
|
||||||
|
1. **M4 主动发包通道结构不变**:
|
||||||
|
- `ReceiverSendPacket`(`0x1CFD120`):依然检查 `*(nint*)(rcx + 0xA70)`(`ZoneClient` 指针),为空则直接返回 `0`;非空则调用 `ZoneClientSendPacket`。
|
||||||
|
- `ZoneClientSendPacket`(`0x1D03020`):依然检查 `*(nint*)(rcx + 0x98)`,调用底层网络发送函数 `ZoneUp`(`0x1D72B00`)。
|
||||||
|
- `ZoneUp`(`0x1D72B00`):发送数据缓冲区协议完全未变(`+0x00`: Opcode, `+0x08`: TotalSize, `+0x20`: Payload)。
|
||||||
|
2. **模块 36 原始收发包 Hook 结构不变**:
|
||||||
|
- `ZoneDown`(`0x1D72680`):检查 `*(nint*)(rcx + 0x88)`,下行载荷解包偏移完全一致。
|
||||||
|
3. **模块 8 状态屏蔽结构不变**:
|
||||||
|
- `StatusSetter`(`0x8A6A30`):汇编代码仍为 `83 FA 3C`(`cmp edx, 3Ch` 即 60 个状态栏槽位),槽位大小依然为 12 字节。
|
||||||
|
- `LocalPlayerPtr`(`0xB37D0A`):依然调用虚函数表 `+0x278`(`FF 90 78 02 00 00`),读取本地玩家。
|
||||||
|
4. **模块 2 最大加速度结构不变**:
|
||||||
|
- `AccelerationHook`(`0x182BB40`):依然核对偏移 `+0x20`、`+0x3C`、`+0x44` 与 owner `+0x3E0`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 网络协议与 Opcode 变动核实
|
||||||
|
|
||||||
|
SE 在本次 `2026.10.06` 补丁中对 Opcode 编号进行了全量重排:
|
||||||
|
- `UP_PingReq`:旧版为 `838`(`0x346`),新版更新为 `137`(`0x89`)。
|
||||||
|
- `DOWN_PingRsp`:依然为 `338`(`0x152`)。
|
||||||
|
- 由于模块 36 属于通用二进制载荷抓取,无需等待所有 Opcode 逆向即可正常捕获和导出所有数据包。
|
||||||
|
- `XIVLauncher` 本地配置 `opcodes.json` 已追加 `2026.10.06.0000.0000` 节点,Ping 对齐功能可立即正常显示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 双版本向后兼容设计(Dual-Version Support)
|
||||||
|
|
||||||
|
为确保过渡平滑与回滚安全,代码更新为支持双审计版本:
|
||||||
|
- 当前审计版本(`AuditedClientVersion`):`2026.10.06.0000.0000`
|
||||||
|
- 兼容基线版本(`BaselineClientVersion`):`2026.09.15.0000.0000`
|
||||||
|
- `IsAuditedVersion(v)` 同时认可两者,未在审计名单中的未知版本(如 `2026.99.99`)继续受 M0 严格门控保护。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 自动化验证与部署
|
||||||
|
|
||||||
|
1. **测试套件**:84/84 项单元测试全部一次性通过(涵盖双版本适配清单核验、门控行为、网络管线、M4 原生缓冲区分组、状态阻断等)。
|
||||||
|
2. **发布与备份**:
|
||||||
|
- 旧版本 `0.1.15` 已备份至 [`deploy-backups/0.1.15-before-0.1.16/`](file:///e:/SapphireServer/Dalamud%20Dev/IC.Rebuild/deploy-backups/0.1.15-before-0.1.16/)。
|
||||||
|
- 新版本 `0.1.16.0` 已部署至 `%APPDATA%\XIVLauncher\dev\ICRebuild`。
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"ActionId": 2259,
|
||||||
|
"Name": "Ten",
|
||||||
|
"Timeline": 233,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 2261,
|
||||||
|
"Name": "Chi",
|
||||||
|
"Timeline": 234,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 2262,
|
||||||
|
"Name": "Shukuchi",
|
||||||
|
"Timeline": 231,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 2263,
|
||||||
|
"Name": "Jin",
|
||||||
|
"Timeline": 235,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 3573,
|
||||||
|
"Name": "Ley Lines",
|
||||||
|
"Timeline": 3972,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 18805,
|
||||||
|
"Name": "Ten",
|
||||||
|
"Timeline": 233,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": false,
|
||||||
|
"AffectsPosition": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 18806,
|
||||||
|
"Name": "Chi",
|
||||||
|
"Timeline": 234,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": false,
|
||||||
|
"AffectsPosition": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 18807,
|
||||||
|
"Name": "Jin",
|
||||||
|
"Timeline": 235,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": false,
|
||||||
|
"AffectsPosition": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29066,
|
||||||
|
"Name": "Guardian",
|
||||||
|
"Timeline": 11501,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29513,
|
||||||
|
"Name": "Shukuchi",
|
||||||
|
"Timeline": 231,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 34675,
|
||||||
|
"Name": "Starry Muse",
|
||||||
|
"Timeline": 11786,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": false,
|
||||||
|
"AffectsPosition": false
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
MATCH RVA=0x8E9980
|
||||||
|
PREFIX 48895C24104C894C242048894C240855565741544155415641574883EC608BEA0F29742450488BD9418BD08BCD418BF8E87B5DFFFF4C8B3D546A1B02448BE04D
|
||||||
|
00000001408E9980 mov [rsp+10h],rbx
|
||||||
|
00000001408E9985 mov [rsp+20h],r9
|
||||||
|
00000001408E998A mov [rsp+8],rcx
|
||||||
|
00000001408E998F push rbp
|
||||||
|
00000001408E9990 push rsi
|
||||||
|
00000001408E9991 push rdi
|
||||||
|
00000001408E9992 push r12
|
||||||
|
00000001408E9994 push r13
|
||||||
|
00000001408E9996 push r14
|
||||||
|
00000001408E9998 push r15
|
||||||
|
00000001408E999A sub rsp,60h
|
||||||
|
00000001408E999E mov ebp,edx
|
||||||
|
00000001408E99A0 movaps [rsp+50h],xmm6
|
||||||
|
00000001408E99A5 mov rbx,rcx
|
||||||
|
00000001408E99A8 mov edx,r8d
|
||||||
|
00000001408E99AB mov ecx,ebp
|
||||||
|
00000001408E99AD mov edi,r8d
|
||||||
|
00000001408E99B0 call 00000001408DF730h
|
||||||
|
00000001408E99B5 mov r15,[142AA0410h]
|
||||||
|
00000001408E99BC mov r12d,eax
|
||||||
|
00000001408E99BF test r15,r15
|
||||||
|
00000001408E99C2 je 00000001408E9B26h
|
||||||
|
00000001408E99C8 mov r14,[rsp+0D0h]
|
||||||
|
00000001408E99D0 xor esi,esi
|
||||||
|
00000001408E99D2 mov ecx,r12d
|
||||||
|
00000001408E99D5 call 0000000140834590h
|
||||||
|
00000001408E99DA mov r13,rax
|
||||||
|
00000001408E99DD test rax,rax
|
||||||
|
00000001408E99E0 je 00000001408E9B26h
|
||||||
|
00000001408E99E6 cmp ebp,5
|
||||||
|
00000001408E99E9 jne short 00000001408E9A1Fh
|
||||||
|
00000001408E99EB mov ecx,edi
|
||||||
|
00000001408E99ED call 0000000140834A90h
|
||||||
|
00000001408E99F2 mov [rsp+48h],rax
|
||||||
|
00000001408E99F7 test rax,rax
|
||||||
|
00000001408E99FA je 00000001408E9B26h
|
||||||
|
00000001408E9A00 movzx ecx,word ptr [rax+0Eh]
|
||||||
|
00000001408E9A04 test cx,cx
|
||||||
|
00000001408E9A07 je short 00000001408E9A1Fh
|
||||||
|
00000001408E9A09 mov edx,ecx
|
||||||
|
00000001408E9A0B lea rcx,[142ACBF20h]
|
||||||
|
00000001408E9A12 call 0000000140C4DD30h
|
||||||
|
00000001408E9A17 test al,al
|
||||||
|
00000001408E9A19 je 00000001408EA9F7h
|
||||||
|
00000001408E9A1F mov edx,[r13+4]
|
||||||
|
00000001408E9A23 test edx,edx
|
||||||
|
00000001408E9A25 je short 00000001408E9A53h
|
||||||
|
00000001408E9A27 mov rax,[142A98D28h]
|
||||||
|
00000001408E9A2E test rax,rax
|
||||||
|
00000001408E9A31 je short 00000001408E9A39h
|
||||||
|
00000001408E9A33 cmp byte ptr [rax+31h],1Fh
|
||||||
|
00000001408E9A37 je short 00000001408E9A53h
|
||||||
|
00000001408E9A39 mov r9b,1
|
||||||
|
00000001408E9A3C lea rcx,[142ACBF20h]
|
||||||
|
00000001408E9A43 xor r8d,r8d
|
||||||
|
00000001408E9A46 call 0000000140C4DD60h
|
||||||
|
00000001408E9A4B test al,al
|
||||||
|
00000001408E9A4D je 00000001408EA9F7h
|
||||||
|
00000001408E9A53 cmp r12d,4
|
||||||
|
00000001408E9A57 jne short 00000001408E9AB6h
|
||||||
|
00000001408E9A59 call 0000000140B79630h
|
||||||
|
00000001408E9A5E mov rcx,rax
|
||||||
|
00000001408E9A61 call 0000000140B86B80h
|
||||||
|
00000001408E9A66 test rax,rax
|
||||||
|
00000001408E9A69 je short 00000001408E9A7Fh
|
||||||
|
00000001408E9A6B mov rdx,[rax]
|
||||||
|
00000001408E9A6E mov rcx,rax
|
||||||
|
00000001408E9A71 call qword ptr [rdx+9A8h]
|
||||||
|
00000001408E9A77 test eax,eax
|
||||||
|
00000001408E9A79 jne 00000001408E9B2Bh
|
||||||
|
00000001408E9A7F call 0000000140B79630h
|
||||||
|
00000001408E9A84 mov rcx,rax
|
||||||
|
00000001408E9A87 call 0000000140B86C30h
|
||||||
|
00000001408E9A8C test rax,rax
|
||||||
|
00000001408E9A8F je short 00000001408E9AA5h
|
||||||
|
00000001408E9A91 mov rdx,[rax]
|
||||||
|
00000001408E9A94 mov rcx,rax
|
||||||
|
00000001408E9A97 call qword ptr [rdx+0A58h]
|
||||||
|
00000001408E9A9D test eax,eax
|
||||||
|
00000001408E9A9F jne 00000001408E9B2Bh
|
||||||
|
00000001408E9AA5 movzx eax,word ptr [r15+688h]
|
||||||
|
00000001408E9AAD test ax,ax
|
||||||
|
00000001408E9AB0 jne 00000001408E9DD5h
|
||||||
|
00000001408E9AB6 cmp ebp,11h
|
||||||
|
00000001408E9AB9 je 00000001408EB70Ch
|
||||||
|
00000001408E9ABF cmp ebp,10h
|
||||||
|
00000001408E9AC2 je 00000001408EB6E3h
|
||||||
|
00000001408E9AC8 cmp ebp,5
|
||||||
|
00000001408E9ACB je short 00000001408E9B48h
|
||||||
|
00000001408E9ACD cmp ebp,0Fh
|
||||||
|
00000001408E9AD0 je 00000001408E9E1Eh
|
||||||
|
00000001408E9AD6 cmp ebp,14h
|
||||||
|
00000001408E9AD9 jne 00000001408E9B63h
|
||||||
|
00000001408E9ADF lea rcx,[r15+960h]
|
||||||
|
00000001408E9AE6 call 00000001408B5310h
|
||||||
|
00000001408E9AEB test al,al
|
||||||
|
00000001408E9AED je short 00000001408E9B63h
|
||||||
|
00000001408E9AEF test r14,r14
|
||||||
|
00000001408E9AF2 je short 00000001408E9AFBh
|
||||||
|
00000001408E9AF4 mov dword ptr [r14],0C1h
|
||||||
|
00000001408E9AFB xor r9d,r9d
|
||||||
|
00000001408E9AFE lea rcx,[142AE6610h]
|
||||||
|
00000001408E9B05 xor r8d,r8d
|
||||||
|
00000001408E9B08 mov edx,0C1h
|
||||||
|
00000001408E9B0D call 0000000140E16130h
|
||||||
|
00000001408E9B12 test al,al
|
||||||
|
00000001408E9B14 je short 00000001408E9B26h
|
||||||
|
00000001408E9B16 movzx ecx,di
|
||||||
|
00000001408E9B19 call 0000000140BC4E70h
|
||||||
|
00000001408E9B1E test al,al
|
||||||
|
00000001408E9B20 je 00000001408EB729h
|
||||||
|
00000001408E9B26 mov eax,243h
|
||||||
|
00000001408E9B2B mov rbx,[rsp+0A8h]
|
||||||
|
00000001408E9B33 movaps xmm6,[rsp+50h]
|
||||||
|
00000001408E9B38 add rsp,60h
|
||||||
|
00000001408E9B3C pop r15
|
||||||
|
00000001408E9B3E pop r14
|
||||||
|
00000001408E9B40 pop r13
|
||||||
|
00000001408E9B42 pop r12
|
||||||
|
00000001408E9B44 pop rdi
|
||||||
|
00000001408E9B45 pop rsi
|
||||||
|
00000001408E9B46 pop rbp
|
||||||
|
00000001408E9B47 ret
|
||||||
|
Matches: 1
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"File": "capture-20261010-233625-cccab3b9c9bb427893d6effe41c910b7.jsonl",
|
||||||
|
"Total": 2048,
|
||||||
|
"Up": 92,
|
||||||
|
"Down": 1956,
|
||||||
|
"Matched": 1675,
|
||||||
|
"FullPayloads": 0,
|
||||||
|
"First": "2026-10-10T23:45:10.7130834+08:00",
|
||||||
|
"Last": "2026-10-10T23:47:24.1238529+08:00"
|
||||||
|
}
|
||||||
@@ -0,0 +1,634 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"ActionId": 94,
|
||||||
|
"Name": "Elusive Jump",
|
||||||
|
"Timeline": 406,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 96,
|
||||||
|
"Name": "Dragonfire Dive",
|
||||||
|
"Timeline": 408,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 112,
|
||||||
|
"Name": "Repelling Shot",
|
||||||
|
"Timeline": 364,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 155,
|
||||||
|
"Name": "Aetherial Manipulation",
|
||||||
|
"Timeline": 431,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 2262,
|
||||||
|
"Name": "Shukuchi",
|
||||||
|
"Timeline": 231,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 7386,
|
||||||
|
"Name": "Onslaught",
|
||||||
|
"Timeline": 5189,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 7419,
|
||||||
|
"Name": "Between the Lines",
|
||||||
|
"Timeline": 5238,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 7492,
|
||||||
|
"Name": "Hissatsu: Gyoten",
|
||||||
|
"Timeline": 5123,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 7493,
|
||||||
|
"Name": "Hissatsu: Yaten",
|
||||||
|
"Timeline": 5124,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 7506,
|
||||||
|
"Name": "Corps-a-corps",
|
||||||
|
"Timeline": 5148,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 7515,
|
||||||
|
"Name": "Displacement",
|
||||||
|
"Timeline": 5149,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 7571,
|
||||||
|
"Name": "Rescue",
|
||||||
|
"Timeline": 5458,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 7857,
|
||||||
|
"Name": "Hissatsu: Soten",
|
||||||
|
"Timeline": 5135,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8765,
|
||||||
|
"Name": "Onslaught",
|
||||||
|
"Timeline": 5189,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8777,
|
||||||
|
"Name": "Plunge",
|
||||||
|
"Timeline": 3812,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8787,
|
||||||
|
"Name": "Shoulder Tackle",
|
||||||
|
"Timeline": 394,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8802,
|
||||||
|
"Name": "Spineshatter Dive",
|
||||||
|
"Timeline": 407,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8812,
|
||||||
|
"Name": "Shukuchi",
|
||||||
|
"Timeline": 231,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8839,
|
||||||
|
"Name": "Repelling Shot",
|
||||||
|
"Timeline": 364,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8869,
|
||||||
|
"Name": "Aetherial Manipulation",
|
||||||
|
"Timeline": 431,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8890,
|
||||||
|
"Name": "Corps-a-corps",
|
||||||
|
"Timeline": 5148,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 8891,
|
||||||
|
"Name": "Displacement",
|
||||||
|
"Timeline": 5149,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 11389,
|
||||||
|
"Name": "Flying Frenzy",
|
||||||
|
"Timeline": 6571,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 11401,
|
||||||
|
"Name": "Loom",
|
||||||
|
"Timeline": 6592,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 16010,
|
||||||
|
"Name": "En Avant",
|
||||||
|
"Timeline": 7002,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 16461,
|
||||||
|
"Name": "Intervene",
|
||||||
|
"Timeline": 7017,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 16480,
|
||||||
|
"Name": "Stardiver",
|
||||||
|
"Timeline": 7050,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 17674,
|
||||||
|
"Name": "Backstep",
|
||||||
|
"Timeline": 6662,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 17694,
|
||||||
|
"Name": "Intervene",
|
||||||
|
"Timeline": 7017,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 17716,
|
||||||
|
"Name": "Rough Divide",
|
||||||
|
"Timeline": 6939,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 17728,
|
||||||
|
"Name": "High Jump",
|
||||||
|
"Timeline": 7048,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 17729,
|
||||||
|
"Name": "Stardiver",
|
||||||
|
"Timeline": 7050,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 17764,
|
||||||
|
"Name": "En Avant",
|
||||||
|
"Timeline": 7002,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 18325,
|
||||||
|
"Name": "J Kick",
|
||||||
|
"Timeline": 7235,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 24295,
|
||||||
|
"Name": "Icarus",
|
||||||
|
"Timeline": 8309,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 24401,
|
||||||
|
"Name": "Hell\u0027s Ingress",
|
||||||
|
"Timeline": 8361,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 24402,
|
||||||
|
"Name": "Hell\u0027s Egress",
|
||||||
|
"Timeline": 8404,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 25753,
|
||||||
|
"Name": "Primal Rend",
|
||||||
|
"Timeline": 8212,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 25762,
|
||||||
|
"Name": "Thunderclap",
|
||||||
|
"Timeline": 8213,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 25777,
|
||||||
|
"Name": "Forked Raiju",
|
||||||
|
"Timeline": 8257,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 27817,
|
||||||
|
"Name": "Hell\u0027s Ingress",
|
||||||
|
"Timeline": 8361,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 27818,
|
||||||
|
"Name": "Hell\u0027s Egress",
|
||||||
|
"Timeline": 8404,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 27834,
|
||||||
|
"Name": "Icarus",
|
||||||
|
"Timeline": 8309,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29065,
|
||||||
|
"Name": "Intervene",
|
||||||
|
"Timeline": 12243,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29066,
|
||||||
|
"Name": "Guardian",
|
||||||
|
"Timeline": 11501,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29079,
|
||||||
|
"Name": "Onslaught",
|
||||||
|
"Timeline": 12244,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29084,
|
||||||
|
"Name": "Primal Rend",
|
||||||
|
"Timeline": 12245,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29092,
|
||||||
|
"Name": "Plunge",
|
||||||
|
"Timeline": 12310,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29123,
|
||||||
|
"Name": "Rough Divide",
|
||||||
|
"Timeline": 12320,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29229,
|
||||||
|
"Name": "Seraph Strike",
|
||||||
|
"Timeline": 12137,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29261,
|
||||||
|
"Name": "Icarus",
|
||||||
|
"Timeline": 8309,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29399,
|
||||||
|
"Name": "Repelling Shot",
|
||||||
|
"Timeline": 12248,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29430,
|
||||||
|
"Name": "En Avant",
|
||||||
|
"Timeline": 7002,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29484,
|
||||||
|
"Name": "Thunderclap",
|
||||||
|
"Timeline": 8213,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29485,
|
||||||
|
"Name": "Meteodrive",
|
||||||
|
"Timeline": 11518,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29493,
|
||||||
|
"Name": "High Jump",
|
||||||
|
"Timeline": 11521,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29513,
|
||||||
|
"Name": "Shukuchi",
|
||||||
|
"Timeline": 231,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29515,
|
||||||
|
"Name": "Seiton Tenchu",
|
||||||
|
"Timeline": 11526,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29532,
|
||||||
|
"Name": "Hissatsu: Soten",
|
||||||
|
"Timeline": 5135,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29537,
|
||||||
|
"Name": "Zantetsuken",
|
||||||
|
"Timeline": 11530,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29550,
|
||||||
|
"Name": "Hell\u0027s Ingress",
|
||||||
|
"Timeline": 8361,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29660,
|
||||||
|
"Name": "Aetherial Manipulation",
|
||||||
|
"Timeline": 431,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29667,
|
||||||
|
"Name": "Crimson Cyclone",
|
||||||
|
"Timeline": 12350,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29699,
|
||||||
|
"Name": "Corps-a-corps",
|
||||||
|
"Timeline": 12352,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 29700,
|
||||||
|
"Name": "Displacement",
|
||||||
|
"Timeline": 12353,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 34646,
|
||||||
|
"Name": "Slither",
|
||||||
|
"Timeline": 11740,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 34684,
|
||||||
|
"Name": "Smudge",
|
||||||
|
"Timeline": 11793,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 36926,
|
||||||
|
"Name": "Shadowstride",
|
||||||
|
"Timeline": 11808,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 36934,
|
||||||
|
"Name": "Trajectory",
|
||||||
|
"Timeline": 11820,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 36951,
|
||||||
|
"Name": "Winged Glide",
|
||||||
|
"Timeline": 11821,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 37008,
|
||||||
|
"Name": "Aetherial Shift",
|
||||||
|
"Timeline": 11880,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 39184,
|
||||||
|
"Name": "Slither",
|
||||||
|
"Timeline": 11740,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 39190,
|
||||||
|
"Name": "World-swallower",
|
||||||
|
"Timeline": 11921,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 39210,
|
||||||
|
"Name": "Smudge",
|
||||||
|
"Timeline": 11793,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 41506,
|
||||||
|
"Name": "Epicycle",
|
||||||
|
"Timeline": 12140,
|
||||||
|
"PvP": true,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 44893,
|
||||||
|
"Name": "Shield Charge",
|
||||||
|
"Timeline": 12442,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 44898,
|
||||||
|
"Name": "Cloud Skim",
|
||||||
|
"Timeline": 12445,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 44930,
|
||||||
|
"Name": "Brutal Rage",
|
||||||
|
"Timeline": 12456,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActionId": 44931,
|
||||||
|
"Name": "Hawkish Talons",
|
||||||
|
"Timeline": 12457,
|
||||||
|
"PvP": false,
|
||||||
|
"PlayerAction": true,
|
||||||
|
"AffectsPosition": true
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user