181 lines
7.4 KiB
C#
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;
|
|
}
|