package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // Bowling ("保龄球") + the GameGround zone family. // WHY THIS FILE EXISTS // The client's mini-game code is complete (lua\LX6\MiniGame\BowlingGame\, 26 files) and the server // side is empty. The hard gate is MethodId.SyncGameGroundZoneInfo (68004609): the // client only builds a bowling session when the server pushes a BowlingZoneInfo. Without it // gameInstance.zoneInfo stays nil, no panel appears and nothing else is reachable. // The byte layouts below are taken from two mutually-confirming authoritative sources — the // client's own Lua serializer and the client's IL2CPP metadata — and NOT from // Drmk.Proxy/proxy/rpc_schema.json, whose `schemas` table is a stale older build. See // .workbuddy/bowling_wire_spec.md for the full derivation and the evidence. // CLIENT FLOW (BowlingGameManager.lua): // player interacts with the alley machine (C# BowlingGameBridge -> CreateAndStartGameCs) // -> TriggerClientGameFromGadget_Async(entityInstanceId) // -> EnterGame(gadgetUid, BowlingGameType.Single, 0) // -> ClientToGameSceneDelegate:EnterBowlingZone(gadgetUid, 0, 0) [67832701, Invoke] // server: reply u64, then push SyncGameGroundZoneInfo(tag 5 + BowlingZoneInfo) // -> OnSyncGameGroundZoneInfo -> CreateGameFromServerImpl -> _Async // -> WaitSceneItemAndGadget_Async: // 1) up to 5 s for CheckDisableSceneItems(ballList) && (pinList). // NOTE that this helper calls SceneItemMgr:TagAndLoadGameobject(id) itself, so the // CLIENT loads the objects — the server only has to make the holds exist. // 2) array.concat(self.sceneItemBallIdList, zoneInfo.BowlingBallSceneItemIdList) // -> the two id lists MUST NOT be null (an empty list is fine). // 3) up to 20 s for SpoonGadgetManager:RegisterWaitLoad({GadgetUId}); on timeout the // client aborts the session via LeaveBowling(gadgetUId, true). // -> GameType == Single && !isPlayAgain -> RealStartGame() // The holds in step 1 are produced by pushing the balls/pins as DESTRUCTIBLES through // SyncDestructibleGridAOIDecrease (68969593) — destructibles and scene items share one id space // (DestructibleHUDCtrl.lua resolves a destructible id via SceneItemMgr:GetSceneItemHold), so this // replaces the earlier "client Lua hot-patch creates the balls" plan entirely — no fastpatch is // needed for the objects. // ⚠ Do NOT go back to SyncDestructibleGridAOIIncrease (68934309): that id has no implementation on // this client build and is silently dropped. See the long note further down this file. // func GameRouter_GameGround_GetBowlingSession(ctx context.Context, rpcCtx *RpcContext) any { // Router implementation for GameRouter_GameGround_GetBowlingSession var zero any; return zero } // // The lane the player is standing on: nearest by z. The 8 entries are the alley's two lanes on // each of its four machines; picking by z means the balls and pins land where the player is // instead of on a hard-coded lane 10 m away. // func GameRouter_GameGround_BowlingPickLane(ctx context.Context, playerZ float32) any { // Router implementation for GameRouter_GameGround_BowlingPickLane var zero any; return zero } // 把第 ordinal 个球瓶(0 起,按推送顺序)映射到候选下标。见 _pinPathProbe。 func GameRouter_GameGround_PinPathProbeCandidate(ctx context.Context, ordinal int32) int32 { // Router implementation for GameRouter_GameGround_PinPathProbeCandidate return 0 } // // Hot-reloaded bowling wire overrides. File: config/bowling_wire.json next to the // server (same folder as private-server.json). Shape: // {"ballCfgId":80002967,"ballPathId":1838106492,"pinCfgId":80003145,"pinPathId":1959229003,"destructibleTag":1} // Missing file / missing key / unparsable value = keep the compiled-in default. Re-read // whenever the file's last-write tick changes, so an edit takes effect on the next zone entry. // destructibleTag (default 1, alternate 255) picks the polymorphic marker byte used for // each DestructibleInfo on the wire — see BowlingWire.DestructibleTagBase for why the old // hard-coded 0xFF made the client answer "deserialize failed ... parameter=DestructibleInfo" // and create no balls or pins at all. // pinPathProbe (0 = off) turns on the 10-pin (cfg,path) A/B sweep described above // _pinPathProbe; 0 (or omitting the key) restores the plain pinCfgId/pinPathId pair. // func GameRouter_GameGround_BowlingWireNow(ctx context.Context, ) any { // Router implementation for GameRouter_GameGround_BowlingWireNow var zero any; return zero } // Minimal flat {"k":number|"s"} reader — no JSON dependency in this assembly. func GameRouter_GameGround_ParseFlatJson(ctx context.Context, txt string) []any { // Router implementation for GameRouter_GameGround_ParseFlatJson return nil } // --------------------------------------------------------------------------------------- // EnterBowlingZone (67832701) — args: u64 gadgetuid, byte gametype, u32 agentid // --------------------------------------------------------------------------------------- func GameRouter_GameGround_EnterBowlingZone4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_GameGround_EnterBowlingZone4229938 var zero any; return zero } // --------------------------------------------------------------------------------------- // GmEnterBowlingZone (69135013) — the GM-console door into the very same zone entry. // // Measured against this build, not guessed: // * ClientToGameSceneGMDelegate.lua:417-425 serialises // (gadgetuid : u64, gametype : u32, agentid : u32) -> 16 bytes. The live log confirmed // "-- rpc GmEnterBowlingZone [0x041EEAA5] 16b". // * RPCDeserializeAuto.lua:16312 gives 69135013 an EMPTY return reader, i.e. this one is void // (unlike EnterBowlingZone at :15955, which reads back a u64 agent id). // // These are two DIFFERENT RPCs and neither implies the other. The in-world GM console — which is // exactly what the debug API's /api/gm/send drives via SyncDoGmCommand — only ever sends // 69135013. So with EnterBowlingZone whitelisted and this one missing, the command fell through // to DefaultUnknownInvoke4229938's typed-default body: the client got a structurally valid empty // reply and simply did nothing, /api/gm/send reported "尚未回显", and the server log showed // "! [RPC4229938] unimplemented GmEnterBowlingZone [0x041EEAA5] -> typed-default void 0b". // The gameplay path (walking up to the alley machine) uses 67832701 and was never affected. // --------------------------------------------------------------------------------------- func GameRouter_GameGround_GmEnterBowlingZone4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_GameGround_GmEnterBowlingZone4229938 var zero any; return zero } // // Shared body of both zone-entry doors (EnterBowlingZone / GmEnterBowlingZone): mint the // per-connection zone session, push the balls + pins as destructibles, then push the // SyncGameGroundZoneInfo(tag 5) payload the client's manager gates on. The caller owns the // RPC reply, because the two doors have different return shapes. // func GameRouter_GameGround_StartBowlingZoneAsync(ctx context.Context, conn any, rpcCtx *RpcContext, gadgetUId uint64, gameType byte, agentTemplateId uint32, source string) any { // Router implementation for GameRouter_GameGround_StartBowlingZoneAsync var zero any; return zero } // --------------------------------------------------------------------------------------- // LeaveBowling (67245027) — args: u64 gadgetuid, bool abort // --------------------------------------------------------------------------------------- func GameRouter_GameGround_LeaveBowling4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_GameGround_LeaveBowling4229938 var zero any; return zero } // --------------------------------------------------------------------------------------- // In-session RPCs. These are all Invokes from ClientToGameSceneDelegate and each caller does // .Callback = function(err) try_handle_error(err, name) end, so a missing reply stalls the loop. // The server keeps no score book: it answers Ok and lets the client's own mini-game drive. // (Scoring/modes belong to P2; online/agent modes belong to P3.) // --------------------------------------------------------------------------------------- func GameRouter_GameGround_BowlingGenericAccept4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_GameGround_BowlingGenericAccept4229938 var zero any; return zero } // --------------------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------------------- // // Push the bowling balls (5) and pins (10) as destructibles so the client creates + loads the // matching SceneItem holds. reason = AoiAddAndRemoveReason.Init (0) — the field is NOT a // DestructibleCreateReason; see BowlingWire.AoiReasonInit. // Why this is the right channel: mid 68969593 lands on // LX6.Service.GameSceneToClientImpl.SyncDestructibleGridAOIDecrease(aoiInfo, addGridInfo, // removeGridInfo), whose aoiInfo is the UX.Game.DestructibleGridAOIIncrease this // file already builds — addInfos / indexList / addUniqueIds / removeIds all ride inside it. The // client then materialises the holds, and SceneItemMgr is what serves the // GetSceneItemHold lookup that BowlingGameManager.CheckDisableSceneItems gates on. // (The previous version of this comment named mid 68934309 / // SceneItemMgr.SyncSceneItemGridAOIIncrease. That method does exist on SceneItemMgr, but // no S2C handler on the client ever reaches it: GameSceneToClientImpl has no // SyncDestructibleGridAOIIncrease at all, so 68934309 is dropped on arrival. Existence of a // client method is NOT evidence that a method id reaches it.) // The game repositions/rents them itself afterwards (BowlingPinSetter.CreatePin -> // BowlingGameManager:Rent), so the spawn position only has to be close enough for the client's // 5 s CheckDisableSceneItems window — it is not the final lane layout. It does still have to be // ON THE LANE, though: the first cut spawned them relative to the player and the whole mini-game // played out in the alley doorway. Positions below come from the lane table measured off the // alley's own GameObjects (see the BowlingLanes block at the top of this file); each lane runs // along +x and is 2 m from its neighbour in z. // func GameRouter_GameGround_PushBowlingSceneItemsAsync(ctx context.Context, conn any, session any, playerPos Vec3) any { // Router implementation for GameRouter_GameGround_PushBowlingSceneItemsAsync var zero any; return zero } type BowlingSession struct { GadgetUId uint64 `json:"gadgetUId"` ZoneSessionId string `json:"zoneSessionId"` StartedUtcTicks int64 `json:"startedUtcTicks"` BallIds []uint64 `json:"ballIds"` PinIds []uint64 `json:"pinIds"` } // --------------------------------------------------------------------------------------- // ★ Official alley frame — measured live off WorldMap_Release::BowlingSceneNode (2026-09-23). // // Why this exists: the first cut of this router laid the lane out RELATIVE TO WHEREVER THE // PLAYER STOOD (state.LastReportedPlayerPosition). The assumption behind it — "the private // server keeps no alley geometry of its own, the alley holds only furniture" — is wrong. // The alley ships a complete gameplay rig, and it is all in the baked scene: // // MiniGameMachine_Bowling_/ItemViewNode/.../BowlingSceneNode ← ⚠ gadget CHILD, not scene-baked // (old numbers below assumed the // gadget root of the day; see the // correction block further down) // ├─ Bowling_lane_Collider plane 19.02 x 0.2 x 1.49 + L/R side cubes + BlockL/BlockR // ├─ PivotNode/BallSpawnPoint (2693.29, 0.12, 1590.44) // ├─ PivotNode/PlayerPoint (2689.30, 0.02, 1590.83) // ├─ PivotNode/PinPointN <- head pin (2711.42, 0.02, 1590.03) // ├─ ArrowTips(Clone) 21 lane arrows, z = 1590.55, x 2693.34 … 2699.64 // ├─ VirtualCamera / EndVirtualCamera / CinemachineConfiner // └─ a node literally named // "碰撞体必须要是 Destructible 层的,不然不会和保龄球产生碰撞" // WorldMap_Release::Gameplay_Bowling02 the throw timeline (actors + camera, ~2693.29,1590.44) // WorldMap_Release::GadgetInstanceRoot/MiniGameMachine_Bowling_/ItemViewNode/BowlingGameNode(Right) // the two lane rigs (pin racks land at x≈2711.3) // // Lane axis is +x (yaw 90°). Two lanes exist side by side 2 m apart; only the z = 1590.44 one // (the "Right" lane) has a lane collider in BowlingSceneNode, and the dev note above says the // ball only collides with a Destructible-layer collider — so lane R is the one to use. // // Symptom this fixes: with the old player-relative layout the player stands at the machine in // the alley doorway, so balls/pins spawned at +2 m / +8 m FORWARD FROM THE DOOR and the whole // mini-game played out in the doorway instead of on the lane. // // These are absolute world coordinates from the official rig; the lane is axis-aligned, so no // yaw math is applied to the pin/ball layout. To move the game to the LEFT lane, add +2.0 to // every Z below (and re-check that a collider exists there). // --------------------------------------------------------------------------------------- // ---- 2026-09-23 11:40 二次修正:机器本身原先就摆错了 15 m ------------------------------- // The block above still had ONE wrong premise: the (2693.29 / 1590.44) rig is a child of the // MiniGameMachine_Bowling gadget, so it is only there if the GADGET is there. The gadget was // being spawned at (2689.57, 1591.53) — which is the alley DOORWAY (it even sits on top of // sector_high_87_54_sound/SoundGate_1 @ (2689.57, 1.22, 1591.48)). The alley's own 11 painted // lanes are at z = 1563.99 … 1582.66 (2 m pitch). Hence: proper interaction point showed up at // the door while the lanes were 15 m away. // // Correct gadget root (two independent derivations agree to 5 mm): // root = (2688.73, 0.02, 1576.845) yaw 271.30 [gym_tools\bowl_machine_spawn.py] // (a) foul line flush with the painted lane head: painted head x 2692.445 - prefab 3.72 // (b) player's own screenshot stance 2686.92 + prefab StandPosRight offset 1.81 // prefab-local offsets (measured): // lane centre lines z = root - 1.515 / root + 0.485 (2 m apart, NOT the node z) // lane surface x = root + 3.72 … root + 22.74 (19.02 long, foul line at +3.72) // head pin x = root + 21.72, z = root - 1.515 // => the constants below are root + those offsets, i.e. the absolute world coords of the // gadget that is currently spawned at (2688.73, 0.02, 1576.845). // If the gadget is ever moved (bowl_machine_spawn.py --lane ), add the same delta here: // Δ = (newRootX - 2688.73, newRootZ - 1576.845) // 2026-09-23 修正:玩家实际站在 L 道(z≈1577.3, 官方锚点 BowlingGameStart=2689.35/1577.291)。 // 之前用的是 R 道(z=1575.3),导致"球瓶在相邻一条道"——差整整一条道(2m)。 // 现直接对齐官方 L 道锚点:BowlingPinPlacementL=2710.49/1577.346(= 道中心线)。 // // ★★ 2026-09-23 三次修正(bowl_audit_before.txt 实测,铁证): // 上面原本写的 "BowlingGameNode(L)=2692.50/1577.755" 是**机器轴心**节点,它横向偏了 // 道中心线 0.41~0.43 m,不能拿来当球的 z 基准。真正的道中心线是 // BowlingPinPlacementL.z = 1577.35 (官方烘焙细板实测 1577.33,差 2 cm) // 本文件上方第 125 行的配方其实早就写明答案:"lane centre lines = root + 0.485 // (NOT the node z)"。旧值 1577.75 = root + 0.905 恰好就是 node z,代码违反了自己的注释。 // 后果:整排球横移 0.42 m,最外球压到道边条;而瓶一直用 1577.35,所以看起来只有球歪。 // --------------------------------------------------------------------------------------- // ★★ 2026-09-23 第四次修正 —— 改用客户端 GameObject 的实测真值(frida gobjs,权威) // // 旧常量把球瓶放在 z = 1577.35。那**不是任何一条道**: // 球馆里只有 4 台机器(MiniGameMachine_Bowling_*),每台两条道(BowlingPinPlacementL / R)。 // 4 台机器的 L 道 z = 1566.773 / 1572.119 / 1576.114 / 1581.446。 // 1577.35 落在机器 3 与机器 4 之间的空档里,而玩家实际站在机器 1(z ≈ 1566.6) // ⇒ 球瓶被推到 10.8 m 之外,什么都看不到。 // // 实测数据(gobjs "BowlingPinPlacement" / "BowlingGameNode" / "BowlingGameStart" / // "MiniGameMachine_Bowling",每条 4 个实例 = 4 台机器): // · machineRoot.z + 0.485 = 该机器 L 道的中心线 z ← 本文件第 125 行的配方本来就是对的 // · 同一台机器 L / R 两个瓶位相差正好 2.00 m // · x 方向:发球线 BowlingGameStart 2689.11 → 机器轴心 BowlingGameNode 2692.26 → // 瓶阵 BowlingPinPlacement 2710.25,约 18 m,与标准保龄球道长度吻合 // · 所有锚点 y = 0.02(场地地面) // --------------------------------------------------------------------------------------- // ★ Lane / machine numbering — MEASURED, not invented (2026-09-23 late, confirmed by the player). // // The alley numbers its eight lanes 1..8 with lane 1 at the LARGEST z and lane 8 at the // SMALLEST z, i.e. the numbering runs OPPOSITE to ascending z. The player stood at z=1566.77 // and read the painted number "7" on that lane — which is this array's index 0. So: // // asc-z order (small→large): 1564.77 1566.77 1570.12 1572.12 1574.11 1576.11 1579.45 1581.45 // game lane number : 8 7 6 5 4 3 2 1 // // MachineNo/LaneNo below are the IN-GAME labels. Do not "fix" them back to ascending order — // the geometry is right, only the naming was reversed before, which made every log line read // one machine off (what the player pointed out as "why does it call this machine 1 when the // number on the lane is 7, shouldn't it be machine 4 L?" — it should, and now it does). type BowlingLane struct { MachineNo int32 `json:"machineNo"` LaneNo int32 `json:"laneNo"` MachineX float32 `json:"machineX"` MachineZ float32 `json:"machineZ"` LaneZ float32 `json:"laneZ"` PinApexX float32 `json:"pinApexX"` StartX float32 `json:"startX"` StartZ float32 `json:"startZ"` GameNodeX float32 `json:"gameNodeX"` GameNodeZ float32 `json:"gameNodeZ"` }