157 lines
8.9 KiB
Go
157 lines
8.9 KiB
Go
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"`
|
|||
|
|
}
|
|||
|
|
|