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