Files

181 lines
7.4 KiB
C#

using Ananta.SDK.Rpc;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
using Auto = Ananta.Server.RpcTypes.Client4229938.Auto;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// In-game GM console (ALT+F1) wallet and backpack grants for build 4229938.
/// The panel calls the native UX.Game.IGmToGamePlayer interface and both commands return void,
/// so the only observable effect is the push this router sends back:
/// AddMoney(money, gold, bindingGold, allowDebt) -> SyncMoney (authoritative totals) + SyncMoneyAdd popup
/// AddItem(itemTemplateId, count, isBind) -> SyncBackpackItemChanged (add or update stack)
/// The wire shapes are hand-decoded from the client metadata because these two commands have no
/// generated contract class; everything else keeps using the typed-neutral compatibility fallback.
/// </summary>
internal sealed partial class GameRouter
{
// 高水位落盘(IdCounterStore §6.10):GM 控制台发的 item instanceId 重启后不能复用。
private const long GmItemSeqFloor = 920000000000L;
private const string GmItemSeqCounter = "GmWallet.GmItemSeq";
[Handler(MethodId.AddMoney, HandlerPacketKind.Invoke)]
private async Task AddMoney(Connection conn, UxRpcMessage msg)
{
var state = GetWorldState(msg.Context);
var grant = ParseMoneyGrant4229938(msg.Body);
double money;
double gold;
double bindingGold;
lock (state.SyncRoot)
{
state.WalletMoney += grant.Money;
state.WalletGold += grant.Gold;
state.WalletBindingGold += grant.BindingGold;
money = state.WalletMoney;
gold = state.WalletGold;
bindingGold = state.WalletBindingGold;
}
conn.Log.Info($"[GM] add money body={Convert.ToHexString(msg.Body)} -> +({grant.Money},{grant.Gold},{grant.BindingGold}) allowDebt={grant.AllowDebt} total=({money},{gold},{bindingGold})");
await conn.NotifyAsync(MethodId.SyncMoney, new GameMethods.SyncMoney4229938
{
money = money,
gold = gold,
bindingGold = bindingGold,
});
if (grant.Money != 0)
{
await conn.NotifyAsync(MethodId.SyncMoneyAdd, new GameMethods.SyncMoneyAdd4229938
{
value = grant.Money,
reason = 0,
silence = false,
});
}
PlayerProgressStore.Save(state, conn.Log);
await conn.ReturnEmptyOkAsync(msg);
}
[Handler(MethodId.AddItem, HandlerPacketKind.Invoke)]
private async Task AddItem(Connection conn, UxRpcMessage msg)
{
var state = GetWorldState(msg.Context);
var grant = ParseItemGrant4229938(msg.Body);
if (grant.TemplateId == 0)
{
conn.Log.Warn($"[GM] add item ignored: no template id in body={Convert.ToHexString(msg.Body)}");
await conn.ReturnEmptyOkAsync(msg);
return;
}
Auto.PlayerPackItem stack;
bool isNewStack;
lock (state.SyncRoot)
{
isNewStack = !state.GmBackpackItems.TryGetValue(grant.TemplateId, out var existing);
if (existing is null)
{
stack = RuntimePayloadFactory.GmPackItem4229938(
grant.TemplateId,
grant.Count,
grant.IsBind,
(ulong)IdCounterStore.Next(GmItemSeqCounter, GmItemSeqFloor));
state.GmBackpackItems[grant.TemplateId] = stack;
}
else
{
// Keep the instance id stable so the client merges the grant into the existing stack.
existing.Count += grant.Count;
existing.IsBind = existing.IsBind || grant.IsBind;
stack = existing;
}
}
conn.Log.Info($"[GM] add item body={Convert.ToHexString(msg.Body)} -> id={grant.TemplateId} count={grant.Count} bind={grant.IsBind} {(isNewStack ? "new" : "stack")} uniqueId={stack.UniqueId} total={stack.Count}");
await conn.NotifyAsync(MethodId.SyncBackpackItemChanged, new GameMethods.SyncBackpackItemChanged4229938
{
addItemList = isNewStack ? [stack] : [],
updateItemList = isNewStack ? [] : [stack],
});
PlayerProgressStore.Save(state, conn.Log);
await conn.ReturnEmptyOkAsync(msg);
}
private readonly record struct MoneyGrant4229938(double Money, double Gold, double BindingGold, bool AllowDebt);
private readonly record struct ItemGrant4229938(uint TemplateId, uint Count, bool IsBind);
// UX.Game.IGmToGamePlayer.AddMoney(money, gold, bindingGold, allowDebt): three 8-byte amounts
// followed by one bool (the captured 25-byte panel packet). Gold and binding gold may be absent
// in older bodies, so decode whatever complete fields are present and default the rest to zero.
private static MoneyGrant4229938 ParseMoneyGrant4229938(byte[] body)
{
var hasTrailingBool = (body.Length % 8) == 1;
var amountBytes = hasTrailingBool ? body.Length - 1 : body.Length;
var amounts = Math.Min(3, amountBytes / 8);
return new MoneyGrant4229938(
amounts > 0 ? ReadAmount4229938(body, 0) : 0d,
amounts > 1 ? ReadAmount4229938(body, 8) : 0d,
amounts > 2 ? ReadAmount4229938(body, 16) : 0d,
hasTrailingBool && body[^1] != 0);
}
// The native panel writes whole amounts as int64 (1 -> 01 00 00 00 00 00 00 00) while the
// signature declares the field as a double, so accept either reading: the int64 form of a
// genuine double is always astronomically large, and the double form of a whole amount is a
// denormal or a nonsense fraction.
private static double ReadAmount4229938(byte[] body, int offset)
{
var asInteger = BitConverter.ToInt64(body, offset);
var asDouble = BitConverter.ToDouble(body, offset);
var integerSane = Math.Abs((double)asInteger) <= 1e15;
var doubleSane = double.IsFinite(asDouble)
&& Math.Abs(asDouble) <= 1e15
&& (asDouble == 0d || Math.Abs(asDouble) >= 1e-6);
if (doubleSane && !integerSane)
return asDouble;
return asInteger;
}
// UX.Game.IGmToGamePlayer.AddItem(itemTemplateId, count, isBind) is 9 bytes on the wire. Item
// ids and counts are 4-byte fields; tolerate a hypothetical 8-byte variant by clamping instead
// of throwing, and always answer OK so the panel never hangs on an unknown body.
private static ItemGrant4229938 ParseItemGrant4229938(byte[] body)
{
var hasTrailingBool = (body.Length % 2) == 1;
var payload = hasTrailingBool ? body.Length - 1 : body.Length;
var isBind = hasTrailingBool && body[^1] != 0;
uint templateId;
uint count;
if (payload >= 16)
{
templateId = ClampToUInt32(BitConverter.ToUInt64(body, 0));
count = ClampToUInt32(BitConverter.ToUInt64(body, 8));
}
else
{
templateId = payload >= 4 ? BitConverter.ToUInt32(body, 0) : 0u;
count = payload >= 8 ? BitConverter.ToUInt32(body, 4) : 0u;
}
return new ItemGrant4229938(templateId, count == 0 ? 1u : count, isBind);
}
private static uint ClampToUInt32(ulong value)
=> value > uint.MaxValue ? uint.MaxValue : (uint)value;
}