Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.Trade.cs
T

164 lines
6.7 KiB
C#

using Ananta.SDK.Rpc;
using Ananta.Server.ClientData.Client4229938;
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>
/// Trading post for build 4229938 (TradeManager.AskTradeGetMarketList -> 63492557 and
/// TradeManager.AskTradeBuyItem -> 63188959).
///
/// Retail fills the market from other players' sell orders; this server has none, so every row that
/// carries a price is answered with one synthesised bucket at the row's floor price and a fixed depth.
/// That is all the purchase window needs to open up: its CanBuy() requires marketCount > 0, maxCount > 0
/// and price > 0, and the quantity slider's maximum is the summed bucket depth. A single bucket also
/// keeps the total the client shows (price x count, via its own auto-buy walk) equal to what the wallet
/// is charged here.
///
/// The market settles in 寰宇币 (ConsumableConfig.RewardGold / MoneyType.Gold) and the buy reply is a
/// bare uint32 "filled count" that the client turns into the reward popup.
/// </summary>
internal sealed partial class GameRouter
{
// TradeItemConfig.ItemType: 0 box/consumable, 1 wardrobe piece, 2 fashion suit (order book only),
// 3 household collectible (painting/statue/valuable).
private const uint TradeItemTypeBox4229938 = 0;
private const uint TradeItemTypeFashion4229938 = 1;
private const uint TradeItemTypeOrder4229938 = 2;
private const uint TradeItemTypeCollectible4229938 = 3;
private const uint TradeCurrency4229938 = CurrencyRewardGold4229938;
/// <summary>Depth of the synthesised bucket; also the quantity slider's maximum in the window.</summary>
private const uint TradeBucketDepth4229938 = 99;
[Handler(MethodId.AskTradeGetMarketList, HandlerPacketKind.Invoke)]
private async Task AskTradeGetMarketList(Connection conn, UxRpcMessage msg)
{
var tradeItemId = ReadUInt32At4229938(msg.Body, 0);
var reply = new GameMethods.TradeMarketList4229938();
if (EconomyConfigRepository.TryTradeItem(tradeItemId, out var item)
&& item.MarketPrice > 0
&& item.ItemType != TradeItemTypeOrder4229938)
{
reply.buckets.Add(new GameMethods.TradeBucketInfo4229938
{
BucketId = item.MarketPrice,
OrderCount = 1,
Count = TradeBucketDepth4229938,
MinPrice = item.MarketPrice,
MaxPrice = item.MarketPrice,
});
conn.Log.Info(
$"[TRADE] market list row={item.Id} tab={item.Tab} type={item.ItemType} " +
$"price={item.MarketPrice} depth={TradeBucketDepth4229938}");
}
else
{
// Empty 7-bit list: the window stays on "no listing" instead of failing to parse.
conn.Log.Warn($"[TRADE] market list has no bucket for row {tradeItemId} (body={Convert.ToHexString(msg.Body)})");
}
await conn.ReturnAsync(msg, reply);
}
[Handler(MethodId.AskTradeBuyItem, HandlerPacketKind.Invoke)]
private async Task AskTradeBuyItem(Connection conn, UxRpcMessage msg)
{
var state = GetWorldState(msg.Context);
var tradeItemId = ReadUInt32At4229938(msg.Body, 0);
var maxPrice = ReadUInt32At4229938(msg.Body, 4);
var requested = ReadUInt32At4229938(msg.Body, 8);
var count = Math.Clamp(requested == 0 ? 1u : requested, 1u, TradeBucketDepth4229938);
if (!EconomyConfigRepository.TryTradeItem(tradeItemId, out var item)
|| item.MarketPrice == 0
|| item.ItemType == TradeItemTypeOrder4229938)
{
conn.Log.Warn($"[TRADE] buy rejected: row {tradeItemId} is not a market row (body={Convert.ToHexString(msg.Body)})");
await conn.ReturnAsync(msg, 0u);
return;
}
var fashions = new List<uint>();
var changes = new EconomyChanges4229938();
var granted = GrantTradePurchase4229938(conn, changes, fashions, item, count);
if (granted == 0)
{
await conn.ReturnAsync(msg, 0u);
return;
}
var unitPrice = maxPrice > 0 ? maxPrice : item.MarketPrice;
ChargeCurrency4229938(
state, changes, TradeCurrency4229938, (double)unitPrice * count, conn.Log, $"trade {item.Id}");
var summary = await ApplyEconomy4229938Async(conn, state, changes, $"trade {item.Id}");
// Wardrobe pieces are ownership, not a stack, so they arrive as a fashion unlock push.
foreach (var fashionId in fashions)
{
await conn.NotifyAsync(MethodId.IGameToClient_SyncAddOrUpdateFashion, new Auto.FashionInfo
{
FashionId = fashionId,
ExpiredTime = 0,
GainTime = 1,
Status = 0,
ApplyColoringSchemeId = 0,
UnlockColoringSlotCount = 0,
ColoringSchemeTopNMaxCollectionScore = 0,
});
}
conn.Log.Info(
$"[TRADE] buy row={item.Id} tab={item.Tab} type={item.ItemType} count={count} " +
$"unitPrice={unitPrice} granted={granted} unlockedFashions={fashions.Count} " +
$"currency={TradeCurrency4229938} -> {summary}");
await conn.ReturnAsync(msg, granted);
}
/// <summary>
/// Queues what the row sells and returns how many units the client should show as bought.
/// Item rows land in the backpack delta, fashion rows in the unlock list, and rows with nothing to
/// hand out (the fashion-suit order rows) report zero so the caller charges nothing.
/// </summary>
private static uint GrantTradePurchase4229938(
Connection conn,
EconomyChanges4229938 changes,
List<uint> fashions,
EconomyConfigRepository.TradeItem item,
uint count)
{
switch (item.ItemType)
{
case TradeItemTypeBox4229938:
case TradeItemTypeCollectible4229938:
if (item.ItemId == 0)
break;
AddGrant4229938(changes, item.ItemId, count, isBind: false);
return count;
case TradeItemTypeFashion4229938:
if (item.FashionId == 0)
break;
fashions.Add(item.FashionId);
return count;
}
conn.Log.Warn(
$"[TRADE] row {item.Id} (type={item.ItemType} item={item.ItemId} fashion={item.FashionId} " +
$"suit={item.FashionSuitId}) has nothing to grant");
return 0;
}
private static uint ReadUInt32At4229938(byte[] body, int offset)
=> body.Length >= offset + 4 ? BitConverter.ToUInt32(body, offset) : 0u;
}