267 lines
15 KiB
Go
267 lines
15 KiB
Go
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"`
|
|||
|
|
}
|
|||
|
|
|