This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/internal/gameserver/routers/game_router_indoor_sector.go

156 lines
8.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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&lt;uint, bool&gt;
// → 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&lt;uint,bool&gt;:
// 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&lt;uint,bool&gt;,一次带多条:
// 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"`
}