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

301 lines
19 KiB
Go
Raw 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
//
// 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"`
}