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

268 lines
15 KiB
Go
Raw Permalink 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
//
// 行人路网(服务端侧 ZoneGraph 简化版)—— 让氛围行人**沿着真实人行道走**,而不是原地站着。
// 数据来自世界车道导出(`All lanes.rar`):
// `_ref/lanes/lanes/pedestrian-lane-polylines.csv` (Handle;Seq;X;Y;Z,244398 点)
// `_ref/lanes/lanes/pedestrian-lane-links.csv` (FromHandle;ToHandle,263448 条)
// 由 `_ref/_gen_ped_lanes.py` 压成两份运行时表:
// `Drmk.Server/Drmk.App/data/ped_lanes.csv` = 每条车道一行:handle;x0;y0;z0;x1;y1;z1;...
// `Drmk.Server/Drmk.App/data/ped_lane_links.csv` = 每条车道一行:handle;to1;to2;...
// ★ 2026-09-22 新增第三份表(本轮 T2-37 落点):
// `Drmk.Server/Drmk.App/data/ped_lane_meta.csv` = handle;width;tags
// 由 `_gen_ped_lane_meta.py` 从 `zonegraph_lanes_ped.csv`(65,547 段)按 1 米网格反查上来,
// 命中率 100%。它带两个自产路网没有的属性:
// width = 人行道真实宽度(中位 2.0 m)⇒ 横向 clamp,别让所有人走在同一条几何中线上
// tags = 位标记,131072 = PedestrianBanned(禁行)⇒ 投放 / 换道时跳过
// 用法:给一个行人分配一条车道 + 起点进度,然后每 tick 调 Advance 推进固定距离,
// 把结果塞进 `SyncLogicAgentCommand`(TaskWayPointMove)发给客户端。走到车道尽头就顺着 link 换下一条,
// 没有合格出边就原地掉头(Dir 取反),所以行人永远不会"走出路网消失"。
//
type PedLaneGraph struct {
//
// 横向偏移的绝对上限(米),来自 `ambientPedsLateralMaxMetres`(配置,默认 0.4)。
// 即使车道被反查到很大的宽度也不超过它 —— `ped_lane_meta.csv` 的 width 是 1 m 网格空间哈希
// 反查出来的,窄路有可能被安上宽路的宽度,这是第二道保险。
// ⛑️ **设成 0 就完全不偏移(全员走车道中线)** —— 排查"行人卡墙"时先用它排除嫌疑。
//
MaxLateralMetres float32 `json:"maxLateralMetres"`
//
// 横向可走半径的**安全余量**(米),来自 `ambientPedsLateralMarginMetres`(配置,默认 0.9)。
// HalfWalk = max(0, width/2 - margin) —— margin 越大,行人越往车道中线收。
// ⛑️ 2026-09-26:原值是硬编码 0.6,而车道宽度**中位只有 2.0 m**(p75=3.0 / p90=4.0):
// 2 m 路上算出 HalfWalk=0.4 ⇒ 实测偏移最大 0.36 m,距路缘只剩 0.64 m ⇒ 行人胶囊 + 护栏正好卡在这里。
// 改成 0.9 后:≤1.8 m 的窄路 HalfWalk 直接归 0(走中线),3~4 m 的宽路仍留 ±0.22 m 差异(不会像排队)。
// 判据:先设 `ambientPedsLateralMaxMetres=0`(全员中线)确认卡护栏确实是偏移造成,再回到本项微调。
//
LateralMarginMetres float32 `json:"lateralMarginMetres"`
//
// 出生点的**最小车道宽度**(米),来自 `ambientPedsSpawnMinWidthMetres`(配置,默认 1.8)。
// ⛑️ 2026-09-26:候选池原来只看 `Banned`,完全不看宽度 ⇒ 随机抽到 0.5~1.8 m 的窄道
// (实测宽度中位 2.0 / p10 1.5 / min 0.5,约 17% ≤1.8 m)就生在树池边、栏杆边的小径上。
// 设 0 = 不过滤(回到旧行为)。
//
MinSpawnWidthMetres float32 `json:"minSpawnWidthMetres"`
Cache Table `json:"cache"`
Available bool `json:"available"`
LaneCount int32 `json:"laneCount"`
}
const (
PedLaneGraph_TagPedestrianBanned int32 = 131072
PedLaneGraph_MaxHopGapMetres float32 = 16
PedLaneGraph_MaxRouteSearchMetres float32 = 400
PedLaneGraph_CellSize float32 = 50
PedLaneGraph_MaxGoalSnapMetres float32 = 8
PedLaneGraph_GoalTries int32 = 6
)
func GameRouter_PedLanes_Load(ctx context.Context, ) any {
// Router implementation for GameRouter_PedLanes_Load
var zero any; return zero
}
func GameRouter_PedLanes_Num(ctx context.Context, s string) float32 {
// Router implementation for GameRouter_PedLanes_Num
return 0
}
func GameRouter_PedLanes_CellOf(ctx context.Context, x float32, z float32) any {
// Router implementation for GameRouter_PedLanes_CellOf
var zero any; return zero
}
// 在玩家附近随机挑一条车道的起点,用来当行人的出生点。禁行车道不参与。
func GameRouter_PedLanes_TryPickSpawn(ctx context.Context, px float32, pz float32, radius float32, rng any, x float32, y float32, z float32, state any) bool {
// Router implementation for GameRouter_PedLanes_TryPickSpawn
return true
}
// 沿车道推进指定距离(米)。返回当前世界坐标与朝向(度,0 = +Z)。
func GameRouter_PedLanes_Advance(ctx context.Context, s any, distance float32, rng any, x float32, y float32, z float32, facing float32) bool {
// Router implementation for GameRouter_PedLanes_Advance
return true
}
//
// 到尽头换下一条车道。
// ⛑️ 2026-09-22 两处修正(原来是"随便挑一条出边 + 永远从起点进"):
// ① **就近入口**:比较当前出口到下一条车道**两端**的距离,从近的一端进(可能要反向走),
// 否则行人会"瞬移到另一条车道起点",看起来就是突然横穿 / 回头。
// ② **间隙闸门**:入口距离 > 16 m 的出边视为断开(广场 / 绿化带),不选;
// 全部超限就在本车道原地掉头。
// ③ **禁止回退**(2026-09-22 实测后加):不许立刻走回刚过来的那条车道。
// ⛑️ ③ 是实测逼出来的,不是拍脑袋:我们的路网**双向边成对存在**(A→B 和 B→A 都在表里),
// 而"就近入口"又总是挑离出口最近的候选 —— 于是 A 出去必选 B,B 出去必选 A,行人在两条
// 3~4 m 的短车道之间无限来回。离线复刻 2000 段的实测:
// 乒乓率 95.2% / 每步 30 m 任务的净位移中位数只有 1.2 m / 57.9% 的步在原地打转。
// 加上禁止回退后:乒乓 0% / 净位移中位 10.4 m(4.5×)/ 打转降到 41.5%。
// ⛑️ 掉头时要**清掉 Prev**:否则死胡同里(唯一出边就是刚来的那条)会永远掉头来回;
// 清掉之后掉头率从 22.0% 降到 4.8%,行人能走出死胡同。
//
func GameRouter_PedLanes_TryHop(ctx context.Context, s any, lane any, rng any, table any, next any) bool {
// Router implementation for GameRouter_PedLanes_TryHop
return true
}
//
// 在出边里挑"离当前出口最近"的一条,返回 handle(-1 = 没有合格候选)。
//
// true = 连最近走过的几条一起躲开(第一级),false = 只挡紧邻上一条(第二级)。
func GameRouter_PedLanes_ScanHop(ctx context.Context, s any, lane any, table any, exitX float32, exitZ float32, useMemory bool, dir int8) int32 {
// Router implementation for GameRouter_PedLanes_ScanHop
return 0
}
// 把刚走过的车道记进环形缓冲(新的在头部)。memory ≤ 1 时不建数组。
func GameRouter_PedLanes_Remember(ctx context.Context, s any, lane int32, memory int32) any {
// Router implementation for GameRouter_PedLanes_Remember
var zero any; return zero
}
func GameRouter_PedLanes_Sample(ctx context.Context, lane any, s any, x float32, y float32, z float32, facing float32) any {
// Router implementation for GameRouter_PedLanes_Sample
var zero any; return zero
}
//
// 沿路网规划一条 from → to 的折线 —— `AskClientNpcFindPath`(67974714) 的真路径应答。
// 客户端只在它自己要走路时才来问(我们的人是被服务端 TaskWayPointMove 驱动着走的,
// 但 NPC / 某些单位会走客户端寻路)。现在默认回的是**空路径**(`0xFF01FF01`),
// 等于告诉客户端"没有路" ⇒ 那条单位就站着。
// ⛑️ 规划不出来(路网没覆盖 / 不在同一连通块 / 距离太远)一律返回 null,
// 调用方照旧回空路径 —— 宁可不动,也不要给一条穿墙的直线。
//
//
// 可选输出:每个路径点所属车道的 Lane.HalfWalk(可横向偏移的上限,米)。
// 调用方要按它夹紧横向偏移 —— 窄路上限接近 0,就不该把行人往路边推(会推到墙里)。
//
func GameRouter_PedLanes_PlanRoute(ctx context.Context, fromX float32, fromZ float32, toX float32, toZ float32, maxPoints int32, minSpacing float32, lateralCap []any) []any {
// Router implementation for GameRouter_PedLanes_PlanRoute
return nil
}
//
// 目标导向行程:从 (fromX,fromZ) 走向"以 (centerX,centerZ) 为中心的环形区域内随机一点"。
// ⛑️ 为什么要有这个:随机游走(Advance + TryHop)在我们的路网上
// 只在**局部邻域**打转 —— 86.6% 的车道只有 2 个采样点、长度中位数 3.58 m,行人走几步就到
// 端点、换道、再回来。2026-09-23 用户实测:"行人只是从一条路的一头走到另一头,路很短就很奇怪"。
// 给它一个几十米外的目标,用已经实机验证 100% 可用的 PlanRoute 规划整条路径,
// 行人就会穿过多个路口、像在赶路。
// 终点取环形区域(而非行人自身附近的随机方向)是为了让行程始终留在玩家活动圈内 ——
// 否则行人一路走出 ambientPedsRecycleMeters 被回收,看起来就是"路人凭空消失"。
//
//
// 横向偏移**比例**(-1..1,带符号,终身锁定一侧)。实际偏移量 = 该点车道的
// Lane.HalfWalk × 这个值,再受 MaxLateralMetres 夹住。
// ⛑️ 这里必须是**比例而不是米数**:2026-09-23 用户实测"窄路上行人卡在街边的墙壁里" ——
// 老代码传的是固定的 0.6 m,跟车道多宽完全无关。窄路的 HalfWalk 常常就是 0(路宽 ≤ 0.8 m),
// 该一点都不偏,结果照样把人往外推 0.21~0.54 m ⇒ 推进墙里。
// 随机游走的 Sample 一直用 `HalfWalk × Offset` 是对的,只有目标导向这条路径抄错了。
//
func GameRouter_PedLanes_TryPlanJourney(ctx context.Context, fromX float32, fromZ float32, centerX float32, centerZ float32, minR float32, maxR float32, lateralRatio float32, rng any, maxPoints int32) []any {
// Router implementation for GameRouter_PedLanes_TryPlanJourney
return nil
}
//
// 在玩家周围环形区域里挑一个**已经在人行道上**的行程终点。
// 直接拿环形随机点当终点是错的 —— 那个点在建筑/江里的概率极高(实测 92% 偏离 ≥ 2 m),
// 而 PlanRoute 尾部会把路径最后一点硬拉到 toX/toZ,
// 于是行人的最后一段任务终点在墙里,客户端到不了就一直往墙里推。
//
func GameRouter_PedLanes_TryPickGoalOnRing(ctx context.Context, centerX float32, centerZ float32, minR float32, maxR float32, rng any, gx float32, gz float32) bool {
// Router implementation for GameRouter_PedLanes_TryPickGoalOnRing
return true
}
//
// 把路网漫游状态吸附回 (x,z) 所在的车道 —— 从"目标导向"退回随机游走之前必须调一次。
// 不吸附的话 WalkState.Lane 还停在生成时那条老车道上,下一拍 Advance
// 会从那里出点 ⇒ 行人瞬移几十米。
//
//
// 出生朝向 = 该点在车道上的走向(度,0 = +Z),`Dir=-1` 时自动反向。
// ⛑️ 2026-09-26:出生朝向原来是 _random * 360 - 180,跟车道走向毫无关系,
// 而位移由 `TaskWayPointMove` 的路径点决定 ⇒ 身体朝 A、位移朝 B = 看起来在倒退走路。
// 与 Sample 共用同一套 facing 定义,保证朝向和采样点永远一致。
//
func GameRouter_PedLanes_TryFacing(ctx context.Context, s any, facing float32) bool {
// Router implementation for GameRouter_PedLanes_TryFacing
return true
}
func GameRouter_PedLanes_SnapTo(ctx context.Context, x float32, z float32, s any) bool {
// Router implementation for GameRouter_PedLanes_SnapTo
return true
}
//
// 「脱困 / 起步」点 —— 规划不出路(或起终点重合)时给客户端一个能走的点,而不是回空路径。
// ⛑️⛑️ 2026-09-23 实测:单位 spawn 后第一个问路包就是 From == To(packets 日志 DECODED 行证实),
// 我们回 4 字节空路径 FF 01 FF 01 ⇒ 它拿不到方向,就一直杵到客户端自驱 wander 重新武装(140 s)。
// 卡在花坛 / 门框里的单位同样只会反复问 from ≈ to,回空 ⇒ 永远挪不出来。
// 这里给的是「最近车道上、朝车道远端走 metres 米」的一个点 —— 只要还落在人行道上就不会穿墙。
//
func GameRouter_PedLanes_TryPickEscape(ctx context.Context, x float32, z float32, metres float32, ex float32, ey float32, ez float32) bool {
// Router implementation for GameRouter_PedLanes_TryPickEscape
return true
}
// 找离 (x,z) 最近的车道,并给出最近的点下标。
func GameRouter_PedLanes_NearestLane(ctx context.Context, table any, x float32, z float32, pointIndex int32) int32 {
// Router implementation for GameRouter_PedLanes_NearestLane
return 0
}
// 车道图上的 BFS。限制访问预算,别为一次问路把 88k 条车道全走一遍。
//
// 按**实际距离**的最短路(Dijkstra),而不是按跳数。
// ⛑️⛑️ 2026-09-23 两个真 bug,都是离线量化逼出来的(模拟器 `_pedgate_sim.py`,250 条真实行程):
// ① **没有间隙闸门**。`TryHop`(随机游走)有 `MaxHopGapMetres` 闸门,这里原来**完全没有** ——
// 而 link 表里有 **20.6% 的边端点间隙 > 16 m,最大 859 m**(全量统计,不是抽样)。
// 实测一条 49 m 的行程规划出 416 m 的路径,轨迹里出现"先去西边 80 m 再回到起点"。
// 规划路径(目标导向行程 + `AskClientNpcFindPath` 应答)一直在走这些跨空间的边。
// ② **按跳数最短路**会疯狂使用长车道:车道长度中位 3.56 m 但最长 856 m,
// 跳数最少 ⇏ 距离最短,方向还可能完全背离目标。
// 修完的实测(目标 45~135 m):
// BFS 无闸门:绕行 3.02x(路径 279 m / 净位移 93 m)
// BFS + 闸门:绕行 2.09x(186 m)
// **Dijkstra + 闸门:绕行 1.82x(165 m),成功率 99.6%,单次 2.3 ms** ← 采用
// 城市路网里 1.8x 的绕行是正常的绕街区,可以接受。
//
func GameRouter_PedLanes_ShortestChain(ctx context.Context, table any, start int32, goal int32) []any {
// Router implementation for GameRouter_PedLanes_ShortestChain
return nil
}
// 诊断用:路网里有多少条禁行车道、多少条带宽度。给 GM 面板 / 启动日志看。
func GameRouter_PedLanes_Stats(ctx context.Context, ) any {
// Router implementation for GameRouter_PedLanes_Stats
var zero any; return zero
}
type WalkState struct {
Lane int32 `json:"lane"`
Pos float32 `json:"pos"`
Dir int8 `json:"dir"`
// 横向站位系数(-1..1),一次分配终身不变。
// 同一个 walker 锁一侧,否则会看到行人在人行道左右横跳(donor round 64 同款结论)。
Offset float32 `json:"offset"`
// 上一条走过的车道 handle(-1 = 没有)。换道时禁止立刻退回去 —— 见 TryHop。
Prev int32 `json:"prev"`
//
// 最近走过的车道环形缓冲(元素 -1 = 空)。长度 = `AmbientPedsRouteMemory`;
// 换道时优先躲开这里面的车道,行人就不会在几条短车道之间绕圈。
//
Recent []int32 `json:"recent"`
}