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; /// /// 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. /// 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; }