289 lines
15 KiB
Go
289 lines
15 KiB
Go
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…)⇒ 它们是<b>双向断开的孤儿车道</b>。
|
||
// Tarjan 的结果更狠:全图 **286 个强连通分量**,最大的那个有 13886 条(97%),
|
||
// 剩下 285 条各自成形单影只的分量。
|
||
//
|
||
// 后果:日志里 `车道图不通 start=1795 target=1972` 反复出现且**永远不解开** —— 因为 lane 1972 /
|
||
// 6901 / 13108 的「能到它的车道数」= 1(只有它自己)。而 <c>SolveRoute</c> 的 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"`
|
||
}
|
||
|