300 lines
19 KiB
Go
300 lines
19 KiB
Go
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"`
|
|||
|
|
}
|
|||
|
|
|