Initialize repository

Added basic info to get in game for like...August 2016 ;-;
It's not much but it's a start
This commit is contained in:
splootybean
2026-02-27 00:58:13 -08:00
parent 05c35b2a18
commit 387ec7ba89
61 changed files with 1722 additions and 0 deletions
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.Activities;
using RecRoomArchive.Services;
namespace RecRoomArchive.Controllers.API.Activities.Charades.V1
{
[Route(template: "api/[controller]/charades/v1")]
[ApiController]
public class ActivitiesController(MessageOfTheDayService motdService) : ControllerBase
{
// TODO: Move out of MOTD service
[HttpGet(template: "words")]
public async Task<ActionResult<List<CharadesWord>>> GetCharadesWords()
{
return Ok(await motdService.GetCharadesWordsList());
}
}
}
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.Avatar;
using RecRoomArchive.Services;
using System.Text.Json;
namespace RecRoomArchive.Controllers.API.Avatar.V2
{
[Route(template: "api/[controller]/v2")]
[ApiController]
public class AvatarController(FileService fileService) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PlayerAvatar>> GetAvatar()
{
var avatarData = fileService.GetData("avatar.json");
if (string.IsNullOrWhiteSpace(avatarData))
{
var baseAvatar = new PlayerAvatar();
fileService.SetData("avatar.json", JsonSerializer.Serialize(baseAvatar));
return Ok(baseAvatar);
}
var avatar = JsonSerializer.Deserialize<PlayerAvatar>(avatarData);
return Ok(avatar);
}
[HttpGet(template: "gifts")]
public async Task<ActionResult<List<object>>> GetPendingGifts()
{
return Ok(new List<object>());
}
[HttpPost(template: "set")]
public async Task<ActionResult<PlayerAvatar>> SetAvatar(PlayerAvatar avatar)
{
fileService.SetData("avatar.json", JsonSerializer.Serialize(avatar));
return Ok();
}
}
}
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
namespace RecRoomArchive.Controllers.API.Avatar.V3
{
[Route(template: "api/[controller]/v3")]
[ApiController]
public class AvatarController() : ControllerBase
{
// TODO: Implement
[HttpGet(template: "items")]
public async Task<ActionResult<List<object>>> GetUnlockedAvatarItems()
{
return Ok(new List<object>());
}
}
}
@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Services;
namespace RecRoomArchive.Controllers.API.Config.V1
{
/// <summary>
/// Configs that control Rec Room. These configs include data like Amplitude, MessageOfTheDay, Objectives, etc...
/// </summary>
[Route(template: "api/[controller]/v1")]
[ApiController]
public class ConfigController(MessageOfTheDayService motdService) : ControllerBase
{
/// <summary>
/// Gets the legacy version of the Message of the Day, to display on the Dorm Room bulletin board, or to show on the login screen in pre-Dorm Room versions of the game
/// </summary>
/// <returns>A basic string to display in game, related to the Message of the Day</returns>
[HttpGet(template: "motd")]
public async Task<ActionResult<string>> GetMessageOfTheDay()
{
var messageOfTheDay = await motdService.GetMessageOfTheDay();
return Ok(messageOfTheDay);
}
/// <summary>
/// Returns the current daily objectives to use before config/v2 existed
/// </summary>
/// <returns>A list of objectives</returns>
[HttpGet(template: "objectives")]
public async Task<ActionResult<List<object>>> GetDailyObjectives()
{
return Ok(new List<object>());
}
}
}
@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.Config;
using RecRoomArchive.Services;
namespace RecRoomArchive.Controllers.API.Config.V2
{
/// <summary>
/// Configs that control Rec Room. These configs include data like Amplitude, MessageOfTheDay, Objectives, etc...
/// </summary>
[Route(template: "api/[controller]/v2")]
[ApiController]
public class ConfigController(ConfigService configService) : ControllerBase
{
/// <summary>
/// Gets the RecRoomConfig, containing lots of data on how the game should function
/// </summary>
/// <returns>RecRoomConfig</returns>
[HttpGet]
public async Task<ActionResult<RecRoomConfig>> GetRecRoomConfig()
{
var recRoomConfig = await configService.GetRecRoomConfig();
return Ok(recRoomConfig);
}
}
}
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.Players;
using RecRoomArchive.Services;
using System.Text.Json;
namespace RecRoomArchive.Controllers.API.Images.V2
{
[Route("api/[controller]/v2")]
[ApiController]
public class ImagesController(FileService fileService, ImageService imageService) : ControllerBase
{
[HttpPost(template: "profile")]
public async Task<ActionResult> SetProfileImage(IFormFile image)
{
using var stream = image.OpenReadStream();
var imageName = await imageService.SaveImageAsync(stream);
var profileData = fileService.GetData("profile.json");
if (profileData == null)
return BadRequest("Profile is not populated");
var profile = JsonSerializer.Deserialize<BaseProfile>(profileData)!;
profile.ProfileImageName = imageName;
fileService.SetData("profile.json", JsonSerializer.Serialize(profile));
return Ok();
}
}
}
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
namespace RecRoomArchive.Controllers.API.Messages.V2
{
[Route(template: "api/[controller]/v2")]
[ApiController]
public class MessagesController : ControllerBase
{
[HttpGet(template: "get")]
public async Task<ActionResult<List<object>>> GetMessages()
{
return new List<object>();
}
}
}
@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.PlatformLogin.Requests;
using RecRoomArchive.Models.API.PlatformLogin.Responses;
using RecRoomArchive.Services;
namespace RecRoomArchive.Controllers.API.PlatformLogin.V2
{
/// <summary>
/// Used to login to accounts on Rec Room
/// </summary>
[Route(template: "api/[controller]/v2")]
[ApiController]
public class PlatformLoginController(AppVersionService appVersionService, AccountService accountService, AuthorizationService authorizationService) : ControllerBase
{
/// <summary>
/// Checks if the appVersion provided is allowed to play (which it most certainly will be unless its a weird build or someone messed with it)
/// </summary>
/// <returns>An Ok response if the version is valid, Forbid if it is not. Forbid will yield the client displaying "Rec Room Update Required" soo maybe don't</returns>
[HttpPost]
public async Task<ActionResult> Login([FromHeader(Name = "X-Rec-Room-Version")] string appVersion, [FromForm] BaseLoginRequest loginRequest)
{
var username = loginRequest.Name ?? string.Empty;
var buildTimestamp = loginRequest.BuildTimestamp;
// We are going to store the appVersion on login now...
await appVersionService.StoreAppVersion(appVersion);
await appVersionService.StoreBuildTimestamp(buildTimestamp);
// See if a profile exists yet...
if(!accountService.AccountExists())
{
// ...if not, create it!
accountService.CreateAccount(username);
}
var accountId = accountService.GetSelfAccount()!.Id;
return Ok(new BaseLoginResponse
{
Token = authorizationService.GenerateToken(accountId),
PlayerId = accountId
});
}
}
}
@@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.Players;
using RecRoomArchive.Services;
using System.ComponentModel.DataAnnotations;
namespace RecRoomArchive.Controllers.API.Players
{
/// <summary>
/// Used in August 2016 RecNet
/// </summary>
[Route(template: "api/[controller]")]
[ApiController]
public class PlayersController(AccountService accountService) : ControllerBase
{
/// <summary>
/// Returns the profile tied to a SteamID (If it exists)
/// </summary>
/// <param name="steamId">The SteamID of the player we are trying to get the profile of</param>
/// <returns>The profile of the player, if it exists</returns>
[HttpGet]
public async Task<ActionResult<AugustProfile>> GetProfile([Required, FromQuery] ulong steamId)
{
var baseProfile = accountService.GetSelfAccount();
if (baseProfile == null)
return NotFound();
var profile = new AugustProfile(baseProfile)
{
SteamID = steamId
};
return profile;
}
/// <summary>
/// Creates a profile and links it to a SteamID if the player does not already exist
/// </summary>
/// <param name="steamId">The SteamID of the player that we are creating a profile for</param>
/// <param name="username">The Steam username of the player that we are creating a profile for</param>
/// <returns>A new AugustProfile</returns>
[HttpPost]
public async Task<ActionResult<AugustProfile>> CreateProfile(
[Required, FromForm(Name = "SteamID")] ulong steamId,
[Required, FromForm(Name = "Name")] string username)
{
if (!accountService.AccountExists())
{
accountService.CreateAccount(username);
}
var baseProfile = accountService.GetSelfAccount();
if (baseProfile == null)
return NotFound();
var profile = new AugustProfile(baseProfile)
{
SteamID = steamId
};
return profile;
}
/// <summary>
/// Stores data related to the player onto the server
/// </summary>
/// <param name="profileId">The Id of the Profile we are storing data for</param>
/// <param name="model">The data the client posts...it happens to be an entire profile but we can take only the data we need :)</param>
/// <returns>A successful response, likely will just return the updated profile</returns>
[HttpPut(template: "{profileId:long}")]
public async Task<ActionResult<AugustProfile>> UpdateProfile([Required] ulong profileId, [FromBody] AugustProfile model)
{
return (AugustProfile)accountService.GetSelfAccount()!;
}
}
}
@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.Config;
using RecRoomArchive.Models.API.Platform;
using RecRoomArchive.Models.API.Players;
using RecRoomArchive.Services;
using System.ComponentModel.DataAnnotations;
namespace RecRoomArchive.Controllers.API.Players.V1
{
/// <summary>
/// Used to get accounts / profiles in 2016-2020 although gets phased out as time goes on...
/// </summary>
[Route(template: "api/[controller]/v1")]
[ApiController]
public class PlayersController(AccountService accountService) : ControllerBase
{
/// <summary>
/// Returns the profile of this ID (If it exists)
/// </summary>
/// <param name="id">The ID of the player we are trying to get the profile of</param>
/// <returns>The profile of the player, if it exists</returns>
[HttpGet(template: "{id:long}")]
public async Task<ActionResult<BaseProfile>> GetProfile([Required] ulong id)
{
return accountService.GetSelfAccount()!;
}
[HttpPost(template: "listByPlatformId")]
public async Task<ActionResult<List<object>>> GetFromServer([FromForm] PlatformType platform, [FromForm] List<ulong> platformIds)
{
return Ok(new List<object>());
}
[HttpGet(template: "search/{query}")]
public async Task<ActionResult<List<BaseProfile>>> SearchForProfiles(string query)
{
return Ok(new List<BaseProfile>());
}
[HttpGet(template: "phoneLastFour")]
public async Task<ActionResult<PhoneNumberDTO>> GetPhoneLastFour()
{
return Ok(new PhoneNumberDTO());
}
[HttpGet(template: "blockDuration")]
public async Task<ActionResult<BlockDurationDTO>> GetBlockDuration()
{
return Ok(new BlockDurationDTO());
}
[HttpPost(template: "objectives")]
public async Task<ActionResult> CompleteObjectives(object objective)
{
return Ok();
}
}
}
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.Players;
using RecRoomArchive.Models.Common;
using RecRoomArchive.Services;
using System.Text.Json;
namespace RecRoomArchive.Controllers.API.Players.V2
{
/// <summary>
/// Used to modify accounts / profiles in 2017-2019 although gets phased out as time goes on...
/// </summary>
[Route(template: "api/[controller]/v2")]
[ApiController]
public class PlayersController(FileService fileService) : ControllerBase
{
/// <summary>
/// Sets the players displayName
/// </summary>
/// <param name="name">The displayName the player is requesting</param>
/// <returns>OkResponse</returns>
[HttpPost(template: "displayName")]
public async Task<ActionResult<OkResponse>> SetDisplayName([FromForm(Name = "Name")] string name)
{
var profileData = fileService.GetData("profile.json");
if (profileData == null)
return BadRequest("Profile is not populated");
var profile = JsonSerializer.Deserialize<BaseProfile>(profileData)!;
profile.DisplayName = name;
fileService.SetData("profile.json", JsonSerializer.Serialize(profile));
return Ok(OkResponse.Ok());
}
}
}
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
namespace RecRoomArchive.Controllers.API.Relationships.V2
{
[Route(template: "api/[controller]/v2")]
[ApiController]
public class RelationshipsController : ControllerBase
{
[HttpGet(template: "get")]
public async Task<ActionResult<List<object>>> GetRelationships()
{
return new List<object>();
}
}
}
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Models.API.Avatar;
using RecRoomArchive.Models.API.Setting;
using RecRoomArchive.Services;
using System.Text.Json;
namespace RecRoomArchive.Controllers.API.Settings.V2
{
[Route(template: "api/[controller]/v2")]
[ApiController]
public class SettingsController(FileService fileService) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<List<Setting>>> GetSettings()
{
var settingsData = fileService.GetData("settings.json");
if (string.IsNullOrWhiteSpace(settingsData))
{
return Ok(new List<Setting>());
}
var settings = JsonSerializer.Deserialize<List<Setting>>(settingsData);
return Ok(settings);
}
[HttpPost(template: "set")]
public async Task<ActionResult> SetSetting(Setting setting)
{
var settingsData = fileService.GetData("settings.json");
var settings = string.IsNullOrWhiteSpace(settingsData) ? [] : JsonSerializer.Deserialize<List<Setting>>(settingsData) ?? [];
settings.RemoveAll(x => x.Key == setting.Key);
settings.Add(setting);
fileService.SetData("settings.json", JsonSerializer.Serialize(settings));
return Ok();
}
}
}
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Services;
namespace RecRoomArchive.Controllers.API.VersionCheck.V1
{
/// <summary>
/// Endpoints used to check if the version the player is playing on is up to date enough to play Rec Room, due to this being a custom server, it doesn't really matter
/// </summary>
[Route(template: "api/[controller]/v1")]
[ApiController]
public class VersionCheckController(AppVersionService appVersionService) : ControllerBase
{
/// <summary>
/// Checks if the appVersion provided is allowed to play (which it most certainly will be unless its a weird build or someone messed with it)
/// </summary>
/// <returns>An Ok response if the version is valid, Forbid if it is not. Forbid will yield the client displaying "Rec Room Update Required" soo maybe don't</returns>
[HttpGet]
public async Task<ActionResult> CheckVersion([FromHeader(Name = "X-Rec-Room-Version")] string appVersion)
{
if (appVersion == null)
return Forbid();
return Ok();
}
}
}
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Services;
using System.Net.Mime;
namespace RecRoomArchive.Controllers.Images
{
[Route("img")]
[ApiController]
public class RootController(ImageService imageService) : ControllerBase
{
[HttpGet(template: "{imageName}")]
public async Task<IActionResult> GetImage(string imageName)
{
var image = await imageService.GetImage(imageName);
if (image == null)
return NotFound();
return File(image, MediaTypeNames.Image.Png);
}
}
}
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Mvc;
using RecRoomArchive.Services;
namespace RecRoomArchive.Controllers
{
/// <summary>
/// Only exists for pre-RecNet versions of Rec Room to be able to fetch a Message of the Day from the server...
/// </summary>
[ApiController]
public class RootController(MessageOfTheDayService motdService) : ControllerBase
{
/// <summary>
/// Gets the legacy version of the Message of the Day, to display on the login screen in pre-Dorm Room versions of the game
/// </summary>
/// <returns>A basic string to display in game, related to the Message of the Day</returns>
[HttpGet(template: "motd")]
public async Task<ActionResult<string>> GetMessageOfTheDay()
{
var messageOfTheDay = await motdService.GetMessageOfTheDay();
return Ok(messageOfTheDay);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using RecRoomArchive.Services;
namespace RecRoomArchive
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
#region Services
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<AccountService>();
builder.Services.AddScoped<AppVersionService>();
builder.Services.AddScoped<AuthorizationService>();
builder.Services.AddScoped<ConfigService>();
builder.Services.AddScoped<FileService>();
builder.Services.AddScoped<ImageService>();
builder.Services.AddScoped<MessageOfTheDayService>();
#endregion
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5090",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7040;http://localhost:5090",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RecRoomArchive.Models\RecRoomArchive.Models.csproj" />
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
@RecRoomArchive_HostAddress = http://localhost:5090
GET {{RecRoomArchive_HostAddress}}/weatherforecast/
Accept: application/json
###
+72
View File
@@ -0,0 +1,72 @@
using RecRoomArchive.Models.API.Players;
using System.Security.Cryptography;
using System.Text.Json;
namespace RecRoomArchive.Services
{
public class AccountService
{
public bool AccountExists()
{
return File.Exists("data/profile.json");
}
public bool CreateAccount(string? username = null)
{
PopulateServerData();
if (string.IsNullOrEmpty(username))
{
username = GetRandomUsername();
}
Console.WriteLine($"Creating account for {username}");
var profile = new BaseProfile
{
Id = (ulong)RandomNumberGenerator.GetInt32(1000, 9999999),
Username = username,
DisplayName = username
};
File.WriteAllText("data/profile.json", JsonSerializer.Serialize(profile));
return File.Exists("data/profile.json");
}
public BaseProfile? GetSelfAccount()
{
return JsonSerializer.Deserialize<BaseProfile>(File.ReadAllText("data/profile.json"));
}
private static string GetRandomUsername()
{
int randomFourDigits = RandomNumberGenerator.GetInt32(1000, 9999);
return $"RRA-User_{randomFourDigits}";
}
private static void PopulateServerData()
{
string basePath = "data";
string[] directories = ["rooms", "images", "blobs"];
string[] files = [];
Directory.CreateDirectory(basePath);
foreach (var directory in directories)
{
Directory.CreateDirectory(Path.Combine(basePath, directory));
}
foreach (var file in files)
{
string fullPath = Path.Combine(basePath, file);
if (!File.Exists(fullPath))
{
File.WriteAllText(fullPath, string.Empty);
}
}
}
}
}
@@ -0,0 +1,85 @@
namespace RecRoomArchive.Services
{
/// <summary>
/// Used to get the appVersion from the client to determine how to run the server
/// </summary>
public class AppVersionService
{
/// <summary>
/// AppVersion reference
/// </summary>
private static string? AppVersion { get; set; }
/// <summary>
/// The AppVersion as it would want to be seen by the game
/// </summary>
private static string? FullAppVersion { get; set; }
/// <summary>
/// The BuildTimestamp of the build, this is a long that when converted to ticks, will be the accurate time at which the game was built.
/// This only exists on some builds so I wouldn't rely on it too much
/// </summary>
private static DateTime? BuildTimestamp { get; set; }
/// <summary>
/// The BuildTimestamp as it would want to be seen by the game
/// </summary>
private static long? FullBuildTimestamp { get; set; }
/// <summary>
/// To store the AppVersion of the current build
/// </summary>
/// <param name="appVersion">The version of the game</param>
/// <returns>If the operation was a success</returns>
public async Task<bool> StoreAppVersion(string appVersion)
{
if (appVersion == null)
return false;
// To remove any _EA's or .01's or just any weird Rec Room bullshit from the build's name
var standardizedAppVersion = await ParseAppVersion(appVersion);
// idrk if storing both of these is overkill
FullAppVersion = appVersion;
AppVersion = standardizedAppVersion;
Console.WriteLine($"appVersion: {FullAppVersion}, standardizedAppVersion: {AppVersion}");
return true;
}
/// <summary>
/// To store the BuildTimestamp of the current build
/// </summary>
/// <param name="buildTimestamp">The BuildTimestamp of the game</param>
/// <returns>If the operation was a success</returns>
public async Task<bool> StoreBuildTimestamp(long buildTimestamp)
{
if (buildTimestamp == 0)
return false;
DateTime buildTimestampDateTime = new(buildTimestamp);
// same here
FullBuildTimestamp = buildTimestamp;
BuildTimestamp = buildTimestampDateTime;
Console.WriteLine($"buildTimestamp: {FullBuildTimestamp}, buildTimestampDateTime: {BuildTimestamp}");
return true;
}
/// <summary>
/// Parses the appVersion to "remove any _EA's or .01's or just any weird Rec Room bullshit from the build's name"
/// </summary>
/// <param name="appVersion">The version of the game</param>
/// <returns>The standardized string of the appVersion</returns>
public async Task<string> ParseAppVersion(string appVersion)
{
// To remove any _EA's or .01's or just any weird Rec Room bullshit from the build's name
int index = appVersion.IndexOfAny(['_', '.']);
string standardizedAppVersion = index >= 0 ? appVersion[..index] : appVersion;
return standardizedAppVersion;
}
}
}
@@ -0,0 +1,34 @@
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace RecRoomArchive.Services
{
public class AuthorizationService
{
private static readonly string Key = "hello diddy blud. you need to replace me. or not, really it doesnt matter...I forgot how long a token like this has to be so I'm just gonna run my mouth. Hi. This is RecRoomArchive. You may wonder why we have JWT tokens in a localhost server, and that's because 2020 requires it and stuff. and 2019. Especially 2021. I wonder if I will be doing 2021 or not...";
public string GenerateToken(ulong id)
{
JwtSecurityTokenHandler handler = new();
List<Claim> claims = new List<Claim>()
{
new(ClaimTypes.NameIdentifier, id.ToString()),
new(ClaimTypes.Role, "gameClient")
};
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor()
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.Add(TimeSpan.FromHours(12)),
Issuer = "https://recroomarchive.org/",
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Key)), SecurityAlgorithms.HmacSha256)
};
JwtSecurityToken token = handler.CreateJwtSecurityToken(tokenDescriptor);
return handler.WriteToken(token);
}
}
}
+188
View File
@@ -0,0 +1,188 @@
using RecRoomArchive.Models.API.Config;
namespace RecRoomArchive.Services
{
/// <summary>
/// I don't even want to explain this one. I'm sorry
/// </summary>
public class ConfigService(MessageOfTheDayService motdService, IHttpContextAccessor httpContextAccessor)
{
private readonly MessageOfTheDayService _motdService = motdService;
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
/// <summary>
/// I'm only doing this because I think that having the full Rec Room config in a controller would be very very ugly.
/// </summary>
/// <returns>RecRoomConfig</returns>
public async Task<RecRoomConfig> GetRecRoomConfig()
{
var request = _httpContextAccessor.HttpContext?.Request;
var host = $"{request!.Scheme}://{request.Host}";
return new RecRoomConfig()
{
MessageOfTheDay = await _motdService.GetMessageOfTheDay(),
CdnBaseUri = host,
MatchmakingParams = new MatchmakingParams(),
LevelProgressionMaps =
[
new LevelProgressionMap(0, 0),
new LevelProgressionMap(1, 10),
new LevelProgressionMap(2, 10),
new LevelProgressionMap(3, 10),
new LevelProgressionMap(4, 20),
new LevelProgressionMap(5, 20),
new LevelProgressionMap(6, 20),
new LevelProgressionMap(7, 20),
new LevelProgressionMap(8, 20),
new LevelProgressionMap(9, 20),
new LevelProgressionMap(10, 20),
new LevelProgressionMap(11, 45),
new LevelProgressionMap(12, 45),
new LevelProgressionMap(13, 45),
new LevelProgressionMap(14, 45),
new LevelProgressionMap(15, 45),
new LevelProgressionMap(16, 45),
new LevelProgressionMap(17, 45),
new LevelProgressionMap(18, 45),
new LevelProgressionMap(19, 45),
new LevelProgressionMap(20, 45),
new LevelProgressionMap(21, 115),
new LevelProgressionMap(22, 115),
new LevelProgressionMap(23, 115),
new LevelProgressionMap(24, 115),
new LevelProgressionMap(25, 115),
new LevelProgressionMap(26, 115),
new LevelProgressionMap(27, 115),
new LevelProgressionMap(28, 115),
new LevelProgressionMap(29, 115),
new LevelProgressionMap(30, 115)
],
DailyObjectives =
[
[
new DailyObjective()
{
Type = ObjectiveType.QuestGames_Scifi1,
Score = 1
},
new DailyObjective()
{
Type = ObjectiveType.QuestEnemyKills_Scifi1,
Score = 10
},
new DailyObjective()
{
Type = ObjectiveType.ArenaGames,
Score = 1
}
],
[
new DailyObjective()
{
Type = ObjectiveType.PaintballCTFGames,
Score = 2
},
new DailyObjective()
{
Type = ObjectiveType.FinishActivity,
Score = 1
},
new DailyObjective()
{
Type = ObjectiveType.CheerAPlayer,
Score = 1
}
],
[
new DailyObjective()
{
Type = ObjectiveType.DodgeballGames,
Score = 2
},
new DailyObjective()
{
Type = ObjectiveType.DodgeballWins,
Score = 2
},
new DailyObjective()
{
Type = ObjectiveType.CheerAPlayer,
Score = 1
}
],
[
new DailyObjective()
{
Type = ObjectiveType.PaintballTeamBattleWins,
Score = 2
},
new DailyObjective()
{
Type = ObjectiveType.PaintballTeamBattleGames,
Score = 20
},
new DailyObjective()
{
Type = ObjectiveType.FinishActivity,
Score = 1
}
],
[
new DailyObjective()
{
Type = ObjectiveType.QuestGames_Goblin1,
Score = 1
},
new DailyObjective()
{
Type = ObjectiveType.QuestEnemyKills_Goblin1,
Score = 10
},
new DailyObjective()
{
Type = ObjectiveType.CheerAPlayer,
Score = 1
}
],
[
new DailyObjective()
{
Type = ObjectiveType.PaintballAnyModeGames,
Score = 2
},
new DailyObjective()
{
Type = ObjectiveType.PaintballAnyModeHits,
Score = 20
},
new DailyObjective()
{
Type = ObjectiveType.ArenaGames,
Score = 1
}
],
[
new DailyObjective()
{
Type = ObjectiveType.QuestGames_Goblin2,
Score = 1
},
new DailyObjective()
{
Type = ObjectiveType.QuestEnemyKills_Goblin2,
Score = 10
},
new DailyObjective()
{
Type = ObjectiveType.FinishActivity,
Score = 1
}
]
],
ConfigTable = [],
PhotonConfig = new PhotonConfig()
};
}
}
}
+58
View File
@@ -0,0 +1,58 @@
using System.Xml.Linq;
namespace RecRoomArchive.Services
{
public class FileService
{
private static void PopulateServerData()
{
string basePath = "data";
string[] directories = ["rooms", "images", "blobs"];
string[] files = ["rooms.json", "avatar.json", "settings.json", "profile.json"];
Directory.CreateDirectory(basePath);
foreach (var directory in directories)
{
Directory.CreateDirectory(Path.Combine(basePath, directory));
}
foreach (var file in files)
{
string fullPath = Path.Combine(basePath, file);
if (!File.Exists(fullPath))
{
File.WriteAllText(fullPath, string.Empty);
}
}
}
public string? GetData(string name)
{
var path = $"data/{name}";
if (!File.Exists(path))
return null;
return File.ReadAllText(path);
}
public void SetData(string name, string data)
{
var path = $"data/{name}";
File.WriteAllText(path, data);
}
public bool FileExists(string name)
{
var path = $"data/{name}";
if (!File.Exists(path))
return false;
return true;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
namespace RecRoomArchive.Services
{
public class ImageService(FileService fileService)
{
private static readonly HttpClient httpClient = new();
private readonly FileService _fileService = fileService;
public async Task<Stream?> GetImage(string imageName)
{
var path = $"images/{imageName}";
if (!_fileService.FileExists(path))
{
var response = await httpClient.GetAsync($"https://cdn.rec.net/img/{imageName}");
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadAsStreamAsync();
}
return new FileStream(Path.Combine("data", path), FileMode.Open, FileAccess.Read, FileShare.Read);
}
public async Task<string> SaveImageAsync(Stream imageStream)
{
var imageName = $"{GetRandomFileName()}.jpg";
var path = ("data/images");
var fullPath = Path.Combine(path, imageName);
await using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 81920, useAsync: true);
await imageStream.CopyToAsync(fileStream);
return imageName;
}
private static string GetRandomFileName()
{
return Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('+', '-').Replace('/', '_').TrimEnd('=');
}
}
}
@@ -0,0 +1,62 @@
using RecRoomArchive.Models.API.Activities;
using System.Text.Json;
namespace RecRoomArchive.Services
{
/// <summary>
/// Service used to get the message of the day as it is used in multiple areas of the server
/// </summary>
public class MessageOfTheDayService
{
/// <summary>
/// HttpClient for making requests to Gitea
/// </summary>
private static readonly HttpClient httpClient = new HttpClient();
/// <summary>
/// MessageOfTheDay reference
/// </summary>
private static string? MessageOfTheDay { get; set; }
/// <summary>
/// Gets the message of the day from Gitea. If the URL cannot be resolved, it will fall back to "Welcome to RecRoomArchive!"
/// </summary>
/// <returns>String related to the Message of the Day</returns>
public async Task<string> GetMessageOfTheDay(string? version = null)
{
// I wouldn't want to re-request the MOTD from the server a bunch of times...
if (string.IsNullOrEmpty(MessageOfTheDay))
{
var motd = await httpClient.GetAsync($"https://git.recroomarchive.org/RecRoomArchive/RRAC/raw/branch/main/MOTD");
if (!motd.IsSuccessStatusCode)
return "Welcome to RecRoomArchive!";
MessageOfTheDay = await motd.Content.ReadAsStringAsync();
}
return MessageOfTheDay;
}
// move out of motdservice
private DateTime CharadesWordsLastFetchedAt { get; set; }
private List<CharadesWord> CachedCharadesWords { get; set; } = [];
public async Task <List<CharadesWord>> GetCharadesWordsList()
{
if (CharadesWordsLastFetchedAt - DateTime.UtcNow > TimeSpan.FromMinutes(30))
return CachedCharadesWords;
var request = await httpClient.GetAsync("https://git.recroomarchive.org/RecRoomArchive/RRAC/raw/branch/main/CharadesWords");
if (!request.IsSuccessStatusCode)
return [];
var words = await request.Content.ReadAsStringAsync();
if (words == null)
return [];
CachedCharadesWords = JsonSerializer.Deserialize<List<CharadesWord>>(words)!;
return CachedCharadesWords;
}
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+1
View File
@@ -0,0 +1 @@
{"OutfitSelections":"e36bcd98-7e85-43fa-89f8-57e4ec33823a,,,,0;c6c08eb5-381a-4193-9722-80da95d62abe,,,,1;d0a9262f-5504-46a7-bb10-7507503db58e,,,,1;40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,018a5c07-e956-457d-a540-a5e2cd68da09,,0","SkinColor":"2d398478-37c4-4c4a-a471-fbcbe3e5b1f5","HairColor":"5e51ac88-e365-418b-a303-8737fd2e6bc8"}
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+1
View File
@@ -0,0 +1 @@
{"Id":1775080,"Username":"splootybean","DisplayName":"city boy","XP":0,"Level":1,"Reputation":1,"Verified":true,"Developer":true,"Bio":"","RegistrationStatus":2,"CanReceiveInvites":true,"ProfileImageName":"Mh6wZ4jA6k2bx-nwb__YnA.jpg","JuniorProfile":false,"ForceJuniorImages":false,"PendingJunior":false,"HasBirthday":true,"HasEmail":true}
+1
View File
@@ -0,0 +1 @@
[{"Key":"google_analytics_clientid_pref_key","Value":"JPOTDRUNb2592478225be25d5b5950fcf20d6f2601e231d2"},{"Key":"DAILY_LOGIN_DATE","Value":"57"},{"Key":"OBJECTIVE_DATE","Value":"57"},{"Key":"OBJECTIVE_PROGRESS0","Value":"1"},{"Key":"OBJECTIVE_COMPLETED0","Value":"1"},{"Key":"OBJECTIVE_PROGRESS1","Value":"0"},{"Key":"OBJECTIVE_COMPLETED1","Value":"0"},{"Key":"OBJECTIVE_PROGRESS2","Value":"0"},{"Key":"OBJECTIVE_COMPLETED2","Value":"0"},{"Key":"Recroom.OOBE","Value":"10"},{"Key":"PlayerSessionCount","Value":"10"}]