Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.RacingVehicle.cs
T

161 lines
9.6 KiB
C#
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.
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;
/// <summary>
/// 赛车「赛事车辆 + 检查点推进」两个 C2S 回执(R217, 2026-10-01)。
///
/// WHY THIS FILE EXISTS
/// 这两个包在 R216 之前**从来没有 handler**(Handlers 里连名字都没有,只有 MethodId 常量),
/// 于是它们落到 <c>DefaultUnknownInvoke4229938</c> 的 typed-default 兜底:客户端拿到 err=0 的**假 OK**
/// 就继续跑。表面无害,实际有两个代价:
/// 1. 服务端**完全不知道**玩家选了哪条赛道 / 哪辆车,也不知道检查点走到第几个 —— 结算、AI 路线
/// 和任何防作弊校验都没有数据来源,只能重新从 raidId 反推。
/// 2. 一旦哪天客户端在回调里读回执的**真实载荷**(而不是只看 err),typed-default 的空体立刻变成
/// 「字段全 0」的伪数据,而那时故障点会跑到客户端里去,比在这里显式回一句更难查。
///
/// 所以本轮把它们补成「显式 ack + 落日志」,语义与 typed-default 完全一致(都是 void → 空 OK),
/// 但服务端从此握有真相。<b>不改变任何既有行为</b>:注册前是兜底回 0,注册后是显式回 0。
///
/// 线格式(客户端 generated writer,LuaGen/AutoGen/ClientToGameDelegate.lua,逐字节核对过):
/// <code>
/// 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(过检查点、换圈、终点)
/// </code>
///
/// ★ 这两个日志是**判读检查点是否真的在推进**的判据(R216 的结论:检查点 = taskCounterList =
/// SpoonTaskMgr 的 workActions,空图 ⇒ 永不上报)。白天实测时:
/// * 出现 <c>[RACE][COUNTER]</c> ⇒ 客户端真的在过检查点 ⇒ 任务图装载成功;
/// * 一条都没有 ⇒ 图仍为空(workActions=0),检查点这条线依旧死。
///
/// ⛑ 回滚:删掉 GameRouter.cs 的 Enabled4229938MethodIds 里对应的两行即可(回到 typed-default 兜底),
/// 本文件保留无害。
/// </summary>
internal sealed partial class GameRouter
{
/// <summary>
/// AskSetRacingCompetitionVehicle(63764162) — 玩家在挑战面板选定的赛事车辆。
///
/// 触发时机:<c>RacerManager.TrySetCompetitionVehicle</c> 走完所有早退分支之后。★ 反过来说,
/// 这条日志**出现与否本身就是「门开没开」的判据**:R216 定案该函数有两个**不打日志**的早退分支
/// (trackId 无效 / 没有赛车手职业),以及一个打日志的「未找到玩家载具」分支。现在把门修好了
/// (urbanJobOverrides 给 15020967 加了 job 4),这条日志应当开始出现。
/// </summary>
[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);
}
/// <summary>
/// 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 写下这段注释时留的那个挂点。
/// </summary>
[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);
}
}