78 lines
3.5 KiB
C#
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),
|
|
});
|
|
}
|