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
Ananta-ps/internal/gameserver/routers/game_router_ambient.go
T

300 lines
15 KiB
Go
Raw 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
//
// 自动氛围行人(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
}