116 lines
5.3 KiB
C#
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);
|
|
}
|
|
}
|