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_racing_auto_start.go
T

214 lines
12 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
//
// 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
// <code>
// SyncGameGroundZoneInfo(68004609)
// -> L50.Spoon.CarRaceManager.SyncCarRaceGame
// -> InitClientRaceData -> InitClientData -> CarRaceData.InitCarRaceData -> CarRaceData.Init
// </code>
// NRE'd on a null SpoonCarRaceData. That was fixed on the CLIENT side (the fastpatch injects
// "&lt;raidId&gt;_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 "&lt;raidId&gt;_RacingDriver" AND MaxGamePeople &gt; 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` 前缀)是否等于 。
// <code>
// "23300888_RacingDriver" -> 23300888 (track 3)
// "23300999_Taffy_RaceGuide" -> 23300999 (track 2)
// "spoon23301294_23301294_Race_Chaos_Gadgets" -> 23301294 (track 6,混厄)
// </code>
// 不能只做 `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 = 不该开赛。
// <para>为什么非改不可:闸门只认任务 id、赛道却由 RacingTrackIdForRaid(Profile.RaidId)
// 决定 —— 后者对 raid 23300888 **恒返 3**。于是主城**任何**一条赛车挑战被点开后:
// 跑的都是 track 3 的 Downtown 路线,人也被传到 track 3 起点
// (用户原话:「王牌02 新启新岸有开始挑战的点,但是开始后启动的是我们已经做完的那条赛道」)。</para>
// <para>判据链(全部读服务端自己持有的生效配表,零硬编码坐标):</para>
// <list type="number">
// <item>task → event:TaskEventCatalog4229938.EventsForTask(taskId)</item>
// <item>event → track 行:RacingDriverTrackInformationConfig.EventIds 相交</item>
// <item>track 行必须可用:MaxPlayers &gt; 0 ∧ RacingPathName 非空
// (几何**随客户端包发布过**的才有值;其余连一个字节都没有)</item>
// <item>同 raid:RacingSpoonRaidMatches</item>
// <item>★ RacingPlayableTrackIds:服务端已有成套门表的赛道</item>
// </list>
// <para>语义(关键):任务在事件链上(events 非空)但筛不出成套赛道 ⇒ 返回 false ⇒
// 「不启动」并打一行写明原因的日志,**绝不回退去乱开一条别人的赛道**。
// 只有「任务压根不在任何事件链上」(events 为空)才回退 RacingTrackIdForRaid。</para>
// <para>一键回退:旗标文件加一行 trackbytaskoff ⇒ 完全回到改前的「按 raid 猜」
// (见 RacingTasks.cs 的 RacingTrackSelectStrict)。</para>
//
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):
// <code>
// 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"
// </code>
// 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"`
}