using Ananta.SDK.Rpc; using Ananta.Server.ClientData.Client4229938; using Ananta.Server.Handlers; using Ananta.Server.Protocol.Client4229938; using Auto = Ananta.Server.RpcTypes.Client4229938.Auto; using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; namespace Ananta.Server.Handlers.Game; /// /// Phone menu skin parts (手机壁纸 / 装饰 / 挂件). /// /// The client's "apply" button only sends AskSetMobileSkinPart and then waits for the answer. /// It never writes the worn suit locally: GameToClientImpl.SyncSpiritMobileSkinPartInfo is the /// sole writer of SpiritInfo.MobileSkinInfo, and it is also what fires /// ON_SYNC_SPIRIT_SKIN_PART_INFO_CHANGE so MainPhonePanelStore.OnSpiritSkinPartChange re-reads the /// value. Because this method previously fell through to the typed-neutral fallback, the whole /// sequence ended with "applied successfully" and an unchanged wallpaper: the preview closed, the /// panel re-read the untouched login struct, and the wallpaper snapped back. /// /// This handler stores the choice per character (and mirrors it to PlayerProgressStore) and replays /// it as SyncSpiritMobileSkinPartInfo so the wallpaper actually changes — and stays changed across /// re-logins, because the login payload now carries the stored suit. /// /// The reply stays an empty 0-error body, byte-identical to what the fallback produced, so the /// client's existing "close preview / show result toast" branch is untouched. /// internal sealed partial class GameRouter { [Handler(MethodId.AskSetMobileSkinPart, HandlerPacketKind.Invoke)] private async Task AskSetMobileSkinPart4229938(Connection conn, UxRpcMessage msg) { if (!msg.TryGetArgs(out var args) || args is null) { conn.Log.Warn($"[PHONE-SKIN] AskSetMobileSkinPart undecodable bytes={msg.Body.Length}"); await conn.ReturnEmptyOkAsync(msg); return; } await ApplyPhoneSkin4229938Async( conn, msg, args.wallpaper, args.decoration, args.pendant, "set"); } [Handler(MethodId.AskResetMobileSkinPart, HandlerPacketKind.Invoke)] private Task AskResetMobileSkinPart4229938(Connection conn, UxRpcMessage msg) => ApplyPhoneSkin4229938Async( conn, msg, NpcCultivationCatalog4229938.DefaultWallpaperPart, NpcCultivationCatalog4229938.DefaultDecorationPart, NpcCultivationCatalog4229938.DefaultPendantPart, "reset"); /// /// Stores the worn suit for the character currently in play and echoes it back. /// /// The spirit id must be a FightSpiritConfig template id, not a unit instance id: the client /// looks the character up through SpiritManager.spiritViewDatas, which SyncPlayerInfoSpirit / /// SyncPlayerAllSpirits key by SpiritInfo.TemplateId, and GetCurrentSpiritSkinInfo resolves the /// same way through gBattleSpiritMgr.currentSpiritTemplateId (= InfoSpirit.ActiveSpirit). /// private async Task ApplyPhoneSkin4229938Async( Connection conn, UxRpcMessage msg, uint wallpaper, uint decoration, uint pendant, string reason) { var state = GetWorldState(msg.Context); var spiritId = state.ActiveSpiritTemplateId; lock (state.SyncRoot) { state.PhoneSkinBySpirit[spiritId] = new Auto.SpiritMobileSkinInfo { Wallpaper = wallpaper, Decoration = decoration, Pendant = pendant, }; } // Re-state the full part list so the packet is self-contained. It must NOT be empty: an // empty Lua list is truthy and would wipe the phone's skin list. When the catalog is // unavailable (no config folder) we pass null instead, which the client reads as // "leave my list alone". var parts = NpcCultivationCatalog4229938.SkinParts; List? available = parts.Count > 0 ? parts.Select(part => part.Id).ToList() : null; await conn.NotifyAsync(MethodId.SyncSpiritMobileSkinPartInfo, new GameMethods.SyncSpiritMobileSkinPartInfo { spiritId = spiritId, mobileSkinInfo = new Auto.SpiritMobileSkinInfo { Wallpaper = wallpaper, Decoration = decoration, Pendant = pendant, }, availableSkinParts = available, }); PlayerProgressStore.Save(state, conn.Log); conn.Log.Info( $"[PHONE-SKIN] {reason} spirit={spiritId} wallpaper={wallpaper} " + $"decoration={decoration} pendant={pendant} parts={available?.Count ?? -1}"); await conn.ReturnEmptyOkAsync(msg); } }