This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.Fashion.cs
T

147 lines
7.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Ananta.SDK.Rpc;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Handlers;
using Ananta.Server.Protocol.Client4229938;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// Fashion/wardrobe handlers. The client's appearance panel sends
/// AskSetSpiritFashionsWithSource when the player confirms a skin. Without a
/// server-side handler the request is dropped and the model reverts to the
/// login default when the panel closes. We accept the choice and echo it back
/// via SyncSetSpiritFashions so the worn skin persists.
/// </summary>
internal sealed partial class GameRouter
{
// ★ 2026-10-02:补上**老版衣柜入口** AskSetSpiritFashions(63347169)(自 7k-v0.6
// GameRouter.Fashion.cs 移植)。它与带 source 的新入口差 2 字节:
// 老包 = [u32 spiritId][wear object],新包 = [u32 spiritId][short source][wear object]。
// 做法是在偏移 4 处补一个 short 8(FashionWearSource.Spirit,小端 = 08 00),
// 然后**走完全同一套**穿戴逻辑 —— 不另起一条分支,避免两套行为漂移。
// ⛑ 已核对主线契约 AskSetSpiritFashionsWithSource.source 就是 short(FashionMethods.cs:11)
// ⇒ 补 2 字节与捐赠端的字节拼接逐字节一致。
[Handler(MethodId.AskSetSpiritFashionsWithSource, HandlerPacketKind.Invoke)]
[Handler(MethodId.AskSetSpiritFashions, HandlerPacketKind.Invoke)]
private async Task AskSetSpiritFashionsWithSource(Connection conn, UxRpcMessage msg)
{
var body = msg.Body;
if (msg.MethodId == MethodId.AskSetSpiritFashions)
{
if (body.Length is < 4 or > 4094)
{
conn.Log.Warn($"[FASHION] legacy wear rejected bytes={body.Length}");
await conn.ReturnEmptyOkAsync(msg);
return;
}
var withSource = new byte[body.Length + 2];
body.AsSpan(0, 4).CopyTo(withSource);
withSource[4] = 8; // FashionWearSource.Spirit(short 小端:08 00)
body.AsSpan(4).CopyTo(withSource.AsSpan(6));
body = withSource;
}
var args = TryDecode4229938<GameMethods.AskSetSpiritFashionsWithSource>(body);
if (args is null)
{
conn.Log.Warn($"[FASHION] AskSetSpiritFashionsWithSource undecodable bytes={msg.Body.Length}"
+ (msg.MethodId == MethodId.AskSetSpiritFashions ? " (legacy entry, spliced)" : ""));
await conn.ReturnEmptyOkAsync(msg);
return;
}
var spiritId = args.spiritOrInstanceId;
var wearCount = args.spiritWearFashionsInfo?.WearFashionInfoList?.Count ?? 0;
var fashionIds = wearCount > 0
? string.Join(",", args.spiritWearFashionsInfo!.WearFashionInfoList.Select(x => x.FashionId))
: "(none)";
conn.Log.Info($"[FASHION] wear spirit={spiritId} source={args.source} count={wearCount} ids=[{fashionIds}]");
// Answer before doing the work. This is an INVOKE, so the wardrobe panel sits blocked until
// the response arrives — and the work below includes a disk write plus a notify. Replying
// first keeps a slow save from freezing the UI. Nothing after this point can report failure
// to the client, so the whole tail is wrapped and logged instead.
await conn.ReturnEmptyOkAsync(msg);
try
{
await ApplyWearAsync(conn, msg, spiritId, args);
}
catch (Exception ex)
{
conn.Log.Warn($"[FASHION] wear apply failed spirit={spiritId}: {ex.Message}");
}
}
private async Task ApplyWearAsync(
Connection conn, UxRpcMessage msg, uint spiritId, GameMethods.AskSetSpiritFashionsWithSource args)
{
// Remember the outfit so a re-login publishes it instead of the authored default. The
// wardrobe panel never edits the worn value locally — SyncSetSpiritFashions is its only
// writer — so without this the character silently undresses on every restart.
var state = GetWorldState(msg.Context);
var wear = args.spiritWearFashionsInfo ?? new GameMethods.SpiritWearFashionsInfo();
var wornIds = (wear.WearFashionInfoList ?? [])
.Where(x => x is not null && x.FashionId != 0)
.Select(x => x.FashionId)
.ToList();
// The id is the character template id (FightSpiritConfig), the same key SyncSetSpiritFashions
// publishes with. Anything the roster does not know is refused so a bad id cannot poison the
// snapshot and leave a character permanently naked.
var knownSpirit = ClientConfigRepository.Characters().Any(x => x.TemplateId == spiritId);
if (knownSpirit && wornIds.Count > 0)
{
var source = wear.WearSourceInfo;
lock (state.SyncRoot)
{
state.FashionBySpirit[spiritId] = new FashionRecord4229938(
wear.FunctionSuitId,
source.Source,
source.SourceId,
wear.IsTryWear,
wear.HiddenParts,
wornIds);
}
PlayerProgressStore.Save(state, conn.Log);
}
// Echo the client's chosen wear back as authoritative SyncSetSpiritFashions.
// This keeps the character model dressed in the selected skin after the
// appearance panel closes, instead of snapping back to login defaults.
var sync = new GameMethods.SyncSetSpiritFashions
{
spiritId = spiritId,
source = args.source,
spiritWearFashionsInfo = args.spiritWearFashionsInfo ?? new GameMethods.SpiritWearFashionsInfo(),
};
await conn.NotifyAsync(MethodId.SyncSetSpiritFashions, sync);
}
/// <summary>
/// Wardrobe bookkeeping: "mark fashions as seen", "toggle favourite", "read suit list".
///
/// These are INVOKE requests that ask for nothing back — the client only waits for the ack. The
/// reference branch answers them with an empty OK, and leaving them unrouted is worse than
/// useless: an unanswered INVOKE leaves the caller awaiting forever, which is exactly the
/// failure mode that shows up as a wardrobe button that never responds.
///
/// The bodies are deliberately not decoded. They are client-local bookkeeping (read receipts,
/// favourite flags), and inventing a contract for bytes we have never seen would risk a decode
/// exception on the only path whose job is to always answer.
/// </summary>
[Handler(MethodId.AskReadFashions)]
[Handler(MethodId.AskFavoriteFashions)]
[Handler(MethodId.AskReadFashionSuits)]
private Task FashionBookkeepingAccept(Connection conn, UxRpcMessage msg)
{
// Logged with the byte count so a future "suits do not show up" bug has a starting point:
// it tells us the client really sent the request, and how much it expected us to read.
conn.Log.Info($"[FASHION] bookkeeping accepted method={msg.MethodId} bytes={msg.Body.Length}");
return conn.ReturnEmptyOkAsync(msg);
}
}