package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // 派对 NPC「驻留行为」—— 让被邀请进派对的角色**自己会动**(2026-09-26 第 46 轮)。 // WHY THIS FILE EXISTS // -------------------- // 用户原话:「最主要的还是邀请进派对角色的行为问题,**不互动一个也要让角色们自由行动, // 自由做动作,不然还是死气沉沉**」。 // 现场事实(第 45/46 轮实测):被邀角色**能被正确认出来(cardId 已修)、会播 POI 预制动作, // 但一动不动地站在原地做完那一套动作**。原因是 `SendCivilianAsync` 只发到「POI 动作」为止, // 少了让客户端接管 locomotion 的那几步。 // ★★★★★ 官方口径(全部来自配表/客户端契约,不是猜) // · `SyncMoveWandering`(68745518) 的契约注释原文(CrowdMethods4229938.cs:204-207): // 「**让一个已投放的 NPC/行人"自己走起来"的唯一官方包。** // 它**不带目的地**——只有半径、角度窗口和时间预算,路径由客户端 // ZoneGraph/AetherAI 自己挑。⇒ 服务端根本不需要路网来做"行走", // 只需要周期续期(MaxTime 是总时长,走完就永久站住)。」 // · 项目自己的 static 路人配方(GameRouter.Ambient.cs:804-834 / 792-794): // `SendCivilianAsync`(建人)+ **`SyncAgentForceGo`(启动行为树)**… 且 // 「crowd 通道是客户端自驱游荡,服务端发游荡包反而多余;**static 通道必须发, // 否则只播 idle**」—— 派对 NPC 正好走 static 通道。 // · `GameRouter.Ambient.cs:16-27` 的终版配方把 `SyncLogicAgentEnter` 列为第 3 步: // 「把 id 注册进 logic-agent 管理器(**任务从它派发**)—— 不发它,任务可能被静默丢弃」。 // ★ 为什么**不能**改用 crowd 通道(`SyncAetherAICrowdAdd` + `TargetLocationReason=1` 自驱游荡): // `ClientCrowdInitData4229938` 的字段只有 // `NpcFormworkId / AgentPersonaId / UrbanDiversityConfigId / DesiredSpeed / ActionId / // TargetLocationReason / FashionSuitId` + 基类 `Id/Position/Facing/EulerAngles` // —— **没有 `AIAgentInfo`** ⇒ 带不了 `FightSpiritId` ⇒ `ClientData.cardId` 恒 0 // ⇒ 第 45 轮刚修好的「烤肉邀请面板」会**立刻退回空列表**。所以通道必须留在 static。 // ★★ 另一条被排除的路(配表级硬判据,别再回头): // 24 个可邀角色的 `AgentConfig.AiSettingId` 指向的行为集 // (`AgentAIBehaviorSetsBattleAISBehaviorConfig`)**全部没有游荡行为**: // 40213395 Companion_Taffy / 40215027 Companion_zhuchang_Lin / 40213369 Companion_Battle / // 40215025 Bot_Common_SMG / 40213004 Behavior_EmptyBehavior(无战斗AI)… // 它们的 `Behavior_Wander` 与 `Behavior_StandBy` **都是空字符串**。 // 对照:会走路的市民档是 40213001/40213002(`Main_Common_NPC`, // `Behavior_Wander = Behavior_Common_Wander`、`Behavior_StandBy = Behavior_Common_Patrol`)。 // ⇒ 这些角色在官方场景里是「**跟着玩家的同伴**」,本来就不游荡;拿它们当驻场 NPC // 就必须由服务端/客户端额外下发行为意图。这正是本文件的职责。 // ⚠ 已知边界(诚实标注) // 1. `SyncMoveWandering` 对 **crowd 通道(ECS/AgentEntity 管线)** 已被证伪 // (Ambient.cs:33-34:走 WanderingActionModule = BaseUnit 管线 ⇒ 静默失败)。 // 本文件只用在 **static 通道** 上(Ambient.cs:792-794 明确说 static 必须发)。 // 若实测仍不动 ⇒ 把 `PartyNpcRoamMode` 换成 `"waypoint"`(路点巡游,同一份契约里 // 被称作「唯一能产生走路步态的通道」),无需改代码。 // 2. 游荡半径取小值(默认 2~10 m)——营地里有家具与碰撞体,绕远会走丢或穿模。 // 3. 走完 `MaxTime` 就永久站住 ⇒ 必须**周期续期**,见 `PartyNpcRoamRearmSeconds`。 // ★★★ 第 47 轮定案(用户现场):「**停走**」才是正解。 // 用户实测 waypoint 档 = 「**可以走了,不过是乱走**」⇒ 走路通道本身已跑通 // (`SyncLogicAgentCommand`(68050574) + `TaskWayPointMove`(tag 39) 有效, // `SyncMoveWandering` 无效),但马上要求「把角色**围成一圈**、**坐在地面**」。 // ⇒ 默认 `PartyNpcRoamMode = "none"`;`SyncLogicAgentEnter` / `SyncAgentForceGo` // 已**上移到模式门之前**(`none` 档也要发 —— 行为树没起来,连 POI 坐姿都保不住)。 // ⛔ 位置与朝向**本来就是对的**(`SpawnPartyNpcsAsync` 圆周排布 + `angle + π` 朝圆心), // 是 walk 任务把人走散、并用行走姿态顶掉了坐姿 ⇒ 不需要改坐标,也不需要另写编排。 // func GameRouter_PartyNpcRoam_RoamStateOf(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_PartyNpcRoam_RoamStateOf var zero any; return zero } // 重开派对(撤掉上一批人)时调用:停掉旧循环、清空账本。 func GameRouter_PartyNpcRoam_ResetPartyNpcRoam(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_PartyNpcRoam_ResetPartyNpcRoam var zero any; return zero } func GameRouter_PartyNpcRoam_PartyRoamActive(ctx context.Context, ) bool { // Router implementation for GameRouter_PartyNpcRoam_PartyRoamActive return true } // // 把一个人登记进驻留行为,并起后台续期循环。 // ⛑ 在 SendCivilianAsync **之后**调用:单位必须先存在,任务才有人接。 // func GameRouter_PartyNpcRoam_ArmPartyNpcRoamAsync(ctx context.Context, session *GameSession, entityId uint64, x float32, y float32, z float32, token context.Context) any { // Router implementation for GameRouter_PartyNpcRoam_ArmPartyNpcRoamAsync var zero any; return zero } // 按当前模式下发一次行为意图(并回写 PartyNpcRoamEntry.ArmedTicks)。 func GameRouter_PartyNpcRoam_PushPartyRoamIntentAsync(ctx context.Context, session *GameSession, entry any, cfg any, token context.Context) any { // Router implementation for GameRouter_PartyNpcRoam_PushPartyRoamIntentAsync var zero any; return zero } // // `SyncMoveWandering`(68745518) —— 客户端自驱游荡意图。 // 逐字段照抄 `GameRouter.Ambient.PushAmbientWanderAsync`(同一条已验证的 static 配方), // 只有半径/时长换成派对用的保守值。 // ⛑ `MoveId` 不是序号,是 `BattleMoveActionType`;填 0(Unknown) 等于「没有 locomotion 状态」。 // func GameRouter_PartyNpcRoam_PushPartyWanderAsync(ctx context.Context, session *GameSession, entry any, cfg any, token context.Context) any { // Router implementation for GameRouter_PartyNpcRoam_PushPartyWanderAsync var zero any; return zero } // // 对照/退路档:用 `SyncLogicAgentCommand`+`TaskWayPointMove`(tag 39) 让 NPC 沿一条 // **绕着营地的小多边形**巡游。`Ambient.cs` 文件头称这条通道是 // 「**唯一能产生走路步态的通道**」。 // 服务端自己记 `Cursor*`(= 上一腿终点)⇒ 不需要客户端上报位置。 // func GameRouter_PartyNpcRoam_PushPartyWaypointAsync(ctx context.Context, session *GameSession, entry any, cfg any, token context.Context) any { // Router implementation for GameRouter_PartyNpcRoam_PushPartyWaypointAsync var zero any; return zero } // // 续期循环。`MaxTime` 走完客户端会**永久站住**,所以必须周期性补发。 // 与 `EnsureLeftBehindLoop` 同款:session 断开后写 socket 会抛异常,循环自己退出, // 不需要额外的停止钩子。 // func GameRouter_PartyNpcRoam_EnsurePartyNpcRoamLoop(ctx context.Context, session *GameSession, state any) any { // Router implementation for GameRouter_PartyNpcRoam_EnsurePartyNpcRoamLoop var zero any; return zero } func GameRouter_PartyNpcRoam_RunPartyNpcRoamLoopAsync(ctx context.Context, session *GameSession, state any, token context.Context) any { // Router implementation for GameRouter_PartyNpcRoam_RunPartyNpcRoamLoopAsync var zero any; return zero } type PartyNpcRoamEntry struct { EntityId uint64 `json:"entityId"` HomeX float32 `json:"homeX"` HomeY float32 `json:"homeY"` HomeZ float32 `json:"homeZ"` // 上一次「下发/续期」的时刻(Environment.TickCount64)。 ArmedTicks int64 `json:"armedTicks"` // waypoint 模式:这一腿预计走完的时刻。 EtaTicks int64 `json:"etaTicks"` // waypoint 模式:下一腿的起点(= 上一腿的终点,服务端自己记账,不依赖客户端上报)。 CursorX float32 `json:"cursorX"` CursorY float32 `json:"cursorY"` CursorZ float32 `json:"cursorZ"` } type PartyNpcRoamState struct { Entries []PartyNpcRoamEntry `json:"entries"` SyncRoot any `json:"syncRoot"` Cts *context.CancelFunc `json:"cts"` Loop *any `json:"loop"` // ★ 第 49 轮:停走档那条日志每批只打一次(原先每人一条 ⇒ 10 条一样的话 // 把 [PARTYNPC] 生成汇总 淹掉,而汇总才是真正要看的证据)。 StopWalkLogged bool `json:"stopWalkLogged"` }