package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background func GameRouter_Balloon_GetBalloonSession(ctx context.Context, rpcCtx *RpcContext) any { // Router implementation for GameRouter_Balloon_GetBalloonSession var zero any; return zero } // --------------------------------------------------------------------------------------- // EnterBalloonZone (67417935) — the gameplay door. // // Arg layout is ClientToGameSceneDelegate.lua (SerializerHelper.EnterBalloonZone_Serializer): // gadgetuid : u64 (WriteUInt64) // gametype : byte (CheckEnum(gametype, 66, 0), i.e. an enum with 66 members) // -> 9 bytes. Confirmed live: "C2S INVOKE EnterBalloonZone [0x0404B74F] body=9b" // with payload 01 18 20 E4 A3 08 00 00 00 : // u64 LE = 0x08A3E4201801 (the stall gadget's instanceId) // byte = 0x00 (gametype 0) // // The return is a COMPLEX BalloonZoneInfo (see the file header) — this is deliberately // different from EnterBowlingZone's bare u64. // --------------------------------------------------------------------------------------- func GameRouter_Balloon_EnterBalloonZone4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Balloon_EnterBalloonZone4229938 var zero any; return zero } // // ★★★ R89 — 按官方武器发布管线的形状,把气枪(80300134)发布为当前手持武器。 // 与 GameRouter.Profiles.PublishWeaponSnapshot 同一条链路: // SyncSpiritWeaponDetail(槽位视图)→ SyncSpiritSwitchWeaponAction(选中实例)。 // ⛔ 不能用 CombatCodec.SpiritWeaponSnapshot —— 它会把 currentWeaponInstanceId // **强行塞进槽位列表**,而玩具枪不在玩家的常规配装上,必须手搓 SpiritWeaponDetail。 // ⛔ 也**不要**补 SyncAgentWeaponWheel(68121167):对照包 2026-09-26 实测客户端解不开 // (Player.log: deserialize failed mode=3 mid=68121167 len=24),且它只写 // agentCurrentWeaponDict,仅 RobotDog/RobotFlyer/RobotSpider 三个机器人控件读它 —— // 气球面板要的 currentWeapon 来自 SyncSpiritWeaponDetail。 // func GameRouter_Balloon_GrantBalloonWeaponAsync(ctx context.Context, conn any, rpcCtx *RpcContext) any { // Router implementation for GameRouter_Balloon_GrantBalloonWeaponAsync var zero any; return zero } // // ★★★ R89 — 退出气球区时还原进气前的手持武器。 // 这里**可以**用官方快照(原武器本来就在配装里),避免被玩具枪顶掉。 // func GameRouter_Balloon_RestoreBalloonWeaponAsync(ctx context.Context, conn any, rpcCtx *RpcContext) any { // Router implementation for GameRouter_Balloon_RestoreBalloonWeaponAsync var zero any; return zero } // // Shared body: mint the per-connection zone session, build the BalloonZoneInfo, push the // SyncGameGroundZoneInfo the client's manager gates on, and hand a copy back for the Invoke // reply of EnterBalloonZone. Returns the zone info so both doors (EnterBalloonZone, and a // future GmEnterBalloonZone) can shape their own reply. // func GameRouter_Balloon_StartBalloonZoneAsync(ctx context.Context, conn any, rpcCtx *RpcContext, gadgetUId uint64, gameType byte, source string) any { // Router implementation for GameRouter_Balloon_StartBalloonZoneAsync var zero any; return zero } // // Spawn the breakable balloons as real scene items and return their instance ids. // ★★★ R88o — THIS is the piece that makes "墙上有气球" actually true, and it is the exact // same three-step dance bowling already does (GameRouter.GameGround.cs:683-930): // 1) mint real instance ids BalloonInstanceIdBase + ordinal(1..count) // 2) push them as DestructibleInfo // SyncDestructibleGridAOIDecrease (68969593) .aoiInfo.addInfos ← creates the hold // SyncDestructibleObjectAddList (68963429) ← the object add // with CfgId = the balloon cfg id, PathId = 五色各一 (R89/R100), // ForceLod0 = false, IScale = 100 (R109, 原 1000 作废), State = Active // ★★★ R100:CfgId 与 PathId 已**成对**换成资源确实随包的「派对气球」 // (旧 80005796..80005800 的美术资源不存在 —— 四条证据链见 BalloonWireContracts.cs)。 // 3) hand those SAME ids back to the caller for BalloonZoneInfo.BalloonInstanceIdList // WHY THIS IS NOT OPTIONAL — the C# consumer keys on INSTANCE ids, not cfg ids: // L50.Gameplay.ZoneGamePlay.BalloonGamePlayContext holds // TargetModelId | TargetSceneItemId | balloonInstanceIdSet | playerScoreDic // and implements OnSceneItemBreak. A break only scores if the broken item's instance id is // in balloonInstanceIdSet, and nothing can be broken at all unless the item was spawned. // ★★★ R89 LAYOUT — 已改为对照包(Lyco-v0.2.1,真机跑通)的摆法:玩家**前方 6m** 的 4×3 墙。 // ⛔ 旧的「摊位固定坐标 10×2」不再用。ClientConfig 的 Balloon_RightNum = 10 是**棋盘上限**, // 不是本局数量;对照包实测本局 = 12(4 列 × 3 行)。 // // // ★★★★★ R122(2026-09-28)—— 把 SceneItemGrid.Protected 里**不属于本局**的气球 id // 全部摘掉并返回(供 AOI 报文的 aoiInfo.RemoveIds 使用)。 // 为什么必须有这一步:Protected 是 Dictionary<ulong,…>、**只增不减**, // 而 5 s 心跳会把表里每个 id 都塞回 AddInfos + ExceptIds // (SceneItemGrid.cs:344-382)⇒ 上一局没打掉的球会被**永远保活**。 // 判定只用高位前缀 BalloonWire.IsBalloonInstanceId(0xBA11), // 所以不会误伤保龄球(0x9C7B 段)或普通场景物。 // = 本局刚分配的那 16 个(它们此刻还没进 Protected, // 传进来只是兜底:任何在本局名单里的 id 一律不删)。 // func GameRouter_Balloon_PurgeStaleBalloonProtection(ctx context.Context, session *GameSession, currentRoundIds []any) []any { // Router implementation for GameRouter_Balloon_PurgeStaleBalloonProtection return nil } // // ★★★ R123(2026-09-28)—— 本局 16 个 id 里,**此刻仍在 Protected 的**(= 本局没打掉的)。 // 为什么在局末抓、而不是下一局抓:FinishBalloonRoundAsync 会紧接着调 // UnprotectDestructibles 把本局 id 全摘出 Protected, // 摘完就再也查不到"哪几颗没打掉"了(R122 的 PurgeStaleBalloonProtection // 在下一局 spawn 时才查 ⇒ 表已空 ⇒ 日志 purge stale 恒 0、修了个寂寞)。 // 结果存进 BalloonSession.RetiredIds,由下一局 spawn 作为 // aoiInfo.RemoveIds 下发。 // func GameRouter_Balloon_SurvivingBalloonIds(ctx context.Context, session *GameSession, roundIds []any) []any { // Router implementation for GameRouter_Balloon_SurvivingBalloonIds return nil } func GameRouter_Balloon_SpawnBalloonsAsync(ctx context.Context, conn any, rpcCtx *RpcContext, session any, source string) []any { // Router implementation for GameRouter_Balloon_SpawnBalloonsAsync return nil } // // ★★★★★ R116(2026-09-27)—— **一局限时看门狗**。 // 用户实测/提问:「打完了不应该是结算游戏吗,现在得自己手动离开」 // 「这种小游戏肯定会限制只能打几枪吧,不然不限制谁都能打完,没有挑战性」。 // 官方口径(ConfigDump_v3/Static/PoiGameConfig.static.json): // Balloon_CountDown = 40(一局 40 秒)+ 分值 绿1/蓝3/紫5/金10 // + 结算线 Balloon_GoodEndCredit = 50 / Balloon_NormalEndCredit = 25。 // 客户端 BalloonGameStore.OnGameEnd 就是按这两条线排 endType,并在 1.5 s 后 // 自己 gPanelManager:Close(BALLOON_PANEL)。 // 到点后走与「打光最后一颗」完全相同的结束链路 // (FinishBalloonRoundAsync:final score → GameOver → Dispose)。 // 客户端提前手动退出时 session.Active 已为 false,这里直接返回(幂等)。 // // // ★★★★★ R126 — 布防那一刻的 BalloonSession.RoundGeneration。 // 醒来时若当前局代已变,说明**本局早被玩家手动退出、这是上一局留下的残骸** // ⇒ 直接丢弃(改前它会把下一局结算掉,用户症状:「第二轮结算时间非常快」)。 // func GameRouter_Balloon_ArmBalloonRoundTimeout(ctx context.Context, conn any, rpcCtx *RpcContext, session any, seconds int32, roundGen int32) any { // Router implementation for GameRouter_Balloon_ArmBalloonRoundTimeout var zero any; return zero } // // Push SyncGameGroundZoneState (68383444). Wire = 30 bytes, no markers: // GameGroundZoneIdentifier (inline struct) GadgetUId u64 | TaskId u32 | ZoneType u8 // state u8 // GameGroundZoneCountDownInfo (inline) 4× u32 // See BalloonWireContracts.cs for the derivation and for why this — not // SyncGameGroundZoneInfo — is the signal that opens/closes the balloon zone context. // func GameRouter_Balloon_PushZoneStateAsync(ctx context.Context, conn any, gadgetUId uint64, state byte) any { // Router implementation for GameRouter_Balloon_PushZoneStateAsync var zero any; return zero } // --------------------------------------------------------------------------------------- // LeaveBalloon (67075062) — the gameplay exit door. // // ClientToGameSceneDelegate.LeaveBalloon(gadgetuid : u64) -> 8 bytes, void return // ("C2S INVOKE LeaveBalloon [0x03FF7BF6] body=8b"). // // ★ Replying Ok is NOT enough to close the panel (see the file header). The close is driven by // MiniGameDataManager.StopBalloon -> store.OnGameEnd -> Timer(1.5s) -> Close(BALLOON_PANEL). // So before we reply, we push the END signal: a final SyncBalloonGameScore plus a // SyncGameGroundZoneInfo carrying ZoneState = GameOver(3). Either of those is what the // client's manager watches for, and pushing both makes the intent unambiguous. // --------------------------------------------------------------------------------------- func GameRouter_Balloon_LeaveBalloon4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Balloon_LeaveBalloon4229938 var zero any; return zero } // // ★★★★★ R115 —— **结束一局**(唯一实现;LeaveBalloon 与「清光自动结算」都走这里)。 // 为什么要抽出来:「打完了不结算、得自己手动离开」(R115 用户实测)—— // 旧实现把结束链路绑死在 LeaveBalloon 上,而客户端**不会**在打光最后一颗球时 // 自己发这个包(packets 实测:12/12 打完后 5 秒内 C2S 无任何包,直到玩家点退出) // ⇒ 面板永远不关。现在 ScoreBalloonBreaksAsync 在 targets.Count == 0 // 时直接调本方法。 // 链路(三者缺一不可,见本文件头的 contract): // ① final SyncBalloonGameScore(客户端按总分排 endType); // ② SyncGameGroundZoneInfo + ZoneState = GameOver(3)(带**与开局完全相同**的 // balloon id 列表 —— 客户端拿它对账,空列表会被忽略); // ③ SyncGameGroundZoneState = Dispose(4) —— ★ 唯一能真正撕掉 zone context 的通道; // 只推 ② 关不掉面板;推 GameOver 会走客户端有 bug 的 OnGameEnd(R93 定案)。 // // // ★★★★★ R126 — 发起者的「局代」(BalloonSession.RoundGeneration); // -1 = 不校验(当前局自身的流程:手动 LeaveBalloon、清光自动结算)。 // 只有 40 s 看门狗会传具体值 —— 它是唯一的**异步延迟**发起者,也是唯一可能 // 「跨局醒来」的调用点。 // func GameRouter_Balloon_FinishBalloonRoundAsync(ctx context.Context, conn any, rpcCtx any, session any, reason string, expectedGen int32) any { // Router implementation for GameRouter_Balloon_FinishBalloonRoundAsync var zero any; return zero } // --------------------------------------------------------------------------------------- // AskBreakDestructibleObject (67463592) — ★★★ R89 计分闭环,R88o 完全缺失的那一环。 // // 不打这个 handler 的后果:客户端打碎气球后上报 → 走到 DefaultUnknownInvoke ⇒ // 一条 Warn + 空应答。客户端 err=0 以为受理,**永远等不到分数推送** ⇒ // 不打结分、面板分数恒 0、也没有爆破碎屑的分数气泡。 // // 线格式(客户端 AskBreakDestructibleObject_Serializer,**权威** = AutoGen): // u64 breaker // Complex brokeninfos: // ★ 先写 1 字节标记 0xFF // u64 InstanceId | u32 Stage | u8 BrokenType | UXVector3 Position | UXVector3 Facing // // ★★★ 2026-09-27 修正(R93,原判定有误,此处为更正后结论): // 旧代码把第 8 字节起直接当 u64 读(BitConverter.ToUInt64(body, 8)),**是错的**。 // 真实第 8 字节是 SerializeBase.WriteComplex 写的**对象标记 0xFF** // (RPCSerializeBase.lua:77-104:WriteComplex 只写 1 字节 mark 再写内联字段, // **不带长度前缀**),真正的 InstanceId 在**偏移 9**。 // 读偏 1 字节 ⇒ 取到 0xFF + InstanceId 前 7 字节 拼成的垃圾值 ⇒ // targets.Remove 永不命中 ⇒ 每次都走 else「ignore break」⇒ // **分数永远 0、气球打了没反应**(真机日志 31 处 score=0 + 零处 broken 即此)。 // // 非气球破坏(普通箱子/罐子)⇒ 与本 handler 注册前行为一致:回空 ACK,不改任何状态。 // --------------------------------------------------------------------------------------- func GameRouter_Balloon_AskBreakDestructibleObject4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Balloon_AskBreakDestructibleObject4229938 var zero any; return zero } // // ★★★★★ R112(2026-09-27)—— **复数版破坏计分:客户端真正发的是这一条。** // 实机取证(logs/packets-latest.log 22:18:29): // C2S NOTIFY AskBreakDestructibleObjects [0x040BF2FC] body=48b // DECODED {"breaker":0,"brokenInfos":[{"InstanceId":204582177210376(=0xBA1100000008), // "Stage":0,"BrokenType":1,…}]} // ⇒ InstanceId 正是服务端登记、注入伪造的那颗气球,解码正确。 // ⛔ 病因:单数版 `AskBreakDestructibleObject`(67463592) 挂着一整套计分逻辑,但 // 客户端**从不发那一条**;而复数版过去只打一行日志 ⇒ **打爆气球永远不加分**。 // (GameRouter.Balloon.cs:721 的老注释说「客户端从不发 AskBreakDestructibleObjects」是**反的**。) // 本方法与单数版共用同一套账本(BalloonTargets)与同一套「按颜色算分」(CreditForCfg)。 // func GameRouter_Balloon_ScoreBalloonBreaksAsync(ctx context.Context, conn any, rpcCtx *RpcContext, breaker uint64, brokenIds []any) int32 { // Router implementation for GameRouter_Balloon_ScoreBalloonBreaksAsync return 0 } // // GmEnterBalloonZone (69363970) — the GM-console door into the same zone entry. // Like its bowling twin (GmEnterBowlingZone 69135013) this is a DIFFERENT RPC from // EnterBalloonZone, and neither implies the other. Arg layout is // ClientToGameSceneGMDelegate.lua (SerializerHelper.GmEnterBalloonZone_Serializer): // gadgetuid : u64 (WriteUInt64) // gametype : u32 (WriteUInt32) <- u32 here, u8 in the gameplay door // -> 12 bytes. This one is void-returning, unlike EnterBalloonZone which returns the object. // The in-world GM console (what /api/gm/send drives via SyncDoGmCommand) only ever sends this // id, so with EnterBalloonZone whitelisted and this one missing, the console command would // fall through to the typed-default body and the client would do nothing. // func GameRouter_Balloon_GmEnterBalloonZone4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Balloon_GmEnterBalloonZone4229938 var zero any; return zero } type BalloonSession struct { GadgetUId uint64 `json:"gadgetUId"` ZoneSessionId string `json:"zoneSessionId"` StartedUtcTicks int64 `json:"startedUtcTicks"` Score int32 `json:"score"` Active bool `json:"active"` // The REAL scene-item instance ids pushed at zone entry (R88o). BalloonIds []uint64 `json:"balloonIds"` // // ★★★ R112 — 本轮每颗气球**实际下发**的 cfg(id → cfgId)。 // 用于「按颜色算分」(BalloonWire.CreditForCfg)。旧代码的账本用的是 // BalloonWire.BalloonSceneItemCfgIds 这个**固定 5 元组**,cfgSweep 生效时与实际不符。 // BalloonCfgById map[uint64]uint32 `json:"balloonCfgById"` // // ★★★ R123 — 上一局**没打掉**的那几颗(局末、撤保护之前抓下来)。 // 为什么必须存它:局末 FinishBalloonRoundAsync 会 // ① UnprotectDestructibles 把本局 16 个 id 全摘出 SceneItemGrid.Protected、 // ② BalloonIds.Clear()。⇒ 下一局 spawn 时**两条线索都没了**, // 服务端根本不知道上一局剩了哪几颗,而客户端 hold 里那几颗**还活着** // (整格重置的心跳只停止"保活",不会主动删客户端的 hold)。 // 新球又落在**同一批 balloonWallSpots** 上 ⇒ 子弹先命中残留球的碰撞体、 // 报的是旧 id ⇒ 服务端 ignore break,新球既打不掉也不加分。 // 实测铁证(R122,2026-09-28 08:19~08:20): // R1 spawn id 0x11..0x20 只打爆 12 颗,未打掉 = {0x15,0x17,0x1C,0x20}; // R2(0x21..0x30)在 08:20:19 收到 // [BALLOON] ignore break n=1 first=BA1100000017 —— 0x17 **正是** R1 的残留之一。 // 用法:下一局 spawn 时把这批 id 填进 AOI 报文的 // aoiInfo.RemoveIds(DestructibleGridAOIIncrease.removeIds, // 与 addUniqueIds 成对;通道说明见 GameRouter.GameGround.cs:655), // 客户端据此删掉对应 hold;下发成功后清空。 // RetiredIds []uint64 `json:"retiredIds"` // // ★★★★★ R126(2026-09-28)—— **局代**:每开一局 +1。 // 为什么必须有它(用户实测:「第二轮结算时间非常快,没法正常测试」): // 本类**按 session 复用、不每局新建**,而 ArmBalloonRoundTimeout 每局都 // Task.Run 起一个 Task.Delay(40s),醒来后只用 session.Active 当守卫。 // 玩家在 40 s 内手动 LeaveBalloon 再马上重进时:上一局的任务醒来时 // **看到的 Active 已经是下一局的 true** ⇒ 直接把新局结算掉。 // 实测铁证(R126,2026-09-28 09:01:29~09:03:15,5 局)—— // 每局的结束时刻**恰好等于上一局开始时刻 + 40 s**: // // R2 结束 09:02:09 = R1 开始 09:01:29 + 40 (R2 只跑了 22 s) // R3 结束 09:02:27 = R2 开始 09:01:47 + 40 (R3 只跑了 9 s) // R4 结束 09:02:58 = R3 开始 09:02:18 + 40 // R5 结束 09:03:15 = R4 开始 09:02:34 + 41 // // ⇒ 越往后局越短(玩家 9~25 s 就退再进),完全对得上。 // 用法:SpawnBalloonsAsync 开头 ++,把当次的值闭包给定时任务; // 任务醒来与 FinishBalloonRoundAsync 都校验「发起者是不是当前局」。 // RoundGeneration int32 `json:"roundGeneration"` }