Files

151 lines
6.8 KiB
C#
Raw Permalink Normal View History

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;
}
}