Initial import of IC.Rebuild

This commit is contained in:
2026-10-13 22:55:05 +08:00
commit 16d61f7a5e
87 changed files with 17054 additions and 0 deletions
+455
View File
@@ -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);
}
}
}
+234
View File
@@ -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);
}
}
}
}
+32
View File
@@ -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;
}
+25
View File
@@ -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;
}
}
+37
View File
@@ -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; } = "未接入";
}
+17
View File
@@ -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);
}
+99
View File
@@ -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; }
}
+13
View File
@@ -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;
}
+71
View File
@@ -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;
}
}
+173
View File
@@ -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,[]);
}
}
}
+20
View File
@@ -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
View File
@@ -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();
}
}
+19
View File
@@ -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;
}
+32
View File
@@ -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;
}
+64
View File
@@ -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();
}
}
+23
View File
@@ -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);
}
+32
View File
@@ -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; }
}
}
+32
View File
@@ -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);
}
+26
View File
@@ -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);
}
}
+36
View File
@@ -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;
}