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

323 lines
19 KiB
Go
Raw Permalink 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
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 使用)。
// <para>为什么必须有这一步:Protected 是 Dictionary&lt;ulong,…&gt;、**只增不减**,
// 而 5 s 心跳会把表里每个 id 都塞回 AddInfos + ExceptIds
// (SceneItemGrid.cs:344-382)⇒ 上一局没打掉的球会被**永远保活**。</para>
// <para>判定只用高位前缀 BalloonWire.IsBalloonInstanceId(0xBA11),
// 所以不会误伤保龄球(0x9C7B 段)或普通场景物。
// = 本局刚分配的那 16 个(它们此刻还没进 Protected,
// 传进来只是兜底:任何在本局名单里的 id 一律不删)。</para>
//
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 的**(= 本局没打掉的)。
// <para>为什么在局末抓、而不是下一局抓:FinishBalloonRoundAsync 会紧接着调
// UnprotectDestructibles 把本局 id 全摘出 Protected,
// 摘完就再也查不到"哪几颗没打掉"了(R122 的 PurgeStaleBalloonProtection
// 在下一局 spawn 时才查 ⇒ 表已空 ⇒ 日志 purge stale 恒 0、修了个寂寞)。</para>
// <para>结果存进 BalloonSession.RetiredIds,由下一局 spawn 作为
// aoiInfo.RemoveIds 下发。</para>
//
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)—— **一局限时看门狗**。
// <para>用户实测/提问:「打完了不应该是结算游戏吗,现在得自己手动离开」
// 「这种小游戏肯定会限制只能打几枪吧,不然不限制谁都能打完,没有挑战性」。</para>
// <para>官方口径(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)。</para>
// <para>到点后走与「打光最后一颗」完全相同的结束链路
// (FinishBalloonRoundAsync:final score → GameOver → Dispose)。
// 客户端提前手动退出时 session.Active 已为 false,这里直接返回(幂等)。</para>
//
//
// ★★★★★ 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 与「清光自动结算」都走这里)。
// <para>为什么要抽出来:「打完了不结算、得自己手动离开」(R115 用户实测)——
// 旧实现把结束链路绑死在 LeaveBalloon 上,而客户端**不会**在打光最后一颗球时
// 自己发这个包(packets 实测:12/12 打完后 5 秒内 C2S 无任何包,直到玩家点退出)
// ⇒ 面板永远不关。现在 ScoreBalloonBreaksAsync 在 targets.Count == 0
// 时直接调本方法。</para>
// <para>链路(三者缺一不可,见本文件头的 contract):
// ① final SyncBalloonGameScore(客户端按总分排 endType);
// ② SyncGameGroundZoneInfo + ZoneState = GameOver(3)(带**与开局完全相同**的
// balloon id 列表 —— 客户端拿它对账,空列表会被忽略);
// ③ SyncGameGroundZoneState = Dispose(4) —— ★ 唯一能真正撕掉 zone context 的通道;
// 只推 ② 关不掉面板;推 GameOver 会走客户端有 bug 的 OnGameEnd(R93 定案)。</para>
//
//
// ★★★★★ 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 <DestructibleBrokenInfo> 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) &lt;- 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 — 上一局**没打掉**的那几颗(局末、撤保护之前抓下来)。
// <para>为什么必须存它:局末 FinishBalloonRoundAsync 会
// ① UnprotectDestructibles 把本局 16 个 id 全摘出 SceneItemGrid.Protected、
// ② BalloonIds.Clear()。⇒ 下一局 spawn 时**两条线索都没了**,
// 服务端根本不知道上一局剩了哪几颗,而客户端 hold 里那几颗**还活着**
// (整格重置的心跳只停止"保活",不会主动删客户端的 hold)。
// 新球又落在**同一批 balloonWallSpots** 上 ⇒ 子弹先命中残留球的碰撞体、
// 报的是旧 id ⇒ 服务端 ignore break,新球既打不掉也不加分。</para>
// <para>实测铁证(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 的残留之一。</para>
// <para>用法:下一局 spawn 时把这批 id 填进 AOI 报文的
// aoiInfo.RemoveIds(DestructibleGridAOIIncrease.removeIds,
// 与 addUniqueIds 成对;通道说明见 GameRouter.GameGround.cs:655),
// 客户端据此删掉对应 hold;下发成功后清空。</para>
//
RetiredIds []uint64 `json:"retiredIds"`
//
// ★★★★★ R126(2026-09-28)—— **局代**:每开一局 +1。
// <para>为什么必须有它(用户实测:「第二轮结算时间非常快,没法正常测试」):
// 本类**按 session 复用、不每局新建**,而 ArmBalloonRoundTimeout 每局都
// Task.Run 起一个 Task.Delay(40s),醒来后只用 session.Active 当守卫。
// 玩家在 40 s 内手动 LeaveBalloon 再马上重进时:上一局的任务醒来时
// **看到的 Active 已经是下一局的 true** ⇒ 直接把新局结算掉。</para>
// <para>实测铁证(R126,2026-09-28 09:01:29~09:03:15,5 局)——
// 每局的结束时刻**恰好等于上一局开始时刻 + 40 s**:
// <code>
// 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
// </code>
// ⇒ 越往后局越短(玩家 9~25 s 就退再进),完全对得上。</para>
// <para>用法:SpawnBalloonsAsync 开头 ++,把当次的值闭包给定时任务;
// 任务醒来与 FinishBalloonRoundAsync 都校验「发起者是不是当前局」。</para>
//
RoundGeneration int32 `json:"roundGeneration"`
}