96 lines
4.6 KiB
C#
96 lines
4.6 KiB
C#
using Ananta.SDK.Network;
|
||
using Ananta.SDK.Rpc;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 伪造匹配 —— 让客户端点「开始」就能进搜打撤图。
|
||
///
|
||
/// <b>为什么需要这一层</b>:正式服的开局要过真实匹配服 / 队伍服,我们手上只有游戏服,
|
||
/// 补不出那条链路;而客户端点完开始就在等切图,没人应答它就卡在开始界面。
|
||
/// 捐赠端 handoff 的做法就是「把这四条请求劫到 RaidType 19 上」,本文件是同一思路的实现。
|
||
///
|
||
/// <b>四条通用开局请求</b>(<c>ClientToGameDelegate.lua</c> 里都能找到):
|
||
/// <c>AskStartSingleMatch</c> 63347492 / <c>StartBotMatch</c> 63474603 /
|
||
/// <c>StartMatchInTeam</c> 63563208 / <c>StartGameInTeam</c> 63121362。
|
||
///
|
||
/// ⛑⛑ <b>这四条是通用请求,不是搜打撤专用</b>:别的玩法开局也走它们。
|
||
/// 所以默认<b>关</b>(<c>world.content.extraction.fakeMatch=false</c>),
|
||
/// 只在面板勾上「伪造匹配」时生效,且改动只活在内存里 —— 重启一定回落到配置值。
|
||
/// 开着的时候任何一次点开始都会被劫到 <c>PreferredRaid</c>。
|
||
///
|
||
/// ⛑ <b>没有 Request 契约</b>:这四条的参数在 dump 里是空壳,也没有可用的 lua writer 对照,
|
||
/// 所以一律裸接、只打 body 长度。把 midToReturnMessageReader 里没有 reader 的请求当成
|
||
/// 「可以随便回」是对的,但反过来<u>不要</u>因为这里没报错就以为参数结构对了 —— 我们一根字段都没读。
|
||
///
|
||
/// ⛑ <c>[Handler]</c> 写完必须把 methodId 加进 <c>GameRouter.cs</c> 的
|
||
/// <c>Enabled4229938MethodIds</c>,否则是空注册 —— 请求不报错,只拿 typed-default void。
|
||
/// </summary>
|
||
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");
|
||
|
||
/// <summary>
|
||
/// 开局请求的公共收尾:先应答,再决定要不要把这次开局劫到搜打撤图。
|
||
///
|
||
/// ⛑ <b>应答必须无条件发</b>:这四条是 await 型,不回会让客户端那条调用永久挂住,
|
||
/// 表现是「点开始没反应」,而且日志里什么都没有。代价为零 —— handoff §6.3 的请求表里
|
||
/// 没有这四条,说明 <c>midToReturnMessageReader</c> 里没有 reader,
|
||
/// 空 OK 不会像 <c>SearchContainer</c> 那样用 nil 覆盖客户端已有的好数据。
|
||
/// </summary>
|
||
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}");
|
||
}
|
||
}
|