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"` }