407 lines
22 KiB
Go
407 lines
22 KiB
Go
package routers
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
var _ = sync.Mutex{}
|
||
var _ = context.Background
|
||
|
||
//
|
||
// T3-7 真实车流 —— 服务端驱动车辆沿车道跑。
|
||
// ★ 为什么必须服务端驱动:客户端侧没有任何「车流密度解锁」S2C 包。
|
||
// 人流有 SyncSetCrowdDensity(68996045) + SyncCrowdSpecifiedDensityRefresh(68195167),
|
||
// 车流只有 C2S 的 GmChangeAetherVehicleDensity(69493525)。实测:服务端一个 vehicle 包都不发
|
||
// 时,街上一台车都没有(含路边停放)。所以车必须逐台下发并持续推进。
|
||
// ★ 推进方式:服务端只发「车道 handle + DistanceAlongLane」,客户端 AetherCity 侧的
|
||
// MassTrafficInterpolationUtils 按本地 ZoneGraph 曲线插值出位置与朝向。
|
||
// 这是捐赠端 §88「车歪 + 走一点就停」的正解 —— 那条路靠服务端算世界坐标,这里不算。
|
||
// ★ ControlType:VehicleControlType { Invalid=0, AetherVehicle=1, RaidVehicle=2 }。
|
||
// T1-16 骇入用 2(不接管驾驶),T3-7 必须用 1,否则客户端不做车道模拟。
|
||
//
|
||
func GameRouter_Traffic_ShouldPushLaneData(ctx context.Context, owner any) bool {
|
||
// Router implementation for GameRouter_Traffic_ShouldPushLaneData
|
||
return true
|
||
}
|
||
|
||
func GameRouter_Traffic_TrafficState(ctx context.Context, session *GameSession) any {
|
||
// Router implementation for GameRouter_Traffic_TrafficState
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 世界就绪后调用一次;幂等。配置关掉或车道表缺失就直接返回。
|
||
// ⛑ 2026-09-29:改为 async —— 起循环前先清掉**上一个会话残留**的车
|
||
// (见 LiveTrafficIds 的注释)。不清的话切场景 / 切角色后
|
||
// 上一批车会原封不动留在客户端世界里冻着。
|
||
//
|
||
func GameRouter_Traffic_EnsureTrafficAsync(ctx context.Context, session *GameSession) any {
|
||
// Router implementation for GameRouter_Traffic_EnsureTrafficAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 断开 / 切场景时停掉循环。
|
||
func GameRouter_Traffic_StopTraffic(ctx context.Context, session *GameSession) any {
|
||
// Router implementation for GameRouter_Traffic_StopTraffic
|
||
var zero any; return zero
|
||
}
|
||
|
||
func GameRouter_Traffic_RunTrafficLoopAsync(ctx context.Context, session *GameSession, owner any, token context.Context) any {
|
||
// Router implementation for GameRouter_Traffic_RunTrafficLoopAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 一台车的服务端视角位置(XZ)。
|
||
// ⛑ Cruise 车(TrafficCar.Cruise)没有真实坐标 —— 客户端不上报 AI 车的
|
||
// 移动,而我们也不再推车道插值 ⇒ 只能用「上次下发的目标点」当估算值。
|
||
// 回收距离判定、出生避撞判定都读这里,别直接调 VehicleLaneGraph.PositionOf
|
||
// (对 Cruise 车那是冻结在出生点的假坐标,会导致永远回收不掉)。
|
||
//
|
||
func GameRouter_Traffic_TrafficCarPos(ctx context.Context, car any, x float32, z float32) any {
|
||
// Router implementation for GameRouter_Traffic_TrafficCarPos
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 这条车道上已经有车了吗(按各车的出生车道比)。见 TrafficSpawnAvoidSameLane。
|
||
// ⛑ 近似判据:车的 Drive.Lane 是出生车道、之后不再更新,所以这是
|
||
// 「这条车道最近被占着」的保守判断(会占到那台车被回收为止),方向正是我们要的「错开」。
|
||
//
|
||
func GameRouter_Traffic_LaneAlreadyUsed(ctx context.Context, owner any, lane int32) bool {
|
||
// Router implementation for GameRouter_Traffic_LaneAlreadyUsed
|
||
return true
|
||
}
|
||
|
||
//
|
||
// 这条车道的前进方向是不是朝着玩家(玩家在车头前方 ⇒ 车会开过来,形成迎面车流)。
|
||
// 见 AetherSettings.TrafficSpawnFacePlayerRatio。
|
||
//
|
||
func GameRouter_Traffic_HeadsTowardPlayer(ctx context.Context, lane int32, sx float32, sz float32, player Vec3) bool {
|
||
// Router implementation for GameRouter_Traffic_HeadsTowardPlayer
|
||
return true
|
||
}
|
||
|
||
// 候选出生点是否与现有车辆离得足够远。
|
||
func GameRouter_Traffic_FarFromOtherCars(ctx context.Context, owner any, x float32, z float32, minGap float32) bool {
|
||
// Router implementation for GameRouter_Traffic_FarFromOtherCars
|
||
return true
|
||
}
|
||
|
||
// 按玩家位置把车辆数维持在 target 台;跑出路网或跑太远的车回收。
|
||
func GameRouter_Traffic_ReconcileTrafficAsync(ctx context.Context, session *GameSession, owner any, player Vec3, cfg any, rng any, stamp float64, token context.Context) any {
|
||
// Router implementation for GameRouter_Traffic_ReconcileTrafficAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 建出 BaseVehicle 实体本身(捐赠端 GameRouter.Vehicles.SpawnDirectAsync 的 proven 序列):
|
||
// SyncLogicVehicleEnter → SyncSpawnVehicle → SyncChangeVehicleInteractable。
|
||
// Aether 的 AddData 只接管**已存在**的车,所以这一步必须在它之前。
|
||
//
|
||
func GameRouter_Traffic_SpawnTrafficVehicleBodyAsync(ctx context.Context, session *GameSession, entityId uint64, configId uint32, x float32, y float32, z float32, facingDeg float32, speed float32, cfg any, token context.Context) any {
|
||
// Router implementation for GameRouter_Traffic_SpawnTrafficVehicleBodyAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// ★★ 方案 A(2026-10-01):给一台车流车下发 SyncAssignVehicleAITask(68549522) +
|
||
// VehicleCruiseParameters,让客户端本地 AI 把它开走。
|
||
// 为什么换这条管线(两轮证伪后的结论):
|
||
// ① 服务端没有「别把车提升成 GO」的有效通道(ForceGo 实机无效);
|
||
// ② 客户端那个疑似负责 GO 切换的类 AetherVehicleViewManager,8 个探针 24 分钟
|
||
// 全 0 命中 ⇒ 根本没在用,按它设计的注入器 hook 方案整体作废。
|
||
// ⇒ 与其继续猜,不如用我们已实机验证过的驱动方式:出租车 / 玩家自驾都走
|
||
// SyncAssignVehicleAITask,车真的会自己开、会避障、会等红灯。
|
||
// ⛑ 前置条件(由调用方保证):这台车的 AddData 必须是
|
||
// ControlType=2 + LaneHandle=-1,否则客户端的车道模拟会和本地 AI 抢方向盘。
|
||
// ⛑ 已知局限:客户端不上报 AI 车的坐标 ⇒ 服务端的 car.CruiseX/Z 只是
|
||
// 「上次下发的目标点」,回收/避撞判定全是近似值。
|
||
//
|
||
//
|
||
// 挑一串「能往前开」的车道当 Cruise 目标点。
|
||
// ⛑⛑ 为什么不能只从「上次目标所在车道」往后规划(2026-10-01 23:2x 实机根因):
|
||
// 车开完第一次目标后,`CruiseLane` 落在那条路径的最后一段上 —— 而我们路网里有
|
||
// 大量死路车道(286 个孤岛分量、很多车道没有合格后继)⇒ `PlanRoute` 只给出 1 段,
|
||
// 日志刷成一串 `points=1/1`,每台车每次都拿到同一个目标点
|
||
// (`target=(901,1775) lane=6817` 反复出现)⇒ 车到点就停,再也不走。
|
||
// ⇒ 判据:规划出来的段数 < trafficCruiseMinHops 就判「死路口袋」,
|
||
// 改从玩家附近随机挑一条车道重新规划(车本来就在玩家周围 ⇒ 新目标一定可达)。
|
||
// 同时避开 RecentTargetLanes ⇒ 不会好几台车挤向同一个点互撞。
|
||
//
|
||
// 车道终点串;空 = 实在规划不出来(调用方应跳过这次下发)。
|
||
func GameRouter_Traffic_PickCruiseRoute(ctx context.Context, owner any, car any, cfg any, rng any) any {
|
||
// Router implementation for GameRouter_Traffic_PickCruiseRoute
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 快照场上其它 Cruise 车的当前目标点(用于新目标的空间去重)。
|
||
func GameRouter_Traffic_SnapshotOtherTargets(ctx context.Context, owner any, self any) []any {
|
||
// Router implementation for GameRouter_Traffic_SnapshotOtherTargets
|
||
return nil
|
||
}
|
||
|
||
// 把这条目标车道记进「最近用过」的滑动窗口(最多 24 条)。
|
||
func GameRouter_Traffic_NoteTarget(ctx context.Context, owner any, lane int32) any {
|
||
// Router implementation for GameRouter_Traffic_NoteTarget
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 从一条车道往后规划,把每段终点收集成点串。起点拿不到几何 ⇒ 返回空串。
|
||
func GameRouter_Traffic_BuildChain(ctx context.Context, startLane int32, cfg any) any {
|
||
// Router implementation for GameRouter_Traffic_BuildChain
|
||
var zero any; return zero
|
||
}
|
||
|
||
func GameRouter_Traffic_AssignTrafficCruiseAsync(ctx context.Context, session *GameSession, owner any, car any, cfg any, rng any, token context.Context) any {
|
||
// Router implementation for GameRouter_Traffic_AssignTrafficCruiseAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// Cruise 车的推算位置推进(dead reckoning):按当前速度朝目标点走,到点即停。
|
||
// ⛑ 为什么必须推:客户端不上报 AI 车的坐标,而回收/避撞/骇入判定全靠这个估算值。
|
||
// · 不推(一直停在出生点)⇒ 车开走了也永远判成「在身边」,老车占着名额;
|
||
// · 直接跳到目标 ⇒ 目标一超出回收半径,车刚出生就被判出界回收(23:2x 实机踩过)。
|
||
//
|
||
func GameRouter_Traffic_AdvanceCruiseEstimates(ctx context.Context, owner any, dt float32) any {
|
||
// Router implementation for GameRouter_Traffic_AdvanceCruiseEstimates
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 客户端回报 AI 任务状态时(AskUpdateVehicleAITaskStatus)在车流这边认领一下。
|
||
// ★ 为什么要接:车开到目标点会自己停住,而服务端收不到到达通知 ⇒ 只能干等
|
||
// trafficCruiseRefreshSec(默认 30s)。日志里其实早就有这些包,只是被
|
||
// `[VEHICLE-HACK] ignored stale/duplicate/unowned AI status … status=2` 丢掉了。
|
||
// ⇒ 这里认领后把刷新时刻归零,下一 tick 立刻续发目标点,车不会在原地杵半分钟。
|
||
// ⛑ 只置标志、不在 handler 里发包(热路径上不能同步 await 一堆 Notify)。
|
||
//
|
||
func GameRouter_Traffic_TryApplyTrafficCruiseStatus(ctx context.Context, session *GameSession, token uint64, status byte) bool {
|
||
// Router implementation for GameRouter_Traffic_TryApplyTrafficCruiseStatus
|
||
return true
|
||
}
|
||
|
||
//
|
||
// 方案 A 的续驶:给到期(TrafficCruiseRefreshSec)的 Cruise 车重发目标点。
|
||
// ⛑ 为什么还要定时重发兜底:客户端不一定回状态(被顶住/卡住时可能什么都不发),
|
||
// 所以「定时器」和「收到终止状态立刻续发」是双保险 ——
|
||
// 前者兜住「客户端沉默」,后者消灭「到了还要干等 30 秒」的空窗。
|
||
// 重发对两种情形都成立:没到 ⇒ 新目标仍在它前方,等价续驶;到了 ⇒ 从脚下继续往前。
|
||
//
|
||
func GameRouter_Traffic_RefreshTrafficCruiseAsync(ctx context.Context, session *GameSession, owner any, cfg any, rng any, token context.Context) any {
|
||
// Router implementation for GameRouter_Traffic_RefreshTrafficCruiseAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 把一台车流车从车道流里摘出来,交还给玩家(骇入 / 上车)。幂等:非车流车直接返回。
|
||
// ★★ 为什么必须摘:车道流每 50ms 用 SyncAetherAIVehicleLaneDatas 覆盖里程,
|
||
// 而骇入下发的是 SyncAssignVehicleAITask(RaidVehicle 通道)—— 两套驱动源。
|
||
// 同时作用时车会「刚加速就被拽回原地」,表现为骇入「能显示但没效果」。
|
||
// 所以玩家接管的第一件事就是停掉车道流。
|
||
// ★ 摘出的手段:重发一条 SyncAetherAIVehicleAddData,
|
||
// ControlType=2(RaidVehicle) + LaneHandle=-1。
|
||
// 这与 RegisterHackVehicleAsync() 的做法一致,其注释原文:
|
||
// "ControlType=RaidVehicle avoids activating lane simulation"。
|
||
//
|
||
func GameRouter_Traffic_DetachTrafficVehicleFromLaneAsync(ctx context.Context, session *GameSession, vehicle any) any {
|
||
// Router implementation for GameRouter_Traffic_DetachTrafficVehicleFromLaneAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 每 tick 计算纵向动力学(跟车 / 交叉让行 / 加减速)→ 推进里程 → 批量下发。
|
||
// 走到死路的车直接回收。
|
||
// ★★ 为什么服务端必须自己做跟车:客户端的 AetherVehicleDetector(前方 trigger)
|
||
// 只对**近距离 GO 车**生效,实测它只避让玩家/行人(AetherVehicleStopReason.Block),
|
||
// 车与车之间照样对撞。客户端那边没有「远程车流互避」逻辑 —— 它只按服务端给的点插值。
|
||
// 所以纵向决策只能放服务端:这里是简化 IDM + 最近接近点(TTC)让行。
|
||
//
|
||
func GameRouter_Traffic_PushTrafficLaneDataAsync(ctx context.Context, session *GameSession, owner any, dt float32, stamp float64, cfg any, rng any, token context.Context) any {
|
||
// Router implementation for GameRouter_Traffic_PushTrafficLaneDataAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 一台车的寿命摘要(回收日志用)—— 用来量化「车开多远就没了」。
|
||
func GameRouter_Traffic_CarLife(ctx context.Context, car any) string {
|
||
// Router implementation for GameRouter_Traffic_CarLife
|
||
return ""
|
||
}
|
||
|
||
//
|
||
// 回收一台车流车 —— **必须连 SyncDestroyVehicle 一起发**。
|
||
// ★★ 只发 SyncAetherAIVehicleRemove 是不够的(2026-09-28 实测):
|
||
// 那个包只把车从 Aether 车流系统里摘掉,**BaseVehicle 实体仍留在客户端场景里**。
|
||
// 服务端这边已经把它从 Cars 移除、不再推里程,所以 [TRAFFIC-STAT] 看着一切正常
|
||
// (stop=0、avg 10m/s),但客户端那台车就那么杵在原地 ——
|
||
// 用户看到的就是「大部分车停在十字路口」,以及「有些车开着开着变色然后停下」
|
||
// (实体脱离 Aether 驱动后渲染状态异常)。
|
||
// ⇒ 补发 SyncDestroyVehicle 才是真正销毁,与 DestroyVehicleAsync 的做法一致。
|
||
//
|
||
func GameRouter_Traffic_RemoveTrafficVehicleAsync(ctx context.Context, session *GameSession, runtime any, id uint64, token context.Context) any {
|
||
// Router implementation for GameRouter_Traffic_RemoveTrafficVehicleAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 本会话当前持有的车流车 id。
|
||
func GameRouter_Traffic_OwnedIds(ctx context.Context, owner any) []any {
|
||
// Router implementation for GameRouter_Traffic_OwnedIds
|
||
return nil
|
||
}
|
||
|
||
//
|
||
// 把本会话的车流车**真回收**:逐个发 SyncAetherAIVehicleRemove + SyncDestroyVehicle。
|
||
// 照抄 RetireAmbientPedsAsync 的做法(分块 + 间隔,别一次性连发打爆 socket)。
|
||
//
|
||
func GameRouter_Traffic_RetireTrafficAsync(ctx context.Context, session *GameSession) any {
|
||
// Router implementation for GameRouter_Traffic_RetireTrafficAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 清掉**上一个会话**留下的残留车(本会话不知道它们、但客户端世界里还在)。
|
||
// 判据:进程级 LiveTrafficIds 里不属于本会话的那些 id。
|
||
//
|
||
func GameRouter_Traffic_RetireStrayTrafficAsync(ctx context.Context, session *GameSession, owner any) any {
|
||
// Router implementation for GameRouter_Traffic_RetireStrayTrafficAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 挑一台「能上路」的车。
|
||
// ★ 第一优先是 VehicleSpawnCatalog4229938(客户端 MassAI.MassTraffic 的
|
||
// VehicleSpawnConfig 表,InMass+CanDrive 共 54 个车型)。只有在这张表里的 cfgId 才能
|
||
// 建出 VehicleDataEntity —— 否则客户端打
|
||
// VehicleInit_Drop_NoConfig ... not found in VehicleSpawn 并丢弃,车看得到但不会动。
|
||
// fleet 列表只作为该表缺失时的兜底。
|
||
//
|
||
func GameRouter_Traffic_PickTrafficConfigId(ctx context.Context, rng any) any {
|
||
// Router implementation for GameRouter_Traffic_PickTrafficConfigId
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ★ 车辆实体必须用 8_200_000_000_000_000_000 号段(与 GameRouter.Vehicle.cs:25 同一分配器)。
|
||
// 2026-09-21 实测:用 7_000_000 这种小整数时客户端完全不建实体 —— 街上没车,
|
||
// 而服务端日志一切正常(AddData / LaneDatas 都在发)。号段不对 = 实体识别失败。
|
||
// 参照:行人 8_300_000_000_000_000_000(DonorNpc.cs:32)、NPC 归属 8_310_...、token 7_100_...
|
||
type TrafficCar struct {
|
||
Id uint64 `json:"id"`
|
||
ConfigId uint32 `json:"configId"`
|
||
Drive DriveState `json:"drive"`
|
||
Speed float32 `json:"speed"`
|
||
TargetSpeed float32 `json:"targetSpeed"`
|
||
// 速度接近 0 的累计秒数(用于僵局解除)。
|
||
StallSeconds float32 `json:"stallSeconds"`
|
||
// --- 寿命计量(2026-09-29):用来量化「车开多远就停/消失」,别再靠肉眼猜 -------
|
||
// 累计行驶里程(米)。回收/超距时打进日志。
|
||
TraveledMeters float32 `json:"traveledMeters"`
|
||
// 累计换道次数。
|
||
Hops int32 `json:"hops"`
|
||
// 存活秒数。
|
||
AliveSeconds float32 `json:"aliveSeconds"`
|
||
// 出生时刻(Environment.TickCount64)。Cruise 车不进 tick 推进,寿命只能这么算。
|
||
SpawnTicks int64 `json:"spawnTicks"`
|
||
// 僵局解除后的「硬冲」剩余秒数:> 0 时无视让行约束,低速强行通过。
|
||
BoostSeconds float32 `json:"boostSeconds"`
|
||
//
|
||
// 红灯前已经等了的秒数(只在真正被红灯刹停时累加)。
|
||
// 超过 TrafficSignalMaxWaitSec 就放行 —— 给「等红灯」一个统一上界,
|
||
// 否则腿数多的路口(4 组周期 44s)会让人觉得车杵在那儿不动。
|
||
//
|
||
RedWaitSeconds float32 `json:"redWaitSeconds"`
|
||
//
|
||
// 在 VehicleRuntimeState.Vehicles 里对应的登记记录(null = 未登记)。
|
||
// 有了它车才是「可骇入」的:ExecuteVehicleControlAsync 第一步就是
|
||
// runtime.Vehicles.TryGetValue(vehicleId),找不到直接拒绝。
|
||
// 每 tick 会把位置/朝向同步进去(骇入的 30m 距离判定和 AI 任务包都读它)。
|
||
//
|
||
Record *VehicleRecord `json:"record"`
|
||
//
|
||
// 预规划路径(接下来要走的车道序列,不含当前车道)。null/空 = 退回随机挑后继。
|
||
// 由 VehicleLaneGraph.PlanRoute 生成,走完会自动重新规划。
|
||
//
|
||
Route []int32 `json:"route"`
|
||
// 当前走到 Route 的第几段。
|
||
RouteIdx int32 `json:"routeIdx"`
|
||
// --- 2026-10-01 方案 A:Cruise(客户端本地 AI 驾驶)------------------------------------
|
||
//
|
||
// true = 这台车交给客户端本地 AI 开(SyncAssignVehicleAITask + Cruise 参数),
|
||
// 不参与服务端的车道推进与 LaneDatas 批次(两套驱动源会打架:
|
||
// 「刚加速就被拽回原地」)。见配置 TrafficCruiseDrive。
|
||
//
|
||
Cruise bool `json:"cruise"`
|
||
// 当前 Cruise 任务的 token(刷新时要先拿它发 Abort(4))。0 = 还没下发过。
|
||
CruiseToken uint64 `json:"cruiseToken"`
|
||
// 上次真正下发 Cruise 目标的时刻(Environment.TickCount64),提前量判定用。
|
||
CruiseAssignedTicks int64 `json:"cruiseAssignedTicks"`
|
||
// 下次重发目标点的时刻(Environment.TickCount64)。
|
||
CruiseRefreshTicks int64 `json:"cruiseRefreshTicks"`
|
||
//
|
||
// ★ 估算位置:朝当前目标按速度推算(dead reckoning)出来的坐标。
|
||
// Cruise 车的位置客户端不上报,服务端只能这么估(回收距离判定 /
|
||
// 出生避撞判定 / 骇入距离判定都读它)。推进逻辑见 AdvanceCruiseEstimates。
|
||
// ⛑⛑ 别直接把它设成目标点 —— 目标一旦超出回收半径,车会「刚出生就被判出界回收」
|
||
// (2026-10-01 23:2x 实机:`despawn id=… (out of range) after=0m alive=0s`)。
|
||
//
|
||
CruiseX float32 `json:"cruiseX"`
|
||
CruiseZ float32 `json:"cruiseZ"`
|
||
// 当前目标点(估算位置朝它推进,到达即停住等下一次下发)。
|
||
CruiseTargetX float32 `json:"cruiseTargetX"`
|
||
CruiseTargetZ float32 `json:"cruiseTargetZ"`
|
||
// 估算位置所在的车道(= 上一个目标点所属车道),下一次规划从这里往后接。
|
||
CruiseLane int32 `json:"cruiseLane"`
|
||
//
|
||
// 客户端最近一次回报的 AI 任务状态
|
||
// (0 Pending / 1 Running / 2 Success / 3 Pause / 4 Abort / 5 Override)。
|
||
// 收到 2/3/4/5 就把 CruiseRefreshTicks 归零 ⇒ 立刻续发目标点,
|
||
// 不用干等 trafficCruiseRefreshSec。
|
||
//
|
||
CruiseStatus byte `json:"cruiseStatus"`
|
||
//
|
||
// 这台车**固定**的巡航速度倍率(出生时按 trafficCruiseSpeedJitter 摇一次,全程不变)。
|
||
// ⛑ 为什么每台车要不一样:全都同一速度 ⇒ 两台车一旦挨上就永远并排贴着开
|
||
// (相对速度 0,谁也甩不开谁)。给了差值,快车自然拉开、慢车自然落后。
|
||
//
|
||
CruiseSpeedScale float32 `json:"cruiseSpeedScale"`
|
||
}
|
||
|
||
type TrafficSessionState struct {
|
||
SyncRoot any `json:"syncRoot"`
|
||
Cts *context.CancelFunc `json:"cts"`
|
||
Loop *any `json:"loop"`
|
||
Cars []TrafficCar `json:"cars"`
|
||
LastReseedTicks int64 `json:"lastReseedTicks"`
|
||
LastTickTicks int64 `json:"lastTickTicks"`
|
||
// 车流健康度诊断([TRAFFIC-STAT])的上次输出时刻。
|
||
LastStatTicks int64 `json:"lastStatTicks"`
|
||
// 推送节拍采样:[TRAFFIC-STAT] 输出 dt 均值/峰值用,输出后清零。
|
||
TickSamples int32 `json:"tickSamples"`
|
||
TickSumMs float64 `json:"tickSumMs"`
|
||
TickMaxMs float64 `json:"tickMaxMs"`
|
||
// 上次真正下发 SyncAetherAIVehicleLaneDatas 的时刻(节流用,见 TrafficLanePushIntervalMs)。
|
||
LastLanePushMs int64 `json:"lastLanePushMs"`
|
||
//
|
||
// 上一包 LaneDatas 的时间戳(秒,Unix)。
|
||
// ★ 客户端的 TimedSpringInterpolationMethod 是「定时弹簧」:每个点带 TargetTimeStamp,
|
||
// 客户端在 currentServerTime 走到该时刻之前把 DeltaDistance 平滑排空。
|
||
// 所以时间戳必须①指向未来(+lead)②单调递增③增量稳定 —— 三条缺一条就会出现
|
||
// 「冲一下就停 / 停很久突然补距离」。
|
||
//
|
||
LastTimestampSeconds float64 `json:"lastTimestampSeconds"`
|
||
//
|
||
// 玩家最近一次的位置(XZ)。Cruise 车掉进死路口袋时要换一个「离玩家不远的新目的地」,
|
||
// 得知道玩家在哪(PickCruiseRoute 用它挑候选车道)。
|
||
//
|
||
PlayerX float32 `json:"playerX"`
|
||
PlayerZ float32 `json:"playerZ"`
|
||
//
|
||
// 最近发过的目标车道(最多 24 条)。挑新目标时避开它们 ⇒ 不会好几台车挤向同一个点
|
||
// (2026-10-01 23:2x 实机:四台车同时被分到 `target=(872,1874)`,后车顶上前车、
|
||
// 客户端 AI 倒车脱困后又停)。
|
||
//
|
||
RecentTargetLanes []int32 `json:"recentTargetLanes"`
|
||
}
|
||
|