using Ananta.SDK.Network; using Ananta.SDK.Rpc; using Ananta.Server.Protocol.Client4229938; namespace Ananta.Server.Handlers.Game; /// /// 伪造匹配 —— 让客户端点「开始」就能进搜打撤图。 /// /// 为什么需要这一层:正式服的开局要过真实匹配服 / 队伍服,我们手上只有游戏服, /// 补不出那条链路;而客户端点完开始就在等切图,没人应答它就卡在开始界面。 /// 捐赠端 handoff 的做法就是「把这四条请求劫到 RaidType 19 上」,本文件是同一思路的实现。 /// /// 四条通用开局请求(ClientToGameDelegate.lua 里都能找到): /// AskStartSingleMatch 63347492 / StartBotMatch 63474603 / /// StartMatchInTeam 63563208 / StartGameInTeam 63121362。 /// /// ⛑⛑ 这四条是通用请求,不是搜打撤专用:别的玩法开局也走它们。 /// 所以默认关(world.content.extraction.fakeMatch=false), /// 只在面板勾上「伪造匹配」时生效,且改动只活在内存里 —— 重启一定回落到配置值。 /// 开着的时候任何一次点开始都会被劫到 PreferredRaid。 /// /// ⛑ 没有 Request 契约:这四条的参数在 dump 里是空壳,也没有可用的 lua writer 对照, /// 所以一律裸接、只打 body 长度。把 midToReturnMessageReader 里没有 reader 的请求当成 /// 「可以随便回」是对的,但反过来不要因为这里没报错就以为参数结构对了 —— 我们一根字段都没读。 /// /// ⛑ [Handler] 写完必须把 methodId 加进 GameRouter.cs 的 /// Enabled4229938MethodIds,否则是空注册 —— 请求不报错,只拿 typed-default void。 /// internal sealed partial class GameRouter { [Handler(MethodId.AskStartSingleMatch, HandlerPacketKind.Both)] private Task OnAskStartSingleMatch(Connection conn, UxRpcMessage msg) => HijackMatchToExtractionAsync(conn, msg, "AskStartSingleMatch"); [Handler(MethodId.StartBotMatch, HandlerPacketKind.Both)] private Task OnStartBotMatch(Connection conn, UxRpcMessage msg) => HijackMatchToExtractionAsync(conn, msg, "StartBotMatch"); [Handler(MethodId.StartMatchInTeam, HandlerPacketKind.Both)] private Task OnStartMatchInTeam(Connection conn, UxRpcMessage msg) => HijackMatchToExtractionAsync(conn, msg, "StartMatchInTeam"); [Handler(MethodId.StartGameInTeam, HandlerPacketKind.Both)] private Task OnStartGameInTeam(Connection conn, UxRpcMessage msg) => HijackMatchToExtractionAsync(conn, msg, "StartGameInTeam"); /// /// 开局请求的公共收尾:先应答,再决定要不要把这次开局劫到搜打撤图。 /// /// ⛑ 应答必须无条件发:这四条是 await 型,不回会让客户端那条调用永久挂住, /// 表现是「点开始没反应」,而且日志里什么都没有。代价为零 —— handoff §6.3 的请求表里 /// 没有这四条,说明 midToReturnMessageReader 里没有 reader, /// 空 OK 不会像 SearchContainer 那样用 nil 覆盖客户端已有的好数据。 /// private async Task HijackMatchToExtractionAsync(Connection conn, UxRpcMessage msg, string label) { if (msg.IsInvoke) await conn.ReturnEmptyOkAsync(msg); var state = GetStateIfExists(msg.Context.Session); if (state is null) return; var raidId = ExtractionPreferredRaid; if (!ExtractionFakeMatch) { conn.Log.Info($"{ExtractionTag} {label} {msg.Body.Length}b ack-only (fake match off)"); return; } if (!IsExtractionRaid(raidId)) { conn.Log.Warn($"{ExtractionTag} {label} dropped: preferred raid {raidId} " + "不在搜打撤名单里(world.content.extraction.enabled / raidIds / preferredRaid)"); return; } // 已经在图里了不再重复切——客户端切图期间会把这四条重发几次。 uint activeRaid; lock (state.SyncRoot) activeRaid = state.ActiveRaidId; if (activeRaid == raidId) { conn.Log.Info($"{ExtractionTag} {label} ignored: already in raid {raidId}"); return; } var (ok, message) = await EnterExtractionRaidAsync(msg.Context.Session, state, raidId); conn.Log.Info($"{ExtractionTag} {label} hijack -> raid {raidId}: {message}"); if (!ok) conn.Log.Warn($"{ExtractionTag} {label} hijack failed: {message}"); } }