using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Campus_live_rewrite.api; using Campus_live_rewrite.api.dbs; using Campus_live_rewrite.api.server; using Classes = Campus_live_rewrite.api.server.Classes; using static Campus_live_rewrite.api.dbs.RoomEconDB; namespace Campus_live_rewrite.Controllers { /// /// Room keys, room currencies and room consumables - created in Room Settings, sold in-room, /// paid for with tokens (or with a room currency), owned afterwards. Every one of these /// routes answered "[]" / an ack before, so a price set in-game was never charged and nothing /// was ever kept. /// /// Purchases with tokens pay the ROOM'S CREATOR their cut (EarningsDB). Request fields are /// read by name from form, query or JSON (RoomModeration.Fields) - the exact spellings this /// build posts are logged on every write so they can be pinned. Purchase answers carry BOTH /// the bare balance-response keys and the {Value,Success,Error,error_id} envelope, so either /// reader finds what it needs. /// [ApiController] public class RoomEconController : ControllerBase { private const int Tokens = (int)Classes.CurrencyType.RecCenterTokens; private static ContentResult J(object o, int status = 200) => new() { Content = JsonConvert.SerializeObject(o), ContentType = "application/json", StatusCode = status }; private static object Envelope(object? value, string? error = null) => new { Value = value, Success = error == null, Error = error, error_id = (string?)null }; private static bool CanEdit(long roomId, ulong accountId) => RoomDB.UserCanEditRoom((ulong)roomId, accountId) || PlayerDB.CheckDevFlag(accountId); private static ulong RoomCreator(long roomId) => RoomDB.GetRoom((ulong)roomId)?.CreatorAccountId ?? 0; private Dictionary F() => RoomModeration.Fields(Request); private static long L(Dictionary f, params string[] n) => RoomModeration.Long(f, n); private static string? S(Dictionary f, params string[] n) => RoomModeration.Str(f, n); private static Guid? G(Dictionary f, params string[] n) { var s = S(f, n); return s != null && Guid.TryParse(s.Trim(), out var g) && g != Guid.Empty ? g : null; } private object BalanceAnswer(long balance, object? data, int updateResponse = 0) { var bare = new { Balance = balance, CurrencyType = Tokens, Platform = Classes.GameCurrency.AllPlatformsBucket, BalanceType = Classes.GameCurrency.AllPlatformsBucket, BalanceUpdates = new[] { new { UpdateResponse = updateResponse, Data = data } } }; return new { Value = bare, Success = true, Error = (string?)null, error_id = (string?)null, bare.Balance, bare.CurrencyType, bare.Platform, bare.BalanceType, bare.BalanceUpdates }; } private ContentResult Refuse(string why, int status = 200) => J(new { Value = (object?)null, Success = false, Error = why, error_id = (string?)null, Balance = 0, BalanceUpdates = Array.Empty() }, status); // ======================================================================= KEYS [HttpGet("/api/roomkeys/v1/mine")] public ContentResult MyKeys() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; return J(KeysOwnedBy(account.Id).Select(KeyDto).ToList()); } [HttpGet("/api/roomkeys/v1/room")] public ContentResult KeysOfRoom([FromQuery] long roomId) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; return J(KeysForRoom(roomId).Select(KeyDto).ToList()); } // ?roomKeyId= -> that key; ?roomId= -> any key of that room. Bare bool. [HttpGet("/api/roomkeys/v1/owns")] public ContentResult OwnsKeyRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return J(false); var f = F(); long keyId = L(f, "roomKeyId", "RoomKeyId", "id"); long roomId = L(f, "roomId", "RoomId"); bool owns = keyId > 0 ? OwnsKey(account.Id, keyId) : roomId > 0 && KeysForRoom(roomId).Any(k => OwnsKey(account.Id, k.RoomKeyId)); return J(owns); } [HttpGet("/api/roomkeys/v1/owns/bulk")] [HttpPost("/api/roomkeys/v1/owns/bulk")] public ContentResult OwnsKeysBulk() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var ids = RoomModeration.ULongs(F(), "id", "ids", "roomKeyIds", "RoomKeyIds", "roomKeyId"); return J(ids.Select(id => new { RoomKeyId = (long)id, IsOwned = OwnsKey(account.Id, (long)id), Owned = OwnsKey(account.Id, (long)id) }).ToList()); } [HttpPost("/api/roomkeys/v1/create")] [HttpPut("/api/roomkeys/v1/create")] public ContentResult CreateKeyRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); Console.WriteLine($"[roomkeys] {account.Id} create: {RoomModeration.Dump(f)}"); long roomId = L(f, "RoomId", "roomId"); if (roomId <= 0) return J(Envelope(null, "RoomId is required"), 400); if (!CanEdit(roomId, account.Id)) return J(Envelope(null, "You cannot edit this room"), 403); var k = CreateKey(new RoomKey { RoomId = roomId, CreatorId = RoomCreator(roomId), Name = (S(f, "Name", "name") ?? "Room Key").Trim(), Description = S(f, "Description", "description") ?? "", Price = (int)Math.Max(0, L(f, "Price", "price")), PurchaseCurrencyId = G(f, "PurchaseCurrencyId", "purchaseCurrencyId", "CurrencyId"), ImageName = S(f, "ImageName", "imageName"), Type = (int)L(f, "Type", "type") }); Console.WriteLine($"[roomkeys] room {roomId}: key #{k.RoomKeyId} '{k.Name}' for {k.Price} {(k.PurchaseCurrencyId.HasValue ? "room currency" : "tokens")}"); var dto = KeyDto(k); return J(new { Value = dto, Success = true, Error = (string?)null, error_id = (string?)null, RoomKey = dto, k.RoomKeyId, k.ReplicationId, k.RoomId, k.Name, k.Description, k.Price, k.PurchaseCurrencyId, CreatedAt = k.CreatedAt.ToString("o"), ImageName = k.ImageName ?? "", k.Type }); } [HttpPost("/api/roomkeys/v1/update")] [HttpPut("/api/roomkeys/v1/update")] [HttpPut("/api/roomkeys/v1/{roomKeyId:long}")] [HttpPost("/api/roomkeys/v1/{roomKeyId:long}")] public ContentResult UpdateKeyRoute(long roomKeyId = 0) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); if (roomKeyId == 0) roomKeyId = L(f, "RoomKeyId", "roomKeyId", "id"); var k = GetKey(roomKeyId); if (k == null) return J(Envelope(null, "No such key"), 404); if (!CanEdit(k.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room"), 403); Console.WriteLine($"[roomkeys] {account.Id} update #{roomKeyId}: {RoomModeration.Dump(f)}"); k = UpdateKey(roomKeyId, x => { if (S(f, "Name", "name") is string n && n.Trim().Length > 0) x.Name = n.Trim(); if (S(f, "Description", "description") is string d) x.Description = d; if (f.ContainsKey("Price") || f.ContainsKey("price")) x.Price = (int)Math.Max(0, L(f, "Price", "price")); if (f.ContainsKey("PurchaseCurrencyId") || f.ContainsKey("purchaseCurrencyId")) x.PurchaseCurrencyId = G(f, "PurchaseCurrencyId", "purchaseCurrencyId"); if (S(f, "ImageName", "imageName") is string img) x.ImageName = img; })!; return J(Envelope(KeyDto(k))); } [HttpDelete("/api/roomkeys/v1/{roomKeyId:long}")] [HttpPost("/api/roomkeys/v1/delete")] public ContentResult DeleteKeyRoute(long roomKeyId = 0) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; if (roomKeyId == 0) roomKeyId = L(F(), "RoomKeyId", "roomKeyId", "id"); var k = GetKey(roomKeyId); if (k == null) return J(Envelope(null, "No such key"), 404); if (!CanEdit(k.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room"), 403); UpdateKey(roomKeyId, x => x.Deleted = true); return J(Envelope(null)); } // Owner hands keys out / takes them back (form or JSON: RoomKeyId + AccountIds). [HttpPost("/api/roomkeys/v1/awardbulk")] public ContentResult AwardKeys() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); long keyId = L(f, "RoomKeyId", "roomKeyId", "id"); var k = GetKey(keyId); if (k == null) return J(Envelope(null, "No such key"), 404); if (!CanEdit(k.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room"), 403); var ids = RoomModeration.ULongs(f, "AccountIds", "accountIds", "PlayerIds", "playerIds", "AccountId", "accountId"); int n = 0; foreach (var pid in ids) if (GrantKey(pid, k)) n++; Console.WriteLine($"[roomkeys] {account.Id} awarded key #{keyId} to {n}/{ids.Count} player(s)"); return J(Envelope(ids.Select(pid => new { AccountId = pid, RoomKeyId = keyId, Success = true, Error = (string?)null }).ToList())); } [HttpPost("/api/roomkeys/v1/revoke")] public ContentResult RevokeKeys() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); long keyId = L(f, "RoomKeyId", "roomKeyId", "id"); var k = GetKey(keyId); if (k == null) return J(Envelope(null, "No such key"), 404); if (!CanEdit(k.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room"), 403); var ids = RoomModeration.ULongs(f, "AccountIds", "accountIds", "PlayerIds", "playerIds", "AccountId", "accountId"); foreach (var pid in ids) RevokeKey(pid, keyId); return J(Envelope(null)); } // Buy a key with tokens ({RoomKeyId, RequestedPrice}) or with the room currency it is priced in. [HttpPost("/api/storefronts/v1/buyRoomKey")] [HttpPost("/api/storefronts/v1/PurchaseRoomKeyWithCurrency")] [HttpPost("/api/roomkeys/v1/purchase")] public ContentResult BuyKey() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); Console.WriteLine($"[roomkeys] {account.Id} buy {Request.Path}: {RoomModeration.Dump(f)}"); long keyId = L(f, "RoomKeyId", "roomKeyId", "id", "Id"); var k = GetKey(keyId); if (k == null) return Refuse("No such key"); if (OwnsKey(account.Id, keyId)) return Refuse("You already own this key"); long requested = L(f, "RequestedPrice", "requestedPrice", "Price"); if (requested > 0 && requested != k.Price) Console.WriteLine($"[roomkeys] client rendered {requested} for key #{keyId}, charging the stored {k.Price}"); if (k.PurchaseCurrencyId.HasValue) { var cur = GetCurrency(k.PurchaseCurrencyId.Value); if (cur == null) return Refuse("This key's currency no longer exists"); var left = AddCurrency(account.Id, cur, -k.Price); if (left == null) return Refuse($"Not enough {cur.Name}"); GrantKey(account.Id, k); Console.WriteLine($"[roomkeys] @{account.Username} bought key #{keyId} '{k.Name}' for {k.Price} {cur.Name} -> {left}"); return J(new { Value = new { Balance = left.Value, CurrencyId = cur.CurrencyId, CurrencyType = 300, RoomKey = KeyDto(k) }, Success = true, Error = (string?)null, error_id = (string?)null, Balance = left.Value, CurrencyType = 300, Platform = Classes.GameCurrency.AllPlatformsBucket, BalanceUpdates = new[] { new { UpdateResponse = 0, Data = KeyDto(k) } } }); } long balance = PlayerDB.GetBalance(account.Id, Tokens); if (k.Price > 0 && balance < k.Price) return Refuse("Not enough tokens"); if (k.Price > 0) balance = PlayerDB.AddBalance(account.Id, Tokens, -k.Price); GrantKey(account.Id, k); EarningsDB.Record(k.CreatorId != 0 ? k.CreatorId : RoomCreator(k.RoomId), account.Id, "roomkey", k.RoomKeyId.ToString(), k.Name, Tokens, k.Price); GiftBoxes.PushBalance(account.Id, Tokens, balance, "room key"); Console.WriteLine($"[roomkeys] @{account.Username} bought key #{keyId} '{k.Name}' for {k.Price} tokens -> {balance}"); return J(BalanceAnswer(balance, KeyDto(k))); } // ======================================================================= CURRENCIES [HttpGet("/api/roomcurrencies/v1/currencies")] public ContentResult CurrenciesOfRoom([FromQuery] long roomId) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; return J(CurrenciesForRoom(roomId).Select(CurrencyDto).ToList()); } // 7 form fields: RoomId, Name, Description, Limit, Shape, Color, ImageName -> ENVELOPED. [HttpPost("/api/roomcurrencies/v1/createCurrency")] public ContentResult CreateCurrencyRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); Console.WriteLine($"[roomcurrency] {account.Id} create: {RoomModeration.Dump(f)}"); long roomId = L(f, "RoomId", "roomId"); if (roomId <= 0) return J(Envelope(null, "RoomId is required")); if (!CanEdit(roomId, account.Id)) return J(Envelope(null, "You cannot edit this room")); var c = CreateCurrency(new RoomCurrency { RoomId = roomId, CreatorId = RoomCreator(roomId), Name = (S(f, "Name", "name") ?? "Coins").Trim(), Description = S(f, "Description", "description") ?? "", Limit = f.ContainsKey("Limit") || f.ContainsKey("limit") ? L(f, "Limit", "limit") : 1_000_000, Shape = (int)L(f, "Shape", "shape"), Color = f.ContainsKey("Color") || f.ContainsKey("color") ? (int)L(f, "Color", "color") : -1, ImageName = S(f, "ImageName", "imageName") }); Console.WriteLine($"[roomcurrency] room {roomId}: currency {c.CurrencyId} '{c.Name}'"); return J(Envelope(CurrencyDto(c))); } [HttpPost("/api/roomcurrencies/v1/updateCurrency")] [HttpPut("/api/roomcurrencies/v1/updateCurrency")] public ContentResult UpdateCurrencyRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); var id = G(f, "CurrencyId", "currencyId", "id"); var c = id.HasValue ? GetCurrency(id.Value) : null; if (c == null) return J(Envelope(null, "No such currency")); if (!CanEdit(c.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room")); Console.WriteLine($"[roomcurrency] {account.Id} update {id}: {RoomModeration.Dump(f)}"); c = UpdateCurrency(id!.Value, x => { if (S(f, "Name", "name") is string n && n.Trim().Length > 0) x.Name = n.Trim(); if (S(f, "Description", "description") is string d) x.Description = d; if (f.ContainsKey("Limit") || f.ContainsKey("limit")) x.Limit = L(f, "Limit", "limit"); if (f.ContainsKey("Shape") || f.ContainsKey("shape")) x.Shape = (int)L(f, "Shape", "shape"); if (f.ContainsKey("Color") || f.ContainsKey("color")) x.Color = (int)L(f, "Color", "color"); if (S(f, "ImageName", "imageName") is string img) x.ImageName = img; })!; return J(Envelope(CurrencyDto(c))); } [HttpPost("/api/roomcurrencies/v1/deleteCurrency")] [HttpDelete("/api/roomcurrencies/v1/deleteCurrency")] public ContentResult DeleteCurrencyRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); var id = G(f, "CurrencyId", "currencyId", "id"); var c = id.HasValue ? GetCurrency(id.Value) : null; if (c == null) return J(Envelope(null, "No such currency")); if (!CanEdit(c.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room")); UpdateCurrency(id!.Value, x => x.Deleted = true); return J(Envelope(null)); } [HttpGet("/api/roomcurrencies/v1/getBalance")] [HttpPost("/api/roomcurrencies/v1/getBalance")] public ContentResult GetBalanceRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); var id = G(f, "CurrencyId", "currencyId", "id"); ulong who = RoomModeration.ULong(f, "AccountId", "accountId", "playerId"); if (who == 0) who = account.Id; long bal = id.HasValue ? GetCurrencyBalance(who, id.Value) : 0; return J(new { CurrencyId = id, AccountId = who, Balance = bal, Amount = bal }); } [HttpGet("/api/roomcurrencies/v1/getAllBalances")] public ContentResult GetAllBalancesRoute([FromQuery] long roomId) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var list = CurrenciesForRoom(roomId).Select(c => new { c.CurrencyId, AccountId = account.Id, RoomId = c.RoomId, Balance = GetCurrencyBalance(account.Id, c.CurrencyId), Amount = GetCurrencyBalance(account.Id, c.CurrencyId) }).ToList(); return J(list); } // JSON array [{AccountId, CurrencyId, Amount}] -> per-award results (circuits / owner grants). [HttpPost("/api/roomcurrencies/v1/awardCurrency/bulk")] public async Task AwardCurrencyBulk() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; string raw = ""; try { using var r = new StreamReader(Request.Body); raw = await r.ReadToEndAsync(); } catch { } var results = new List(); try { var tok = Newtonsoft.Json.Linq.JToken.Parse(string.IsNullOrWhiteSpace(raw) ? "[]" : raw); var arr = tok as Newtonsoft.Json.Linq.JArray ?? (tok["Awards"] ?? tok["awards"] ?? tok["Requests"]) as Newtonsoft.Json.Linq.JArray ?? new Newtonsoft.Json.Linq.JArray(); foreach (var a in arr) { ulong pid = (ulong?)a["AccountId"] ?? (ulong?)a["accountId"] ?? (ulong?)a["PlayerId"] ?? 0; Guid.TryParse((string?)a["CurrencyId"] ?? (string?)a["currencyId"] ?? "", out var cid); long amount = (long?)a["Amount"] ?? (long?)a["amount"] ?? (long?)a["CurrencyAmount"] ?? 0; var cur = cid != Guid.Empty ? GetCurrency(cid) : null; if (cur == null || pid == 0) { results.Add(new { AccountId = pid, CurrencyId = cid, Success = false, Error = "No such currency", ResultingBalance = 0L }); continue; } // The room's owners (and the room's own circuits, which run as a player in it) may award. var hb = HeartbeatDB.GetPlayerHeartbeat(account.Id); bool inRoom = hb?.roomInstance != null && hb.roomInstance.roomId == cur.RoomId; if (!CanEdit(cur.RoomId, account.Id) && !inRoom) { results.Add(new { AccountId = pid, CurrencyId = cid, Success = false, Error = "Not allowed", ResultingBalance = 0L }); continue; } var nb = AddCurrency(pid, cur, amount); results.Add(new { AccountId = pid, CurrencyId = cid, Success = nb != null, Error = nb == null ? "Insufficient balance" : (string?)null, ResultingBalance = nb ?? GetCurrencyBalance(pid, cid) }); } } catch (Exception ex) { Console.WriteLine($"[roomcurrency] award parse failed: {ex.Message} body={raw[..Math.Min(200, raw.Length)]}"); } return J(results); } [HttpPost("/api/roomcurrencies/v1/createPurchaseOffer")] public ContentResult CreateOfferRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); Console.WriteLine($"[roomcurrency] {account.Id} create offer: {RoomModeration.Dump(f)}"); var cid = G(f, "CurrencyId", "currencyId"); var cur = cid.HasValue ? GetCurrency(cid.Value) : null; if (cur == null) return J(Envelope(null, "No such currency")); if (!CanEdit(cur.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room")); var o = CreateOffer(new CurrencyOffer { CurrencyId = cur.CurrencyId, RoomId = cur.RoomId, Order = (int)L(f, "Order", "order"), Name = (S(f, "Name", "name") ?? $"{cur.Name} pack").Trim(), CurrencyAmount = Math.Max(0, L(f, "CurrencyAmount", "currencyAmount", "Amount")), Price = Math.Max(0, L(f, "Price", "price")) }); return J(Envelope(OfferDto(o))); } [HttpPost("/api/roomcurrencies/v1/updatePurchaseOffer")] [HttpPut("/api/roomcurrencies/v1/updatePurchaseOffer")] public ContentResult UpdateOfferRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); var oid = G(f, "CurrencyPurchaseOfferId", "currencyPurchaseOfferId", "OfferId", "id"); var o = oid.HasValue ? GetOffer(oid.Value) : null; if (o == null) return J(Envelope(null, "No such offer")); if (!CanEdit(o.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room")); Console.WriteLine($"[roomcurrency] {account.Id} update offer {oid}: {RoomModeration.Dump(f)}"); o = UpdateOffer(oid!.Value, x => { if (S(f, "Name", "name") is string n && n.Trim().Length > 0) x.Name = n.Trim(); if (f.ContainsKey("Order") || f.ContainsKey("order")) x.Order = (int)L(f, "Order", "order"); if (f.ContainsKey("CurrencyAmount") || f.ContainsKey("currencyAmount")) x.CurrencyAmount = Math.Max(0, L(f, "CurrencyAmount", "currencyAmount")); if (f.ContainsKey("Price") || f.ContainsKey("price")) x.Price = Math.Max(0, L(f, "Price", "price")); })!; return J(Envelope(OfferDto(o))); } [HttpPost("/api/roomcurrencies/v1/deletePurchaseOffer")] [HttpDelete("/api/roomcurrencies/v1/deletePurchaseOffer")] public ContentResult DeleteOfferRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); var oid = G(f, "CurrencyPurchaseOfferId", "currencyPurchaseOfferId", "OfferId", "id"); var o = oid.HasValue ? GetOffer(oid.Value) : null; if (o == null) return J(Envelope(null, "No such offer")); if (!CanEdit(o.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room")); UpdateOffer(oid!.Value, x => x.Deleted = true); return J(Envelope(null)); } // GET ?ids=&ids=... (<100) or POST form ids= (>=100). One element PER REQUESTED id. [HttpGet("/api/roomcurrencies/v1/getPurchaseOffersBatch")] [HttpPost("/api/roomcurrencies/v1/getPurchaseOffersBatch")] public ContentResult OffersBatch() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); var raw = (S(f, "ids", "Ids", "id", "currencyIds") ?? "").Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries); var list = new List(); foreach (var s in raw) { if (!Guid.TryParse(s.Trim(), out var cid)) continue; list.Add(new { CurrencyId = cid, PurchaseOffers = OffersFor(cid).Select(OfferDto).ToList() }); } return J(list); } // Buy a currency offer with tokens: {CurrencyPurchaseOfferId, RequestedPrice}. [HttpPost("/api/roomCurrencies/v2/purchase")] [HttpPost("/api/roomcurrencies/v1/purchase")] public ContentResult BuyCurrency() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); Console.WriteLine($"[roomcurrency] {account.Id} buy: {RoomModeration.Dump(f)}"); var oid = G(f, "CurrencyPurchaseOfferId", "currencyPurchaseOfferId", "OfferId", "offerId", "PurchaseOfferId", "id"); var o = oid.HasValue ? GetOffer(oid.Value) : null; if (o == null) return Refuse("No such offer"); var cur = GetCurrency(o.CurrencyId); if (cur == null) return Refuse("This currency no longer exists"); long requested = L(f, "RequestedPrice", "requestedPrice", "Price"); if (requested > 0 && requested != o.Price) Console.WriteLine($"[roomcurrency] client rendered {requested} for offer {oid}, charging the stored {o.Price}"); long balance = PlayerDB.GetBalance(account.Id, Tokens); if (o.Price > 0 && balance < o.Price) return Refuse("Not enough tokens"); if (o.Price > 0) balance = PlayerDB.AddBalance(account.Id, Tokens, -o.Price); long got = AddCurrency(account.Id, cur, o.CurrencyAmount) ?? GetCurrencyBalance(account.Id, cur.CurrencyId); EarningsDB.Record(cur.CreatorId != 0 ? cur.CreatorId : RoomCreator(cur.RoomId), account.Id, "roomcurrency", cur.CurrencyId.ToString(), $"{o.CurrencyAmount} {cur.Name}", Tokens, o.Price); GiftBoxes.PushBalance(account.Id, Tokens, balance, "room currency"); Console.WriteLine($"[roomcurrency] @{account.Username} bought {o.CurrencyAmount} {cur.Name} for {o.Price} tokens -> {balance} tokens, {got} {cur.Name}"); var data = new { CurrencyId = cur.CurrencyId, Balance = got, Amount = o.CurrencyAmount, PurchaseOffer = OfferDto(o), Currency = CurrencyDto(cur) }; return J(new { Value = new { Balance = balance, CurrencyType = Tokens, Platform = Classes.GameCurrency.AllPlatformsBucket, BalanceUpdates = new[] { new { UpdateResponse = 0, Data = data } }, CurrencyBalance = got, CurrencyId = cur.CurrencyId }, Success = true, Error = (string?)null, error_id = (string?)null, Balance = balance, CurrencyType = Tokens, Platform = Classes.GameCurrency.AllPlatformsBucket, BalanceType = Classes.GameCurrency.AllPlatformsBucket, BalanceUpdates = new[] { new { UpdateResponse = 0, Data = data } }, CurrencyBalance = got, CurrencyId = cur.CurrencyId }); } // ======================================================================= CONSUMABLES [HttpGet("/api/roomconsumables/v1/roomConsumable/room/{roomId:long}")] public ContentResult ConsumablesOfRoom(long roomId) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; return J(ConsumablesForRoom(roomId).Select(ConsumableDto).ToList()); } [HttpGet("/api/roomconsumables/v1/roomConsumable/room/{roomId:long}/me")] public ContentResult MyConsumablesInRoom(long roomId) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var list = ConsumablesOwnedInRoom(account.Id, roomId).Select(o => { var c = GetConsumable(o.RoomConsumableId); return new { o.RoomConsumableId, o.RoomId, AccountId = account.Id, PlayerId = account.Id, o.Count, Quantity = o.Count, ModifiedAt = o.ModifiedAt.ToString("o"), RoomConsumable = c != null ? ConsumableDto(c) : null }; }).ToList(); return J(list); } [HttpGet("/api/roomconsumables/v1/roomConsumable/{id:guid}")] public ContentResult GetConsumableRoute(Guid id) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var c = GetConsumable(id); return c == null ? J(Envelope(null, "No such consumable"), 404) : J(ConsumableDto(c)); } [HttpGet("/api/roomconsumables/v1/roomConsumable/{id:guid}/isOwned")] public ContentResult ConsumableOwned(Guid id) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return J(false); return J(ConsumableCount(account.Id, id) > 0); } // Create (PUT/POST roomConsumable) - form/JSON: RoomId, Name, Description, ImageName, Price, PurchaseCurrencyId, MaximumCountPerPurchase. [HttpPut("/api/roomconsumables/v1/roomConsumable")] [HttpPost("/api/roomconsumables/v1/roomConsumable")] [HttpPut("/roomconsumables/v1/roomConsumable")] [HttpPost("/api/roomconsumables/v1/roomConsumable/create")] public ContentResult CreateConsumableRoute() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); Console.WriteLine($"[roomconsumable] {account.Id} create: {RoomModeration.Dump(f)}"); long roomId = L(f, "RoomId", "roomId"); if (roomId <= 0) return J(Envelope(null, "RoomId is required"), 400); if (!CanEdit(roomId, account.Id)) return J(Envelope(null, "You cannot edit this room"), 403); var c = CreateConsumable(new RoomConsumable { RoomId = roomId, CreatorId = RoomCreator(roomId), Name = (S(f, "Name", "name") ?? "Consumable").Trim(), Description = S(f, "Description", "description") ?? "", ImageName = S(f, "ImageName", "imageName"), Price = Math.Max(0, L(f, "Price", "price")), PurchaseCurrencyId = G(f, "PurchaseCurrencyId", "purchaseCurrencyId"), MaximumCountPerPurchase = f.ContainsKey("MaximumCountPerPurchase") || f.ContainsKey("maximumCountPerPurchase") ? (int)Math.Max(1, L(f, "MaximumCountPerPurchase", "maximumCountPerPurchase")) : 10 }); var dto = ConsumableDto(c); return J(new { Value = dto, Success = true, Error = (string?)null, error_id = (string?)null, c.RoomConsumableId, c.RoomId, c.Name, c.Description, ImageName = c.ImageName ?? "", c.Price, c.PurchaseCurrencyId, ModifiedAt = c.ModifiedAt.ToString("o"), c.MaximumCountPerPurchase }); } [HttpPut("/api/roomconsumables/v1/roomConsumable/{id:guid}")] [HttpPost("/api/roomconsumables/v1/roomConsumable/{id:guid}")] [HttpPost("/api/roomconsumables/v1/roomConsumable/{id:guid}/update")] public ContentResult UpdateConsumableRoute(Guid id) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var c = GetConsumable(id); if (c == null) return J(Envelope(null, "No such consumable"), 404); if (!CanEdit(c.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room"), 403); var f = F(); Console.WriteLine($"[roomconsumable] {account.Id} update {id}: {RoomModeration.Dump(f)}"); c = UpdateConsumable(id, x => { if (S(f, "Name", "name") is string n && n.Trim().Length > 0) x.Name = n.Trim(); if (S(f, "Description", "description") is string d) x.Description = d; if (S(f, "ImageName", "imageName") is string img) x.ImageName = img; if (f.ContainsKey("Price") || f.ContainsKey("price")) x.Price = Math.Max(0, L(f, "Price", "price")); if (f.ContainsKey("PurchaseCurrencyId") || f.ContainsKey("purchaseCurrencyId")) x.PurchaseCurrencyId = G(f, "PurchaseCurrencyId", "purchaseCurrencyId"); if (f.ContainsKey("MaximumCountPerPurchase") || f.ContainsKey("maximumCountPerPurchase")) x.MaximumCountPerPurchase = (int)Math.Max(1, L(f, "MaximumCountPerPurchase", "maximumCountPerPurchase")); })!; return J(Envelope(ConsumableDto(c))); } [HttpDelete("/api/roomconsumables/v1/roomConsumable/{id:guid}")] [HttpPost("/api/roomconsumables/v1/roomConsumable/{id:guid}/delete")] public ContentResult DeleteConsumableRoute(Guid id) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var c = GetConsumable(id); if (c == null) return J(Envelope(null, "No such consumable"), 404); if (!CanEdit(c.RoomId, account.Id)) return J(Envelope(null, "You cannot edit this room"), 403); UpdateConsumable(id, x => x.Deleted = true); return J(Envelope(null)); } // Use one (or Count) - the room's circuits consume on the player's behalf. [HttpPost("/api/roomconsumables/v1/roomConsumable/{id:guid}/consume")] public ContentResult ConsumeRoute(Guid id) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var c = GetConsumable(id); if (c == null) return J(Envelope(null, "No such consumable"), 404); var f = F(); int n = (int)Math.Max(1, L(f, "Count", "count", "Amount", "amount")); var left = AddConsumable(account.Id, c, -n); if (left == null) return J(Envelope(null, "You do not have enough of that")); return J(new { Value = new { RoomConsumableId = id, Count = left.Value, Quantity = left.Value }, Success = true, Error = (string?)null, error_id = (string?)null, RoomConsumableId = id, Count = left.Value }); } [HttpPost("/api/roomconsumables/v1/roomConsumable/awardBulk")] public async Task AwardConsumables() { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; string raw = ""; try { using var r = new StreamReader(Request.Body); raw = await r.ReadToEndAsync(); } catch { } var results = new List(); try { var tok = Newtonsoft.Json.Linq.JToken.Parse(string.IsNullOrWhiteSpace(raw) ? "[]" : raw); var arr = tok as Newtonsoft.Json.Linq.JArray ?? (tok["Awards"] ?? tok["awards"]) as Newtonsoft.Json.Linq.JArray ?? new Newtonsoft.Json.Linq.JArray(); foreach (var a in arr) { ulong pid = (ulong?)a["AccountId"] ?? (ulong?)a["accountId"] ?? (ulong?)a["PlayerId"] ?? 0; Guid.TryParse((string?)a["RoomConsumableId"] ?? (string?)a["roomConsumableId"] ?? "", out var cid); int count = (int?)a["Count"] ?? (int?)a["count"] ?? (int?)a["Amount"] ?? 1; var c = cid != Guid.Empty ? GetConsumable(cid) : null; if (c == null || pid == 0) { results.Add(new { AccountId = pid, RoomConsumableId = cid, Success = false, Error = "No such consumable" }); continue; } var hb = HeartbeatDB.GetPlayerHeartbeat(account.Id); bool inRoom = hb?.roomInstance != null && hb.roomInstance.roomId == c.RoomId; if (!CanEdit(c.RoomId, account.Id) && !inRoom) { results.Add(new { AccountId = pid, RoomConsumableId = cid, Success = false, Error = "Not allowed" }); continue; } var nc = AddConsumable(pid, c, count); results.Add(new { AccountId = pid, RoomConsumableId = cid, Success = nc != null, Error = (string?)null, Count = nc ?? 0 }); } } catch (Exception ex) { Console.WriteLine($"[roomconsumable] award parse failed: {ex.Message}"); } return J(results); } // Buy with tokens or with the room currency the consumable is priced in: {Count, RequestedPrice}. [HttpPost("/api/roomconsumables/v1/roomconsumable/{id:guid}/purchase/tokens")] [HttpPost("/api/roomconsumables/v1/roomconsumable/{id:guid}/purchase/currency")] [HttpPost("/api/roomconsumables/v1/roomConsumable/{id:guid}/purchase")] public ContentResult BuyConsumable(Guid id) { var (account, error) = AuthHelper.ValidateToken(HttpContext); if (error != null) return error; var f = F(); Console.WriteLine($"[roomconsumable] {account.Id} buy {Request.Path}: {RoomModeration.Dump(f)}"); var c = GetConsumable(id); if (c == null) return Refuse("No such consumable"); int count = (int)Math.Max(1, L(f, "Count", "count", "Amount", "amount", "Quantity")); if (c.MaximumCountPerPurchase > 0 && count > c.MaximumCountPerPurchase) count = c.MaximumCountPerPurchase; long unit = Math.Max(0, c.Price); long total = unit * count; long requested = L(f, "RequestedPrice", "requestedPrice", "Price"); if (requested > 0 && requested != unit && requested != total) Console.WriteLine($"[roomconsumable] client rendered {requested}, charging {total} ({unit} x {count})"); bool withCurrency = Request.Path.Value!.EndsWith("/currency", StringComparison.OrdinalIgnoreCase) || (c.PurchaseCurrencyId.HasValue && !Request.Path.Value.EndsWith("/tokens", StringComparison.OrdinalIgnoreCase)); if (withCurrency && c.PurchaseCurrencyId.HasValue) { var cur = GetCurrency(c.PurchaseCurrencyId.Value); if (cur == null) return Refuse("This item's currency no longer exists"); var left = AddCurrency(account.Id, cur, -total); if (left == null) return Refuse($"Not enough {cur.Name}"); int have = AddConsumable(account.Id, c, count) ?? 0; Console.WriteLine($"[roomconsumable] @{account.Username} bought {count}x '{c.Name}' for {total} {cur.Name} -> {left}, has {have}"); var d = new { RoomConsumableId = id, Count = have, Quantity = have, RoomConsumable = ConsumableDto(c) }; return J(new { Value = new { Balance = left.Value, CurrencyId = cur.CurrencyId, CurrencyType = 300, BalanceUpdates = new[] { new { UpdateResponse = 0, Data = d } } }, Success = true, Error = (string?)null, error_id = (string?)null, Balance = left.Value, CurrencyType = 300, CurrencyId = cur.CurrencyId, Platform = Classes.GameCurrency.AllPlatformsBucket, BalanceUpdates = new[] { new { UpdateResponse = 0, Data = d } } }); } long balance = PlayerDB.GetBalance(account.Id, Tokens); if (total > 0 && balance < total) return Refuse("Not enough tokens"); if (total > 0) balance = PlayerDB.AddBalance(account.Id, Tokens, -total); int now = AddConsumable(account.Id, c, count) ?? 0; EarningsDB.Record(c.CreatorId != 0 ? c.CreatorId : RoomCreator(c.RoomId), account.Id, "roomconsumable", c.RoomConsumableId.ToString(), c.Name, Tokens, total); GiftBoxes.PushBalance(account.Id, Tokens, balance, "room consumable"); Console.WriteLine($"[roomconsumable] @{account.Username} bought {count}x '{c.Name}' for {total} tokens -> {balance}, has {now}"); return J(BalanceAnswer(balance, new { RoomConsumableId = id, Count = now, Quantity = now, RoomConsumable = ConsumableDto(c) })); } } }