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
Ananta-ps/internal/gameserver/routers/game_router_npc_invite.go
T

212 lines
13 KiB
Go
Raw Normal View History

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
}