using Ananta.SDK.Rpc;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
///
/// 赛车「赛事车辆 + 检查点推进」两个 C2S 回执(R217, 2026-10-01)。
///
/// WHY THIS FILE EXISTS
/// 这两个包在 R216 之前**从来没有 handler**(Handlers 里连名字都没有,只有 MethodId 常量),
/// 于是它们落到 DefaultUnknownInvoke4229938 的 typed-default 兜底:客户端拿到 err=0 的**假 OK**
/// 就继续跑。表面无害,实际有两个代价:
/// 1. 服务端**完全不知道**玩家选了哪条赛道 / 哪辆车,也不知道检查点走到第几个 —— 结算、AI 路线
/// 和任何防作弊校验都没有数据来源,只能重新从 raidId 反推。
/// 2. 一旦哪天客户端在回调里读回执的**真实载荷**(而不是只看 err),typed-default 的空体立刻变成
/// 「字段全 0」的伪数据,而那时故障点会跑到客户端里去,比在这里显式回一句更难查。
///
/// 所以本轮把它们补成「显式 ack + 落日志」,语义与 typed-default 完全一致(都是 void → 空 OK),
/// 但服务端从此握有真相。不改变任何既有行为:注册前是兜底回 0,注册后是显式回 0。
///
/// 线格式(客户端 generated writer,LuaGen/AutoGen/ClientToGameDelegate.lua,逐字节核对过):
///
/// AskSetRacingCompetitionVehicle 63764162
/// SerializerHelper.AskSetRacingCompetitionVehicle_Serializer(writer, trackid, vehicleid)
/// WriteUInt32(trackid) + WriteUInt32(vehicleid) = 8 B
/// 客户端调用点:RacerManager.lua:208 gClientToGameDelegate:AskSetRacingCompetitionVehicle(trackId, vehicleId)
///
/// AskSetTaskCounterValue 63753212
/// SerializerHelper.AskSetTaskCounterValue_Serializer(writer, taskid, counterindex, value)
/// WriteUInt32(taskid) + WriteInt32(counterindex) + WriteInt32(value) = 12 B
/// 客户端调用点:CarRaceManager.lua:204 / :247 / :431(过检查点、换圈、终点)
///
///
/// ★ 这两个日志是**判读检查点是否真的在推进**的判据(R216 的结论:检查点 = taskCounterList =
/// SpoonTaskMgr 的 workActions,空图 ⇒ 永不上报)。白天实测时:
/// * 出现 [RACE][COUNTER] ⇒ 客户端真的在过检查点 ⇒ 任务图装载成功;
/// * 一条都没有 ⇒ 图仍为空(workActions=0),检查点这条线依旧死。
///
/// ⛑ 回滚:删掉 GameRouter.cs 的 Enabled4229938MethodIds 里对应的两行即可(回到 typed-default 兜底),
/// 本文件保留无害。
///
internal sealed partial class GameRouter
{
///
/// AskSetRacingCompetitionVehicle(63764162) — 玩家在挑战面板选定的赛事车辆。
///
/// 触发时机:RacerManager.TrySetCompetitionVehicle 走完所有早退分支之后。★ 反过来说,
/// 这条日志**出现与否本身就是「门开没开」的判据**:R216 定案该函数有两个**不打日志**的早退分支
/// (trackId 无效 / 没有赛车手职业),以及一个打日志的「未找到玩家载具」分支。现在把门修好了
/// (urbanJobOverrides 给 15020967 加了 job 4),这条日志应当开始出现。
///
[Handler(MethodId.AskSetRacingCompetitionVehicle, HandlerPacketKind.Invoke)]
private async Task AskSetRacingCompetitionVehicleAccept(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var trackId = b.Length >= 4 ? ReadU32(b, 0) : 0u;
var vehicleId = b.Length >= 8 ? ReadU32(b, 4) : 0u;
conn.Log.Info(
$"[RACE][VEHICLE] AskSetRacingCompetitionVehicle track={trackId} vehicle={vehicleId} " +
$"bytes={b.Length} (>=1 条即说明客户端已越过职业门)");
// 2026-10-01 R217: 协议里该 RPC 的回执是 void(客户端 Callback 只读 err),空 OK 即完整语义。
await conn.ReturnEmptyOkAsync(msg);
}
///
/// AskSetTaskCounterValue(63753212) — 检查点计数器上报。
///
/// counterIndex 在客户端是**下标**语义(CarRaceManager.lua 传的是 rankPointIndex-1 之类),
/// value 是 0/1(0=离开,1=到达)。这里原样记录,不做校验 —— 检查点状态由客户端自己
/// 维护 taskCounterList,服务端目前没有可与之对齐的权威表(任务图的 workActions 才是,
/// 而它取决于 Spoon 图是否装载成功)。
///
/// ★ R234(2026-10-01):本 handler 不再只是「记录」。`index == RacingClientCheckpointCount`
/// 的那一条 = 客户端**跑完全程**的完成上报(见方法内注释),它会立刻叫醒
/// `RunRacingAutoStartAsync` 里等待的那局 ⇒ 推 `GAMEOVER` + `SyncRacingResultData`。
/// 这就是「服务端接进结算逻辑」——R217 写下这段注释时留的那个挂点。
///
[Handler(MethodId.AskSetTaskCounterValue, HandlerPacketKind.Invoke)]
private async Task AskSetTaskCounterValueAccept(Connection conn, UxRpcMessage msg)
{
var b = msg.Body;
var taskId = b.Length >= 4 ? ReadU32(b, 0) : 0u;
var counterIndex = b.Length >= 8 ? unchecked((int)ReadU32(b, 4)) : 0;
var value = b.Length >= 12 ? unchecked((int)ReadU32(b, 8)) : 0;
conn.Log.Info(
$"[RACE][COUNTER] AskSetTaskCounterValue task={taskId} index={counterIndex} " +
$"value={value} bytes={b.Length}");
// ★ R294:记住本局最后上报到的下标 —— 结算日志会带出来,用来区分
// 「终点判据坏了」和「300s 到点没跑完」。见 TakeRacingLastCounter 的说明。
if (value != 0)
NoteRacingCounter(taskId, counterIndex);
// ★★★ R234:「跑完全程」的唯一权威信号 ⇒ 立刻结算(不再干等 MaxGameTime=300s)。
//
// 客户端证据(`CarRaceManager.lua:201-204`,19:03/19:05 两轮实测均在日志中出现):
// 通过**最后一个检查点**时,`OnCurrentTaskChanged` 走完成分支并上报
// `AskSetTaskCounterValue(taskID, self.taskCounterCount, 1)`
// —— 即 `index == taskCounterCount`(= 客户端的检查点总数,见
// `RacingClientCheckpointCount`)。**这是整条链里唯一一处用 `taskCounterCount`
// 当 index 上报的地方**(另一处 `:431` 发 `nextRankPointIndex - 1` = 普通检查点
// 的 0..N-1),所以两者不会混淆。
//
// 同时它也是客户端**关掉赛车 HUD 面板**的那一刻
// (完成分支里的 `gPanelManager:Close(gPanelId.S_RACING_PANEL)`)——
// 在旧实现下,玩家看到的是「面板自己消失了,然后什么都没有」,
// 这正是用户报的「没有办法到终点结束」。
if (value != 0 && counterIndex == RacingClientCheckpointCount)
{
var wokeLiveRace = RequestRacingSettle(taskId);
conn.Log.Info(
$"[RACE][COUNTER] ** FINISH ** task={taskId} index={counterIndex}"
+ $" == RacingClientCheckpointCount({RacingClientCheckpointCount})"
+ $" -> settle signal {(wokeLiveRace ? "FIRED (GAMEOVER + SyncRacingResultData 立即推)" : "FIRED-NOWHERE (没有在跑的比赛:已结过账 / 不是本进程启动的)")}");
}
// ★★★★★ R294(2026-10-02)—— **过点即撤门**。用户原话:「检查点开过去不会消失」。
//
// 我们一局 spawn 52 个门(26 点 × A/B 两半)后**从不回收**;而官方的检查点标记
// 只有一个、跟着当前目标走(`CarRaceManager.arrowGO`:`AddArrow` 建、`RemoveArrow` 销毁)
// ⇒ 玩家看到的「过点即消失」在官方那边是**客户端自建物**的行为,我们这份是服务端 gadget,
// 不主动撤就永远在。撤哪几个见 `GameRouter.RacingGates` 的 R294 段:
//
// **上报的 index 恰好等于我们自己的检查点下标** —— 客户端 `taskCounterList` 就是我们 Lua
// 喂的 `R255_OFFICIAL_CP`(`racing_guard.lua:1077-1080, 1211`),过 `list[k+1]`(= 我们的
// cp k)时 `UpdateEntityRankInfoById` 发 `nextRankPointIndex - 1 = k`。
// 跑完全程时走 `OnCurrentTaskChanged` 的完成分支,发 `taskCounterCount`
// (= 26 = `RacingClientCheckpointCount`)⇒ 那一条触发**全清**。
//
// 闸门:`racingGates.removeOnPass`(默认 true)。关掉 = 回到 R293 的「一局只摆不清」。
if (value != 0 && counterIndex >= 0
&& PrivateServerConfigStore.Current.World.Content.RacingGates.RemoveOnPass)
{
try
{
var st = GetStateStatic(conn.Session);
if (counterIndex >= RacingClientCheckpointCount)
{
await RemoveAllRacingGatesAsync(conn.Session, st);
}
else
{
await RemoveCheckpointGatesAsync(conn.Session, st, counterIndex);
}
}
catch (Exception ex)
{
// 撤门失败绝不能拖垮计数器回执 —— 门留在原地只是观感问题。
conn.Log.Warn($"[RACEGATE] remove-on-pass failed cp={counterIndex}: {ex.Message}");
}
}
await conn.ReturnEmptyOkAsync(msg);
}
}