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

135 lines
8.9 KiB
Go
Raw Permalink Normal View History

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 ""
}