Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.FashionColoring.cs
T

116 lines
5.3 KiB
C#

using Ananta.SDK.Rpc;
using Ananta.Server.Protocol.Client4229938;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// Wardrobe dye surface (build 4229938), ported from the Lyco donor build on 2026-09-26.
///
/// ⛑⛑ The client commits every dye change locally into <c>PlayerFashionsInfo</c> the moment the
/// invoke answers err=0 (<c>DressData.lua</c> reads the error branch as success — see 硬规则 18 on
/// inverted decompiles, and the err==0 = OK polarity proven by the donor's Player.log).
/// <b>No S2C push is needed for the player's own unit</b>: the handlers only have to persist the
/// state, and <c>RuntimePayloadFactory</c> replays it on the next login seed.
///
/// ⛑ The four bodies are decoded defensively. These are C2S shapes taken from the donor's
/// <c>ClientToGameDelegate.lua</c> 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).
/// </summary>
internal sealed partial class GameRouter
{
[Handler(MethodId.AskUnlockFashionColoringSlot, HandlerPacketKind.Invoke)]
private async Task AskUnlockFashionColoringSlot(Connection conn, UxRpcMessage msg)
{
if (!msg.TryGetArgs<GameMethods.AskUnlockFashionColoringSlotArgs>(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<GameMethods.AskSetFashionColoringSchemeInfosArgs>(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<GameMethods.AskApplyFashionColoringSchemeInfosArgs>(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<GameMethods.AskResetFashionColoringSchemeInfArgs>(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);
}
}