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

406 lines
22 KiB
Go
Raw Normal View History

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