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)
## 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
+}