using Ananta.SDK.Rpc; using Ananta.Server.Protocol.Client4229938; using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; namespace Ananta.Server.Handlers.Game; /// /// Wardrobe dye surface (build 4229938), ported from the Lyco donor build on 2026-09-26. /// /// ⛑⛑ The client commits every dye change locally into PlayerFashionsInfo the moment the /// invoke answers err=0 (DressData.lua reads the error branch as success — see 硬规则 18 on /// inverted decompiles, and the err==0 = OK polarity proven by the donor's Player.log). /// No S2C push is needed for the player's own unit: the handlers only have to persist the /// state, and RuntimePayloadFactory replays it on the next login seed. /// /// ⛑ The four bodies are decoded defensively. These are C2S shapes taken from the donor's /// ClientToGameDelegate.lua serializer reading; an Int7/Int32 guess that turns out wrong must /// degrade to "dye not remembered", never to a decode exception on the only path whose job is to /// answer the INVOKE (an unanswered INVOKE leaves the dye panel waiting forever). /// internal sealed partial class GameRouter { [Handler(MethodId.AskUnlockFashionColoringSlot, HandlerPacketKind.Invoke)] private async Task AskUnlockFashionColoringSlot(Connection conn, UxRpcMessage msg) { if (!msg.TryGetArgs(out var args) || args is null) { conn.Log.Warn($"[FASHION-DYE] unlock undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}"); await conn.ReturnEmptyOkAsync(msg); return; } var state = GetWorldState(msg.Context); FashionColoringStore.AddUnlockedSlots(state, args.fashionid, args.unlockslotcount); conn.Log.Info( $"[FASHION-DYE] unlock fashion={args.fashionid} +{args.unlockslotcount} " + $"slots={FashionColoringStore.GetUnlockedSlots(state, args.fashionid)}"); await conn.ReturnEmptyOkAsync(msg); PlayerProgressStore.Save(state, conn.Log, quiet: true); } [Handler(MethodId.AskSetFashionColoringSchemeInfos, HandlerPacketKind.Invoke)] private async Task AskSetFashionColoringSchemeInfos(Connection conn, UxRpcMessage msg) { if (!msg.TryGetArgs(out var args) || args is null) { conn.Log.Warn($"[FASHION-DYE] set-scheme undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}"); await conn.ReturnEmptyOkAsync(msg); return; } var state = GetWorldState(msg.Context); foreach (var entry in args.fashionColoringSchemeInfoList) { foreach (var (planId, coloring) in entry.FashionColoringSchemeInfoDict) { FashionColoringStore.SetSchemes(state, entry.FashionId, planId, coloring.ColoringType2ColorIdDict); conn.Log.Info( $"[FASHION-DYE] set scheme fashion={entry.FashionId} plan={planId} " + $"colors=[{string.Join(',', coloring.ColoringType2ColorIdDict.Select(kv => $"{kv.Key}:{kv.Value}"))}]"); } } await conn.ReturnEmptyOkAsync(msg); PlayerProgressStore.Save(state, conn.Log, quiet: true); } [Handler(MethodId.AskApplyFashionColoringSchemeInfos, HandlerPacketKind.Invoke)] private async Task AskApplyFashionColoringSchemeInfos(Connection conn, UxRpcMessage msg) { if (!msg.TryGetArgs(out var args) || args is null) { conn.Log.Warn($"[FASHION-DYE] apply undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}"); await conn.ReturnEmptyOkAsync(msg); return; } var state = GetWorldState(msg.Context); foreach (var (fashionId, planId) in args.applyfashioncoloringschemeiddict) { FashionColoringStore.SetApplied(state, fashionId, planId); conn.Log.Info($"[FASHION-DYE] apply fashion={fashionId} plan={planId}"); } await conn.ReturnEmptyOkAsync(msg); PlayerProgressStore.Save(state, conn.Log, quiet: true); } [Handler(MethodId.AskResetFashionColoringSchemeInf, HandlerPacketKind.Invoke)] private async Task AskResetFashionColoringSchemeInf(Connection conn, UxRpcMessage msg) { if (!msg.TryGetArgs(out var args) || args is null) { conn.Log.Warn($"[FASHION-DYE] reset undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}"); await conn.ReturnEmptyOkAsync(msg); return; } var state = GetWorldState(msg.Context); foreach (var entry in args.resetFashionColoringSchemeInfoList) { foreach (var (planId, reset) in entry.resetFashionColoringSchemeInfoDict) { FashionColoringStore.ResetTypes(state, entry.FashionId, reset.resetColoringTypeList); conn.Log.Info( $"[FASHION-DYE] reset fashion={entry.FashionId} plan={planId} " + $"types=[{string.Join(',', reset.resetColoringTypeList)}]"); } } await conn.ReturnEmptyOkAsync(msg); PlayerProgressStore.Save(state, conn.Log, quiet: true); } }