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

213 lines
13 KiB
Go
Raw 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
//
// 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
}