156 lines
8.7 KiB
Go
156 lines
8.7 KiB
Go
package routers
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
var _ = sync.Mutex{}
|
||
var _ = context.Background
|
||
|
||
//
|
||
// 室内扇区(sector_high_indoor_<IndoorId>)—— 服务端主动驱动,不需要 frida 注入器。
|
||
//
|
||
//
|
||
// ★★★ 这是 2026-09-27 之前服务端唯一的「协议上完全可行、但完全够不着」的通道:
|
||
// 6 个 indoor sector 的 MethodId 常量早就存在(MethodId.cs 3074 / 3813 / 3650 /
|
||
// 2162 / 1977 / 3885),但全仓零使用 —— 没有任何 Handler 推过它们。
|
||
// (⛔ 2026-09-27 订正:之前记成「连常量都没有」是错的,常量在,缺的是 Handler。)
|
||
// 通道归属:独立于 gadget 网格(68956797)/ sceneitem 网格(68969593)/ 单件摆放
|
||
// (68843189)/ PCG(无外部触发点)—— 是第五条通道。sector 只管「这块区域显不显示」,
|
||
// 子树里是 _lights / _decoration(灯、镜子、LOD 组等静态装饰)。
|
||
// 家具本体不归它管(挂在 GadgetInstanceRoot,走 F/A 通道),
|
||
// 所以室内完整还原 = 本通道开区域 + PropRestock 补家具,两条都要。
|
||
// IndoorId 从哪来:不需要客户端上报,也不需要 frida 导出
|
||
// WorldStateIndoorSectorConfig。服务端读 Configs/IndoorConfig.json,
|
||
// 按「离玩家最近的室内条目」推出 IndoorId。
|
||
// 实测对得上:客户端日志 Enter GpsBound: RaidId 23300888, IndoorId 61
|
||
// ⇔ IndoorConfig.Id=61 = "WowDonald's" @ (859,1847),玩家在 (866,1840),差 9.5 m。
|
||
// ★★ 2026-09-27 补证:推下去的到底是哪个号
|
||
// 1. 先由位置推出 IndoorId(上面这条,来自 IndoorConfig);
|
||
// 2. 再用 Configs/WorldStateIndoorSectorConfig.json 把 IndoorId 翻成
|
||
// 扇区 Id(17600002…17600109) —— 详见 IndoorSectorCatalog4229938。
|
||
// 扇区 Id ≠ IndoorId,是该表的 BelongingIndoorId 字段指回 IndoorId。
|
||
// 一个 IndoorId 可以有多个扇区(Spiko Hotel 7 个、ACD Nova Branch 9 个)。
|
||
// 3. 参数语义 2026-09-27 20:1x 已用 A/B 定案:就是扇区 Id。
|
||
// 同一轮推 9 个扇区 Id + 1 个裸 IndoorId,客户端只报错裸 IndoorId:
|
||
// #NoCreateIssue Nexus SetIndoorSectorState …(17600101,True)…(17600109,True),(76,True)
|
||
// + config not found for id 76 —— 9 个扇区 Id 全被接受。
|
||
// ⇒ alsoPushRawIndoorId 已默认关闭,别再打开。
|
||
// ⛑ SyncLoadIndoorSector 这条 9 个里成功 7 个(2026-09-27 20:4x 实机)。
|
||
// 客户端每轮固定报 2 条
|
||
// LoadSectorByName: nameHash:-1386073846 / -1386073848 not found,
|
||
// 时间戳与服务端每次推送逐秒对齐(20:41:45/:50/:55 …),hash 恒定不变 ⇒
|
||
// 不是随机错位,是「9 个扇区里有 2 个的场景对象不在当前已加载子场景里」。
|
||
// 另外 9 个 id 在 SetIndoorSectorState 里 0 条 config not found(全被接受)。
|
||
// ⇒ 通道是通的,剩下 2 个多半在别的楼层(ACD MapFloorList=[-1,1,2])。
|
||
// (hash 不是 FNV1/FNV1a/CRC32/djb2/sdbm/BKDR/RS/JS/PJW/ELF/DEK/AP/Java31/
|
||
// Murmur2/OAAT,已逐个反查过 44 个 SectorName,别再反查。)
|
||
// ★ 2026-09-27 20:4x 抖动根因 + 修复:ACD Nova Branch(Id=76) 与
|
||
// ACD快乐房(Id=129) 的 MiniMapPos 矩形互相重叠,纯几何最近邻
|
||
// 在重叠区每几秒 76↔129 跳一次 ⇒ 客户端反复建删 9 棵扇区子树。
|
||
// 修法两条:① 优先采信客户端 SyncChangeIndoor 上报的权威 indoorConfigId
|
||
// (preferClientReport);② 再叠 switchDelayMs 防抖(客户端自己也会
|
||
// 瞬时抖到 0,光靠 ① 治不了)。实测把玩家传送到 76 的非重叠地段后立刻稳定。
|
||
// ⛔ 覆盖上限:全表 44 扇区 / 约 25 场馆,全是剧情室内;普通店铺(麦当劳等)
|
||
// 一条都没有,走 PCG,本通道管不着。
|
||
// 线格式(C# 原生,无 lua 实现 —— 已从 lua_full 全文 grep 确认为 native):
|
||
// <code>
|
||
// S2C SyncLoadIndoorSector(68095220) = uint indoorSectorControlId
|
||
// → 4 字节裸 UInt32(单字段 args,无 marker)
|
||
// S2C SyncIndoorSectorControl(68971505) = Dictionary<uint, bool>
|
||
// → FF(marker)+ Int7 count(7bit,+1 偏置)
|
||
// + 每条 (UInt32 LE key + u8 0/1)
|
||
// </code>
|
||
// 依据:dump.cs UXRPCMethodArgs68095220.indoorSectorControlId /
|
||
// UXRPCMethodArgs68971505.indoorSectorControl;集合字段恒定带 marker(硬规则 182)。
|
||
// ★★ 2026-09-27 19:5x 实机订正(必须记住):
|
||
// count 一开始写成了 I32(照搬 UxContract.WriteDictionary 的框架默认
|
||
// UxCountEncoding.Int32),结果客户端把 4 字节计数当 varint 读,整条 payload 错位:
|
||
// Player.log 刷了 153 行
|
||
// SetIndoorSectorState: config not found for id 1694498816 / 1711341836 / …。
|
||
// 把收到的错位字节反推求解后,九个产物值与「FF + Int7(count) + U32 LE key」的模拟
|
||
// 结果逐个相等(差分全 0),而且「发 10 条 / 客户端只处理 9 条」正好对应
|
||
// varint(0x0A)=10 再 −1 ⇒ Int7。
|
||
// ⇒ 这条字段在客户端契约里是显式 [UxCollection(Count=Int7)],
|
||
// 不能用框架默认的 Int32(见 TOPIC_序列化UxContract.md 第 10 行)。
|
||
// ⇒ 以后再手写 any Dictionary payload,count 一律先试 Int7。
|
||
// 回滚:world.content.indoorSectors.enabled = false(不需要重新编译)。
|
||
//
|
||
func GameRouter_IndoorSector_IndoorSectorStateOf(ctx context.Context, session *GameSession) any {
|
||
// Router implementation for GameRouter_IndoorSector_IndoorSectorStateOf
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 重连 / 切场景:忘掉当前室内,下次重新判定(客户端也把扇区状态丢了)。
|
||
func GameRouter_IndoorSector_ResetIndoorSector(ctx context.Context, session *GameSession) any {
|
||
// Router implementation for GameRouter_IndoorSector_ResetIndoorSector
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 由 gadget 网格 tick 顺带驱动。玩家走进/离开某个 IndoorConfig 条目附近时,
|
||
// 推 SyncLoadIndoorSector + SyncIndoorSectorControl。
|
||
//
|
||
func GameRouter_IndoorSector_TickIndoorSectorAsync(ctx context.Context, session *GameSession, state any, px float32, pz float32) any {
|
||
// Router implementation for GameRouter_IndoorSector_TickIndoorSectorAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 把上一次推下去的扇区全部关掉(离开室内 / 换室内时调用)。
|
||
func GameRouter_IndoorSector_ClosePushedAsync(ctx context.Context, session *GameSession, owner any, previousIndoorId uint32) any {
|
||
// Router implementation for GameRouter_IndoorSector_ClosePushedAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// SyncLoadIndoorSector(68095220) —— 单个 UInt32,单字段 args 不带 marker。
|
||
func GameRouter_IndoorSector_PushLoadIndoorSectorAsync(ctx context.Context, session *GameSession, id uint32) any {
|
||
// Router implementation for GameRouter_IndoorSector_PushLoadIndoorSectorAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// SyncIndoorSectorControl(68971505) —— Dictionary<uint,bool>:
|
||
// FF marker + Int32 count(原始,非 7bit)+ 每条 (UInt32 key + u8)。
|
||
//
|
||
func GameRouter_IndoorSector_PushIndoorSectorControlAsync(ctx context.Context, session *GameSession, id uint32, on bool) any {
|
||
// Router implementation for GameRouter_IndoorSector_PushIndoorSectorControlAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// SyncIndoorSectorControl(68971505) —— Dictionary<uint,bool>,一次带多条:
|
||
// FF marker + Int32 count(原始,非 7bit)+ 每条 (UInt32 key + u8)。
|
||
// 一个 IndoorId 常常对应好几个扇区(Spiko Hotel 7 个、ACD Nova Branch 9 个),
|
||
// 所以批量推,避免连发 9 个包。
|
||
//
|
||
func GameRouter_IndoorSector_PushIndoorSectorControlBatchAsync(ctx context.Context, session *GameSession, pairs []any) any {
|
||
// Router implementation for GameRouter_IndoorSector_PushIndoorSectorControlBatchAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
type IndoorSectorSessionState struct {
|
||
SyncRoot any `json:"syncRoot"`
|
||
// 上一次推下去的 indoor id;0 = 当前判定在室外。
|
||
CurrentId uint32 `json:"currentId"`
|
||
// 进过至少一个室内(用于区分「还没判定过」和「判定为室外」)。
|
||
HasJudged bool `json:"hasJudged"`
|
||
LastPushMs int64 `json:"lastPushMs"`
|
||
WarnedEmpty bool `json:"warnedEmpty"`
|
||
WarnedNoRaid bool `json:"warnedNoRaid"`
|
||
Pushes int32 `json:"pushes"`
|
||
// 上一次真正推下去的扇区 Id 列表(离开时要逐条关掉)。
|
||
PushedSectors []uint32 `json:"pushedSectors"`
|
||
// 上一次是否额外推了「裸 IndoorId」那一路。
|
||
PushedRawId bool `json:"pushedRawId"`
|
||
WarnedNoSectorTable bool `json:"warnedNoSectorTable"`
|
||
//
|
||
// 防抖用:正在观察的候选 indoor id(0 = 没有候选)。
|
||
// 见 PrivateServerConfigStore 的 indoorSectors.switchDelayMs。
|
||
//
|
||
PendingId uint32 `json:"pendingId"`
|
||
// 候选 id 第一次被观察到的时刻(TickCount64)。
|
||
PendingSinceMs int64 `json:"pendingSinceMs"`
|
||
}
|
||
|