the files
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
using Newtonsoft.Json;
|
||||
using LiteDB;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class ActionLink
|
||||
{
|
||||
[BsonId]
|
||||
[JsonIgnore]
|
||||
public string LinkId { get; set; } = Utils.GenerateShortCode(6);
|
||||
public ulong creatorPlayerId { get; set; }
|
||||
public string? data { get; set; }
|
||||
public bool isValid { get; set; }
|
||||
[JsonIgnore]
|
||||
public LinkType CodeType { get; set; }
|
||||
[JsonIgnore]
|
||||
public int ValidHours { get; set; }
|
||||
[JsonIgnore]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
[JsonIgnore]
|
||||
public DateTime ExpiresAt => CreatedAt.AddHours(ValidHours);
|
||||
[JsonIgnore]
|
||||
public int ExtraDataId { get; set; }
|
||||
}
|
||||
|
||||
public enum LinkType
|
||||
{
|
||||
Unknown = -1,
|
||||
Friend, // 0
|
||||
Referral, // 1
|
||||
Meetup, // 2
|
||||
Club, // 3
|
||||
PlayerEvent, // 4
|
||||
Room, // 5
|
||||
Influencer, // 6
|
||||
Photo // 7
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class Amplitude2023
|
||||
{
|
||||
public string? AmplitudeKey { get; set; }
|
||||
public bool? UseRudderStack { get; set; }
|
||||
public string? RudderStackKey { get; set; }
|
||||
public bool? UseStatSig { get; set; }
|
||||
public string? StatSigKey { get; set; }
|
||||
public int? StatSigEnvironment { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class Config
|
||||
{
|
||||
public string MessageOfTheDay { get; set; }
|
||||
public string CdnBaseUri { get; set; }
|
||||
public string ShareBaseUrl { get; set; }
|
||||
public List<LevelProgressionMap> LevelProgressionMaps { get; set; }
|
||||
public MatchmakingParams MatchmakingParams { get; set; }
|
||||
public List<List<DailyObjective>> DailyObjectives { get; set; }
|
||||
public ServerMaintenance ServerMaintainence { get; set; }
|
||||
public AutoMicMutingConfig AutoMicMutingConfig { get; set; }
|
||||
public StorefrontConfig StorefrontConfig { get; set; }
|
||||
public RoomKeyConfig RoomKeyConfig { get; set; }
|
||||
public RoomCurrencyConfig RoomCurrencyConfig { get; set; }
|
||||
public List<ConfigTableEntry> ConfigTable { get; set; }
|
||||
public PhotonConfig PhotonConfig { get; set; }
|
||||
}
|
||||
|
||||
public class LevelProgressionMap
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public int RequiredXp { get; set; }
|
||||
public int? GiftDropId { get; set; }
|
||||
}
|
||||
|
||||
public class MatchmakingParams
|
||||
{
|
||||
public double PreferFullRoomsFrequency { get; set; }
|
||||
public double PreferEmptyRoomsFrequency { get; set; }
|
||||
}
|
||||
|
||||
public class DailyObjective
|
||||
{
|
||||
public int Type { get; set; }
|
||||
public int Score { get; set; }
|
||||
public int Xp { get; set; }
|
||||
}
|
||||
|
||||
public class ServerMaintenance
|
||||
{
|
||||
public int StartsInMinutes { get; set; }
|
||||
}
|
||||
|
||||
public class AutoMicMutingConfig
|
||||
{
|
||||
public double MicSpamVolumeThreshold { get; set; }
|
||||
public double MicVolumeSampleInterval { get; set; }
|
||||
public double MicVolumeSampleRollingWindowLength { get; set; }
|
||||
public double MicSpamSamplePercentageForWarning { get; set; }
|
||||
public double MicSpamSamplePercentageForWarningToEnd { get; set; }
|
||||
public double MicSpamSamplePercentageForForceMute { get; set; }
|
||||
public double MicSpamSamplePercentageForForceMuteToEnd { get; set; }
|
||||
public double MicSpamWarningStateVolumeMultiplier { get; set; }
|
||||
}
|
||||
|
||||
public class StorefrontConfig
|
||||
{
|
||||
public int MinPlayerLevelForGifting { get; set; }
|
||||
}
|
||||
|
||||
public class RoomKeyConfig
|
||||
{
|
||||
public int MaxKeysPerRoom { get; set; }
|
||||
}
|
||||
|
||||
public class RoomCurrencyConfig
|
||||
{
|
||||
public double AwardCurrencyCooldownSeconds { get; set; }
|
||||
}
|
||||
|
||||
public class ConfigTableEntry
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class PhotonConfig
|
||||
{
|
||||
public string CloudRegion { get; set; }
|
||||
public bool CrcCheckEnabled { get; set; }
|
||||
public bool EnableServerTracingAfterDisconnect { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
/// <summary>
|
||||
/// Currency identifiers, taken from the client's own enum (il2cpp dump, 2026-08-24).
|
||||
///
|
||||
/// These are NOT arbitrary - the value is the path segment the client uses when
|
||||
/// asking for a balance, e.g. /api/storefronts/v4/balance/2 is RecCenterTokens and
|
||||
/// /api/storefronts/v4/balance/500 is RoomieCredits. Both appear in every session log.
|
||||
///
|
||||
/// Kept as a real enum so reward/balance code stops passing bare ints around.
|
||||
/// NOTE: this is the BALANCES enum. Game rewards use a different one.
|
||||
/// </summary>
|
||||
public enum CurrencyType
|
||||
{
|
||||
Invalid = 0,
|
||||
LaserTagTickets = 1,
|
||||
RecCenterTokens = 2,
|
||||
LostSkullsGold = 100,
|
||||
DraculaSilver = 101,
|
||||
RecRoyale_Season1 = 200,
|
||||
RoomCurrency = 300,
|
||||
RoomInventoryItem = 301,
|
||||
ProgressionEvent = 400,
|
||||
RoomieCredits = 500,
|
||||
PaintballPaint = 600,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class CustomAvatarItems
|
||||
{
|
||||
public List<object>? Results { get; set; }
|
||||
public int TotalResults { get; set; }
|
||||
}
|
||||
|
||||
public class CreateCustomAvatarItemResponse
|
||||
{
|
||||
// {"name":"eeeeeeeeeeeeeeeeeeeee","Description":"eawwwwwwwwwwweweawewaewa","Price":1000,"BaseAvatarItemId":2184,"BaseAvatarItemColor":"#F55C1A","Accessibility":1}
|
||||
public string? Name { get; set; } // Custom shirt name
|
||||
public string? Description { get; set; } // Custom shirt description
|
||||
public int Price { get; set; } // This range from 1000 to 10,000
|
||||
public int BaseAvatarItemId { get; set; } // the avatar id from the avatar db
|
||||
public string? BaseAvatarItemColor { get; set; } // Hex color code default is #F55C1A
|
||||
public int Accessibility { get; set; } // enum 0 for me only, 1 for everyone
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
/// <summary>
|
||||
/// Which currency a game mode pays out in.
|
||||
///
|
||||
/// The client asks /api/storefronts/v2/balance to add currency and NAMES the currency
|
||||
/// itself. Trusting that is how a completed Golden Trophy match paid out 1000 Dracula
|
||||
/// Silver - Crescendo's currency. The room the player is actually in decides now; the
|
||||
/// client's CurrencyType is only a fallback when the room is unknown.
|
||||
///
|
||||
/// Room names come from the live server (Room_server/rooms/search).
|
||||
/// </summary>
|
||||
public static class GameCurrency
|
||||
{
|
||||
/// <summary>
|
||||
/// The one balance bucket this server keeps per currency, on the wire as "Platform"
|
||||
/// (BalancePlatform.NonPurchasedNotUsableInP2P = -2, the reference's ALL_PLATFORMS). The
|
||||
/// client holds a balance per (CurrencyType, Platform) and SUMS them, so every balance
|
||||
/// frame - the GET, a purchase response, a balance add, the 61 push - has to name this
|
||||
/// same bucket; one that says 0 (SteamPurchased) makes a phantom bucket that is added to
|
||||
/// the real one for the rest of the session.
|
||||
/// </summary>
|
||||
public const int AllPlatformsBucket = -2;
|
||||
|
||||
/// <summary>Currency for a room name, or null when the room has no special currency.</summary>
|
||||
public static CurrencyType? ForRoomName(string? roomName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(roomName)) return null;
|
||||
|
||||
// Room instance names arrive as "^RoomName".
|
||||
string n = roomName.TrimStart('^').Trim();
|
||||
|
||||
if (n.Equals("Crescendo", StringComparison.OrdinalIgnoreCase))
|
||||
return CurrencyType.DraculaSilver;
|
||||
|
||||
if (n.Equals("IsleOfLostSkulls", StringComparison.OrdinalIgnoreCase))
|
||||
return CurrencyType.LostSkullsGold;
|
||||
|
||||
if (n.StartsWith("RecRoyale", StringComparison.OrdinalIgnoreCase))
|
||||
return CurrencyType.RecRoyale_Season1;
|
||||
|
||||
if (n.StartsWith("Paintball", StringComparison.OrdinalIgnoreCase))
|
||||
return CurrencyType.PaintballPaint;
|
||||
|
||||
if (n.Equals("LaserTag", StringComparison.OrdinalIgnoreCase))
|
||||
return CurrencyType.LaserTagTickets;
|
||||
|
||||
// GoldenTrophy and everything else pay in Rec Center Tokens - which is what the
|
||||
// Golden Trophy storefront (StorefrontType 102) actually prices its items in.
|
||||
return CurrencyType.RecCenterTokens;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// STRICT balance-room whitelist: the ONLY rooms in which POST v2/balance may mint
|
||||
/// currency, and which currency each mints. Anything not listed returns null and mints
|
||||
/// nothing. Rec Center Tokens are deliberately absent - they are server/admin only.
|
||||
///
|
||||
/// Crescendo -> DraculaSilver (101)
|
||||
/// IsleOfLostSkulls -> LostSkullsGold (100)
|
||||
/// LaserTag -> LaserTagTickets (1)
|
||||
/// </summary>
|
||||
public static CurrencyType? ForBalanceRoom(string? roomName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(roomName)) return null;
|
||||
|
||||
// Room instance names arrive as "^RoomName".
|
||||
string n = roomName.TrimStart('^').Trim();
|
||||
|
||||
if (n.Equals("Crescendo", StringComparison.OrdinalIgnoreCase))
|
||||
return CurrencyType.DraculaSilver;
|
||||
|
||||
if (n.Equals("IsleOfLostSkulls", StringComparison.OrdinalIgnoreCase))
|
||||
return CurrencyType.LostSkullsGold;
|
||||
|
||||
if (n.Equals("LaserTag", StringComparison.OrdinalIgnoreCase))
|
||||
return CurrencyType.LaserTagTickets;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a currency may be minted by a PLAYER-facing endpoint (v2/balance, game rewards).
|
||||
/// Only the three room currencies qualify. Rec Center Tokens (and every other bucket) are
|
||||
/// NOT player-grantable - they can only be granted server-side / from the admin panel.
|
||||
/// </summary>
|
||||
public static bool IsPlayerGrantable(int currencyType)
|
||||
=> currencyType == (int)CurrencyType.DraculaSilver
|
||||
|| currencyType == (int)CurrencyType.LostSkullsGold
|
||||
|| currencyType == (int)CurrencyType.LaserTagTickets;
|
||||
|
||||
/// <summary>
|
||||
/// Most a single award may grant, per currency. 0 = no cap.
|
||||
///
|
||||
/// Crescendo pays per CHECKPOINT, and a checkpoint is worth 0..30,000 Silver -
|
||||
/// five checkpoints, so 150,000 is a full game. The cap is enforced per request so
|
||||
/// a bad (or forged) amount cannot mint a fortune in one call.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Whether a currency is an account balance a purchase can debit. Invalid (0) never is;
|
||||
/// everything else the enum defines is a real balance bucket. Used by bulkpurchase to
|
||||
/// reject a bag posted against a currency there is no wallet to spend from (400).
|
||||
/// </summary>
|
||||
public static bool IsSpendable(int currencyType)
|
||||
=> currencyType != (int)CurrencyType.Invalid
|
||||
&& Enum.IsDefined(typeof(CurrencyType), currencyType);
|
||||
|
||||
public static long MaxPerAward(CurrencyType currency)
|
||||
{
|
||||
switch (currency)
|
||||
{
|
||||
case CurrencyType.DraculaSilver: return 30000; // per Crescendo checkpoint
|
||||
default: return 0; // uncapped
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clamps an award to 0..MaxPerAward for that currency.</summary>
|
||||
public static long ClampAward(CurrencyType currency, long amount)
|
||||
{
|
||||
if (amount < 0) return 0;
|
||||
|
||||
long max = MaxPerAward(currency);
|
||||
return (max > 0 && amount > max) ? max : amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
/// <summary>
|
||||
/// RewardType values the client sends to /api/gamerewards/v1/request and
|
||||
/// /api/avatar/v3/gifts/generate (observed in real client traffic, 2026-08).
|
||||
/// Sent as the STRING name in the form body, e.g. "RewardType=PostGameActivity".
|
||||
/// This is a partial list - extend as more are observed.
|
||||
/// </summary>
|
||||
public enum GameRewardType
|
||||
{
|
||||
Unknown = 0,
|
||||
PostGameActivity, // end-of-game gift box
|
||||
ReferralReward, // referral / gift generate
|
||||
FirstActivityOfDay, // observed live: "rewardType=FirstActivityOfDay&Message=First Game of the Day"
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What each reward type pays, and whether it is limited to once per day.
|
||||
///
|
||||
/// The real grant table was never captured, so these amounts are our own - but the
|
||||
/// DAILY LIMIT is not cosmetic: FirstActivityOfDay is named for a once-a-day event
|
||||
/// and the client will happily request it again on the next activity. Without a gate
|
||||
/// it pays out every single time and is trivially farmed.
|
||||
/// </summary>
|
||||
public static class GameRewardRules
|
||||
{
|
||||
public static GameRewardType Parse(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return GameRewardType.Unknown;
|
||||
return Enum.TryParse<GameRewardType>(raw, ignoreCase: true, out var t)
|
||||
? t
|
||||
: GameRewardType.Unknown;
|
||||
}
|
||||
|
||||
/// <summary>Amount awarded, in the currency returned by CurrencyFor.</summary>
|
||||
public static long AmountFor(GameRewardType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case GameRewardType.FirstActivityOfDay: return 100;
|
||||
case GameRewardType.PostGameActivity: return 25;
|
||||
case GameRewardType.ReferralReward: return 250;
|
||||
default: return 10;
|
||||
}
|
||||
}
|
||||
|
||||
public static CurrencyType CurrencyFor(GameRewardType type) => CurrencyType.RecCenterTokens;
|
||||
|
||||
/// <summary>True when this reward may only be claimed once per calendar day (UTC).</summary>
|
||||
public static bool IsOncePerDay(GameRewardType type)
|
||||
=> type == GameRewardType.FirstActivityOfDay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GiftContext ids seen in client traffic. These identify WHICH gift box the reward
|
||||
/// is for; the id is game/activity specific. Kept for reference - the reward request
|
||||
/// carries the raw int, not this enum.
|
||||
/// 4003 Golden 4503 Jumbotron 4013 Crimson 4103 Isle 4206 Crescendo
|
||||
/// 5001 Charades 10001 Bowling 1200 RecRoyale 12000 RecRally 12100 Showdown
|
||||
/// 12200 MyLittleMonster 12300 RunTheBlock 11007 StuntRunner
|
||||
/// 3010/3011 DiscGolf 2010-2016 Paintball maps
|
||||
/// </summary>
|
||||
public static class GiftContextIds
|
||||
{
|
||||
public const int GoldenGiftBox = 4003;
|
||||
public const int JumbotronGiftBox = 4503;
|
||||
public const int CrimsonGiftBox = 4013;
|
||||
public const int IsleGiftBox = 4103;
|
||||
public const int CrescendoGiftBox = 4206;
|
||||
|
||||
/// <summary>
|
||||
/// Which currency a gift box pays out in.
|
||||
///
|
||||
/// Each box belongs to a game, and those games have their own currency: the Isle
|
||||
/// boxes pay Lost Skulls Gold, Crescendo pays Dracula Silver, Paintball pays
|
||||
/// Paint, and so on. Anything without a game-specific currency falls back to
|
||||
/// Rec Center Tokens.
|
||||
///
|
||||
/// This is a reasonable mapping rather than a captured one - rec.net's real
|
||||
/// payout table was never observed. It is easy to change: the amount is chosen
|
||||
/// in the admin panel, and this only decides WHICH currency it lands in.
|
||||
/// </summary>
|
||||
public static CurrencyType CurrencyForGiftBox(int giftContext)
|
||||
{
|
||||
switch (giftContext)
|
||||
{
|
||||
case 4103: // Isle
|
||||
return CurrencyType.LostSkullsGold;
|
||||
|
||||
case 4206: // Crescendo
|
||||
return CurrencyType.DraculaSilver;
|
||||
|
||||
case 1200: // Rec Royale
|
||||
return CurrencyType.RecRoyale_Season1;
|
||||
|
||||
case 2010: case 2011: case 2012: // Paintball maps
|
||||
case 2013: case 2014: case 2015:
|
||||
case 2016:
|
||||
return CurrencyType.PaintballPaint;
|
||||
|
||||
default:
|
||||
return CurrencyType.RecCenterTokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class Hile
|
||||
{
|
||||
public enum HileTypes
|
||||
{
|
||||
Obscured = 0,
|
||||
Time = 1,
|
||||
Inject = 2,
|
||||
GiftCount = 3,
|
||||
Engine = 4,
|
||||
UnknownDll = 5,
|
||||
ImageSignature = 6,
|
||||
AvatarHack = 7,
|
||||
|
||||
NetworkCertificatePublicKey = 100,
|
||||
NetworkCertificateIssuer = 101,
|
||||
NetworkCertificateMissing = 102,
|
||||
NetworkCertificateMismatch = 103,
|
||||
|
||||
AutosaveChecksumMismatch = 150,
|
||||
AutosaveSubRoomIdMismatch = 151,
|
||||
AutosaveChecksumException = 152,
|
||||
|
||||
Photon_MissingHash = 200,
|
||||
Photon_CorruptHash = 201,
|
||||
Photon_DifferentHash = 203,
|
||||
|
||||
AppData_Runtime_LengthMismatch = 300,
|
||||
AppData_Runtime_LastWriteTimeMismatch = 301,
|
||||
AppData_Runtime_FileModified = 302,
|
||||
|
||||
AppData_Boot_InvalidSignature = 310,
|
||||
AppData_Boot_UnableToVerifySignatures = 311,
|
||||
|
||||
Config_MissingHash = 320,
|
||||
Config_DifferentHash = 321,
|
||||
|
||||
Photon_InstantiateTool = 400,
|
||||
|
||||
Memory_Hash_Mismatch = 500,
|
||||
|
||||
Driver_Invalid_Signature = 600
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
// Which avatar items the CLIENT BUILD can actually render.
|
||||
//
|
||||
// The client carries its own item database. Hand it an item whose id isn't in there and
|
||||
// it has no asset, so it cannot work out the item's outfit type either - which is the
|
||||
// "None does not have an associated browsable avatar item filter" warning in Player.log.
|
||||
// The item then belongs to no wardrobe category and never appears while browsing, even
|
||||
// though it is present in the /api/avatar/v4/items response and findable by search.
|
||||
//
|
||||
// No server change makes such an item render. The only thing the server can usefully do
|
||||
// is stop listing items the client cannot draw.
|
||||
//
|
||||
// Data/APIS/items/ClientKnownGuids.json holds the ids, in the same shape the 2023 server
|
||||
// used: { "_comment": "...", "Guids": [ ... ] }.
|
||||
//
|
||||
// IF THE FILE IS ABSENT, NOTHING IS FILTERED. That is the default: the list shipped
|
||||
// alongside this was extracted from the 2023 build, and the 2025 client is a newer build
|
||||
// that almost certainly knows items 2023 never had. Filtering on the older list would
|
||||
// hide items that currently work. Turn it on deliberately, and compare.
|
||||
public static class ItemGuids
|
||||
{
|
||||
private static HashSet<string>? _known;
|
||||
private static readonly object _lock = new();
|
||||
|
||||
public static HashSet<string> KnownGuids()
|
||||
{
|
||||
if (_known != null) return _known;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_known != null) return _known;
|
||||
|
||||
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "items", "ClientKnownGuids.json");
|
||||
if (File.Exists(path))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (doc.RootElement.TryGetProperty("Guids", out var arr) && arr.ValueKind == JsonValueKind.Array)
|
||||
foreach (var g in arr.EnumerateArray())
|
||||
{
|
||||
var s = g.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
var n = NormalizeGuid(s);
|
||||
if (n != null) set.Add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[items] could not read ClientKnownGuids.json ({ex.Message}) - nothing will be filtered");
|
||||
}
|
||||
|
||||
_known = set;
|
||||
Console.WriteLine($"[items] client-known GUIDs loaded: {set.Count}" +
|
||||
(set.Count == 0 ? " (no filter file - nothing will be filtered)" : ""));
|
||||
return _known;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rec Room writes item ids two ways: a normal UUID, or a 22-character base64
|
||||
/// "short GUID". Both normalise to a plain lowercase UUID so they compare equal.
|
||||
/// </summary>
|
||||
public static string? NormalizeGuid(string? s)
|
||||
{
|
||||
s = (s ?? "").Trim();
|
||||
if (s.Length == 0) return null;
|
||||
|
||||
if (Guid.TryParse(s, out var g)) return g.ToString().ToLowerInvariant();
|
||||
|
||||
if (s.Length == 22)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(s.Replace('-', '+').Replace('_', '/') + "==");
|
||||
if (bytes.Length == 16) return new Guid(bytes).ToString().ToLowerInvariant();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the client can render this item. Always true when no filter file is
|
||||
/// present, so an absent list can never cost anyone their wardrobe.
|
||||
/// </summary>
|
||||
// ---- HiddenItemsStorefronts --------------------------------------------------------
|
||||
//
|
||||
// An ALLOWLIST: while the flag is on, a store lists an avatar item only if its base guid is
|
||||
// in Data/APIS/items/ShownItems.json ({ "Guids": [...] }) - or, when that file does not
|
||||
// exist, only if it is in the starter wardrobe (DefaultAvatarItems.json), which every avatar
|
||||
// on the server wears and which is known to render.
|
||||
//
|
||||
// ShownItems.json is generated from the reference server's legacy item catalog
|
||||
// (recflare apps/econ/static/db/avatar-items.json, 3,061 items / 977 base guids) - the
|
||||
// list its 2025 Watch store (sf3-2025.json) sells to this very client build, with nothing
|
||||
// but AvatarItemDesc on each row. Regenerate it from that file if the reference updates.
|
||||
//
|
||||
// History: the 2023 embedded-item list was the gate first, then a hand-grown "seen to
|
||||
// render" list, because items on the 2023 list still came up "Can't load item". That was
|
||||
// not the embedded set: the captured store rows carried AvatarItemInfo.DownloadableAvatarItemId,
|
||||
// which sends the client to download a CAI bundle instead of drawing the legacy item, and
|
||||
// that download always fails on this client. StoreGenerator.ShapeRow now strips it, so a
|
||||
// legacy item on this list renders the way it does on the reference.
|
||||
// ShownItems.json is re-read when it changes.
|
||||
private static HashSet<string>? _shown;
|
||||
private static string _shownStamp = "";
|
||||
|
||||
private static HashSet<string> ShownGuids()
|
||||
{
|
||||
string shownPath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "items", "ShownItems.json");
|
||||
string defaultsPath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "items", "DefaultAvatarItems.json");
|
||||
string stamp = (File.Exists(shownPath) ? File.GetLastWriteTimeUtc(shownPath).Ticks.ToString() : "-")
|
||||
+ "|" + (File.Exists(defaultsPath) ? File.GetLastWriteTimeUtc(defaultsPath).Ticks.ToString() : "-");
|
||||
if (_shown != null && stamp == _shownStamp) return _shown;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_shown != null && stamp == _shownStamp) return _shown;
|
||||
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
string source = "nothing";
|
||||
try
|
||||
{
|
||||
if (File.Exists(shownPath))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(shownPath));
|
||||
if (doc.RootElement.TryGetProperty("Guids", out var arr) && arr.ValueKind == JsonValueKind.Array)
|
||||
foreach (var g in arr.EnumerateArray())
|
||||
{
|
||||
var n = NormalizeGuid((g.GetString() ?? "").Split(',')[0]);
|
||||
if (n != null) set.Add(n);
|
||||
}
|
||||
source = "ShownItems.json";
|
||||
}
|
||||
else if (File.Exists(defaultsPath))
|
||||
{
|
||||
var arr = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>>(File.ReadAllText(defaultsPath)) ?? new();
|
||||
foreach (var d in arr)
|
||||
{
|
||||
var n = NormalizeGuid((d ?? "").Split(',')[0]);
|
||||
if (n != null) set.Add(n);
|
||||
}
|
||||
source = "DefaultAvatarItems.json (no ShownItems.json yet)";
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"[items] could not read the shown-items list: {ex.Message}"); }
|
||||
|
||||
Console.WriteLine($"[items] HiddenItemsStorefronts: stores list only the {set.Count} item id(s) in {source}");
|
||||
_shown = set;
|
||||
_shownStamp = stamp;
|
||||
return _shown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// May a STORE list this avatar item? False only while HiddenItemsStorefronts is on and
|
||||
/// the item's base guid is not in the known-embedded set. Items with no descriptor
|
||||
/// (consumables, boxes, equipment skins) are never hidden - their art is in the client.
|
||||
/// </summary>
|
||||
public static bool StoreCanList(string? avatarItemDesc)
|
||||
{
|
||||
if (!ConfigCampus.HiddenItemsStorefronts) return true;
|
||||
if (string.IsNullOrWhiteSpace(avatarItemDesc)) return true; // consumables, boxes, skins: no bundle
|
||||
var shown = ShownGuids();
|
||||
var baseGuid = NormalizeGuid(avatarItemDesc.Split(',').FirstOrDefault());
|
||||
// An unparseable descriptor is hidden, not waved through - this is an allowlist.
|
||||
return baseGuid != null && shown.Contains(baseGuid);
|
||||
}
|
||||
|
||||
public static bool ClientCanRender(string? avatarItemDesc)
|
||||
{
|
||||
var known = KnownGuids();
|
||||
if (known.Count == 0) return true;
|
||||
|
||||
var baseGuid = NormalizeGuid((avatarItemDesc ?? "").Split(',').FirstOrDefault());
|
||||
return baseGuid == null || known.Contains(baseGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class LockedItems
|
||||
{
|
||||
public List<string> AvatarItemDescriptions { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class MyObjectives
|
||||
{
|
||||
public List<object> Objectives { get; set; } = new List<object>();
|
||||
public List<object> ObjectiveGroups { get; set; } = new List<object>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class NS
|
||||
{
|
||||
public string Accounts { get; set; } = "";
|
||||
public string API { get; set; } = "";
|
||||
public string Auth { get; set; } = "";
|
||||
public string BugReporting { get; set; } = "";
|
||||
public string Cards { get; set; } = "";
|
||||
public string CDN { get; set; } = "";
|
||||
public string Chat { get; set; } = "";
|
||||
public string Clubs { get; set; } = "";
|
||||
public string CMS { get; set; } = "";
|
||||
public string Commerce { get; set; } = "";
|
||||
public string Data { get; set; } = "";
|
||||
public string DataCollection { get; set; } = "";
|
||||
public string Discovery { get; set; } = "";
|
||||
public string Econ { get; set; } = "";
|
||||
public string GameLogs { get; set; } = "";
|
||||
public string Geo { get; set; } = "";
|
||||
public string Images { get; set; } = "";
|
||||
public string Leaderboard { get; set; } = "";
|
||||
public string Link { get; set; } = "";
|
||||
public string Lists { get; set; } = "";
|
||||
public string Matchmaking { get; set; } = "";
|
||||
public string Moderation { get; set; } = "";
|
||||
public string Notifications { get; set; } = "";
|
||||
public string PlatformNotifications { get; set; } = "";
|
||||
public string PlayerSettings { get; set; } = "";
|
||||
public string RoomComments { get; set; } = "";
|
||||
public string Rooms { get; set; } = "";
|
||||
public string Storage { get; set; } = "";
|
||||
public string Strings { get; set; } = "";
|
||||
public string StringsCDN { get; set; } = "";
|
||||
public string Studio { get; set; } = "";
|
||||
public string Thorn { get; set; } = "";
|
||||
public string Videos { get; set; } = "";
|
||||
public string WWW { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using Campus_live_rewrite.api.dbs;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class Progress
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public ulong PlayerId { get; set; }
|
||||
public int XP { get; set; }
|
||||
|
||||
// ---- The real Rec Room progression curve, ported from RecFlare -------------------------
|
||||
// LEVEL_REQUIRED_XP[level] is the XP needed to LEAVE that level (indexed BY level). A
|
||||
// player's stored XP is the progress INTO their current level - NOT a lifetime total -
|
||||
// and every LEVEL_REQUIRED_XP[level] XP earned lifts them one level, carrying the
|
||||
// remainder forward. This replaces the old made-up "1 + xp/1000" rule; the previous author
|
||||
// left a note that the real curve should replace exactly that one method when it turned up.
|
||||
//
|
||||
// This is also what the client renders its progress bar against (see ConfigV2's
|
||||
// LevelProgressionMaps, which now sends this same table), so client and server agree on
|
||||
// what level an XP total is - the disagreement is what tripped the Level/XP anti-cheat
|
||||
// ("obscured") check and dropped players to their dorm.
|
||||
public static readonly int[] LEVEL_REQUIRED_XP = new int[]
|
||||
{
|
||||
0, 10, 10, 10, 20, 20, 20, 20, 20, 20, 20, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
|
||||
115, 115, 115, 115, 115, 115, 115, 115, 115, 115, 360, 360, 360, 360, 360, 360, 360,
|
||||
360, 360, 360, 1080, 1080, 1080, 1080, 1080, 1080, 1080, 1080, 1080, 1080,
|
||||
// 51..98: unreachable by play (20,000,000 XP each) - they exist only so the client's
|
||||
// table (ConfigV2 LevelProgressionMaps sends this) can render level 99 for the one
|
||||
// account that holds it (ConfigCampus.Level99Username). Everyone else is capped at
|
||||
// PLAYER_MAX_LEVEL below.
|
||||
20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000,
|
||||
20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000,
|
||||
20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000,
|
||||
20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000,
|
||||
20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000, 20000000
|
||||
};
|
||||
|
||||
public static readonly int MAX_LEVEL = LEVEL_REQUIRED_XP.Length - 1; // 99 (100 entries, 0..99)
|
||||
/// <summary>The level any ordinary account can reach. Only the Level99Username account goes past it.</summary>
|
||||
public const int PLAYER_MAX_LEVEL = 50;
|
||||
|
||||
public static bool MayExceedPlayerCap(ulong playerId)
|
||||
{
|
||||
var acc = PlayerDB.GetAccountById(playerId);
|
||||
return acc != null && string.Equals(acc.Username, ConfigCampus.Level99Username, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Synthetic GiftDropId for level N's reward tile = BASE + N; Gameconfigs.json's
|
||||
// Lifecycle.PSF.Progression.RewardMap maps each to its image.
|
||||
public const int LEVEL_REWARD_GIFTDROP_BASE = 2_000_000;
|
||||
|
||||
// XP a single game reward pays out. Tunable - see ConfigCampus.GameRewardXp (was a
|
||||
// constant 5, the reference's value, which made the top levels take ~216 games each).
|
||||
public static int GAME_REWARD_XP => ConfigCampus.GameRewardXp;
|
||||
|
||||
/// <summary>
|
||||
/// Spend XP on any levels it now pays for. Takes a (level, xp) pair where xp is progress
|
||||
/// into the current level, and returns the pair after applying every level-up the xp
|
||||
/// covers - each level costs LEVEL_REQUIRED_XP[level], and the remainder carries forward.
|
||||
/// Mirrors RecFlare's applyLevelUps.
|
||||
/// </summary>
|
||||
public static (int level, int xp) ApplyLevelUps(int level, int xp)
|
||||
{
|
||||
if (level < 1) level = 1;
|
||||
if (xp < 0) xp = 0;
|
||||
// Play stops at the player cap; 51+ is not earned.
|
||||
while (level < PLAYER_MAX_LEVEL)
|
||||
{
|
||||
int cost = LEVEL_REQUIRED_XP[level];
|
||||
if (cost <= 0 || xp < cost) break;
|
||||
xp -= cost;
|
||||
level += 1;
|
||||
}
|
||||
// At the cap there is nothing left to buy; park any overflow at 0 so the pair stays
|
||||
// consistent. A level above the cap (the Coach's 99) is left exactly as stored.
|
||||
if (level == PLAYER_MAX_LEVEL) xp = 0;
|
||||
if (level > MAX_LEVEL) { level = MAX_LEVEL; xp = 0; }
|
||||
return (level, xp);
|
||||
}
|
||||
|
||||
// The CUMULATIVE (lifetime) XP for a given (level, xp-into-level) pair. The client derives
|
||||
// the crown/nameplate LEVEL NUMBER from the XP value against LEVEL_REQUIRED_XP (proven: a
|
||||
// 160-XP account shows "level 10", and 150 = the cost to reach level 10). Our stored XP is
|
||||
// progress INTO the current level, so reporting it raw made the client recompute a low
|
||||
// level (e.g. Kyle at Level 50 / into-level XP 0 -> the client read XP 0 -> level 1). So we
|
||||
// REPORT cumulative XP; internal storage stays per-level.
|
||||
public static int CumulativeXpForLevel(int level, int xpIntoLevel)
|
||||
{
|
||||
if (level < 1) level = 1;
|
||||
if (level > MAX_LEVEL) level = MAX_LEVEL;
|
||||
long total = 0;
|
||||
for (int l = 1; l < level && l < LEVEL_REQUIRED_XP.Length; l++) total += LEVEL_REQUIRED_XP[l];
|
||||
if (xpIntoLevel > 0) total += xpIntoLevel;
|
||||
return (int)total;
|
||||
}
|
||||
|
||||
public static Progress GetPlayerLevel(ulong player_id)
|
||||
{
|
||||
var account = PlayerDB.GetFullAccount(player_id);
|
||||
if (account == null || account.player == null)
|
||||
{
|
||||
return new Progress { Level = 1, PlayerId = player_id, XP = 0 };
|
||||
}
|
||||
|
||||
int level = account.player.Level;
|
||||
int xp = account.player.XP;
|
||||
|
||||
// Normalise to a consistent (level, xp) pair before reporting it. Every legacy account
|
||||
// has XP 0 (the 2023 server only incremented Level, never tracked XP), and XP 0 is
|
||||
// consistent at any level in this per-level model - so nothing jumps. A stored pair
|
||||
// that somehow carries leftover XP is settled through the ladder here.
|
||||
if (ConfigCampus.EnforceLevelXpConsistency)
|
||||
{
|
||||
var (cLevel, cXp) = ApplyLevelUps(level < 1 ? 1 : level, xp);
|
||||
if (cLevel != level || cXp != xp)
|
||||
{
|
||||
Console.WriteLine($"[progression] {account.player.Id}: stored Level {level}/XP {xp} " +
|
||||
$"normalised to Level {cLevel}/XP {cXp}");
|
||||
level = cLevel; xp = cXp;
|
||||
}
|
||||
}
|
||||
else if (level < 1)
|
||||
{
|
||||
level = 1;
|
||||
}
|
||||
|
||||
// Level 99 is the Coach's alone; anyone else past the player cap is shown at the cap.
|
||||
if (level > PLAYER_MAX_LEVEL && !MayExceedPlayerCap(account.player.Id)) { level = PLAYER_MAX_LEVEL; xp = 0; }
|
||||
if (level > PLAYER_MAX_LEVEL) { level = MAX_LEVEL; xp = 0; }
|
||||
|
||||
// Report CUMULATIVE XP (see CumulativeXpForLevel) so the client computes the SAME level
|
||||
// for the crown/nameplate that we stored - not a low level recomputed from into-level XP.
|
||||
return new Progress { Level = level, PlayerId = account.player.Id, XP = CumulativeXpForLevel(level, xp) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class RRPlus
|
||||
{
|
||||
public class SubscriptionResponse
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public Subscription subscription { get; set; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public ulong? platformAccountSubscribedPlayerId { get; set; }
|
||||
}
|
||||
|
||||
public class Subscription
|
||||
{
|
||||
[JsonPropertyOrder(1)] public int subscriptionId { get; set; }
|
||||
[JsonPropertyOrder(2)] public ulong recNetPlayerId { get; set; }
|
||||
[JsonPropertyOrder(3)] public int platformType { get; set; }
|
||||
[JsonPropertyOrder(4)] public string platformId { get; set; }
|
||||
[JsonPropertyOrder(5)] public string platformPurchaseId { get; set; }
|
||||
[JsonPropertyOrder(6)] public object? @event { get; set; }
|
||||
[JsonPropertyOrder(7)] public int type { get; set; }
|
||||
[JsonPropertyOrder(8)] public int level { get; set; }
|
||||
[JsonPropertyOrder(9)] public int period { get; set; }
|
||||
[JsonPropertyOrder(10)] public DateTime expirationDate { get; set; }
|
||||
[JsonPropertyOrder(11)] public bool isAutoRenewing { get; set; }
|
||||
[JsonPropertyOrder(12)] public DateTime createdAt { get; set; }
|
||||
[JsonPropertyOrder(13)] public DateTime modifiedAt { get; set; }
|
||||
}
|
||||
|
||||
public enum SubscriptionLevel
|
||||
{
|
||||
Gold,
|
||||
Platinum,
|
||||
}
|
||||
|
||||
public enum SubscriptionPeriod
|
||||
{
|
||||
Month,
|
||||
Year,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Campus_live_rewrite.api.server.Classes.db_class;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class Rep
|
||||
{
|
||||
public ulong AccountId { get; set; }
|
||||
public bool IsCheerful { get; set; }
|
||||
public int Noteriety { get; set; }
|
||||
public CheerCategoryEnum SelectedCheer { get; set; }
|
||||
public int CheerCredit { get; set; }
|
||||
public int CheerGeneral { get; set; }
|
||||
public int CheerHelpful { get; set; }
|
||||
public int CheerCreative { get; set; }
|
||||
public int CheerGreatHost { get; set; }
|
||||
public int CheerSportsman { get; set; }
|
||||
// RecFlare's ReputationDto carries these two; the profile reads them off the reputation
|
||||
// object, so they must be present (a missing field can make the client's parser throw).
|
||||
public int SubscriberCount { get; set; }
|
||||
public int SubscribedCount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class Sanitize
|
||||
{
|
||||
public string Context { get; set; } = "RoomChat";
|
||||
public int Intent { get; set; } = 1;
|
||||
public bool PreRemoveBlockedCharacters { get; set; } = true;
|
||||
public string ReplacementChar { get; set; } = "*";
|
||||
public string Value { get; set; } = "";
|
||||
public int ruleset { get; set; } = 0;
|
||||
|
||||
private const char MaskChar = '*';
|
||||
|
||||
|
||||
// Words that should never be censored
|
||||
private static readonly HashSet<string> Whitelist = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"password",
|
||||
"pass",
|
||||
"class",
|
||||
"assistant",
|
||||
"account"
|
||||
};
|
||||
|
||||
// Bad words in NORMALIZED form (see Normalize): lowercase letters only, leet
|
||||
// characters folded, runs of 3+ identical letters collapsed to 2. Both sides of
|
||||
// the comparison go through the same normaliser, so a plain set lookup catches
|
||||
// "F.U.C.K" and "b1tch" with no pattern matching at all.
|
||||
//
|
||||
// This replaced 1749 compiled regexes of the form ^f.{0,1}u.{0,1}c.{0,1}k$, run
|
||||
// against every token of every message. The .{0,1} gaps were meant to jump
|
||||
// separator characters - but Normalize already strips every non-letter before the
|
||||
// match, so all they did was let a 3-letter entry match a 4-or-5-letter innocent
|
||||
// word: "fag" matched FLAG, "gay" matched GRAY, "cum" matched CHUM, "hoe" matched
|
||||
// HOSE. Exact comparison on the normalised token is both correct and far cheaper.
|
||||
private static readonly HashSet<string> Swears = new(StringComparer.Ordinal);
|
||||
|
||||
static Sanitize()
|
||||
{
|
||||
// Absolute path. This runs in a static constructor, so a miss here throws
|
||||
// TypeInitializationException on EVERY caller - room chat, DM threads, room
|
||||
// names, usernames, display names, bios. A missing word list must degrade to
|
||||
// "nothing is filtered", never take those endpoints down.
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "badwords.txt");
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.WriteLine($"[Sanitize] word list not found at {path} - nothing will be filtered");
|
||||
return;
|
||||
}
|
||||
|
||||
int masked = 0;
|
||||
foreach (var line in File.ReadAllLines(path))
|
||||
{
|
||||
// A masked entry ("f***", "****..just", "h*ll") is somebody's already-censored
|
||||
// OUTPUT, not a word: normalised it becomes "just", "hll", "f" - which is how
|
||||
// "just" got censored in chat. Skipped, never learnt.
|
||||
if (line.Contains('*')) { masked++; continue; }
|
||||
var normalized = Normalize(line);
|
||||
if (normalized.Length >= 3) Swears.Add(normalized);
|
||||
}
|
||||
if (masked > 0) Console.WriteLine($"[Sanitize] skipped {masked} masked (*) entries in the word list");
|
||||
|
||||
Console.WriteLine($"[Sanitize] loaded {Swears.Count} words from {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Sanitize] could not load word list ({ex.Message}) - nothing will be filtered");
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
* PUBLIC API
|
||||
* --------------------------------------------------*/
|
||||
|
||||
public static bool ContainsSwears(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return false;
|
||||
|
||||
if (ConfigCampus.AllowSwears)
|
||||
return false;
|
||||
|
||||
if (SplitKeepingWhitespace(message).Any(chunk => !IsWhitespace(chunk) && ChunkIsSwear(chunk))) return true;
|
||||
return FindConcealed(message).Count > 0;
|
||||
}
|
||||
|
||||
public static string SanitizeMessage(string message, char mask = MaskChar)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return message;
|
||||
|
||||
var sb = new StringBuilder(message.Length);
|
||||
|
||||
// Two passes, because they catch different evasions.
|
||||
//
|
||||
// 1. The whole whitespace-delimited chunk, punctuation stripped. This is what
|
||||
// catches "f.u.c.k", "f-u-c-k", "f*ck" and zero-width characters wedged
|
||||
// between letters - splitting on \W+ first would turn those into
|
||||
// single-letter tokens that are too short to test, which is exactly how
|
||||
// they used to get through.
|
||||
//
|
||||
// 2. Failing that, the individual words inside the chunk, so "hey,bitch"
|
||||
// (one chunk, normalising to the harmless "heybitch") still gets caught on
|
||||
// the word "bitch".
|
||||
// 3. The CONCEALED forms neither of those see, run first over the whole message so
|
||||
// the masked characters are already in place when the token passes look:
|
||||
// letters spread over several tokens ("s h i t", "sh it") and a swear
|
||||
// repeated or lightly suffixed inside one token ("shitshit", "shitty").
|
||||
// See FindConcealed for the rules that keep "this hit" and "cockpit" alone.
|
||||
var concealed = FindConcealed(message);
|
||||
if (concealed.Count > 0)
|
||||
{
|
||||
var chars = message.ToCharArray();
|
||||
foreach (var (start, end) in concealed)
|
||||
for (int i = start; i < end; i++)
|
||||
if (char.IsLetterOrDigit(chars[i])) chars[i] = mask;
|
||||
Console.WriteLine($"[Sanitize] Censored concealed word(s) in message: '{message}'");
|
||||
message = new string(chars);
|
||||
}
|
||||
|
||||
foreach (var chunk in SplitKeepingWhitespace(message))
|
||||
{
|
||||
if (IsWhitespace(chunk))
|
||||
{
|
||||
sb.Append(chunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ChunkIsSwear(chunk))
|
||||
{
|
||||
sb.Append(new string(mask, chunk.Length));
|
||||
Console.WriteLine($"[Sanitize] Censored word in message: '{chunk}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var token in Regex.Split(chunk, @"(\W+)"))
|
||||
{
|
||||
if (IsWord(token) && ChunkIsSwear(token))
|
||||
{
|
||||
sb.Append(new string(mask, token.Length));
|
||||
Console.WriteLine($"[Sanitize] Censored word in message: '{token}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
* INTERNAL HELPERS
|
||||
* --------------------------------------------------*/
|
||||
|
||||
private static string[] SplitKeepingWhitespace(string input) =>
|
||||
Regex.Split(input, @"(\s+)");
|
||||
|
||||
// Swears of 4+ letters, the only ones worth hunting inside other text: the 3-letter
|
||||
// entries ("ass", "cum") are exactly the ones that live inside innocent words.
|
||||
private static readonly Lazy<string[]> LongSwears = new(() =>
|
||||
Swears.Where(w => w.Length >= 4).OrderByDescending(w => w.Length).ToArray());
|
||||
|
||||
/// <summary>
|
||||
/// Character spans [start, end) of swears that the token passes cannot see:
|
||||
///
|
||||
/// spread - the letters of one swear spread across several whitespace-separated
|
||||
/// tokens ("s h i t", "sh it", "s.h.i t"). A match counts only when it
|
||||
/// starts at the start of a token and ends at the end of one, covering
|
||||
/// whole tokens - so "this hit" (thi|shit spans two partial tokens) is
|
||||
/// not a match, "s h i t" is.
|
||||
/// compound - one token that IS a swear repeated or lightly suffixed: "shitshit",
|
||||
/// "shitty", "fucked". The rest of the token after the swear(s) must be
|
||||
/// at most 2 letters, and the swear must start the token - so "cockpit"
|
||||
/// (3 letters after) and "scunthorpe" (swear in the middle) are left alone.
|
||||
///
|
||||
/// Whole-token exact matches are the caller's job (ChunkIsSwear); this only adds the
|
||||
/// forms it misses.
|
||||
/// </summary>
|
||||
private static List<(int start, int end)> FindConcealed(string message)
|
||||
{
|
||||
var spans = new List<(int, int)>();
|
||||
if (string.IsNullOrWhiteSpace(message) || ConfigCampus.AllowSwears || Swears.Count == 0) return spans;
|
||||
|
||||
// Normalised letter stream with, for every letter, its index in the original.
|
||||
var letters = new StringBuilder(message.Length);
|
||||
var origIdx = new List<int>(message.Length);
|
||||
var tokenStarts = new HashSet<int>(); // stream positions that begin a token
|
||||
var tokenEnds = new HashSet<int>(); // stream positions just past a token's last letter
|
||||
bool inToken = false;
|
||||
for (int i = 0; i < message.Length; i++)
|
||||
{
|
||||
char c = message[i];
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
if (inToken) { tokenEnds.Add(letters.Length); inToken = false; }
|
||||
continue;
|
||||
}
|
||||
string n = Normalize(c.ToString());
|
||||
if (n.Length == 0) continue; // punctuation / digit that folds to nothing
|
||||
if (!inToken) { tokenStarts.Add(letters.Length); inToken = true; }
|
||||
letters.Append(n[0]);
|
||||
origIdx.Add(i);
|
||||
}
|
||||
if (inToken) tokenEnds.Add(letters.Length);
|
||||
string stream = letters.ToString();
|
||||
if (stream.Length < 4) return spans;
|
||||
|
||||
// Per-token bounds in stream coordinates, for the compound rule.
|
||||
var tokens = tokenStarts.OrderBy(x => x).Zip(tokenEnds.OrderBy(x => x), (a, b) => (a, b)).ToList();
|
||||
|
||||
foreach (var swear in LongSwears.Value)
|
||||
{
|
||||
int from = 0;
|
||||
while ((from = stream.IndexOf(swear, from, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
int end = from + swear.Length;
|
||||
bool spread = tokenStarts.Contains(from) && tokenEnds.Contains(end)
|
||||
&& tokens.Count(t => t.a >= from && t.b <= end) >= 2;
|
||||
bool compound = false;
|
||||
if (!spread)
|
||||
{
|
||||
var tok = tokens.FirstOrDefault(t => t.a <= from && end <= t.b);
|
||||
if (tok != default && tok.a == from)
|
||||
{
|
||||
// Strip every leading repeat of the swear, then allow a tiny tail.
|
||||
int p = from;
|
||||
while (p + swear.Length <= tok.b && string.CompareOrdinal(stream, p, swear, 0, swear.Length) == 0) p += swear.Length;
|
||||
int tail = tok.b - p;
|
||||
// The tail has to be an actual suffix ("s", "y", "ed", "er"...). Any
|
||||
// two letters used to count, so "hello" was hell+o and censored.
|
||||
string tailText = tail > 0 ? stream.Substring(p, tail) : "";
|
||||
bool suffix = tail == 0 || CompoundSuffixes.Contains(tailText);
|
||||
if (tail <= 2 && suffix && (p > from + swear.Length || tail > 0)) // repeated, or one swear + short suffix
|
||||
{
|
||||
compound = true; end = tok.b;
|
||||
string whole = stream.Substring(tok.a, tok.b - tok.a);
|
||||
if (Whitelist.Contains(whole) || Swears.Contains(whole)) compound = false; // exact forms are the token pass's
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spread || compound)
|
||||
{
|
||||
if (!spans.Any(x => x.Item1 <= origIdx[from] && origIdx[end - 1] < x.Item2))
|
||||
spans.Add((origIdx[from], origIdx[end - 1] + 1));
|
||||
from = end;
|
||||
}
|
||||
else from++;
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> CompoundSuffixes = new(StringComparer.Ordinal)
|
||||
{ "s", "y", "z", "ed", "er", "es", "in", "ie", "ty", "rs", "ah" };
|
||||
|
||||
private static bool IsWhitespace(string s) =>
|
||||
s.Length > 0 && char.IsWhiteSpace(s[0]);
|
||||
|
||||
private static bool IsWord(string input) =>
|
||||
input.Any(char.IsLetterOrDigit);
|
||||
|
||||
private static bool ChunkIsSwear(string raw)
|
||||
{
|
||||
var normalized = Normalize(raw);
|
||||
|
||||
if (normalized.Length < 3) return false;
|
||||
if (Whitelist.Contains(normalized)) return false;
|
||||
if (Swears.Contains(normalized)) return true;
|
||||
|
||||
// "fuuuuck". Normalize leaves runs at 2 ("fuuck") so that "ass" does not
|
||||
// become "as", but that means a stretched word no longer matches its entry.
|
||||
// Squeezing every run to a single letter gives "fuck", which does. Only the
|
||||
// TOKEN is squeezed - the word list keeps its 2-letter runs, so this cannot
|
||||
// make the innocent "as" match the entry "ass".
|
||||
var squeezed = Regex.Replace(normalized, @"(.)\1+", "$1");
|
||||
return squeezed.Length >= 3
|
||||
&& !Whitelist.Contains(squeezed)
|
||||
&& Swears.Contains(squeezed);
|
||||
}
|
||||
|
||||
private static string Normalize(string input)
|
||||
{
|
||||
input = input.ToLowerInvariant();
|
||||
|
||||
input = Regex.Replace(input, @"[0@]", "o");
|
||||
input = Regex.Replace(input, @"[1!|]", "i");
|
||||
input = Regex.Replace(input, @"[$5]", "s");
|
||||
input = Regex.Replace(input, @"3", "e");
|
||||
input = Regex.Replace(input, @"7", "t");
|
||||
|
||||
// Remove non letters
|
||||
input = Regex.Replace(input, @"[^a-z]", "");
|
||||
|
||||
// Collapse repeats (fuuuuuck → fuuck)
|
||||
input = Regex.Replace(input, @"(.)\1{2,}", "$1$1");
|
||||
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class SteamAuthResponse
|
||||
{
|
||||
public SteamResponse? response { get; set; }
|
||||
}
|
||||
|
||||
public class SteamResponse
|
||||
{
|
||||
[JsonProperty("params")]
|
||||
public SteamParams? paramsData { get; set; }
|
||||
}
|
||||
|
||||
public class SteamParams
|
||||
{
|
||||
public string? result { get; set; }
|
||||
public string? steamid { get; set; }
|
||||
public string? ownersteamid { get; set; }
|
||||
public bool vacbanned { get; set; }
|
||||
public bool publisherbanned { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
/// <summary>
|
||||
/// Storefront ids, taken from the 2025 client's own enum (PNOBDCBDLNP, build 20250718.01).
|
||||
/// /api/storefronts/v3/giftdropstore/{id} uses these, and buyItem sends "StorefrontType": N.
|
||||
///
|
||||
/// This used to be the 2023 client's list, which numbers everything from 1400 up
|
||||
/// differently: 2000-2004 were called PopUpShop_1..5 there, but in 2025 they are the
|
||||
/// PROGRESSION-EVENT pop-up shops, and the real pop-up shops are 1700 up. (Source:
|
||||
/// skills-main wire-shapes giftdropstore-storefront-type.md, carved from the 2025 metadata.)
|
||||
/// </summary>
|
||||
public enum StorefrontType
|
||||
{
|
||||
None = 0,
|
||||
LaserTag = 1,
|
||||
RecCenter = 2,
|
||||
Watch = 3, // the main token store
|
||||
|
||||
Quest_LostSkulls = 100, // Isle
|
||||
Quest_Dracula = 101, // Crescendo
|
||||
Quest_GoldenTrophy = 102, // Crimson
|
||||
Quest_CrimsonCauldron = 103,
|
||||
|
||||
RecRoyale = 200,
|
||||
Cafe = 300,
|
||||
|
||||
Paintball = 400,
|
||||
Paintball_River = 401,
|
||||
Paintball_Homestead = 402,
|
||||
Paintball_Quarry = 403,
|
||||
Paintball_ClearCut = 404,
|
||||
Paintball_Spillway = 405,
|
||||
Paintball_SunsetDriveIn = 406,
|
||||
|
||||
Bowling = 500,
|
||||
StuntRunner = 600,
|
||||
DormMirror = 700,
|
||||
InventionStore = 800,
|
||||
RoomKeys = 900,
|
||||
|
||||
Player_Profile = 1000,
|
||||
Room_Save = 1100,
|
||||
RoomCurrencies = 1200,
|
||||
Wishlist = 1300,
|
||||
Friendotron = 1400,
|
||||
RoomConsumables = 1500,
|
||||
RecRally = 1600,
|
||||
|
||||
// The pop-up shop kiosks a room can place. A room asks for each one it has while
|
||||
// loading, so an unstocked one must answer promptly with an empty store.
|
||||
PopUpShop_1 = 1700,
|
||||
PopUpShop_2 = 1701,
|
||||
PopUpShop_3 = 1702,
|
||||
PopUpShop_4 = 1703,
|
||||
PopUpShop_5 = 1704,
|
||||
PopUpShop_6 = 1705,
|
||||
PopUpShop_7 = 1706,
|
||||
PopUpShop_8 = 1707,
|
||||
PopUpShop_9 = 1708,
|
||||
PopUpShop_10 = 1709,
|
||||
PopUpShop_11 = 1710,
|
||||
PopUpShop_12 = 1711,
|
||||
PopUpShop_13 = 1712,
|
||||
PopUpShop_14 = 1713,
|
||||
PopUpShop_15 = 1714,
|
||||
PopUpShop_16 = 1715,
|
||||
PopUpShop_17 = 1716,
|
||||
PopUpShop_18 = 1717,
|
||||
PopUpShop_19 = 1718,
|
||||
PopUpShop_20 = 1719,
|
||||
|
||||
CustomAvatarItems = 1800,
|
||||
ProgressionEventPurchasableXpBoosts = 1900,
|
||||
|
||||
// The Limited Time Challenge's shops (the Dorm Mirror customize screen asks for these).
|
||||
PopUpShop_ProgressionEvent_1 = 2000,
|
||||
PopUpShop_ProgressionEvent_2 = 2001,
|
||||
PopUpShop_ProgressionEvent_3 = 2002,
|
||||
PopUpShop_ProgressionEvent_4 = 2003,
|
||||
PopUpShop_ProgressionEvent_5 = 2004,
|
||||
|
||||
RoomOffers = 2100,
|
||||
MyLittleMonsters = 2200,
|
||||
Destiny = 2201,
|
||||
RunTheBlock = 2202,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class Sub
|
||||
{
|
||||
public ulong creatorAccountId { get; set; }
|
||||
public ulong clubId { get; set; }
|
||||
public int membershipType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class SuccessResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
|
||||
public object? value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes
|
||||
{
|
||||
public class VersionCheck
|
||||
{
|
||||
public VersionStatus ValidVersion { get; set; }
|
||||
public VersionStatus VersionStatus { get; set; }
|
||||
public UpdateNoti UpdateNotificationStage { get; set; }
|
||||
public bool IsVersionIslanded { get; set; }
|
||||
public bool IsCrossPlayDisabled { get; set; }
|
||||
}
|
||||
|
||||
public enum VersionStatus
|
||||
{
|
||||
ValidForPlay,
|
||||
UpdateRequired
|
||||
}
|
||||
|
||||
public enum UpdateNoti
|
||||
{
|
||||
None,
|
||||
Silent,
|
||||
Warn,
|
||||
Prompt,
|
||||
Require
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes.db_class
|
||||
{
|
||||
public class ChatDBClasses
|
||||
{
|
||||
public class ChatThread
|
||||
{
|
||||
public List<ChatMessage> Messages { get; set; }
|
||||
public long ChatThreadId { get; set; }
|
||||
public List<ulong> PlayerIds { get; set; }
|
||||
public long LastReadMessageId { get; set; }
|
||||
public string ChatThreadName { get; set; }
|
||||
public DateTime? SnoozedUntil { get; set; }
|
||||
public bool IsFavorited { get; set; }
|
||||
|
||||
// 0 = a normal DM / group thread, 1 = a PARTY thread (the party's chat; the client
|
||||
// finds it through /chat/thread/party and shows it in-room).
|
||||
public int ChatThreadType { get; set; } = 0;
|
||||
// Per-member read pointer / snooze / favorite. These are PER PLAYER in the real
|
||||
// service; the three thread-level fields above are the pre-existing shared ones and
|
||||
// are only read when a player has no entry here yet.
|
||||
public Dictionary<string, ChatMemberState> MemberStates { get; set; } = new();
|
||||
// Who created the party thread (party leader) - only meaningful for ChatThreadType 1.
|
||||
public ulong CreatorPlayerId { get; set; }
|
||||
// Nullable on purpose: a thread stored before these existed deserialises with null
|
||||
// (an initialiser would stamp "now" on every old thread and sort them above new ones).
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? LastActivityAt { get; set; }
|
||||
}
|
||||
|
||||
public class ChatMemberState
|
||||
{
|
||||
public long LastReadMessageId { get; set; }
|
||||
public DateTime? SnoozedUntil { get; set; }
|
||||
public bool IsFavorited { get; set; }
|
||||
}
|
||||
|
||||
public class ChatMessage
|
||||
{
|
||||
public long ChatMessageId { get; set; }
|
||||
public long ChatThreadId { get; set; }
|
||||
public int SenderPlayerId { get; set; }
|
||||
public DateTime TimeSent { get; set; }
|
||||
public string Contents { get; set; }
|
||||
public int ModerationState { get; set; } // todo find the enum
|
||||
}
|
||||
|
||||
public class MessageJson
|
||||
{
|
||||
public MessageContentType Type { get; set; }
|
||||
public int Version { get; set; } // default ver is 2
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public struct SendMessageResponse
|
||||
{
|
||||
public ChatResults ChatResult { get; set; }
|
||||
public ChatThread ChatThread { get; set; }
|
||||
}
|
||||
|
||||
public class ChatPrivacySettings
|
||||
{
|
||||
public ulong playerId { get; set; }
|
||||
public ChatPrivacy directMessagePrivacySetting { get; set; } = ChatPrivacy.Friends;
|
||||
public ChatPrivacy groupChatPrivacySetting { get; set; } = ChatPrivacy.Friends;
|
||||
}
|
||||
|
||||
public enum ChatResults
|
||||
{
|
||||
Success,
|
||||
InvalidArguments,
|
||||
ThreadNotFound,
|
||||
MembershipNotFound,
|
||||
PlayerAlreadyOnThread,
|
||||
CannotMessagePlayer,
|
||||
InvalidCharacters,
|
||||
RecentlyLeftThread,
|
||||
ThreadTooLarge
|
||||
}
|
||||
|
||||
public enum ModerationState : byte
|
||||
{
|
||||
Active,
|
||||
Junior_Pending = 11,
|
||||
Moderation_Pending = 100,
|
||||
Moderation_Closed,
|
||||
Moderation_Banned,
|
||||
MarkedForDelete = 255
|
||||
}
|
||||
|
||||
public enum QueryMode
|
||||
{
|
||||
Latest,
|
||||
NewerThan,
|
||||
OlderThan
|
||||
}
|
||||
|
||||
public enum MessageContentType
|
||||
{
|
||||
Text,
|
||||
PartyInvite,
|
||||
Photo
|
||||
}
|
||||
|
||||
public enum ChatPrivacy
|
||||
{
|
||||
Friends,
|
||||
Favorites,
|
||||
NoOne
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Campus_live_rewrite.api.server.Classes.db_class
|
||||
{
|
||||
public class ClubDBClasses
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using LiteDB;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes.db_class
|
||||
{
|
||||
public class EventDBClasses
|
||||
{
|
||||
public class CreateEventRequest
|
||||
{
|
||||
// {"RoomId":708,"SubRoomId":1444,"ClubId":null,"name":"awdadwdawdadwdawdadwd","Description":"awdadwdawdadwdawdadwdawdadwdawdadwdawdadwdawdadwdawdadwdawdadwdawdadwd","Tags":["grandopening"],"ImageName":"J9W8A0FUGWXG4CMBKFDJ46CUZHJOHP.png","StartTime":"2025-12-13T14:20:41.6244242Z","EndTime":"2025-12-13T14:50:41.6244242Z","Accessibility":1,"IsMultiInstance":false,"SupportMultiInstanceRoomChat":false,"DefaultBroadcastPermissions":0,"CanRequestBroadcastPermissions":0}
|
||||
public ulong RoomId { get; set; }
|
||||
public ulong? SubRoomId { get; set; }
|
||||
public ulong? ClubId { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public List<string>? Tags { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
public PlayerEventAccessibility Accessibility { get; set; }
|
||||
public bool IsMultiInstance { get; set; } // this is just multi instance room
|
||||
public bool SupportMultiInstanceRoomChat { get; set; } // this let player talk betweeen room instance using room chat
|
||||
public int DefaultBroadcastPermissions { get; set; } // idk what this is used for
|
||||
public int CanRequestBroadcastPermissions { get; set; } // idk what this is used for
|
||||
}
|
||||
|
||||
public class FullPlayerEvent
|
||||
{
|
||||
public List<FullTag>? Tags { get; set; }
|
||||
public List<FullEventResponses>? Responses { get; set; }
|
||||
[BsonId]
|
||||
public ulong PlayerEventId { get; set; }
|
||||
public ulong? CreatorPlayerId { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
public ulong RoomId { get; set; }
|
||||
public ulong? SubRoomId { get; set; }
|
||||
public ulong? ClubId { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
public int AttendeeCount { get; set; }
|
||||
public int State { get; set; }
|
||||
public PlayerEventAccessibility Accessibility { get; set; }
|
||||
public bool IsMultiInstance { get; set; } // this is just multi instance room
|
||||
public bool SupportMultiInstanceRoomChat { get; set; } // this let player talk betweeen room instance using room chat
|
||||
public BroadcastPermissionsEnum DefaultBroadcastPermissions { get; set; } // idk what this is used for
|
||||
public BroadcastPermissionsEnum CanRequestBroadcastPermissions { get; set; } // idk what this is used for
|
||||
public ulong BroadcastingRoomInstanceId { get; set; }
|
||||
public DateTime? RecurrenceSchedule { get; set; }
|
||||
}
|
||||
|
||||
public class FullTag
|
||||
{
|
||||
public string? Tag { get; set; }
|
||||
public TagType Type { get; set; }
|
||||
}
|
||||
|
||||
public class FullEventResponses
|
||||
{
|
||||
public ulong PlayerEventResponseId { get; set; }
|
||||
public ulong PlayerEventId { get; set; }
|
||||
public ulong PlayerId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public PlayerEventResponseType Type { get; set; } = PlayerEventResponseType.None;
|
||||
}
|
||||
|
||||
public enum PlayerEventResponseType
|
||||
{
|
||||
None = -1,
|
||||
Yes,
|
||||
Interested,
|
||||
No,
|
||||
Pending
|
||||
}
|
||||
|
||||
public enum BroadcastPermissionsEnum
|
||||
{
|
||||
None,
|
||||
RoomOwners = 256,
|
||||
All = 2147483647
|
||||
}
|
||||
|
||||
public enum PlayerEventAccessibility
|
||||
{
|
||||
Private,
|
||||
Public,
|
||||
Unlisted
|
||||
}
|
||||
|
||||
public enum TagType
|
||||
{
|
||||
Unknown1 = 0,
|
||||
Unknown2 = 1,
|
||||
Unknown3 = 2
|
||||
}
|
||||
|
||||
public enum EventResults
|
||||
{
|
||||
Success,
|
||||
HasModeratorClosedEvent,
|
||||
DoesNotExist,
|
||||
PlayerDoesNotExist,
|
||||
RoomDoesNotExist,
|
||||
StatusUnchanged,
|
||||
PrivateEvent,
|
||||
SomethingWentWrong,
|
||||
DoesNotOwnRoom,
|
||||
ResponseDoesNotExist,
|
||||
PlayerAlreadyInvited,
|
||||
EventDatesInvalid,
|
||||
EventTooLong,
|
||||
EventTooShort,
|
||||
InappropriateName,
|
||||
InappropriateDescription,
|
||||
SomeInvitesFailed,
|
||||
CannotInviteJunior,
|
||||
EventCountLimitReached,
|
||||
DoesNotOwnEvent,
|
||||
UnregisteredOrJuniorNotAllowed,
|
||||
InvalidClubPermissions,
|
||||
ImageDoesNotExist,
|
||||
SubRoomDoesNotExist,
|
||||
DoesNotOwnSubRoom,
|
||||
ModifyTagsFailed,
|
||||
RoomCapacityTooLow,
|
||||
BroadcastEventNotMultiInstance,
|
||||
PlayerNotAllowedToCreateMultiInstanceEvents,
|
||||
PlayerBannedFromEventCreation,
|
||||
EventIsModerationClosed,
|
||||
EventIsModerationPendingReview
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using LiteDB;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes.db_class
|
||||
{
|
||||
public class YourFriendData
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId? Id { get; set; }
|
||||
public ulong PlayerID { get; set; }
|
||||
public RelationshipDetail Relationship { get; set; }
|
||||
}
|
||||
|
||||
public class FriendData
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId? Id { get; set; }
|
||||
public string? FriendId { get; set; }
|
||||
public RelationshipType Relationship { get; set; }
|
||||
public FriendInfo FriendFlags { get; set; }
|
||||
public ReciprocalStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class RelationshipDetail
|
||||
{
|
||||
public ulong Id { get; set; }
|
||||
public ulong PlayerID { get; set; }
|
||||
public ulong OtherPlayerID { get; set; }
|
||||
public RelationshipType RelationshipType { get; set; }
|
||||
public ReciprocalStatus Muted { get; set; }
|
||||
public ReciprocalStatus Ignored { get; set; }
|
||||
public ReciprocalStatus Favorited { get; set; }
|
||||
public int VoiceVolume { get; set; } = 100;
|
||||
}
|
||||
|
||||
public enum RelationshipType
|
||||
{
|
||||
None,
|
||||
FriendRequestSent,
|
||||
FriendRequestReceived,
|
||||
Friend,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum FriendInfo
|
||||
{
|
||||
None = 0,
|
||||
Favorited = 1,
|
||||
Muted = 2,
|
||||
Ignored = 4,
|
||||
Blocked = 5
|
||||
}
|
||||
|
||||
public enum ReciprocalStatus
|
||||
{
|
||||
None,
|
||||
Local,
|
||||
Remote,
|
||||
Mutual
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes.db_class
|
||||
{
|
||||
public class LeaderboardDBClasses
|
||||
{
|
||||
public class GetNearbyScoresRequest
|
||||
{
|
||||
// {"PlayerId":2,"StatChannel":2,"RoomId":29,"FilterType":1,"SortAscending":false,"WindowSize":10}
|
||||
public ulong PlayerId { get; set; }
|
||||
public int StatChannel { get; set; }
|
||||
public ulong RoomId { get; set; }
|
||||
public FilterTypeEnum FilterType { get; set; }
|
||||
public bool SortAscending { get; set; }
|
||||
public int WindowSize { get; set; }
|
||||
}
|
||||
|
||||
public class GetRanksRequest
|
||||
{
|
||||
// {"PlayerId":2,"StatChannel":2,"RoomId":29,"FilterType":0,"SortAscending":false,"RankStart":0,"RankEnd":9}
|
||||
public ulong PlayerId { get; set; }
|
||||
public int StatChannel { get; set; }
|
||||
public ulong RoomId { get; set; }
|
||||
public FilterTypeEnum FilterType { get; set; }
|
||||
public bool SortAscending { get; set; }
|
||||
public int RankStart { get; set; } // skip
|
||||
public int RankEnd { get; set; } // take
|
||||
}
|
||||
|
||||
public class CheckAndSetStatRequest
|
||||
{
|
||||
// {"StatChannel":2,"RoomId":29,"StatValue":1,"CurrentStatValue":null}
|
||||
public int StatChannel { get; set; }
|
||||
public ulong RoomId { get; set; }
|
||||
public int StatValue { get; set; }
|
||||
public int? CurrentStatValue { get; set; }
|
||||
}
|
||||
|
||||
public class Entry
|
||||
{
|
||||
[Newtonsoft.Json.JsonIgnore] // serialized with Newtonsoft (JsonConvert); STJ's JsonIgnore is ignored there and leaked Id/StatChannel/RoomId
|
||||
public int Id { get; set; } // LiteDB PK
|
||||
|
||||
public int PlayerId { get; set; }
|
||||
[Newtonsoft.Json.JsonIgnore] // serialized with Newtonsoft (JsonConvert); STJ's JsonIgnore is ignored there and leaked Id/StatChannel/RoomId
|
||||
public int StatChannel { get; set; }
|
||||
[Newtonsoft.Json.JsonIgnore] // serialized with Newtonsoft (JsonConvert); STJ's JsonIgnore is ignored there and leaked Id/StatChannel/RoomId
|
||||
public long RoomId { get; set; }
|
||||
|
||||
public int Score { get; set; }
|
||||
public int Rank { get; set; }
|
||||
|
||||
// LeaderboardGuard bookkeeping. Not on the wire.
|
||||
[Newtonsoft.Json.JsonIgnore] public DateTime UpdatedAt { get; set; }
|
||||
[Newtonsoft.Json.JsonIgnore] public DateTime DayGainDate { get; set; }
|
||||
[Newtonsoft.Json.JsonIgnore] public long DayGain { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class SingleLeaderboard
|
||||
{
|
||||
public List<Entry> Rows { get; set; }
|
||||
}
|
||||
|
||||
public class FullLeaderboard
|
||||
{
|
||||
public List<Entry> GlobalOverall { get; set; }
|
||||
public List<Entry> GlobalPeriodic { get; set; }
|
||||
public List<Entry> FriendsOverall { get; set; }
|
||||
public List<Entry> FriendsPeriodic { get; set; }
|
||||
public DateTime NextResetUTC { get; set; }
|
||||
}
|
||||
|
||||
public enum RequestResult
|
||||
{
|
||||
Success,
|
||||
InvalidStat,
|
||||
RedisConnectionError
|
||||
}
|
||||
|
||||
public enum FilterType
|
||||
{
|
||||
Global,
|
||||
Friends
|
||||
}
|
||||
|
||||
public enum Timeframe
|
||||
{
|
||||
AllTime,
|
||||
TwoWeeks
|
||||
}
|
||||
|
||||
public enum FilterTypeEnum
|
||||
{
|
||||
NearbyScoresGlobal,
|
||||
NearbyScoresFriends,
|
||||
Champions,
|
||||
NUM_VALUES // wat
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using LiteDB;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes.db_class
|
||||
{
|
||||
public class account_data
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public ulong accountId { get; set; }
|
||||
[JsonPropertyOrder(2)]
|
||||
public string? username { get; set; }
|
||||
[JsonPropertyOrder(3)]
|
||||
public string? displayName { get; set; }
|
||||
[JsonPropertyOrder(4)]
|
||||
public string? profileImage { get; set; }
|
||||
[JsonPropertyOrder(5)]
|
||||
public bool? isJunior { get; set; }
|
||||
[JsonPropertyOrder(6)]
|
||||
public long platforms { get; set; }
|
||||
[JsonPropertyOrder(7)]
|
||||
public long personalPronouns { get; set; }
|
||||
[JsonPropertyOrder(8)]
|
||||
public long identityFlags { get; set; }
|
||||
[JsonPropertyOrder(9)]
|
||||
public DateTime createdAt { get; set; }
|
||||
[JsonPropertyOrder(10)]
|
||||
public bool isMetaPlatformBlocked { get; set; } = false;
|
||||
}
|
||||
public class setting_data
|
||||
{
|
||||
public ulong accountId { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Val { get; set; }
|
||||
}
|
||||
|
||||
public class player_data
|
||||
{
|
||||
public ulong id { get; set; }
|
||||
public ulong playerid { get; set; }
|
||||
public Platforms platform { get; set; }
|
||||
public List<string>? deviceId { get; set; }
|
||||
// Device bindings WITH the platform each was seen on. The flat deviceId list above stays
|
||||
// the source of truth for the anti-alt "one account per device" check; this parallel list
|
||||
// adds the platform so the account lock can enforce "one PC + one Quest headset" (see
|
||||
// AccountLock.TryEnforceAndBind). Null on legacy rows; populated lazily on next login.
|
||||
public List<mDeviceBind>? DeviceBindings { get; set; }
|
||||
// Where the account was made: true = in-game (the client's first-launch create_account),
|
||||
// false = the website. Null on rows older than the flag - see PlayerDB.OriginOf for how
|
||||
// those are classified. Drives the one-game-account-plus-one-website-account-per-device
|
||||
// rule (ConfigCampus.MaxAccountsPerDevice / AccountLock.CanShareSlot).
|
||||
public bool? CreatedInGame { get; set; }
|
||||
public string? authtoken { get; set; }
|
||||
public string? password { get; set; }
|
||||
public player_info? player { get; set; }
|
||||
public player_extra? player_Extra { get; set; }
|
||||
public DevFlag dev_Flag { get; set; }
|
||||
}
|
||||
|
||||
public enum Platforms
|
||||
{
|
||||
All = -1,
|
||||
Steam,
|
||||
Oculus,
|
||||
PlayStation,
|
||||
Xbox,
|
||||
RecNet,
|
||||
IOS,
|
||||
GooglePlay,
|
||||
Standalone
|
||||
}
|
||||
|
||||
public class player_extra
|
||||
{
|
||||
// Equipment skins this player owns, as "PrefabName|ModificationGuid". Null = none (the
|
||||
// tools' default looks). Devs ignore this and get the whole catalog (the dev locker).
|
||||
public List<string>? EquipmentSkins { get; set; }
|
||||
|
||||
public ulong? DormRoomID { get; set; }
|
||||
public List<string>? IpAddresses { get; set; }
|
||||
public ulong DiscordId { get; set; }
|
||||
// The linked Discord user's tag/username, stored purely so the website Settings > Discord
|
||||
// tab can show "Linked as <name>" instead of a bare numeric id. Set by the /link bot flow.
|
||||
public string? DiscordUsername { get; set; }
|
||||
// Name lock (admin panel > Edit Player). While true the player cannot change their own
|
||||
// @name or display name - in-game, on the website, or through the first-run onboarding
|
||||
// path. Admin edits still go through. Absent/false on every existing record.
|
||||
public bool NameLocked { get; set; }
|
||||
// Moderator locks on the rest of the profile (admin panel, Edit Player): the player can't
|
||||
// change it themselves - in-game, on the website or through the Discord bot. Admin edits
|
||||
// are never gated. See PlayerDB.IsProfileLocked.
|
||||
public bool BioLocked { get; set; }
|
||||
public bool PictureLocked { get; set; }
|
||||
public bool BannerLocked { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? avatar { get; set; }
|
||||
public List<avatar_data_saved>? SavedOutfits { get; set; }
|
||||
public Moderation_Detail.ModerationBlockDetails? ModerationBlockDetails { get; set; }
|
||||
// MIC BAN (token claim rn.privilege BanVChat - see PlayerDB "MIC BAN / ROOM-CHAT BAN").
|
||||
// Kept apart from ModerationBlockDetails so it never overwrites a real ban/kick (or the
|
||||
// reverse) and never counts as "banned" for login/matchmaking.
|
||||
public Moderation_Detail.ModerationBlockDetails? VoiceBan { get; set; }
|
||||
public List<ulong>? CheeredRooms { get; set; }
|
||||
public List<ulong>? FavoritedRooms { get; set; }
|
||||
// Last time each once-per-day game reward was claimed, keyed by reward type name.
|
||||
// Stored as a UTC date string (yyyy-MM-dd) so "already claimed today" is a plain
|
||||
// string compare and cannot drift with timezones.
|
||||
public Dictionary<string, string> DailyRewardClaims { get; set; } = new();
|
||||
|
||||
// Gift boxes waiting to be opened. Served by /api/avatar/v2/gifts.
|
||||
public List<PendingGiftBox> PendingGiftBoxes { get; set; } = new();
|
||||
|
||||
// Avatar items this account actually OWNS, as raw AvatarItemDesc strings.
|
||||
//
|
||||
// Until this existed /api/avatar/v4/items handed the entire 3061-item catalog to
|
||||
// everyone, so every account owned every item. Non-developers now start from
|
||||
// Data/APIS/items/DefaultAvatarItems.json and earn the rest; developers keep the
|
||||
// full catalog (the dev locker). See PlayerDB.OwnedAvatarItemDescs.
|
||||
//
|
||||
// null means "never granted the starter set" - distinct from an empty list, which
|
||||
// would mean a player who somehow owns nothing. The grant is lazy on first read so
|
||||
// existing accounts do not need a migration pass.
|
||||
public List<string>? AvatarItems { get; set; }
|
||||
/// <summary>Custom avatar items (UGC) this player bought or was gifted - the ids.</summary>
|
||||
public List<string>? CustomItems { get; set; }
|
||||
// Consumables the player owns: ConsumableItemDesc -> count left. Filled by gift boxes /
|
||||
// store buys; /api/consumables/v2/getUnlocked lists only these (devs keep the whole file).
|
||||
public Dictionary<string, int>? Consumables { get; set; }
|
||||
|
||||
// Per-currency balances, keyed by CurrencyType (see api/server/Classes/CurrencyType.cs):
|
||||
// 1=LaserTagTickets, 2=RecCenterTokens, 100=LostSkullsGold, 101=DraculaSilver,
|
||||
// 300=RoomCurrency, 500=RoomieCredits, etc. Value is the amount held.
|
||||
// Persisted with the rest of player_extra, so balances survive restarts.
|
||||
public Dictionary<int, long> Balances { get; set; } = new();
|
||||
|
||||
// ---- Quests / rewards progress (see APIController objectives + challenge, RewardsDB) ----
|
||||
// Daily objectives: the client owns the objective CATALOG (what Group/Index means, its
|
||||
// target and reward); the server only stores per-(Group,Index) progress numbers and a
|
||||
// once-only reward latch. Weekly challenges: only completion flags per (map,challenge),
|
||||
// plus which challenge maps have already paid their gift.
|
||||
public List<ObjectiveProgress> Objectives { get; set; } = new();
|
||||
public List<ObjectiveGroupProgress> ObjectiveGroups { get; set; } = new();
|
||||
public List<ChallengeStatus> ChallengeStatuses { get; set; } = new();
|
||||
public List<int> ClaimedChallengeMaps { get; set; } = new();
|
||||
|
||||
public int sub_count { get; set; } = 0;
|
||||
public List<ulong> SubbedTo { get; set; } = new();
|
||||
public bool IsRecentRoomHistoryVisible { get; set; } = true;
|
||||
public Influencer? Influencer { get; set; }
|
||||
public CustomAvatarItems CustomAvatarItems { get; set; } = new CustomAvatarItems();
|
||||
public MyRoomData MyRoomData { get; set; } = new MyRoomData();
|
||||
public List<MessageData>? Messages { get; set; }
|
||||
public PlayerPhotoTaggingSetting PlayerPhotoTaggingSetting { get; set; } = PlayerPhotoTaggingSetting.Anyone;
|
||||
|
||||
}
|
||||
|
||||
public enum PlayerPhotoTaggingSetting
|
||||
{
|
||||
Anyone,
|
||||
Friends,
|
||||
NoOne
|
||||
}
|
||||
|
||||
public class SetPlayerPhotoTaggingSettingRequest
|
||||
{
|
||||
public PlayerPhotoTaggingSetting Setting { get; set; }
|
||||
}
|
||||
|
||||
public class MyRoomData
|
||||
{
|
||||
public List<string> owned_room_keys { get; set; } = new List<string>();
|
||||
public List<PlayerData> PlayerData { get; set; } = new List<PlayerData>();
|
||||
public List<My_Room_currencies> Roomcurrencies { get; set; } = new List<My_Room_currencies>();
|
||||
}
|
||||
|
||||
public class PlayerData
|
||||
{
|
||||
public ulong RoomId { get; set; }
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public class My_Room_currencies
|
||||
{
|
||||
public long RoomId { get; set; }
|
||||
public string Id { get; set; }
|
||||
public long Value { get; set; }
|
||||
}
|
||||
|
||||
public class SubscribedToEntry
|
||||
{
|
||||
public ulong PlayerId { get; set; }
|
||||
}
|
||||
|
||||
public class avatar_data_saved
|
||||
{
|
||||
public ulong Slot { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
|
||||
public List<CustomAvatarItemEntry>? CustomAvatarItems { get; set; }
|
||||
|
||||
public string? FaceFeatures { get; set; }
|
||||
public string? HairColor { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? SkinColor { get; set; }
|
||||
public string? OutfitSelections { get; set; }
|
||||
public string? OutfitSelectionsV2 { get; set; }
|
||||
public string? PreviewImageName { get; set; }
|
||||
}
|
||||
|
||||
public class CustomAvatarItemEntry
|
||||
{
|
||||
public string? CustomAvatarItemId { get; set; }
|
||||
public int BodyPart { get; set; }
|
||||
}
|
||||
|
||||
public class avatar_data
|
||||
{
|
||||
public string? OutfitSelections { get; set; }
|
||||
public string? FaceFeatures { get; set; }
|
||||
public string? SkinColor { get; set; }
|
||||
public string? HairColor { get; set; }
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A gift box granted to a player but not yet opened.
|
||||
/// GiftContext identifies which box (4003 Golden, 4503 Jumbotron, ... see
|
||||
/// api/server/Classes/GameRewardType.cs -> GiftContextIds).
|
||||
/// </summary>
|
||||
public class PendingGiftBox
|
||||
{
|
||||
public string Id { get; set; } = System.Guid.NewGuid().ToString();
|
||||
/// <summary>How many of a consumable the box holds (the store row's ItemCount; 1 when unknown).</summary>
|
||||
public int ConsumableCount { get; set; }
|
||||
public int GiftContext { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public ulong FromAccountId { get; set; }
|
||||
public System.DateTime CreatedAt { get; set; } = System.DateTime.UtcNow;
|
||||
public bool Opened { get; set; }
|
||||
|
||||
// Int32-safe numeric id. The client's gift package field is a 32-bit int: a
|
||||
// 13-digit millisecond timestamp overflows it, the package fails to deserialize,
|
||||
// and the box renders the "Update your game" placeholder while the item never
|
||||
// lands live. (Learned by the 2023 server - D:\Puck\Campus-Server.)
|
||||
public int NumericId { get; set; }
|
||||
|
||||
// What the box actually contains, when it came from a store purchase.
|
||||
//
|
||||
// AvatarItemDesc is stored RAW - exactly as the store file writes it, e.g.
|
||||
// "4ee2133d-...,,,". The client parses this as guid,combination,combination,
|
||||
// combination; hand it a bare guid with the empty fields stripped and it cannot
|
||||
// read it, so the item never reaches the locker. 746 of the 968 Watch items use
|
||||
// that trailing-comma form, which is why Laser Tag purchases always worked and
|
||||
// Watch ones did not.
|
||||
public string? AvatarItemDesc { get; set; }
|
||||
public string? ConsumableItemDesc { get; set; }
|
||||
public int AvatarItemType { get; set; }
|
||||
public int GiftDropId { get; set; }
|
||||
public string? FriendlyName { get; set; }
|
||||
public int Rarity { get; set; }
|
||||
public string? EquipmentPrefabName { get; set; }
|
||||
public string? EquipmentModificationGuid { get; set; }
|
||||
/// <summary>A UGC item (custom avatar item id) the box carries - a bought/gifted custom clothing item.</summary>
|
||||
public string? CustomAvatarItemId { get; set; }
|
||||
public bool Anonymous { get; set; }
|
||||
|
||||
// What a NON-item box pays: tokens (a duplicate reward converted, a level-up's token
|
||||
// tier) and/or XP. These are DISPLAY copies - the balance and the XP are banked when the
|
||||
// box is minted, not when it is opened - but the box has to carry them or the gifts list
|
||||
// and the open animation read "0 Tokens" / no XP, which is what players saw.
|
||||
public int CurrencyType { get; set; }
|
||||
public long Currency { get; set; }
|
||||
public int Xp { get; set; }
|
||||
// True = the Currency above is credited when the box is OPENED (consume), so the number
|
||||
// moves as the box pops rather than silently beforehand. False = already credited.
|
||||
public bool PayOnOpen { get; set; }
|
||||
// True = the Xp above is banked when the box is opened (an admin XP box). Game-reward boxes
|
||||
// carry Xp for display only (already banked at pick), so they leave this false.
|
||||
public bool XpOnOpen { get; set; }
|
||||
// Image the box / picker shows for a drop that has no item of its own (the token coin).
|
||||
// A plain file name under Data/Images, served by /img/<name> like a consumable's.
|
||||
public string? ThumbnailImageName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Per-player progress for one daily objective, keyed by (Group, Index).</summary>
|
||||
public class ObjectiveProgress
|
||||
{
|
||||
public long Group { get; set; }
|
||||
public int Index { get; set; }
|
||||
public double Progress { get; set; }
|
||||
public double VisualProgress { get; set; }
|
||||
public bool IsCompleted { get; set; }
|
||||
// Latches true the first time the objective completes and never unlatches, so a replayed
|
||||
// or double-fired completion only ever pays its reward once.
|
||||
public bool HasClaimedReward { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Per-player state of one objective group.</summary>
|
||||
public class ObjectiveGroupProgress
|
||||
{
|
||||
public long Group { get; set; }
|
||||
public bool IsCompleted { get; set; }
|
||||
public string? ClearedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Per-player completion of one weekly challenge within a rotation (map).</summary>
|
||||
public class ChallengeStatus
|
||||
{
|
||||
public int ChallengeMapId { get; set; }
|
||||
public int ChallengeId { get; set; }
|
||||
public bool Complete { get; set; }
|
||||
public string? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class player_info
|
||||
{
|
||||
public ulong Id { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public string? DisplayEmoji { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
// ISO-8601 date the player entered at website signup. Used to drive account_data_me.birthday
|
||||
// (the client requires a valid date) and to decide junior status at creation. Null on legacy
|
||||
// accounts, which fall back to the old fixed placeholder date.
|
||||
public string? Birthday { get; set; }
|
||||
public int XP { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int pronounFlags { get; set; }
|
||||
public int identityFlags { get; set; }
|
||||
public bool IsJunior { get; set; }
|
||||
public bool AvoidJuniors { get; set; }
|
||||
public mPlayerReputation? PlayerReputation { get; set; }
|
||||
public List<mPlatformID>? PlatformIds { get; set; }
|
||||
public DateTime created_at { get; set; }
|
||||
public DateTime last_login_time { get; set; }
|
||||
public List<ulong> recent_rooms { get; set; } = new List<ulong>();
|
||||
public string? bannerImage { get; set; }
|
||||
public string? profileImage { get; set; }
|
||||
}
|
||||
|
||||
public class AccountLogin
|
||||
{
|
||||
public DateTime createdAt { get; set; }
|
||||
public DateTime lastLoginTime { get; set; }
|
||||
public ulong accountId { get; set; }
|
||||
public Platforms platform { get; set; }
|
||||
public string? platformId { get; set; }
|
||||
// false = platform ownership is the credential -> the client cached-logs-in with no prompt.
|
||||
// true = no account/link behind this entry -> the client shows the username/password login
|
||||
// screen for it (the reference's FAKE cached-login trick). See AuthController.CachedLogins.
|
||||
public bool requirePassword { get; set; } = false;
|
||||
}
|
||||
|
||||
public class mPlatformID
|
||||
{
|
||||
public Platforms Platform { get; set; }
|
||||
public ulong PlatformId { get; set; }
|
||||
}
|
||||
|
||||
// One device the account has signed in from, plus the platform it signed in on. Used by the
|
||||
// account lock to cap an account at one PC device and one Quest (Oculus) device.
|
||||
public class mDeviceBind
|
||||
{
|
||||
public string? DeviceId { get; set; }
|
||||
public Platforms Platform { get; set; }
|
||||
/// <summary>Last login from this device (UTC). Null on rows written before it was tracked.</summary>
|
||||
public DateTime? LastSeen { get; set; }
|
||||
}
|
||||
|
||||
public class mPlayerReputation
|
||||
{
|
||||
public bool IsCheerful { get; set; }
|
||||
public int Noteriety { get; set; }
|
||||
public int CheerGeneral { get; set; }
|
||||
public int CheerHelpful { get; set; }
|
||||
public int CheerGreatHost { get; set; }
|
||||
public int CheerSportsman { get; set; }
|
||||
public int CheerCreative { get; set; }
|
||||
public int CheerCredit { get; set; }
|
||||
public int SubscriberCount { get; set; }
|
||||
public int SubscribedCount { get; set; }
|
||||
public CheerCategoryEnum SelectedCheer { get; set; }
|
||||
}
|
||||
|
||||
public class DeleteMessagesRequest
|
||||
{
|
||||
public List<ulong> MessageIds { get; set; }
|
||||
}
|
||||
|
||||
// POST /api/messages/v1/sendMultiple body: one message to several recipients.
|
||||
// JSON e.g. {"ToPlayerIds":[205],"Type":20,"Data":""}.
|
||||
public class SendMultipleMessagesRequest
|
||||
{
|
||||
public List<ulong>? ToPlayerIds { get; set; }
|
||||
public int Type { get; set; }
|
||||
public string? Data { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public enum CheerCategoryEnum
|
||||
{
|
||||
None = -1,
|
||||
General = 0,
|
||||
Helpful = 10,
|
||||
Sportmanship = 20,
|
||||
GreatHost = 30,
|
||||
Creative = 40,
|
||||
RecRoomDeveloper = 9000
|
||||
}
|
||||
|
||||
// The PUBLIC account DTO - the backbone of a player's profile.
|
||||
//
|
||||
// Every string here coalesces null -> "" via its setter, so the client NEVER receives a
|
||||
// null (or a missing key) for a string field. RecFlare's account DTO does exactly this on
|
||||
// purpose - it reads these as strings, and a null makes its parser throw, which is why a
|
||||
// profile with no banner / emoji / (rarely) display name would fail to load entirely even
|
||||
// though the account exists. Fixing it on the DTO covers every path that builds one
|
||||
// (account/{id}, account/bulk, account/me, admin) at once.
|
||||
public class account_data_web
|
||||
{
|
||||
private string _displayName = "";
|
||||
private string _displayEmoji = "";
|
||||
private string _profileImage = "DefaultProfileImage.jpg";
|
||||
private string _bannerImage = "";
|
||||
private string _username = "";
|
||||
|
||||
public ulong accountId { get; set; }
|
||||
public DateTime createdAt { get; set; }
|
||||
public string displayName { get => _displayName; set => _displayName = value ?? ""; }
|
||||
public string displayEmoji { get => _displayEmoji; set => _displayEmoji = value ?? ""; }
|
||||
public long identityFlags { get; set; }
|
||||
public bool isJunior { get; set; }
|
||||
public long personalPronouns { get; set; }
|
||||
public long platforms { get; set; }
|
||||
// Unset profile image falls back to "DefaultProfileImage.jpg" (RecFlare's default), not ""
|
||||
// - the client renders whatever name it is handed, and an empty name is a broken image.
|
||||
public string profileImage { get => _profileImage; set => _profileImage = string.IsNullOrEmpty(value) ? "DefaultProfileImage.jpg" : value; }
|
||||
public string bannerImage { get => _bannerImage; set => _bannerImage = value ?? ""; }
|
||||
public string username { get => _username; set => _username = value ?? ""; }
|
||||
}
|
||||
|
||||
// The /account/me (self) DTO. Matches RecFlare's toSelfAccountDto: the public account fields
|
||||
// PLUS email, birthday, availableUsernameChanges. Two rules the reference's tests spell out
|
||||
// and that a strict client parser enforces:
|
||||
// * string fields are "" when unset, never null - a null makes the client's parser throw
|
||||
// (which can leave a screen that reads the full account, like the profile, blank);
|
||||
// * the client reads `availableUsernameChanges` (capital U) and `birthday`. The birthday
|
||||
// just has to be PRESENT and a valid date - "the game client needs this to be set" - so a
|
||||
// fixed old date stands in until real birthdays are stored.
|
||||
public class account_data_me
|
||||
{
|
||||
private string _bannerImage = "";
|
||||
private string _email = "";
|
||||
private string _displayName = "";
|
||||
private string _displayEmoji = "";
|
||||
private string _profileImage = "DefaultProfileImage.jpg";
|
||||
private string _username = "";
|
||||
|
||||
public ulong accountId { get; set; }
|
||||
public int availableUsernameChanges { get; set; } = 3;
|
||||
public string bannerImage { get => _bannerImage; set => _bannerImage = value ?? ""; }
|
||||
public DateTime createdAt { get; set; }
|
||||
public string birthday { get; set; } = "1904-01-01T00:00:00.000Z";
|
||||
public string email { get => _email; set => _email = value ?? ""; }
|
||||
public string displayName { get => _displayName; set => _displayName = value ?? ""; }
|
||||
public string displayEmoji { get => _displayEmoji; set => _displayEmoji = value ?? ""; }
|
||||
public long identityFlags { get; set; }
|
||||
public bool hasBirthday { get; set; } = true;
|
||||
public bool isJunior { get; set; }
|
||||
public long personalPronouns { get; set; }
|
||||
public long platforms { get; set; }
|
||||
public string profileImage { get => _profileImage; set => _profileImage = string.IsNullOrEmpty(value) ? "DefaultProfileImage.jpg" : value; }
|
||||
public bool TreatAsJunior { get; set; } = false;
|
||||
public string username { get => _username; set => _username = value ?? ""; }
|
||||
}
|
||||
|
||||
public class CustomAvatarItems
|
||||
{
|
||||
public bool isCreationAllowedForAccount { get; set; } = true;
|
||||
public bool isCreationEnabled { get; set; } = true;
|
||||
public bool isRenderingEnabled { get; set; } = true;
|
||||
|
||||
public List<string> ownedItems { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
public class Influencer
|
||||
{
|
||||
public string creatorCode { get; set; } = "";
|
||||
public bool isInfluencer { get; set; } = false;
|
||||
public ulong SupportingInfluencer { get; set; }
|
||||
}
|
||||
|
||||
public class MessageData
|
||||
{
|
||||
/*
|
||||
[
|
||||
{
|
||||
"Id": 15909836337,
|
||||
"FromPlayerId": 64495891,
|
||||
"SentTime": "2025-05-27T11:39:25.12Z",
|
||||
"Type": 200,
|
||||
"Data": "58129955",
|
||||
"RoomId": null,
|
||||
"PlayerEventId": null
|
||||
}
|
||||
]
|
||||
*/
|
||||
public ulong Id { get; set; }
|
||||
public ulong FromPlayerId { get; set; }
|
||||
// The recipient - the reference's game-invite Message includes ToPlayerId; ours omitted it.
|
||||
// Harmless on chat/other messages (0 when unset), present on invites so the client's
|
||||
// invite-message parser has the field it expects.
|
||||
public ulong ToPlayerId { get; set; }
|
||||
public DateTime SentTime { get; set; }
|
||||
public WebsocketEvents.MessageType Type { get; set; }
|
||||
public string? Data { get; set; }
|
||||
public ulong? RoomId { get; set; }
|
||||
public ulong? PlayerEventId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public enum DeviceClasses
|
||||
{
|
||||
Unknown,
|
||||
VR,
|
||||
Screen,
|
||||
Mobile,
|
||||
VRLow,
|
||||
Quest2
|
||||
}
|
||||
|
||||
public class account_login<T>
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
[JsonIgnore]
|
||||
public ulong accid { get; set; }
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum DevFlag
|
||||
{
|
||||
none = 0,
|
||||
screenshare = 1,
|
||||
dev = 2,
|
||||
mod = 4,
|
||||
owner = 8,
|
||||
junior = 16,
|
||||
keepsake = 32,
|
||||
newly_created_account = 64,
|
||||
restricted_account = 128,
|
||||
support = 256, // website Support staff: can see & manage all support tickets (SiteController.IsStaff)
|
||||
studio = 512, // CampusStudio: may publish recoloured items from the app (StudioController)
|
||||
unused_4 = 1024,
|
||||
unused_5 = 2048,
|
||||
unused_6 = 4096,
|
||||
unused_7 = 8192,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
using LiteDB;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes.db_class
|
||||
{
|
||||
public class RoomDBClasses
|
||||
{
|
||||
public class Resultslist<T>
|
||||
{
|
||||
public List<T>? Results { get; set; }
|
||||
public long TotalResults { get; set; }
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum WarningMaskType
|
||||
{
|
||||
None = 0,
|
||||
Scary = 1,
|
||||
Mature = 2,
|
||||
FlashingLights = 4,
|
||||
IntenseMotion = 8,
|
||||
Violence = 16,
|
||||
Custom = 32,
|
||||
Reports = 64
|
||||
}
|
||||
|
||||
/// <summary>Writes JSON null whatever the value is (reads normally).</summary>
|
||||
public class NeverSentToClient : Newtonsoft.Json.JsonConverter
|
||||
{
|
||||
public override bool CanRead => false;
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer) => writer.WriteNull();
|
||||
public override object? ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer) => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class RoomRoot
|
||||
{
|
||||
// The UI shows FriendlyName; `Name` is the internal slug (devin1s_MakerRoom_0601).
|
||||
// It is a real key on the client's 44-key Room DTO and was simply absent from ours
|
||||
// (wire-shapes: rooms-contributedby). Computed, never stored.
|
||||
[LiteDB.BsonIgnore]
|
||||
public string FriendlyName => Name ?? "";
|
||||
|
||||
// Live player count, shown on room cards. Nullable on the wire and filled only by the
|
||||
// list endpoints that can afford one presence pass; null = "unknown", which the client
|
||||
// renders as no count rather than zero.
|
||||
[LiteDB.BsonIgnore]
|
||||
public int? CCU { get; set; }
|
||||
|
||||
public RoomDBClasses.RoomAccessibility Accessibility { get; set; }
|
||||
public int AgeRating { get; set; }
|
||||
public bool AutoLocalizeRoom { get; set; }
|
||||
public DateTime? BecameRRStudioRoomAt { get; set; }
|
||||
public bool CloningAllowed { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public ulong CreatorAccountId { get; set; }
|
||||
public string? CurrentSnapshotId { get; set; }
|
||||
public bool NeedsSnapshotId { get; set; }
|
||||
public string CustomWarning { get; set; }
|
||||
public string DataBlob { get; set; }
|
||||
// public string DataBlobHash { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool DisableMicAutoMute { get; set; }
|
||||
public bool DisableRoomComments { get; set; }
|
||||
public bool EncryptVoiceChat { get; set; }
|
||||
public bool ExcludeFromLists { get; set; }
|
||||
public bool ExcludeFromSearch { get; set; }
|
||||
public string ImageName { get; set; }
|
||||
public bool IsDeveloperOwned { get; set; }
|
||||
// Set from the admin panel's "Dev room" tick (RoomDB.SetDevRoom): it is what keeps IsDeveloperOwned on
|
||||
// a player-made room, because FixPlayerRoomFlags clears that flag at every start. Server-side only.
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public bool IsDevRoom { get; set; }
|
||||
public bool IsDorm { get; set; }
|
||||
public bool IsJuniorCreated { get; set; }
|
||||
public bool IsPlacePlay { get; set; }
|
||||
public bool IsRRO { get; set; }
|
||||
public bool IsRecRoomApproved { get; set; }
|
||||
public bool LoadScreenLocked { get; set; }
|
||||
public List<LoadScreen> LoadScreens { get; set; }
|
||||
public LocalizationContext LocalizationContext { get; set; }
|
||||
public int MaxPlayerCalculationMode { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public int MinLevel { get; set; }
|
||||
public int? MinUgcSubVersion { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int? PersistenceVersion { get; set; }
|
||||
public List<string> PromoExternalContent { get; set; }
|
||||
public List<string> PromoImages { get; set; }
|
||||
public int PublishState { get; set; }
|
||||
public DateTime PublishedAt { get; set; }
|
||||
public string RankedEntityId { get; set; }
|
||||
public object RankingContext { get; set; }
|
||||
public List<string> RestrictedCircuitsAllowListNames { get; set; }
|
||||
public List<RoleClass> Roles { get; set; }
|
||||
// The room-wide "last ban" record, kept in the db for the admin tools but NEVER sent to the game. The
|
||||
// client reads this key as THE VIEWER's own ban on the room and shows "Unable to Join Room - you have
|
||||
// been banned" from it; sent to everyone, one ban made the room look banned to every player who opened
|
||||
// it (and any room with a ban in its history, which is why it seemed to happen everywhere). The ban
|
||||
// itself is enforced at join (BannedFromRoom), so nothing is lost by sending null.
|
||||
[Newtonsoft.Json.JsonConverter(typeof(NeverSentToClient))]
|
||||
public RoomBanDetails RoomBanDetails { get; set; }
|
||||
// Every ban ever issued on this room (active, elapsed and lifted). RoomBanDetails above
|
||||
// is the single "current" record older code reads; this is the real list. Not part of
|
||||
// the room DTO the client gets.
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public List<RoomBanDetails>? RoomBans { get; set; }
|
||||
[BsonId]
|
||||
public ulong RoomId { get; set; }
|
||||
public RoomState State { get; set; }
|
||||
public Stats Stats { get; set; }
|
||||
public List<SubRoom> SubRooms { get; set; }
|
||||
public bool SupportsJuniors { get; set; }
|
||||
public bool SupportsLevelVoting { get; set; }
|
||||
public bool SupportsMobile { get; set; }
|
||||
public bool SupportsQuest2 { get; set; }
|
||||
public bool SupportsScreens { get; set; }
|
||||
public bool SupportsTeleportVR { get; set; }
|
||||
public bool SupportsVRLow { get; set; }
|
||||
public bool SupportsWalkVR { get; set; }
|
||||
public List<TagClass> Tags { get; set; }
|
||||
public bool ToxmodEnabled { get; set; }
|
||||
public int? UgcSubVersion { get; set; }
|
||||
public int? UgcVersion { get; set; }
|
||||
public WarningMaskType WarningMask { get; set; }
|
||||
}
|
||||
|
||||
public class LoadScreen
|
||||
{
|
||||
public string ImageName { get; set; }
|
||||
public string Subtitle { get; set; }
|
||||
public string Title { get; set; }
|
||||
}
|
||||
|
||||
public class LocalizationContext
|
||||
{
|
||||
public List<string> LocalizedFields { get; set; }
|
||||
public string Scope { get; set; }
|
||||
public string TargetLocale { get; set; }
|
||||
}
|
||||
|
||||
public class RoleClass
|
||||
{
|
||||
public long AccountId { get; set; }
|
||||
public int InvitedRole { get; set; }
|
||||
public long? LastChangedByAccountId { get; set; }
|
||||
public RoomDBClasses.Role_data Role { get; set; }
|
||||
}
|
||||
|
||||
public class RoomBanDetails
|
||||
{
|
||||
public long AccountId { get; set; }
|
||||
public DateTime? BanEndTime { get; set; }
|
||||
public DateTime BanStartTime { get; set; }
|
||||
public long BannedByAccountId { get; set; }
|
||||
public string Reason { get; set; }
|
||||
public int Status { get; set; }
|
||||
public long? UnbannedByAccountId { get; set; }
|
||||
}
|
||||
|
||||
public class Stats
|
||||
{
|
||||
public int CheerCount { get; set; }
|
||||
public int FavoriteCount { get; set; }
|
||||
public int VisitCount { get; set; }
|
||||
public int VisitorCount { get; set; }
|
||||
}
|
||||
|
||||
public class SubRoom
|
||||
{
|
||||
[BsonId]
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public ObjectId? Id { get; set; }
|
||||
|
||||
public CurrentSave? CurrentSave { get; set; }
|
||||
[JsonIgnore]
|
||||
public List<CurrentSave> AllSaves { get; set; } = new();
|
||||
public RoomAccessibility Accessibility { get; set; }
|
||||
public long? CreatorAccountId { get; set; }
|
||||
public bool IsSandbox { get; set; }
|
||||
public int LastModeratedSaveModerationState { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public string Name { get; set; }
|
||||
public ulong RoomId { get; set; }
|
||||
public bool ShouldAutoStageSaves { get; set; }
|
||||
public long? StagedSubRoomDataSaveId { get; set; }
|
||||
public ulong SubRoomId { get; set; }
|
||||
public string UnitySceneId { get; set; }
|
||||
|
||||
// The permission overrides a manager saved for THIS subroom (RoomPermissions). Not on
|
||||
// the wire - the client reads them merged into GET /photon_access_token, never here.
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public List<Campus_live_rewrite.api.server.RoomPermissions.Entry>? Permissions { get; set; }
|
||||
}
|
||||
|
||||
public class CurrentSave
|
||||
{
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string DataBlob { get; set; }
|
||||
//public string DataBlobHash { get; set; }
|
||||
public string Description { get; set; }
|
||||
public int ModerationState { get; set; }
|
||||
public int OMVersion { get; set; }
|
||||
public int PersistenceVersion { get; set; }
|
||||
public List<string> ReferencedUnityAssetIds { get; set; }
|
||||
public ulong? SavedByAccountId { get; set; }
|
||||
public DeviceClasses? SavedOnDeviceClass { get; set; }
|
||||
public Platforms? SavedOnPlatform { get; set; }
|
||||
public ulong SubRoomDataSaveId { get; set; }
|
||||
public ulong SubRoomId { get; set; }
|
||||
public int UgcSubVersion { get; set; }
|
||||
public string UnityAssetId { get; set; } // optional, may be null
|
||||
}
|
||||
|
||||
public class TagClass
|
||||
{
|
||||
public string Tag { get; set; }
|
||||
public int Type { get; set; } // 0 owner-set, 1 client-derived (autoTag: limitsv2 / beta), 2 server-derived (rro)
|
||||
// Present ONLY on the room's primary genre tag (never false) - the client keys on presence.
|
||||
[Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
|
||||
[System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? IsPrimaryGenre { get; set; }
|
||||
}
|
||||
|
||||
public class SubroomSaveResults
|
||||
{
|
||||
public List<CurrentSave> Results { get; set; } = new();
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class RoomFileData
|
||||
{
|
||||
public RoomData? RoomData { get; set; }
|
||||
public SubRoomData? SubRoomData { get; set; }
|
||||
public string? InventionUsage { get; set; }
|
||||
public int PersistenceVersion { get; set; }
|
||||
public int OMVersion { get; set; }
|
||||
public int UgcSubVersion { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool AutoPublish { get; set; }
|
||||
}
|
||||
|
||||
public class RoomData
|
||||
{
|
||||
public string? Filename { get; set; }
|
||||
public string? Hash { get; set; }
|
||||
public string? OwnershipProof { get; set; }
|
||||
}
|
||||
|
||||
public class SubRoomData
|
||||
{
|
||||
public string? Filename { get; set; }
|
||||
public string? Hash { get; set; }
|
||||
public string? OwnershipProof { get; set; }
|
||||
}
|
||||
|
||||
public enum RoomAccessibility
|
||||
{
|
||||
Private,
|
||||
Public,
|
||||
Unlisted,
|
||||
Dev_only,
|
||||
Dev_Unlisted,
|
||||
}
|
||||
|
||||
public enum RoomState
|
||||
{
|
||||
Active,
|
||||
PendingJunior = 11,
|
||||
Moderation_PendingReview = 100,
|
||||
Moderation_Closed,
|
||||
MarkedForDelete = 1000
|
||||
}
|
||||
|
||||
public class Tags
|
||||
{
|
||||
public string? Tag { get; set; }
|
||||
public int Type { get; set; }
|
||||
}
|
||||
public class LoadScreens
|
||||
{
|
||||
public string? ImageName { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
}
|
||||
|
||||
public enum Role_data : byte
|
||||
{
|
||||
None,
|
||||
Banned,
|
||||
Host = 10,
|
||||
Moderator = 20,
|
||||
Contributor = 25, // the client's value (was missing -> a contributor invite landed as CoOwner)
|
||||
CoOwner = 30,
|
||||
TemporaryCoOwner,
|
||||
Creator = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Campus_live_rewrite.api.server.Classes.db_class
|
||||
{
|
||||
public class PlayerSetting
|
||||
{
|
||||
[BsonId]
|
||||
public ulong PlayerId { get; set; }
|
||||
public List<Setting> Settings { get; set; } = new List<Setting>();
|
||||
}
|
||||
|
||||
public class Setting
|
||||
{
|
||||
public string? Key { get; set; }
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user