Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.ChallengeRecords.cs

152 lines
6.8 KiB
C#
Raw Permalink 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 System.Buffers.Binary;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Protocol.Client4229938;
using Auto = Ananta.Server.RpcTypes.Client4229938.Auto;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// 挑战(Challenge / NewChallenge)记录 —— 桌面清单 **T0-2** 的运行时那一半。
///
/// 背景:客户端 2026-10-02 11:58:17Z 实机跑过一整轮挑战流程(`logs/unknown-methods.log`):
/// 11:58:17.276 AskNewChallengeRecordList(63830456) 74B = List7 18 个 id(1025..1042)
/// 11:58:17.291 StartNewChallenge(63092905) 4B = challengeId 1025
/// 11:58:26.091 SetNewChallengeData(63990449) 12B = 1025 + [false,false] + score 1.0
/// 11:58:26.106 FinishNewChallenge(63261145) 8B = 1025 + taskId
/// 四个调用当时**全部落在 typed-default 兜底** ⇒ 服务端一条成绩都没记下,玩家看到的永远是空面板。
///
/// 这里只接**记录读写**这一半(`SetNewChallengeData` / `AskNewChallengeRecord` / `AskNewChallengeRecordList`),
/// 理由:`StartNewChallenge` 与 `SetNewChallengeData` 的回包是 **void**,兜底给的就是空包,
/// 字节上与我方回包**完全等价**(接了只多一行日志);而 `FinishNewChallenge` 的回包是
/// `Complex(Reader[607])` = `ChallengeRecord(594) + u32 + RewardInfo(308)`,其中 `RewardInfo`
/// 还嵌套 `ExtraInfo(1428)` / `Reward(500)` ⇒ 要接就得新造 4 个契约类,
/// **而兜底那 88 字节(`DefaultReturnCatalog4229938.cs:384`)本来就是对的**,
/// 接了没有增益、只有错位风险。⇒ 那两条继续留兜底,注释在此备案。
///
/// 🔴 `UX.Game.ChallengeResult` 在 `dump.cs:1910662` 是**空类**,但 lua `Auto.Reader[607]` 有 3 个字段
/// —— 这正是硬规则 209(契约以 lua `Auto.Reader[n]` 为准,dump.cs 会丢字段)。
/// 第四个捐赠端(`_rollback_20260925_donor4/Challenge/ChallengeMethods.cs`)就信了 dump、
/// 把 `ChallengeResult` 写成空类 ⇒ `FinishNewChallenge` 回包会少 87 字节。**别照抄那个文件。**
///
/// 🔴 硬规则 94:下面每个 methodId 都必须同时登记进 `GameRouter.cs` 的 `Enabled4229938MethodIds`。
/// 🔴 硬规则 168:`[Handler]` 必须紧贴方法,中间不能插 helper —— helper 一律放在文件末尾。
///
/// 返回契约逐字段核过(lua `RPCDeserializeAuto.lua`):
/// AskNewChallengeRecord(63126760) uint challengeId -> Complex(Reader[594] NewChallengeRecord)
/// AskNewChallengeRecordList(63830456) List&lt;uint&gt; ids -> List7Bit(Complex(Reader[594]))
/// SetNewChallengeData(63990449) uint, List&lt;bool&gt;, float -> void
/// 请求形状取自 `ClientToGameDelegate.lua` 的 SerializerHelper。
/// </summary>
internal sealed partial class GameRouter
{
[Handler(MethodId.SetNewChallengeData, HandlerPacketKind.Invoke)]
private Task SetNewChallengeData(Connection conn, UxRpcMessage msg)
{
var (challengeId, nodes, score) = ReadChallengeData(msg.Body);
ChallengeRecordStore4229938.Apply(challengeId, nodes, score);
var hit = 0;
foreach (var n in nodes)
if (n)
hit++;
conn.Log.Info($"[CHALLENGE] set-data challengeId={challengeId} nodes={nodes.Count} " +
$"hit={hit} score={score:0.###}");
return conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.AskNewChallengeRecord, HandlerPacketKind.Invoke)]
private Task AskNewChallengeRecord(Connection conn, UxRpcMessage msg)
{
var challengeId = msg.Body.Length >= 4
? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body)
: 0u;
var record = BuildNewChallengeRecord(challengeId);
conn.Log.Info($"[CHALLENGE] ask-record challengeId={challengeId} " +
$"highest={record.HighestLevel} best={record.BestScore:0.###}");
return conn.ReturnAsync(msg, record);
}
[Handler(MethodId.AskNewChallengeRecordList, HandlerPacketKind.Invoke)]
private Task AskNewChallengeRecordList(Connection conn, UxRpcMessage msg)
{
var ids = ReadU32List(msg.Body);
var records = new List<Auto.NewChallengeRecord>(ids.Count);
foreach (var id in ids)
records.Add(BuildNewChallengeRecord(id));
conn.Log.Info($"[CHALLENGE] ask-record-list count={ids.Count} " +
$"ids=[{string.Join(",", ids)}]");
return conn.ReturnAsync(msg, records);
}
// ------------------------------------------------------------------ helper(必须在所有 [Handler] 之后)
/// <summary>把存档快照拼成 `Auto.NewChallengeRecord`(lua Reader[594])。</summary>
private static Auto.NewChallengeRecord BuildNewChallengeRecord(uint challengeId)
{
var (highest, received, isNew, best, param) = ChallengeRecordStore4229938.Snapshot(challengeId);
return new Auto.NewChallengeRecord
{
ChallengeId = challengeId,
HighestLevel = highest,
ReceivedRewardLevel = received,
CurrentIsNewRewardLevel = isNew,
BestScore = best,
ParamData = param,
};
}
/// <summary>
/// `uint challengeId + List&lt;bool&gt;(Int7 count,每元素 1 字节)+ float score`。
/// 宽容解码:布局意外也绝不许中断请求 —— 这条流程仍然需要它的 ack
/// (猜不准的 wire 形状,优先把包答回去)。
/// </summary>
private static (uint ChallengeId, List<bool> Nodes, float Score) ReadChallengeData(byte[] body)
{
var nodes = new List<bool>();
if (body.Length < 4)
return (0u, nodes, 0f);
try
{
var reader = new UxReader(body);
var challengeId = reader.U32();
var count = reader.Int7();
for (var i = 0; i < count && reader.Remaining > 4; i++)
nodes.Add(reader.Bool());
var score = reader.Remaining >= 4 ? reader.F32() : 0f;
return (challengeId, nodes, score);
}
catch (Exception)
{
return (body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(body) : 0u, nodes, 0f);
}
}
/// <summary>`List&lt;uint&gt;`(Int7 count + 每元素 4 字节)。</summary>
private static List<uint> ReadU32List(byte[] body)
{
var ids = new List<uint>();
if (body.Length == 0)
return ids;
try
{
var reader = new UxReader(body);
var count = reader.Int7();
for (var i = 0; i < count && reader.Remaining >= 4; i++)
ids.Add(reader.U32());
}
catch (Exception)
{
ids.Clear();
}
return ids;
}
}