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.WeaponSkins.cs

78 lines
3.5 KiB
C#

using Ananta.SDK.Rpc;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Protocol.Client4229938;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// Weapon-skin surface (build 4229938), ported from the Lyco donor build on 2026-09-26.
///
/// Ownership is seeded at login: <c>PlayerWeaponSkinInfo.OwnedSkinIds</c> gets every authored
/// WeaponSkinConfig id (221 in stock 4229938), which is what makes the skin picker list anything at
/// all. These handlers then persist the per-character <b>equipped</b> set
/// (<c>SpiritWeaponSkinDict</c>) and answer empty OK — the client commits the assignment locally.
///
/// ⛑ Both bodies are decoded defensively: they are C2S shapes read off the donor's
/// ClientToGameDelegate.lua serializers, and an unanswered INVOKE would leave the picker waiting
/// forever, which is worse than a skin the server forgets.
/// </summary>
internal sealed partial class GameRouter
{
[Handler(MethodId.AskSetWeaponSkins, HandlerPacketKind.Invoke)]
private async Task AskSetWeaponSkins(Connection conn, UxRpcMessage msg)
{
if (!msg.TryGetArgs<GameMethods.AskSetWeaponSkinsArgs>(out var args) || args is null)
{
conn.Log.Warn($"[WEAPON-SKIN] set undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}");
await conn.ReturnEmptyOkAsync(msg);
return;
}
var state = GetWorldState(msg.Context);
foreach (var spiritId in args.spiritIds)
WeaponSkinStore.SetSkin(state, spiritId, args.sceneItemIds, args.skinId);
conn.Log.Info(
$"[WEAPON-SKIN] set skin={args.skinId} spirits=[{string.Join(',', args.spiritIds)}] " +
$"sceneItems=[{string.Join(',', args.sceneItemIds)}]");
await conn.ReturnEmptyOkAsync(msg);
PlayerProgressStore.Save(state, conn.Log, quiet: true);
}
[Handler(MethodId.AskSyncWeaponSkinToSpirits, HandlerPacketKind.Invoke)]
private async Task AskSyncWeaponSkinToSpirits(Connection conn, UxRpcMessage msg)
{
if (!msg.TryGetArgs<GameMethods.AskSyncWeaponSkinToSpiritsArgs>(out var args) || args is null)
{
conn.Log.Warn($"[WEAPON-SKIN] sync undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}");
await conn.ReturnEmptyOkAsync(msg);
return;
}
var state = GetWorldState(msg.Context);
var source = WeaponSkinStore.GetEquipped(state, args.sourceSpiritId);
foreach (var spiritId in args.targetSpiritIds)
WeaponSkinStore.SetAll(state, spiritId, source);
conn.Log.Info(
$"[WEAPON-SKIN] sync spirit={args.sourceSpiritId} -> [{string.Join(',', args.targetSpiritIds)}] " +
$"entries={source.Count}");
await conn.ReturnEmptyOkAsync(msg);
PlayerProgressStore.Save(state, conn.Log, quiet: true);
}
/// <summary>
/// Push newly acquired skins (skinId -> acquireTime). With unlock-all the whole catalog is owned
/// from login, so this stays unused — it is the only channel for granting a skin mid-session and
/// is kept in shape for that.
/// </summary>
private Task PushAddWeaponSkinsAsync(Connection conn, IReadOnlyDictionary<uint, uint> skinIdToAcquireTime)
=> conn.NotifyAsync(MethodId.IGameToClient_SyncAddWeaponSkinList, new GameMethods.SyncAddWeaponSkinList
{
skinIdToAcquireTime = new Dictionary<uint, uint>(skinIdToAcquireTime),
});
}