package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // Server-side racing auto-start — turns "点开始挑战" into an actual race WITHOUT the manual // /api/racing/zone-probe call. // WHY THIS IS THE MISSING PIECE (skill drmk-ananta-rpc-diag §59.14–§59.17) // The private server never sends SceneSpoonNames, so the client never merged the racing // Spoon graph and the C# chain // // SyncGameGroundZoneInfo(68004609) // -> L50.Spoon.CarRaceManager.SyncCarRaceGame // -> InitClientRaceData -> InitClientData -> CarRaceData.InitCarRaceData -> CarRaceData.Init // // NRE'd on a null SpoonCarRaceData. That was fixed on the CLIENT side (the fastpatch injects // "<raidId>_RacingDriver" into EnterSceneInfo.SceneSpoonNames before the C# loader runs). // With the graph loaded the NRE is gone — but nothing PUSHES the zone, so the race never stages. // The client cannot start it either: it never sends AskStartCompetitionRacing(63221200) in // offline mode, and the challenge panel's 开始挑战 only calls gTaskManager:SetCurrentTask() // locally (GPS arming). The zone packet is server-authoritative — so the server has to drive it. // WHAT IT DOES // On AskAcceptAndSetCurrentTask for a task owned by a Racing challenge, it later pushes // SyncGameGroundZoneInfo four times: ZoneState=Display (stage the grid, with a real // DisplayCountDownEndTime so the client's countdown panel actually runs and releases vehicle // control), ZoneState=GameStart (start the lap clock with a real unix StartTime), then // ZoneState=GameOver and a null-zone Dispose once the race length elapses — so the zone can never // stay armed (and the player can never be left in a permanently undrivable car). The delays let the // client finish arming the challenge (SyncCurrentTask / SyncPlayerAllTask) before the zone lands. // 🔴 It is fire-and-forget and reply-neutral: AskAcceptAndSetCurrentTask4229938 still returns // its usual empty OK, so task-accept semantics are unchanged. The only new observable is the log // line [RACE] auto-start ... plus the zone packets. // 🔴 AI OPPONENTS are OPT-IN. The LinkAIAgentInfo / LinkAIFashionInfo contracts are now declared // (RacingZoneMethods.cs), and RacingZoneProbe.BuildRacingAiGrid populates // AIVehicleInfos + ParticipantInfos from the track's HighAI/MiddleAI/LowAI pools when // asked. It is OFF by default (RacingAiGridCount = 0 in RacingZoneProbe.cs, and the debug // /api/racing/zone-probe accepts an aiCount override) because a non-empty grid changes // what the client parses on zone sync — enable only after confirming the wire format via the debug // API, or leave it 0 and experiment per-shot there. // // // 「玩家跑完全程了」——由计数器上报 handler 调用。返回 true 表示这次真的叫醒了等待中的那局 // (false = 当时没有在跑的比赛 / 已经结过账,两种都属于正常幂等)。调用方负责打日志。 // func GameRouter_RacingAutoStart_RequestRacingSettle(ctx context.Context, taskId uint32) bool { // Router implementation for GameRouter_RacingAutoStart_RequestRacingSettle return true } // R294:记录本局最后上报的检查点下标(计数器 handler 调)。 func GameRouter_RacingAutoStart_NoteRacingCounter(ctx context.Context, taskId uint32, counterIndex int32) any { // Router implementation for GameRouter_RacingAutoStart_NoteRacingCounter var zero any; return zero } // R294:取并清除本局最后上报的检查点下标(结算时调)。返回 -1 = 一条都没上报。 func GameRouter_RacingAutoStart_TakeRacingLastCounter(ctx context.Context, taskId uint32) int32 { // Router implementation for GameRouter_RacingAutoStart_TakeRacingLastCounter return 0 } // // 玩家上车(或下车,但只取上车那一侧)—— 由 GameRouter.Vehicle 的两个 // EnterOrExitVehicle handler 在**解析成功**之后调用。返回 true = 这次真的放行了等待中的一局。 // 没有等待中的局(旗标关掉 / 手动开车 / 比赛进行中上车)⇒ 静默 false。 // func GameRouter_RacingAutoStart_SignalRacingBoarding(ctx context.Context, rpcCtx *RpcContext, vehicleId uint64, seat int32, source string) bool { // Router implementation for GameRouter_RacingAutoStart_SignalRacingBoarding return true } // R301:等玩家上车。★ R303 起**默认不启用** —— 只有 ablation 里显式写了 // boarding 才会真的等(官方实测没有这个闸,见 RacingTasks.cs 的长注释)。 func GameRouter_RacingAutoStart_WaitForRacingBoardingAsync(ctx context.Context, rpcCtx *RpcContext, taskId uint32) any { // Router implementation for GameRouter_RacingAutoStart_WaitForRacingBoardingAsync var zero any; return zero } func GameRouter_RacingAutoStart_RecordRacingCompletion(ctx context.Context, trackId uint32, taskId uint32, maxGameTime uint32, elapsedMs uint64) any { // Router implementation for GameRouter_RacingAutoStart_RecordRacingCompletion var zero any; return zero } // // The racing Spoon graph name of a raid. The client fastpatch injects exactly this string // (see __DRMK_RacingSpoonNameForRaid in client_game_switch_source.lua), and it is the // RaidSpoonName authored on the matching RacingDriverTrackInformationConfig rows // (raid 23300888 -> track 3, 5, 8, 9, 10, 13). // func GameRouter_RacingAutoStart_RacingSpoonNameForRaid(ctx context.Context, raidId uint32) string { // Router implementation for GameRouter_RacingAutoStart_RacingSpoonNameForRaid return "" } // // Which playable track belongs to . // Candidates = rows whose RaidSpoonName is "<raidId>_RacingDriver" AND MaxGamePeople > 0 // (the Id 1000+ block are map POI placeholders, not tracks — §59.16 D). // Several tracks share one Spoon graph, so a tie is broken by preferring a track that a // RacingDriverCompetitionGroupConfig group actually races (that is the authored "real" circuit // for the map; for raid 23300888 that selects track 3 / event 2040, group 1 = C级赛 新启01). // func GameRouter_RacingAutoStart_RacingTrackIdForRaid(ctx context.Context, raidId uint32) uint32 { // Router implementation for GameRouter_RacingAutoStart_RacingTrackIdForRaid return 0 } // // R334:`RaidSpoonName` 的 raid 段(去掉可选的 `spoon` 前缀)是否等于 。 // // "23300888_RacingDriver" -> 23300888 (track 3) // "23300999_Taffy_RaceGuide" -> 23300999 (track 2) // "spoon23301294_23301294_Race_Chaos_Gadgets" -> 23301294 (track 6,混厄) // // 不能只做 `StartsWith(raidId)` —— 那样 2330129**4** 会被 2330129 前缀误配。 // func GameRouter_RacingAutoStart_RacingSpoonRaidMatches(ctx context.Context, raidSpoonName string, raidId uint32) bool { // Router implementation for GameRouter_RacingAutoStart_RacingSpoonRaidMatches return true } // // ★★★★★ R334:**按任务选轨**(取代「按 raid 猜」)。返回 false = 不该开赛。 // 为什么非改不可:闸门只认任务 id、赛道却由 RacingTrackIdForRaid(Profile.RaidId) // 决定 —— 后者对 raid 23300888 **恒返 3**。于是主城**任何**一条赛车挑战被点开后: // 跑的都是 track 3 的 Downtown 路线,人也被传到 track 3 起点 // (用户原话:「王牌02 新启新岸有开始挑战的点,但是开始后启动的是我们已经做完的那条赛道」)。 // 判据链(全部读服务端自己持有的生效配表,零硬编码坐标): // // task → event:TaskEventCatalog4229938.EventsForTask(taskId) // event → track 行:RacingDriverTrackInformationConfig.EventIds 相交 // track 行必须可用:MaxPlayers > 0 ∧ RacingPathName 非空 // (几何**随客户端包发布过**的才有值;其余连一个字节都没有) // 同 raid:RacingSpoonRaidMatches // ★ RacingPlayableTrackIds:服务端已有成套门表的赛道 // // 语义(关键):任务在事件链上(events 非空)但筛不出成套赛道 ⇒ 返回 false ⇒ // 「不启动」并打一行写明原因的日志,**绝不回退去乱开一条别人的赛道**。 // 只有「任务压根不在任何事件链上」(events 为空)才回退 RacingTrackIdForRaid。 // 一键回退:旗标文件加一行 trackbytaskoff ⇒ 完全回到改前的「按 raid 猜」 // (见 RacingTasks.cs 的 RacingTrackSelectStrict)。 // func GameRouter_RacingAutoStart_TryPickRacingTrack(ctx context.Context, raidId uint32, taskId uint32, trackId uint32, reason string) bool { // Router implementation for GameRouter_RacingAutoStart_TryPickRacingTrack return true } func GameRouter_RacingAutoStart_TeleportPlayerAndVehicleToRacingStartAsync(ctx context.Context, rpcCtx *RpcContext) any { // Router implementation for GameRouter_RacingAutoStart_TeleportPlayerAndVehicleToRacingStartAsync var zero any; return zero } // // Called from the AskAcceptAndSetCurrentTask handler. No-ops unless the accepted task belongs to // a Racing challenge; otherwise schedules the Display -> GameStart zone push. // func GameRouter_RacingAutoStart_ScheduleRacingAutoStart(ctx context.Context, rpcCtx *RpcContext, taskId uint32) any { // Router implementation for GameRouter_RacingAutoStart_ScheduleRacingAutoStart var zero any; return zero } func GameRouter_RacingAutoStart_RunRacingAutoStartAsync(ctx context.Context, rpcCtx *RpcContext, taskId uint32, trackId uint32, maxGameTime uint32) any { // Router implementation for GameRouter_RacingAutoStart_RunRacingAutoStartAsync var zero any; return zero } // // Build a (FABRICATED, offline) ranking for SyncRacingResultData(68006935). Offline // single-player mode has no client→server lap times, so a real ranking is impossible — we just // put the player at rank 1 and the AI opponents at ranks 2..N with increasing (fake) total times // so the client's _BuildRaceRankData produces a sensible ordered leaderboard and triggers // the ending panel. // 🔴 AI ENTRY KEY (fixed 2026-09-23, user report "名称没看到"): // the client resolves each AI entry like this (ChallengeManager.lua:_BuildRaceRankData): // // local isAIEntry = ulong.equals(data.Pid, 0) // local key = isAIEntry and data.AIVehicle or data.Pid // local info = self.racingParticipantInfo[key] -- nil ⇒ "[...] key not match, isAI=true" // // This method used to write the SAME invented ids as the old ghost grid // (0x6000_0000_0000_0000 + n), while racingParticipantInfo is built by C# from the // participants whose ids are the REAL spawned entity ids — so every AI row missed and all // opponent names came out empty. It now takes the spawned vehicles and reuses // RacingAiVehicle.EntityId, i.e. exactly the ids that went into // AIVehicleInfos/ParticipantInfos (AgentInstanceId / VehicleInstanceId / // AIAgentInfo.Uid all carry that same value). // func GameRouter_RacingAutoStart_BuildRacingResultData(ctx context.Context, trackId uint32, taskId uint32, aiVehicles []any, elapsedMs uint64) []any { // Router implementation for GameRouter_RacingAutoStart_BuildRacingResultData return nil } // // Wire args for SyncOpenPanelCountDown(68652507) — a method-frame body (Inline, no root // 0xFF marker): panelId u32 + countDown f32 + redTenSeconds bool + isCountDown bool + // showMillisecond bool = 11 bytes. Booleans are 1 byte each, the same convention as // RacingZoneInfo4229938.GameTypeRandom. A wrong width shows up immediately as a client // deserialize failed for 68652507 and is then fixed from the log. // type SyncOpenPanelCountDownArgs struct { PanelId uint32 `json:"panelId"` CountDown float32 `json:"countDown"` RedTenSeconds bool `json:"redTenSeconds"` IsCountDown bool `json:"isCountDown"` ShowMillisecond bool `json:"showMillisecond"` }