package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // 车辆路网(服务端侧 ZoneGraph 简化版)—— 给 T3-7 车流提供「车道 handle + 沿车道里程」。 // 数据来自捐赠端 `zonegraph_lanes.csv`(WorldMap_Release,14171 条车道), // 由 `_gen_vehicle_lanes.py` 压成两份运行时表: // `Drmk.Server/Drmk.App/data/vehicle_lanes.csv` = handle;bx;by;bz;ex;ey;ez;length;speed // `Drmk.Server/Drmk.App/data/vehicle_lane_links.csv` = handle;to1;to2;... // ★ 为什么用捐赠端那份而不是 `_ref/lanes/vehicle-lane-links.csv`: // 后者 Type==1 前向边只有 10911 条、只覆盖 56.9% 的车道,且是捐赠端 next 的**真子集**; // 捐赠端 next 是多值后继,覆盖 13910/14171 = 98.2%,换道接缝中位 0.00m。 // ★ 与 PedLaneGraph 的关键差别:客户端按 DistanceAlongLane 在**它本地的真实车道曲线**上 // 插值(MassTrafficInterpolationUtils),所以服务端不需要车道折线,只要里程单调推进。 // 这里的 bx/ez 只用于「玩家附近挑车道」和 despawn 判定。 // type VehicleLaneGraph struct { Cache Table `json:"cache"` Available bool `json:"available"` LaneCount int32 `json:"laneCount"` // 最大强连通分量里的车道数(导航只会吸附到这些车道上)。排障用。 RoutableLaneCount int32 `json:"routableLaneCount"` // 强连通分量个数。理想值 1;2026-09-30 实测 286(见 MarkRoutableLanes)。 ComponentCount int32 `json:"componentCount"` NavCache NavIndex `json:"navCache"` } const ( VehicleLaneGraph_CellSize float32 = 100 ) // // 算一条车道的弓高(弦中点相对真实曲线的最大横向偏离,米)。见 Lane.Sagitta。 // 直车道(弧长≈弦长)返回 0。 // func GameRouter_VehicleLanes_SagittaOf(ctx context.Context, lane any) float32 { // Router implementation for GameRouter_VehicleLanes_SagittaOf return 0 } // // 弦上参数 t(0..1)处的横向偏离(米)。t=0/1 落在端点 ⇒ 偏离 0;t=0.5 最大 ⇒ 等于弓高。 // func GameRouter_VehicleLanes_LateralDeviationAt(ctx context.Context, sagitta float32, t float32) float32 { // Router implementation for GameRouter_VehicleLanes_LateralDeviationAt return 0 } func GameRouter_VehicleLanes_Load(ctx context.Context, ) any { // Router implementation for GameRouter_VehicleLanes_Load var zero any; return zero } // ----------------------------------------------------------------------------------------- // ⛑⛑ 2026-09-30:车道图有 **285 条孤岛车道**,这是「自动驾驶开不到自定义标点」的真因。 // // 体检脚本 `_vehicle_lane_graph_check.py` 的结果(可直接复跑): // 车道 14171 条、链接表只覆盖 13910 条 ⇒ **261 条没有任何后继、255 条没有任何前驱**, // 两者的样例 id 完全重合(89/93/135/140/232/236/410/417…)⇒ 它们是双向断开的孤儿车道。 // Tarjan 的结果更狠:全图 **286 个强连通分量**,最大的那个有 13886 条(97%), // 剩下 285 条各自成形单影只的分量。 // // 后果:日志里 `车道图不通 start=1795 target=1972` 反复出现且**永远不解开** —— 因为 lane 1972 / // 6901 / 13108 的「能到它的车道数」= 1(只有它自己)。而 SolveRoute 的 Dijkstra 只走前向 // 后继,玩家把目的地标点落到这种车道附近时,**永远解不出路** ⇒ Start 之后根本没有 cruise 下发 // ⇒ 现象就是「按了自动驾驶没反应」。 // 商店/POI 之所以大多没事:它们的 endPos 是走 `GpsAreaConnectMgr` 算出来的**路口/出入口点**, // 天然落在主干道上;自定义标点给的是玩家自己点的原始坐标,撞上孤儿车道就是纯运气。 // // ⇒ 治法:**导航吸附时只认最大强连通分量里的车道**。孤儿车道本来就没法参与路径, // 拿它当起点/终点必然失败;跳过它去找次近的正常车道,代价是终点可能偏移几米。 // (真要根治得重跑 `_gen_vehicle_lanes.py` 把这 261 条接上去,本次不动数据。) // ----------------------------------------------------------------------------------------- func GameRouter_VehicleLanes_MarkRoutableLanes(ctx context.Context, table any) any { // Router implementation for GameRouter_VehicleLanes_MarkRoutableLanes var zero any; return zero } func GameRouter_VehicleLanes_Next(ctx context.Context, table any, handle int32) any { // Router implementation for GameRouter_VehicleLanes_Next var zero any; return zero } func GameRouter_VehicleLanes_Num(ctx context.Context, s string) float32 { // Router implementation for GameRouter_VehicleLanes_Num return 0 } func GameRouter_VehicleLanes_CellOf(ctx context.Context, x float32, z float32) any { // Router implementation for GameRouter_VehicleLanes_CellOf var zero any; return zero } // // 在 (px,pz) 的**环形**区间内随机挑一条车道当作出生点:距玩家 // [minDistance, radius]。 // ⛑ 为什么必须有 minDistance(2026-09-29 修):原来只筛「≤ radius」, // 于是车会直接刷在玩家脚边几米处;而且出生点固定取车道起点(Distance=0), // 而车道起点大量落在路口里 ⇒ 观感就是「车刷在我身边,还不在车道上」。 // 更糟的是客户端 AetherVehicleViewManager 会在 // CANDIDATE_GO_DISTANCE_SQ=400(20m)内把车流车提升成 GO(完整载具实体), // 提升后不再吃我们的 Aether 车道插值 ⇒ 冻在原地,于是玩家身边越积越多不动的车。 // func GameRouter_VehicleLanes_TryPickSpawn(ctx context.Context, px float32, pz float32, radius float32, minDistance float32, rng any, state any, x float32, y float32, z float32, speed float32) bool { // Router implementation for GameRouter_VehicleLanes_TryPickSpawn return true } // // 「召唤载具从车道开过来」的生成点(2026-10-03):在 (px,pz) 的环形区间 // [minDistance, maxDistance] 内,挑一条车道上距离合适的点,并返回车道朝向。 // ⛑⛑ 与 TryPickSpawn(T3-7 车流出生)的三点差别,别混用: // ① 只挑 Lane.Routable 的车道。吸附到那 285 条孤岛车道上 ⇒ // Dijkstra 解不出路 ⇒ 一条 Cruise 都不发 ⇒ 「叫了车但没反应」(2026-09-30 血泪)。 // ② 不只取车道起点:沿弦采样 5 个 t,用落在环内的那一段 —— 车道起点大量落在路口里, // 刷在路口的车开出来会横穿。 // ③ 返回车道朝向(FacingOf 同一套角度约定),车头顺着路, // 不然车要先原地掉头,观感是「车卡了一下才走」。 // :允许的横向偏离(米),0 = 不过滤(旧行为)。 // 车道 CSV 只有弦端点,弯道时弦中点在真实路面之外 ⇒ 车会刷到人行道/花坛里。 // 给 1.5 之类的值后,弯车道只保留靠近端点的那些采样点(偏离随 t 两端趋 0)。 // :多台车连叫时不想每次都刷在同一个位置,交给调用方给随机源。 // func GameRouter_VehicleLanes_TryPickDriveInSpawn(ctx context.Context, px float32, pz float32, minDistance float32, maxDistance float32, maxLateralMeters float32, rng any, x float32, y float32, z float32, facingDegrees float32) bool { // Router implementation for GameRouter_VehicleLanes_TryPickDriveInSpawn return true } func GameRouter_VehicleLanes_BuildNavIndex(ctx context.Context, ) any { // Router implementation for GameRouter_VehicleLanes_BuildNavIndex var zero any; return zero } // // 找 (px,pz) 半径内最近的车道,并把点投影到它的弦上(T = 归一化里程)。 // 只看 XZ 平面:竖直方向(y) 对「哪条路最近」没有意义,而我们本来也只有端点。 // ⛑⛑ 2026-09-30:只返回 Lane.Routable 的车道(最大强连通分量内的)。 // 吸附到孤儿车道上 ⇒ Dijkstra 永远解不出路 ⇒ 按自动驾驶没反应。详见 MarkRoutableLanes。 // 车流的 TryPickSpawn 不受影响(那条路径不走这里)。 // func GameRouter_VehicleLanes_TrySnapLane(ctx context.Context, px float32, pz float32, maxRadius float32, snap any) bool { // Router implementation for GameRouter_VehicleLanes_TrySnapLane return true } // // 同 TrySnapLane(float, float, float, out LaneSnap),多一个横向偏离过滤。 // ⛑⛑ 召唤车「停在人行道 / 花坛里」的另一半根因:停车点是玩家坐标投影到最近车道弦上 // 得到的,弯车道时这个点同样在路面之外。给 (如 1.5) // 即可只接受贴近真实路面的投影位置。 // ⛑ 传 0(= 上面那个重载)保持旧行为,自动驾驶导航走的就是那条,别动它。 // func GameRouter_VehicleLanes_TrySnapLane_2(ctx context.Context, px float32, pz float32, maxRadius float32, maxLateralMeters float32, snap any) bool { // Router implementation for GameRouter_VehicleLanes_TrySnapLane_2 return true } // 一条车道的弦与长度。找不到返回 false。 func GameRouter_VehicleLanes_TryLaneGeometry(ctx context.Context, handle int32, bx float32, by float32, bz float32, ex float32, ey float32, ez float32, length float32) bool { // Router implementation for GameRouter_VehicleLanes_TryLaneGeometry return true } // 后继车道(多值)。没有后继返回空数组。 func GameRouter_VehicleLanes_NextOf(ctx context.Context, handle int32) []any { // Router implementation for GameRouter_VehicleLanes_NextOf return nil } // // 规划一条**直行优先**的路径:从起始车道出发,每一步都挑「方向最贴合当前朝向」的后继。 // 返回接下来要走的车道序列(不含起始车道本身)。 // ★★ 为什么需要它(2026-09-28):车流原来是每到一条车道就**随机挑后继**(PickNext), // 于是车会乱拐、掉头、钻死路、很快被回收 —— 用户看到的就是「开很短距离就停下/消失」。 // 有了预规划路径,车沿道路连续跑 1~2 段甚至更远,观感从"随机乱窜"变成"正常行驶"。 // ★ 不直接用导航那套 Dijkstra:Dijkstra 求的是**到指定目标**的最短路, // 而车流没有目的地,要的是"顺着路往前开"。直行优先恰好表达这个意图, // 且天然排除掉头(dot 为负的后继排最后)。 // ★ 接缝超过 的后继直接跳过 —— 与 Advance 的硬上限一致, // 否则规划出来的路径里会混进几百米的瞬移跳。 // func GameRouter_VehicleLanes_PlanRoute(ctx context.Context, startLane int32, maxHops int32, maxHop float32) []any { // Router implementation for GameRouter_VehicleLanes_PlanRoute return nil } // // 沿车道推进指定距离(米)。换道时把 切到下一条并把里程归零。 // 返回 false = 走到路网死路,调用方应当回收这辆车。 // ★ :只在接缝 ≤ 该值的后继里挑。捐赠端 next 是多值后继, // 实测 5.4% 的后继接缝 > 12m(最大 340m),直接随机挑会让车「瞬移一段甚至倒退」。 // 找不到合格后继时退回「接缝最小」的那条,并如实把 Gap 报给调用方。 // // // 换道接缝的**绝对硬上限**(米)。超过就判定"开到尽头",返回 false 让调用方回收。 // 为什么需要它:PickNext 在所有后继都被"方向反折"过滤掉时会退回 fallback // (全局最小接缝)—— 那条可能远在天边。实测日志里出现过 worstGap=394.1m, // 而车道数据本身 p99 接缝就有 347m、max 2965m(>30m 的占 13.92%)。 // 跳过去就是几百米的瞬移,比车在路口消失难看得多 —— 宁可回收。 // func GameRouter_VehicleLanes_Advance(ctx context.Context, s any, meters float32, rng any, maxGap float32, hop any, maxHop float32, preferredNext int32) bool { // Router implementation for GameRouter_VehicleLanes_Advance return true } // // 挑下一条车道:在「接缝最小」的基础上放宽 米的容差再随机选, // 并排除方向反折的后继(那会让车掉头,视觉上像倒退瞬移)。 // ★ 为什么是**相对容差**而不是「接缝 ≤ N 米」的绝对阈值: // 实测 400 起点 × 600 跳,绝对阈值 8m 会让 15.05% 的衔接 > 12m、p99 达 607m; // 而「总选最小缝」只有 2.55% > 12m、p99 50m。原因是绝对阈值一旦命中就随机选, // 容易拐进后继质量差的车道;贪心取最近则倾向留在连贯的主干道上。 // 所以 maxGap=0 = 严格取接缝最小(并列最小里随机),是实测最优。 // func GameRouter_VehicleLanes_PickNext(ctx context.Context, from any, table any, rng any, maxGap float32, gap float32) int32 { // Router implementation for GameRouter_VehicleLanes_PickNext return 0 } // 服务端侧的线性插值的粗略世界坐标(只用于 AOI 判定 / 日志,客户端自己有真实曲线)。 func GameRouter_VehicleLanes_PositionOf(ctx context.Context, s any, x float32, y float32, z float32) any { // Router implementation for GameRouter_VehicleLanes_PositionOf var zero any; return zero } // 距当前车道末端还有多少米(用于「接近路口就减速」)。 func GameRouter_VehicleLanes_RemainOf(ctx context.Context, s any) float32 { // Router implementation for GameRouter_VehicleLanes_RemainOf return 0 } // 当前车道的限速(m/s)。车换道后会变,所以每 tick 都要重取。 func GameRouter_VehicleLanes_LimitOf(ctx context.Context, s any) float32 { // Router implementation for GameRouter_VehicleLanes_LimitOf return 0 } // // 朝向 弧度(0 = +Z,顺时针为正),由车道端点方向推出。 // ★ 2026-09-21 修正:原先这里返回「度」却被直接填进 ClientVehicleInitData.Facing, // 实测线上包里 Facing = -88.5 —— 那是度数当弧度用了。Facing 是弧度, // EulerAngles 才是度(调用处各自换算)。 // func GameRouter_VehicleLanes_FacingOf(ctx context.Context, s any) float32 { // Router implementation for GameRouter_VehicleLanes_FacingOf return 0 } type DriveState struct { Lane int32 `json:"lane"` Distance float32 `json:"distance"` } // --------------------------------------------------------------------------------------------- // 路线规划(GameRouter.VehicleNav.cs)用的取值口。数据仍然只有这一份: // 只为「最近车道」另建了一份索引(按起点/中点/终点三个关键 cell 挂号),因为 try 9 是长车道 // 只按起点挂号会漏掉「从路中间上车」的情形,而我们不能动 TryPickSpawn 那张 begin-only 索引 // (改了会改变 T3-7 车流的随机出生分布)。 // --------------------------------------------------------------------------------------------- type NavIndex struct { Cells map[any][]int32 `json:"cells"` } // 一次 TrySnapLane 的结果。 type LaneSnap struct { Handle int32 `json:"handle"` T float32 `json:"t"` Distance float32 `json:"distance"` X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` } // 一次车道衔接的细节(诊断用)。 type LaneHop struct { FromLane int32 `json:"fromLane"` ToLane int32 `json:"toLane"` // 旧车道终点 → 新车道起点的接缝距离(米)。越大越像瞬移。 Gap float32 `json:"gap"` }