98 lines
4.4 KiB
C#
98 lines
4.4 KiB
C#
using Ananta.SDK.Rpc;
|
|||
|
|
using Ananta.Server.ClientData.Client4229938;
|
||
|
|
using Ananta.Server.Configuration;
|
||
|
|
using Ananta.Server.Protocol.Client4229938;
|
||
|
|
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
||
|
|
|
||
|
|
namespace Ananta.Server.Handlers.Game;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// NPC shop shelves for build 4229938 (ShopManager: AskNpcShop -> SyncFullCommodityInfos).
|
||
|
|
///
|
||
|
|
/// Why this file exists: a placed shop clerk opens its panel fine but the panel is empty. The
|
||
|
|
/// client builds the rows from tables it already owns (ShopConfig / ShopCommodityGroupConfig /
|
||
|
|
/// ShopCommodityConfig) and only waits for the server to say what is actually on the shelf —
|
||
|
|
/// <c>ShopManager.lua</c> aborts with "获取不到服务端的商品数据" when
|
||
|
|
/// <c>shopCommodityDataDict[shopId]</c> has no entry, which is exactly the empty panel. That
|
||
|
|
/// dictionary is filled by nothing but SyncFullCommodityInfos (64769566).
|
||
|
|
///
|
||
|
|
/// The server is not asked to price or validate anything here: it restocks every row at its
|
||
|
|
/// configured price with unlimited stock. Charging for a purchase is a separate step
|
||
|
|
/// (AskBuyCommodity) and is deliberately not implemented yet.
|
||
|
|
/// </summary>
|
||
|
|
internal sealed partial class GameRouter
|
||
|
|
{
|
||
|
|
/// <summary>Solution Pawn — the shop standing 1.95 m from the 典斥屋 scene preset, used as the A0 pilot.</summary>
|
||
|
|
private const uint PilotShopId = 94100024u;
|
||
|
|
|
||
|
|
[Handler(MethodId.AskNpcShop, HandlerPacketKind.Invoke)]
|
||
|
|
private async Task AskNpcShop(Connection conn, UxRpcMessage msg)
|
||
|
|
{
|
||
|
|
var mode = PrivateServerConfigStore.Current.Gameplay.NpcShopPayloadMode;
|
||
|
|
|
||
|
|
// AskNpcShop is a void INVOKE the client awaits, so it must always be answered — a shop we
|
||
|
|
// cannot restock still has to return OK, otherwise the clerk looks like a failed interaction.
|
||
|
|
if (string.Equals(mode, "off", StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var shopId = msg.Body.Length >= 4 ? BitConverter.ToUInt32(msg.Body, 0) : 0u;
|
||
|
|
if (shopId == 0)
|
||
|
|
{
|
||
|
|
conn.Log.Warn($"[NPCSHOP] AskNpcShop without a shop id (body={msg.Body.Length}B hex={Convert.ToHexString(msg.Body)})");
|
||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (string.Equals(mode, "pilot", StringComparison.OrdinalIgnoreCase) && shopId != PilotShopId)
|
||
|
|
{
|
||
|
|
conn.Log.Info($"[NPCSHOP] skip shop={shopId} (pilot mode only serves {PilotShopId})");
|
||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var loadError = NpcShopCatalog4229938.LoadError;
|
||
|
|
if (loadError is not null)
|
||
|
|
{
|
||
|
|
conn.Log.Warn($"[NPCSHOP] catalog unusable: {loadError}");
|
||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!NpcShopCatalog4229938.TryGetShop(shopId, out var shop))
|
||
|
|
{
|
||
|
|
conn.Log.Info($"[NPCSHOP] shop={shopId} is not in NpcShopCatalog.json ({NpcShopCatalog4229938.Count} shops known)");
|
||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var infos = new List<GameMethods.NpcShopCommodityInfo4229938>(shop.Commodities.Count);
|
||
|
|
foreach (var commodity in shop.Commodities)
|
||
|
|
{
|
||
|
|
infos.Add(new GameMethods.NpcShopCommodityInfo4229938
|
||
|
|
{
|
||
|
|
TemplateId = commodity.TemplateId,
|
||
|
|
// A positive stock keeps the row out of the sold-out state; LimitNum <= 0 means the
|
||
|
|
// official row is unlimited, in which case there is no meaningful number to print.
|
||
|
|
Count = commodity.LimitNum > 0 ? commodity.LimitNum : 9999,
|
||
|
|
RefreshTime = 0,
|
||
|
|
Status = 0,
|
||
|
|
Discount = 100, // 100 = no discount; anything else flips the client's HasDiscount.
|
||
|
|
DiscountPrice = commodity.Price,
|
||
|
|
MaxBuyCount = 0, // 0 = the client leaves the stock figure alone.
|
||
|
|
DailyDiscount = 0,
|
||
|
|
CommodityBadgeDiscountDict = new Dictionary<uint, uint>(),
|
||
|
|
BlackMarketPriceModifier = 0,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
await conn.NotifyAsync(
|
||
|
|
MethodId.IGameToClient_SyncFullCommodityInfos,
|
||
|
|
new GameMethods.SyncFullCommodityInfosArgs4229938 { shopId = shopId, infos = infos });
|
||
|
|
|
||
|
|
conn.Log.Info($"[NPCSHOP] shop={shopId} '{shop.Name}' restocked {infos.Count} rows mode={mode}");
|
||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
||
|
|
}
|
||
|
|
}
|