214 lines
12 KiB
Go
214 lines
12 KiB
Go
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
|
||
// "<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` 前缀)是否等于 。
|
||
// <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 > 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"`
|
||
}
|
||
|