This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/internal/gameserver/routers/game_router_traffic.go
T

407 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"`
}