213 lines
13 KiB
Go
213 lines
13 KiB
Go
package routers
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
var _ = sync.Mutex{}
|
||
var _ = context.Background
|
||
|
||
//
|
||
// NPC 邀请(HUD / 机器 / 玩法面板上的「邀请」按钮)—— 全部「邀 NPC 一起玩」玩法的唯一入口。
|
||
// 2026-09-24 第一批:原先只认 91056000(Bowling),现场实测按飞镖机的「邀请」键发的是
|
||
// **84002017(Dart)**、跳舞机是 **84002014(Dance808)**,两者都被"只回 Ok 不推送"挡掉 ⇒
|
||
// 客户端 NpcChatManager.OnSyncNpcChatLeaveGameplay 收不到推送 ⇒ 机器(C# Spoon 图)卡在等待态
|
||
// ⇒ ★ **交互按钮从此不再出现**(用户实测)。
|
||
// 2026-09-24 第二批(本文件当次改动):把已验证过交集的 9 个玩法一并接通 ——
|
||
// 麻将 84002011 · 摩天轮(市区) 84002012 · 露天SPA 84002018 · 派对 91055000 ·
|
||
// 派对引导 91056001(5人) · 蹦迪派对 91056002 · KTV 91056003(3人) · 影院 84002002 · LiveHouse 84002010。
|
||
// 与第一批合起来共 12 个 gameplay 走同一份 handler(含保龄球)。
|
||
// 全表与交集依据见 gym_tools\多玩法邀请_接通说明.md。
|
||
// WHY THIS FILE EXISTS
|
||
// 客户端 BowlingGameManager.CreateAndStartGameCs 在 mode == GameMode.NPC_BATTLE 时
|
||
// (lua\LX6\MiniGame\BowlingGame\BowlingGameManager.lua:314)
|
||
// * 发 AskSimulationInviteNpc(91056000);
|
||
// * 把 self.gameInstance.pendingInviteTask 挂起来(同文件 :318-325),
|
||
// 等着 OnNpcInvited(npcCultivationId)(:355-361)来执行。
|
||
// 而 OnNpcInvited 的唯一来源是 **SyncNpcChatLeaveGameplay** 这条推送
|
||
// (NpcChatManager.lua:180-223)。旧的私服只做了前者、完全没做后者:
|
||
// * AskSimulationInviteNpc 落在 DefaultReturnCatalog4229938.cs:414 的兜底
|
||
// case 63301526u: body = Array.Empty<byte>(); shape = "void";;
|
||
// * 服务端**从不主动下发** SyncNpcChatLeaveGameplay(在全部报文日志里 0 命中)。
|
||
// 结果:invoke 回包 error=0 body=0b(成功但空)→ 客户端回调只拿到 err=Ok →
|
||
// OnNpcInvited 永不触发 → 按下按钮后**完全没反应**(不报错、不弹提示、不生成 NPC)。
|
||
// 证据(现场实测,非推断)
|
||
// * C2S:logs\packets-20260924-001712.log:236503
|
||
// C2S INVOKE AskSimulationInviteNpc [0x03C5E796] invoke=165283230 body=4b
|
||
// 载荷 80 67 6D 05 = 小端 u32 = 91056000(= 上述 gameplay)
|
||
// * S2C:logs\packets-20260924-001712.log:236505
|
||
// S2C RETURN AskSimulationInviteNpc [0x03C5E796] error=0 body=0b,之后无任何推送
|
||
// * 回包形状:RPCDeserializeAuto.lua:9694 midToReturnMessageReader[63301526] = function (reader) end
|
||
// ⇒ **回 void 是对的**,缺的只有那条推送(别去改回包)
|
||
// * 推送形状:同文件 :2268(见 NpcInviteWireContracts.cs 顶部)
|
||
// 「邀请谁」由服务端决定:客户端请求体里**只有 gameplay 一个 u32**,没有任何候选 NPC 字段。
|
||
// 候选 = NpcCultivationGameplayTypeConfig[91056000].InvitableNPC(11 个),
|
||
// 已逐个核过 NpcCultivationConfig → FightSpiritID → FightSpiritConfig.AgentId 链完整。
|
||
// 复现验证(配 NpcInviteTable 的日志 tag):
|
||
// 1. 走到保龄球机前,选「对战 / NPC」模式;
|
||
// 2. 按 HUD 上第 3 个按钮(键位 Z,文案「邀请伙伴」);
|
||
// 3. 服务端应出现 [NPCINVITE] AskSimulationInviteNpc gameplay=91056000 -> pushed ...;
|
||
// 4. 报文日志应出现 S2C NOTIFY SyncNpcChatLeaveGameplay [0x03D4B6DC] body=8b
|
||
// (★ 必须是 8b;如果是 9b 且首字节 FF,说明载荷契约漏了 Inline=true,见 NpcInviteWireContracts.cs 顶部);
|
||
// 5. 画面:NPC 被生成(入场 Timeline)→ 进入保龄球对战。
|
||
//
|
||
// ---------------------------------------------------------------------------------------
|
||
// AskSimulationInviteNpc (63301526) — args: u32 gameplay ; return: void
|
||
//
|
||
// 两件事缺一不可:
|
||
// (a) 回一个空的 Ok(形状本来就是 void,DefaultReturnCatalog 那条 body 本身没错);
|
||
// (b) ★ 主动推 SyncNpcChatLeaveGameplay(npcCultivationId, gameplay) —— 这才是让客户端动起来的东西。
|
||
// 只做 (a) 就是修复前的「按了没反应」。
|
||
// ---------------------------------------------------------------------------------------
|
||
func GameRouter_NpcInvite_AskSimulationInviteNpc4229938(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_NpcInvite_AskSimulationInviteNpc4229938
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// AskRestaurantInviteNpc (67057178) — args: u32 restaurantId ; **NOTIFY,没有回包**
|
||
//
|
||
// 因为是单向 notify,客户端不会等任何 invoke 回包 —— 唯一的响应手段就是那条推送。
|
||
// 修之前服务端只做了「unimplemented notify -> accepted」,等于什么都不做 ⇒ 按了没反应。
|
||
//
|
||
// ⚠ 载荷是 RestaurantConfig.Id,不是 gameplay id。别照 AskSimulationInviteNpc 那样直接用。
|
||
// ---------------------------------------------------------------------------------------
|
||
func GameRouter_NpcInvite_AskRestaurantInviteNpc4229938(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_NpcInvite_AskRestaurantInviteNpc4229938
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// AskGetNpcRandomWearFashions (63398939) — args: u32 spiritId ; return: list<u32>
|
||
//
|
||
// CreateNpcGame_Async 在生成被邀 NPC 之前会拿它抽随机时装(BowlingGameManager.lua:423-434):
|
||
// local npcFashionList = fashionResult.list
|
||
// gCS.LuaUtils.CreateClientAgentByCfg(agentTemplateId, pos, eulerAngles, cb, npcFashionList or 0)
|
||
//
|
||
// 旧路径落在 DefaultReturnCatalog4229938.cs:483 的 "list7-empty"(体 = FF 01)。
|
||
// ⚠ 但按上面的标记规则,FF 01 解析出来是**非空的空表 {}**,而 Lua 里 {} 是**真值**
|
||
// ⇒ `{} or 0` 得到 {} ⇒ 把一张表当 suitId 传进 C# 绑定,后果不可控。
|
||
// 这里显式回 **null 列表**(单字节 00)⇒ 客户端拿到 nil ⇒ `nil or 0` = 0。
|
||
// 0 是客户端自己写明的"不套随机时装"分支,最安全,且不影响 NPC 生成。
|
||
// ---------------------------------------------------------------------------------------
|
||
func GameRouter_NpcInvite_AskGetNpcRandomWearFashions4229938(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_NpcInvite_AskGetNpcRandomWearFashions4229938
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// 派对 NPC 的「预制动作」表选择(2026-09-26 第 38 轮)。
|
||
//
|
||
// 判据来源(都是配表,不是我编的):
|
||
// · `PartyConfig.GamePlayType`(ConfigDump_v3\PartyConfig.json,7 条全有值)
|
||
// 13030000 露营=91055000 13030001 凌云=91055000
|
||
// 13030002 引导=91056001 13030003 舞会=91056002
|
||
// 13030004/13030005 联机=91055000(哨兵落点)
|
||
// · 动作 id 表 = `GameplaySettings.PartyNpcPoi*`(private-server.json 可调)
|
||
//
|
||
// ⚠ 为什么不用 `SpawnPartyNpcsAsync` 的 `anchorNote`/`spotCfg` 推断玩法:
|
||
// `spotCfg` 只在 `HasUsableSpot` 时才有意义,而联机那两条(13030004/13030005)落点被
|
||
// HasUsableSpot 判掉 ⇒ 走玩家锚点分支 ⇒ 若在那边推断就永远拿不到 GamePlayType。
|
||
// 所以这里**只看 partyId → PartyConfig.GamePlayType**,与落点走哪条分支解耦。
|
||
// ---------------------------------------------------------------------------------------
|
||
func GameRouter_NpcInvite_PartyNpcPoiActionIdsFor(ctx context.Context, partyId uint32, note string) []any {
|
||
// Router implementation for GameRouter_NpcInvite_PartyNpcPoiActionIdsFor
|
||
return nil
|
||
}
|
||
|
||
func GameRouter_NpcInvite_StartSingleParty4229938(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_NpcInvite_StartSingleParty4229938
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 算出「本次派对每个受邀 NPC 该穿哪一套」(2026-09-26 第 53 轮)。
|
||
// 返回 NpcCultivationId → FashionSuitConfig.Id(查不到的角色**不入表** ⇒ 走旧兜底)。
|
||
// <para>
|
||
// 为什么必须由服务端决定(全部有实证,不是推断):
|
||
// <list type="number">
|
||
// <item>客户端在**露营**这条路上送不出玩家选的服装 —— PartyConfig#13030000.FashionsCanRent
|
||
// 是空数组(只有舞会 #13030003 配了 [11199901, 11199900]),
|
||
// ChangeFashionPanelStore_RentMode.lua:555 BuildRentSuitList 的租借列表因此恒空,
|
||
// rentMap 恒空 ⇒ 「自行搭配」分支产出 fashionsInfo = nil。</item>
|
||
// <item>实测三条 C2S 报文(30 字节,逐字节相同)是
|
||
// {Rents={}, UseDefaultFashion=true} ⇒ 玩家点的是「使用默认套装」。</item>
|
||
// <item>两条出路都只能落到「服务端自己按配表选」⇒ 本函数。</item>
|
||
// </list>
|
||
// </para>
|
||
// <para>
|
||
// 取值优先级:
|
||
// <list type="number">
|
||
// <item>客户端 Rents(玩家明确选的租赁套装,露营送不出、舞会能送):
|
||
// Rents[spiritId] = FashionRentConfig.Id ⇒ 再跳 FashionSuitId。</item>
|
||
// <item>该角色的 DressCode 原皮:FashionSpiritDressCodeConfig 里
|
||
// SpiritId 相同、TagList 含 PartyConfig.SuitTag(露营 = 1)、
|
||
// RecommendPriority 最小的那条。⛔ 放宽规则:18 个角色里只有 15021040 有
|
||
// TagList ∋ 2 的条目 ⇒ 舞会(SuitTag = 2)必须能退到「任意 tag 最优」。</item>
|
||
// <item>都查不到 ⇒ 不入表 ⇒ SuitId = 0 ⇒ 客户端回落 11190001(= 改动前行为)。</item>
|
||
// </list>
|
||
// </para>
|
||
// <para>
|
||
// 关掉本功能的官方开关:gameplay.partyNpcFashion = "off"
|
||
// (见 PrivateServerConfig.GameplaySettings.PartyNpcFashion)。
|
||
// </para>
|
||
//
|
||
func GameRouter_NpcInvite_ResolvePartyNpcSuits(ctx context.Context, partyId uint32, npcCultivationIds []any, fashions any) any {
|
||
// Router implementation for GameRouter_NpcInvite_ResolvePartyNpcSuits
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// PartyOver (63396665) — **无参数**(客户端 writer 是空体);return: void
|
||
//
|
||
// ★★ 第 38 轮:派对「结束演出」的最后一块拼图。
|
||
//
|
||
// 为什么以前「结束派对」什么都不发生:
|
||
// PartyHudPanelStore.lua:970-992 OnClickExitBtn(确认框 MessageConfig.StopParty 之后)
|
||
// gClientToGameDelegate:PartyOver().Callback = function (errorId)
|
||
// if errorId ~= LTConfig.MessageConfig.Ok then ... return end ← 反编译 ==/JMPF 取反
|
||
// gPanelManager:Close(self.m_Id) -- 只关 HUD
|
||
// end
|
||
// 服务端侧只有 `DefaultReturnCatalog4229938.cs:481` 的**空 Ok 兜底**(未登记白名单、
|
||
// 没有 handler)⇒ 客户端关掉 HUD 就完了,**演出与结束面板永远不出现**。
|
||
//
|
||
// 真正的触发者是**服务端推 SyncPartySettleData(68990488)**:
|
||
// Service\GameToClientImpl.lua:2341 SyncPartySettleData → gPartyManager:OnSyncSettleData
|
||
// Manager\GameSystem\PartyManager.lua:746 OnSyncSettleData
|
||
// · 单机(CheckIsLinkMode()=false,PartyManager.lua:781-792):
|
||
// timelineData.loadFromLocal = true
|
||
// gTimelineManager:Timeline_LoadAndPlay("play_party_end", timelineData)
|
||
// onPlayCallback = CheckShow(PARTY_END_PANEL, settleData)
|
||
// onLoadFailedCallback = 同上 ⇒ 本地没这段 timeline 也不会卡住
|
||
// ⇒ **演出是本地资源**,settleData 只负责喂结束面板 + 起播。
|
||
// · 联机才用 settleData.OnlineSettleResult 的四个 pid 组 linkPidList 播 ol_play_party_end。
|
||
//
|
||
// ★ 参数:**没有**。`LuaGen\AutoGen\ClientToGameDelegate.lua` 里
|
||
// SerializerHelper.PartyOver_Serializer = function(writer) end ← 空体
|
||
// ⇒ 千万不要按「第一个 u32 是 partyId」去解 body(body 长度 0)。
|
||
// partyId 从 session 取(StartSingleParty4229938 写进 PartySingleIdSessionKey)。
|
||
//
|
||
// ★ 顺序:**先推结算再回 Ok**。回 Ok 的 Callback 只做 Close(PARTY_HUD_PANEL),
|
||
// 而 OnSyncSettleData 的 PreparePartyEnd 也会关它 —— 先推能让「演出起播」
|
||
// 发生在 HUD 关闭的同一帧之前,不会出现「面板关了但演出还没起」的空档。
|
||
//
|
||
// ★ 推送形状(100% 来自客户端自己的 writer,不是猜 —— 铁律 5):
|
||
// RPCSerializeAuto.lua Auto.WritePartySettleData + Auto.WritePartyOnlineSettleResult
|
||
// + Auto.WritePartyPlayerSettleData
|
||
// RPCDeserializeAuto.lua `midToReader[68990488] = Base.ReadComplex(reader, Auto.Reader[541])`
|
||
// ⇒ 外层是 **Complex(带 0xFF)**;`OnlineSettleResult` 走 Base.WriteComplex(0xFF);
|
||
// `SettleList` 的每一项走 WriteComplexWrap(也是 0xFF,与 reader 的 ReadComplex 对称)。
|
||
// 三个 list 都是 WriteList7Bit ⇒ `[UxCollection]` 用默认的 Count=Int7 / Marker=0xFF 即可。
|
||
// ---------------------------------------------------------------------------------------
|
||
func GameRouter_NpcInvite_PartyOver4229938(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_NpcInvite_PartyOver4229938
|
||
var zero any; return zero
|
||
}
|
||
|
||
func GameRouter_NpcInvite_InviteMultiNpcChat4229938(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_NpcInvite_InviteMultiNpcChat4229938
|
||
var zero any; return zero
|
||
}
|
||
|