forked from zombieb/galvanic-corrosion-rewrite
duhhhhhhhh
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
/.env
|
/.env
|
||||||
/persist/
|
/persist/
|
||||||
/worklist.txt
|
|
||||||
/node_modules
|
/node_modules
|
||||||
/build
|
/build
|
||||||
|
/todo.txt
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"@oneday/http-status": "jsr:@oneday/http-status@^0.2.0",
|
"@oneday/http-status": "jsr:@oneday/http-status@^0.2.0",
|
||||||
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.5.0",
|
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.5.0",
|
||||||
"@std/assert": "jsr:@std/assert@1",
|
"@std/assert": "jsr:@std/assert@1",
|
||||||
|
"chalk": "npm:chalk@^5.6.2",
|
||||||
"sharp": "npm:sharp@^0.34.3",
|
"sharp": "npm:sharp@^0.34.3",
|
||||||
"zod": "npm:zod@^4.0.5"
|
"zod": "npm:zod@^4.0.5"
|
||||||
},
|
},
|
||||||
|
|||||||
7
deno.lock
generated
7
deno.lock
generated
@@ -18,6 +18,7 @@
|
|||||||
"jsr:@std/path@^1.1.1": "1.1.2",
|
"jsr:@std/path@^1.1.1": "1.1.2",
|
||||||
"npm:@types/node@*": "24.2.0",
|
"npm:@types/node@*": "24.2.0",
|
||||||
"npm:chalk@^5.3.0": "5.6.0",
|
"npm:chalk@^5.3.0": "5.6.0",
|
||||||
|
"npm:chalk@^5.6.2": "5.6.2",
|
||||||
"npm:sharp@~0.34.3": "0.34.3",
|
"npm:sharp@~0.34.3": "0.34.3",
|
||||||
"npm:zod@^4.0.5": "4.1.5"
|
"npm:zod@^4.0.5": "4.1.5"
|
||||||
},
|
},
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
"@proxnet/undead-logging@1.5.1": {
|
"@proxnet/undead-logging@1.5.1": {
|
||||||
"integrity": "f858b6357d52c4bc1bbab279200dae86ed573ea45d945a8dfaf2f2cb23c4b649",
|
"integrity": "f858b6357d52c4bc1bbab279200dae86ed573ea45d945a8dfaf2f2cb23c4b649",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:chalk"
|
"npm:chalk@^5.3.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/assert@1.0.14": {
|
"@std/assert@1.0.14": {
|
||||||
@@ -240,6 +241,9 @@
|
|||||||
"chalk@5.6.0": {
|
"chalk@5.6.0": {
|
||||||
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="
|
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="
|
||||||
},
|
},
|
||||||
|
"chalk@5.6.2": {
|
||||||
|
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
|
||||||
|
},
|
||||||
"color-convert@2.0.1": {
|
"color-convert@2.0.1": {
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -330,6 +334,7 @@
|
|||||||
"jsr:@oneday/http-status@0.2",
|
"jsr:@oneday/http-status@0.2",
|
||||||
"jsr:@proxnet/undead-logging@^1.5.0",
|
"jsr:@proxnet/undead-logging@^1.5.0",
|
||||||
"jsr:@std/assert@1",
|
"jsr:@std/assert@1",
|
||||||
|
"npm:chalk@^5.6.2",
|
||||||
"npm:sharp@~0.34.3",
|
"npm:sharp@~0.34.3",
|
||||||
"npm:zod@^4.0.5"
|
"npm:zod@^4.0.5"
|
||||||
]
|
]
|
||||||
|
|||||||
254
res/consumables.json
Normal file
254
res/consumables.json
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
{
|
||||||
|
"m_GameObject": {
|
||||||
|
"m_FileID": 0,
|
||||||
|
"m_PathID": 0
|
||||||
|
},
|
||||||
|
"m_Enabled": 1,
|
||||||
|
"m_Script": {
|
||||||
|
"m_FileID": 1,
|
||||||
|
"m_PathID": 1574
|
||||||
|
},
|
||||||
|
"m_Name": "ConsumableCollectionRuntimeConfig",
|
||||||
|
"consumablePrefabData": [
|
||||||
|
{
|
||||||
|
"fileName": "[CameraFilterConsumable_BW]",
|
||||||
|
"consumableName": "frOMH6WxDEG1fBqC4_83vg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[CameraFilterConsumable_Dawn]",
|
||||||
|
"consumableName": "m0bVLwWGj0GuIBSb6wCk6Q"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[CameraFilterConsumable_Dracula]",
|
||||||
|
"consumableName": "oG7CdvW7p0-S8sVe9w5vRw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[CameraFilterConsumable_GhostBeard]",
|
||||||
|
"consumableName": "1c0Djlp090uBDczEobYNQw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[CameraFilterConsumable_GoblinKing]",
|
||||||
|
"consumableName": "6SyCoJCgo0Wd6qlPlnMOtg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[CameraFilterConsumable_Jumbotron]",
|
||||||
|
"consumableName": "Il4VmrnjDkqjmjzddqoIEw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[CameraFilterConsumable_Sepia]",
|
||||||
|
"consumableName": "A5M-yf9tgUihq1uab3v58g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[CameraFilterConsumable_Witch]",
|
||||||
|
"consumableName": "mL3zCEuy2UWZxAV4V-OsMA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HeadPotionConsumable_DKMode]",
|
||||||
|
"consumableName": "Tpxqe_lycUelySRHM8B0Vw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HandPotionConsumable_Goblin1]",
|
||||||
|
"consumableName": "-hy0qD-iUk-v4NHxNzanmg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HandPotionConsumable_Pirate1]",
|
||||||
|
"consumableName": "YEfbJTnsR0yT_p7e7tb_kQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HandPotionConsumable_SciFi1]",
|
||||||
|
"consumableName": "xHOwjwpXd0GDkvBz2VqieA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HandPotionConsumable_Goblin2]",
|
||||||
|
"consumableName": "VQSgL2pTLkWx4B3kwYG7UA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HandPotionConsumable_Dracula1]",
|
||||||
|
"consumableName": "lag2tZyB90W04lQ7ol4vMw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_Donut_Assorted]",
|
||||||
|
"consumableName": "ZuvkidodzkuOfGLDnTOFyg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_Bubbly]",
|
||||||
|
"consumableName": "iiGTvhOCHkOTNJhb16Zbyw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_CheesePizza]",
|
||||||
|
"consumableName": "5hIAZ9wg5EyG1cILf4FS2A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_Donut_Frosted]",
|
||||||
|
"consumableName": "mMCGPgK3tki5S_15q2Z81A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_Donut_Glazed]",
|
||||||
|
"consumableName": "7OZ5AE3uuUyqa0P-2W1ptg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_HawaiianPizza]",
|
||||||
|
"consumableName": "_jnjYGBcyEWY5Ub4OezXcA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_PepperoniPizza]",
|
||||||
|
"consumableName": "mq23W-RSP0G8iGNLdrcpUw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_Popcorn_Butter]",
|
||||||
|
"consumableName": "QRx0aSTT9keMFdAJMQHdTg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_RedApples]",
|
||||||
|
"consumableName": "uMHrUPLYFk2rJOW_uop5Aw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_RootBeer]",
|
||||||
|
"consumableName": "JfnVXFmilU6ysv-VbTAe3A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_Pretzel_Salted]",
|
||||||
|
"consumableName": "InQ25wQMGkG_bvuD5rf2Ag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[FoodConsumable_SupremePizza]",
|
||||||
|
"consumableName": "wUCIKdJSvEmiQHYMyx4X4w"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_BearClaw]",
|
||||||
|
"consumableName": "ZtZnYBpKkECJlhHmkj4MiA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_Bees]",
|
||||||
|
"consumableName": "g3kxdlJv5kO8PuBXveM48w"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_Cola]",
|
||||||
|
"consumableName": "wx--2TPTdEuGAqLCjs9Qag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_Fangs]",
|
||||||
|
"consumableName": "XOZcxx-Klkyhe-MDbTqiwA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_FryingPan]",
|
||||||
|
"consumableName": "uGVFydNSokCXFAmXu3aceQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_Grenade]",
|
||||||
|
"consumableName": "EAhk3ZZdXEmH5wRAXXT24Q"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_Ranger]",
|
||||||
|
"consumableName": "Av-wvjXvvkmNVSz7ZZnTiA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_Shield]",
|
||||||
|
"consumableName": "5AJin8T2iEG7BzOPOgx2HA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_SkullWings]",
|
||||||
|
"consumableName": "U38Qe6rhEk6mFvArHfYjng"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_StarPower]",
|
||||||
|
"consumableName": "yvqSbK2czkS2sUCRdrGaEw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[KOConsumable_TireTrack]",
|
||||||
|
"consumableName": "J1WqFNUWo0OBi4LGKPDHWw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Orange]",
|
||||||
|
"consumableName": "xr45B5QC4EyKXThtwZto_A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Blue]",
|
||||||
|
"consumableName": "JwJeh15cjkOc9WWaGRibDQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_DarkRed]",
|
||||||
|
"consumableName": "3R1bzI35fkChoFFRFbponQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Cyan]",
|
||||||
|
"consumableName": "x9ntSHto50GpNWPwYCLUEg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelTeal]",
|
||||||
|
"consumableName": "4L3JoaqkAUa1kz98nciPXw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Green]",
|
||||||
|
"consumableName": "YaVzcoefhk6zjbNDELLx8A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_DarkPurple]",
|
||||||
|
"consumableName": "iEtWDLmv7ES87J4NE_TGJQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelGreen]",
|
||||||
|
"consumableName": "jA3zzS31zEitBxtRCk7oew"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelCyan]",
|
||||||
|
"consumableName": "P7HixsdkskmzgFMdFJ-WdA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Teal]",
|
||||||
|
"consumableName": "5Ugg8k19n0WH8GPFCDWTCQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Red]",
|
||||||
|
"consumableName": "P1kCuPlRc0-NAg6JUlEH_g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelPurple]",
|
||||||
|
"consumableName": "Hh_O_RejGE-_PubyM8xHaw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelPink]",
|
||||||
|
"consumableName": "s2pxukIEt0uDeGVJmZayOw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Pink]",
|
||||||
|
"consumableName": "vXL0rrAk70SFxJlaJk0uqQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelBlue]",
|
||||||
|
"consumableName": "UAOH6ccEbEmehJg8XW_D8w"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Yellow]",
|
||||||
|
"consumableName": "o0hG5O_eaUO0R8vPyW-Hvg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_Purple]",
|
||||||
|
"consumableName": "xRtKWR-D40GUW43R0EFpNg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelOrange]",
|
||||||
|
"consumableName": "AQTYAh1it0GIGdgGO5sUHQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_DarkBlue]",
|
||||||
|
"consumableName": "8vlVslaWaUyFS-iDHhxW9g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelRed]",
|
||||||
|
"consumableName": "STFKahjHJ0SQJPfoDe4S1g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_DarkPink]",
|
||||||
|
"consumableName": "eJh_BQ5y4UWwiMJzBKcwMQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[HairDyePotionConsumable_PastelYellow]",
|
||||||
|
"consumableName": "inIuPzhhOEmz1RI8mZtSLg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "[CouponConsumable_20%Tokens]",
|
||||||
|
"consumableName": "50oCPkzd3EerBd7nYNdkCw"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
res/staticJoinInProgressLookup.json
Normal file
38
res/staticJoinInProgressLookup.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"f5fbd9c9-e853-4036-9d48-5f68e861af04": false,
|
||||||
|
"76d98498-60a1-430c-ab76-b54a29b7a163": false,
|
||||||
|
"cbad71af-0831-44d8-b8ef-69edafa841f6": true,
|
||||||
|
"4078dfed-24bb-4db7-863f-578ba48d726b": true,
|
||||||
|
"f6f7256c-e438-4299-b99e-d20bef8cf7e0": true,
|
||||||
|
"d9378c9f-80bc-46fb-ad1e-1bed8a674f55": true,
|
||||||
|
"3d474b26-26f7-45e9-9a36-9b02847d5e6f": true,
|
||||||
|
"d89f74fa-d51e-477a-a425-025a891dd499": false,
|
||||||
|
"e122fe98-e7db-49e8-a1b1-105424b6e1f0": true,
|
||||||
|
"a785267d-c579-42ea-be43-fec1992d1ca7": true,
|
||||||
|
"ff4c6427-7079-4f59-b22a-69b089420827": true,
|
||||||
|
"380d18b5-de9c-49f3-80f7-f4a95c1de161": true,
|
||||||
|
"58763055-2dfb-4814-80b8-16fac5c85709": true,
|
||||||
|
"65ddbb48-5a01-4e3e-972d-e5c7419e2bc3": true,
|
||||||
|
"91e16e35-f48f-4700-ab8a-a1b79e50e51b": false,
|
||||||
|
"acc06e66-c2d0-4361-b0cd-46246a4c455c": false,
|
||||||
|
"949fa41f-4347-45c0-b7ac-489129174045": false,
|
||||||
|
"7e01cfe0-820a-406f-b1b3-0a5bf575235c": false,
|
||||||
|
"6d5eea4b-f069-4ed0-9916-0e2f07df0d03": true,
|
||||||
|
"239e676c-f12f-489f-bf3a-d4c383d692c3": true,
|
||||||
|
"9d6456ce-6264-48b4-808d-2d96b3d91038": true,
|
||||||
|
"253fa009-6e65-4c90-91a1-7137a56a267f": false,
|
||||||
|
"b010171f-4875-4e89-baba-61e878cd41e1": false,
|
||||||
|
"a067557f-ca32-43e6-b6e5-daaec60b4f5a": true,
|
||||||
|
"9932f88f-3929-43a0-a012-a40b5128e346": true,
|
||||||
|
"a75f7547-79eb-47c6-8986-6767abcb4f92": true,
|
||||||
|
"0a864c86-5a71-4e18-8041-8124e4dc9d98": true,
|
||||||
|
"42699ed2-0c1b-4f3d-93a2-ce01dfce7a79": false,
|
||||||
|
"49cb8993-a956-43e2-86f4-1318f279b22a": false,
|
||||||
|
"ae929543-9a07-41d5-8ee9-dbbee8c36800": true,
|
||||||
|
"a95c349c-0f96-4c2d-a4c8-4969ffa8ea44": false,
|
||||||
|
"b7281665-a715-4051-826b-8e08e69c6172": true,
|
||||||
|
"3a636bd2-f896-424c-9225-c184522c0d87": false,
|
||||||
|
"882e9b96-7115-4b03-86f6-c0c9d8e22e00": true,
|
||||||
|
"cf61556d-68fd-4288-9ae5-7a512621e569": false,
|
||||||
|
"bf268f5f-b55b-41af-8628-32fa4b5d70b6": false
|
||||||
|
}
|
||||||
@@ -182,7 +182,7 @@ Server.Commands.addRootCommand(new Command({
|
|||||||
else return null;
|
else return null;
|
||||||
},
|
},
|
||||||
zod: z.tuple([
|
zod: z.tuple([
|
||||||
z.string().transform(Number)
|
z.coerce.number()
|
||||||
]),
|
]),
|
||||||
help: 'Get ping (in ms) to the server'
|
help: 'Get ping (in ms) to the server'
|
||||||
}));
|
}));
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { createHonoRoute } from "../../../util/import.ts";
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
import { authenticate, galvanicError, GalvanicErrors, RateLimiter, recNetError } from "../../../util/api.ts";
|
import { authenticate, galvanicError, GalvanicErrors, RateLimiter, recNetError, statusResponse } from "../../../util/api.ts";
|
||||||
import Server from "../../../server/server.ts";
|
import Server from "../../../server/server.ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
|
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
|
||||||
import { PlatformType } from "../../../server/platforms/types.ts";
|
import { PlatformType } from "../../../server/platforms/types.ts";
|
||||||
import Steam from "../../../util/steam/steam.ts";
|
import Steam from "../../../util/steam/steam.ts";
|
||||||
|
import { HTTPStatus } from "@oneday/http-status";
|
||||||
|
|
||||||
export const route = createHonoRoute('/account');
|
export const route = createHonoRoute('/account');
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ route.app.get('/bulk', typedZValidator('query', bulkAccountQuerySchema), async c
|
|||||||
|
|
||||||
const postCreateRateLimiter = new RateLimiter(60, 3);
|
const postCreateRateLimiter = new RateLimiter(60, 3);
|
||||||
const createAccountBodySchema = z.object({
|
const createAccountBodySchema = z.object({
|
||||||
platform: z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
platform: z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||||
platformId: z.string().min(14).max(20),
|
platformId: z.string().min(14).max(20),
|
||||||
deviceId: z.string().min(32).max(64)
|
deviceId: z.string().min(32).max(64)
|
||||||
});
|
});
|
||||||
@@ -71,9 +72,16 @@ route.app.post('/create', postCreateRateLimiter.middle(), typedZValidator('form'
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
route.app.use(authenticate);
|
route.app.get('/me', authenticate, c => {
|
||||||
|
|
||||||
route.app.get('/me', c => {
|
|
||||||
const profile = c.get('profile');
|
const profile = c.get('profile');
|
||||||
return c.json(profile.selfExport());
|
return c.json(profile.selfExport());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getAccountByIdParamSchema = z.object({
|
||||||
|
id: z.coerce.number().max(Math.pow(2, 31))
|
||||||
|
});
|
||||||
|
route.app.get('/:id', typedZValidator('param', getAccountByIdParamSchema), async c => {
|
||||||
|
const prof = await Server.Profiles.get(c.req.valid('param').id);
|
||||||
|
if (prof) return c.json(prof.export());
|
||||||
|
else return statusResponse(c, HTTPStatus.NotFound);
|
||||||
|
});
|
||||||
8
src/routes/api/routes/announcement.ts
Normal file
8
src/routes/api/routes/announcement.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import Server from "../../../server/server.ts";
|
||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute("/announcement");
|
||||||
|
|
||||||
|
route.app.get('/v1/get', c => {
|
||||||
|
return c.json(Server.getAnnouncements());
|
||||||
|
});
|
||||||
@@ -25,3 +25,11 @@ route.app.post('/v2/set', typedZValidator('json', profileAvatarSchema), async c
|
|||||||
await c.get('profile').Avatar.setAvatar(outfit);
|
await c.get('profile').Avatar.setAvatar(outfit);
|
||||||
return c.status(200);
|
return c.status(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
route.app.get('/v3/saved', c => {
|
||||||
|
return c.json([]); // stub
|
||||||
|
});
|
||||||
|
|
||||||
|
route.app.get('/v2/gifts', c => {
|
||||||
|
return c.json([]); // stub
|
||||||
|
});
|
||||||
19
src/routes/api/routes/challenge.ts
Normal file
19
src/routes/api/routes/challenge.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute('/challenge');
|
||||||
|
|
||||||
|
route.app.get('/v2/getCurrent', c => {
|
||||||
|
return c.json({
|
||||||
|
ChallengeMapId: 0,
|
||||||
|
CompletedRequired: false,
|
||||||
|
StartAt: new Date(new Date().getTime() - 604_800_000).toISOString(),
|
||||||
|
EndAt: new Date(new Date().getTime() + 999_999_999_999).toISOString(),
|
||||||
|
ServerTime: new Date().toISOString(),
|
||||||
|
Challenges: [],
|
||||||
|
Gift: {
|
||||||
|
GiftDropId: 0,
|
||||||
|
Xp: 0,
|
||||||
|
Level: 0
|
||||||
|
}
|
||||||
|
}) // stub
|
||||||
|
});
|
||||||
171
src/routes/api/routes/checklist.ts
Normal file
171
src/routes/api/routes/checklist.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { ObjectiveType } from "../../../server/objectives/base.ts";
|
||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute('/checklist');
|
||||||
|
|
||||||
|
route.app.get('/v1/current', c => {
|
||||||
|
return c.json([
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Order: 1,
|
||||||
|
Objective: ObjectiveType.CompleteAnyDaily,
|
||||||
|
Count: 1,
|
||||||
|
CreditAmount: -1
|
||||||
|
}
|
||||||
|
]); // stub
|
||||||
|
});
|
||||||
23
src/routes/api/routes/communityboard.ts
Normal file
23
src/routes/api/routes/communityboard.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute("/communityboard");
|
||||||
|
|
||||||
|
route.app.get('/v1/current', c => {
|
||||||
|
return c.json({
|
||||||
|
FeaturedPlayer: {
|
||||||
|
Id: 1,
|
||||||
|
TitleOverride: "",
|
||||||
|
UrlOverride: ""
|
||||||
|
},
|
||||||
|
FeaturedRoomGroup: {
|
||||||
|
Name: "",
|
||||||
|
FeaturedRooms: []
|
||||||
|
},
|
||||||
|
CurrentAnnouncement: {
|
||||||
|
Message: "Galvanic Corrosion",
|
||||||
|
MoreInfoUrl: ""
|
||||||
|
},
|
||||||
|
InstagramImages: [],
|
||||||
|
Videos: []
|
||||||
|
}); // stub
|
||||||
|
});
|
||||||
8
src/routes/api/routes/consumables.ts
Normal file
8
src/routes/api/routes/consumables.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import Server from "../../../server/server.ts";
|
||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute("/consumables");
|
||||||
|
|
||||||
|
route.app.get('/v1/getUnlocked', c => {
|
||||||
|
return c.json(Server.Consumables.getAllDev());
|
||||||
|
});
|
||||||
7
src/routes/api/routes/equipment.ts
Normal file
7
src/routes/api/routes/equipment.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute("/equipment");
|
||||||
|
|
||||||
|
route.app.get('/v2/getUnlocked', c => {
|
||||||
|
return c.json([]); // stub
|
||||||
|
});
|
||||||
7
src/routes/api/routes/images.ts
Normal file
7
src/routes/api/routes/images.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute('/images');
|
||||||
|
|
||||||
|
route.app.get('/v2/named', c => {
|
||||||
|
return c.json([]); // stub
|
||||||
|
});
|
||||||
10
src/routes/api/routes/objectives.ts
Normal file
10
src/routes/api/routes/objectives.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute('/objectives');
|
||||||
|
|
||||||
|
route.app.get('/v1/myprogress', c => {
|
||||||
|
return c.json({
|
||||||
|
Objectives: [],
|
||||||
|
ObjectiveGroups: []
|
||||||
|
}); // stub
|
||||||
|
});
|
||||||
10
src/routes/api/routes/playerevents.ts
Normal file
10
src/routes/api/routes/playerevents.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute('/playerevents');
|
||||||
|
|
||||||
|
route.app.get('/v1/all', c => {
|
||||||
|
return c.json({
|
||||||
|
Created: [],
|
||||||
|
Responses: []
|
||||||
|
}) // stub
|
||||||
|
});
|
||||||
@@ -14,14 +14,14 @@ const getProgParamSchema = z.object({
|
|||||||
id: z.coerce.number()
|
id: z.coerce.number()
|
||||||
});
|
});
|
||||||
route.app.get('/v1/progression/:id', authenticate, typedZValidator('param', getProgParamSchema), async c => {
|
route.app.get('/v1/progression/:id', authenticate, typedZValidator('param', getProgParamSchema), async c => {
|
||||||
return c.json(await c.get('profile').Reputation.export());
|
return c.json(await c.get('profile').Progression.get());
|
||||||
});
|
});
|
||||||
|
|
||||||
const getProgBulkBodySchema = z.object({
|
const getProgBulkBodySchema = z.object({
|
||||||
Ids: z.union([z.array(z.coerce.number()), z.coerce.number()])
|
Ids: z.union([z.array(z.coerce.number()), z.coerce.number()])
|
||||||
});
|
});
|
||||||
route.app.post('/v1/progression/bulk', authenticate, typedZValidator('form', getProgBulkBodySchema), async c => {
|
route.app.post('/v1/progression/bulk', authenticate, typedZValidator('json', getProgBulkBodySchema), async c => {
|
||||||
const ids = c.req.valid('form').Ids;
|
const ids = c.req.valid('json').Ids;
|
||||||
|
|
||||||
if (typeof ids == 'object') {
|
if (typeof ids == 'object') {
|
||||||
const profs = await Server.Profiles.getMany(...ids);
|
const profs = await Server.Profiles.getMany(...ids);
|
||||||
|
|||||||
7
src/routes/api/routes/playersubscriptions.ts
Normal file
7
src/routes/api/routes/playersubscriptions.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute("/playersubscriptions");
|
||||||
|
|
||||||
|
route.app.get('/v1/my', c => {
|
||||||
|
return c.json([]); // stub
|
||||||
|
});
|
||||||
@@ -8,7 +8,6 @@ route.app.get('/v2/get', c => {
|
|||||||
return c.json([]);
|
return c.json([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// deno-lint-ignore require-await
|
route.app.post('/v1/bulkignoreplatformusers', c => {
|
||||||
route.app.post('/v1/bulkignoreplatformusers', async c => {
|
|
||||||
return statusResponse(c, HTTPStatus.OK);
|
return statusResponse(c, HTTPStatus.OK);
|
||||||
});
|
});
|
||||||
@@ -2,6 +2,7 @@ import z from "zod";
|
|||||||
import { createHonoRoute } from "../../../util/import.ts";
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
import { typedZValidator } from "../../../util/validators.ts";
|
import { typedZValidator } from "../../../util/validators.ts";
|
||||||
import Server from "../../../server/server.ts";
|
import Server from "../../../server/server.ts";
|
||||||
|
import { authenticate } from "../../../util/api.ts";
|
||||||
|
|
||||||
export const route = createHonoRoute("/rooms");
|
export const route = createHonoRoute("/rooms");
|
||||||
|
|
||||||
@@ -24,3 +25,12 @@ route.app.get('/v2/name/:name', typedZValidator('param', getRoomByNameParamSchem
|
|||||||
if (room == null) return await nxt();
|
if (room == null) return await nxt();
|
||||||
else return c.json((await room.export()).Room);
|
else return c.json((await room.export()).Room);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
route.app.get('/v2/myrooms', authenticate, async c => {
|
||||||
|
const myrooms = c.get('profile').Rooms.getRooms().values().toArray();
|
||||||
|
const factories = await Server.Rooms.getMany(...myrooms);
|
||||||
|
const exs = await Promise.all(factories.map(factory => factory.export()));
|
||||||
|
const rooms = exs.map(ex => ex.Room);
|
||||||
|
|
||||||
|
return c.json(rooms);
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { createHonoRoute } from "../../../util/import.ts";
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
|
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
|
||||||
import { PlatformType } from "../../../server/platforms/types.ts";
|
import { PlatformType } from "../../../server/platforms/types.ts";
|
||||||
import Server from "../../../server/server.ts";
|
import Server from "../../../server/server.ts";
|
||||||
import { authenticate } from "../../../util/api.ts";
|
import { authenticate } from "../../../util/api.ts";
|
||||||
@@ -11,7 +11,7 @@ const log = new Logging("CachedLoginDebug");
|
|||||||
export const route = createHonoRoute("/cachedlogin");
|
export const route = createHonoRoute("/cachedlogin");
|
||||||
|
|
||||||
const cachedLoginFetchParamSchema = z.object({
|
const cachedLoginFetchParamSchema = z.object({
|
||||||
platformType: z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
platformType: z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||||
platformId: z.string().min(4)
|
platformId: z.string().min(4)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createHonoRoute } from "../../../util/import.ts";
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
|
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
|
||||||
import { DeviceClass, PlatformType, TokenFormat, TokenType } from "../../../server/platforms/types.ts";
|
import { DeviceClass, PlatformType, TokenFormat, TokenType } from "../../../server/platforms/types.ts";
|
||||||
import { steamAuthTicketSchema } from "../../../server/platforms/base.ts";
|
import { steamAuthTicketSchema } from "../../../server/platforms/base.ts";
|
||||||
import { gameVerString } from "../../api/routes/versioncheck.ts";
|
import { gameVerString } from "../../api/routes/versioncheck.ts";
|
||||||
@@ -16,13 +16,13 @@ export const route = createHonoRoute("/connect");
|
|||||||
|
|
||||||
const authBodyBaseSchema = z.object({
|
const authBodyBaseSchema = z.object({
|
||||||
client_id: z.literal("recroom"),
|
client_id: z.literal("recroom"),
|
||||||
platform: z.string().transform(Number).transform((arg, ctx) => { // we only support steam right now
|
platform: z.coerce.number().transform((arg, ctx) => { // we only support steam right now
|
||||||
if (arg !== PlatformType.Steam) ctx.addIssue("platform was not Steam");
|
if (arg !== PlatformType.Steam) ctx.addIssue("platform was not Steam");
|
||||||
else return PlatformType.Steam;
|
else return PlatformType.Steam;
|
||||||
}),
|
}),
|
||||||
platform_id: z.string().min(4),
|
platform_id: z.string().min(4),
|
||||||
device_id: z.string().min(4),
|
device_id: z.string().min(4),
|
||||||
device_class: z.string().transform(transformStringToEnum<DeviceClass>(DeviceClass)),
|
device_class: z.string().transform(transformCheckEnum<DeviceClass>(DeviceClass)),
|
||||||
time: z.coerce.date(),
|
time: z.coerce.date(),
|
||||||
ver: z.literal(gameVerString),
|
ver: z.literal(gameVerString),
|
||||||
asid: z.coerce.number(),
|
asid: z.coerce.number(),
|
||||||
@@ -39,7 +39,7 @@ const authBodyBaseSchema = z.object({
|
|||||||
|
|
||||||
const cachedLoginGrantSchema = authBodyBaseSchema.extend({
|
const cachedLoginGrantSchema = authBodyBaseSchema.extend({
|
||||||
grant_type: z.literal('cached_login'),
|
grant_type: z.literal('cached_login'),
|
||||||
account_id: z.string().transform(Number),
|
account_id: z.coerce.number(),
|
||||||
});
|
});
|
||||||
const refreshTokenGrantSchema = authBodyBaseSchema.extend({
|
const refreshTokenGrantSchema = authBodyBaseSchema.extend({
|
||||||
grant_type: z.literal('refresh_token'),
|
grant_type: z.literal('refresh_token'),
|
||||||
@@ -97,11 +97,12 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
|||||||
|
|
||||||
const profile = await Server.Profiles.get(token.sub);
|
const profile = await Server.Profiles.get(token.sub);
|
||||||
if (!profile) return error(TokenRequestError.AccessDenied);
|
if (!profile) return error(TokenRequestError.AccessDenied);
|
||||||
const accessToken = await Server.Platforms.getToken(profile.getId(), TokenType.Access);
|
const accessToken = await Server.Platforms.getToken(profile, TokenType.Access);
|
||||||
|
const refreshToken = await Server.Platforms.getToken(profile, TokenType.Refresh);
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
refresh_token: form.refresh_token,
|
refresh_token: refreshToken,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.w(`Authentication error (token req): ${(err as Error).stack}`);
|
log.w(`Authentication error (token req): ${(err as Error).stack}`);
|
||||||
@@ -113,8 +114,8 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
|||||||
const profile = await Server.Profiles.get(form.account_id);
|
const profile = await Server.Profiles.get(form.account_id);
|
||||||
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
|
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
|
||||||
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
|
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
|
||||||
const accessToken = await Server.Platforms.getToken(profile.getId(), TokenType.Access);
|
const accessToken = await Server.Platforms.getToken(profile, TokenType.Access);
|
||||||
const refreshToken = await Server.Platforms.getToken(profile.getId(), TokenType.Refresh);
|
const refreshToken = await Server.Platforms.getToken(profile, TokenType.Refresh);
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
|
|||||||
@@ -1,50 +1,7 @@
|
|||||||
import { createHonoRoute, routeImporter } from "../../util/import.ts";
|
import { createHonoRoute, routeImporter } from "../../util/import.ts";
|
||||||
import { Context, Next } from "@hono/hono";
|
|
||||||
import z from "zod";
|
|
||||||
import { HonoEnv } from "../../util/types.ts";
|
|
||||||
import { statusResponse } from "../../util/api.ts";
|
|
||||||
import { HTTPStatus } from "@oneday/http-status";
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
|
||||||
|
|
||||||
const log = new Logging("MatchRoute");
|
|
||||||
|
|
||||||
export const route = createHonoRoute('/match');
|
export const route = createHonoRoute('/match');
|
||||||
|
|
||||||
const loginLockBodySchema = z.object({
|
|
||||||
LoginLock: z.uuidv4()
|
|
||||||
});
|
|
||||||
export const loginLockMiddleware = async (c: Context<HonoEnv>, nxt: Next) => {
|
|
||||||
function unauthorized() {
|
|
||||||
return statusResponse(c, HTTPStatus.Unauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c.req.header("Content-Type") !== "application/x-www-form-urlencoded") return unauthorized();
|
|
||||||
try {
|
|
||||||
const form = await c.req.formData();
|
|
||||||
|
|
||||||
const body = await loginLockBodySchema.safeParseAsync(Object.fromEntries(form.entries()));
|
|
||||||
if (body.success) {
|
|
||||||
if (typeof c.get('profile') == 'undefined') {
|
|
||||||
log.w(`Profile was not set, cannot validate LoginLock. Was the request authorized?`);
|
|
||||||
return statusResponse(c, HTTPStatus.InternalServerError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = c.get('profile');
|
|
||||||
|
|
||||||
const loginLock = await profile.Matchmaking.getLoginLock();
|
|
||||||
if (!loginLock) await profile.Matchmaking.setLoginLock(body.data.LoginLock);
|
|
||||||
else if (body.data.LoginLock !== loginLock) {
|
|
||||||
log.w(`LoginLock did not match. The token for this profile could be compromised or the client is an unknown state.`);
|
|
||||||
return unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
return await nxt();
|
|
||||||
} else return unauthorized();
|
|
||||||
} catch {
|
|
||||||
return unauthorized();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await routeImporter(route.app, 'src/routes/match/', [
|
await routeImporter(route.app, 'src/routes/match/', [
|
||||||
'routes'
|
'routes'
|
||||||
]);
|
]);
|
||||||
@@ -1,13 +1,76 @@
|
|||||||
import { HTTPStatus } from "@oneday/http-status";
|
import { HTTPStatus } from "@oneday/http-status";
|
||||||
import { statusResponse } from "../../../util/api.ts";
|
import { authenticate, loginLockMiddleware, statusResponse } from "../../../util/api.ts";
|
||||||
import { createHonoRoute } from "../../../util/import.ts";
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
import Server from "../../../server/server.ts";
|
||||||
|
import z from "zod";
|
||||||
|
import { typedZValidator } from "../../../util/validators.ts";
|
||||||
|
import { type MatchmakingResponse } from "../../../server/matchmaking/base.ts";
|
||||||
|
import { roomNameSchema } from "../../../server/rooms/base.ts";
|
||||||
|
|
||||||
export const route = createHonoRoute("/goto");
|
export const route = createHonoRoute("/goto");
|
||||||
|
|
||||||
route.app.post('/room/:roomName', c => {
|
const gotoRoomBodySchema = z.object({
|
||||||
return statusResponse(c, HTTPStatus.NotImplemented);
|
CreatePrivateInstance: z.boolean().optional(),
|
||||||
|
ExpectedPlayerIds: z.array(z.int()).optional(),
|
||||||
|
BypassMovementModeRestriction: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
route.app.post('/room/:roomName/:subRoomName', c => {
|
const gotoRoomParamSchema = z.object({
|
||||||
return statusResponse(c, HTTPStatus.NotImplemented);
|
roomName: roomNameSchema
|
||||||
});
|
});
|
||||||
|
route.app.post('/room/:roomName',
|
||||||
|
authenticate,
|
||||||
|
loginLockMiddleware,
|
||||||
|
|
||||||
|
typedZValidator('json', gotoRoomBodySchema),
|
||||||
|
typedZValidator('param', gotoRoomParamSchema),
|
||||||
|
|
||||||
|
async c => {
|
||||||
|
const body = c.req.valid("json");
|
||||||
|
|
||||||
|
const res = await Server.Matchmaking.matchmake({
|
||||||
|
roomName: c.req.param('roomName'),
|
||||||
|
private: body.CreatePrivateInstance,
|
||||||
|
profile: c.get('profile')
|
||||||
|
});
|
||||||
|
if (!res) return statusResponse(c, HTTPStatus.InternalServerError, "Matchmaking failed");
|
||||||
|
|
||||||
|
const m: MatchmakingResponse = {
|
||||||
|
roomInstance: res.roomInstance ? res.roomInstance.export() : undefined,
|
||||||
|
errorCode: res.errorCode
|
||||||
|
}
|
||||||
|
return c.json(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
const gotoSubroomParamSchema = gotoRoomParamSchema.extend({
|
||||||
|
subRoomName: roomNameSchema
|
||||||
|
});
|
||||||
|
route.app.post('/room/:roomName/:subRoomName',
|
||||||
|
|
||||||
|
authenticate,
|
||||||
|
loginLockMiddleware,
|
||||||
|
|
||||||
|
typedZValidator('json', gotoRoomBodySchema),
|
||||||
|
typedZValidator('param', gotoSubroomParamSchema),
|
||||||
|
|
||||||
|
async c => {
|
||||||
|
const body = c.req.valid("json");
|
||||||
|
|
||||||
|
const res = await Server.Matchmaking.matchmake({
|
||||||
|
roomName: c.req.param('roomName'),
|
||||||
|
subRoomName: c.req.param('subRoomName'),
|
||||||
|
private: body.CreatePrivateInstance,
|
||||||
|
profile: c.get('profile')
|
||||||
|
});
|
||||||
|
if (!res) return statusResponse(c, HTTPStatus.InternalServerError, "Matchmaking failed");
|
||||||
|
|
||||||
|
const m: MatchmakingResponse = {
|
||||||
|
roomInstance: res.roomInstance ? res.roomInstance.export() : undefined,
|
||||||
|
errorCode: res.errorCode
|
||||||
|
}
|
||||||
|
return c.json(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
@@ -1,20 +1,70 @@
|
|||||||
import { authenticate, statusResponse } from "../../../util/api.ts";
|
import z from "zod";
|
||||||
|
import Server from "../../../server/server.ts";
|
||||||
|
import { authenticate, loginLockMiddleware, statusResponse } from "../../../util/api.ts";
|
||||||
import { createHonoRoute } from "../../../util/import.ts";
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
import { loginLockMiddleware } from "../root.ts";
|
|
||||||
import { HTTPStatus } from "@oneday/http-status";
|
import { HTTPStatus } from "@oneday/http-status";
|
||||||
|
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
|
||||||
|
import { PlayerStatusVisibility, VRMovementMode } from "../../../server/presence/base.ts";
|
||||||
|
|
||||||
export const route = createHonoRoute("/player");
|
export const route = createHonoRoute("/player");
|
||||||
|
|
||||||
route.app.use(authenticate);
|
const playerIdsQuerySchema = z.object({
|
||||||
|
id: z.union([z.coerce.number(), z.array(z.coerce.number())])
|
||||||
|
});
|
||||||
|
route.app.get('/', typedZValidator('query', playerIdsQuerySchema), async c => {
|
||||||
|
const id = c.req.valid('query').id;
|
||||||
|
|
||||||
route.app.post('/login', authenticate, loginLockMiddleware, async c => {
|
if (typeof id == 'object') {
|
||||||
|
const profs = await Server.Profiles.getMany(...id);
|
||||||
|
return c.json(profs.map(prof => Server.Presence.getPresence(prof).export()));
|
||||||
|
} else {
|
||||||
|
const prof = await Server.Profiles.get(id);
|
||||||
|
if (!prof) return c.json([]);
|
||||||
|
|
||||||
|
return c.json([Server.Presence.getPresence(prof).export()]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
route.app.post('/player/vrmovementmode', authenticate, loginLockMiddleware, async c => {
|
route.app.post('/login', authenticate, loginLockMiddleware, c => {
|
||||||
return statusResponse(c, HTTPStatus.OK); // stub
|
const pres = Server.Presence.getPresence(c.get('profile'));
|
||||||
|
pres.updateLastSeen();
|
||||||
|
|
||||||
|
return statusResponse(c, HTTPStatus.OK);
|
||||||
|
});
|
||||||
|
route.app.post('/logout', authenticate, loginLockMiddleware, c => {
|
||||||
|
const pres = Server.Presence.getPresence(c.get('profile'));
|
||||||
|
pres.updateLastSeen();
|
||||||
|
|
||||||
|
pres.setStatusVisibility(PlayerStatusVisibility.Offline);
|
||||||
|
|
||||||
|
c.get('profile').updateInstance(null);
|
||||||
|
|
||||||
|
Server.Instances.clearEmptyInstances();
|
||||||
|
|
||||||
|
return statusResponse(c, HTTPStatus.OK);
|
||||||
});
|
});
|
||||||
|
|
||||||
route.app.post('/player/statusvisibility', authenticate, loginLockMiddleware, async c => {
|
const vrMovementModeBodySchema = z.object({
|
||||||
return statusResponse(c, HTTPStatus.OK); // stub
|
vrMovementMode: z.coerce.number().transform(transformCheckEnum<VRMovementMode>(VRMovementMode))
|
||||||
|
});
|
||||||
|
route.app.put('/vrmovementmode', authenticate, typedZValidator('form', vrMovementModeBodySchema), c => {
|
||||||
|
const pres = Server.Presence.getPresence(c.get('profile'));
|
||||||
|
pres.updateLastSeen();
|
||||||
|
|
||||||
|
return statusResponse(c, HTTPStatus.OK);
|
||||||
|
});
|
||||||
|
const statusVisibilityBodySchema = z.object({
|
||||||
|
statusVisibility: z.coerce.number().transform(transformCheckEnum<PlayerStatusVisibility>(PlayerStatusVisibility))
|
||||||
|
});
|
||||||
|
route.app.put('/statusvisibility', authenticate, typedZValidator('form', statusVisibilityBodySchema), c => {
|
||||||
|
const pres = Server.Presence.getPresence(c.get('profile'));
|
||||||
|
pres.updateLastSeen();
|
||||||
|
|
||||||
|
return statusResponse(c, HTTPStatus.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
route.app.post('/heartbeat', authenticate, loginLockMiddleware, c => {
|
||||||
|
const pres = Server.Presence.getPresence(c.get('profile'));
|
||||||
|
|
||||||
|
return c.json(pres.export());
|
||||||
});
|
});
|
||||||
@@ -1,2 +1,20 @@
|
|||||||
|
import type Profile from "../profiles/profile.ts";
|
||||||
|
|
||||||
// deno-lint-ignore no-explicit-any
|
// deno-lint-ignore no-explicit-any
|
||||||
export type CommandExec = (...args: any[]) => unknown | Promise<unknown>;
|
export type CommandExec = (...args: any[]) => unknown | Promise<unknown>;
|
||||||
|
|
||||||
|
export enum CommandSenderType {
|
||||||
|
Console,
|
||||||
|
Profile
|
||||||
|
}
|
||||||
|
export interface CommandSenderBase {
|
||||||
|
type: CommandSenderType
|
||||||
|
}
|
||||||
|
export interface ConsoleSender extends CommandSenderBase {
|
||||||
|
type: CommandSenderType.Console
|
||||||
|
}
|
||||||
|
export interface ProfileSender extends CommandSenderBase {
|
||||||
|
type: CommandSenderType.Profile,
|
||||||
|
prof: Profile
|
||||||
|
}
|
||||||
|
export type CommandSender = ConsoleSender | ProfileSender;
|
||||||
@@ -34,6 +34,10 @@ export default class Command {
|
|||||||
return this.exec(...args);
|
return this.exec(...args);
|
||||||
else if (!root) return new Error('No execution target for this root');
|
else if (!root) return new Error('No execution target for this root');
|
||||||
|
|
||||||
|
if (root === 'help') return JSON.stringify(this.subCmds.values()
|
||||||
|
.map(cmd => cmd.getKey()).toArray()
|
||||||
|
.reduce((prev, accumulator) => prev.concat(accumulator), []));
|
||||||
|
|
||||||
const cmd = this.subCmds.find(cmd => cmd.getKey().includes(root));
|
const cmd = this.subCmds.find(cmd => cmd.getKey().includes(root));
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
const newArgs = args.slice(1);
|
const newArgs = args.slice(1);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ServerContentBase } from "../ContentBase.ts";
|
import { ServerContentBase } from "../ContentBase.ts";
|
||||||
|
import { CommandSender, CommandSenderType } from "./cmdtypes.ts";
|
||||||
import type Command from "./command.ts";
|
import type Command from "./command.ts";
|
||||||
|
|
||||||
export class CommandsBase extends ServerContentBase {
|
export class CommandsBase extends ServerContentBase {
|
||||||
@@ -18,12 +19,20 @@ export class CommandsBase extends ServerContentBase {
|
|||||||
this.#cmds.delete(cmd);
|
this.#cmds.delete(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispatch(...args: string[]): Promise<unknown> {
|
async dispatch(sender: CommandSender, ...args: string[]): Promise<unknown> {
|
||||||
|
if (sender.type == CommandSenderType.Profile)
|
||||||
|
if (await sender.prof.getRole() !== "developer") return new Error("Unauthorized");
|
||||||
|
|
||||||
const root = args[0];
|
const root = args[0];
|
||||||
if (typeof root !== 'string') return new Error("Root command must be of primitive type 'string'");
|
if (typeof root !== 'string') return new Error("Root command must be of primitive type 'string'");
|
||||||
else {
|
else {
|
||||||
|
if (root === "help") return JSON.stringify(this.#cmds.values()
|
||||||
|
.map(cmd => cmd.getKey()).toArray()
|
||||||
|
.reduce((prev, accumulator) => prev.concat(accumulator), []));
|
||||||
|
|
||||||
const cmd = this.#cmds.values().toArray().find(cmd => cmd.getKey().includes(root));
|
const cmd = this.#cmds.values().toArray().find(cmd => cmd.getKey().includes(root));
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
|
|
||||||
const val = await cmd.dispatch(...args.slice(1));
|
const val = await cmd.dispatch(...args.slice(1));
|
||||||
if (val == null) return "null";
|
if (val == null) return "null";
|
||||||
else if (typeof val == 'string') return `"${val}"`;
|
else if (typeof val == 'string') return `"${val}"`;
|
||||||
|
|||||||
29
src/server/consumables/base.ts
Normal file
29
src/server/consumables/base.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { ServerContentBase } from "../ContentBase.ts";
|
||||||
|
import { Consumable, ConsumableCollectionRuntimeConfig } from "./types.ts";
|
||||||
|
import { RootPath } from "../../util/path.ts";
|
||||||
|
import { PlatformMask } from "../platforms/types.ts";
|
||||||
|
|
||||||
|
export class ServerConsumablesBase extends ServerContentBase {
|
||||||
|
|
||||||
|
#consumableConfig: ConsumableCollectionRuntimeConfig | null = null;
|
||||||
|
|
||||||
|
protected override start() {
|
||||||
|
this.#consumableConfig = JSON.parse(Deno.readTextFileSync(path.join(RootPath, '/res/consumables.json')));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllDev(): Consumable[] {
|
||||||
|
if (this.#consumableConfig == null) return [];
|
||||||
|
else return this.#consumableConfig.consumablePrefabData.map(cons => ({
|
||||||
|
Id: this.#consumableConfig!.consumablePrefabData.indexOf(cons),
|
||||||
|
ConsumableItemDesc: cons.consumableName,
|
||||||
|
PlatformMask: this.server.generateMask(PlatformMask.All),
|
||||||
|
CreatedAt: new Date().toISOString(),
|
||||||
|
Count: 99999999,
|
||||||
|
InitialCount: 99999999,
|
||||||
|
UnlockedLevel: 1,
|
||||||
|
IsActive: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
src/server/consumables/types.ts
Normal file
19
src/server/consumables/types.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export interface ConsumablePrefab {
|
||||||
|
fileName: string,
|
||||||
|
consumableName: string
|
||||||
|
}
|
||||||
|
export interface ConsumableCollectionRuntimeConfig {
|
||||||
|
consumablePrefabData: ConsumablePrefab[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Consumable {
|
||||||
|
Id: number,
|
||||||
|
ConsumableItemDesc: string,
|
||||||
|
PlatformMask: number,
|
||||||
|
CreatedAt: string,
|
||||||
|
Count: number,
|
||||||
|
InitialCount: number,
|
||||||
|
UnlockedLevel: number,
|
||||||
|
IsActive: boolean,
|
||||||
|
ActiveDurationMinutes?: number
|
||||||
|
}
|
||||||
@@ -1,45 +1,104 @@
|
|||||||
import { CloudRegionCode } from "../../util/photon.ts";
|
import { CloudRegionCode } from "../../util/photon.ts";
|
||||||
import type Profile from "../profiles/profile.ts";
|
import type Profile from "../profiles/profile.ts";
|
||||||
|
import { RoomFactory } from "../rooms/internal/RoomFactory.ts";
|
||||||
|
import { SubroomFactory } from "../rooms/internal/SubroomFactory.ts";
|
||||||
|
import { type ServerBase } from "../server.ts";
|
||||||
import { RoomInstance, RoomLocation } from "./types.ts";
|
import { RoomInstance, RoomLocation } from "./types.ts";
|
||||||
|
|
||||||
export interface InstanceCreationOptions {
|
export interface InstanceCreationOptions {
|
||||||
roomId: number,
|
room: RoomFactory,
|
||||||
subRoomId: number,
|
subroom: SubroomFactory,
|
||||||
name: string,
|
name: string,
|
||||||
maxCapacity: number,
|
private?: boolean,
|
||||||
private?: boolean
|
eventId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Instance {
|
export class Instance {
|
||||||
|
|
||||||
#createdAt = new Date();
|
#createdAt = new Date();
|
||||||
|
|
||||||
#players: Set<Profile> = new Set();
|
#players: Set<Profile> = new Set();
|
||||||
|
#server: ServerBase;
|
||||||
|
#init: boolean = true;
|
||||||
|
|
||||||
#instanceId: number;
|
#instanceId: number;
|
||||||
#roomId: number;
|
#roomId: number = -1;
|
||||||
#subRoomId: number;
|
#subRoomId: number = -1;
|
||||||
#location: RoomLocation;
|
#location: RoomLocation = RoomLocation.MakerRoom;
|
||||||
#name: string;
|
#name: string = "Uninitialized Instance";
|
||||||
#maxCapacity: number;
|
#maxCapacity: number = 8;
|
||||||
#isFull: boolean = false;
|
#isFull: boolean = false;
|
||||||
#isPrivate: boolean;
|
#isPrivate: boolean = false;
|
||||||
#isInProgress: boolean = false;
|
#isInProgress: boolean = false;
|
||||||
#photonRegionId: string = CloudRegionCode.us;
|
#photonRegionId: string = CloudRegionCode.us;
|
||||||
#photonRoomId: string;
|
#photonRoomId: string = "uninit";
|
||||||
#dataBlob?: string;
|
#dataBlob?: string;
|
||||||
#eventId?: number
|
#eventId?: number;
|
||||||
|
|
||||||
constructor(id: number, location: RoomLocation, options: InstanceCreationOptions) {
|
get instanceId() { return this.#instanceId }
|
||||||
|
|
||||||
|
get roomId() { return this.#roomId }
|
||||||
|
set roomId(data) { this.#roomId = data }
|
||||||
|
|
||||||
|
get subRoomId() { return this.#subRoomId }
|
||||||
|
set subRoomId(data) { this.#subRoomId = data }
|
||||||
|
|
||||||
|
get location() { return this.#location }
|
||||||
|
set location(data) { this.#location = data }
|
||||||
|
|
||||||
|
get name() { return this.#name }
|
||||||
|
set name(data) { this.#name = data }
|
||||||
|
|
||||||
|
get maxCapacity() { return this.#maxCapacity }
|
||||||
|
set maxCapacity(data) { this.#maxCapacity = data }
|
||||||
|
|
||||||
|
get isFull() { return this.#isFull }
|
||||||
|
set isFull(data) { this.#isFull = data }
|
||||||
|
|
||||||
|
get isPrivate() { return this.#isPrivate }
|
||||||
|
set isPrivate(data) { this.#isPrivate = data }
|
||||||
|
|
||||||
|
get isInProgress() { return this.#isInProgress }
|
||||||
|
set isInProgress(data) { this.#isInProgress = data }
|
||||||
|
|
||||||
|
get photonRegionId() { return this.#photonRegionId }
|
||||||
|
set photonRegionId(data) { this.#photonRegionId = data }
|
||||||
|
|
||||||
|
get photonRoomId() { return this.#photonRoomId }
|
||||||
|
set photonRoomId(data) { this.#photonRoomId = data }
|
||||||
|
|
||||||
|
get dataBlob() { return this.#dataBlob }
|
||||||
|
set dataBlob(data) { this.#dataBlob = data }
|
||||||
|
|
||||||
|
get eventId() { return this.#eventId }
|
||||||
|
set eventId(data) { this.#eventId = data }
|
||||||
|
|
||||||
|
supportsJoinInProgress: boolean;
|
||||||
|
|
||||||
|
constructor(server: ServerBase, id: number, options: InstanceCreationOptions) {
|
||||||
this.#instanceId = id;
|
this.#instanceId = id;
|
||||||
this.#location = location;
|
this.location = options.subroom.RoomSceneLocationId;
|
||||||
|
this.#server = server;
|
||||||
|
|
||||||
this.#roomId = options.roomId;
|
this.roomId = options.room.getRoomId();
|
||||||
this.#subRoomId = options.subRoomId;
|
this.subRoomId = options.subroom.RoomSceneId;
|
||||||
this.#isPrivate = typeof options.private == 'boolean' ? options.private : false;
|
this.isPrivate = typeof options.private == 'boolean' ? options.private : false;
|
||||||
this.#name = options.name;
|
this.name = options.name;
|
||||||
this.#maxCapacity = options.maxCapacity;
|
this.maxCapacity = options.subroom.MaxPlayers;
|
||||||
this.#photonRoomId = `GCR-${this.#instanceId}`;
|
this.photonRoomId = `GCR-${this.instanceId}`;
|
||||||
|
|
||||||
|
this.supportsJoinInProgress = server.Rooms.sceneSupportsJoinInProgress(options.room, options.subroom);
|
||||||
|
|
||||||
|
this.#init = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be heavy, so promise is used
|
||||||
|
*/
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async #multiPresenceUpdate() {
|
||||||
|
if (this.#init) return;
|
||||||
|
for (const prof of this.#players)
|
||||||
|
prof.getSocketHandler()?.sendNotification("PresenceUpdate", this.#server.Presence.getPresence(prof).export());
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayers() {
|
getPlayers() {
|
||||||
@@ -61,21 +120,26 @@ export class Instance {
|
|||||||
|
|
||||||
export() {
|
export() {
|
||||||
const inst: RoomInstance = {
|
const inst: RoomInstance = {
|
||||||
roomInstanceId: this.#instanceId,
|
roomInstanceId: this.instanceId,
|
||||||
roomId: this.#roomId,
|
roomId: this.roomId,
|
||||||
subRoomId: this.#subRoomId,
|
subRoomId: this.subRoomId,
|
||||||
location: this.#location,
|
location: this.location,
|
||||||
name: this.#name,
|
name: this.name,
|
||||||
maxCapacity: this.#maxCapacity,
|
maxCapacity: this.maxCapacity,
|
||||||
isFull: this.#isFull,
|
isFull: this.isFull,
|
||||||
isPrivate: this.#isPrivate,
|
isPrivate: this.isPrivate,
|
||||||
isInProgress: this.#isInProgress,
|
isInProgress: this.isInProgress,
|
||||||
photonRegionId: this.#photonRegionId,
|
photonRegionId: this.photonRegionId,
|
||||||
photonRoomId: this.#photonRoomId,
|
photonRoomId: this.photonRoomId,
|
||||||
dataBlob: this.#dataBlob,
|
dataBlob: this.dataBlob,
|
||||||
eventId: this.#eventId
|
eventId: this.eventId
|
||||||
};
|
};
|
||||||
return inst;
|
return inst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current player count (instance size)
|
||||||
|
*/
|
||||||
|
get size() { return this.getPlayers().length }
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { ServerContentBase } from "../ContentBase.ts";
|
import { ServerContentBase } from "../ContentBase.ts";
|
||||||
import { type Instance } from "./Instance.ts";
|
import { Instance, InstanceCreationOptions } from "./Instance.ts";
|
||||||
|
import Command from "../commands/command.ts";
|
||||||
|
import z from "zod";
|
||||||
|
import { PushNotificationId } from "../socket/signalr/types.ts";
|
||||||
|
|
||||||
const log = new Logging("Instances");
|
const log = new Logging("Instances");
|
||||||
|
|
||||||
@@ -8,40 +11,80 @@ export class InstanceManager extends ServerContentBase {
|
|||||||
|
|
||||||
#instances: Set<Instance> = new Set();
|
#instances: Set<Instance> = new Set();
|
||||||
|
|
||||||
clearEmptyInstances() {
|
protected override start() {
|
||||||
|
this.server.Commands.addRootCommand(new Command({
|
||||||
|
key: ["inst", "i", "instance", "instances"],
|
||||||
|
subcommands: [
|
||||||
|
new Command({
|
||||||
|
key: ["getall", "all", "fetchall", "list"],
|
||||||
|
exec: () => {
|
||||||
|
return this.#instances.values().toArray().map(inst => inst.export());
|
||||||
|
},
|
||||||
|
zod: z.tuple([]),
|
||||||
|
help: "Get all instances"
|
||||||
|
}),
|
||||||
|
new Command({
|
||||||
|
key: ["kicklive", "modkick", "quitgame"],
|
||||||
|
exec: (id: number) => {
|
||||||
|
const inst = this.server.Instances.getInstance(id);
|
||||||
|
if (!inst) return false;
|
||||||
|
|
||||||
|
inst.getPlayers().forEach(prof => prof.getSocketHandler()?.sendNotification(PushNotificationId.ModerationKick));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
zod: z.tuple([z.coerce.number()]),
|
||||||
|
help: "Send ModerationKick to all players in an instance. Returns true if successful."
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be heavy if instance count is high.
|
||||||
|
*/
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async clearEmptyInstances() {
|
||||||
log.i(`Starting instance purge\n Before: ${
|
log.i(`Starting instance purge\n Before: ${
|
||||||
this.#instances.size
|
this.#instances.size
|
||||||
} instances, ${
|
} instances, ${
|
||||||
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
||||||
} players`);
|
} players`);
|
||||||
|
|
||||||
return new Promise(() => {
|
for (const inst of this.#instances)
|
||||||
for (const inst of this.#instances) {
|
|
||||||
if (inst.getPlayers().length === 0) this.deleteInstance(inst);
|
if (inst.getPlayers().length === 0) this.deleteInstance(inst);
|
||||||
}
|
|
||||||
|
|
||||||
log.i(`Instance purge complete\n After: ${
|
log.i(`Instance purge complete\n After: ${
|
||||||
this.#instances.size
|
this.#instances.size
|
||||||
} instances, ${
|
} instances, ${
|
||||||
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
||||||
} players`);
|
} players`);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllInstances() {
|
getAllInstances() {
|
||||||
|
return this.#instances;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerInstance(inst: Instance) {
|
#generateAvailableId() {
|
||||||
|
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||||
|
while (this.#instances.values().find(inst => inst.instanceId === id)) id = this.#generateAvailableId();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
#registerInstance(inst: Instance) {
|
||||||
|
if (!this.#instances.has(inst)) this.#instances.add(inst);
|
||||||
|
}
|
||||||
|
createInstance(options: InstanceCreationOptions) {
|
||||||
|
const inst = new Instance(this.server, this.#generateAvailableId(), options);
|
||||||
|
this.#instances.add(inst);
|
||||||
|
return inst;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInstance(id: number) {
|
getInstance(id: number) {
|
||||||
|
return this.#instances.values().find(inst => inst.instanceId === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteInstance(inst: Instance) {
|
deleteInstance(inst: Instance) {
|
||||||
|
this.#instances.delete(inst);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { type Instance } from "./Instance.ts";
|
||||||
|
|
||||||
export enum RoomLocation {
|
export enum RoomLocation {
|
||||||
Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04",
|
Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04",
|
||||||
DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
|
DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
|
||||||
@@ -53,3 +55,7 @@ export interface RoomInstance {
|
|||||||
dataBlob?: string;
|
dataBlob?: string;
|
||||||
eventId?: number;
|
eventId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InstanceUpdatedEvent {
|
||||||
|
instance: Instance
|
||||||
|
}
|
||||||
@@ -1,7 +1,121 @@
|
|||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { ServerContentBase } from "../ContentBase.ts";
|
import { ServerContentBase } from "../ContentBase.ts";
|
||||||
|
import { Instance } from "../instances/Instance.ts";
|
||||||
|
import { type RoomInstance } from "../instances/types.ts";
|
||||||
|
import type Profile from "../profiles/profile.ts";
|
||||||
|
import { RoomDataTypes } from "../rooms/internal/RoomDataTypes.ts";
|
||||||
|
import { type RoomFactory } from "../rooms/internal/RoomFactory.ts";
|
||||||
|
import { MatchmakingErrorCode } from "./types.ts";
|
||||||
|
|
||||||
|
const log = new Logging("Matchmaking");
|
||||||
|
|
||||||
|
export interface MatchmakingOptions {
|
||||||
|
roomName: string,
|
||||||
|
subRoomName?: string,
|
||||||
|
private?: boolean,
|
||||||
|
instanceId?: number,
|
||||||
|
profile: Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InternalMatchmakingResponse {
|
||||||
|
errorCode: MatchmakingErrorCode,
|
||||||
|
roomInstance?: Instance
|
||||||
|
}
|
||||||
|
export interface MatchmakingResponse {
|
||||||
|
errorCode: MatchmakingErrorCode,
|
||||||
|
roomInstance?: RoomInstance
|
||||||
|
}
|
||||||
|
|
||||||
export class ServerMatchmakingBase extends ServerContentBase {
|
export class ServerMatchmakingBase extends ServerContentBase {
|
||||||
|
|
||||||
|
async matchmake(options: MatchmakingOptions): Promise<InternalMatchmakingResponse | null> {
|
||||||
|
|
||||||
|
function getInstanceName() {
|
||||||
|
if (options.roomName === 'DormRoom')
|
||||||
|
return `@${options.profile.getUsername()}'s Dorm`;
|
||||||
|
else {
|
||||||
|
if (options.subRoomName || options.subRoomName !== "Home") return `^${options.roomName}.${options.subRoomName}`;
|
||||||
|
else return `^${options.roomName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.instanceId) {
|
||||||
|
|
||||||
|
const instance = this.server.Instances.getInstance(options.instanceId);
|
||||||
|
if (instance) {
|
||||||
|
|
||||||
|
if (instance.hasPlayer(options.profile)) return { errorCode: MatchmakingErrorCode.AlreadyInTargetInstance }
|
||||||
|
else {
|
||||||
|
if (instance.isFull) return { errorCode: MatchmakingErrorCode.InsufficientSpace }
|
||||||
|
else if (instance.isPrivate) return { errorCode: MatchmakingErrorCode.RoomInstanceIsPrivate }
|
||||||
|
|
||||||
|
options.profile.updateInstance(instance);
|
||||||
|
await this.server.Instances.clearEmptyInstances();
|
||||||
|
|
||||||
|
return { errorCode: MatchmakingErrorCode.Success, roomInstance: instance };
|
||||||
|
}
|
||||||
|
|
||||||
|
} else return { errorCode: MatchmakingErrorCode.NoSuchGame };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
let targetRoom: RoomFactory | null = null;
|
||||||
|
if (options.roomName == 'DormRoom') targetRoom = await this.server.Rooms.getPlayerDorm(options.profile);
|
||||||
|
else targetRoom = await this.server.Rooms.getByName(options.roomName);
|
||||||
|
|
||||||
|
if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom };
|
||||||
|
if (targetRoom.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.CreatorPlayerId !== options.profile.getId())
|
||||||
|
return { errorCode: MatchmakingErrorCode.RoomIsPrivate };
|
||||||
|
if (targetRoom.State !== RoomDataTypes.RoomState.Active) return { errorCode: MatchmakingErrorCode.RoomIsNotActive };
|
||||||
|
|
||||||
|
await this.server.Instances.clearEmptyInstances();
|
||||||
|
|
||||||
|
let allInstances = this.server.Instances.getAllInstances();
|
||||||
|
const subroom = (await targetRoom.getAllSubrooms()).values().find(factory => factory.Name == options.subRoomName);
|
||||||
|
|
||||||
|
// if a subroom was specified, filter instances that
|
||||||
|
if (subroom) allInstances = new Set(allInstances.values().filter(inst => inst.subRoomId == subroom.RoomSceneId));
|
||||||
|
|
||||||
|
// filter out instances that are in progress and do not support joininprogress
|
||||||
|
allInstances = new Set(allInstances.values().filter(inst => !(inst.isInProgress && !inst.supportsJoinInProgress)));
|
||||||
|
|
||||||
|
// sort instances
|
||||||
|
allInstances = new Set(allInstances.values().toArray().sort((a, b) => a.size + b.size));
|
||||||
|
|
||||||
|
const foundInstance = allInstances.values().toArray()[0];
|
||||||
|
if (!foundInstance) {
|
||||||
|
|
||||||
|
const matchmakeableSubrooms = (await targetRoom.getAllSubrooms()).values().filter(scene => scene.CanMatchmakeInto).toArray();
|
||||||
|
const index = Math.floor(Math.random() * matchmakeableSubrooms.length);
|
||||||
|
|
||||||
|
log.d(`Scene ${matchmakeableSubrooms[index].RoomSceneId} was chosen for matchmaking into new instance of room ${targetRoom.getRoomId()}`);
|
||||||
|
const newInst = this.server.Instances.createInstance({
|
||||||
|
room: targetRoom,
|
||||||
|
subroom: matchmakeableSubrooms[index],
|
||||||
|
private: options.private,
|
||||||
|
name: getInstanceName()
|
||||||
|
});
|
||||||
|
|
||||||
|
options.profile.updateInstance(newInst);
|
||||||
|
this.server.Instances.clearEmptyInstances();
|
||||||
|
|
||||||
|
return { errorCode: MatchmakingErrorCode.Success, roomInstance: newInst };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const currentInst = options.profile.getInstance();
|
||||||
|
if (currentInst?.instanceId === foundInstance.instanceId)
|
||||||
|
return { errorCode: MatchmakingErrorCode.AlreadyInBestInstance };
|
||||||
|
|
||||||
|
options.profile.updateInstance(foundInstance);
|
||||||
|
this.server.Instances.clearEmptyInstances();
|
||||||
|
|
||||||
|
return { errorCode: MatchmakingErrorCode.Success, roomInstance: foundInstance };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
enum MatchmakingErrorCode {
|
export enum MatchmakingErrorCode {
|
||||||
Success,
|
Success,
|
||||||
NoSuchGame,
|
NoSuchGame,
|
||||||
PlayerNotOnline,
|
PlayerNotOnline,
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import Command from "../commands/command.ts";
|
import Command from "../commands/command.ts";
|
||||||
import { ServerContentBase } from "../ContentBase.ts";
|
import { ServerContentBase } from "../ContentBase.ts";
|
||||||
import { transformCheckEnum, transformStringToEnum } from "../../util/validators.ts";
|
import { transformCheckEnum } from "../../util/validators.ts";
|
||||||
import { sign } from "@hono/hono/jwt";
|
import { sign } from "@hono/hono/jwt";
|
||||||
import { CachedLogin, DbCachedLogin, PlatformMask, PlatformType, TokenFormat, TokenType } from "./types.ts";
|
import { CachedLogin, DbCachedLogin, PlatformMask, PlatformType, TokenFormat, TokenType } from "./types.ts";
|
||||||
|
import type Profile from "../profiles/profile.ts";
|
||||||
|
import { getNetConfig } from "../../net.ts";
|
||||||
|
|
||||||
export const steamAuthTicketSchema = z.object({
|
export const steamAuthTicketSchema = z.object({
|
||||||
Ticket: z.string().min(256),
|
Ticket: z.string().min(256),
|
||||||
AppId: z.literal("471710")
|
AppId: z.literal("471710")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const netConfig = getNetConfig();
|
||||||
|
|
||||||
export class PlatformsManager extends ServerContentBase {
|
export class PlatformsManager extends ServerContentBase {
|
||||||
|
|
||||||
static platformsKey = "platforms";
|
static platformsKey = "platforms";
|
||||||
@@ -18,15 +22,18 @@ export class PlatformsManager extends ServerContentBase {
|
|||||||
return [PlatformsManager.platformsKey, ...keys.filter(val => typeof val == 'string')];
|
return [PlatformsManager.platformsKey, ...keys.filter(val => typeof val == 'string')];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToken(accountId: number, type: TokenType) {
|
async getToken(prof: Profile, type: TokenType) {
|
||||||
const secret = Deno.env.get('SECRET');
|
const secret = Deno.env.get('SECRET');
|
||||||
if (!secret) throw new Error("No SECRET in env. Did you forget to set it?");
|
if (!secret) throw new Error("No SECRET in env. Did you forget to set it?");
|
||||||
|
const exp = type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952;
|
||||||
|
|
||||||
const token: TokenFormat = {
|
const token: TokenFormat = {
|
||||||
typ: type,
|
typ: type,
|
||||||
sub: accountId,
|
sub: prof.getId(),
|
||||||
iss: "https://yarns.proxnet.dev/auth/",
|
role: await prof.getRole(),
|
||||||
exp: type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952
|
iss: `${netConfig.securePublicHost ? "https" : "http"}://${netConfig.publicHost}/auth`,
|
||||||
|
iat: Math.round(Date.now() / 1000) - 5,
|
||||||
|
exp
|
||||||
}
|
}
|
||||||
return await sign(JSON.parse(JSON.stringify(token)), secret);
|
return await sign(JSON.parse(JSON.stringify(token)), secret);
|
||||||
}
|
}
|
||||||
@@ -138,9 +145,9 @@ export class PlatformsManager extends ServerContentBase {
|
|||||||
return await this.addCachedLogin(type, platformId, accountId);
|
return await this.addCachedLogin(type, platformId, accountId);
|
||||||
},
|
},
|
||||||
zod: z.tuple([
|
zod: z.tuple([
|
||||||
z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.string().transform(Number)
|
z.coerce.number()
|
||||||
]),
|
]),
|
||||||
help: 'Add a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
|
help: 'Add a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
|
||||||
}),
|
}),
|
||||||
@@ -150,9 +157,9 @@ export class PlatformsManager extends ServerContentBase {
|
|||||||
return await this.deleteCachedLogin(type, platformId, accountId);
|
return await this.deleteCachedLogin(type, platformId, accountId);
|
||||||
},
|
},
|
||||||
zod: z.tuple([
|
zod: z.tuple([
|
||||||
z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.string().transform(Number)
|
z.coerce.number()
|
||||||
]),
|
]),
|
||||||
help: 'Remove a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
|
help: 'Remove a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ export interface TokenFormatBase {
|
|||||||
export interface TokenFormat extends TokenFormatBase {
|
export interface TokenFormat extends TokenFormatBase {
|
||||||
iss: string,
|
iss: string,
|
||||||
exp: number,
|
exp: number,
|
||||||
|
iat: number,
|
||||||
sub: number,
|
sub: number,
|
||||||
|
role: ProfileRole
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProfileRole {
|
export const ProfileRole = ["developer", "gameClient", "webClient"] as const;
|
||||||
Developer = 'developer',
|
export type ProfileRole = typeof ProfileRole[number];
|
||||||
Web = 'webClient',
|
|
||||||
Game = 'gameClient'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PlatformType {
|
export enum PlatformType {
|
||||||
All = -1,
|
All = -1,
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { DeviceClass } from "../platforms/types.ts";
|
|||||||
import Profile from "../profiles/profile.ts";
|
import Profile from "../profiles/profile.ts";
|
||||||
import { type ServerBase } from "../server.ts";
|
import { type ServerBase } from "../server.ts";
|
||||||
import { RoomInstance } from "../instances/types.ts";
|
import { RoomInstance } from "../instances/types.ts";
|
||||||
|
import Command from "../commands/command.ts";
|
||||||
|
import z from "zod";
|
||||||
|
import { PushNotificationId } from "../socket/signalr/types.ts";
|
||||||
|
|
||||||
export enum VRMovementMode {
|
export enum VRMovementMode {
|
||||||
TELEPORT,
|
TELEPORT,
|
||||||
@@ -33,7 +36,6 @@ export class Presence {
|
|||||||
|
|
||||||
#statusVisibility: PlayerStatusVisibility = PlayerStatusVisibility.Offline;
|
#statusVisibility: PlayerStatusVisibility = PlayerStatusVisibility.Offline;
|
||||||
#deviceClass: DeviceClass = DeviceClass.Unknown;
|
#deviceClass: DeviceClass = DeviceClass.Unknown;
|
||||||
#roomInstance: RoomInstance | null = null;
|
|
||||||
#vrMovementMove: VRMovementMode | undefined;
|
#vrMovementMove: VRMovementMode | undefined;
|
||||||
|
|
||||||
#lastExported: Date = new Date();
|
#lastExported: Date = new Date();
|
||||||
@@ -63,12 +65,16 @@ export class Presence {
|
|||||||
this.#statusVisibility = sv;
|
this.#statusVisibility = sv;
|
||||||
this.updateLastSeen();
|
this.updateLastSeen();
|
||||||
this.update();
|
this.update();
|
||||||
|
|
||||||
|
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
setVRMovementMode(mm: VRMovementMode) {
|
setVRMovementMode(mm: VRMovementMode) {
|
||||||
this.#vrMovementMove = mm;
|
this.#vrMovementMove = mm;
|
||||||
this.updateLastSeen();
|
this.updateLastSeen();
|
||||||
this.update();
|
this.update();
|
||||||
|
|
||||||
|
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
getLastExported() {
|
getLastExported() {
|
||||||
@@ -87,14 +93,14 @@ export class Presence {
|
|||||||
statusVisibility: this.#statusVisibility,
|
statusVisibility: this.#statusVisibility,
|
||||||
deviceClass: this.#deviceClass,
|
deviceClass: this.#deviceClass,
|
||||||
vrMovementMode: this.#vrMovementMove,
|
vrMovementMode: this.#vrMovementMove,
|
||||||
roomInstance: this.#roomInstance
|
roomInstance: this.#profile.getInstance()?.export() ?? null
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PresenceBase extends ServerContentBase {
|
export class ServerPresenceBase extends ServerContentBase {
|
||||||
|
|
||||||
#log = new Logging("Presence");
|
#log = new Logging("Presence");
|
||||||
|
|
||||||
@@ -119,9 +125,32 @@ export class PresenceBase extends ServerContentBase {
|
|||||||
|
|
||||||
override start() {
|
override start() {
|
||||||
this.#intervalId = setInterval(() => {
|
this.#intervalId = setInterval(() => {
|
||||||
|
if (this.#presenceMap.size === 0) return;
|
||||||
|
|
||||||
this.#log.i('Clearing dead presences');
|
this.#log.i('Clearing dead presences');
|
||||||
this.#deleteDeadPresences();
|
this.#deleteDeadPresences();
|
||||||
}, 300_000);
|
}, 300_000);
|
||||||
|
|
||||||
|
this.server.Commands.addRootCommand(new Command({
|
||||||
|
key: ["presence", "pres"],
|
||||||
|
subcommands: [
|
||||||
|
new Command({
|
||||||
|
key: ["quit", "quitgame"],
|
||||||
|
exec: async (pid: number) => {
|
||||||
|
const prof = await this.server.Profiles.get(pid);
|
||||||
|
if (!prof) return false;
|
||||||
|
|
||||||
|
const socket = prof.getSocketHandler();
|
||||||
|
if (!socket) return false;
|
||||||
|
|
||||||
|
socket.sendNotification(PushNotificationId.ModerationQuitGame);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
zod: z.tuple([z.coerce.number()]),
|
||||||
|
help: "Sends ModerationQuitGame to a player's socket if it is connected. Returns true if successful."
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
override destroy() {
|
override destroy() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Profile from "./profile.ts";
|
|||||||
import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||||
import Command from "./../commands/command.ts";
|
import Command from "./../commands/command.ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { PlatformMask, PlatformType, ProfileRole } from "../platforms/types.ts";
|
import { PlatformMask, PlatformType, ProfileRole, TokenType } from "../platforms/types.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
|
||||||
const profiles: Map<number, Profile> = new Map();
|
const profiles: Map<number, Profile> = new Map();
|
||||||
@@ -116,7 +116,7 @@ class ProfileManagerBase extends ServerContentBase {
|
|||||||
else return prof.export();
|
else return prof.export();
|
||||||
},
|
},
|
||||||
zod: z.tuple([
|
zod: z.tuple([
|
||||||
z.string().transform(Number)
|
z.coerce.number()
|
||||||
]),
|
]),
|
||||||
help: 'Fetch a profile: <id: number>'
|
help: 'Fetch a profile: <id: number>'
|
||||||
}),
|
}),
|
||||||
@@ -137,9 +137,9 @@ class ProfileManagerBase extends ServerContentBase {
|
|||||||
else return await profile.getRole();
|
else return await profile.getRole();
|
||||||
},
|
},
|
||||||
zod: z.tuple([
|
zod: z.tuple([
|
||||||
z.string().transform(Number)
|
z.coerce.number()
|
||||||
]),
|
]),
|
||||||
help: 'Set the profile role: <id: number>'
|
help: 'Get profile role: <id: number>'
|
||||||
}),
|
}),
|
||||||
new Command({
|
new Command({
|
||||||
key: ['setrole', 'sr'],
|
key: ['setrole', 'sr'],
|
||||||
@@ -150,9 +150,9 @@ class ProfileManagerBase extends ServerContentBase {
|
|||||||
},
|
},
|
||||||
zod: z.tuple([
|
zod: z.tuple([
|
||||||
z.coerce.number(),
|
z.coerce.number(),
|
||||||
z.string()
|
z.literal(ProfileRole)
|
||||||
]),
|
]),
|
||||||
help: 'Set the profile role: <id: number, role: "gameClient" | "webClient" | "developer">'
|
help: 'Set profile role: <id: number, role: "gameClient" | "webClient" | "developer">'
|
||||||
}),
|
}),
|
||||||
new Command({
|
new Command({
|
||||||
key: ['settings', 'setting'],
|
key: ['settings', 'setting'],
|
||||||
@@ -163,6 +163,16 @@ class ProfileManagerBase extends ServerContentBase {
|
|||||||
},
|
},
|
||||||
zod: z.tuple([z.coerce.number()]),
|
zod: z.tuple([z.coerce.number()]),
|
||||||
help: "Get player settings"
|
help: "Get player settings"
|
||||||
|
}),
|
||||||
|
new Command({
|
||||||
|
key: ["refreshtoken", "simutoken"],
|
||||||
|
exec: async (id: number) => {
|
||||||
|
const profile = await this.get(id);
|
||||||
|
if (profile) return await this.server.Platforms.getToken(profile, TokenType.Refresh);
|
||||||
|
else return null;
|
||||||
|
},
|
||||||
|
zod: z.tuple([z.coerce.number()]),
|
||||||
|
help: "Get a profile's refresh token / simulate generation of refresh token"
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ class Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructProfilePropertyKey(...keys: (string | undefined)[]) {
|
constructProfilePropertyKey(...keys: (string | undefined)[]) {
|
||||||
return [ ProfileManagerBase.profilesKey, this.#id, ...keys.filter(val => typeof val == 'string') ];
|
return [ProfileManagerBase.profilesKey, this.#id, ...keys.filter(val => typeof val == 'string')];
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsername() {
|
getUsername() {
|
||||||
return this.#selfAcc.username;
|
return this.#selfAcc.username;
|
||||||
}
|
}
|
||||||
async setUsername(username: string) {
|
async setUsername(username: string) {
|
||||||
this.#kv.getKv().delete([ ProfileManagerBase.profilesKey, this.#selfAcc.username ]);
|
this.#kv.getKv().delete([ProfileManagerBase.profilesKey, this.#selfAcc.username]);
|
||||||
this.#kv.getKv().set([ ProfileManagerBase.profilesKey, username ], this.getId());
|
this.#kv.getKv().set([ProfileManagerBase.profilesKey, username], this.getId());
|
||||||
|
|
||||||
this.#selfAcc.username = username;
|
this.#selfAcc.username = username;
|
||||||
await this.#saveSelfAcc();
|
await this.#saveSelfAcc();
|
||||||
@@ -81,7 +81,7 @@ class Profile {
|
|||||||
await this.#saveSelfAcc();
|
await this.#saveSelfAcc();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBio(){
|
async getBio() {
|
||||||
const key = this.constructProfilePropertyKey('bio');
|
const key = this.constructProfilePropertyKey('bio');
|
||||||
const val = await this.#kv.getKv().get<string>(key);
|
const val = await this.#kv.getKv().get<string>(key);
|
||||||
if (!val.value) return null;
|
if (!val.value) return null;
|
||||||
@@ -136,7 +136,17 @@ class Profile {
|
|||||||
getInstance() {
|
getInstance() {
|
||||||
return this.#instance;
|
return this.#instance;
|
||||||
}
|
}
|
||||||
setInstance(inst: Instance) {
|
updateInstance(inst: Instance | null) {
|
||||||
|
if (inst == null) {
|
||||||
|
if (this.#instance) this.#instance.removePlayer(this);
|
||||||
|
this.#instance = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#instance) this.#instance.removePlayer(this);
|
||||||
|
inst.addPlayer(this);
|
||||||
|
|
||||||
|
this.#server.emit('presence.update', { profile: this, presence: this.#server.Presence.getPresence(this) });
|
||||||
this.#instance = inst;
|
this.#instance = inst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ import { AGRoom, AGRoomLocation, AGRoomRuntimeConfig } from "./internal/ClientRo
|
|||||||
import Command from "../commands/command.ts";
|
import Command from "../commands/command.ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { RoomLocation } from "../instances/types.ts";
|
import { RoomLocation } from "../instances/types.ts";
|
||||||
|
import { RootPath } from "../../util/path.ts";
|
||||||
|
import path from "node:path";
|
||||||
|
import { SubroomFactory } from "./internal/SubroomFactory.ts";
|
||||||
|
|
||||||
|
export const roomNameSchema = z.string().min(4).max(128).regex(/^[A-Za-z0-9._-]+$/);
|
||||||
|
export const roomIdSchema = z.coerce.number().min(1).max(Math.pow(2, 31));
|
||||||
|
|
||||||
const roomIdSchema = z.coerce.number().min(1).max(Math.pow(2, 31));
|
|
||||||
export class ServerRoomsBase extends ServerContentBase {
|
export class ServerRoomsBase extends ServerContentBase {
|
||||||
|
|
||||||
#subroomKv = new KV('subrooms', true);
|
#subroomKv = new KV('subrooms', true);
|
||||||
@@ -21,19 +26,24 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
static roomNamesKey = "room_names";
|
static roomNamesKey = "room_names";
|
||||||
static playerDormsKey = "dorms";
|
static playerDormsKey = "dorms";
|
||||||
|
|
||||||
#agrooms: Set<number> = new Set();
|
#agroomIds: Set<number> = new Set();
|
||||||
#baserooms: Set<number> = new Set();
|
#baseroomIds: Set<number> = new Set();
|
||||||
|
#agRoomRuntimeConfig: AGRoomRuntimeConfig | null = null;
|
||||||
|
#joinInProgressLookup: Record<RoomLocation, boolean> | null = null;
|
||||||
|
|
||||||
override async start() {
|
override async start() {
|
||||||
await this.#subroomKv.init();
|
await this.#subroomKv.init();
|
||||||
this.#log.i('[sub]rooms database initialized');
|
this.#log.i('[sub]rooms database initialized');
|
||||||
|
|
||||||
const agrooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.agRoomIdsKey]);
|
const agrooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.agRoomIdsKey]);
|
||||||
if (agrooms.value !== null) this.#agrooms = agrooms.value;
|
if (agrooms.value !== null) this.#agroomIds = agrooms.value;
|
||||||
this.#log.i(`${this.#agrooms.size} AG rooms exist`);
|
this.#log.i(`${this.#agroomIds.size} AG rooms exist`);
|
||||||
|
|
||||||
const baserooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.baseRoomIdsKey]);
|
const baserooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.baseRoomIdsKey]);
|
||||||
if (baserooms.value !== null) this.#baserooms = baserooms.value;
|
if (baserooms.value !== null) this.#baseroomIds = baserooms.value;
|
||||||
|
|
||||||
|
this.#agRoomRuntimeConfig = JSON.parse(Deno.readTextFileSync(path.join(RootPath, "/res/rooms.json")));
|
||||||
|
this.#joinInProgressLookup = JSON.parse(Deno.readTextFileSync(path.join(RootPath, "/res/staticJoinInProgressLookup.json")));
|
||||||
|
|
||||||
this.server.Commands.addRootCommand(new Command({
|
this.server.Commands.addRootCommand(new Command({
|
||||||
key: ["rooms", "r", "room"],
|
key: ["rooms", "r", "room"],
|
||||||
@@ -82,10 +92,17 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
async #writeAgRooms() {
|
async #writeAgRooms() {
|
||||||
await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agrooms);
|
await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agroomIds);
|
||||||
}
|
}
|
||||||
async #writeBaseRooms() {
|
async #writeBaseRooms() {
|
||||||
await this.kv.getKv().set([ServerRoomsBase.baseRoomIdsKey], this.#baserooms);
|
await this.kv.getKv().set([ServerRoomsBase.baseRoomIdsKey], this.#baseroomIds);
|
||||||
|
}
|
||||||
|
getAgRoomIds() {
|
||||||
|
return this.#agroomIds;
|
||||||
|
}
|
||||||
|
getAgRoomRuntimeConfig() {
|
||||||
|
if (!this.#agRoomRuntimeConfig) throw new Error("Config has not yet been initialized");
|
||||||
|
return this.#agRoomRuntimeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initBuiltinRooms(rooms: AGRoom[], locations: AGRoomLocation[]) {
|
async initBuiltinRooms(rooms: AGRoom[], locations: AGRoomLocation[]) {
|
||||||
@@ -98,7 +115,10 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
"ARRoom",
|
"ARRoom",
|
||||||
"Registration",
|
"Registration",
|
||||||
"DormRoom"
|
"DormRoom"
|
||||||
].includes(room.Name)) return;
|
].includes(room.Name)) {
|
||||||
|
this.#log.w(`Room '${room.Name}' is not eligible for builtin room generation`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const roomFactory = await this.write();
|
const roomFactory = await this.write();
|
||||||
if (roomFactory == null) {
|
if (roomFactory == null) {
|
||||||
@@ -118,7 +138,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
roomFactory.Description = room.Description;
|
roomFactory.Description = room.Description;
|
||||||
roomFactory.IsAGRoom = true;
|
roomFactory.IsAGRoom = true;
|
||||||
roomFactory.CloningAllowed = room.CloningAllowed;
|
roomFactory.CloningAllowed = room.CloningAllowed;
|
||||||
roomFactory.ImageName = `${room.Name}.png`
|
roomFactory.ImageName = `${room.Name}.png`;
|
||||||
|
|
||||||
const supportPromises: Promise<unknown>[] = [];
|
const supportPromises: Promise<unknown>[] = [];
|
||||||
roomFactory.removeAllHardwareSupport();
|
roomFactory.removeAllHardwareSupport();
|
||||||
@@ -134,7 +154,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
|
|
||||||
subroomFactory.RoomId = roomFactory.getRoomId();
|
subroomFactory.RoomId = roomFactory.getRoomId();
|
||||||
subroomFactory.Name = scene.Name;
|
subroomFactory.Name = scene.Name;
|
||||||
subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId;
|
subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId as RoomLocation;
|
||||||
subroomFactory.IsSandbox = scene.IsSandbox;
|
subroomFactory.IsSandbox = scene.IsSandbox;
|
||||||
subroomFactory.CanMatchmakeInto = scene.CanMatchmakeInto;
|
subroomFactory.CanMatchmakeInto = scene.CanMatchmakeInto;
|
||||||
subroomFactory.MaxPlayers = scene.MaxPlayers;
|
subroomFactory.MaxPlayers = scene.MaxPlayers;
|
||||||
@@ -146,15 +166,33 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
|
|
||||||
await Promise.all(supportPromises);
|
await Promise.all(supportPromises);
|
||||||
|
|
||||||
this.#agrooms.add(roomFactory.getRoomId());
|
this.#agroomIds.add(roomFactory.getRoomId());
|
||||||
await roomFactory.write();
|
await roomFactory.write();
|
||||||
|
|
||||||
if (room.CloningAllowed) this.#baserooms.add(roomFactory.getRoomId());
|
if (room.CloningAllowed) this.#baseroomIds.add(roomFactory.getRoomId());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await this.#writeAgRooms();
|
await this.#writeAgRooms();
|
||||||
await this.#writeBaseRooms();
|
await this.#writeBaseRooms();
|
||||||
this.#log.i(`${this.#agrooms.size} AG rooms added: [${this.#agrooms.values().toArray().join(',')}]`);
|
this.#log.i(`${this.#agroomIds.size} AG rooms added: [${this.#agroomIds.values().toArray().join(',')}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneSupportsJoinInProgress(roomFactory: RoomFactory, subroomFactory: SubroomFactory) {
|
||||||
|
const agRoomRuntimeConfig = this.server.Rooms.getAgRoomRuntimeConfig();
|
||||||
|
|
||||||
|
const builtinScene = agRoomRuntimeConfig.Rooms.find(room => room.Name === roomFactory.Name)
|
||||||
|
?.Scenes.find(scene => scene.RoomSceneLocationId === subroomFactory.RoomSceneLocationId);
|
||||||
|
if (builtinScene) return builtinScene.SupportsJoinInProgress;
|
||||||
|
else {
|
||||||
|
if (!this.#joinInProgressLookup) throw new Error("JoinInProgress lookup table is not yet initialized");
|
||||||
|
const lookup = this.#joinInProgressLookup[subroomFactory.RoomSceneLocationId];
|
||||||
|
if (lookup) return lookup;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMany(...ids: number[]) {
|
||||||
|
return (await Promise.all(ids.map(id => this.get(id)))).filter(val => val !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableRoomId() {
|
async getAvailableRoomId() {
|
||||||
@@ -163,6 +201,10 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByRoomSceneId(id: number) {
|
||||||
|
return await new SubroomFactory(this.server, this.#subroomKv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
||||||
|
}
|
||||||
|
|
||||||
getKv() {
|
getKv() {
|
||||||
return this.kv;
|
return this.kv;
|
||||||
}
|
}
|
||||||
@@ -176,7 +218,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
async getPlayerDorm(profile: Profile) {
|
async getPlayerDorm(profile: Profile) {
|
||||||
const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]);
|
const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]);
|
||||||
if (id.value == null) {
|
if (id.value == null) {
|
||||||
const roomFactory = await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
const roomFactory = await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
||||||
if (!roomFactory) return null;
|
if (!roomFactory) return null;
|
||||||
roomFactory.setRoomProperties({
|
roomFactory.setRoomProperties({
|
||||||
Name: `DormRoom`,
|
Name: `DormRoom`,
|
||||||
@@ -216,7 +258,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
|
|
||||||
return roomFactory;
|
return roomFactory;
|
||||||
}
|
}
|
||||||
else return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id.value });
|
else return await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Fetch, id: id.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByName(name: string) {
|
async getByName(name: string) {
|
||||||
@@ -226,11 +268,11 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(id: number) {
|
async get(id: number) {
|
||||||
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id });
|
return await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Fetch, id: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async write(mode?: WriteMode) {
|
async write(mode?: WriteMode) {
|
||||||
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
return await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { RoomLocation } from "../../instances/types.ts";
|
||||||
|
|
||||||
export enum WriteMode {
|
export enum WriteMode {
|
||||||
Overwrite = "overwrite",
|
Overwrite = "overwrite",
|
||||||
WriteIfFree = "if_free"
|
WriteIfFree = "if_free"
|
||||||
@@ -34,7 +36,7 @@ export enum RoomAccessibility {
|
|||||||
export interface RoomScene {
|
export interface RoomScene {
|
||||||
RoomSceneId: number,
|
RoomSceneId: number,
|
||||||
RoomId: number,
|
RoomId: number,
|
||||||
RoomSceneLocationId: string,
|
RoomSceneLocationId: RoomLocation,
|
||||||
Name: string,
|
Name: string,
|
||||||
IsSandbox: boolean,
|
IsSandbox: boolean,
|
||||||
DataBlobName: string,
|
DataBlobName: string,
|
||||||
@@ -151,7 +153,7 @@ export interface DatabaseRoom {
|
|||||||
|
|
||||||
export interface SubroomProps {
|
export interface SubroomProps {
|
||||||
RoomId: number,
|
RoomId: number,
|
||||||
RoomSceneLocationId: string,
|
RoomSceneLocationId: RoomLocation,
|
||||||
Name: string,
|
Name: string,
|
||||||
IsSandbox: boolean,
|
IsSandbox: boolean,
|
||||||
MaxPlayers: number,
|
MaxPlayers: number,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class RoomFactory {
|
|||||||
|
|
||||||
#server: ServerBase;
|
#server: ServerBase;
|
||||||
#kv: KV;
|
#kv: KV;
|
||||||
|
#subroomKv: KV;
|
||||||
|
|
||||||
#roomId: number | undefined;
|
#roomId: number | undefined;
|
||||||
|
|
||||||
@@ -43,10 +44,11 @@ export class RoomFactory {
|
|||||||
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
|
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
|
||||||
#cannotWriteBeforeInitError = new Error("Cannot write before initialization");
|
#cannotWriteBeforeInitError = new Error("Cannot write before initialization");
|
||||||
|
|
||||||
constructor(server: ServerBase, kv: KV) {
|
constructor(server: ServerBase, kv: KV, subroomKv: KV) {
|
||||||
|
|
||||||
this.#server = server;
|
this.#server = server;
|
||||||
this.#kv = kv;
|
this.#kv = kv;
|
||||||
|
this.#subroomKv = subroomKv;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,9 +163,7 @@ export class RoomFactory {
|
|||||||
const autoTags = this.getTags();
|
const autoTags = this.getTags();
|
||||||
const galvTags = this.getGalvanicTags();
|
const galvTags = this.getGalvanicTags();
|
||||||
|
|
||||||
const subroomExports = (await Promise.all(
|
const subroomExports = (await Promise.all((await this.getAllSubrooms()).values())).map(factory => factory.export());
|
||||||
this.getSubrooms().values().map(subroom => this.getSubroom(subroom))
|
|
||||||
)).map(factory => factory.export());
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Room: {
|
Room: {
|
||||||
@@ -208,22 +208,27 @@ export class RoomFactory {
|
|||||||
return this.#roomId;
|
return this.#roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubrooms() {
|
getSubroomIds() {
|
||||||
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||||
return this.#subrooms;
|
return this.#subrooms;
|
||||||
}
|
}
|
||||||
async getSubroom(id: number) {
|
async getSubroom(id: number) {
|
||||||
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||||
if (!this.#subrooms.has(id)) throw new Error("Subroom not available to this room");
|
if (!this.#subrooms.has(id)) throw new Error("Subroom not available to this room");
|
||||||
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
return await new SubroomFactory(this.#server, this.#subroomKv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
||||||
}
|
}
|
||||||
getAvailableSubroomId() {
|
async getAllSubrooms() {
|
||||||
|
return new Set(await Promise.all(
|
||||||
|
this.getSubroomIds().values().map(subroom => this.getSubroom(subroom))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
async getAvailableSubroomId() {
|
||||||
let id = Math.round(Math.random() * Math.pow(2, 31));
|
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||||
if (this.getSubrooms().has(id)) id = this.getAvailableSubroomId();
|
if ((await this.#subroomKv.getKv().get<unknown>([id])).value) id = await this.getAvailableSubroomId();
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
async newSubroom(mode?: WriteMode) {
|
async newSubroom(mode?: WriteMode) {
|
||||||
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: this.getAvailableSubroomId() });
|
return await new SubroomFactory(this.#server, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableSubroomId() });
|
||||||
}
|
}
|
||||||
addSubroom(id: number) {
|
addSubroom(id: number) {
|
||||||
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Logging from "@proxnet/undead-logging";
|
|||||||
import type KV from "../../persistence/kv.ts";
|
import type KV from "../../persistence/kv.ts";
|
||||||
import { type ServerBase } from "../../server.ts";
|
import { type ServerBase } from "../../server.ts";
|
||||||
import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, RoomSaveMap, WriteMode } from "./RoomDataTypes.ts";
|
import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, RoomSaveMap, WriteMode } from "./RoomDataTypes.ts";
|
||||||
|
import { RoomLocation } from "../../instances/types.ts";
|
||||||
|
|
||||||
export interface SubroomFactoryOptions {
|
export interface SubroomFactoryOptions {
|
||||||
mode: FactoryMode,
|
mode: FactoryMode,
|
||||||
@@ -44,7 +45,7 @@ export class SubroomFactory {
|
|||||||
|
|
||||||
this.#obj = options.mode == FactoryMode.Fetch ? data.value : {
|
this.#obj = options.mode == FactoryMode.Fetch ? data.value : {
|
||||||
RoomId: 0,
|
RoomId: 0,
|
||||||
RoomSceneLocationId: "",
|
RoomSceneLocationId: RoomLocation.MakerRoom,
|
||||||
Name: "Subroom data init failed, contact an admin!",
|
Name: "Subroom data init failed, contact an admin!",
|
||||||
IsSandbox: false,
|
IsSandbox: false,
|
||||||
MaxPlayers: 8,
|
MaxPlayers: 8,
|
||||||
@@ -92,7 +93,7 @@ export class SubroomFactory {
|
|||||||
|
|
||||||
get RoomSceneId() { if (!this.#subroomId) throw this.#cannotAccessBeforeInitError; else return this.#subroomId; }
|
get RoomSceneId() { if (!this.#subroomId) throw this.#cannotAccessBeforeInitError; else return this.#subroomId; }
|
||||||
|
|
||||||
get RoomSceneLocationId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.RoomSceneLocationId; }
|
get RoomSceneLocationId(): RoomLocation { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.RoomSceneLocationId; }
|
||||||
set RoomSceneLocationId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.RoomSceneLocationId = data }
|
set RoomSceneLocationId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.RoomSceneLocationId = data }
|
||||||
|
|
||||||
get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Name; }
|
get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Name; }
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { ServerUpdateEvent } from "../serverevents.ts";
|
|||||||
import { AvatarContentBase } from "./avatars/base.ts";
|
import { AvatarContentBase } from "./avatars/base.ts";
|
||||||
import { EventManager } from "./baseevent.ts";
|
import { EventManager } from "./baseevent.ts";
|
||||||
import { CommandsBase } from "./commands/commands.ts";
|
import { CommandsBase } from "./commands/commands.ts";
|
||||||
|
import { ServerConsumablesBase } from "./consumables/base.ts";
|
||||||
import { ServerContentManager } from "./content/base.ts";
|
import { ServerContentManager } from "./content/base.ts";
|
||||||
import GameConfigsBase from "./gameconfigs/base.ts";
|
import GameConfigsBase from "./gameconfigs/base.ts";
|
||||||
import { InstanceManager } from "./instances/base.ts";
|
import { InstanceManager } from "./instances/base.ts";
|
||||||
|
import { ServerMatchmakingBase } from "./matchmaking/base.ts";
|
||||||
import { Objective, ObjectiveType } from "./objectives/base.ts";
|
import { Objective, ObjectiveType } from "./objectives/base.ts";
|
||||||
import { PlatformsManager } from "./platforms/base.ts";
|
import { PlatformsManager } from "./platforms/base.ts";
|
||||||
|
import { ServerPresenceBase } from "./presence/base.ts";
|
||||||
import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.ts";
|
import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.ts";
|
||||||
import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts";
|
import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts";
|
||||||
import { ProfileUpdatedSettingEvent } from "./profiles/events/ProfileUpdatedSetting.ts";
|
import { ProfileUpdatedSettingEvent } from "./profiles/events/ProfileUpdatedSetting.ts";
|
||||||
@@ -15,6 +18,7 @@ import { type RoleUpdateEvent } from "./profiles/events/RoleUpdate.ts";
|
|||||||
import ProfileManagerBase from "./profiles/manager.ts";
|
import ProfileManagerBase from "./profiles/manager.ts";
|
||||||
import { ServerRoomsBase } from "./rooms/base.ts";
|
import { ServerRoomsBase } from "./rooms/base.ts";
|
||||||
import { RoomUpdatedEvent, SubroomUpdatedEvent } from "./rooms/internal/RoomEvents.ts";
|
import { RoomUpdatedEvent, SubroomUpdatedEvent } from "./rooms/internal/RoomEvents.ts";
|
||||||
|
import { AnnouncementDTO } from "./types.ts";
|
||||||
|
|
||||||
interface ServerEvents {
|
interface ServerEvents {
|
||||||
'profile.roleupdate': RoleUpdateEvent,
|
'profile.roleupdate': RoleUpdateEvent,
|
||||||
@@ -42,7 +46,7 @@ interface AutoMicMutingConfig {
|
|||||||
MicSpamWarningStateVolumeMultiplier: number;
|
MicSpamWarningStateVolumeMultiplier: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PublicConfig = {
|
export interface PublicConfig {
|
||||||
ShareBaseUrl: string;
|
ShareBaseUrl: string;
|
||||||
ServerMaintenance: {
|
ServerMaintenance: {
|
||||||
StartsInMinutes: number;
|
StartsInMinutes: number;
|
||||||
@@ -61,6 +65,9 @@ class ServerBase extends EventManager<ServerEvents> {
|
|||||||
Instances = new InstanceManager(this, 'instances');
|
Instances = new InstanceManager(this, 'instances');
|
||||||
Content = new ServerContentManager(this, "content");
|
Content = new ServerContentManager(this, "content");
|
||||||
Rooms = new ServerRoomsBase(this, 'rooms', true);
|
Rooms = new ServerRoomsBase(this, 'rooms', true);
|
||||||
|
Matchmaking = new ServerMatchmakingBase(this, "match");
|
||||||
|
Presence = new ServerPresenceBase(this, "pres");
|
||||||
|
Consumables = new ServerConsumablesBase(this, "consumables");
|
||||||
|
|
||||||
generateMask(...num: number[]) {
|
generateMask(...num: number[]) {
|
||||||
return num.reduce((sum, val) => sum + val, 0);
|
return num.reduce((sum, val) => sum + val, 0);
|
||||||
@@ -137,6 +144,11 @@ class ServerBase extends EventManager<ServerEvents> {
|
|||||||
|
|
||||||
return conf;
|
return conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAnnouncements(): AnnouncementDTO[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Server = new ServerBase();
|
const Server = new ServerBase();
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { type ConsoleItem, ConsoleItemSchema } from "./zod.ts";
|
|||||||
import Server from "../../server.ts";
|
import Server from "../../server.ts";
|
||||||
import { getSourceAddress } from "../../../util/net.ts";
|
import { getSourceAddress } from "../../../util/net.ts";
|
||||||
import { consoleSockets } from "../../../main.ts";
|
import { consoleSockets } from "../../../main.ts";
|
||||||
import chalk from "npm:chalk@^5.3.0";
|
import chalk from "chalk";
|
||||||
|
import { CommandSenderType } from "../../commands/cmdtypes.ts";
|
||||||
|
|
||||||
export default class SocketConsoleHandler {
|
export default class SocketConsoleHandler {
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ export default class SocketConsoleHandler {
|
|||||||
if (!zodParsed.success) this.destroy();
|
if (!zodParsed.success) this.destroy();
|
||||||
|
|
||||||
else if (zodParsed.data.e == ConsoleEvent.Command) {
|
else if (zodParsed.data.e == ConsoleEvent.Command) {
|
||||||
const data = await Server.Commands.dispatch(...zodParsed.data.d.split(' '));
|
const data = await Server.Commands.dispatch({ type: CommandSenderType.Console }, ...zodParsed.data.d.split(' '));
|
||||||
if (data instanceof Error) throw data;
|
if (data instanceof Error) throw data;
|
||||||
|
|
||||||
this.send(ConsoleEvent.Message, chalk.gray(`> ${chalk.yellow(data)}`));
|
this.send(ConsoleEvent.Message, chalk.gray(`> ${chalk.yellow(data)}`));
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { SocketTarget } from "./targets/targetbase.ts";
|
|||||||
import type Profile from "../../profiles/profile.ts";
|
import type Profile from "../../profiles/profile.ts";
|
||||||
import { detailedLog } from "../../../main.ts";
|
import { detailedLog } from "../../../main.ts";
|
||||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||||
|
import Server from "../../server.ts";
|
||||||
|
import { PresenceUpdateEvent } from "../../presence/events/PresenceUpdateEvent.ts";
|
||||||
|
|
||||||
const logmessages = true;
|
const logmessages = true;
|
||||||
|
|
||||||
@@ -32,6 +34,10 @@ export class SignalRSocketHandler {
|
|||||||
|
|
||||||
#killed = false;
|
#killed = false;
|
||||||
|
|
||||||
|
#presCb = (ev: PresenceUpdateEvent) => {
|
||||||
|
if (ev.profile == this.#profile) this.sendNotification("PresenceUpdate", ev.presence.export());
|
||||||
|
}
|
||||||
|
|
||||||
constructor(socket: WebSocket, player: Profile) {
|
constructor(socket: WebSocket, player: Profile) {
|
||||||
|
|
||||||
this.#socket = socket;
|
this.#socket = socket;
|
||||||
@@ -69,6 +75,8 @@ export class SignalRSocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #onMessage(message: Message) {
|
async #onMessage(message: Message) {
|
||||||
|
this.#profile.Matchmaking.updateLastSeen();
|
||||||
|
|
||||||
if (message.kind == MessageKind.Protocol) {
|
if (message.kind == MessageKind.Protocol) {
|
||||||
this.sendRaw({});
|
this.sendRaw({});
|
||||||
return;
|
return;
|
||||||
@@ -130,6 +138,8 @@ export class SignalRSocketHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Server.on('presence.update', this.#presCb);
|
||||||
|
|
||||||
this.#socket.addEventListener('close', this.destroy(this, true));
|
this.#socket.addEventListener('close', this.destroy(this, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +147,8 @@ export class SignalRSocketHandler {
|
|||||||
return (ev: CloseEvent) => {
|
return (ev: CloseEvent) => {
|
||||||
handler.#killed = true;
|
handler.#killed = true;
|
||||||
|
|
||||||
|
Server.off('presence.update', this.#presCb);
|
||||||
|
|
||||||
let errorReason = "Socket closed by server";
|
let errorReason = "Socket closed by server";
|
||||||
this.#log.d(`Socket close code: ${ev.code}`);
|
this.#log.d(`Socket close code: ${ev.code}`);
|
||||||
if (ev.reason.includes('Bye!')) errorReason = "Socket closed by client request";
|
if (ev.reason.includes('Bye!')) errorReason = "Socket closed by client request";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
|||||||
#ids: number[] = [];
|
#ids: number[] = [];
|
||||||
|
|
||||||
override zod = z.object({
|
override zod = z.object({
|
||||||
PlayerIds: z.array(z.number().nonnegative().max(2_147_483_647))
|
PlayerIds: z.array(z.number().nonnegative().max(Math.pow(2, 31)))
|
||||||
});
|
});
|
||||||
|
|
||||||
override exec(...ids: number[]) {
|
override exec(...ids: number[]) {
|
||||||
|
|||||||
29
src/server/types.ts
Normal file
29
src/server/types.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { PlatformType } from "./platforms/types.ts";
|
||||||
|
|
||||||
|
export enum AnnouncementType {
|
||||||
|
Update,
|
||||||
|
Contest,
|
||||||
|
Store,
|
||||||
|
Event,
|
||||||
|
Warning
|
||||||
|
}
|
||||||
|
enum AnnouncementLinkType {
|
||||||
|
Url,
|
||||||
|
AccountId,
|
||||||
|
EventId,
|
||||||
|
RoomName,
|
||||||
|
Storefront
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnouncementDTO {
|
||||||
|
AnnouncementId: number,
|
||||||
|
AnnouncementType: AnnouncementType,
|
||||||
|
Title: string,
|
||||||
|
Body: string,
|
||||||
|
ImageName: string,
|
||||||
|
LinkType: AnnouncementLinkType,
|
||||||
|
LinkName: string,
|
||||||
|
LinkUri: string,
|
||||||
|
Platform: PlatformType,
|
||||||
|
CreatedAt: string,
|
||||||
|
}
|
||||||
@@ -135,3 +135,41 @@ export class RateLimiter {
|
|||||||
clearInterval(this.#intervalId);
|
clearInterval(this.#intervalId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginLockBodySchema = z.object({
|
||||||
|
LoginLock: z.uuidv4()
|
||||||
|
});
|
||||||
|
export const loginLockMiddleware = async (c: Context<HonoEnv>, nxt: Next) => {
|
||||||
|
function unauthorized() {
|
||||||
|
return statusResponse(c, HTTPStatus.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.req.header("Content-Type") !== "application/x-www-form-urlencoded") return unauthorized();
|
||||||
|
try {
|
||||||
|
const form = await c.req.formData();
|
||||||
|
|
||||||
|
const body = await loginLockBodySchema.safeParseAsync(Object.fromEntries(form.entries()));
|
||||||
|
if (body.success) {
|
||||||
|
if (typeof c.get('profile') == 'undefined') {
|
||||||
|
log.w(`Profile was not set, cannot validate LoginLock. Was the request authorized?`);
|
||||||
|
return statusResponse(c, HTTPStatus.InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = c.get('profile');
|
||||||
|
|
||||||
|
const loginLock = await profile.Matchmaking.getLoginLock();
|
||||||
|
if (!loginLock) await profile.Matchmaking.setLoginLock(body.data.LoginLock);
|
||||||
|
else if (body.data.LoginLock !== loginLock) {
|
||||||
|
log.w(`LoginLock did not match. The token for this profile could be compromised or the client is an unknown state.`);
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await nxt();
|
||||||
|
} else {
|
||||||
|
log.w(`LoginLock parse failed: ${JSON.stringify(body.error)}`);
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,23 +11,23 @@ export async function routeImporter(hono: Hono<HonoEnv>, prefix: string, paths:
|
|||||||
for (const route of items) hono.route(route.path, route.app);
|
for (const route of items) hono.route(route.path, route.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importer<T>(importKey: string, prefix: string, paths: string[]): Promise<T[]> {
|
export async function importer<T>(importKey: string, p: string, paths: string[]): Promise<T[]> {
|
||||||
const log = new Logging(`Importer:'${importKey}'-${prefix}`);
|
const log = new Logging(`Importer:'${importKey}':${p}`);
|
||||||
const items: T[] = [];
|
const items: T[] = [];
|
||||||
|
|
||||||
for (const pathStr of paths) {
|
for (const pathStr of paths) {
|
||||||
|
|
||||||
const importPath = path.join(process.cwd(), prefix, pathStr);
|
const importPath = path.join(process.cwd(), p, pathStr);
|
||||||
if (debug) log.d(`'${importKey}' found ${importPath}`);
|
if (debug) log.d(`'${importKey}' found ${importPath}`);
|
||||||
|
|
||||||
for await (const localPath of Deno.readDir(importPath)) {
|
for await (const localPath of Deno.readDir(importPath)) {
|
||||||
if (localPath.isDirectory) continue;
|
if (localPath.isDirectory) continue;
|
||||||
if (localPath.isFile && localPath.name.endsWith('.ts')) {
|
if (localPath.isFile && localPath.name.endsWith('.ts')) {
|
||||||
|
|
||||||
const fullPath = path.join('file://', importPath, localPath.name);
|
const fullPath = path.join(importPath, localPath.name);
|
||||||
|
|
||||||
if (debug) log.d(`'${importKey}' importing ${fullPath}`);
|
if (debug) log.d(`'${importKey}' importing ${fullPath}`);
|
||||||
await import(fullPath).then(val => {
|
await import(`file://${fullPath}`).then(val => {
|
||||||
|
|
||||||
if (val[importKey]) items.push(val[importKey]);
|
if (val[importKey]) items.push(val[importKey]);
|
||||||
else log.w(`Import key '${importKey}' not found on: '${fullPath}'`);
|
else log.w(`Import key '${importKey}' not found on: '${fullPath}'`);
|
||||||
|
|||||||
@@ -43,9 +43,25 @@ export const transformStringToEnum = <T>(anEnum: { [s: string]: string | number
|
|||||||
|
|
||||||
export const transformCheckEnum = <T>(anEnum: { [s: string]: string | number }) => {
|
export const transformCheckEnum = <T>(anEnum: { [s: string]: string | number }) => {
|
||||||
return (arg: number | string, ctx: z.RefinementCtx<number | string>) => {
|
return (arg: number | string, ctx: z.RefinementCtx<number | string>) => {
|
||||||
if (typeof anEnum[arg] == 'undefined') {
|
function invalid() {
|
||||||
ctx.addIssue("Not an enum member");
|
ctx.addIssue("Invalid enum member");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof arg == 'string') {
|
||||||
|
if (typeof anEnum[arg] == 'number') return anEnum[arg] as T;
|
||||||
|
else {
|
||||||
|
invalid();
|
||||||
return null;
|
return null;
|
||||||
} else return anEnum[arg] as T;
|
}
|
||||||
|
} else if (typeof arg == 'number') {
|
||||||
|
if (typeof anEnum[arg] == 'string') return arg as T;
|
||||||
|
else {
|
||||||
|
invalid();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
invalid();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user