300 lines
15 KiB
Go
300 lines
15 KiB
Go
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
|
||
}
|
||
|