diff --git a/RecRoomArchive.Models/API/Activities/CharadesWord.cs b/RecRoomArchive.Models/API/Activities/CharadesWord.cs new file mode 100644 index 0000000..58f507f --- /dev/null +++ b/RecRoomArchive.Models/API/Activities/CharadesWord.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Activities +{ + public class CharadesWord(string word, CharadesWordsDifficulty difficulty = CharadesWordsDifficulty.Easy) + { + [JsonPropertyName(name: "Difficulty")] public CharadesWordsDifficulty Difficulty { get; set; } = difficulty; + [JsonPropertyName(name: "EN_US")] public string EN_US { get; set; } = word; + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Activities/CharadesWordsDifficulty.cs b/RecRoomArchive.Models/API/Activities/CharadesWordsDifficulty.cs new file mode 100644 index 0000000..0c151ab --- /dev/null +++ b/RecRoomArchive.Models/API/Activities/CharadesWordsDifficulty.cs @@ -0,0 +1,8 @@ +namespace RecRoomArchive.Models.API.Activities +{ + public enum CharadesWordsDifficulty + { + Easy, + Hard + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Avatar/PlayerAvatar.cs b/RecRoomArchive.Models/API/Avatar/PlayerAvatar.cs new file mode 100644 index 0000000..d02995f --- /dev/null +++ b/RecRoomArchive.Models/API/Avatar/PlayerAvatar.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Avatar +{ + /// + /// The avatar of the player + /// + public class PlayerAvatar + { + /// + /// The outfit that the player has on. This includes data like their hair model, torso, hats, glasses, torso, etc... + /// + [JsonPropertyName(name: "OutfitSelections")] public string OutfitSelections { get; set; } = string.Empty; + /// + /// The skin color guid of the player + /// + [JsonPropertyName(name: "SkinColor")] public string SkinColor { get; set; } = string.Empty; + /// + /// The hair color guid of the player + /// + [JsonPropertyName(name: "HairColor")] public string HairColor { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Config/DailyObjective.cs b/RecRoomArchive.Models/API/Config/DailyObjective.cs new file mode 100644 index 0000000..4c26d38 --- /dev/null +++ b/RecRoomArchive.Models/API/Config/DailyObjective.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Config +{ + public class DailyObjective + { + [JsonPropertyName(name: "type")] public ObjectiveType Type { get; set; } + [JsonPropertyName(name: "score")] public int Score { get; set; } + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Config/GameConfig.cs b/RecRoomArchive.Models/API/Config/GameConfig.cs new file mode 100644 index 0000000..2aae8bd --- /dev/null +++ b/RecRoomArchive.Models/API/Config/GameConfig.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Config +{ + public class GameConfig + { + [JsonPropertyName(name: "Key")] public required string Key { get; set; } + [JsonPropertyName(name: "Value")] public required string Value { get; set; } + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Config/LevelProgressionMap.cs b/RecRoomArchive.Models/API/Config/LevelProgressionMap.cs new file mode 100644 index 0000000..94fe336 --- /dev/null +++ b/RecRoomArchive.Models/API/Config/LevelProgressionMap.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Config +{ + public class LevelProgressionMap(int level, int requiredXp) + { + [JsonPropertyName(name: "Level")] public int Level { get; set; } = level; + [JsonPropertyName(name: "RequiredXp")] public int RequiredXp { get; set; } = requiredXp; + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Config/MatchmakingParams.cs b/RecRoomArchive.Models/API/Config/MatchmakingParams.cs new file mode 100644 index 0000000..b0e70db --- /dev/null +++ b/RecRoomArchive.Models/API/Config/MatchmakingParams.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Config +{ + public class MatchmakingParams + { + [JsonPropertyName(name: "PreferFullRoomsFrequency")] public float PreferFullRoomsFrequency { get; set; } = 1.0f; + [JsonPropertyName(name: "PreferEmptyRoomsFrequency")] public float PreferEmptyRoomsFrequency { get; set; } = 0.0f; + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Config/ObjectiveType.cs b/RecRoomArchive.Models/API/Config/ObjectiveType.cs new file mode 100644 index 0000000..64115ce --- /dev/null +++ b/RecRoomArchive.Models/API/Config/ObjectiveType.cs @@ -0,0 +1,139 @@ +namespace RecRoomArchive.Models.API.Config +{ + public enum ObjectiveType + { + Default = -1, + FirstSessionOfDay = 1, + AddAFriend, + PartyUp, + AllOtherChallenges, + LevelUp, + CheerAPlayer, + PointedAtPlayer, + CheerARoom, + SubscribeToPlayer, + DailyObjective1, + DailyObjective2, + DailyObjective3, + AllDailyObjectives, + CompleteAnyDaily, + CompleteAnyWeekly, + OOBE_GoToLockerRoom = 20, + OOBE_GoToActivity, + OOBE_FinishActivity, + NUX_PunchcardObjective = 25, + NUX_AllPunchcardObjectives, + GoToRecCenter = 30, + FinishActivity, + VisitACustomRoom, + CreateACustomRoom, + ScoreBasketInRecCenter = 35, + UploadPhotoToRecNet, + UpdatePlayerBio, + SaveOutfitSlot, + PurchaseClothingItem, + PurchaseNonClothingItem, + DrinkWater, + ColorOnWhiteboard, + SetBasketballSkin, + ThrowBasketball, + PlaceInventionInDorm, + ChangeDormRoomSkin, + ToggleOwnedClothes, + EquipHat, + LoadOutfit, + SaveNewOutfitSlot, + SpawnCamera, + TakeSelfie, + PrintSelfie, + TakePictureOfPlayer, + PrintPictureOfPlayer, + PublishSelfieWithPlayer, + SpawnFoodWithOtherPlayers, + EmoteInRecCenter, + SendRoomChatInRecCenter, + UseFrendotron, + GoToDormRoom, + VisitSpecificRoom, + VisitPublicRRO, + VisitPublicRoomBySource, + FavoriteARoom, + TakePhotoWithFilter, + OpenYourPlayerProfile, + OpenOnlineStatusModal, + ChangeProfilePicture, + ChangePlayerDisplayName, + ChangePlayerDescriptionText, + OpenPlayerPronounsModal, + OpenOtherPlayersProfile, + VisitPlayersPortfolio, + FavoriteAFriend, + CharadesGames = 100, + CharadesWinsPerformer, + CharadesWinsGuesser, + DiscGolfWins = 200, + DiscGolfGames, + DiscGolfHolesUnderPar, + DodgeballWins = 300, + DodgeballGames, + DodgeballHits, + PaddleballGames = 400, + PaddleballWins, + PaddleballScores, + PaintballAnyModeGames = 500, + PaintballAnyModeWins, + PaintballAnyModeHits, + PaintballCTFWins = 600, + PaintballCTFGames, + PaintballCTFHits, + PaintballFlagCaptures, + PaintballTeamBattleWins = 700, + PaintballTeamBattleGames, + PaintballTeamBattleHits, + PaintballFreeForAllWins = 710, + PaintballFreeForAllGames, + PaintballFreeForAllHits, + SoccerWins = 800, + SoccerGames, + SoccerGoals, + BowlingGames = 900, + BowlingWins, + BowlingStrike, + QuestGames = 1000, + QuestWins, + QuestPlayerRevives, + QuestEnemyKills, + QuestGames_Goblin1 = 1010, + QuestWins_Goblin1, + QuestPlayerRevives_Goblin1, + QuestEnemyKills_Goblin1, + QuestGames_Goblin2 = 1020, + QuestWins_Goblin2, + QuestPlayerRevives_Goblin2, + QuestEnemyKills_Goblin2, + QuestGames_Scifi1 = 1030, + QuestWins_Scifi1, + QuestPlayerRevives_Scifi1, + QuestEnemyKills_Scifi1, + QuestGames_Pirate1 = 1040, + QuestWins_Pirate1, + QuestPlayerRevives_Pirate1, + QuestEnemyKills_Pirate1, + QuestGames_Dracula1 = 1050, + QuestWins_Dracula1, + QuestPlayerRevives_Dracula1, + QuestEnemyKills_Dracula1, + ArenaGames = 2000, + ArenaWins, + ArenaPlayerRevives, + ArenaHeroTags, + ArenaBotTags, + RecRoyaleGames = 3000, + RecRoyaleWins, + RecRoyaleTags, + StuntRunnerGames = 4000, + StuntRunnerWins, + RecRallyGames = 5000, + RecRallyWins + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Config/PhotonConfig.cs b/RecRoomArchive.Models/API/Config/PhotonConfig.cs new file mode 100644 index 0000000..8c68fff --- /dev/null +++ b/RecRoomArchive.Models/API/Config/PhotonConfig.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Config +{ + public class PhotonConfig + { + [JsonPropertyName(name: "CloudRegion")] public string CloudRegion { get; set; } = "us"; + [JsonPropertyName(name: "CrcCheckEnabled")] public bool CrcCheckEnabled { get; set; } = true; + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Config/RecRoomConfig.cs b/RecRoomArchive.Models/API/Config/RecRoomConfig.cs new file mode 100644 index 0000000..ac073fd --- /dev/null +++ b/RecRoomArchive.Models/API/Config/RecRoomConfig.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; +using System.Xml.Linq; + +namespace RecRoomArchive.Models.API.Config +{ + public class RecRoomConfig + { + [JsonPropertyName(name: "MessageOfTheDay")] public string MessageOfTheDay { get; set; } = string.Empty; + [JsonPropertyName(name: "CdnBaseUri")] public string CdnBaseUri { get; set; } = string.Empty; + [JsonPropertyName(name: "MatchmakingParams")] public required MatchmakingParams MatchmakingParams { get; set; } + [JsonPropertyName(name: "LevelProgressionMaps")] public required List LevelProgressionMaps { get; set; } + [JsonPropertyName(name: "DailyObjectives")] public required DailyObjective[][] DailyObjectives { get; set; } + [JsonPropertyName(name: "PhotonConfig")] public required PhotonConfig PhotonConfig { get; set; } + [JsonPropertyName(name: "ConfigTable")] public required List ConfigTable { get; set; } + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Messages/GameInviteResponseDetails.cs b/RecRoomArchive.Models/API/Messages/GameInviteResponseDetails.cs new file mode 100644 index 0000000..ff59e90 --- /dev/null +++ b/RecRoomArchive.Models/API/Messages/GameInviteResponseDetails.cs @@ -0,0 +1,12 @@ +namespace RecRoomArchive.Models.API.Messages +{ + public enum GameInviteResponseDetails + { + Declined, + Accepted, + ComeToMe, + GiveMe2, + GiveMe5, + GiveMe10 + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Messages/MessageType.cs b/RecRoomArchive.Models/API/Messages/MessageType.cs new file mode 100644 index 0000000..0ba092c --- /dev/null +++ b/RecRoomArchive.Models/API/Messages/MessageType.cs @@ -0,0 +1,9 @@ +namespace RecRoomArchive.Models.API.Messages +{ + public enum MessageType + { + GameInvite, + GameInviteResponse, + GameJoinFailed + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Platform/PlatformMask.cs b/RecRoomArchive.Models/API/Platform/PlatformMask.cs new file mode 100644 index 0000000..3f7ead2 --- /dev/null +++ b/RecRoomArchive.Models/API/Platform/PlatformMask.cs @@ -0,0 +1,21 @@ +namespace RecRoomArchive.Models.API.Platform +{ + /// + /// The types of platforms that Rec Room has support for...as a mask! + /// + [Flags] + public enum PlatformMask + { + None = 0, + Steam = 1, + Oculus = 2, + PlayStation = 4, + Xbox = 8, + RecNet = 16, + IOS = 32, + GooglePlay = 64, + Standalone = 128, + Pico = 256, + All = -1 + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Platform/PlatformType.cs b/RecRoomArchive.Models/API/Platform/PlatformType.cs new file mode 100644 index 0000000..1f82adb --- /dev/null +++ b/RecRoomArchive.Models/API/Platform/PlatformType.cs @@ -0,0 +1,19 @@ +namespace RecRoomArchive.Models.API.Platform +{ + /// + /// The types of platforms that Rec Room has support for + /// + public enum PlatformType + { + All = -1, + Steam, + Oculus, + PlayStation, + Xbox, + RecNet, + IOS, + GooglePlay, + Standalone, + Pico + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/PlatformLogin/Requests/BaseLoginRequest.cs b/RecRoomArchive.Models/API/PlatformLogin/Requests/BaseLoginRequest.cs new file mode 100644 index 0000000..b8d6d35 --- /dev/null +++ b/RecRoomArchive.Models/API/PlatformLogin/Requests/BaseLoginRequest.cs @@ -0,0 +1,16 @@ +using RecRoomArchive.Models.API.Platform; + +namespace RecRoomArchive.Models.API.PlatformLogin.Requests +{ + public class BaseLoginRequest + { + public required PlatformType Platform { get; set; } + public required string PlatformId { get; set; } + public string? Name { get; set; } + public required long ClientTimestamp { get; set; } + public required string DeviceId { get; set; } + public required long BuildTimestamp { get; set; } + public required string AuthParams { get; set; } + public required string Verify { get; set; } + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/PlatformLogin/Responses/BaseLoginResponse.cs b/RecRoomArchive.Models/API/PlatformLogin/Responses/BaseLoginResponse.cs new file mode 100644 index 0000000..b0d4ea9 --- /dev/null +++ b/RecRoomArchive.Models/API/PlatformLogin/Responses/BaseLoginResponse.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.PlatformLogin.Responses +{ + public class BaseLoginResponse + { + [JsonPropertyName(name: "Token")] + public string Token { get; set; } = string.Empty; + + [JsonPropertyName(name: "PlayerId")] + public ulong PlayerId { get; set; } + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Players/AugustProfile.cs b/RecRoomArchive.Models/API/Players/AugustProfile.cs new file mode 100644 index 0000000..0e4a1e3 --- /dev/null +++ b/RecRoomArchive.Models/API/Players/AugustProfile.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Players +{ + /// + /// Extensions of data used in August 2016 Rec Room + /// + public class AugustProfile : BaseProfile + { + public AugustProfile(BaseProfile baseProfile) + { + Id = baseProfile.Id; + Username = baseProfile.Username; + DisplayName = baseProfile.DisplayName; + XP = baseProfile.XP; + Level = baseProfile.Level; + Reputation = baseProfile.Reputation; + Developer = baseProfile.Developer; + Bio = baseProfile.Bio; + RegistrationStatus = baseProfile.RegistrationStatus; + CanReceiveInvites = baseProfile.CanReceiveInvites; + ProfileImageName = baseProfile.ProfileImageName; + JuniorProfile = baseProfile.JuniorProfile; + ForceJuniorImages = baseProfile.ForceJuniorImages; + PendingJunior = baseProfile.PendingJunior; + HasBirthday = baseProfile.HasBirthday; + HasEmail = baseProfile.HasEmail; + AvoidJuniors = baseProfile.AvoidJuniors; + } + + /// + /// Used in early versions of Rec Room from 2016 as the players DisplayName + /// + [JsonPropertyName(name: "Name")] public string Name => DisplayName; + /// + /// The SteamID of the local player, used in 2016 + /// + [JsonPropertyName(name: "SteamID")] public ulong SteamID { get; set; } + /// + /// The..gender?? of the local player, this isn't used by Rec Room at all so I'm guessing whoever made their webmanager just added this to add it + /// + [JsonPropertyName(name: "Gender")] public string Gender { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Players/BaseProfile.cs b/RecRoomArchive.Models/API/Players/BaseProfile.cs new file mode 100644 index 0000000..9485096 --- /dev/null +++ b/RecRoomArchive.Models/API/Players/BaseProfile.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Players +{ + /// + /// The profile of the player from 2016-2019 + /// + public class BaseProfile + { + /// + /// The unique id of the player + /// + [JsonPropertyName(name: "Id")] public ulong Id { get; set; } + /// + /// The username of the player. This usually doesn't show up in game until around 2017... + /// + [JsonPropertyName(name: "Username")] public string Username { get; set; } = string.Empty; + /// + /// The display name of the player, this is what's most commonly used in game and is seen on the players nametag + /// + [JsonPropertyName(name: "DisplayName")] public string DisplayName { get; set; } = string.Empty; + /// + /// The XP of the player, this determines how much XP is required until the next level up but because this is a local server, who gaf + /// + [JsonPropertyName(name: "XP")] public int XP { get; set; } = 0; + /// + /// The level of the player, usually in a range from 1-30 or 1-50 depending on your time period + /// + [JsonPropertyName(name: "Level")] public int Level { get; set; } = 1; + /// + /// The internal reputation of the player, reputation works in mysterious ways so I'm not too sure about this one... + /// + [JsonPropertyName(name: "Reputation")] public float Reputation { get; set; } = 1.0f; + /// + /// If the player has a verified email on their Rec Room account (which they always will) + /// + [JsonPropertyName(name: "Verified")] public bool Verified => RegistrationStatus == RegistrationStatus.Registered; + /// + /// If the player is a developer of Rec Room (they always will be) + /// + [JsonPropertyName(name: "Developer")] public bool Developer { get; set; } = true; + + /// + /// The bio of the player, I need to find what build this is added in so I can add it to its model... + /// + [JsonPropertyName(name: "Bio")] public string Bio { get; set; } = string.Empty; + /// + /// The registration status of the player, determined by if they have an email + /// + [JsonPropertyName(name: "RegistrationStatus")] public RegistrationStatus RegistrationStatus { get; set; } = RegistrationStatus.Registered; + /// + /// If the local player is allowed to recieve invites (junior restriction?) + /// + [JsonPropertyName(name: "CanReceiveInvites")] public bool CanReceiveInvites { get; set; } = true; + /// + /// The image name of the player + /// + [JsonPropertyName(name: "ProfileImageName")] public string ProfileImageName { get; set; } = "DefaultProfileImage"; + /// + /// If the player is allowed to recieve invites (junior restriction?) + /// + [JsonPropertyName(name: "JuniorProfile")] public bool JuniorProfile { get; set; } = false; + /// + /// If the player is forced to only see alt images like a junior player + /// + [JsonPropertyName(name: "ForceJuniorImages")] public bool ForceJuniorImages { get; set; } = false; + /// + /// If the player is about to become a junior + /// + [JsonPropertyName(name: "PendingJunior")] public bool PendingJunior { get; set; } = false; + /// + /// If the player has a birthday on their account + /// + [JsonPropertyName(name: "HasBirthday")] public bool HasBirthday { get; set; } = true; + /// + /// If the player has an email on their account + /// + [JsonPropertyName(name: "HasEmail")] public bool HasEmail { get; set; } = true; + /// + /// If the player perfers to not matchmake with junior players, can be null + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull), JsonPropertyName(name: "AvoidJuniors")] public bool? AvoidJuniors { get; set; } + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Players/BlockDurationDTO.cs b/RecRoomArchive.Models/API/Players/BlockDurationDTO.cs new file mode 100644 index 0000000..2860587 --- /dev/null +++ b/RecRoomArchive.Models/API/Players/BlockDurationDTO.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Players +{ + public class BlockDurationDTO + { + [JsonPropertyName(name: "BlockedDuration")] public int BlockedDuration { get; set; } = 0; + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Players/PhoneNumberDTO.cs b/RecRoomArchive.Models/API/Players/PhoneNumberDTO.cs new file mode 100644 index 0000000..7d40354 --- /dev/null +++ b/RecRoomArchive.Models/API/Players/PhoneNumberDTO.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Players +{ + public class PhoneNumberDTO + { + [JsonPropertyName(name: "PhoneNumber")] public string PhoneNumber { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Players/RegistrationStatus.cs b/RecRoomArchive.Models/API/Players/RegistrationStatus.cs new file mode 100644 index 0000000..06a1fef --- /dev/null +++ b/RecRoomArchive.Models/API/Players/RegistrationStatus.cs @@ -0,0 +1,21 @@ +namespace RecRoomArchive.Models.API.Players +{ + /// + /// The status of the players registration to Rec Room + /// + public enum RegistrationStatus + { + /// + /// This player has no email entered for Rec Room and may be prompted to enter one + /// + Unregistered, + /// + /// This player has a pending email from Rec Room that they have not accepted yet + /// + PendingEmailVerification, + /// + /// This player has a verified Rec Room Profile! + /// + Registered + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/API/Setting/Setting.cs b/RecRoomArchive.Models/API/Setting/Setting.cs new file mode 100644 index 0000000..0990676 --- /dev/null +++ b/RecRoomArchive.Models/API/Setting/Setting.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.API.Setting +{ + /// + /// The players local settings and preferences + /// + public class Setting + { + /// + /// The key of the setting (ex: DebugGuiEnabled) + /// + [JsonPropertyName(name: "Key")] public required string Key { get; set; } + /// + /// The value related to the key (ex: true) + /// + [JsonPropertyName(name: "Value")] public required string Value { get; set; } + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/Common/OkResponse.cs b/RecRoomArchive.Models/Common/OkResponse.cs new file mode 100644 index 0000000..dfca52c --- /dev/null +++ b/RecRoomArchive.Models/Common/OkResponse.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace RecRoomArchive.Models.Common +{ + public class OkResponse + { + public static OkResponse Ok(string? message = null) => new() { Success = true, Message = message }; + public static OkResponse Fail(string message) => new() { Success = false, Message = message }; + + [JsonPropertyName("Success")] public bool Success { get; set; } + [JsonPropertyName("Message")] public string? Message { get; set; } + } +} \ No newline at end of file diff --git a/RecRoomArchive.Models/RecRoomArchive.Models.csproj b/RecRoomArchive.Models/RecRoomArchive.Models.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/RecRoomArchive.Models/RecRoomArchive.Models.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/RecRoomArchive.slnx b/RecRoomArchive.slnx new file mode 100644 index 0000000..c90c7a0 --- /dev/null +++ b/RecRoomArchive.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/RecRoomArchive/Controllers/API/Activities/Charades/V1/ActivitiesController.cs b/RecRoomArchive/Controllers/API/Activities/Charades/V1/ActivitiesController.cs new file mode 100644 index 0000000..1e4b912 --- /dev/null +++ b/RecRoomArchive/Controllers/API/Activities/Charades/V1/ActivitiesController.cs @@ -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>> GetCharadesWords() + { + return Ok(await motdService.GetCharadesWordsList()); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Avatar/V2/AvatarController.cs b/RecRoomArchive/Controllers/API/Avatar/V2/AvatarController.cs new file mode 100644 index 0000000..13e27d5 --- /dev/null +++ b/RecRoomArchive/Controllers/API/Avatar/V2/AvatarController.cs @@ -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> 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(avatarData); + return Ok(avatar); + } + + [HttpGet(template: "gifts")] + public async Task>> GetPendingGifts() + { + return Ok(new List()); + } + + [HttpPost(template: "set")] + public async Task> SetAvatar(PlayerAvatar avatar) + { + fileService.SetData("avatar.json", JsonSerializer.Serialize(avatar)); + return Ok(); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Avatar/V3/AvatarController.cs b/RecRoomArchive/Controllers/API/Avatar/V3/AvatarController.cs new file mode 100644 index 0000000..a9274af --- /dev/null +++ b/RecRoomArchive/Controllers/API/Avatar/V3/AvatarController.cs @@ -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>> GetUnlockedAvatarItems() + { + return Ok(new List()); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Config/V1/ConfigController.cs b/RecRoomArchive/Controllers/API/Config/V1/ConfigController.cs new file mode 100644 index 0000000..3b00a11 --- /dev/null +++ b/RecRoomArchive/Controllers/API/Config/V1/ConfigController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using RecRoomArchive.Services; + +namespace RecRoomArchive.Controllers.API.Config.V1 +{ + /// + /// Configs that control Rec Room. These configs include data like Amplitude, MessageOfTheDay, Objectives, etc... + /// + [Route(template: "api/[controller]/v1")] + [ApiController] + public class ConfigController(MessageOfTheDayService motdService) : ControllerBase + { + /// + /// 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 + /// + /// A basic string to display in game, related to the Message of the Day + [HttpGet(template: "motd")] + public async Task> GetMessageOfTheDay() + { + var messageOfTheDay = await motdService.GetMessageOfTheDay(); + return Ok(messageOfTheDay); + } + + /// + /// Returns the current daily objectives to use before config/v2 existed + /// + /// A list of objectives + [HttpGet(template: "objectives")] + public async Task>> GetDailyObjectives() + { + return Ok(new List()); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Config/V2/ConfigController.cs b/RecRoomArchive/Controllers/API/Config/V2/ConfigController.cs new file mode 100644 index 0000000..0bb513b --- /dev/null +++ b/RecRoomArchive/Controllers/API/Config/V2/ConfigController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using RecRoomArchive.Models.API.Config; +using RecRoomArchive.Services; + +namespace RecRoomArchive.Controllers.API.Config.V2 +{ + /// + /// Configs that control Rec Room. These configs include data like Amplitude, MessageOfTheDay, Objectives, etc... + /// + [Route(template: "api/[controller]/v2")] + [ApiController] + public class ConfigController(ConfigService configService) : ControllerBase + { + /// + /// Gets the RecRoomConfig, containing lots of data on how the game should function + /// + /// RecRoomConfig + [HttpGet] + public async Task> GetRecRoomConfig() + { + var recRoomConfig = await configService.GetRecRoomConfig(); + return Ok(recRoomConfig); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Images/V2/ImagesController.cs b/RecRoomArchive/Controllers/API/Images/V2/ImagesController.cs new file mode 100644 index 0000000..1e6d3f3 --- /dev/null +++ b/RecRoomArchive/Controllers/API/Images/V2/ImagesController.cs @@ -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 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(profileData)!; + + profile.ProfileImageName = imageName; + + fileService.SetData("profile.json", JsonSerializer.Serialize(profile)); + + return Ok(); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Messages/V2/MessagesController.cs b/RecRoomArchive/Controllers/API/Messages/V2/MessagesController.cs new file mode 100644 index 0000000..070a57b --- /dev/null +++ b/RecRoomArchive/Controllers/API/Messages/V2/MessagesController.cs @@ -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>> GetMessages() + { + return new List(); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/PlatformLogin/V2/PlatformLoginController.cs b/RecRoomArchive/Controllers/API/PlatformLogin/V2/PlatformLoginController.cs new file mode 100644 index 0000000..ce192f8 --- /dev/null +++ b/RecRoomArchive/Controllers/API/PlatformLogin/V2/PlatformLoginController.cs @@ -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 +{ + /// + /// Used to login to accounts on Rec Room + /// + [Route(template: "api/[controller]/v2")] + [ApiController] + public class PlatformLoginController(AppVersionService appVersionService, AccountService accountService, AuthorizationService authorizationService) : ControllerBase + { + /// + /// 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) + /// + /// 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 + [HttpPost] + public async Task 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 + }); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Players/PlayersController.cs b/RecRoomArchive/Controllers/API/Players/PlayersController.cs new file mode 100644 index 0000000..1042ef6 --- /dev/null +++ b/RecRoomArchive/Controllers/API/Players/PlayersController.cs @@ -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 +{ + /// + /// Used in August 2016 RecNet + /// + [Route(template: "api/[controller]")] + [ApiController] + public class PlayersController(AccountService accountService) : ControllerBase + { + /// + /// Returns the profile tied to a SteamID (If it exists) + /// + /// The SteamID of the player we are trying to get the profile of + /// The profile of the player, if it exists + [HttpGet] + public async Task> GetProfile([Required, FromQuery] ulong steamId) + { + var baseProfile = accountService.GetSelfAccount(); + if (baseProfile == null) + return NotFound(); + + var profile = new AugustProfile(baseProfile) + { + SteamID = steamId + }; + + return profile; + } + + /// + /// Creates a profile and links it to a SteamID if the player does not already exist + /// + /// The SteamID of the player that we are creating a profile for + /// The Steam username of the player that we are creating a profile for + /// A new AugustProfile + [HttpPost] + public async Task> 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; + } + + /// + /// Stores data related to the player onto the server + /// + /// The Id of the Profile we are storing data for + /// The data the client posts...it happens to be an entire profile but we can take only the data we need :) + /// A successful response, likely will just return the updated profile + [HttpPut(template: "{profileId:long}")] + public async Task> UpdateProfile([Required] ulong profileId, [FromBody] AugustProfile model) + { + return (AugustProfile)accountService.GetSelfAccount()!; + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Players/V1/PlayersController.cs b/RecRoomArchive/Controllers/API/Players/V1/PlayersController.cs new file mode 100644 index 0000000..fb898cc --- /dev/null +++ b/RecRoomArchive/Controllers/API/Players/V1/PlayersController.cs @@ -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 +{ + /// + /// Used to get accounts / profiles in 2016-2020 although gets phased out as time goes on... + /// + [Route(template: "api/[controller]/v1")] + [ApiController] + public class PlayersController(AccountService accountService) : ControllerBase + { + /// + /// Returns the profile of this ID (If it exists) + /// + /// The ID of the player we are trying to get the profile of + /// The profile of the player, if it exists + [HttpGet(template: "{id:long}")] + public async Task> GetProfile([Required] ulong id) + { + return accountService.GetSelfAccount()!; + } + + [HttpPost(template: "listByPlatformId")] + public async Task>> GetFromServer([FromForm] PlatformType platform, [FromForm] List platformIds) + { + return Ok(new List()); + } + + [HttpGet(template: "search/{query}")] + public async Task>> SearchForProfiles(string query) + { + return Ok(new List()); + } + + [HttpGet(template: "phoneLastFour")] + public async Task> GetPhoneLastFour() + { + return Ok(new PhoneNumberDTO()); + } + + [HttpGet(template: "blockDuration")] + public async Task> GetBlockDuration() + { + return Ok(new BlockDurationDTO()); + } + + [HttpPost(template: "objectives")] + public async Task CompleteObjectives(object objective) + { + return Ok(); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Players/V2/PlayersController.cs b/RecRoomArchive/Controllers/API/Players/V2/PlayersController.cs new file mode 100644 index 0000000..4ce7e18 --- /dev/null +++ b/RecRoomArchive/Controllers/API/Players/V2/PlayersController.cs @@ -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 +{ + /// + /// Used to modify accounts / profiles in 2017-2019 although gets phased out as time goes on... + /// + [Route(template: "api/[controller]/v2")] + [ApiController] + public class PlayersController(FileService fileService) : ControllerBase + { + /// + /// Sets the players displayName + /// + /// The displayName the player is requesting + /// OkResponse + [HttpPost(template: "displayName")] + public async Task> 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(profileData)!; + + profile.DisplayName = name; + + fileService.SetData("profile.json", JsonSerializer.Serialize(profile)); + + return Ok(OkResponse.Ok()); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Relationships/V2/RelationshipController.cs b/RecRoomArchive/Controllers/API/Relationships/V2/RelationshipController.cs new file mode 100644 index 0000000..f46276e --- /dev/null +++ b/RecRoomArchive/Controllers/API/Relationships/V2/RelationshipController.cs @@ -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>> GetRelationships() + { + return new List(); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/Settings/V2/SettingsController.cs b/RecRoomArchive/Controllers/API/Settings/V2/SettingsController.cs new file mode 100644 index 0000000..d5eed26 --- /dev/null +++ b/RecRoomArchive/Controllers/API/Settings/V2/SettingsController.cs @@ -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>> GetSettings() + { + var settingsData = fileService.GetData("settings.json"); + + if (string.IsNullOrWhiteSpace(settingsData)) + { + return Ok(new List()); + } + + var settings = JsonSerializer.Deserialize>(settingsData); + return Ok(settings); + } + + [HttpPost(template: "set")] + public async Task SetSetting(Setting setting) + { + var settingsData = fileService.GetData("settings.json"); + + var settings = string.IsNullOrWhiteSpace(settingsData) ? [] : JsonSerializer.Deserialize>(settingsData) ?? []; + + settings.RemoveAll(x => x.Key == setting.Key); + settings.Add(setting); + + fileService.SetData("settings.json", JsonSerializer.Serialize(settings)); + + return Ok(); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/API/VersionCheck/V1/VersionCheckController.cs b/RecRoomArchive/Controllers/API/VersionCheck/V1/VersionCheckController.cs new file mode 100644 index 0000000..4420352 --- /dev/null +++ b/RecRoomArchive/Controllers/API/VersionCheck/V1/VersionCheckController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using RecRoomArchive.Services; + +namespace RecRoomArchive.Controllers.API.VersionCheck.V1 +{ + /// + /// 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 + /// + [Route(template: "api/[controller]/v1")] + [ApiController] + public class VersionCheckController(AppVersionService appVersionService) : ControllerBase + { + /// + /// 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) + /// + /// 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 + [HttpGet] + public async Task CheckVersion([FromHeader(Name = "X-Rec-Room-Version")] string appVersion) + { + if (appVersion == null) + return Forbid(); + + return Ok(); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/Images/RootController.cs b/RecRoomArchive/Controllers/Images/RootController.cs new file mode 100644 index 0000000..627ede3 --- /dev/null +++ b/RecRoomArchive/Controllers/Images/RootController.cs @@ -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 GetImage(string imageName) + { + var image = await imageService.GetImage(imageName); + + if (image == null) + return NotFound(); + + return File(image, MediaTypeNames.Image.Png); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Controllers/RootController.cs b/RecRoomArchive/Controllers/RootController.cs new file mode 100644 index 0000000..76d32bb --- /dev/null +++ b/RecRoomArchive/Controllers/RootController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using RecRoomArchive.Services; + +namespace RecRoomArchive.Controllers +{ + /// + /// Only exists for pre-RecNet versions of Rec Room to be able to fetch a Message of the Day from the server... + /// + [ApiController] + public class RootController(MessageOfTheDayService motdService) : ControllerBase + { + /// + /// Gets the legacy version of the Message of the Day, to display on the login screen in pre-Dorm Room versions of the game + /// + /// A basic string to display in game, related to the Message of the Day + [HttpGet(template: "motd")] + public async Task> GetMessageOfTheDay() + { + var messageOfTheDay = await motdService.GetMessageOfTheDay(); + return Ok(messageOfTheDay); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Program.cs b/RecRoomArchive/Program.cs new file mode 100644 index 0000000..c3f36c9 --- /dev/null +++ b/RecRoomArchive/Program.cs @@ -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(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + #endregion + + var app = builder.Build(); + + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + } + + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Properties/launchSettings.json b/RecRoomArchive/Properties/launchSettings.json new file mode 100644 index 0000000..8506764 --- /dev/null +++ b/RecRoomArchive/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/RecRoomArchive/RecRoomArchive.csproj b/RecRoomArchive/RecRoomArchive.csproj new file mode 100644 index 0000000..c162a1c --- /dev/null +++ b/RecRoomArchive/RecRoomArchive.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + \ No newline at end of file diff --git a/RecRoomArchive/RecRoomArchive.http b/RecRoomArchive/RecRoomArchive.http new file mode 100644 index 0000000..a394e5c --- /dev/null +++ b/RecRoomArchive/RecRoomArchive.http @@ -0,0 +1,6 @@ +@RecRoomArchive_HostAddress = http://localhost:5090 + +GET {{RecRoomArchive_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/RecRoomArchive/Services/AccountService.cs b/RecRoomArchive/Services/AccountService.cs new file mode 100644 index 0000000..7298e32 --- /dev/null +++ b/RecRoomArchive/Services/AccountService.cs @@ -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(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); + } + } + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Services/AppVersionService.cs b/RecRoomArchive/Services/AppVersionService.cs new file mode 100644 index 0000000..4d63345 --- /dev/null +++ b/RecRoomArchive/Services/AppVersionService.cs @@ -0,0 +1,85 @@ +namespace RecRoomArchive.Services +{ + /// + /// Used to get the appVersion from the client to determine how to run the server + /// + public class AppVersionService + { + /// + /// AppVersion reference + /// + private static string? AppVersion { get; set; } + + /// + /// The AppVersion as it would want to be seen by the game + /// + private static string? FullAppVersion { get; set; } + + /// + /// 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 + /// + private static DateTime? BuildTimestamp { get; set; } + /// + /// The BuildTimestamp as it would want to be seen by the game + /// + private static long? FullBuildTimestamp { get; set; } + + /// + /// To store the AppVersion of the current build + /// + /// The version of the game + /// If the operation was a success + public async Task 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; + } + + /// + /// To store the BuildTimestamp of the current build + /// + /// The BuildTimestamp of the game + /// If the operation was a success + public async Task 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; + } + + /// + /// Parses the appVersion to "remove any _EA's or .01's or just any weird Rec Room bullshit from the build's name" + /// + /// The version of the game + /// The standardized string of the appVersion + public async Task 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; + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Services/AuthorizationService.cs b/RecRoomArchive/Services/AuthorizationService.cs new file mode 100644 index 0000000..a1f8880 --- /dev/null +++ b/RecRoomArchive/Services/AuthorizationService.cs @@ -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 claims = new List() + { + 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); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Services/ConfigService.cs b/RecRoomArchive/Services/ConfigService.cs new file mode 100644 index 0000000..2445c41 --- /dev/null +++ b/RecRoomArchive/Services/ConfigService.cs @@ -0,0 +1,188 @@ +using RecRoomArchive.Models.API.Config; + +namespace RecRoomArchive.Services +{ + /// + /// I don't even want to explain this one. I'm sorry + /// + public class ConfigService(MessageOfTheDayService motdService, IHttpContextAccessor httpContextAccessor) + { + private readonly MessageOfTheDayService _motdService = motdService; + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; + + /// + /// I'm only doing this because I think that having the full Rec Room config in a controller would be very very ugly. + /// + /// RecRoomConfig + public async Task 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() + }; + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Services/FileService.cs b/RecRoomArchive/Services/FileService.cs new file mode 100644 index 0000000..d8ff88e --- /dev/null +++ b/RecRoomArchive/Services/FileService.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Services/ImageService.cs b/RecRoomArchive/Services/ImageService.cs new file mode 100644 index 0000000..cd62b64 --- /dev/null +++ b/RecRoomArchive/Services/ImageService.cs @@ -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 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 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('='); + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/Services/MessageOfTheDayService.cs b/RecRoomArchive/Services/MessageOfTheDayService.cs new file mode 100644 index 0000000..c29ccb6 --- /dev/null +++ b/RecRoomArchive/Services/MessageOfTheDayService.cs @@ -0,0 +1,62 @@ +using RecRoomArchive.Models.API.Activities; +using System.Text.Json; + +namespace RecRoomArchive.Services +{ + /// + /// Service used to get the message of the day as it is used in multiple areas of the server + /// + public class MessageOfTheDayService + { + /// + /// HttpClient for making requests to Gitea + /// + private static readonly HttpClient httpClient = new HttpClient(); + + /// + /// MessageOfTheDay reference + /// + private static string? MessageOfTheDay { get; set; } + + /// + /// Gets the message of the day from Gitea. If the URL cannot be resolved, it will fall back to "Welcome to RecRoomArchive!" + /// + /// String related to the Message of the Day + public async Task 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 CachedCharadesWords { get; set; } = []; + + public async Task > 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>(words)!; + + return CachedCharadesWords; + } + } +} \ No newline at end of file diff --git a/RecRoomArchive/appsettings.Development.json b/RecRoomArchive/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/RecRoomArchive/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RecRoomArchive/appsettings.json b/RecRoomArchive/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/RecRoomArchive/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/RecRoomArchive/data/avatar.json b/RecRoomArchive/data/avatar.json new file mode 100644 index 0000000..068715b --- /dev/null +++ b/RecRoomArchive/data/avatar.json @@ -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"} \ No newline at end of file diff --git a/RecRoomArchive/data/images/Mh6wZ4jA6k2bx-nwb__YnA.jpg b/RecRoomArchive/data/images/Mh6wZ4jA6k2bx-nwb__YnA.jpg new file mode 100644 index 0000000..48cd54d Binary files /dev/null and b/RecRoomArchive/data/images/Mh6wZ4jA6k2bx-nwb__YnA.jpg differ diff --git a/RecRoomArchive/data/images/VH-K69qfP0S4R7SvLq8Vtw.jpg b/RecRoomArchive/data/images/VH-K69qfP0S4R7SvLq8Vtw.jpg new file mode 100644 index 0000000..71b6937 Binary files /dev/null and b/RecRoomArchive/data/images/VH-K69qfP0S4R7SvLq8Vtw.jpg differ diff --git a/RecRoomArchive/data/images/Wc_cH1llykak2uqyNvgY_w.jpg b/RecRoomArchive/data/images/Wc_cH1llykak2uqyNvgY_w.jpg new file mode 100644 index 0000000..a5c8c38 Binary files /dev/null and b/RecRoomArchive/data/images/Wc_cH1llykak2uqyNvgY_w.jpg differ diff --git a/RecRoomArchive/data/images/wn5ZWdnp-Ea0inPtIOQPmg.jpg b/RecRoomArchive/data/images/wn5ZWdnp-Ea0inPtIOQPmg.jpg new file mode 100644 index 0000000..61eb647 Binary files /dev/null and b/RecRoomArchive/data/images/wn5ZWdnp-Ea0inPtIOQPmg.jpg differ diff --git a/RecRoomArchive/data/profile.json b/RecRoomArchive/data/profile.json new file mode 100644 index 0000000..26baedc --- /dev/null +++ b/RecRoomArchive/data/profile.json @@ -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} \ No newline at end of file diff --git a/RecRoomArchive/data/settings.json b/RecRoomArchive/data/settings.json new file mode 100644 index 0000000..2f1cc90 --- /dev/null +++ b/RecRoomArchive/data/settings.json @@ -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"}] \ No newline at end of file