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

136 lines
8.9 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
//
// 派对 NPC 落地 —— 把「被邀请的养成 NPC」真正放进场景(2026-09-26 第 30 轮)。
// WHY THIS FILE EXISTS
// --------------------
// 用户实测「可以选 NPC 了,但邀请的 NPC 看不见」。逐层查证后定位到:**派对分支根本不生成实体**。
// 客户端 NpcChatManager.lua:180-223(收到 SyncNpcChatLeaveGameplay 后的分发):
// · 餐厅 → gRestaurantManager:InviteClientNpc(npcId, gameplay)
// → RestaurantManager.lua:789
// gNpcInviteManager:InviteClientNpcForGamePlayWithPos(
// cfg.RestaurantNpcid, gameplay, self, self.invitePos, ...) ← ★ 生成实体
// · 蹦迪 → BengdiActionManager.lua:380 同上(用 cfg.BengdiNpcid) ← ★ 生成实体
// · 派对 → gPartyManager:StartSingleParty(singlePartyId, multiNpcTidList)
// → 只写 partyInviteNpcIdList + CheckShow(PARTY_HUD_PANEL) ← ✗ 没有实体
// 全量 grep(_lua_allgrep.py,含 212 个含 NUL 的文件)确认 InviteClientNpc* 的调用者
// 只有 RestaurantManager 与 BengdiActionManager 两处 —— **派对一次都没有**。
// 佐证:PartyNpcInvitePanelStore.lua:113-135(派对专用 NPC 面板)是**扫场景实体**挑人的:
// gCS.SceneDataMgr.UnitsManager:LuaGetAllUnits(units)
// for _, unit in ipairs(units) do
// local npcId = self.GetNpcCultivationId(self, unit) -- :180
// ⇒ 场景里没有这个 unit,面板就是空的、场景里也看不见人。
// GetNpcCultivationId 的判据(:180-188):
// local spiritCfg = LTConfig.FightSpiritConfig.GetConfig(unit.ClientData.cardId)
// return spiritCfg and spiritCfg.NpcCultivationRelatedId or 0
// ⇒ 反查用的是 `unit.ClientData.cardId`,而 cardId 属于
// **FightSpiritConfig 的 Id 空间(15xxxxxx)**。
// 🛑 **第 45 轮更正**:本节初稿写的「取值链 = NpcCultivationConfig.FightSpiritID →
// FightSpiritConfig.AgentId,cardId 由 AgentConfig 推出」**是错的**。证据:
// ① NpcCatalog[40140035](Taffy 的 AgentConfig 行)里**没有**
// cardId/FightSpiritId 字段,只有 GeneralModelId/AgentPersonaId/
// BehaviorActionConfigId/AiSettingId —— 推不出 cardId;
// ② CoreHudUIManager_Decisions.lua:557 把 `ClientData.cardId` 直接和
// `LTConfig.FightSpiritConfig.Taffy`(=15020992) 比较。
// ⇒ cardId 只能来自 **AIAgentInfo.FightSpiritId**(下方 dress 参数)。
// AgentId 那张表仍然要(它管 NpcFormworkId=实体模板),
// 但**光有它不够** —— 缺了 FightSpiritId,客户端就不把它当成可玩角色。
// 服务端本来就有「把 NPC 放进场景」的通道(不是新增机制,是复用已验证的):
// GameRouter.Civilians.cs SendCivilianAsync 的**四步序列**
// (SyncAetherAIStaticNpcAddData → SyncManagedLogicAgent → 可选 belongings → 可选 POI),
// GameRouter.FunctionalNpc.cs 的 PlaceFunctionalNpcAsync 就用它铺城市里的店员/售票员。
// 本文件只做一件事:把**派对邀请名单**喂给同一个通道,位置放在玩家身边。
// ⇒ 不发明新的 spawn 路径(对照 FunctionalNpc.cs 开头那句 "this file does not invent a spawn path")。
// ⚠ 已知边界(诚实标注,未验证的部分不要当结论)
// 1. 「能不能被 PartyNpcInvitePanelStore 认成养成 NPC」取决于客户端 unit.ClientData.cardId
// 是否由 AgentConfig 推出 15020992(Taffy)/ 对应 FightSpiritID。本实现只保证
// **实体生成且可见**;识别问题要等实测反馈。
// 2. 落点用「玩家身边环绕一圈、半径 2.5m」,理由见铁律 17(落点必须实测站得住)——
// 2.5m 是保守值,小于这个容易和玩家胶囊互顶。若发现半空抽搐/被顶飞,先把半径调大。
// 3. ~~不做驻留 AI:POI Action 传 0(无闲置动作),NPC 会站着。~~ **★ 第 38 轮已接上**
// —— 改为由 `GameplaySettings.PartyNpcPoi*` 提供「预制动作」表,按人轮转传给
// `CivilianSpawnItem.PoiActionId`(露营 = 地面野餐/围坐,舞会 = 鼓掌/围观)。
// 仍然**不做**驻留巡逻 AI(不推移动任务),但「一动不动站着」这一条已经不成立。
//
//
//
// 撤掉当前场上这一批派对 NPC,返回撤掉的人数。
// ★ 第 46 轮新增:抽出来给两处复用 —— ①重开派对(`SpawnPartyNpcsAsync` 开头);
// ②**派对结束**(`PartyOver4229938`)。在此之前只有①有撤人逻辑,派对结束后
// 那批 NPC 会**永久留在原地**(用户反馈「派对结束后角色一直不走」)。
// ⛑⛑ **双包并用**(2026-09-26 现场实测后修正):
// · `SyncAetherAINpcRemove`(68332284) 的契约注释写着
// 「`pid` = 当初 **`ClientCrowdInitData.Id`**」⇒ 它是 **crowd 通道**的撤人包。
// 派对 NPC 走的是 **static 通道**(`SyncAetherAIStaticNpcAddData`)
// ⇒ 单独发它**撤不掉**(packets 日志 5 条 body=8b 都在、客户端毫无反应)。
// · 真正「**销毁一个单位**」的是 `SyncLogicAgentLeave`(68281244),它是
// `SyncLogicAgentEnter`(68719890) 的另一半(注册 / 注销进 logic-agent 管理器)。
// 依据:`GameRouter.Switching.cs` 的**反向**用法已实测 —— 切人留场时**不发**它,
// 单位就留在大世界里;发了它单位才消失(迁移期「Leave 之后模型才重建」那条记录)。
// ⇒ 两个都发:Leave 负责 static 通道,Remove 兜住 crowd 通道,互不冲突。
// (⚠ `GameRouter.EnemyTerritory.cs:313` 那句「Enter(68281243)」是**错注释** ——
// 权威 id 表 `MethodId.dump.cs` 里没有 68281243。)
// ⛑ 撤完就把 `placed` 清空 ⇒ 重复调用是安全的(第二次进不来)。
//
func GameRouter_PartyNpc_RemovePlacedPartyNpcsAsync(ctx context.Context, session *GameSession) int32 {
// Router implementation for GameRouter_PartyNpc_RemovePlacedPartyNpcsAsync
return 0
}
//
// 记下「本次派对邀请了谁」。由 InviteMultiNpcChat handler 在选定 NPC 之后调用。
// 服务端是**自己选**名单的(GameRouter.NpcInvite.cs 的 candidates[cursor]),
// 所以这里记的就是真话,不需要去解客户端那份 List7Bit。
//
func GameRouter_PartyNpc_RememberPartyInvitedNpc(ctx context.Context, session *GameSession, npcCultivationId uint32) any {
// Router implementation for GameRouter_PartyNpc_RememberPartyInvitedNpc
var zero any; return zero
}
// StartSingleParty 用:取走本次邀请名单并清空(一次派对消费一次)。
func GameRouter_PartyNpc_TakePartyInvitedNpcs(ctx context.Context, session *GameSession) []any {
// Router implementation for GameRouter_PartyNpc_TakePartyInvitedNpcs
return nil
}
//
// 把名单里的养成 NPC 放到**调用方指定的圆心**周围。返回一行人类可读的日志摘要(调用方直接写日志)。
// 位置策略:**按玩法分两档**(2026-09-26 第 49 轮起)——
// <list type="bullet">
// <item>露营档( = null,默认):
// 以 (x,y,z) 为圆心、半径 = PartyNpcSpawnRadius(默认 2 m),从
// 起均匀环绕;NPC 一律**面向圆心**。</item>
// <item>舞会档( 有值):以 (x,y,z) 为**舞池点**、
// 沿「舞池正面」的**横排**铺开(与朝向垂直,间距 = 同一个 PartyNpcSpawnRadius),
// 全员**统一朝 **(官方 DanceRotation)——
// **不是**朝圆心。舞池点的来源见 PartyDanceSpotCatalog4229938。</item>
// </list>
// ★ 第 34 轮:**圆心与基准角由调用方给**,本函数不再自己决定「圆心 = 玩家」。
// 调用方优先传配表的 `PartySpot` + `PartyRotation`(官方规定场地,
// 见 `ClientData\Client4229938\PartySpotCatalog4229938.cs`),
// 拿不到才回退玩家位置 —— 那只是兜底,不是设计意图。
// ★★ 第 38 轮:新增 = **预制动作**表(按人轮转)。
// 在此之前这里写死 `new CivilianSpawnItem(agentId, 0)` ⇒ 第 4 步被跳过
// ⇒ 实测 `poi=0 action=0` ⇒ NPC 只有兜底 idle。空数组 = 保持旧行为(不发 POI)。
// 动作 id 的语义与来源见 `GameplaySettings.PartyNpcPoi*` 的注释。
// ★★★ 第 49 轮(舞会「走官方路径」):新增 ——
// **舞会摆法**的开关。null(默认)= 露营摆法(圆周 + 朝圆心,一字未改);
// 有值 = 舞会摆法(横排 + 全员统一朝这个角度,值 = 官方
// PartyDanceSpotConfig.DanceRotation)。
// ⚠ 两档在同一个循环里**互斥**,不是把露营的圆周参数套到舞会上 —— 这正是用户那句
// 「舞会有舞会的摆法,不要参考露营」的落点。
//
func GameRouter_PartyNpc_SpawnPartyNpcsAsync(ctx context.Context, session *GameSession, npcCultivationIds []any, x float32, y float32, z float32, baseAngle float32, anchorNote string, poiActionIds []any, unifiedFacingDeg any, suitByNpcId map[string]any, scatterRadiusMeters any) string {
// Router implementation for GameRouter_PartyNpc_SpawnPartyNpcsAsync
return ""
}