feat: Upgrade server with HyNB v2.4 modular architecture, minigames, admin GM web panel, companion summons, and car radio

This commit is contained in:
CAtlas0784 committed 2026-10-29 02:44:24 +07:00
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),
});
}