161 lines
9.6 KiB
C#
161 lines
9.6 KiB
C#
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);
|
||
}
|
||
}
|