323 lines
19 KiB
Go
323 lines
19 KiB
Go
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<ulong,…>、**只增不减**,
|
||
// 而 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) <- 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"`
|
||
}
|
||
|