package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // 自动氛围行人(ambient pedestrians)—— 玩家一进世界,街上就有会走的人。 // ★★★★★ 2026-09-20 终版配方(移植 donor1「PrivateServer修改」实机验证的路人网络): // 1. `SyncAetherAICrowdAdd`(crowd 通道)建人:ActionId=**400**(行为动作组,非 POI 动作)、 // TargetLocationReason=1(Wander)、speed=1.6。 // 2. `SyncManagedLogicAgent`(68771737)(LogicHandoff)—— **必发**。没有它 ⇒ T-pose; // 而且它还是**客户端来要路径(AskClientNpcFindPath)的前提**(donor1 第 81 轮三重证据: // 成功会话 12 条 vs 失败 2 条 / FindPath 310 次 vs 0 次)。 // 3. `SyncLogicAgentEnter`(68719890) —— 把 id 注册进 logic-agent 管理器(任务从它派发)。 // 4. `SyncAgentForceGo`(68698324) —— 启动行为树。 // 5. **移动任务** `SyncLogicAgentCommand`(68050574) + `TaskWayPointMoveCommandData` // (wire tag 39):自带速度/步态(SpecificMethod=Walk)/动画组,客户端的 // LogicBehaviorSchedulerModule 负责寻路+避障+步态。**这是唯一能产生走路步态的通道**。 // 6. 沿真实人行道路网(PedLaneGraph,88361 条车道)每 30m 一段、预计走完+5s 后发下一段。 // ⛑️ 已证伪的两条路(别回头): // · **位置流** `SyncLogicAgentSyncData`(68838628):不带任务 ⇒ 客户端产生不了步态。 // 我们 10Hz 推 6280 条、字节布局逐字节对齐客户端上报,人纹丝不动; // donor1 第 16~23 轮的「服务端硬推位置」同一条死路。 // · **`SyncMoveWandering`**(68745518):走 `WanderingActionModule`(BaseUnit 管线), // 而 crowd NPC 是 AgentEntity(ECS 管线)⇒ 静默失败。 // 多态命令线格式(donor1 第 40 轮反汇编钉死,见 LogicAgentCommandMethods.cs): // tag 字节 = 类型名字母序(39=TaskWayPointMove);基类尾部 MoveId(1B)+DataSource(2B, // Other=255);Lua 表偏 1 不可信。 // func GameRouter_Ambient_AmbientState(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_Ambient_AmbientState var zero any; return zero } // // 用客户端上报的真实位置校正行人的服务端账本。 // ⛑️ 这是**唯一**的客户端位置回馈通道:散人是客户端自驱的(硬规则 19),服务端推完 // `TaskWayPointMove` 就不知道人到底走到哪了,只在自己的账本里把 ped.X/Z 推到 // 任务终点。一旦客户端实际被卡住(贴墙、被玩家推开、插值中断),两边就永久脱节 ⇒ // 之后每一段任务都从幻想位置出发,行人看起来"站在原地不动"。 // 2026-09-23 用户实测:"有 1 个卡在原地不动,打他几下让他位移之后又正常走了" —— // 正是因为击退让客户端位置重新贴合了服务端账本。 // 触发源:AskClientNpcFindPath(67974714) 请求里的 From,就是客户端手里那个 // 单位的真实坐标(同一 session 34/40 个行人 id 都能对上,确认是我们的单位)。 // ⛑️ 双阈值是必须的:小于 min 是正常误差(车道采样点有间距),大于 max 说明这不是 // 我们的单位或者数据异常 —— 两种情况都不能写回,否则行人会被拖着乱跳。 // func GameRouter_Ambient_TryResyncAmbientPed(ctx context.Context, session *GameSession, pedId uint64, realX float32, realZ float32, drift float32, hardReset bool) bool { // Router implementation for GameRouter_Ambient_TryResyncAmbientPed return true } // 点到折线的最短距离,并给出最近点的下标。 func GameRouter_Ambient_RouteDistance(ctx context.Context, route []any, x float32, z float32, nearestIdx int32) float32 { // Router implementation for GameRouter_Ambient_RouteDistance return 0 } func GameRouter_Ambient_SegmentDistance(ctx context.Context, ax float32, az float32, bx float32, bz float32, px float32, pz float32) float32 { // Router implementation for GameRouter_Ambient_SegmentDistance return 0 } // 世界就绪后调用一次;幂等。配置关掉就直接返回。 func GameRouter_Ambient_EnsureAmbientPeds(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_Ambient_EnsureAmbientPeds var zero any; return zero } // // 把场上所有服务端行人**真回收**:逐个发 SyncAetherAINpcRemove 再停循环。 // ⛑️ 为什么不能直接 StopAmbientPeds:那个只停循环、本地链表一清了事, // 客户端上的人还站着 —— 面板关「启用人群」后用户看到的"还是有人"多半就是这批。 // 分块(RemoveBatchSize + RemoveBatchDelayMs)沿用回收路径的老规矩,一次性连发会打爆 socket。 // func GameRouter_Ambient_RetireAmbientPedsAsync(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_Ambient_RetireAmbientPedsAsync var zero any; return zero } // 断开/切场景时停掉循环(不主动移除行人 —— 场景卸载客户端自己清)。 func GameRouter_Ambient_StopAmbientPeds(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_Ambient_StopAmbientPeds var zero any; return zero } func GameRouter_Ambient_RunAmbientLoopAsync(ctx context.Context, session *GameSession, owner any, token context.Context) any { // Router implementation for GameRouter_Ambient_RunAmbientLoopAsync var zero any; return zero } // // 服务端驱动行走循环(donor1 实机配方,2026-09-20 移植):**移动任务步进**,不是位置流。 // 每个 Walking 的行人沿 PedLaneGraph 抽一段 30m 折线,发一条 `TaskWayPointMove` 命令 // (`SyncLogicAgentCommand` 68050574 + wire tag 39,自带速度/步态/动画组); // 预计走完的时间(步长 ÷ 速度 + 5s 余量)过后再发下一段 —— 与 donor1 的 // `CrowdLaneNeedsTarget` 同一条时钟。 // ⛑️ 为什么不用位置流:`SyncLogicAgentSyncData`(68838628)不携带任何任务, // 客户端产生不了步态。donor1 第 39 轮实测 Servo 流 = 走两秒动画然后站住; // 我们 09-20 实测 10Hz 推 6280 条、字节布局与客户端上报逐字节对齐,人依然纹丝不动。 // donor1 AI_HANDOFF 明确把「服务端硬推位置」标记为**已证伪路线**(路人第 16~23 轮的老路)。 // func GameRouter_Ambient_RunAmbientMoveLoopAsync(ctx context.Context, session *GameSession, owner any, token context.Context) any { // Router implementation for GameRouter_Ambient_RunAmbientMoveLoopAsync var zero any; return zero } // // 给一个行人发下一段 `TaskWayPointMove` 移动任务:从当前位置沿路网推进 // 米,沿途采样折线(起点 + 每 ~6m 一点 + 终点)。 // 只有推进成功才写回路网状态 —— 失败时 AmbientPed.Walk 原样保留,下一拍重试。 // func GameRouter_Ambient_PushAmbientStepCommandAsync(ctx context.Context, session *GameSession, owner any, ped any, stepMetres float32, token context.Context) bool { // Router implementation for GameRouter_Ambient_PushAmbientStepCommandAsync return true } // 把折线封装成一条 `TaskWayPointMove` 发出,并回写行人位置与实际路程。 func GameRouter_Ambient_SendAmbientStepAsync(ctx context.Context, session *GameSession, ped any, points []any, token context.Context) bool { // Router implementation for GameRouter_Ambient_SendAmbientStepAsync return true } func GameRouter_Ambient_TickAmbientAsync(ctx context.Context, session *GameSession, owner any, token context.Context) any { // Router implementation for GameRouter_Ambient_TickAmbientAsync var zero any; return zero } func GameRouter_Ambient_IsCrowdChannel(ctx context.Context, cfg any) bool { // Router implementation for GameRouter_Ambient_IsCrowdChannel return true } func GameRouter_Ambient_SpawnAmbientPedAsync(ctx context.Context, session *GameSession, owner any, cfg any, sx float32, sy float32, sz float32, walking bool, walk any, token context.Context) any { // Router implementation for GameRouter_Ambient_SpawnAmbientPedAsync var zero any; return zero } // static 通道:静态 NPC + 行为树 + simulation owner(人一定出得来、有动画)。 func GameRouter_Ambient_SendAmbientStaticPedAsync(ctx context.Context, session *GameSession, cfg any, formwork uint32, id uint64, spot any, facing float32, token context.Context) any { // Router implementation for GameRouter_Ambient_SendAmbientStaticPedAsync var zero any; return zero } // // crowd 通道:`SyncAetherAICrowdAdd`(68757757)(donor_lyco082 实机验证的市民通道)。 // 客户端自驱游荡,服务端不用发移动包。⚠️ NpcFormworkId 必须是 AgentConfig id(4013xxxx)。 // func GameRouter_Ambient_SendAmbientCrowdPedAsync(ctx context.Context, session *GameSession, cfg any, formwork uint32, id uint64, spot any, facing float32, token context.Context) any { // Router implementation for GameRouter_Ambient_SendAmbientCrowdPedAsync var zero any; return zero } // // 下发/续期一个行人的游荡意图。MoveId 不是序号,是 BattleMoveActionType, // 填 0(Unknown) 等于"没有 locomotion 状态",所以填 2(WalkFront)。 // ⛑️ PathTags / 角度窗口是最容易踩的两个静默失败点,见文件头。 // func GameRouter_Ambient_PushAmbientWanderAsync(ctx context.Context, session *GameSession, cfg any, ped any, token context.Context) any { // Router implementation for GameRouter_Ambient_PushAmbientWanderAsync var zero any; return zero } type AmbientPed struct { Id uint64 `json:"id"` X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` WanderArmedTicks int64 `json:"wanderArmedTicks"` // 服务端驱动行走的路网状态(PedLaneGraph)。Walking=false 就是原地不动。 Walking bool `json:"walking"` Walk WalkState `json:"walk"` Speed float32 `json:"speed"` // 上一个移动任务的下发时间(TickCount64)—— 任务步进的续接时钟。 TargetSetTicks int64 `json:"targetSetTicks"` // 上一次实际下发的路程(米)。<=0 表示还没量过,按 stepMetres 兜底。 // 用它算续接时钟才准:末段常常短于 stepMetres,按满步长等就会"多站一会儿"。 LastStepMetres float32 `json:"lastStepMetres"` // 续接时钟的随机相位(毫秒)。同一批生成的人如果节拍完全一致, // 就会"一起起步、一起掉头" —— 各人偏移一点就错开了。 StepJitterMs int32 `json:"stepJitterMs"` // 目标导向行程的剩余折线(服务端规划,客户端沿它走)。null = 退回随机游走。 Route []any `json:"route"` // 下一个要走的折线下标。 RouteIdx int32 `json:"routeIdx"` // // 连续派单失败次数(成功即清零)。达到 `AmbientPedsStepFailLimit` 就回收重生。 // ⛑️ 这是**服务端自己就能发现**的一路卡死:路网状态坏了(车道没了 / 规划不出来 / // 采样推进失败)时,这个行人会永久停在"派不出任务"的状态 —— 客户端看不到任何异常, // 表现出来就是一个人杵着不动。donor round 84 的判据是"连续 5 次失败 ⇒ 判定客户端已不再走"。 // StepFailCount int32 `json:"stepFailCount"` // // 位置回馈校正次数,计在 `AmbientPedsStallWindowMs` 这个滑动窗口里。 // 达到 `AmbientPedsStallResyncLimit` 说明这个单位**反复**跟服务端账本脱节(贴墙 / 卡地形), // 校正已经救不回来 ⇒ 回收重生到别处,而不是让它一直杵在那儿。 // ResyncCount int32 `json:"resyncCount"` // ResyncCount 的计数窗口起点(TickCount64)。 ResyncWindowTicks int64 `json:"resyncWindowTicks"` // // 客户端回执「命令已装好」的次数(AskReportLogicAgentCommandSuccess2 67057559)。 // 2026-09-24 自捐赠端 DremkaPS 移植。他们那句注释是移植的理由: // SyncLogicAgentCommand 对服务端是 fire-and-forget —— 客户端装了这条命令、还是拿到 // 就丢了,在出站日志里长得一模一样。有了这对计数,「行人还是不走」才能分成 // 「收了 12 条一条没跑」和「每一条都被拒了」两种完全不同的故障。 // CommandAcks int32 `json:"commandAcks"` // 客户端回执「命令失败」的次数(AskReportLogicAgentCommandFail 67193144)。 CommandFails int32 `json:"commandFails"` // 最近一条回执带回的 commandId(与 s_ambientCommandSeq 同源)。 LastAckCommandId int32 `json:"lastAckCommandId"` } type AmbientSessionState struct { SyncRoot any `json:"syncRoot"` Cts *context.CancelFunc `json:"cts"` MoveCts *context.CancelFunc `json:"moveCts"` // 最近一次已知的玩家位置 —— 目标导向行程以它为中心取终点, // 保证行人不会一路走出 ambientPedsRecycleMeters 被回收。 HomeX float32 `json:"homeX"` HomeZ float32 `json:"homeZ"` Loop *any `json:"loop"` MoveLoop *any `json:"moveLoop"` Peds []AmbientPed `json:"peds"` } // 一个候选站位(真实行人车道端点,带地面高度)。 type PedSpawnPoint struct { X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` } // // 行人布点表:`Drmk.Server/Drmk.App/data/ped_spawn_points.csv`(x,y,z)。 // 100 米网格索引,按玩家位置查半径内候选。 // type PedSpawnTable struct { Cache Table `json:"cache"` Count int32 `json:"count"` } const ( PedSpawnTable_CellSize float32 = 100 ) func GameRouter_Ambient_Load(ctx context.Context, ) any { // Router implementation for GameRouter_Ambient_Load var zero any; return zero } func GameRouter_Ambient_CellOf(ctx context.Context, v float32) int32 { // Router implementation for GameRouter_Ambient_CellOf return 0 } // // 在 (x,z) 半径内随机挑一个站位,且与已有行人保持 minGap 米。 // 找不到就返回 null(比如玩家站在水域/荒地,路网上没点)。 // func GameRouter_Ambient_PickNear(ctx context.Context, x float32, z float32, radius float32, minGap float32, existing []any) any { // Router implementation for GameRouter_Ambient_PickNear var zero any; return zero } // // 氛围行人的 NPC 模板池(= AgentConfig id)。直接复用已投影好的市民组目录, // 只取 NpcFormworkId —— POI 一律留 0,否则人会去播动作而不走路。 // `CivilianGroups` 是为"给每个 POI 动作配一个体型匹配的人形"建的, // 它不保证模板是路人 —— 池里原本混着店员、摊主、刷怪NPC、剧情角色,还有上百个 // 共用同一具通用模特的"假人"。这里按 `GameRouter.IsCivilianFormwork` 的三道筛子收敛成纯市民。 // ⛑️ **来源恒为 `CivilianGroups.json`(用户 2026-09-26 定)**:本池只做减法, // 绝不从 `NpcCatalog`(9345 行全表)往里补 id —— 那里有敌人、动物、机器人、淤泥怪。 // 过滤掉一批之后剩下多少就是多少(现在 1985 个),不要为了凑数去别的表里捞。 // type PedFormworkPool struct { Cache []uint32 `json:"cache"` Count int32 `json:"count"` // 过滤前 / 过滤后的数量,给日志和面板看(判"是不是滤空了")。 RawCount int32 `json:"rawCount"` Filtered int32 `json:"filtered"` } func GameRouter_Ambient_Load_2(ctx context.Context, ) []any { // Router implementation for GameRouter_Ambient_Load_2 return nil } func GameRouter_Ambient_Pick(ctx context.Context, ) any { // Router implementation for GameRouter_Ambient_Pick var zero any; return zero }