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,115 @@
|
||||
using Ananta.SDK.Rpc;
|
||||
using Ananta.Server.Protocol.Client4229938;
|
||||
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
||||
|
||||
namespace Ananta.Server.Handlers.Game;
|
||||
|
||||
/// <summary>
|
||||
/// Wardrobe dye surface (build 4229938), ported from the Lyco donor build on 2026-09-26.
|
||||
///
|
||||
/// ⛑⛑ The client commits every dye change locally into <c>PlayerFashionsInfo</c> the moment the
|
||||
/// invoke answers err=0 (<c>DressData.lua</c> reads the error branch as success — see 硬规则 18 on
|
||||
/// inverted decompiles, and the err==0 = OK polarity proven by the donor's Player.log).
|
||||
/// <b>No S2C push is needed for the player's own unit</b>: the handlers only have to persist the
|
||||
/// state, and <c>RuntimePayloadFactory</c> replays it on the next login seed.
|
||||
///
|
||||
/// ⛑ The four bodies are decoded defensively. These are C2S shapes taken from the donor's
|
||||
/// <c>ClientToGameDelegate.lua</c> serializer reading; an Int7/Int32 guess that turns out wrong must
|
||||
/// degrade to "dye not remembered", never to a decode exception on the only path whose job is to
|
||||
/// answer the INVOKE (an unanswered INVOKE leaves the dye panel waiting forever).
|
||||
/// </summary>
|
||||
internal sealed partial class GameRouter
|
||||
{
|
||||
[Handler(MethodId.AskUnlockFashionColoringSlot, HandlerPacketKind.Invoke)]
|
||||
private async Task AskUnlockFashionColoringSlot(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
if (!msg.TryGetArgs<GameMethods.AskUnlockFashionColoringSlotArgs>(out var args) || args is null)
|
||||
{
|
||||
conn.Log.Warn($"[FASHION-DYE] unlock undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}");
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = GetWorldState(msg.Context);
|
||||
FashionColoringStore.AddUnlockedSlots(state, args.fashionid, args.unlockslotcount);
|
||||
conn.Log.Info(
|
||||
$"[FASHION-DYE] unlock fashion={args.fashionid} +{args.unlockslotcount} " +
|
||||
$"slots={FashionColoringStore.GetUnlockedSlots(state, args.fashionid)}");
|
||||
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||||
}
|
||||
|
||||
[Handler(MethodId.AskSetFashionColoringSchemeInfos, HandlerPacketKind.Invoke)]
|
||||
private async Task AskSetFashionColoringSchemeInfos(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
if (!msg.TryGetArgs<GameMethods.AskSetFashionColoringSchemeInfosArgs>(out var args) || args is null)
|
||||
{
|
||||
conn.Log.Warn($"[FASHION-DYE] set-scheme undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}");
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = GetWorldState(msg.Context);
|
||||
foreach (var entry in args.fashionColoringSchemeInfoList)
|
||||
{
|
||||
foreach (var (planId, coloring) in entry.FashionColoringSchemeInfoDict)
|
||||
{
|
||||
FashionColoringStore.SetSchemes(state, entry.FashionId, planId, coloring.ColoringType2ColorIdDict);
|
||||
conn.Log.Info(
|
||||
$"[FASHION-DYE] set scheme fashion={entry.FashionId} plan={planId} " +
|
||||
$"colors=[{string.Join(',', coloring.ColoringType2ColorIdDict.Select(kv => $"{kv.Key}:{kv.Value}"))}]");
|
||||
}
|
||||
}
|
||||
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||||
}
|
||||
|
||||
[Handler(MethodId.AskApplyFashionColoringSchemeInfos, HandlerPacketKind.Invoke)]
|
||||
private async Task AskApplyFashionColoringSchemeInfos(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
if (!msg.TryGetArgs<GameMethods.AskApplyFashionColoringSchemeInfosArgs>(out var args) || args is null)
|
||||
{
|
||||
conn.Log.Warn($"[FASHION-DYE] apply undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}");
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = GetWorldState(msg.Context);
|
||||
foreach (var (fashionId, planId) in args.applyfashioncoloringschemeiddict)
|
||||
{
|
||||
FashionColoringStore.SetApplied(state, fashionId, planId);
|
||||
conn.Log.Info($"[FASHION-DYE] apply fashion={fashionId} plan={planId}");
|
||||
}
|
||||
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||||
}
|
||||
|
||||
[Handler(MethodId.AskResetFashionColoringSchemeInf, HandlerPacketKind.Invoke)]
|
||||
private async Task AskResetFashionColoringSchemeInf(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
if (!msg.TryGetArgs<GameMethods.AskResetFashionColoringSchemeInfArgs>(out var args) || args is null)
|
||||
{
|
||||
conn.Log.Warn($"[FASHION-DYE] reset undecodable bytes={msg.Body.Length} hex={Convert.ToHexString(msg.Body)}");
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = GetWorldState(msg.Context);
|
||||
foreach (var entry in args.resetFashionColoringSchemeInfoList)
|
||||
{
|
||||
foreach (var (planId, reset) in entry.resetFashionColoringSchemeInfoDict)
|
||||
{
|
||||
FashionColoringStore.ResetTypes(state, entry.FashionId, reset.resetColoringTypeList);
|
||||
conn.Log.Info(
|
||||
$"[FASHION-DYE] reset fashion={entry.FashionId} plan={planId} " +
|
||||
$"types=[{string.Join(',', reset.resetColoringTypeList)}]");
|
||||
}
|
||||
}
|
||||
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user