Files
Campus-25/static/Servers/Campus/api/server/StorePrices.cs
T
2026-10-14 06:59:51 +02:00

123 lines
6.0 KiB
C#

using Newtonsoft.Json.Linq;
using Campus_live_rewrite.api.server.Classes;
namespace Campus_live_rewrite.api.server
{
/// <summary>
/// One Rec Center Token price per item across every store (2026-09-27 request: Thriller Suit
/// (Orange) was 4000 in the Watch and 4500 in Pop Up Shop 1).
///
/// The price an item sells for is the one the admin most recently SET for it in the Stores tab,
/// in whichever store (rows carry "PriceSetAt" from then on; older edits count as set before any
/// stamped one, newest admin row first among them). An item never priced there keeps the Watch
/// store's price. Every served store row priced in tokens is rewritten to it (subscriber price
/// = the usual 10% off), and a purchase is charged it. Items priced in room currencies or other
/// currencies are left alone.
///
/// An item is its GiftDropId plus the pack size (ItemCount), so a 1-pack and a 5-pack of the
/// same consumable keep their own prices.
/// </summary>
public static class StorePrices
{
public const string PriceSetAtKey = "PriceSetAt";
private const int Tokens = (int)CurrencyType.RecCenterTokens;
public static string? ItemKey(JToken row)
{
var gd = row["GiftDrop"];
int drop = (int?)gd?["GiftDropId"] ?? 0;
if (drop == 0) return null;
int count = Math.Max(1, (int?)gd?["ItemCount"] ?? 1);
return $"{drop}x{count}";
}
/// <summary>The row's token price, or null when it is not sold for tokens.</summary>
public static int? TokenPrice(JToken row)
{
foreach (var p in row["Prices"] as JArray ?? new JArray())
if (((int?)p["CurrencyType"] ?? 0) == Tokens) return (int?)p["Price"];
return null;
}
// ---- the canonical price table ------------------------------------------------------------
private static readonly object _lock = new();
private static string _key = "";
private static Dictionary<string, int> _prices = new();
private static Dictionary<string, int> _watchBase = new();
private static string _watchKey = "";
/// <summary>Called while the Watch store is built, BEFORE unification: its own prices are the fallback.</summary>
public static void SetWatchBase(JObject watchRoot, string watchKey)
{
var map = new Dictionary<string, int>();
foreach (var r in watchRoot["StoreItems"] as JArray ?? new JArray())
if (ItemKey(r) is { } k && TokenPrice(r) is { } p && !map.ContainsKey(k)) map[k] = p;
lock (_lock) { _watchBase = map; _watchKey = watchKey; }
}
public static string WatchKey { get { lock (_lock) return _watchKey; } }
private static Dictionary<string, int> Table()
{
string key = $"{StoreEdits.Version}|{WatchKey}";
lock (_lock)
{
if (key == _key) return _prices;
// Every admin price, newest first: stamped rows by stamp, unstamped (older) rows by
// admin id (a higher id was added later).
var edits = new List<(string item, int price, DateTime at, int pid)>();
foreach (int store in Enum.GetValues(typeof(StorefrontType)).Cast<int>().Distinct())
{
if (StoreServe.IsPerPlayer(store)) continue;
foreach (var r in StoreEdits.Rows(store))
{
if (ItemKey(r) is not { } item || TokenPrice(r) is not { } price) continue;
DateTime at = DateTime.TryParse((string?)r[PriceSetAtKey], null, System.Globalization.DateTimeStyles.RoundtripKind, out var t) ? t : DateTime.MinValue;
edits.Add((item, price, at, (int?)r["PurchasableItemId"] ?? 0));
}
}
var table = new Dictionary<string, int>(_watchBase);
foreach (var e in edits.OrderBy(e => e.at).ThenBy(e => e.pid))
table[e.item] = e.price; // later wins
_prices = table;
_key = key;
return table;
}
}
/// <summary>The one token price this item sells for, or null when no store has it for tokens.</summary>
public static int? PriceFor(JToken row) =>
ItemKey(row) is { } k && Table().TryGetValue(k, out var p) ? p : null;
/// <summary>Rewrite every token-priced row of a served store to the item's one price. Returns how many changed.</summary>
public static int Apply(JObject root)
{
int changed = 0;
foreach (var r in (root["StoreItems"] as JArray ?? new JArray()).OfType<JObject>())
{
r.Remove(PriceSetAtKey); // bookkeeping only - never sent to the client
if (TokenPrice(r) is not { } now || PriceFor(r) is not { } want || want == now) continue;
foreach (var p in r["Prices"] as JArray ?? new JArray())
if (((int?)p["CurrencyType"] ?? 0) == Tokens) p["Price"] = want;
foreach (var p in r["SubscriberPrices"] as JArray ?? new JArray())
if (((int?)p["CurrencyType"] ?? 0) == Tokens) p["Price"] = StoreGenerator.SubscriberPriceFor(want);
changed++;
}
return changed;
}
/// <summary>Charge the served (unified) price for a purchase priced in tokens.</summary>
public static void ApplyTo(StoreCatalog.GiftDropInfo item, JObject? servedRow)
{
if (servedRow == null || item.Currency != Tokens || TokenPrice(servedRow) is not { } price) return;
item.Cost = price;
if (item.SubscriberCurrency < 0 || item.SubscriberCurrency == Tokens)
{
item.SubscriberCost = StoreGenerator.SubscriberPriceFor(price);
item.SubscriberCurrency = Tokens;
}
}
}
}