121 lines
6.9 KiB
Go
121 lines
6.9 KiB
Go
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 脚本换写法不用改这边:
|
|||
|
|
// <code>
|
|||
|
|
// { "items": [
|
|||
|
|
// { "id": 123456789, "cfg": 80001921, "path": 1154760923,
|
|||
|
|
// "x": -4713.9, "y": 176.2, "z": -2821.7, "yaw": 91.5, "scale": 100 },
|
|||
|
|
// ...
|
|||
|
|
// ] }
|
|||
|
|
// </code>
|
|||
|
|
// 键名兼容: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"`
|
|||
|
|
}
|
|||
|
|
|