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; /// /// 挑战(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<uint> ids -> List7Bit(Complex(Reader[594])) /// SetNewChallengeData(63990449) uint, List<bool>, float -> void /// 请求形状取自 `ClientToGameDelegate.lua` 的 SerializerHelper。 /// 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(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] 之后) /// 把存档快照拼成 `Auto.NewChallengeRecord`(lua Reader[594])。 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, }; } /// /// `uint challengeId + List<bool>(Int7 count,每元素 1 字节)+ float score`。 /// 宽容解码:布局意外也绝不许中断请求 —— 这条流程仍然需要它的 ack /// (猜不准的 wire 形状,优先把包答回去)。 /// private static (uint ChallengeId, List Nodes, float Score) ReadChallengeData(byte[] body) { var nodes = new List(); 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); } } /// `List<uint>`(Int7 count + 每元素 4 字节)。 private static List ReadU32List(byte[] body) { var ids = new List(); 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; } }