Files
Campus-25/server/Server/2025-RecRoom-Server-E12354-main/Controllers/RoomEconController.cs
T
2026-10-14 06:59:51 +02:00

686 lines
41 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
[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<string, string> F() => RoomModeration.Fields(Request);
private static long L(Dictionary<string, string> f, params string[] n) => RoomModeration.Long(f, n);
private static string? S(Dictionary<string, string> f, params string[] n) => RoomModeration.Str(f, n);
private static Guid? G(Dictionary<string, string> 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<object>() }, 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<ContentResult> 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<object>();
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=<guid>&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<object>();
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<ContentResult> 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<object>();
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) }));
}
}
}