diff --git a/.gitignore b/.gitignore index 91dc27d..118e45b 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,6 @@ dist # galvanic corrosion build/ -config.json \ No newline at end of file +config.json + +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 3f8357b..cfe30e8 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,23 @@ # Galvanic Corrosion + delectable yum yum -Rec Room custom server for communities. Fast runtime and easy setup.
Built for Rec Room build 526 (Timestamp: 637098805133024772, Version: 20191120) +Rec Room custom server for communities. Fast runtime and easy setup.
Built +for Rec Room build 526 (Timestamp: 637098805133024772, Version: 20191120) drawing ## Compiling Galvanic Corrosion -* Install [Deno 2.x](https://docs.deno.com/runtime/getting_started/installation/) -* Configure project - - Clone this repo - - Install dependencies with `deno i` - - Compile server with `deno run cross-compile` + +- Install + [Deno 2.x](https://docs.deno.com/runtime/getting_started/installation/) +- Configure project + - Clone this repo + - Install dependencies with `deno i` + - Compile server with `deno run cross-compile` ## Client Patches -You can configure some client patches from the server. See the IL2CPP universal patch for a list of patch IDs. -
Place desired patch ID strings into the config `public.patches`. \ No newline at end of file + +You can configure some client patches from the server. See the IL2CPP universal +patch for a list of patch IDs. +
Place desired patch ID strings into the config `public.patches`. diff --git a/deno.json b/deno.json index b0ae982..efa0165 100644 --- a/deno.json +++ b/deno.json @@ -1,27 +1,28 @@ { - "tasks": { - "compile-win": "deno compile --target x86_64-pc-windows-msvc -o build/GalvanicCorrosion.exe -A src/main.ts", - "compile-linux": "deno compile --target x86_64-unknown-linux-gnu -o build/GalvanicCorrosion -A src/main.ts", - "cross-compile": "deno run compile-win && deno run compile-linux", - "dev": "deno run -A src/main.ts --dev" - }, - "imports": { - "@gz/jwt": "jsr:@gz/jwt@^0.1.0", - "@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0", - "@std/assert": "jsr:@std/assert@1", - "@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8", - "@types/express": "npm:@types/express@^5.0.0", - "@types/validator": "npm:@types/validator@^13.12.2", - "cookie-parser": "npm:cookie-parser@^1.4.7", - "discord.js": "npm:discord.js@^14.16.3", - "express": "npm:express@^4.21.2", - "ioredis": "npm:ioredis@^5.5.0", - "validator": "npm:validator@^13.12.0", - "bcrypt": "https://deno.land/x/bcrypt@v0.3.0/mod.ts" - }, - "compilerOptions": { - "types": [ - "./src/types/express.ts" - ] - } + "tasks": { + "compile-win": "deno compile --include res --include src --target x86_64-pc-windows-msvc -o build/GalvanicCorrosion.exe -A src/main.ts", + "compile-linux": "deno compile --include res --include src --target x86_64-unknown-linux-gnu -o build/GalvanicCorrosion -A src/main.ts", + "cross-compile": "deno run compile-win && deno run compile-linux", + "dev": "deno run -A src/main.ts --dev" + }, + "imports": { + "@gz/jwt": "jsr:@gz/jwt@^0.1.0", + "@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0", + "@std/assert": "jsr:@std/assert@1", + "@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8", + "@types/express": "npm:@types/express@^5.0.0", + "@types/validator": "npm:@types/validator@^13.12.2", + "cookie-parser": "npm:cookie-parser@^1.4.7", + "discord.js": "npm:discord.js@^14.16.3", + "express": "npm:express@^4.21.2", + "ioredis": "npm:ioredis@^5.5.0", + "validator": "npm:validator@^13.12.0", + "bcrypt": "https://deno.land/x/bcrypt@v0.3.0/mod.ts" + }, + "files": [], + "compilerOptions": { + "types": [ + "./src/types/express.ts" + ] + } } diff --git a/deno.lock b/deno.lock index 14387da..248013e 100644 --- a/deno.lock +++ b/deno.lock @@ -8,6 +8,7 @@ "jsr:@std/crypto@^1.0.3": "1.0.3", "jsr:@std/internal@^1.0.5": "1.0.5", "jsr:@std/uuid@*": "1.0.4", + "npm:@imagemagick/magick-wasm@0.0.31": "0.0.31", "npm:@types/cookie-parser@*": "1.4.8_@types+express@5.0.0", "npm:@types/cookie-parser@^1.4.8": "1.4.8_@types+express@5.0.0", "npm:@types/express@*": "5.0.0", @@ -110,6 +111,9 @@ "ws" ] }, + "@imagemagick/magick-wasm@0.0.31": { + "integrity": "sha512-QNivAUxSaItuiY8ziI/vRy6TtoecD7TOsD1LGZCG3wv8lfbdGbIj2QiJk0FlGkGwAVR966NlD3mkxPNvQrvq0w==" + }, "@ioredis/commands@1.2.0": { "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, @@ -708,6 +712,9 @@ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==" } }, + "redirects": { + "https://deno.land/x/imagemagick_deno/mod.ts": "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts" + }, "remote": { "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", @@ -808,6 +815,35 @@ "https://deno.land/x/emit@0.25.0/_utils.ts": "98412edc7aa29e77d592b54fbad00bdec1b05d0c25eb772a5f8edc9813e08d88", "https://deno.land/x/emit@0.25.0/emit.generated.js": "0728e0cd293b930db2532f8cb5087fdb77aee1f30a059207533780f40250fd6a", "https://deno.land/x/emit@0.25.0/mod.ts": "66ef8ddaedcfca033eeee851379af59ed3f0e0aa6e025e7cdd24e4e158d874f3", + "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts": "124d7f045429f6e6c486b86e72d025410d09576bc0d8075e69f97118a1a33413", + "https://deno.land/x/imagescript@1.3.0/ImageScript.js": "cf90773c966031edd781ed176c598f7ed495e7694cd9b86c986d2d97f783cca0", + "https://deno.land/x/imagescript@1.3.0/mod.ts": "18a6cb83c55e690c873505f6fe867364c678afb64934fe7aef593a6b92f79995", + "https://deno.land/x/imagescript@1.3.0/png/src/crc.mjs": "5cf50de181d61dd00e66a240d811018ba5070afa8bba302f393604404604de84", + "https://deno.land/x/imagescript@1.3.0/png/src/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d", + "https://deno.land/x/imagescript@1.3.0/png/src/png.mjs": "96ef0ceff1b5a6cd9304749e5f187b4ab238509fb5f9a8be8ee934240271ed8d", + "https://deno.land/x/imagescript@1.3.0/png/src/zlib.mjs": "9867dc3fab1d31b664f9344b0d7e977f493d9c912a76c760d012ed2b89f7061c", + "https://deno.land/x/imagescript@1.3.0/utils/buffer.js": "952cb1beb8827e50a493a5d1f29a4845e8c648789406d389dd51f51205ba02d8", + "https://deno.land/x/imagescript@1.3.0/utils/crc32.js": "573d6222b3605890714ebc374e687ec2aa3e9a949223ea199483e47ca4864f7d", + "https://deno.land/x/imagescript@1.3.0/utils/png.js": "fbed9117e0a70602645d70df9c103ff6e79c03e987bd5c1685dcb4200729b6de", + "https://deno.land/x/imagescript@1.3.0/utils/wasm/font.js": "9e75d842608c057045698d6a7cdf5ffd27241b5cdea0391c89a1917b31294524", + "https://deno.land/x/imagescript@1.3.0/utils/wasm/gif.js": "8b86f7b96486bb8ff50fbc7c7487f86cb5cef85e6acd71e1def78a1aa2f12e4f", + "https://deno.land/x/imagescript@1.3.0/utils/wasm/jpeg.js": "75295e2fcf96b4f7bb894b3844fdaa8140d63169d28b466b5d5be89d59a7b6e6", + "https://deno.land/x/imagescript@1.3.0/utils/wasm/png.js": "0659536a8dd8f892c8346e268b2754b4414fad0ec1e9794dfcde1ba1c804ee02", + "https://deno.land/x/imagescript@1.3.0/utils/wasm/svg.js": "f5c8a9d1977b51a7c07549ceb6bbbaca9497321a193f28b3dc229a42d91bcf14", + "https://deno.land/x/imagescript@1.3.0/utils/wasm/tiff.js": "c2d7bdaef094df25aae1752e75167f485e89275d76a1379e39d8949580b7af4f", + "https://deno.land/x/imagescript@1.3.0/utils/wasm/zlib.js": "749875f83abffe24d3b977475a0cbd5f9b52bee1fbdbef61ec183cbfc17805f6", + "https://deno.land/x/imagescript@1.3.0/v2/framebuffer.mjs": "add44ff184636659714b3c6d4b896f628545451abffbc30b5bcc2e8d9a73d012", + "https://deno.land/x/imagescript@1.3.0/v2/ops/blur.mjs": "80716f1ffab8a2aeb54a036f583bf51a2b9dd37e005adc000add803df8e8a12f", + "https://deno.land/x/imagescript@1.3.0/v2/ops/color.mjs": "5e72cdcbf97dc939a2795223f01e3cb0544c0c56b03ea2aa026050df58348814", + "https://deno.land/x/imagescript@1.3.0/v2/ops/crop.mjs": "69431fa6f687fd9f0c31eff0ec27d7ac925275005e53a37f0c3fab4cc4d9a9ea", + "https://deno.land/x/imagescript@1.3.0/v2/ops/fill.mjs": "cf1b9488314753fbc9ebf03410ac74c2a34ea5a69fb6892cd6e8366cd1930d93", + "https://deno.land/x/imagescript@1.3.0/v2/ops/flip.mjs": "825a34a66567dcf15e76a719f1bf2f66fb106503cd69942292b1b0ae05c5718e", + "https://deno.land/x/imagescript@1.3.0/v2/ops/index.mjs": "423ba687119be2bba8cec72890577d3afa3621b6b8108912242fe937a183f2aa", + "https://deno.land/x/imagescript@1.3.0/v2/ops/iterator.mjs": "c2adf3d90ce00719a02c48c97634574176a3501ff026676259bd71aa8f5d69b9", + "https://deno.land/x/imagescript@1.3.0/v2/ops/overlay.mjs": "7e6e2c2ffd25006d52597ab8babc5f8f503d388a3fdf2fbc0eaea02799a020c9", + "https://deno.land/x/imagescript@1.3.0/v2/ops/resize.mjs": "814e78ebce8eaf8f1f918688db7b52a141405e06a36ed4b25d04413d69e7d17b", + "https://deno.land/x/imagescript@1.3.0/v2/ops/rotate.mjs": "a1b65616717bd2eed8db406affea3263b4674dada46b56441ef38167a187455d", + "https://deno.land/x/imagescript@1.3.0/v2/util/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d", "https://deno.land/x/leaf@v1.0.4/constants.ts": "2b18c5be5a57cea4d3d6298d7c4c636e5db821c580c3197f9c9bcab65f8c3bf0", "https://deno.land/x/leaf@v1.0.4/functions/getFileInMem.ts": "cec6c3c6add22c0c3316d8301994ab583feac5c3052df3072ad12976ea2aeec4", "https://deno.land/x/leaf@v1.0.4/functions/getFilePath.ts": "80ce141c1bd9735d3b7961b6ec8736070475c296c342be6bb4e189483f020801", diff --git a/res/img/3DCharades.png b/res/img/3DCharades.png new file mode 100644 index 0000000..5004132 Binary files /dev/null and b/res/img/3DCharades.png differ diff --git a/res/img/Clearcut.png b/res/img/Clearcut.png new file mode 100644 index 0000000..b34f7a1 Binary files /dev/null and b/res/img/Clearcut.png differ diff --git a/res/img/CrimsonCauldron.png b/res/img/CrimsonCauldron.png new file mode 100644 index 0000000..7c07c9c Binary files /dev/null and b/res/img/CrimsonCauldron.png differ diff --git a/res/img/CyberJunkCity.png b/res/img/CyberJunkCity.png new file mode 100644 index 0000000..224bea7 Binary files /dev/null and b/res/img/CyberJunkCity.png differ diff --git a/res/img/DefaultProfileImage.png b/res/img/DefaultProfileImage.png new file mode 100644 index 0000000..ad9c1e7 Binary files /dev/null and b/res/img/DefaultProfileImage.png differ diff --git a/res/img/DiscGolfLake.png b/res/img/DiscGolfLake.png new file mode 100644 index 0000000..4ec39a1 Binary files /dev/null and b/res/img/DiscGolfLake.png differ diff --git a/res/img/DiscGolfPropulsion.png b/res/img/DiscGolfPropulsion.png new file mode 100644 index 0000000..1c15e08 Binary files /dev/null and b/res/img/DiscGolfPropulsion.png differ diff --git a/res/img/Dodgeball.png b/res/img/Dodgeball.png new file mode 100644 index 0000000..9193f7c Binary files /dev/null and b/res/img/Dodgeball.png differ diff --git a/res/img/GoldenTrophy.png b/res/img/GoldenTrophy.png new file mode 100644 index 0000000..4cfc4cf Binary files /dev/null and b/res/img/GoldenTrophy.png differ diff --git a/res/img/Gym.png b/res/img/Gym.png new file mode 100644 index 0000000..9193f7c Binary files /dev/null and b/res/img/Gym.png differ diff --git a/res/img/Hangar.png b/res/img/Hangar.png new file mode 100644 index 0000000..960ceb2 Binary files /dev/null and b/res/img/Hangar.png differ diff --git a/res/img/Homestead.png b/res/img/Homestead.png new file mode 100644 index 0000000..37a475d Binary files /dev/null and b/res/img/Homestead.png differ diff --git a/res/img/IsleOfLostSkulls.png b/res/img/IsleOfLostSkulls.png new file mode 100644 index 0000000..7b7a511 Binary files /dev/null and b/res/img/IsleOfLostSkulls.png differ diff --git a/res/img/Lounge.png b/res/img/Lounge.png new file mode 100644 index 0000000..453a828 Binary files /dev/null and b/res/img/Lounge.png differ diff --git a/res/img/Paddleball.png b/res/img/Paddleball.png new file mode 100644 index 0000000..dc23055 Binary files /dev/null and b/res/img/Paddleball.png differ diff --git a/res/img/Paintball.png b/res/img/Paintball.png new file mode 100644 index 0000000..a0c6171 Binary files /dev/null and b/res/img/Paintball.png differ diff --git a/res/img/Park.png b/res/img/Park.png new file mode 100644 index 0000000..755f80e Binary files /dev/null and b/res/img/Park.png differ diff --git a/res/img/PerformanceHall.png b/res/img/PerformanceHall.png new file mode 100644 index 0000000..9d29e34 Binary files /dev/null and b/res/img/PerformanceHall.png differ diff --git a/res/img/PropulsionTestRange.png b/res/img/PropulsionTestRange.png new file mode 100644 index 0000000..1c15e08 Binary files /dev/null and b/res/img/PropulsionTestRange.png differ diff --git a/res/img/Quarry.png b/res/img/Quarry.png new file mode 100644 index 0000000..fccf855 Binary files /dev/null and b/res/img/Quarry.png differ diff --git a/res/img/RecCenter.png b/res/img/RecCenter.png new file mode 100644 index 0000000..45ede58 Binary files /dev/null and b/res/img/RecCenter.png differ diff --git a/res/img/RecRoyaleSolos.png b/res/img/RecRoyaleSolos.png new file mode 100644 index 0000000..c0273da Binary files /dev/null and b/res/img/RecRoyaleSolos.png differ diff --git a/res/img/RecRoyaleSquads.png b/res/img/RecRoyaleSquads.png new file mode 100644 index 0000000..c0273da Binary files /dev/null and b/res/img/RecRoyaleSquads.png differ diff --git a/res/img/River.png b/res/img/River.png new file mode 100644 index 0000000..a0c6171 Binary files /dev/null and b/res/img/River.png differ diff --git a/res/img/Soccer.png b/res/img/Soccer.png new file mode 100644 index 0000000..be23907 Binary files /dev/null and b/res/img/Soccer.png differ diff --git a/res/img/Spillway.png b/res/img/Spillway.png new file mode 100644 index 0000000..82c303a Binary files /dev/null and b/res/img/Spillway.png differ diff --git a/res/img/Stadium.png b/res/img/Stadium.png new file mode 100644 index 0000000..be23907 Binary files /dev/null and b/res/img/Stadium.png differ diff --git a/res/img/TheRiseofJumbotron.png b/res/img/TheRiseofJumbotron.png new file mode 100644 index 0000000..5a0b4fb Binary files /dev/null and b/res/img/TheRiseofJumbotron.png differ diff --git a/res/rooms.json b/res/rooms.json index 6fdf39e..2a391d2 100644 --- a/res/rooms.json +++ b/res/rooms.json @@ -1,1625 +1,1023 @@ -{ - "Locations": [ +[ { - "Name": "Dorm Room", - "ReplicationId": "76d98498-60a1-430c-ab76-b54a29b7a163", - "SceneName": "dormroom2", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 1, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 0 - }, - { - "Name": "Rec Center", - "ReplicationId": "cbad71af-0831-44d8-b8ef-69edafa841f6", - "SceneName": "reccenter", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 1 - }, - { - "Name": "Charades", - "ReplicationId": "4078dfed-24bb-4db7-863f-578ba48d726b", - "SceneName": "charades", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 5000, - "LocationEnum": 2 - }, - { - "Name": "Lake", - "ReplicationId": "f6f7256c-e438-4299-b99e-d20bef8cf7e0", - "SceneName": "discgolf", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 3001, - "LocationEnum": 3 - }, - { - "Name": "Propulsion", - "ReplicationId": "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", - "SceneName": "Discgolf_Propulsion", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 3000, - "LocationEnum": 4 - }, - { - "Name": "Dodgeball", - "ReplicationId": "3d474b26-26f7-45e9-9a36-9b02847d5e6f", - "SceneName": "dodgeball", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 8000, - "LocationEnum": 5 - }, - { - "Name": "The Lounge", - "ReplicationId": "a067557f-ca32-43e6-b6e5-daaec60b4f5a", - "SceneName": "eventroom", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": false, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 6 - }, - { - "Name": "Paddleball", - "ReplicationId": "d89f74fa-d51e-477a-a425-025a891dd499", - "SceneName": "paddleball", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 7000, - "LocationEnum": 7 - }, - { - "Name": "River", - "ReplicationId": "e122fe98-e7db-49e8-a1b1-105424b6e1f0", - "SceneName": "paintball", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 2003, - "LocationEnum": 8 - }, - { - "Name": "Homestead", - "ReplicationId": "a785267d-c579-42ea-be43-fec1992d1ca7", - "SceneName": "paintball2_open", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 2001, - "LocationEnum": 9 - }, - { - "Name": "Quarry", - "ReplicationId": "ff4c6427-7079-4f59-b22a-69b089420827", - "SceneName": "Paintball_Castle", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 2002, - "LocationEnum": 10 - }, - { - "Name": "Clear Cut", - "ReplicationId": "380d18b5-de9c-49f3-80f7-f4a95c1de161", - "SceneName": "Paintball_ClearCut", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 2000, - "LocationEnum": 11 - }, - { - "Name": "Spillway", - "ReplicationId": "58763055-2dfb-4814-80b8-16fac5c85709", - "SceneName": "Paintball_Dam", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 2004, - "LocationEnum": 12 - }, - { - "Name": "Quest For The Golden Trophy", - "ReplicationId": "91e16e35-f48f-4700-ab8a-a1b79e50e51b", - "SceneName": "Quest_additive", - "RequiredSubSceneNames": [ - "Quest_Foyer" - ], - "LevelRoomSubSceneNames": [ - "Quest_Armory", - "Quest_Hallway1", - "Quest_Hallway2", - "Quest_Hallway3", - "Quest_DeadIsland", - "Quest_Hallway4", - "Quest_Library", - "Quest_Hallway5", - "Quest_Battlefield", - "Quest_Cafeteria" - ], - "MaxPlayers": 4, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 13 - }, - { - "Name": "The Rise Of JumboTron", - "ReplicationId": "acc06e66-c2d0-4361-b0cd-46246a4c455c", - "SceneName": "Quest_Scifi_Additive", - "RequiredSubSceneNames": [ - "Quest_Scifi_Foyer" - ], - "LevelRoomSubSceneNames": [ - "Quest_Scifi_Armory", - "Quest_Scifi_Battlefield1", - "Quest_Scifi_Hallway1", - "Quest_Scifi_Hallway2", - "Quest_Scifi_Reception", - "Quest_Scifi_StadiumConcession", - "Quest_Scifi_StadiumEntry", - "Quest_Scifi_Garage", - "Quest_Scifi_Cargo1", - "Quest_Scifi_Stadium" - ], - "MaxPlayers": 4, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 14 - }, - { - "Name": "Curse of the Crimson Cauldron", - "ReplicationId": "949fa41f-4347-45c0-b7ac-489129174045", - "SceneName": "Quest_Goblin2_additive", - "RequiredSubSceneNames": [ - "Quest_Goblin2_Foyer" - ], - "LevelRoomSubSceneNames": [ - "Quest_Goblin2_Armory", - "Quest_Goblin2_CastleCourtyard", - "Quest_Goblin2_Forest1", - "Quest_Goblin2_GoblinCamp", - "Quest_Goblin2_Forest2", - "Quest_Goblin2_Bog", - "Quest_Goblin2_Mines1", - "Quest_Goblin2_BoilerRoom", - "Quest_Goblin2_BellTowerStairs", - "Quest_Goblin2_BellTowerArena" - ], - "MaxPlayers": 4, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 15 - }, - { - "Name": "The Isle of Lost Skulls", - "ReplicationId": "7e01cfe0-820a-406f-b1b3-0a5bf575235c", - "SceneName": "Quest_Pirate1_additive", - "RequiredSubSceneNames": [ - "Quest_Pirate1_Foyer" - ], - "LevelRoomSubSceneNames": [ - "Quest_Pirate1_Hallway1", - "Quest_Pirate1_Hallway2", - "Quest_Pirate1_ShipDeck", - "Quest_Pirate1_Beach1", - "Quest_Pirate1_Beach2", - "Quest_Pirate1_Caves1", - "Quest_Pirate1_SunkenShip", - "Quest_Pirate1_BossArena" - ], - "MaxPlayers": 3, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 16 - }, - { - "Name": "Soccer", - "ReplicationId": "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", - "SceneName": "soccer", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 6000, - "LocationEnum": 17 - }, - { - "Name": "Art Testing", - "ReplicationId": "42699ed2-0c1b-4f3d-93a2-ce01dfce7a79", - "SceneName": "ArtTesting", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 1, - "IsEditorOnly": true, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 18 - }, - { - "Name": "Performance Hall", - "ReplicationId": "9932f88f-3929-43a0-a012-a40b5128e346", - "SceneName": "PerformanceHall", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 40, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 19 - }, - { - "Name": "PSVR Room Calibration", - "ReplicationId": "f5fbd9c9-e853-4036-9d48-5f68e861af04", - "SceneName": "room_calibration", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 1, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 20 - }, - { - "Name": "Park", - "ReplicationId": "0a864c86-5a71-4e18-8041-8124e4dc9d98", - "SceneName": "Park", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": false, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 21 - }, - { - "Name": "Warehouse", - "ReplicationId": "239e676c-f12f-489f-bf3a-d4c383d692c3", - "SceneName": "Arena_Hangar3", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": 0, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": true, - "TeamOutfitColorEmissionAmount": 0.6, - "CustomTeamColors": [ - { - "Team": 0, - "Color": { - "r": 0.38039216, - "g": 1.0, - "b": 1.0, - "a": 1.0 - }, - "AlternateColor": { - "r": 0.0, - "g": 0.1882353, - "b": 1.0, - "a": 1.0 + "Name": "Calibration", + "ReplicationId": "30040e05-b7b9-9f44-eb08-b9f154d2ecfc", + "Description": "PSVR room calibration", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": false, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "fe66923e-4034-4ec4-4bca-11a973bf5515", + "RoomSceneLocationId": "f5fbd9c9-e853-4036-9d48-5f68e861af04", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 1 } - }, - { - "Team": 1, - "Color": { - "r": 1.0, - "g": 0.11764706, - "b": 0.41960785, - "a": 1.0 - }, - "AlternateColor": { - "r": 1.0, - "g": 0.0, - "b": 0.15686275, - "a": 1.0 - } - } ] - }, - "GiftContext": 0, - "LocationEnum": 22 }, { - "Name": "CyberJunk City", - "ReplicationId": "9d6456ce-6264-48b4-808d-2d96b3d91038", - "SceneName": "Arena_Cyberjunk_City", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 20, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": 0, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": true, - "TeamOutfitColorEmissionAmount": 0.6, - "CustomTeamColors": [ - { - "Team": 1, - "Color": { - "r": 1.0, - "g": 0.58431375, - "b": 0.15294118, - "a": 1.0 - }, - "AlternateColor": { - "r": 1.0, - "g": 0.0, - "b": 0.0, - "a": 1.0 + "Name": "DormRoom", + "ReplicationId": "68251132-5662-5c34-08b1-4a830a27955b", + "Description": "Your private room", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": false, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "92084aee-1f44-a3b4-18f1-375601606506", + "RoomSceneLocationId": "76d98498-60a1-430c-ab76-b54a29b7a163", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 1 } - }, - { - "Team": 0, - "Color": { - "r": 0.18431373, - "g": 1.0, - "b": 0.5921569, - "a": 1.0 - }, - "AlternateColor": { - "r": 0.0, - "g": 0.41960785, - "b": 1.0, - "a": 1.0 - } - } ] - }, - "GiftContext": 0, - "LocationEnum": 23 }, { - "Name": "Maker Room", - "ReplicationId": "a75f7547-79eb-47c6-8986-6767abcb4f92", - "SceneName": "Basement", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 40, - "IsEditorOnly": false, - "EmptyOnSandboxClone": false, - "SupportedGameMode": -1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 24 + "Name": "RecCenter", + "ReplicationId": "02ed2947-2db9-62c4-49b0-76d70fd432bb", + "Description": "A social hub to meet and mingle with friends new and old.", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": false, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "6016e455-652d-54d4-3838-ecc6c9aa4ca8", + "RoomSceneLocationId": "cbad71af-0831-44d8-b8ef-69edafa841f6", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": true, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 12 + } + ] }, { - "Name": "Frontier Squads", - "ReplicationId": "253fa009-6e65-4c90-91a1-7137a56a267f", - "SceneName": "RecRoyale_Frontier", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 18, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": 1, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 25 + "Name": "3DCharades", + "ReplicationId": "1080b559-5294-b904-5b82-2d2aa4dea17b", + "Description": "Take turns drawing, acting, and guessing funny phrases with your friends!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": false, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "b7150f3d-393e-ac74-2801-8a834b13e2bc", + "RoomSceneLocationId": "4078dfed-24bb-4db7-863f-578ba48d726b", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": true, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + } + ] }, { - "Name": "Frontier Solos", - "ReplicationId": "b010171f-4875-4e89-baba-61e878cd41e1", - "SceneName": "RecRoyale_Frontier", - "RequiredSubSceneNames": [], - "LevelRoomSubSceneNames": [], - "MaxPlayers": 16, - "IsEditorOnly": false, - "EmptyOnSandboxClone": true, - "SupportedGameMode": 0, - "GameTeamColorSettings": { - "TeamOutfitColorEmissionEnabled": false, - "TeamOutfitColorEmissionAmount": 0.0, - "CustomTeamColors": [] - }, - "GiftContext": 0, - "LocationEnum": 26 + "Name": "DiscGolfLake", + "ReplicationId": "9365f155-a900-a864-1aa6-ae0500026994", + "Description": "A leisurely stroll through the grass. Throw your disc into the goal. Sounds easy, right?", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "6cd09971-c9fb-8e44-9b7f-3cb9ff5f6bd0", + "RoomSceneLocationId": "f6f7256c-e438-4299-b99e-d20bef8cf7e0", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 4 + } + ] + }, + { + "Name": "DiscGolfPropulsion", + "ReplicationId": "e002b533-ae3f-0e64-8941-73ed5eb2303c", + "Description": "Throw your disc through hazards and around wind machines on this challenging course!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "e687ebed-6502-ad24-2ae1-d4a1db441d34", + "RoomSceneLocationId": "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 4 + } + ] + }, + { + "Name": "Dodgeball", + "ReplicationId": "aa1ecc2e-fad7-57d4-f840-a4b39e911313", + "Description": "Throw dodgeballs to knock out your friends in this gym classic!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "27dd5218-dbea-b444-9b7c-a64e687aae67", + "RoomSceneLocationId": "3d474b26-26f7-45e9-9a36-9b02847d5e6f", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 6 + } + ] + }, + { + "Name": "Paddleball", + "ReplicationId": "8dfa5b25-d0a7-21e4-0a3e-b77e3ba6a8d0", + "Description": "A simple rally game between two players in a plexiglass tube with a zero-g ball.", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": false, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "11512b3c-bf64-acc4-7bf6-139786237328", + "RoomSceneLocationId": "d89f74fa-d51e-477a-a425-025a891dd499", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 2 + } + ] + }, + { + "Name": "Paintball", + "ReplicationId": "42b5faef-e211-4f02-98e1-f4633e18209c", + "Description": "Red and Blue teams splat each other in capture the flag and team battle.", + "Accessibility": 1, + "SupportsLevelVoting": true, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "River", + "ReplicationId": "84545710-2bb4-4867-ad8b-24863a16d1b2", + "RoomSceneLocationId": "e122fe98-e7db-49e8-a1b1-105424b6e1f0", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + }, + { + "Name": "Homestead", + "ReplicationId": "f6709dc2-af81-46fc-88ba-f88f6c5035aa", + "RoomSceneLocationId": "a785267d-c579-42ea-be43-fec1992d1ca7", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + }, + { + "Name": "Quarry", + "ReplicationId": "1c549cfb-455b-4f5e-b15e-467702a71240", + "RoomSceneLocationId": "ff4c6427-7079-4f59-b22a-69b089420827", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + }, + { + "Name": "Clearcut", + "ReplicationId": "28363a22-f0bb-4e46-8346-d76e8ac634f7", + "RoomSceneLocationId": "380d18b5-de9c-49f3-80f7-f4a95c1de161", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + }, + { + "Name": "Spillway", + "ReplicationId": "7d661e29-f036-4fe9-8e9d-4b919fee638d", + "RoomSceneLocationId": "58763055-2dfb-4814-80b8-16fac5c85709", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + } + ] + }, + { + "Name": "PaintballVR", + "ReplicationId": "6672b30b-108c-cdc4-9873-917bbd882a27", + "Description": "Red and Blue teams splat each other in capture the flag and team battle.", + "Accessibility": 1, + "SupportsLevelVoting": true, + "CloningAllowed": true, + "SupportsScreens": false, + "SupportsWalkVR": false, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "River", + "ReplicationId": "a4d7afc1-ae7a-8e04-1bea-f7d71df384fd", + "RoomSceneLocationId": "e122fe98-e7db-49e8-a1b1-105424b6e1f0", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + }, + { + "Name": "Homestead", + "ReplicationId": "d686129f-fdf7-1954-a9f6-f0a80d1af234", + "RoomSceneLocationId": "a785267d-c579-42ea-be43-fec1992d1ca7", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + }, + { + "Name": "Quarry", + "ReplicationId": "a5ba53f2-a7cf-d3b4-8836-80fa08e02a27", + "RoomSceneLocationId": "ff4c6427-7079-4f59-b22a-69b089420827", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + }, + { + "Name": "Clearcut", + "ReplicationId": "1636a319-9826-bfc4-18f8-ee1b1be6f806", + "RoomSceneLocationId": "380d18b5-de9c-49f3-80f7-f4a95c1de161", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + }, + { + "Name": "Spillway", + "ReplicationId": "9a5a9875-67bb-f444-e804-0e0296d3c19c", + "RoomSceneLocationId": "58763055-2dfb-4814-80b8-16fac5c85709", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + } + ] + }, + { + "Name": "GoldenTrophy", + "ReplicationId": "8b5f720a-29b0-fee4-1aab-a76d99405a21", + "Description": "The goblin king stole Coach's Golden Trophy. Team up and embark on an epic quest to recover it!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "3518de14-8412-96f4-db0a-41abe0196bbe", + "RoomSceneLocationId": "91e16e35-f48f-4700-ab8a-a1b79e50e51b", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": true, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 4 + } + ] + }, + { + "Name": "TheRiseofJumbotron", + "ReplicationId": "0fe6e761-9adc-14e4-2b7c-ecf2e365b80a", + "Description": "Robot invaders threaten the galaxy! Team up with your friends and bring the laser heat!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "4a6968bf-0283-2d34-ca18-29f1145e1f69", + "RoomSceneLocationId": "acc06e66-c2d0-4361-b0cd-46246a4c455c", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": true, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 4 + } + ] + }, + { + "Name": "CrimsonCauldron", + "ReplicationId": "fa6d5c07-c7fa-38a4-eb98-acaee6c8fd7b", + "Description": "Can your band of adventurers brave the enchanted wilds, and lift the curse of the crimson cauldron?", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "4a169c2f-28b2-e9a4-7ab1-61c9c270b1ab", + "RoomSceneLocationId": "949fa41f-4347-45c0-b7ac-489129174045", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": true, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 4 + } + ] + }, + { + "Name": "IsleOfLostSkulls", + "ReplicationId": "51fe6f77-a545-66d4-684c-20505d9472eb", + "Description": "Can your pirate crew get to the Isle, defeat its fearsome guardian, and escape with the gold?", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "02d23e1d-4c1a-15b4-aba5-98458d750417", + "RoomSceneLocationId": "7e01cfe0-820a-406f-b1b3-0a5bf575235c", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": true, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 3 + } + ] + }, + { + "Name": "Soccer", + "ReplicationId": "99e24047-c765-8584-78d0-f55d604ecb00", + "Description": "Teams of three run around slamming themselves into an over-sized soccer ball. Goal!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "9a4b4871-28dc-4b24-8b8f-e0914db002eb", + "RoomSceneLocationId": "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 6 + } + ] + }, + { + "Name": "LaserTagHangar", + "ReplicationId": "8cf5a0b5-d683-51f4-2bb7-57821b533cad", + "Description": "Teams battle each other and waves of robots in a classic warehouse arena.", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "8f8fcaf8-7b13-e114-880c-72ee8c9fcb78", + "RoomSceneLocationId": "239e676c-f12f-489f-bf3a-d4c383d692c3", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + } + ] + }, + { + "Name": "LaserTagCyberJunk", + "ReplicationId": "c47969aa-c9ac-85e4-ea35-470f2a11d47f", + "Description": "Teams battle each other and waves of robots in a totally cyber neon future city.", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "d59f7e16-9f27-6924-a899-2739bfc056fe", + "RoomSceneLocationId": "9d6456ce-6264-48b4-808d-2d96b3d91038", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 8 + } + ] + }, + { + "Name": "RecRoyaleSquads", + "ReplicationId": "224046e6-4159-49eb-98cf-1e602849ce54", + "Description": "Squads of three battle it out on Frontier Island. Last squad standing wins!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "1d282c2d-6edc-4910-afb3-48fbb9ad74a4", + "RoomSceneLocationId": "253fa009-6e65-4c90-91a1-7137a56a267f", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": true, + "MaxPlayers": 18 + } + ] + }, + { + "Name": "RecRoyaleVR", + "ReplicationId": "729cddc7-1488-8004-3abc-b7ab05c3ec83", + "Description": "Squads of three battle it out on Frontier Island. Last squad standing wins!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": false, + "SupportsWalkVR": true, + "SupportsTeleportVR": false, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "1edd6d85-40fb-7e34-aa26-8ef754ef654e", + "RoomSceneLocationId": "253fa009-6e65-4c90-91a1-7137a56a267f", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": true, + "MaxPlayers": 18 + } + ] + }, + { + "Name": "RecRoyaleSolos", + "ReplicationId": "f4a10613-c6dc-a574-6abd-17e853cd223c", + "Description": "Battle it out on Frontier Island. Last person standing wins!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "85b43509-77f7-3884-3a6b-9a4e737d6d11", + "RoomSceneLocationId": "b010171f-4875-4e89-baba-61e878cd41e1", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": true, + "MaxPlayers": 16 + } + ] + }, + { + "Name": "Lounge", + "ReplicationId": "94b533d0-08b3-7964-c89c-491906e032b9", + "Description": "A low-key lounge to chill with your friends. Great for private parties!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "c9e928de-ed1f-d564-38af-9bf525ac0feb", + "RoomSceneLocationId": "a067557f-ca32-43e6-b6e5-daaec60b4f5a", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "PerformanceHall", + "ReplicationId": "22ab0d3c-3d7d-70e4-eb5c-c8c47cca1906", + "Description": "A theater for plays, music, comedy and other performances.", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "b55204c1-9a48-6ea4-fbd0-69fb125a68cf", + "RoomSceneLocationId": "9932f88f-3929-43a0-a012-a40b5128e346", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 40 + } + ] + }, + { + "Name": "MakerRoom", + "ReplicationId": "3a2d8a48-2a7e-4344-bb88-254ea105043e", + "Description": "This room is a blank canvas. Make it into whatever you like!", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "aa889faf-91c8-b7d4-5945-ab7e714f7efa", + "RoomSceneLocationId": "a75f7547-79eb-47c6-8986-6767abcb4f92", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 40 + } + ] + }, + { + "Name": "Park", + "ReplicationId": "00788628-81ba-3df4-da7b-142051bdff98", + "Description": "A sprawling park with amphitheater, play fields, and a cave.", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "570a59bf-b4e2-6db4-ca9a-42329563dd30", + "RoomSceneLocationId": "0a864c86-5a71-4e18-8041-8124e4dc9d98", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "ArtTesting", + "ReplicationId": "cc338cd1-9883-0c14-99b3-ab069a360ee3", + "Description": "bla", + "Accessibility": 0, + "SupportsLevelVoting": false, + "CloningAllowed": false, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "33bdd61b-bd99-abe4-6943-3032c741f5e8", + "RoomSceneLocationId": "42699ed2-0c1b-4f3d-93a2-ce01dfce7a79", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 1 + } + ] + }, + { + "Name": "River", + "ReplicationId": "af923b9b-d5fd-4435-919b-d8621de3865e", + "Description": "The original outdoor paintball course. Simple, balanced, classic.", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "dc445d24-01cf-43f9-a7c4-33f432514735", + "RoomSceneLocationId": "e122fe98-e7db-49e8-a1b1-105424b6e1f0", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "Homestead", + "ReplicationId": "3369e698-ad83-43cf-bf44-5767da6de2b9", + "Description": "A day on the farm. Great for asymmetrical battles!", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "daaa8763-868d-4081-9e8d-e931c111cc90", + "RoomSceneLocationId": "a785267d-c579-42ea-be43-fec1992d1ca7", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "Quarry", + "ReplicationId": "37e59cf8-50c5-4075-8e61-f51515836101", + "Description": "The sun sets on this construction site in the desert. Great for sniping battles!", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "f083ff9a-98af-4d8b-b7ff-5b84a66367d5", + "RoomSceneLocationId": "ff4c6427-7079-4f59-b22a-69b089420827", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "Clearcut", + "ReplicationId": "af8b3bdd-4084-488f-aa4a-b78dec436753", + "Description": "The sun rises on this logging camp in the forest. Great for mid-range splatting!", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "c5f45037-8c60-494d-ad62-3e5168560b36", + "RoomSceneLocationId": "380d18b5-de9c-49f3-80f7-f4a95c1de161", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "Spillway", + "ReplicationId": "2d250de9-9bb2-4168-96a6-a9a0e14aa2ba", + "Description": "Night shift at the hydroelectric plant. Great for stealthy splatting!", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "d336999f-9db0-42fc-8af5-592e9043102f", + "RoomSceneLocationId": "58763055-2dfb-4814-80b8-16fac5c85709", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "Lake", + "ReplicationId": "12131500-c742-437c-8487-b9e1e2edc381", + "Description": "A leisurely trail to the lake and beyond.", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "28253579-64cb-421b-988b-9fea890fd129", + "RoomSceneLocationId": "f6f7256c-e438-4299-b99e-d20bef8cf7e0", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "PropulsionTestRange", + "ReplicationId": "fd06572c-e828-440d-8783-d87c792facdd", + "Description": "The science department left some of their equipment laying around. What will you do with it?", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "cd82c0aa-5dbc-4db3-ade3-30576d3bc473", + "RoomSceneLocationId": "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "Gym", + "ReplicationId": "cb7ce944-9415-4d6e-aa6a-274390d2f849", + "Description": "A school gymnasium for smaller sporting events.", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "8cecdc5b-d17b-4eba-9dd3-7b0ca54d3fd2", + "RoomSceneLocationId": "3d474b26-26f7-45e9-9a36-9b02847d5e6f", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "Stadium", + "ReplicationId": "01fbdc4c-1a74-4843-8fe3-c64fc46b606e", + "Description": "A professional stadium for larger sporting events.", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "b4f8632d-ae8b-4c3d-a433-0687131f8507", + "RoomSceneLocationId": "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "Hangar", + "ReplicationId": "4830b10c-0faf-4e73-8244-10cf55a503d9", + "Description": "A late-eighties-style laser tag battle arcade.", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "26330026-ebc7-4d44-8c85-4f38d2ad30a1", + "RoomSceneLocationId": "239e676c-f12f-489f-bf3a-d4c383d692c3", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "CyberJunkCity", + "ReplicationId": "c3f801c2-13dd-aed4-0862-85d095bca56b", + "Description": "A late-2080s-style cyberpunk dystopian laser tag battle arcade", + "Accessibility": 2, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": true, + "SupportsWalkVR": true, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "6e0edd15-b904-f074-3b1f-cec488eba1a3", + "RoomSceneLocationId": "9d6456ce-6264-48b4-808d-2d96b3d91038", + "IsSandbox": true, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": true, + "UseLevelBasedMatchmaking": false, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 20 + } + ] + }, + { + "Name": "DodgeballVR", + "ReplicationId": "c29d7c49-f4df-45cd-9a53-a6a192e44fbd", + "Description": "Throw dodgeballs to knock out your friends in this gym classic!", + "Accessibility": 1, + "SupportsLevelVoting": false, + "CloningAllowed": true, + "SupportsScreens": false, + "SupportsWalkVR": false, + "SupportsTeleportVR": true, + "Scenes": [ + { + "Name": "Home", + "ReplicationId": "bcbda991-93e7-4d05-b2e2-7c9512e4d4f2", + "RoomSceneLocationId": "3d474b26-26f7-45e9-9a36-9b02847d5e6f", + "IsSandbox": false, + "CanMatchmakeInto": true, + "SupportsJoinInProgress": false, + "UseLevelBasedMatchmaking": true, + "UseAgeBasedMatchmaking": false, + "UseRecRoyaleMatchmaking": false, + "MaxPlayers": 6 + } + ] } - ], - "Rooms": [ - { - "Name": "Calibration", - "ReplicationId": "30040e05-b7b9-9f44-eb08-b9f154d2ecfc", - "Description": "PSVR room calibration", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": false, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "fe66923e-4034-4ec4-4bca-11a973bf5515", - "RoomSceneLocationId": "f5fbd9c9-e853-4036-9d48-5f68e861af04", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 1 - } - ] - }, - { - "Name": "DormRoom", - "ReplicationId": "68251132-5662-5c34-08b1-4a830a27955b", - "Description": "Your private room", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": false, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "92084aee-1f44-a3b4-18f1-375601606506", - "RoomSceneLocationId": "76d98498-60a1-430c-ab76-b54a29b7a163", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 1 - } - ] - }, - { - "Name": "RecCenter", - "ReplicationId": "02ed2947-2db9-62c4-49b0-76d70fd432bb", - "Description": "A social hub to meet and mingle with friends new and old.", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": false, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "6016e455-652d-54d4-3838-ecc6c9aa4ca8", - "RoomSceneLocationId": "cbad71af-0831-44d8-b8ef-69edafa841f6", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": true, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 12 - } - ] - }, - { - "Name": "3DCharades", - "ReplicationId": "1080b559-5294-b904-5b82-2d2aa4dea17b", - "Description": "Take turns drawing, acting, and guessing funny phrases with your friends!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": false, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "b7150f3d-393e-ac74-2801-8a834b13e2bc", - "RoomSceneLocationId": "4078dfed-24bb-4db7-863f-578ba48d726b", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": true, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - } - ] - }, - { - "Name": "DiscGolfLake", - "ReplicationId": "9365f155-a900-a864-1aa6-ae0500026994", - "Description": "A leisurely stroll through the grass. Throw your disc into the goal. Sounds easy, right?", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "6cd09971-c9fb-8e44-9b7f-3cb9ff5f6bd0", - "RoomSceneLocationId": "f6f7256c-e438-4299-b99e-d20bef8cf7e0", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 4 - } - ] - }, - { - "Name": "DiscGolfPropulsion", - "ReplicationId": "e002b533-ae3f-0e64-8941-73ed5eb2303c", - "Description": "Throw your disc through hazards and around wind machines on this challenging course!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "e687ebed-6502-ad24-2ae1-d4a1db441d34", - "RoomSceneLocationId": "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 4 - } - ] - }, - { - "Name": "Dodgeball", - "ReplicationId": "aa1ecc2e-fad7-57d4-f840-a4b39e911313", - "Description": "Throw dodgeballs to knock out your friends in this gym classic!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "27dd5218-dbea-b444-9b7c-a64e687aae67", - "RoomSceneLocationId": "3d474b26-26f7-45e9-9a36-9b02847d5e6f", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 6 - } - ] - }, - { - "Name": "Paddleball", - "ReplicationId": "8dfa5b25-d0a7-21e4-0a3e-b77e3ba6a8d0", - "Description": "A simple rally game between two players in a plexiglass tube with a zero-g ball.", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": false, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "11512b3c-bf64-acc4-7bf6-139786237328", - "RoomSceneLocationId": "d89f74fa-d51e-477a-a425-025a891dd499", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 2 - } - ] - }, - { - "Name": "Paintball", - "ReplicationId": "42b5faef-e211-4f02-98e1-f4633e18209c", - "Description": "Red and Blue teams splat each other in capture the flag and team battle.", - "Accessibility": 1, - "SupportsLevelVoting": true, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "River", - "ReplicationId": "84545710-2bb4-4867-ad8b-24863a16d1b2", - "RoomSceneLocationId": "e122fe98-e7db-49e8-a1b1-105424b6e1f0", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - }, - { - "Name": "Homestead", - "ReplicationId": "f6709dc2-af81-46fc-88ba-f88f6c5035aa", - "RoomSceneLocationId": "a785267d-c579-42ea-be43-fec1992d1ca7", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - }, - { - "Name": "Quarry", - "ReplicationId": "1c549cfb-455b-4f5e-b15e-467702a71240", - "RoomSceneLocationId": "ff4c6427-7079-4f59-b22a-69b089420827", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - }, - { - "Name": "Clearcut", - "ReplicationId": "28363a22-f0bb-4e46-8346-d76e8ac634f7", - "RoomSceneLocationId": "380d18b5-de9c-49f3-80f7-f4a95c1de161", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - }, - { - "Name": "Spillway", - "ReplicationId": "7d661e29-f036-4fe9-8e9d-4b919fee638d", - "RoomSceneLocationId": "58763055-2dfb-4814-80b8-16fac5c85709", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - } - ] - }, - { - "Name": "PaintballVR", - "ReplicationId": "6672b30b-108c-cdc4-9873-917bbd882a27", - "Description": "Red and Blue teams splat each other in capture the flag and team battle.", - "Accessibility": 1, - "SupportsLevelVoting": true, - "CloningAllowed": true, - "SupportsScreens": false, - "SupportsWalkVR": false, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "River", - "ReplicationId": "a4d7afc1-ae7a-8e04-1bea-f7d71df384fd", - "RoomSceneLocationId": "e122fe98-e7db-49e8-a1b1-105424b6e1f0", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - }, - { - "Name": "Homestead", - "ReplicationId": "d686129f-fdf7-1954-a9f6-f0a80d1af234", - "RoomSceneLocationId": "a785267d-c579-42ea-be43-fec1992d1ca7", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - }, - { - "Name": "Quarry", - "ReplicationId": "a5ba53f2-a7cf-d3b4-8836-80fa08e02a27", - "RoomSceneLocationId": "ff4c6427-7079-4f59-b22a-69b089420827", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - }, - { - "Name": "Clearcut", - "ReplicationId": "1636a319-9826-bfc4-18f8-ee1b1be6f806", - "RoomSceneLocationId": "380d18b5-de9c-49f3-80f7-f4a95c1de161", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - }, - { - "Name": "Spillway", - "ReplicationId": "9a5a9875-67bb-f444-e804-0e0296d3c19c", - "RoomSceneLocationId": "58763055-2dfb-4814-80b8-16fac5c85709", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - } - ] - }, - { - "Name": "GoldenTrophy", - "ReplicationId": "8b5f720a-29b0-fee4-1aab-a76d99405a21", - "Description": "The goblin king stole Coach's Golden Trophy. Team up and embark on an epic quest to recover it!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "3518de14-8412-96f4-db0a-41abe0196bbe", - "RoomSceneLocationId": "91e16e35-f48f-4700-ab8a-a1b79e50e51b", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": true, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 4 - } - ] - }, - { - "Name": "TheRiseofJumbotron", - "ReplicationId": "0fe6e761-9adc-14e4-2b7c-ecf2e365b80a", - "Description": "Robot invaders threaten the galaxy! Team up with your friends and bring the laser heat!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "4a6968bf-0283-2d34-ca18-29f1145e1f69", - "RoomSceneLocationId": "acc06e66-c2d0-4361-b0cd-46246a4c455c", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": true, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 4 - } - ] - }, - { - "Name": "CrimsonCauldron", - "ReplicationId": "fa6d5c07-c7fa-38a4-eb98-acaee6c8fd7b", - "Description": "Can your band of adventurers brave the enchanted wilds, and lift the curse of the crimson cauldron?", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "4a169c2f-28b2-e9a4-7ab1-61c9c270b1ab", - "RoomSceneLocationId": "949fa41f-4347-45c0-b7ac-489129174045", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": true, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 4 - } - ] - }, - { - "Name": "IsleOfLostSkulls", - "ReplicationId": "51fe6f77-a545-66d4-684c-20505d9472eb", - "Description": "Can your pirate crew get to the Isle, defeat its fearsome guardian, and escape with the gold?", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "02d23e1d-4c1a-15b4-aba5-98458d750417", - "RoomSceneLocationId": "7e01cfe0-820a-406f-b1b3-0a5bf575235c", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": true, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 3 - } - ] - }, - { - "Name": "Soccer", - "ReplicationId": "99e24047-c765-8584-78d0-f55d604ecb00", - "Description": "Teams of three run around slamming themselves into an over-sized soccer ball. Goal!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "9a4b4871-28dc-4b24-8b8f-e0914db002eb", - "RoomSceneLocationId": "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 6 - } - ] - }, - { - "Name": "LaserTagHangar", - "ReplicationId": "8cf5a0b5-d683-51f4-2bb7-57821b533cad", - "Description": "Teams battle each other and waves of robots in a classic warehouse arena.", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "8f8fcaf8-7b13-e114-880c-72ee8c9fcb78", - "RoomSceneLocationId": "239e676c-f12f-489f-bf3a-d4c383d692c3", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - } - ] - }, - { - "Name": "LaserTagCyberJunk", - "ReplicationId": "c47969aa-c9ac-85e4-ea35-470f2a11d47f", - "Description": "Teams battle each other and waves of robots in a totally cyber neon future city.", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "d59f7e16-9f27-6924-a899-2739bfc056fe", - "RoomSceneLocationId": "9d6456ce-6264-48b4-808d-2d96b3d91038", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 8 - } - ] - }, - { - "Name": "RecRoyaleSquads", - "ReplicationId": "224046e6-4159-49eb-98cf-1e602849ce54", - "Description": "Squads of three battle it out on Frontier Island. Last squad standing wins!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "1d282c2d-6edc-4910-afb3-48fbb9ad74a4", - "RoomSceneLocationId": "253fa009-6e65-4c90-91a1-7137a56a267f", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": true, - "MaxPlayers": 18 - } - ] - }, - { - "Name": "RecRoyaleVR", - "ReplicationId": "729cddc7-1488-8004-3abc-b7ab05c3ec83", - "Description": "Squads of three battle it out on Frontier Island. Last squad standing wins!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": false, - "SupportsWalkVR": true, - "SupportsTeleportVR": false, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "1edd6d85-40fb-7e34-aa26-8ef754ef654e", - "RoomSceneLocationId": "253fa009-6e65-4c90-91a1-7137a56a267f", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": true, - "MaxPlayers": 18 - } - ] - }, - { - "Name": "RecRoyaleSolos", - "ReplicationId": "f4a10613-c6dc-a574-6abd-17e853cd223c", - "Description": "Battle it out on Frontier Island. Last person standing wins!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "85b43509-77f7-3884-3a6b-9a4e737d6d11", - "RoomSceneLocationId": "b010171f-4875-4e89-baba-61e878cd41e1", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": true, - "MaxPlayers": 16 - } - ] - }, - { - "Name": "Lounge", - "ReplicationId": "94b533d0-08b3-7964-c89c-491906e032b9", - "Description": "A low-key lounge to chill with your friends. Great for private parties!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "c9e928de-ed1f-d564-38af-9bf525ac0feb", - "RoomSceneLocationId": "a067557f-ca32-43e6-b6e5-daaec60b4f5a", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "PerformanceHall", - "ReplicationId": "22ab0d3c-3d7d-70e4-eb5c-c8c47cca1906", - "Description": "A theater for plays, music, comedy and other performances.", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "b55204c1-9a48-6ea4-fbd0-69fb125a68cf", - "RoomSceneLocationId": "9932f88f-3929-43a0-a012-a40b5128e346", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 40 - } - ] - }, - { - "Name": "MakerRoom", - "ReplicationId": "3a2d8a48-2a7e-4344-bb88-254ea105043e", - "Description": "This room is a blank canvas. Make it into whatever you like!", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "aa889faf-91c8-b7d4-5945-ab7e714f7efa", - "RoomSceneLocationId": "a75f7547-79eb-47c6-8986-6767abcb4f92", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 40 - } - ] - }, - { - "Name": "Park", - "ReplicationId": "00788628-81ba-3df4-da7b-142051bdff98", - "Description": "A sprawling park with amphitheater, play fields, and a cave.", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "570a59bf-b4e2-6db4-ca9a-42329563dd30", - "RoomSceneLocationId": "0a864c86-5a71-4e18-8041-8124e4dc9d98", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "ArtTesting", - "ReplicationId": "cc338cd1-9883-0c14-99b3-ab069a360ee3", - "Description": "bla", - "Accessibility": 0, - "SupportsLevelVoting": false, - "CloningAllowed": false, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "33bdd61b-bd99-abe4-6943-3032c741f5e8", - "RoomSceneLocationId": "42699ed2-0c1b-4f3d-93a2-ce01dfce7a79", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 1 - } - ] - }, - { - "Name": "River", - "ReplicationId": "af923b9b-d5fd-4435-919b-d8621de3865e", - "Description": "The original outdoor paintball course. Simple, balanced, classic.", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "dc445d24-01cf-43f9-a7c4-33f432514735", - "RoomSceneLocationId": "e122fe98-e7db-49e8-a1b1-105424b6e1f0", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "Homestead", - "ReplicationId": "3369e698-ad83-43cf-bf44-5767da6de2b9", - "Description": "A day on the farm. Great for asymmetrical battles!", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "daaa8763-868d-4081-9e8d-e931c111cc90", - "RoomSceneLocationId": "a785267d-c579-42ea-be43-fec1992d1ca7", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "Quarry", - "ReplicationId": "37e59cf8-50c5-4075-8e61-f51515836101", - "Description": "The sun sets on this construction site in the desert. Great for sniping battles!", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "f083ff9a-98af-4d8b-b7ff-5b84a66367d5", - "RoomSceneLocationId": "ff4c6427-7079-4f59-b22a-69b089420827", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "Clearcut", - "ReplicationId": "af8b3bdd-4084-488f-aa4a-b78dec436753", - "Description": "The sun rises on this logging camp in the forest. Great for mid-range splatting!", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "c5f45037-8c60-494d-ad62-3e5168560b36", - "RoomSceneLocationId": "380d18b5-de9c-49f3-80f7-f4a95c1de161", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "Spillway", - "ReplicationId": "2d250de9-9bb2-4168-96a6-a9a0e14aa2ba", - "Description": "Night shift at the hydroelectric plant. Great for stealthy splatting!", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "d336999f-9db0-42fc-8af5-592e9043102f", - "RoomSceneLocationId": "58763055-2dfb-4814-80b8-16fac5c85709", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "Lake", - "ReplicationId": "12131500-c742-437c-8487-b9e1e2edc381", - "Description": "A leisurely trail to the lake and beyond.", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "28253579-64cb-421b-988b-9fea890fd129", - "RoomSceneLocationId": "f6f7256c-e438-4299-b99e-d20bef8cf7e0", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "PropulsionTestRange", - "ReplicationId": "fd06572c-e828-440d-8783-d87c792facdd", - "Description": "The science department left some of their equipment laying around. What will you do with it?", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "cd82c0aa-5dbc-4db3-ade3-30576d3bc473", - "RoomSceneLocationId": "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "Gym", - "ReplicationId": "cb7ce944-9415-4d6e-aa6a-274390d2f849", - "Description": "A school gymnasium for smaller sporting events.", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "8cecdc5b-d17b-4eba-9dd3-7b0ca54d3fd2", - "RoomSceneLocationId": "3d474b26-26f7-45e9-9a36-9b02847d5e6f", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "Stadium", - "ReplicationId": "01fbdc4c-1a74-4843-8fe3-c64fc46b606e", - "Description": "A professional stadium for larger sporting events.", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "b4f8632d-ae8b-4c3d-a433-0687131f8507", - "RoomSceneLocationId": "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "Hangar", - "ReplicationId": "4830b10c-0faf-4e73-8244-10cf55a503d9", - "Description": "A late-eighties-style laser tag battle arcade.", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "26330026-ebc7-4d44-8c85-4f38d2ad30a1", - "RoomSceneLocationId": "239e676c-f12f-489f-bf3a-d4c383d692c3", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "CyberJunkCity", - "ReplicationId": "c3f801c2-13dd-aed4-0862-85d095bca56b", - "Description": "A late-2080s-style cyberpunk dystopian laser tag battle arcade", - "Accessibility": 2, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": true, - "SupportsWalkVR": true, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "6e0edd15-b904-f074-3b1f-cec488eba1a3", - "RoomSceneLocationId": "9d6456ce-6264-48b4-808d-2d96b3d91038", - "IsSandbox": true, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": true, - "UseLevelBasedMatchmaking": false, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 20 - } - ] - }, - { - "Name": "DodgeballVR", - "ReplicationId": "c29d7c49-f4df-45cd-9a53-a6a192e44fbd", - "Description": "Throw dodgeballs to knock out your friends in this gym classic!", - "Accessibility": 1, - "SupportsLevelVoting": false, - "CloningAllowed": true, - "SupportsScreens": false, - "SupportsWalkVR": false, - "SupportsTeleportVR": true, - "Scenes": [ - { - "Name": "Home", - "ReplicationId": "bcbda991-93e7-4d05-b2e2-7c9512e4d4f2", - "RoomSceneLocationId": "3d474b26-26f7-45e9-9a36-9b02847d5e6f", - "IsSandbox": false, - "CanMatchmakeInto": true, - "SupportsJoinInProgress": false, - "UseLevelBasedMatchmaking": true, - "UseAgeBasedMatchmaking": false, - "UseRecRoyaleMatchmaking": false, - "MaxPlayers": 6 - } - ] - } - ] -} \ No newline at end of file +] diff --git a/src/apiutils.ts b/src/apiutils.ts index ff9d24e..9076934 100644 --- a/src/apiutils.ts +++ b/src/apiutils.ts @@ -4,27 +4,29 @@ import Logging from "@proxnet/undead-logging"; import { decode } from "@gz/jwt"; import { Config } from "./config.ts"; import { AuthType, User, UserTokenFormat } from "./data/users.ts"; +import Profile, { ProfileTokenFormat } from "./data/profiles.ts"; const config = Config.getConfig(); -const log = new Logging('APIUtils'); +const log = new Logging("APIUtils"); type AppRouter = { - path: string, - router: express.Router -} + path: string; + router: express.Router; +}; export function createRouter(path: string) { const router: AppRouter = { path: path, - router: express.Router() - } + router: express.Router(), + }; return router; } export function generateRandomString(length: number) { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let randomString = ''; + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let randomString = ""; for (let i = 0; i < length; i++) { const randomIndex = Math.floor(Math.random() * characters.length); @@ -35,11 +37,20 @@ export function generateRandomString(length: number) { } export function checkQueryTypes(typeDef: T) { - return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { + return ( + rq: express.Request, + rs: express.Response, + nxt: express.NextFunction, + ) => { for (const key in typeDef) { - if (typeof rq.query[key] !== typeof (typeDef)[key]) { + if (typeof rq.query[key] !== typeof typeDef[key]) { rs.statusCode = 400; - rs.json(genericResponseFormat(true, "One or more query parameters were invalid or not found.")); + rs.json( + genericResponseFormat( + true, + "One or more query parameters were invalid or not found.", + ), + ); return; } } @@ -47,12 +58,21 @@ export function checkQueryTypes(typeDef: T) { }; } export function checkBodyTypes(typeDef: T) { - return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { - for (const key in typeDef) { - if (typeof rq.body[key] !== typeof (typeDef)[key]) { + return ( + rq: express.Request, + rs: express.Response, + nxt: express.NextFunction, + ) => { + for (const key in typeDef) { + if (typeof rq.body[key] !== typeof typeDef[key]) { log.e(`Body check for key '${key}' failed.`); rs.statusCode = 400; - rs.json(genericResponseFormat(true, "One or more body values were invalid or not found.")); + rs.json( + genericResponseFormat( + true, + "One or more body values were invalid or not found.", + ), + ); return; } } @@ -60,28 +80,40 @@ export function checkBodyTypes(typeDef: T) { }; } -export function genericResponseFormat(failure: boolean, msg: string | null = null, data: object | null = null) { +export function genericResponseFormat( + failure: boolean, + msg: string | null = null, + data: object | null = null, +) { return { failed: failure, message: msg, data: data }; } -export function genericResponse(failure: boolean, msg: string | null = null, data: object | null = null) { +export function genericResponse( + failure: boolean, + msg: string | null = null, + data: object | null = null, +) { return (_rq: express.Request, rs: express.Response) => { rs.json({ failed: failure, message: msg, data: data }); }; } type RecNetResponse = { - Success: boolean, - Message: string + Success: boolean; + Message: string; }; export function RecNetResponse(success: boolean, message: string) { const msg: RecNetResponse = { Success: success, Message: message }; return (_rq: express.Request, rs: express.Response) => { rs.json(msg); - } + }; } -export function logBody(rq: express.Request, _rs: express.Response, nxt: express.NextFunction) { - nxt(); +export function logBody( + rq: express.Request, + _rs: express.Response, + nxt: express.NextFunction, +) { log.d(`Request body: ${JSON.stringify(rq.body)}`); + nxt(); } export function emptyArrayResponse(_rq: express.Request, rs: express.Response) { @@ -89,27 +121,26 @@ export function emptyArrayResponse(_rq: express.Request, rs: express.Response) { } export function getSrcIpDefault(rq: express.Request) { - const cfIp = rq.header('cf-connecting-ip'); + const cfIp = rq.header("cf-connecting-ip"); if (cfIp !== undefined) return cfIp; - const xrIp = rq.header('x-real-ip'); + const xrIp = rq.header("x-real-ip"); if (xrIp !== undefined) return xrIp; - const ip = typeof rq.ip === 'undefined' ? '(unknown source)' : rq.ip; + const ip = typeof rq.ip === "undefined" ? "(unknown source)" : rq.ip; return ip; } export function statusResponse(code: number) { return (_rq: express.Request, rs: express.Response) => { rs.sendStatus(code); - } + }; } export class RateLimiter { + #intervalId: number; - #intervalId: number - - #hitLimit: number + #hitLimit: number; #addressHits: Map = new Map(); @@ -118,17 +149,15 @@ export class RateLimiter { * @param limit Number of hits (inclusive) before requests are blocked */ constructor(interval: number, limit: number) { - this.#hitLimit = limit; this.#intervalId = setInterval(() => { this.#addressHits.clear(); }, interval * 1000); - Deno.addSignalListener('SIGINT', () => { + Deno.addSignalListener("SIGINT", () => { this.#close(); }); - } #addressIncrement(address: string) { @@ -147,66 +176,89 @@ export class RateLimiter { } middle() { - - return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { + return ( + rq: express.Request, + rs: express.Response, + nxt: express.NextFunction, + ) => { const address = getSrcIpDefault(rq); this.#addressIncrement(address); const hits = this.#getAddressHits(address); if (hits && hits > this.#hitLimit) { rs.statusCode = 429; - rs.json(genericResponseFormat(true, `Rate limit for address ${address} reached. Try again in a moment.`)); + rs.json( + genericResponseFormat( + true, + `Rate limit for address ${address} reached. Try again in a moment.`, + ), + ); return; } else nxt(); - } - + }; } #close() { clearInterval(this.#intervalId); } - } -export async function UserAuthentication(rq: express.Request, rs: express.Response, nxt: express.NextFunction) { - +export interface TokenBaseFormat { + typ: AuthType; + iss: string; + nbf: number; + exp: number; + iat: number; +} +export type TokenFormat = UserTokenFormat | ProfileTokenFormat; + +export async function Authentication( + rq: express.Request, + rs: express.Response, + nxt: express.NextFunction, +) { function returnUnauthorized() { rs.statusCode = 401; - rs.json(genericResponseFormat(true, 'Authorization required.')); + rs.json(genericResponseFormat(true, "Authorization required.")); } - const token: string | undefined = rq.header('GalvanicAuth'); - if (typeof token == 'undefined') { + const token: string | undefined = rq.header("GalvanicAuth"); + if (typeof token == "undefined") { returnUnauthorized(); return; } try { - const decodedToken = await decode(token, config.auth.secret, { algorithm: "HS512" }); + const decodedToken = await decode( + token, + config.auth.secret, + { + algorithm: "HS512", + }, + ); const valid = ![ decodedToken.iss == config.web.publichost, decodedToken.nbf < Math.round(Date.now() / 1000), decodedToken.exp > Math.round(Date.now() / 1000), - decodedToken.typ == AuthType.Web ].includes(false); if (valid) { - rs.locals.user = new User(decodedToken.sub); + if (decodedToken.typ == AuthType.Web) { + rs.locals.user = new User(decodedToken.sub); + } else if (decodedToken.typ == AuthType.Game) { + rs.locals.profile = new Profile(decodedToken.sub); + } nxt(); - } - else { + } else { returnUnauthorized(); return; } - } catch (err) { returnUnauthorized(); log.w(`User Authentication failed: ${err}`); } - - } -export type NoBody = Record +export type NoBody = Record; -export * as APIUtils from "./apiutils.ts" \ No newline at end of file +export * as APIUtils from "./apiutils.ts"; diff --git a/src/config.ts b/src/config.ts index cb6c679..3a1270d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,58 +4,58 @@ import * as fs from "node:fs"; const log = new Logging("Config"); type RedisConfiguration = { - host: string, - port: number, - username: string, - password: string, - db: number -} + host: string; + port: number; + username: string; + password: string; + db: number; +}; type WebConfiguration = { - port: number, - host: string, - publichost: string, - securepublichost: boolean -} + port: number; + host: string; + publichost: string; + securepublichost: boolean; +}; type PublicConfiguration = { - serverName: string, - serverId: string, - owner: string, - motd: string, - levelScale: number, - maxLevels: number - patches: string[], -} + serverName: string; + serverId: string; + owner: string; + motd: string; + levelScale: number; + maxLevels: number; + patches: string[]; +}; type LoggingConfiguration = { - notfound: boolean, - debug: boolean, - network: boolean -} + notfound: boolean; + debug: boolean; + network: boolean; +}; type DiscordConfiguration = { - token: string, - clientId: string, - guildId: string -} + token: string; + clientId: string; + guildId: string; +}; type AuthConfiguration = { - secret: string, + secret: string; /** * In Hours */ - timeout: number -} + timeout: number; +}; export type GalvanicConfiguration = { - redis: RedisConfiguration, - web: WebConfiguration, - public: PublicConfiguration, - logging: LoggingConfiguration, - discord: DiscordConfiguration | null, - auth: AuthConfiguration -} + redis: RedisConfiguration; + web: WebConfiguration; + public: PublicConfiguration; + logging: LoggingConfiguration; + discord: DiscordConfiguration | null; + auth: AuthConfiguration; +}; export const defaultConfig: GalvanicConfiguration = { redis: { @@ -63,7 +63,7 @@ export const defaultConfig: GalvanicConfiguration = { port: 6379, username: "", password: "", - db: 0 + db: 0, }, web: { port: 3000, @@ -83,20 +83,20 @@ export const defaultConfig: GalvanicConfiguration = { logging: { notfound: false, debug: false, - network: false + network: false, }, discord: null, auth: { secret: "CHANGE-ME-PLEASE", - timeout: 48 - } -} + timeout: 48, + }, +}; /** The current configuration. Read and parsed only during startup. */ let config: GalvanicConfiguration; try { if (!configurationExists()) generateDefaultConfig(); - config = JSON.parse(fs.readFileSync('./config.json').toString()); + config = JSON.parse(fs.readFileSync("./config.json").toString()); } catch (err) { log.e(`Could not get config: ${err}`); Deno.exit(1); @@ -104,12 +104,15 @@ try { /** Does the configuration file exist on the disk? */ export function configurationExists() { - return fs.existsSync('./config.json'); + return fs.existsSync("./config.json"); } /** Place [or overwrite] the [existing] default configuration in the current directory */ export function generateDefaultConfig() { - fs.writeFileSync('./config.json', JSON.stringify(defaultConfig, undefined, ' ')); + fs.writeFileSync( + "./config.json", + JSON.stringify(defaultConfig, undefined, " "), + ); } /** Get current server configuration */ @@ -117,6 +120,6 @@ export function getConfig() { return config; } -export const devMode = Deno.args.includes('--dev'); +export const devMode = Deno.args.includes("--dev"); -export * as Config from './config.ts'; \ No newline at end of file +export * as Config from "./config.ts"; diff --git a/src/data/config.ts b/src/data/config.ts index 0225589..3889dee 100644 --- a/src/data/config.ts +++ b/src/data/config.ts @@ -3,42 +3,46 @@ import { Redis } from "../db.ts"; import { Objectives } from "./objectives.ts"; export type Config = { - Key: string, - Value: string -} + Key: string; + Value: string; +}; export type LevelProgressionItem = { - Level: number, - RequiredXp: number -} + Level: number; + RequiredXp: number; +}; export type PublicConfig = { ServerMaintenance: { - StartsInMinutes: number - }, - LevelProgressionMaps: LevelProgressionItem[], - DailyObjectives: Objectives.Objective[][], - ConfigTable: Config[] -} + StartsInMinutes: number; + }; + LevelProgressionMaps: LevelProgressionItem[]; + DailyObjectives: Objectives.Objective[][]; + ConfigTable: Config[]; +}; export function getConfig() { const c = Config.getConfig(); - if (typeof c == 'undefined') return null; + if (typeof c == "undefined") return null; const config = c as Config.GalvanicConfiguration; function generateLevelProgressionMap() { const m: LevelProgressionItem[] = []; - for (let i = 0; i < config.public.maxLevels + 1; i++) - m.push({Level: i, RequiredXp: Math.round(i * config.public.levelScale * 20)}); + for (let i = 0; i < config.public.maxLevels + 1; i++) { + m.push({ + Level: i, + RequiredXp: Math.round(i * config.public.levelScale * 20), + }); + } return m; } - + const conf: PublicConfig = { ServerMaintenance: { - StartsInMinutes: 0 + StartsInMinutes: 0, }, LevelProgressionMaps: generateLevelProgressionMap(), DailyObjectives: [], - ConfigTable: [] - } + ConfigTable: [], + }; return conf; } @@ -46,11 +50,17 @@ export function getConfig() { export async function getAllGameConfigs() { try { const gameConfigs = new Map(); - const val = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game)); + const val = await Redis.Database.hgetall( + Redis.buildKey( + Redis.KeyGroups.Config.Root, + Redis.KeyGroups.Config.Game, + ), + ); - for (const key of Object.keys(val)) + for (const key of Object.keys(val)) { gameConfigs.set(key, val[key]); - + } + return gameConfigs; } catch (error) { console.error("Error fetching game configs:", error); @@ -59,10 +69,23 @@ export async function getAllGameConfigs() { } export function setGameConfig(key: string, value: string) { - return Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game), key, value); + return Redis.Database.hset( + Redis.buildKey( + Redis.KeyGroups.Config.Root, + Redis.KeyGroups.Config.Game, + ), + key, + value, + ); } export function getGameConfig(key: string) { - return Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game), key); + return Redis.Database.hget( + Redis.buildKey( + Redis.KeyGroups.Config.Root, + Redis.KeyGroups.Config.Game, + ), + key, + ); } -export * as GameConfigs from "./config.ts"; \ No newline at end of file +export * as GameConfigs from "./config.ts"; diff --git a/src/data/content/avatar.ts b/src/data/content/avatar.ts index 939a526..25fee34 100644 --- a/src/data/content/avatar.ts +++ b/src/data/content/avatar.ts @@ -13,5 +13,5 @@ enum AvatarItemType { Wrist = 200, Glove, Watch, - TeamWrist -} \ No newline at end of file + TeamWrist, +} diff --git a/src/data/content/baseimages.ts b/src/data/content/baseimages.ts new file mode 100644 index 0000000..f8894f4 --- /dev/null +++ b/src/data/content/baseimages.ts @@ -0,0 +1,15 @@ +const base = Deno.mainModule.substring(8, Deno.mainModule.length - 11); + +export function getBaseImage(name: string) { + try { + return Deno.readFileSync(`${base}/res/img/${name}`); + } catch (_err) { + return null; + } +} +export function getAllBaseImages() { + return Array.from( + Deno.readDirSync(`${base}/res/img/`).map((val) => val.isFile ? val.name : undefined), + ).filter((val) => typeof val == "string"); +} +// todo: make this async diff --git a/src/data/content/comsumable.ts b/src/data/content/comsumable.ts index 69d71d4..32f52da 100644 --- a/src/data/content/comsumable.ts +++ b/src/data/content/comsumable.ts @@ -5,7 +5,7 @@ export enum Consumable { CHOCOLATE_FROSTED_DONUTS, CHEESE_PIZZA, PEPPERONI_PIZZA, - GLAZED_DONUTS + GLAZED_DONUTS, } const ids = [ @@ -15,26 +15,21 @@ const ids = [ "mMCGPgK3tki5S_15q2Z81A", "5hIAZ9wg5EyG1cILf4FS2A", "mq23W-RSP0G8iGNLdrcpUw", - "7OZ5AE3uuUyqa0P-2W1ptg" + "7OZ5AE3uuUyqa0P-2W1ptg", ] as const; export class ConsumableSelection { - type: Consumable; guid: string; constructor(type: Consumable) { - this.type = type; this.guid = ids[type]; - } - } export class ConsumableBuilder { - Id: number; ConsumableItemDesc: string; @@ -47,7 +42,13 @@ export class ConsumableBuilder { IsActive: boolean; - constructor(selection: ConsumableSelection, id: number, createdAt: Date, count: number, active: boolean) { + constructor( + selection: ConsumableSelection, + id: number, + createdAt: Date, + count: number, + active: boolean, + ) { this.Id = id; this.ConsumableItemDesc = selection.guid; this.CreatedAt = createdAt.toUTCString(); @@ -55,5 +56,4 @@ export class ConsumableBuilder { this.UnlockedLevel = 0; // All players have access to every consumable - avatars and equipment are different this.IsActive = active; } - -} \ No newline at end of file +} diff --git a/src/data/content/gamerooms.ts b/src/data/content/gamerooms.ts new file mode 100644 index 0000000..f95686a --- /dev/null +++ b/src/data/content/gamerooms.ts @@ -0,0 +1,36 @@ +interface InternalScene { + Name: string; + ReplicationId: string; + RoomSceneLocationId: string; + IsSandbox: boolean; + CanMatchmakeInto: boolean; + SupportsJoinInProgress: boolean; + UseLevelBasedMatchmaking: boolean; + UseAgeBasedMatchmaking: boolean; + MaxPlayers: number; +} +interface InternalRoom { + Name: string; + ReplicationId: string; + Description: string; + Accessibility: number; + SupportsLevelVoting: boolean; + CloningAllowed: boolean; + SupportsScreens: boolean; + SupportsWalkVR: boolean; + SupportsTeleportVR: boolean; + Scenes: InternalScene[]; +} +const rooms: InternalRoom[] = JSON.parse( + Deno.readTextFileSync(import.meta.dirname + "/res/rooms.json"), +); + +export function getInternalRoom(name: string) { + return rooms.find((val) => val.Name == name); +} +export function getAllInternalRooms() { + return rooms; +} +export function getAllInternalRoomNames() { + return rooms.map((val) => val.Name); +} diff --git a/src/data/content/images.ts b/src/data/content/images.ts new file mode 100644 index 0000000..bedd5d8 --- /dev/null +++ b/src/data/content/images.ts @@ -0,0 +1,24 @@ +import { Buffer } from "node:buffer"; +import { Redis } from "../../db.ts"; + +export async function getImage(filename: string) { + const data = await Redis.Database.get( + Redis.buildKey( + Redis.KeyGroups.Content.Root, + Redis.KeyGroups.Content.Images, + filename, + ), + ); + if (data == null) return null; + else return Buffer.from(data); +} +export async function setImage(filename: string, data: Uint8Array) { + await Redis.Database.set( + Redis.buildKey( + Redis.KeyGroups.Content.Root, + Redis.KeyGroups.Content.Images, + filename, + ), + Buffer.from(data), + ); +} diff --git a/src/data/objectives.ts b/src/data/objectives.ts index b3b085c..7fe8753 100644 --- a/src/data/objectives.ts +++ b/src/data/objectives.ts @@ -85,12 +85,12 @@ export enum ObjectiveType { ArenaBotTags, RecRoyaleGames = 3000, RecRoyaleWins, - RecRoyaleTags + RecRoyaleTags, } export type Objective = { - type: ObjectiveType, - score: number -} + type: ObjectiveType; + score: number; +}; -export * as Objectives from "./objectives.ts"; \ No newline at end of file +export * as Objectives from "./objectives.ts"; diff --git a/src/data/profiles.ts b/src/data/profiles.ts index c1a88cd..3214763 100644 --- a/src/data/profiles.ts +++ b/src/data/profiles.ts @@ -3,86 +3,171 @@ import Dictionary from "./usernames.ts"; import { Config } from "../config.ts"; import { AuthType } from "./users.ts"; import * as JsonWebToken from "@gz/jwt"; +import { TokenBaseFormat } from "../apiutils.ts"; const config = Config.getConfig(); interface ProfileInitOptions { - username: string + username: string; } interface AccountExport { - accountId: number, - profileImage: string, - isJunior: boolean, - platforms: number, - username: string, - displayName: string + accountId: number; + profileImage: string; + isJunior: boolean; + platforms: number; + username: string; + displayName: string; } -export type ProfileTokenFormat = { - iss: string; +export interface ProfileTokenFormat extends TokenBaseFormat { sub: number; - nbf: number; - iat: number; - exp: number; - typ: AuthType; + typ: AuthType.Game; } class Profile { + static async exists(id: number) { + return (await Redis.Database.exists( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + id.toString(), + Redis.KeyGroups.Profiles.Username, + ), + )) >= 1; + } static async getUniqueId() { let id = Math.round(Math.random() * Math.pow(2, 31)); - while ((await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username))) >= 1) + while ( + (await Redis.Database.exists( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + id.toString(), + Redis.KeyGroups.Profiles.Username, + ), + )) >= 1 + ) { id = await this.getUniqueId(); + } return id; } static async byName(name: string) { - const id = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name)); + const id = await Redis.Database.get( + Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name), + ); if (id == null) return null; else return new Profile(parseInt(id, 10)); } static async getUniqueUsername() { - let username = `${Dictionary.Adjectives[Math.floor(Math.random() * Dictionary.Adjectives.length)]}${Dictionary.Nouns[Math.floor(Math.random() * Dictionary.Nouns.length)]}${Math.round(Math.random() * 10000)}` - while ((await Profile.byName(username)) !== null) username = await this.getUniqueUsername(); + let username = `${ + Dictionary + .Adjectives[ + Math.floor(Math.random() * Dictionary.Adjectives.length) + ] + }${ + Dictionary + .Nouns[Math.floor(Math.random() * Dictionary.Nouns.length)] + }${Math.round(Math.random() * 10000)}`; + while ((await Profile.byName(username)) !== null) { + username = await this.getUniqueUsername(); + } return username; } static async init(options?: ProfileInitOptions) { + const optionsSpecified = typeof options !== "undefined"; - const optionsSpecified = typeof options !== 'undefined'; - const newId = await this.getUniqueId(); - const newUsername = optionsSpecified ? options.username : await this.getUniqueUsername(); + const newUsername = optionsSpecified + ? options.username + : await this.getUniqueUsername(); - await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profile_Usernames, newUsername), newId); - await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, newId.toString(), Redis.KeyGroups.Profiles.Username), newUsername); + await Redis.Database.set( + Redis.buildKey(Redis.KeyGroups.Profile_Usernames, newUsername), + newId, + ); + await Redis.Database.set( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + newId.toString(), + Redis.KeyGroups.Profiles.Username, + ), + newUsername, + ); return new Profile(newId); - } // surely this can be written better static getExportAccount(id: number): Promise { return new Promise((resolve, _reject) => { - Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username)).then(val => { + Redis.Database.get( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + id.toString(), + Redis.KeyGroups.Profiles.Username, + ), + ).then((val) => { if (val == null) resolve(null); else { const promises = { - profileImage: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.ProfileImage)), - isJunior: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Junior)), - platforms: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Platforms)), - displayName: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.DisplayName)), - username: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username)), - } - + profileImage: Redis.Database.get( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + id.toString(), + Redis.KeyGroups.Profiles.ProfileImage, + ), + ), + isJunior: Redis.Database.get( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + id.toString(), + Redis.KeyGroups.Profiles.Junior, + ), + ), + platforms: Redis.Database.get( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + id.toString(), + Redis.KeyGroups.Profiles.Platforms, + ), + ), + displayName: Redis.Database.get( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + id.toString(), + Redis.KeyGroups.Profiles.DisplayName, + ), + ), + username: Redis.Database.get( + Redis.buildKey( + Redis.KeyGroups.Profiles.Root, + id.toString(), + Redis.KeyGroups.Profiles.Username, + ), + ), + }; + Promise.all(Object.values(promises)).then((values) => { resolve({ accountId: id, - profileImage: values[0] == null ? "DefaultProfileImage" : values[0], - isJunior: values[1] == null ? false : JSON.parse(values[1]), - platforms: values[2] == null ? 1 : JSON.parse(values[2]), - displayName: values[3] == null ? (values[4] == null ? "DATABASEERROR" : values[4]) : values[3], - username: values[4] == null ? "DATABASEERROR" : values[4], + profileImage: values[0] == null + ? "DefaultProfileImage" + : values[0], + isJunior: values[1] == null + ? false + : JSON.parse(values[1]), + platforms: values[2] == null + ? 1 + : JSON.parse(values[2]), + displayName: values[3] == null + ? (values[4] == null + ? "DATABASEERROR" + : values[4]) + : values[3], + username: values[4] == null + ? "DATABASEERROR" + : values[4], }); }); } @@ -91,10 +176,10 @@ class Profile { } static async getExportAccountsBulk(ids: number[]) { - - const accs = await Promise.all(ids.map(val => this.getExportAccount(val))); - return accs.filter(val => val !== null); - + const accs = await Promise.all( + ids.map((val) => this.getExportAccount(val)), + ); + return accs.filter((val) => val !== null); } #id: number; @@ -103,7 +188,7 @@ class Profile { this.#id = id; } - getId() { + getId() { return this.#id; } @@ -117,12 +202,14 @@ class Profile { sub: this.#id, nbf: Math.round(Date.now() / 1000) - 200, iat: Math.round(Date.now() / 1000), - exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60), - typ: AuthType.Web + exp: Math.round(Date.now() / 1000) + + (config.auth.timeout * 60 * 60), + typ: AuthType.Game, }; - return await JsonWebToken.encode(payload, config.auth.secret, { algorithm: "HS512" }); + return await JsonWebToken.encode(payload, config.auth.secret, { + algorithm: "HS512", + }); } - } -export default Profile; \ No newline at end of file +export default Profile; diff --git a/src/data/usernames.ts b/src/data/usernames.ts index bacf63c..64ca4c9 100644 --- a/src/data/usernames.ts +++ b/src/data/usernames.ts @@ -1,6 +1,301 @@ const Dictionary = { - Adjectives: ["Amazing","Adventurous","Affable","Agreeable","Ambitious","Amicable","Animated","Approachable","Articulate","Astute","Attractive","Authentic","Benevolent","Blissful","Bold","Bright","Buoyant","Calm","Captivating","Charismatic","Cheerful","Clever","Compassionate","Confident","Considerate","Content","Cooperative","Courageous","Creative","Cultured","Curious","Dashing","Dazzling","Dedicated","Delightful","Dependable","Determined","Diligent","Dynamic","Earnest","Easygoing","Ebullient","Effervescent","Empathetic","Enchanting","Endearing","Energetic","Engaging","Enthusiastic","Exuberant","Fantastic","Fearless","Fervent","Friendly","Funny","Generous","Gentle","Genuine","Gracious","Grateful","Harmonious","Heartwarming","Helpful","Honest","Humble","Humorous","Imaginative","Impeccable","Incisive","Incredible","Independent","Industrious","Ingenious","Insightful","Intelligent","Intuitive","Invigorating","Jovial","Jubilant","Just","Kind","Knowledgeable","Likable","Lively","Lovable","Loving","Loyal","Luminous","Magnetic","Marvelous","Masterful","Mature","Merciful","Methodical","Meticulous","Mindful","Motivated","Natural","Nurturing","Observant","Optimistic","Outgoing","Passionate","Patient","Peaceful","Perceptive","Perseverant","Persistent","Persuasive","Personable","Philanthropic","Placid","Playful","Pleasant","Poised","Positive","Powerful","Pragmatic","Proactive","Proficient","Prudent","Punctual","Purposeful","Radiant","Rational","Real","Receptive","Reflective","Reliable","Resilient","Resourceful","Respectful","Responsible","Robust","Sagacious","Serene","Sincere","Skillful","Smart","Sociable","Spirited","Splendid","Spontaneous","Steady","Sterling","Strong","Sublime","Successful","Supportive","Sympathetic","Talented","Tenacious","Thoughtful","Tireless","Tolerant","Tough","Tranquil","Trustworthy","Unassuming","Understanding","Unique","Unpretentious","Upbeat","Valiant","Vibrant","Virtuous","Visionary","Vivacious","Warmhearted","Welcoming","Wise","Witty","Wonderful","Zealous"], - Nouns: ["Nomad","Solstice","Elysium","Horizon","Catalyst","Luminescence","Utopia","Eclipse","Nebula","Arcadia","Apex","Harmony","Zenith","Radiant","Infinity","Echo","Quasar","Cascade","Empyrean","Nebula","Odyssey","Aether","Empower","Zephyr","Vibrance","Astral","Jubilant","Ascendancy","Zen","Nebulous","Ecliptic","Stellar","Quantum","Ethereal","Nexus","Synergy","Quantum","Enigma","Luminous","Epoch","Serendipity","Zenithal","Paragon","Panorama","Maverick","Voyager","Luminary","Catalyst","Phoenix","Dynamo","Zenith","Nexus","Pinnacle","Rhapsody","Serenity","Quantum","Apex","Harmony","Odyssey","Endeavor","Visionary","Epoch","Renaissance","Panache","Jubilee","Resonance","Zen","Nimbus","Ethereal","Cascade","Radiance","Synchronicity","Nebula","Equinox","Pulsar","Apex","Ethos","Wanderlust","Zenith","Nebula","Vertex","Equinox","Odyssey","Pantheon","Elysian","Nebulous","Quantum","Harmonic","Luminance","Paragon","Radiant","Epoch","Vortex","Celestia","Infinitum","Empyrean","Zephyr","Nimbus","Seraphic","Enigma","Synergy","Ecliptic","Utopian","Phoenix","Catalyst","Euphoria","Astral","Nebula","Ethereal","Zenith","Nexus","Empower","Panorama","Cascade","Quantum","Jubilant","Zen","Radiance","Labyrinth"] -} + Adjectives: [ + "Amazing", + "Adventurous", + "Affable", + "Agreeable", + "Ambitious", + "Amicable", + "Animated", + "Approachable", + "Articulate", + "Astute", + "Attractive", + "Authentic", + "Benevolent", + "Blissful", + "Bold", + "Bright", + "Buoyant", + "Calm", + "Captivating", + "Charismatic", + "Cheerful", + "Clever", + "Compassionate", + "Confident", + "Considerate", + "Content", + "Cooperative", + "Courageous", + "Creative", + "Cultured", + "Curious", + "Dashing", + "Dazzling", + "Dedicated", + "Delightful", + "Dependable", + "Determined", + "Diligent", + "Dynamic", + "Earnest", + "Easygoing", + "Ebullient", + "Effervescent", + "Empathetic", + "Enchanting", + "Endearing", + "Energetic", + "Engaging", + "Enthusiastic", + "Exuberant", + "Fantastic", + "Fearless", + "Fervent", + "Friendly", + "Funny", + "Generous", + "Gentle", + "Genuine", + "Gracious", + "Grateful", + "Harmonious", + "Heartwarming", + "Helpful", + "Honest", + "Humble", + "Humorous", + "Imaginative", + "Impeccable", + "Incisive", + "Incredible", + "Independent", + "Industrious", + "Ingenious", + "Insightful", + "Intelligent", + "Intuitive", + "Invigorating", + "Jovial", + "Jubilant", + "Just", + "Kind", + "Knowledgeable", + "Likable", + "Lively", + "Lovable", + "Loving", + "Loyal", + "Luminous", + "Magnetic", + "Marvelous", + "Masterful", + "Mature", + "Merciful", + "Methodical", + "Meticulous", + "Mindful", + "Motivated", + "Natural", + "Nurturing", + "Observant", + "Optimistic", + "Outgoing", + "Passionate", + "Patient", + "Peaceful", + "Perceptive", + "Perseverant", + "Persistent", + "Persuasive", + "Personable", + "Philanthropic", + "Placid", + "Playful", + "Pleasant", + "Poised", + "Positive", + "Powerful", + "Pragmatic", + "Proactive", + "Proficient", + "Prudent", + "Punctual", + "Purposeful", + "Radiant", + "Rational", + "Real", + "Receptive", + "Reflective", + "Reliable", + "Resilient", + "Resourceful", + "Respectful", + "Responsible", + "Robust", + "Sagacious", + "Serene", + "Sincere", + "Skillful", + "Smart", + "Sociable", + "Spirited", + "Splendid", + "Spontaneous", + "Steady", + "Sterling", + "Strong", + "Sublime", + "Successful", + "Supportive", + "Sympathetic", + "Talented", + "Tenacious", + "Thoughtful", + "Tireless", + "Tolerant", + "Tough", + "Tranquil", + "Trustworthy", + "Unassuming", + "Understanding", + "Unique", + "Unpretentious", + "Upbeat", + "Valiant", + "Vibrant", + "Virtuous", + "Visionary", + "Vivacious", + "Warmhearted", + "Welcoming", + "Wise", + "Witty", + "Wonderful", + "Zealous", + ], + Nouns: [ + "Nomad", + "Solstice", + "Elysium", + "Horizon", + "Catalyst", + "Luminescence", + "Utopia", + "Eclipse", + "Nebula", + "Arcadia", + "Apex", + "Harmony", + "Zenith", + "Radiant", + "Infinity", + "Echo", + "Quasar", + "Cascade", + "Empyrean", + "Nebula", + "Odyssey", + "Aether", + "Empower", + "Zephyr", + "Vibrance", + "Astral", + "Jubilant", + "Ascendancy", + "Zen", + "Nebulous", + "Ecliptic", + "Stellar", + "Quantum", + "Ethereal", + "Nexus", + "Synergy", + "Quantum", + "Enigma", + "Luminous", + "Epoch", + "Serendipity", + "Zenithal", + "Paragon", + "Panorama", + "Maverick", + "Voyager", + "Luminary", + "Catalyst", + "Phoenix", + "Dynamo", + "Zenith", + "Nexus", + "Pinnacle", + "Rhapsody", + "Serenity", + "Quantum", + "Apex", + "Harmony", + "Odyssey", + "Endeavor", + "Visionary", + "Epoch", + "Renaissance", + "Panache", + "Jubilee", + "Resonance", + "Zen", + "Nimbus", + "Ethereal", + "Cascade", + "Radiance", + "Synchronicity", + "Nebula", + "Equinox", + "Pulsar", + "Apex", + "Ethos", + "Wanderlust", + "Zenith", + "Nebula", + "Vertex", + "Equinox", + "Odyssey", + "Pantheon", + "Elysian", + "Nebulous", + "Quantum", + "Harmonic", + "Luminance", + "Paragon", + "Radiant", + "Epoch", + "Vortex", + "Celestia", + "Infinitum", + "Empyrean", + "Zephyr", + "Nimbus", + "Seraphic", + "Enigma", + "Synergy", + "Ecliptic", + "Utopian", + "Phoenix", + "Catalyst", + "Euphoria", + "Astral", + "Nebula", + "Ethereal", + "Zenith", + "Nexus", + "Empower", + "Panorama", + "Cascade", + "Quantum", + "Jubilant", + "Zen", + "Radiance", + "Labyrinth", + ], +}; -export default Dictionary; \ No newline at end of file +export default Dictionary; diff --git a/src/data/users.ts b/src/data/users.ts index 7f92b03..fc87e0f 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -2,36 +2,38 @@ import { Redis } from "../db.ts"; import * as JsonWebToken from "@gz/jwt"; import { Config } from "../config.ts"; import Profile from "./profiles.ts"; +import { TokenBaseFormat } from "../apiutils.ts"; type UserInitOptions = { - client_id: string, - pubkey: string -} + client_id: string; + pubkey: string; +}; type UserCreatedObj = { - user: User -} + user: User; +}; export enum AuthType { Game, - Web + Web, } -export type UserTokenFormat = { - iss: string; +export interface UserTokenFormat extends TokenBaseFormat { sub: string; - nbf: number; - iat: number; - exp: number; - typ: AuthType; + typ: AuthType.Web; } const config = Config.getConfig(); export class User { - static async exists(id: string) { - return (await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Users.Root, id, Redis.KeyGroups.Users.Pubkey))) == 1; + return (await Redis.Database.exists( + Redis.buildKey( + Redis.KeyGroups.Users.Root, + id, + Redis.KeyGroups.Users.Pubkey, + ), + )) == 1; } /** @@ -41,8 +43,15 @@ export class User { static async init(options: UserInitOptions) { if (await User.exists(options.client_id)) return null; - Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Users.Root, options.client_id, Redis.KeyGroups.Users.Pubkey), options.pubkey); - + Redis.Database.set( + Redis.buildKey( + Redis.KeyGroups.Users.Root, + options.client_id, + Redis.KeyGroups.Users.Pubkey, + ), + options.pubkey, + ); + const user = new User(options.client_id); return user; } @@ -53,12 +62,18 @@ export class User { this.#client_id = client_id; } - getUuid() { + getId() { return this.#client_id; } async exists() { - return (await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Pubkey))) >= 1; + return (await Redis.Database.exists( + Redis.buildKey( + Redis.KeyGroups.Users.Root, + this.#client_id, + Redis.KeyGroups.Users.Pubkey, + ), + )) >= 1; } async getToken() { @@ -67,10 +82,13 @@ export class User { sub: this.#client_id, nbf: Math.round(Date.now() / 1000) - 200, iat: Math.round(Date.now() / 1000), - exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60), - typ: AuthType.Web + exp: Math.round(Date.now() / 1000) + + (config.auth.timeout * 60 * 60), + typ: AuthType.Web, }; - return await JsonWebToken.encode(payload, config.auth.secret, {algorithm: "HS512"}); + return await JsonWebToken.encode(payload, config.auth.secret, { + algorithm: "HS512", + }); } async exportAssociatedProfiles() { @@ -79,28 +97,83 @@ export class User { } async getAssociatedProfiles() { - const list = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Profiles)); - return new Set(list.filter(val => !Number.isNaN(parseInt(val, 10))).map(val => parseInt(val, 10))); + const list = await Redis.Database.smembers( + Redis.buildKey( + Redis.KeyGroups.Users.Root, + this.#client_id, + Redis.KeyGroups.Users.Profiles, + ), + ); + return new Set( + list.filter((val) => !Number.isNaN(parseInt(val, 10))).map((val) => + parseInt(val, 10) + ), + ); } async removeAssociatedProfile(id: number) { - await Redis.Database.srem(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Profiles), id); + await Redis.Database.srem( + Redis.buildKey( + Redis.KeyGroups.Users.Root, + this.#client_id, + Redis.KeyGroups.Users.Profiles, + ), + id, + ); } async addAssociatedProfile(id: number) { - await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Profiles), id); + const dbkey = Redis.buildKey( + Redis.KeyGroups.Users.Root, + this.#client_id, + Redis.KeyGroups.Users.Profiles, + ); + if ((await Redis.Database.sismember(dbkey, id)) >= 1) return false; + await Redis.Database.sadd(dbkey, id); + return true; } async addAssociatedPlatformId(id: string) { - await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.PlatformIds), id); + const dbkey = Redis.buildKey( + Redis.KeyGroups.Users.Root, + this.#client_id, + Redis.KeyGroups.Users.AssociatedPlatforms, + ); + if ((await Redis.Database.sismember(dbkey, id)) >= 1) return false; + await Redis.Database.sadd(dbkey, id); + return true; + } + + async addAssociatedDeviceId(id: string) { + const dbkey = Redis.buildKey( + Redis.KeyGroups.Users.Root, + this.#client_id, + Redis.KeyGroups.Users.AssociatedDeviceIds, + ); + if ((await Redis.Database.sismember(dbkey, id)) >= 1) return false; + await Redis.Database.sadd(dbkey, id); + return true; + } + + async addAssociatedIp(ip: string) { + const dbkey = Redis.buildKey( + Redis.KeyGroups.Users.Root, + this.#client_id, + Redis.KeyGroups.Users.AssociatedIps, + ); + if ((await Redis.Database.sismember(dbkey, ip)) >= 1) return false; + await Redis.Database.sadd(dbkey, ip); + return true; } async addNonce(str: string) { - await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Nonces), str); + const dbkey = Redis.buildKey( + Redis.KeyGroups.Users.Root, + this.#client_id, + Redis.KeyGroups.Users.Nonces, + ); + if ((await Redis.Database.sismember(dbkey, str)) >= 1) return false; + await Redis.Database.sadd(dbkey, str); + return true; } - - async hasNonce(str: string) { - return (await Redis.Database.sismember(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Nonces), str)) >= 1; - } - -} \ No newline at end of file +} diff --git a/src/db.ts b/src/db.ts index 9801940..7d78619 100644 --- a/src/db.ts +++ b/src/db.ts @@ -6,16 +6,16 @@ import chalk from "npm:chalk@^5.3.0"; const log = new Logging("Redis"); const config = Config.getConfig(); -if (typeof config == 'undefined') { +if (typeof config == "undefined") { log.e(`Cannot start: Redis configuration failed`); Deno.exit(1); } let shuttingDown = false; -Deno.addSignalListener('SIGINT', () => { +Deno.addSignalListener("SIGINT", () => { if (shuttingDown) return; shuttingDown = true; - log.n('Disconnecting from Redis'); + log.n("Disconnecting from Redis"); Database.quit(); }); @@ -25,30 +25,37 @@ export const Database = new Redis({ username: config.redis.username == "" ? undefined : config.redis.username, password: config.redis.password == "" ? undefined : config.redis.password, db: config.redis.db, - lazyConnect: true + lazyConnect: true, }); -Database.on('connect', async () => { +Database.on("connect", async () => { log.i(`Connected to Redis`); - if (Deno.args.includes('--db-flush')) await Database.flushall(() => { - log.w(`${chalk.inverse('The database was flushed.')}`); - }); + if (Deno.args.includes("--db-flush")) { + await Database.flushall(() => { + log.w(`${chalk.inverse("The database was flushed.")}`); + }); + } }); -Database.on('connecting', () => { - log.n('Connecting to Redis..'); +Database.on("connecting", () => { + log.n("Connecting to Redis.."); }); -Database.on('error', (err) => { +Database.on("error", (err) => { log.e(`Redis error: ${err.stack}`); }); export function buildKey(...args: string[]) { - return args.join(':'); + return args.join(":"); } export const KeyGroups = { Config: { Root: "config", Dynamic: "dynamic", - Game: "game" + Game: "game", + }, + Content: { + Root: "content", + Images: "images", + Rooms: "rooms", }, Profile_Usernames: "profile-usernames", Profiles: { @@ -57,14 +64,16 @@ export const KeyGroups = { ProfileImage: "profileImage", Junior: "isJunior", Platforms: "platforms", - DisplayName: "displayname" + DisplayName: "displayname", }, Users: { Root: "users", Profiles: "profiles", Pubkey: "pubkey", Nonces: "nonces", - PlatformIds: "associatedPlatforms" - } -} -export * as Redis from "./db.ts"; \ No newline at end of file + AssociatedPlatforms: "associatedPlatforms", + AssociatedDeviceIds: "associatedDeviceIds", + AssociatedIps: "associatedIps", + }, +}; +export * as Redis from "./db.ts"; diff --git a/src/discord.ts b/src/discord.ts index 488d76f..58c1088 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -5,34 +5,41 @@ import Logging from "@proxnet/undead-logging"; const log = new Logging("Discord"); const config = Config.getConfig(); -if (typeof config == 'undefined') { +if (typeof config == "undefined") { log.e(`Cannot start: Discord configuration is unavailable`); Deno.exit(1); } -export const client = new discord.Client({ intents: [discord.GatewayIntentBits.Guilds, discord.GatewayIntentBits.GuildPresences] }); +export const client = new discord.Client({ + intents: [ + discord.GatewayIntentBits.Guilds, + discord.GatewayIntentBits.GuildPresences, + ], +}); -client.once(discord.Events.ClientReady, client => { +client.once(discord.Events.ClientReady, (client) => { log.i(`Logged in to Discord as "${client.user.tag}"`); - client.user?.setActivity(config.public.motd, { type: discord.ActivityType.Custom }); + client.user?.setActivity(config.public.motd, { + type: discord.ActivityType.Custom, + }); }); let shuttingDown = false; -Deno.addSignalListener('SIGINT', () => { +Deno.addSignalListener("SIGINT", () => { if (client.readyTimestamp == null) return; if (shuttingDown) return; shuttingDown = true; - log.n('Disconnecting from Discord'); + log.n("Disconnecting from Discord"); client.destroy(); }); export function login() { if (config.discord?.token == Config.defaultConfig.discord?.token) { - log.i('Discord not configured, ignoring'); + log.i("Discord not configured, ignoring"); return; } log.i(`Creating Discord connection..`); client.login(config.discord?.token); } -export * as Discord from "./discord.ts"; \ No newline at end of file +export * as Discord from "./discord.ts"; diff --git a/src/main.ts b/src/main.ts index 0b91b06..d902621 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,16 +15,20 @@ log.i(`Starting Galvanic Corrosion..`); const config = Config.getConfig(); -if (typeof config == 'undefined') { - log.e('Cannot start: Configuration is undefined'); +if (typeof config == "undefined") { + log.e("Cannot start: Configuration is undefined"); Deno.exit(1); } if (config.auth.secret == Config.defaultConfig.auth.secret) { - log.e(`Cannot start: Auth secret is default. Please change 'secrets.authSecret' in 'config.json'`); + log.e( + `Cannot start: Auth secret is default. Please change 'secrets.authSecret' in 'config.json'`, + ); Deno.exit(1); } if (config.public.serverId == Config.defaultConfig.public.serverId) { - log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`); + log.e( + `Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`, + ); Deno.exit(1); } @@ -45,41 +49,52 @@ log.n(`Starting HTTP server on http://${host}:${port}`); const app = express(); -app.disable('etag'); -app.disable('x-powered-by'); +app.disable("etag"); +app.disable("x-powered-by"); -app.use((rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { - rs.setHeader('Instance', instanceId) - log.n(`${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`); - nxt(); -}); +app.use( + (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { + rs.setHeader("Instance", instanceId); + log.n(`${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`); + nxt(); + }, +); -app.get('/info', (_rq, rs) => { +app.get("/info", (_rq, rs) => { rs.json({ name: config.public.serverName, id: config.public.serverId, motd: config.public.motd, - patches: config.public.patches + patches: config.public.patches, }); }); // content routes -const nameserverRouter = await import('./routes/nameserver.ts'); -const apiRouter = await import('./routes/api.ts'); -const userRouter = await import('./routes/user.ts'); -const authRouter = await import('./routes/auth.ts'); -const accountRouter = await import('./routes/account.ts'); +const nameserverRouter = await import("./routes/nameserver.ts"); +const apiRouter = await import("./routes/api.ts"); +const userRouter = await import("./routes/user.ts"); +const authRouter = await import("./routes/auth.ts"); +const accountRouter = await import("./routes/account.ts"); +const imgRouter = await import("./routes/img.ts"); app.use(nameserverRouter.route.path, nameserverRouter.route.router); app.use(apiRouter.route.path, apiRouter.route.router); app.use(userRouter.route.path, userRouter.route.router); app.use(authRouter.route.path, authRouter.route.router); app.use(accountRouter.route.path, accountRouter.route.router); +app.use(imgRouter.route.path, imgRouter.route.router); app.use((rq: express.Request, rs: express.Response) => { - log.e(`${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`); + log.e( + `${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`, + ); rs.statusCode = 404; - rs.json(APIUtils.genericResponseFormat(true, 'Endpoint not found. Check your syntax and/or method.')); + rs.json( + APIUtils.genericResponseFormat( + true, + "Endpoint not found. Check your syntax and/or method.", + ), + ); }); try { @@ -87,7 +102,7 @@ try { log.n(`Listening on http://${config.web.host}:${config.web.port}`); let shuttingDown = false; - Deno.addSignalListener('SIGINT', () => { + Deno.addSignalListener("SIGINT", () => { if (shuttingDown) return; shuttingDown = true; log.i(`Shutting down`); @@ -100,4 +115,4 @@ try { Deno.exit(1); } -Discord.login(); \ No newline at end of file +Discord.login(); diff --git a/src/routes/account.ts b/src/routes/account.ts index 91c6998..f9af5fb 100644 --- a/src/routes/account.ts +++ b/src/routes/account.ts @@ -1,6 +1,6 @@ import { APIUtils } from "../apiutils.ts"; import { route as AccountRoute } from "./account/account.ts"; -export const route = APIUtils.createRouter('/accountservice'); +export const route = APIUtils.createRouter("/accountservice"); -route.router.use(AccountRoute.path, AccountRoute.router); \ No newline at end of file +route.router.use(AccountRoute.path, AccountRoute.router); diff --git a/src/routes/account/account.ts b/src/routes/account/account.ts index f6650e5..55f387d 100644 --- a/src/routes/account/account.ts +++ b/src/routes/account/account.ts @@ -5,16 +5,23 @@ import Profile from "../../data/profiles.ts"; export const route = APIUtils.createRouter("/account"); interface CreateAccountRequestBody { - platform: string, - platformId: string, - deviceId: string + platform: string; + platformId: string; + deviceId: string; } -route.router.post('/create', +const rateLimit = new APIUtils.RateLimiter(25, 5); - APIUtils.UserAuthentication, +route.router.post("/create", + + rateLimit.middle(), + APIUtils.Authentication, express.urlencoded({ extended: true }), - APIUtils.checkBodyTypes({platform: "", platformId: "", deviceId: ""}), + APIUtils.checkBodyTypes({ + platform: "", + platformId: "", + deviceId: "", + }), async (_rq, rs) => { const newAcc = await Profile.init(); @@ -23,8 +30,43 @@ route.router.post('/create', rs.json({ success: true, - value: await newAcc.export() + value: await newAcc.export(), }); }, +); -); \ No newline at end of file +route.router.get("/bulk", + + rateLimit.middle(), + + async (rq: express.Request, rs: express.Response) => { + + if (typeof rq.query.id == "object") { + + const ids = Object.values(rq.query.id).filter((val) => typeof val == "string").map((val) => parseInt(val, 10)).filter((val) => !isNaN(val)); + rs.json([...await Profile.getExportAccountsBulk(ids)]); + + } else if (typeof rq.query.id == "string") { + + const id = parseInt(rq.query.id, 10); + if (isNaN(id)) { + rs.json( + APIUtils.genericResponseFormat(true, "Query data error"), + ); + return; + } else { + rs.json( + [await Profile.getExportAccount(id)].filter((val) => + val !== null + ), + ); + return; + } + + } else { + rs.json([]); + return; + } + + }, +); diff --git a/src/routes/api.ts b/src/routes/api.ts index 3888a70..9375b56 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -3,8 +3,8 @@ import { route as ConfigRoute } from "./api/config.ts"; import { route as GameConfig } from "./api/gameconfigs.ts"; import { APIUtils } from "../apiutils.ts"; -export const route = APIUtils.createRouter('/api'); +export const route = APIUtils.createRouter("/api"); route.router.use(VersionCheckRoute.path, VersionCheckRoute.router); route.router.use(ConfigRoute.path, ConfigRoute.router); -route.router.use(GameConfig.path, GameConfig.router); \ No newline at end of file +route.router.use(GameConfig.path, GameConfig.router); diff --git a/src/routes/api/config.ts b/src/routes/api/config.ts index ad2ed09..5adcbda 100644 --- a/src/routes/api/config.ts +++ b/src/routes/api/config.ts @@ -1,10 +1,10 @@ import { APIUtils } from "../../apiutils.ts"; import { GameConfigs } from "../../data/config.ts"; -export const route = APIUtils.createRouter('/config'); +export const route = APIUtils.createRouter("/config"); -route.router.get('/v2', (_rq, rs) => { +route.router.get("/v2", (_rq, rs) => { const config = GameConfigs.getConfig(); if (config == null) rs.sendStatus(500); else rs.json(config); -}); \ No newline at end of file +}); diff --git a/src/routes/api/gameconfigs.ts b/src/routes/api/gameconfigs.ts index a6aece8..32358b7 100644 --- a/src/routes/api/gameconfigs.ts +++ b/src/routes/api/gameconfigs.ts @@ -1,7 +1,7 @@ import { APIUtils } from "../../apiutils.ts"; -export const route = APIUtils.createRouter('/gameconfigs'); +export const route = APIUtils.createRouter("/gameconfigs"); -route.router.get('/v1/all', (_rq, rs) => { +route.router.get("/v1/all", (_rq, rs) => { rs.json([]); -}); \ No newline at end of file +}); diff --git a/src/routes/api/versioncheck.ts b/src/routes/api/versioncheck.ts index af0d16e..20db52b 100644 --- a/src/routes/api/versioncheck.ts +++ b/src/routes/api/versioncheck.ts @@ -1,34 +1,35 @@ import { APIUtils } from "../../apiutils.ts"; -export const route = APIUtils.createRouter('/versioncheck'); +export const route = APIUtils.createRouter("/versioncheck"); -const validVersion = '20191120'; +const validVersion = "20191120"; enum VersionStatus { ValidForPlay, ValidForMenu, - UpdateRequired + UpdateRequired, } type ValidVersionResponse = { - VersionStatus: VersionStatus -} + VersionStatus: VersionStatus; +}; -route.router.get('/v4', (rq, rs) => { - const requestedVer = rq.query['v']; - const pQuery = rq.query['p']; - if (typeof requestedVer == 'undefined' || typeof pQuery == 'undefined') { +route.router.get("/v4", (rq, rs) => { + + const requestedVer = rq.query["v"]; + const pQuery = rq.query["p"]; + + if (typeof requestedVer == "undefined" || typeof pQuery == "undefined") { rs.statusCode = 400; - rs.json(APIUtils.genericResponseFormat(true, 'One or more query parameters were not found.')); - } - else if (requestedVer !== validVersion) { + rs.json(APIUtils.genericResponseFormat(true, "One or more query parameters were not found.")); + } else if (requestedVer !== validVersion) { const res: ValidVersionResponse = { - VersionStatus: VersionStatus.UpdateRequired - } + VersionStatus: VersionStatus.UpdateRequired, + }; rs.json(res); } else { const res: ValidVersionResponse = { - VersionStatus: VersionStatus.ValidForPlay - } + VersionStatus: VersionStatus.ValidForPlay, + }; rs.json(res); } -}); \ No newline at end of file +}); diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 61bed05..07b38fa 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -2,7 +2,7 @@ import { APIUtils } from "../apiutils.ts"; import { route as CachedLoginRoute } from "./auth/cachedlogin.ts"; import { route as ConnectRoute } from "./auth/connect.ts"; -export const route = APIUtils.createRouter('/authservice'); +export const route = APIUtils.createRouter("/authservice"); route.router.use(CachedLoginRoute.path, CachedLoginRoute.router); -route.router.use(ConnectRoute.path, ConnectRoute.router); \ No newline at end of file +route.router.use(ConnectRoute.path, ConnectRoute.router); diff --git a/src/routes/auth/cachedlogin.ts b/src/routes/auth/cachedlogin.ts index e2ef2a7..2b4bad7 100644 --- a/src/routes/auth/cachedlogin.ts +++ b/src/routes/auth/cachedlogin.ts @@ -2,14 +2,19 @@ import { APIUtils } from "../../apiutils.ts"; export const route = APIUtils.createRouter("/cachedlogin"); -route.router.get('/forplatformid/:platformtype/:platformid', +route.router.get("/forplatformid/:platformtype/:platformid", - APIUtils.UserAuthentication, + APIUtils.Authentication, async (_rq, rs) => { - - rs.json(await rs.locals.user.exportAssociatedProfiles()); + const profiles = await rs.locals.user.exportAssociatedProfiles(); + rs.json(profiles.map((acc) => ({ + platform: 0, + platformId: rs.locals.user.getId(), + accountId: acc.accountId, + lastLoginTime: new Date().toISOString(), + requirePassword: false, + }))); + }, - } - -); \ No newline at end of file +); diff --git a/src/routes/auth/connect.ts b/src/routes/auth/connect.ts index 5ea8cc0..34b0380 100644 --- a/src/routes/auth/connect.ts +++ b/src/routes/auth/connect.ts @@ -1,5 +1,94 @@ -import { APIUtils } from "../../apiutils.ts"; +import { APIUtils, NoBody } from "../../apiutils.ts"; +import express from "express"; +import Profile from "../../data/profiles.ts"; export const route = APIUtils.createRouter("/connect"); -//route.router.post() \ No newline at end of file +interface TokenRequestBody { + grant_type: string; + account_id: string; + client_id: string; + client_secret: string; + platform: string; + platform_id: string; + device_id: string; + device_class: string; + time: string; + ver: string; + asid: string; + platform_auth: string; +} + +interface TokenResponseBody { + error?: string; + error_description?: string; + access_token: string; + refresh_token: string; +} + +route.router.post("/token", + + APIUtils.Authentication, + express.urlencoded({ extended: true }), + APIUtils.checkBodyTypes({ + grant_type: "", + account_id: "", + client_id: "", + client_secret: "", + platform: "", + platform_id: "", + device_id: "", + device_class: "", + time: "", + ver: "", + asid: "", + platform_auth: "", + }), + + async ( + rq: express.Request, + rs: express.Response, + ) => { + + function requestFailed(msg: string = "invalid_request") { + rs.json({ + error: msg, + access_token: "", + refresh_token: "", + }); + return; + } + + const conditionsMet = ![ + rq.body.grant_type == "cached_login", + rq.body.client_id == "recroom", + rq.body.platform == "0", + rq.body.ver == '20191120', + !(rq.body.device_id.length > 96), + !(rq.body.client_secret.length > 96), + !(rq.body.platform_id.length > 32), + !(rq.body.time.length > 32), + !(rq.body.asid.length > 32), + ].includes(false); + + if (conditionsMet) { + const accounts = await rs.locals.user.getAssociatedProfiles(); + const targetAccount = parseInt(rq.body.account_id); + + if (isNaN(targetAccount)) requestFailed(); + if (!accounts.has(targetAccount)) requestFailed("access_denied"); + + rs.locals.user.addAssociatedDeviceId(rq.body.device_id); + rs.locals.user.addAssociatedPlatformId(rq.body.platform_id); + + const profile = new Profile(targetAccount); + if (!(await Profile.exists(profile.getId()))) requestFailed(); + + const token = await profile.getToken(); + rs.json({ + access_token: token, + refresh_token: token, + }); + } else requestFailed(); + }, +); diff --git a/src/routes/img.ts b/src/routes/img.ts new file mode 100644 index 0000000..f875203 --- /dev/null +++ b/src/routes/img.ts @@ -0,0 +1,106 @@ +import { APIUtils, NoBody } from "../apiutils.ts"; +import * as BaseImages from "../data/content/baseimages.ts"; +import Logging from "@proxnet/undead-logging"; +import express from "express"; +import * as Images from "./../data/content/images.ts"; +import { Image } from "https://deno.land/x/imagescript@1.3.0/mod.ts"; +import { Buffer } from "node:buffer"; + +export const route = APIUtils.createRouter("/img"); + +const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890. " + .split(""); +function sanitizeString(input: string) { + return input.split("").filter((char) => chars.includes(char)).join(""); +} + +const baseImages = BaseImages.getAllBaseImages(); + +interface ImageQueryOptions { + cropSquare?: string; + width?: string; + height?: string; +} + +route.router.get( + "*", + async ( + rq: express.Request, + rs: express.Response, + nxt: express.NextFunction, + ) => { + const filename = sanitizeString( + rq.path.substring(1, rq.path.length).replaceAll("%20", " "), + ); + + // why does it think it is never reassigned? line 39 + // deno-lint-ignore prefer-const + let image: Image; + const imageSource = baseImages.includes(filename) + ? BaseImages.getBaseImage(filename) + : await Images.getImage(filename); + if (imageSource == null) { + nxt(); + return; + } + image = await Image.decode(imageSource); + + let cropSquare: boolean = false; + if (typeof rq.query.cropSquare == "string") { + const d = JSON.parse(rq.query.cropSquare); + if (typeof d == "boolean" && d) cropSquare = true; + } + let width: number | null = null; + if (typeof rq.query.width == "string") { + const num = parseInt(rq.query.width); + if (isNaN(num)) width = null; + else width = num; + } + let height: number | null = null; + if (typeof rq.query.height == "string") { + const num = parseInt(rq.query.height); + if (isNaN(num)) height = null; + else height = num; + } + + if (cropSquare) { + if (image.width > image.height) { + image.crop( + Math.round(image.width / 2) - Math.round(image.height / 2), + 0, + image.height, + image.height, + ); + } else {image.crop( + 0, + Math.round(image.height / 2) - Math.round(image.width / 2), + image.width, + image.width, + );} + } + if (width && height) { + const targetWidth = width > image.width ? image.width : width; + const targetHeight = height > image.height ? image.height : height; + if (image.width > image.height) { + image.resize(Image.RESIZE_AUTO, height); + image.crop( + Math.round(image.width / 2) - Math.round(targetWidth / 2), + 0, + targetWidth, + image.height, + ); + } else { + image.resize(width, Image.RESIZE_AUTO); + image.crop( + 0, + Math.round(image.height / 2) - Math.round(targetHeight / 2), + image.width, + targetHeight, + ); + } + } else if (width) image.resize(width, Image.RESIZE_AUTO); + else if (height) image.resize(Image.RESIZE_AUTO, height); + + rs.type("png").send(Buffer.from(await image.encode())); + }, +); diff --git a/src/routes/nameserver.ts b/src/routes/nameserver.ts index 0081fa2..f6399ff 100644 --- a/src/routes/nameserver.ts +++ b/src/routes/nameserver.ts @@ -2,38 +2,38 @@ import { APIUtils } from "../apiutils.ts"; import { Config } from "../config.ts"; const config = Config.getConfig() as Config.GalvanicConfiguration; -const protocol = config.web.securepublichost ? 'https' : 'http'; +const protocol = config.web.securepublichost ? "https" : "http"; -export const route = APIUtils.createRouter('/ns'); +export const route = APIUtils.createRouter("/ns"); type NameserverHosts = { - Auth: string, - API: string, - WWW: string, - Notifications: string, - Images: string, - CDN: string, - Commerce: string, - Matchmaking: string, - Storage: string, - Chat: string, - Leaderboard: string -} + Auth: string; + API: string; + WWW: string; + Notifications: string; + Images: string; + CDN: string; + Commerce: string; + Matchmaking: string; + Storage: string; + Chat: string; + Leaderboard: string; +}; const nameserver: NameserverHosts = { - Auth: `${protocol}://${config.web.publichost}/auth`, - API: `${protocol}://${config.web.publichost}`, - WWW: `${protocol}://${config.web.publichost}`, - Notifications: `${protocol}://${config.web.publichost}/notify`, - Images: `${protocol}://${config.web.publichost}/img`, - CDN: `${protocol}://${config.web.publichost}/cdn`, - Commerce: `${protocol}://${config.web.publichost}/commerce`, - Matchmaking: `${protocol}://${config.web.publichost}/match`, - Storage: `${protocol}://${config.web.publichost}/storage`, - Chat: `${protocol}://${config.web.publichost}/chat`, - Leaderboard: `${protocol}://${config.web.publichost}/leaderboard` -} + Auth: `${protocol}://${config.web.publichost}/auth`, + API: `${protocol}://${config.web.publichost}`, + WWW: `${protocol}://${config.web.publichost}`, + Notifications: `${protocol}://${config.web.publichost}/notify`, + Images: `${protocol}://${config.web.publichost}/img`, + CDN: `${protocol}://${config.web.publichost}/cdn`, + Commerce: `${protocol}://${config.web.publichost}/commerce`, + Matchmaking: `${protocol}://${config.web.publichost}/match`, + Storage: `${protocol}://${config.web.publichost}/storage`, + Chat: `${protocol}://${config.web.publichost}/chat`, + Leaderboard: `${protocol}://${config.web.publichost}/leaderboard`, +}; -route.router.get('*', (_rq, rs) => { - rs.json(nameserver); +route.router.get("*", (_rq, rs) => { + rs.json(nameserver); }); \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index eda37c9..fad9b7d 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -1,4 +1,4 @@ -import { APIUtils, NoBody } from "../apiutils.ts"; +import { APIUtils, getSrcIpDefault, NoBody } from "../apiutils.ts"; // @ts-types = "npm:@types/express" import express from "express"; import { User } from "../data/users.ts"; @@ -10,25 +10,25 @@ const log = new Logging("UserRoute"); const config = Config.getConfig(); -export const route = APIUtils.createRouter('/user'); +export const route = APIUtils.createRouter("/user"); interface AuthRequestSec { - timestamp: number, - nonce: string, - server_id: string + timestamp: number; + nonce: string; + server_id: string; } interface AuthRequestRoot { - client_id: string, - message: AuthRequestSec, - signature: string, - pubkey: string + client_id: string; + message: AuthRequestSec; + signature: string; + pubkey: string; } const rateLimit = new APIUtils.RateLimiter(60, 1); -route.router.post('/auth', - +route.router.post( + "/auth", rateLimit.middle(), express.json(), APIUtils.checkBodyTypes({ @@ -36,72 +36,85 @@ route.router.post('/auth', message: { timestamp: 0, nonce: "asdf", - server_id: "asdf" + server_id: "asdf", }, signature: "asdf", - pubkey: "asdf" + pubkey: "asdf", }), - - async (rq: express.Request, rs: express.Response) => { - + async ( + rq: express.Request, + rs: express.Response, + ) => { function authFailed(msg: string) { rs.json(APIUtils.genericResponseFormat(true, msg)); } if (rq.body.message.server_id !== config.public.serverId) { - log.w(`Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`); - authFailed('Authentication request not intended for this server.'); + log.w( + `Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`, + ); + authFailed("Authentication request not intended for this server."); return; } try { - const verify = crypto.createVerify('SHA256'); + const verify = crypto.createVerify("SHA256"); verify.update(JSON.stringify(rq.body.message)); verify.end(); const publicKey = await crypto.subtle.importKey( "spki", - (Uint8Array.from(atob(rq.body.pubkey), c => c.charCodeAt(0))).buffer, + (Uint8Array.from(atob(rq.body.pubkey), (c) => c.charCodeAt(0))) + .buffer, { name: "ECDSA", namedCurve: "P-256" }, false, - ["verify"] + ["verify"], + ); + const messageBytes = new TextEncoder().encode( + JSON.stringify(rq.body.message), + ); + const signatureBytes = Uint8Array.from( + atob(rq.body.signature), + (c) => c.charCodeAt(0), ); - const messageBytes = new TextEncoder().encode(JSON.stringify(rq.body.message)); - const signatureBytes = Uint8Array.from(atob(rq.body.signature), c => c.charCodeAt(0)); const isValid = await crypto.subtle.verify( { name: "ECDSA", hash: "SHA-256" }, publicKey, signatureBytes.buffer, - messageBytes + messageBytes, ); if (!isValid) { log.w(`Auth failed for clientId '${rq.body.client_id}'`); - authFailed('Authentication request failed.'); + authFailed("Authentication request failed."); return; } } catch (err) { log.d(`Error when verifying auth request: ${err}`); - authFailed('Authentication request failed.'); + authFailed("Authentication request failed."); return; } let user = new User(rq.body.client_id); if (!(await user.exists())) { - const obj = await User.init({ client_id: rq.body.client_id, pubkey: rq.body.pubkey }); + const obj = await User.init({ + client_id: rq.body.client_id, + pubkey: rq.body.pubkey, + }); if (obj == null) { rs.sendStatus(500); return; } else user = obj; } - if (await user.hasNonce(rq.body.message.nonce)) { - log.w(`Client '${rq.body.client_id}' has already used nonce. Replay attack?`); - authFailed('Authentication request failed.'); + if (!(await user.addNonce(rq.body.message.nonce))) { + log.w( + `Client '${rq.body.client_id}' has already used nonce. Replay attack?`, + ); + authFailed("Authentication request failed."); return; - } else user.addNonce(rq.body.message.nonce); - + } + user.addAssociatedIp(getSrcIpDefault(rq)); + const token = await user.getToken(); rs.json({ token: token }); - - } - -); \ No newline at end of file + }, +); diff --git a/src/types/express.ts b/src/types/express.ts index 67e2490..3d497d9 100644 --- a/src/types/express.ts +++ b/src/types/express.ts @@ -4,8 +4,8 @@ import { User } from "../data/users.ts"; declare global { namespace Express { interface Locals { - profile: Profile - user: User + profile: Profile; + user: User; } } -} \ No newline at end of file +}