feat: Upgrade server with HyNB v2.4 modular architecture, minigames, admin GM web panel, companion summons, and car radio
This commit is contained in:
1 parent
74e2963a12
commit
3e5adf1dc5
1571 files changed
+336468
-6348
No files matched your search
@@ -0,0 +1,77 @@
|
||||
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),
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user