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_game_ground.go
T

300 lines
19 KiB
Go
Raw Normal View History

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_<uid>/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_<uid>/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 <z>), 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"`
}