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