package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // 场景物的「显式下发」通道 —— 把摊位 / 货箱 / 柴火 之类的烘焙场景物,从 // 「客户端按 40m 格从 blob 铺出来的占位壳」升级成「真的挂了 GameObject 的物体」。 // ════════════════════════════════════════════════════════════════════════════════════════ // ★ 为什么必须有这个文件(第 79 轮,2026-09-26) // 症状:服务端 push 的 40m 格窗口从 49 格一路放到 289 格,在用 hold 数 2498 → 4531 → 5094 // (数据层面全部成功、日志干净、帧率 73),**但玩家在集市里什么都看不见**。 // frida 实测(只读,`gym_tools/_r79_render.py` / `_r79_ecs.py`): // · 5094 个在用 hold 里,只有 157 个的 SceneItemObj 非空(3.1%)⇒ 93.7% 只是占位壳; // · 交叉表 _useECSRender × SceneItemObj: // ecs=false | go=true 18 // ecs=true | go=false 4975 ← 98% // ⇒ 走 ECS 渲染的 hold **不生成 GameObject**; // · 全部 5094 个的 _forceLODLevel 都 = Init,配合硬规则 8 的实测指纹 // (Lod0 = 服务端摆的 / Init = 客户端烘焙物)⇒ **它们全部来自客户端自己的 blob**, // 没有任何一个是服务端显式下发的; // · 客户端日志里那句 场景不支持ecs的group数量过多,可能会影响性能, levelConfigType:10, // count:2725(从进入世界第一秒就有、不是本改动引入)说明本场景的 ECS 渲染被关卡守卫关掉 ⇒ // 被判给 ECS 的 4975 件**没有任何东西去渲染它们** ⇒ 看不见。 // ⇒ 结论:PushSceneItemGridAsync 只推「哪几格」,客户端就从 blob 铺壳; // 要真正物化,必须走 aoiInfo.addInfos 显式逐对象下发。 // 这一点在仓库里早有明文(`BowlingWireContracts.cs:591`): // 「the explicit addInfos inside aoiInfo are what actually materialise an object // when the blob has nothing for that cell」 // 而**能用的健身房通道**(`GameRouter.Gadget.cs:137`)就是同一个模式:显式下发 + ForceLod0 = true // ——注释原话「ForceLod0 keeps a freshly spawned prop from being culled to a billboard」, // 并且用**客户端 blob 的真实 uid** 当 uniqueId,「so the entity the server places is the same // instance the client would have built itself」。 // ════════════════════════════════════════════════════════════════════════════════════════ // ★ 所以在整个链路里的位置 // 1) 一次性:从客户端 dump 摆位(`gym_tools/_r79_dump_si_place.py`)→ // config/scene_item_placements.json(mtime 热重载,改完不用重启服务端); // 2) 每次要让东西出现:POST /api/scene-item/push-explicit 或直接调本类的 // PushExplicitSceneItemsAsync;半径 / 条数 / 标记 / 通道全部是参数, // **不需要 rebuild、不需要重启**,可以现场 A/B。 // ⛑️ 复用 Drmk.Server.RpcTypes.Client4229938 的保龄球 DTO(`BowlingWireContracts.cs`), // 不用 `SceneItemGridMethods4229938.cs` 里那份同名 DTO:两者的元素类型标记不同 // (保龄球版 TypeMark = 1 = 客户端跳转表里的 BASE;SceneItem 版是默认标记), // 而保龄球版是**在线上验证过能建出 hold 的**那一份。标记写错的表现是 // `parameter=DestructibleInfo` 解析失败——donor 第 23B 轮为这个赔过一整个会话。 // // // 找 config/scene_item_placements.json:从运行目录 / 当前目录各向上找 6 层 // (与 BowlingWireNow 同款做法)。找不到返回 null。 // func GameRouter_SceneItemExplicit_FindSceneItemPlacementFile(ctx context.Context, ) any { // Router implementation for GameRouter_SceneItemExplicit_FindSceneItemPlacementFile var zero any; return zero } // // 读摆位表(mtime 缓存)。JSON 键名做了宽松兼容,所以 dump 脚本换写法不用改这边: // // { "items": [ // { "id": 123456789, "cfg": 80001921, "path": 1154760923, // "x": -4713.9, "y": 176.2, "z": -2821.7, "yaw": 91.5, "scale": 100 }, // ... // ] } // // 键名兼容:id|uid|uniqueId|unique_id / cfg|cfgId|configId|config_id / path|pathId|path_id / // x,y,z / yaw|ry|facing|rotY / scale|iScale|i_scale。缺 yaw / scale 时取 0 / 100。 // func GameRouter_SceneItemExplicit_LoadSceneItemPlacements(ctx context.Context, ) []any { // Router implementation for GameRouter_SceneItemExplicit_LoadSceneItemPlacements return nil } func GameRouter_SceneItemExplicit_ParsePlacement(ctx context.Context, it any) any { // Router implementation for GameRouter_SceneItemExplicit_ParsePlacement var zero any; return zero } // --------------------------------------------------------------------------------------- // 下发 // --------------------------------------------------------------------------------------- // // 把玩家周围 内的场景物显式下发,让客户端**真的**把 GameObject 装出来。 // : // aoi = 只走 SyncDestructibleGridAOIDecrease(68969593) 的 aoiInfo.addInfos // (保龄球用的就是这条,hold 建得出来); // addlist = 只走 SyncDestructibleObjectAddList(68963429); // both = 两条都发(保龄球实际是这么干的,默认)。 // 默认 true —— 这是**和健身房那条能用的通道唯一对齐的字段** // (`GameRouter.Gadget.cs:137`),关掉它只剩「显式下发」一半的效果,留作 A/B 对照组。 // = true 时把下发的 uid 塞进 addGridInfo.ExceptIds, // 并按它们所在的格填 StandardIndexList,让客户端**别再**从 blob 铺同一批壳 // (ExceptIds 这个字段就是为这件事存在的)。false = 只加不减,作对照组。 // func GameRouter_SceneItemExplicit_PushExplicitSceneItemsAsync(ctx context.Context, session any, radiusMeters float64, maxItems int32, mode string, forceLod0 bool, exceptIds bool, addListReason byte) any { // Router implementation for GameRouter_SceneItemExplicit_PushExplicitSceneItemsAsync var zero any; return zero } // 诊断:摆位文件是否可用、总条数、解析失败的条数。 func GameRouter_SceneItemExplicit_SceneItemPlacementStatus(ctx context.Context, ) any { // Router implementation for GameRouter_SceneItemExplicit_SceneItemPlacementStatus var zero any; return zero } // 一条场景物摆位。字段名与客户端 hold 上的同名(cfgId / pathId / uniqueId / objPosition)。 type SceneItemPlacement struct { UniqueId uint64 `json:"uniqueId"` CfgId uint32 `json:"cfgId"` PathId int32 `json:"pathId"` X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` Yaw float32 `json:"yaw"` Scale int32 `json:"scale"` }