using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Campus_live_rewrite.api.dbs;
using Campus_live_rewrite.api.server;
using Campus_live_rewrite.api.server.Classes;
namespace Campus_live_rewrite.Controllers
{
///
/// Admin panel Stores tab: pick a storefront, see its items grouped by set (GiftDrop.ItemSetId
/// / ItemSetFriendlyName - the sets the in-game store shows), add any item, change prices,
/// sets and the featured flag, take items off sale. Every change is an overlay in
/// Data/APIS/store-edits.json (StoreEdits) - the store files are never written.
///
public class AdminStoresController : Controller
{
private ContentResult J(object o, int status = 200) => new ContentResult
{
Content = Newtonsoft.Json.JsonConvert.SerializeObject(o),
ContentType = "application/json",
StatusCode = status
};
// Storefronts whose contents the server builds per player/room - not editable stock.
private static readonly HashSet NotStock = new()
{
(int)StorefrontType.None, (int)StorefrontType.Wishlist, (int)StorefrontType.RoomKeys,
(int)StorefrontType.Player_Profile, (int)StorefrontType.Room_Save, (int)StorefrontType.RoomCurrencies,
(int)StorefrontType.RoomConsumables, (int)StorefrontType.CustomAvatarItems,
(int)StorefrontType.ProgressionEventPurchasableXpBoosts, (int)StorefrontType.RoomOffers,
};
/// The store's own rows before edits (the Watch store as generated). Do not mutate the Watch rows.
private static JArray BaseRows(int store)
{
try
{
// Minus the items no store sells (StoreServe.Excluded), so the tab lists what
// players actually see.
if (store == StoreGenerator.WatchStorefront)
return new JArray((StoreGenerator.MergedWatchStore()["StoreItems"] as JArray ?? new JArray()).Where(r => !StoreServe.Excluded(r)));
var f = StoreCatalog.StoreFileForId(store);
if (f.Length == 0) return new JArray();
var arr = JObject.Parse(System.IO.File.ReadAllText(f))["StoreItems"] as JArray ?? new JArray();
foreach (var r in arr.OfType()) StoreGenerator.ShapeRow(r);
return new JArray(arr.Where(r => !StoreServe.Excluded(r)));
}
catch (Exception ex)
{
Console.WriteLine($"[admin-stores] could not read store {store}: {ex.Message}");
return new JArray();
}
}
private static int Pid(JToken r) => (int?)r["PurchasableItemId"] ?? 0;
/// The row as the store now sells it (edit first, else base), cloned; null if neither.
private static JObject? Effective(int store, int pid, JArray? baseRows = null)
{
var e = StoreEdits.RowFor(store, pid);
if (e != null) return e;
var b = (baseRows ?? BaseRows(store)).FirstOrDefault(r => Pid(r) == pid);
return b?.DeepClone() as JObject;
}
private static string Kind(JObject gd)
{
if ((bool?)gd["IsQuery"] == true) return "random box";
if (!string.IsNullOrWhiteSpace((string?)gd["CustomAvatarItemId"])) return "custom item";
if (!string.IsNullOrWhiteSpace((string?)gd["AvatarItemDesc"])) return "avatar item";
if (!string.IsNullOrWhiteSpace((string?)gd["EquipmentModificationGuid"])) return "skin";
if (!string.IsNullOrWhiteSpace((string?)gd["ConsumableItemDesc"])) return "consumable";
return "other";
}
private static object Dto(JObject r, string state)
{
var gd = r["GiftDrop"] as JObject ?? new JObject();
var p0 = (r["Prices"] as JArray)?.FirstOrDefault();
var s0 = (r["SubscriberPrices"] as JArray)?.FirstOrDefault();
return new
{
pid = Pid(r),
giftDropId = (int?)gd["GiftDropId"] ?? 0,
name = (string?)gd["FriendlyName"] ?? "",
kind = Kind(gd),
desc = (string?)gd["AvatarItemDesc"] ?? "",
rarity = (int?)gd["Rarity"] ?? 0,
price = (int?)p0?["Price"] ?? 0,
currency = (int?)p0?["CurrencyType"] ?? 0,
subPrice = (int?)s0?["Price"],
setId = gd["ItemSetId"]?.Type == JTokenType.Integer ? (int?)gd["ItemSetId"] : null,
setName = (string?)gd["ItemSetFriendlyName"] ?? "",
featured = (bool?)r["IsFeatured"] ?? false,
thumb = (string?)gd["ThumbnailImageName"] ?? "",
state
};
}
private static string StoreName(int id) => Enum.IsDefined(typeof(StorefrontType), id) ? ((StorefrontType)id).ToString() : $"Store {id}";
private static int MainCurrency(JArray rows)
{
var c = rows.Select(r => (int?)(r["Prices"] as JArray)?.FirstOrDefault()?["CurrencyType"] ?? 0).Where(x => x > 0)
.GroupBy(x => x).OrderByDescending(g => g.Count()).FirstOrDefault();
return c?.Key ?? (int)CurrencyType.RecCenterTokens;
}
private async Task Body()
{
using var r = new StreamReader(Request.Body);
var s = await r.ReadToEndAsync();
return string.IsNullOrWhiteSpace(s) ? new JObject() : JObject.Parse(s);
}
private static void SetPrices(JObject row, int price, int currency, int? subPrice)
{
int sub = subPrice ?? StoreGenerator.SubscriberPriceFor(price);
row["Prices"] = new JArray { new JObject { ["CurrencyType"] = currency, ["Price"] = price, ["StorefrontSaleData"] = null } };
row["SubscriberPrices"] = new JArray { new JObject { ["CurrencyType"] = currency, ["Price"] = sub, ["StorefrontSaleData"] = null } };
// When the price was set: the latest price set for an item, in any store, is what every
// store sells it for (StorePrices). Stripped before the row reaches the client.
row[StorePrices.PriceSetAtKey] = DateTime.UtcNow.ToString("o");
}
private static void SetSet(JObject row, int? setId, string? setName)
{
var gd = (JObject)(row["GiftDrop"] ??= new JObject());
gd["ItemSetId"] = setId.HasValue ? setId.Value : JValue.CreateNull();
gd["ItemSetFriendlyName"] = setId.HasValue ? (setName ?? (string?)gd["ItemSetFriendlyName"] ?? "") : "";
}
// ---- reads ----------------------------------------------------------------------------
[HttpGet("/admin/api/stores")]
public ContentResult List()
{
var (account, error) = AuthHelper.ValidateTokenDev(HttpContext);
if (error != null) return error;
var stores = new List