build config changes
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 1m50s
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 1m50s
* Commit hash shipped with builds
* Post & pre-build events
* Objective fixes
* Orientation challenge filler
* Custom Rooms base
- Currently cannot save rooms (CDN not set up)
* Moved root path to path.ts
* Room cloning
* Rewrote instances - the whole thing
* Relationships are still untested
* Charades Words
* AG Room fetch
* Private room matchmaking
* Socket fixes
This commit is contained in:
@@ -1,19 +1,18 @@
|
||||
# Galvanic Corrosion
|
||||
|
||||
delectable yum yum
|
||||
delectable yum yum<br>
|
||||
|
||||
Rec Room custom server for communities. Fast runtime and easy setup.<br>Built
|
||||
for Rec Room build 1063 (Timestamp: 637191339113673856, Version: 20200306)
|
||||
|
||||
<img src="galv4.jpg" alt="drawing" width="200"/>
|
||||
<img src="galv4.jpg" alt="drawing" width="200"/><br>
|
||||
Photo taken by Nick Gromicko, CMI®
|
||||
|
||||
# Disclaimer
|
||||
Galvanic Corrosion and its contributors are **not** associated in **any** form with:
|
||||
* RecNet ("the RecNet platform")
|
||||
* Rec Room, Inc.
|
||||
* Exit Games Inc.
|
||||
* Against Gravity
|
||||
* Photon Network, Photon Engine, or services associated
|
||||
* Exit Games Inc.
|
||||
* Any person(s) in contact with or employed for or with Rec Room Inc.
|
||||
|
||||
## Configuration
|
||||
|
||||
15
deno.json
15
deno.json
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"tasks": {
|
||||
"compile-win": "deno compile --include res --include src --target x86_64-pc-windows-msvc -o build/GalvanicCorrosion.exe -A src/main.ts",
|
||||
"compile-linux": "deno compile --include res --include src --target x86_64-unknown-linux-gnu -o build/GalvanicCorrosion -A src/main.ts",
|
||||
"cross-compile": "deno run compile-win && deno run compile-linux",
|
||||
"dev": "deno run -A src/main.ts --dev"
|
||||
"compile-win-a": "deno compile --include res --include src --target x86_64-pc-windows-msvc -o build/GalvanicCorrosion.exe -A src/main.ts",
|
||||
"compile-linux-a": "deno compile --include res --include src --target x86_64-unknown-linux-gnu -o build/GalvanicCorrosion -A src/main.ts",
|
||||
"compile-win": "deno run prebuild && deno run compile-win-a && deno run postbuild",
|
||||
"compile-linux": "deno run prebuild && deno run compile-linux-a && deno run postbuild",
|
||||
"cross-compile": "deno run prebuild && deno run compile-win-a && deno run compile-linux-a && deno run postbuild",
|
||||
"dev": "deno run -A src/main.ts --dev",
|
||||
"prebuild": "deno run -A ./prebuild.ts",
|
||||
"postbuild": "deno run -A ./postbuild.ts"
|
||||
},
|
||||
"imports": {
|
||||
"@gz/jwt": "jsr:@gz/jwt@^0.1.0",
|
||||
@@ -28,5 +32,6 @@
|
||||
"./src/types/express.ts",
|
||||
"./src/types/http.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": "0.1.0"
|
||||
}
|
||||
|
||||
24
postbuild.ts
Normal file
24
postbuild.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
try {
|
||||
Deno.removeSync('./ver.ts');
|
||||
Deno.renameSync('./ver.ts.bak', 'ver.ts');
|
||||
} catch (err) {
|
||||
console.error(`Cannot post-build version information: ${err}`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
38
prebuild.ts
Normal file
38
prebuild.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
interface DenoProj {
|
||||
version: string
|
||||
}
|
||||
|
||||
try {
|
||||
const file = JSON.parse(Deno.readTextFileSync('./deno.json').toString()) as DenoProj;
|
||||
|
||||
const devVer = Deno.readTextFileSync('./ver.ts');
|
||||
const commitHash = new Deno.Command("git", { args: ["rev-parse", "--short=12", "HEAD"] }).outputSync();
|
||||
|
||||
const newVerString = `${file.version}-${new TextDecoder().decode(commitHash.stdout).trim()}`;
|
||||
|
||||
if (file.version) {
|
||||
Deno.writeTextFileSync('./ver.ts.bak', devVer);
|
||||
Deno.writeTextFileSync('./ver.ts', devVer.replace('development', newVerString));
|
||||
console.info('Built version information');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Cannot build version information: ${err}`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
262
res/words.json
Normal file
262
res/words.json
Normal file
@@ -0,0 +1,262 @@
|
||||
{
|
||||
"easy": [
|
||||
"Bike",
|
||||
"Light",
|
||||
"Jacket",
|
||||
"Butterfly",
|
||||
"Orange",
|
||||
"Triangle",
|
||||
"Lemon",
|
||||
"Crab",
|
||||
"Alligator",
|
||||
"Shoe",
|
||||
"Purse",
|
||||
"Leg",
|
||||
"Neck",
|
||||
"Starfish",
|
||||
"Box",
|
||||
"Backpack",
|
||||
"House",
|
||||
"Duck",
|
||||
"Skateboard",
|
||||
"Cupcake",
|
||||
"Hamburger",
|
||||
"Owl",
|
||||
"Cup",
|
||||
"Hand",
|
||||
"Smile",
|
||||
"Curl",
|
||||
"Hippo",
|
||||
"Turtle",
|
||||
"Truck",
|
||||
"Beach",
|
||||
"Ocean",
|
||||
"Pants",
|
||||
"Lips",
|
||||
"Drum",
|
||||
"Dragon",
|
||||
"Egg",
|
||||
"Fish",
|
||||
"Desk",
|
||||
"Mouth",
|
||||
"Feather",
|
||||
"Grapes",
|
||||
"Zoo",
|
||||
"Bathroom",
|
||||
"Bus",
|
||||
"Shirt",
|
||||
"Grass",
|
||||
"Snail",
|
||||
"Ice Cream Cone",
|
||||
"Snake",
|
||||
"Arm",
|
||||
"Cookie",
|
||||
"Table",
|
||||
"Dinosaur",
|
||||
"Dream",
|
||||
"Frog",
|
||||
"Ant",
|
||||
"Sheep",
|
||||
"Boat",
|
||||
"Baby",
|
||||
"Bowl",
|
||||
"Banana",
|
||||
"Pig",
|
||||
"Dog",
|
||||
"Ants",
|
||||
"Corn",
|
||||
"Coat",
|
||||
"Slide",
|
||||
"Comb",
|
||||
"Bug",
|
||||
"Pizza",
|
||||
"Plant",
|
||||
"Pencil",
|
||||
"Key",
|
||||
"Cloud",
|
||||
"Lamp",
|
||||
"Balloon",
|
||||
"Robot",
|
||||
"Chimney",
|
||||
"Motorcycle",
|
||||
"Bounce",
|
||||
"Square",
|
||||
"Pie",
|
||||
"Swimming Pool",
|
||||
"Bumblebee",
|
||||
"Flower",
|
||||
"Lollipop",
|
||||
"Bird",
|
||||
"King",
|
||||
"Jellyfish",
|
||||
"Bone",
|
||||
"Island",
|
||||
"Moon",
|
||||
"Seashell",
|
||||
"Nail",
|
||||
"Bunk Bed",
|
||||
"Jail",
|
||||
"Ring",
|
||||
"Family",
|
||||
"Airplane",
|
||||
"Earth",
|
||||
"Hair",
|
||||
"Snowman",
|
||||
"Car",
|
||||
"Hook",
|
||||
"Sea",
|
||||
"Pen",
|
||||
"Diamond",
|
||||
"Spoon",
|
||||
"Kite",
|
||||
"Woman",
|
||||
"Socks",
|
||||
"Mouse",
|
||||
"Cube",
|
||||
"Finger",
|
||||
"Lizard",
|
||||
"Angel",
|
||||
"Mickey Mouse",
|
||||
"Spider",
|
||||
"Mountain",
|
||||
"Branch",
|
||||
"Spider Web",
|
||||
"Ship",
|
||||
"Bunny",
|
||||
"Face",
|
||||
"Music",
|
||||
"Bell",
|
||||
"Rainbow",
|
||||
"Star"
|
||||
],
|
||||
"hard": [
|
||||
"Parent",
|
||||
"Concession Stand",
|
||||
"Tablespoon",
|
||||
"Puppet",
|
||||
"Gold",
|
||||
"Mirror",
|
||||
"Nanny",
|
||||
"Irrigation",
|
||||
"Humidity",
|
||||
"Postcard",
|
||||
"Musician",
|
||||
"Chairman",
|
||||
"Human",
|
||||
"Barber",
|
||||
"Judge",
|
||||
"Jedi",
|
||||
"Customer",
|
||||
"Fur",
|
||||
"Punk",
|
||||
"Elf",
|
||||
"Sled",
|
||||
"Vitamin",
|
||||
"Print",
|
||||
"Internet",
|
||||
"Swarm",
|
||||
"Zoo",
|
||||
"Shelter",
|
||||
"Blizzard",
|
||||
"Dust Bunny",
|
||||
"Printer Ink",
|
||||
"Oxcart",
|
||||
"Carat",
|
||||
"Black Belt",
|
||||
"Fireman Pole",
|
||||
"Darkness",
|
||||
"Carpenter",
|
||||
"Coach",
|
||||
"Back Flip",
|
||||
"Extension Cord",
|
||||
"Flu",
|
||||
"Telephone Booth",
|
||||
"Country",
|
||||
"Sticky Note",
|
||||
"Traffic Jam",
|
||||
"Science",
|
||||
"Wooly Mammoth",
|
||||
"Captain",
|
||||
"Driveway",
|
||||
"Mysterious",
|
||||
"Dew",
|
||||
"Injury",
|
||||
"Sunburn",
|
||||
"Grandpa",
|
||||
"Ceiling Fan",
|
||||
"Cruise",
|
||||
"Chime",
|
||||
"Accounting",
|
||||
"Hour",
|
||||
"End Zone",
|
||||
"Miner",
|
||||
"Government",
|
||||
"Lunar Rover",
|
||||
"Pro",
|
||||
"Date",
|
||||
"Police",
|
||||
"Fireside",
|
||||
"Hand Soap",
|
||||
"Ditch",
|
||||
"Expert",
|
||||
"Fade",
|
||||
"Nap",
|
||||
"Last",
|
||||
"Hot Tub",
|
||||
"Loveseat",
|
||||
"Braid",
|
||||
"Rodeo",
|
||||
"Coastline",
|
||||
"Molar",
|
||||
"Drugstore",
|
||||
"Tow Truck",
|
||||
"Pickup Truck",
|
||||
"Shrink Ray",
|
||||
"Crane",
|
||||
"Bedbug",
|
||||
"Beluga Whale",
|
||||
"Fiance",
|
||||
"Fizz",
|
||||
"Junk",
|
||||
"Yolk",
|
||||
"Photosynthesis",
|
||||
"Wobble",
|
||||
"Hairspray",
|
||||
"Crop Duster",
|
||||
"Recycle",
|
||||
"Script",
|
||||
"Vein",
|
||||
"Jaw",
|
||||
"Thaw",
|
||||
"Goalkeeper",
|
||||
"Optometrist",
|
||||
"Shower Curtain",
|
||||
"Cowboy",
|
||||
"Tide",
|
||||
"Quadrant",
|
||||
"Bride",
|
||||
"Pile",
|
||||
"Midnight",
|
||||
"Amusement Park",
|
||||
"Orbit",
|
||||
"Goblin",
|
||||
"Bonnet",
|
||||
"Snore",
|
||||
"Cape",
|
||||
"Truck Stop",
|
||||
"Ginger",
|
||||
"Hermit Crab",
|
||||
"Safe",
|
||||
"Jungle",
|
||||
"Student",
|
||||
"Headache",
|
||||
"Scuba Diving",
|
||||
"Neighborhood",
|
||||
"Bookend",
|
||||
"Crow's Nest",
|
||||
"Plantation",
|
||||
"Owner",
|
||||
"Stuffed Animal",
|
||||
"Gown"
|
||||
]
|
||||
}
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Config } from "../config.ts";
|
||||
import { Redis } from "../db.ts";
|
||||
import { Objectives } from "./objectives.ts";
|
||||
import { Objectives, ObjectiveType } from "./objectives.ts";
|
||||
|
||||
export type LevelProgressionItem = {
|
||||
Level: number;
|
||||
@@ -67,7 +67,43 @@ export function getConfig() {
|
||||
StartsInMinutes: 0,
|
||||
},
|
||||
LevelProgressionMaps: generateLevelProgressionMap(),
|
||||
DailyObjectives: [[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}],[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}],[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}]],
|
||||
DailyObjectives: [
|
||||
[
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0}
|
||||
],
|
||||
[
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0}
|
||||
],
|
||||
[
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0}
|
||||
],
|
||||
[
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0}
|
||||
],
|
||||
[
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0}
|
||||
],
|
||||
[
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0}
|
||||
],
|
||||
[
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0},
|
||||
{type: ObjectiveType.Default, score: 0}
|
||||
]
|
||||
],
|
||||
AutoMicMutingConfig: {
|
||||
MicSpamVolumeThreshold: 1.125,
|
||||
MicVolumeSampleInterval: 0.25,
|
||||
|
||||
47
src/data/content/activities.ts
Normal file
47
src/data/content/activities.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { RootPath } from "../../path.ts";
|
||||
|
||||
const log = new Logging("Activities");
|
||||
|
||||
enum CharadesWordsDifficulty {
|
||||
Easy,
|
||||
Hard
|
||||
}
|
||||
interface CharadesWord {
|
||||
EN_US: string,
|
||||
Difficulty: CharadesWordsDifficulty
|
||||
}
|
||||
interface WordsConfig {
|
||||
easy: string[],
|
||||
hard: string[]
|
||||
}
|
||||
let charades: WordsConfig | undefined;
|
||||
try {
|
||||
const data = Deno.readTextFileSync(`${RootPath}/res/words.json`);
|
||||
charades = JSON.parse(data);
|
||||
} catch (err) {
|
||||
log.e(`Could not read charades words config from disk!`);
|
||||
}
|
||||
|
||||
export function getWords() {
|
||||
if (!charades) return [{EN_US: "Galvanic Corrosion", Difficulty: CharadesWordsDifficulty.Easy}] as CharadesWord[]
|
||||
else return charades.easy.map(val => ({EN_US: val, Difficulty: CharadesWordsDifficulty.Easy} as CharadesWord))
|
||||
.concat(charades.hard.map(val => ({EN_US: val, Difficulty: CharadesWordsDifficulty.Hard} as CharadesWord)))
|
||||
}
|
||||
@@ -15,9 +15,7 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { platform } from "node:process";
|
||||
|
||||
export const RootPath = Deno.mainModule.substring(platform == 'win32' ? 8 : 7, Deno.mainModule.length - 11);
|
||||
import { RootPath } from "../../path.ts";
|
||||
|
||||
export function getBaseImage(name: string) {
|
||||
try {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Buffer } from "node:buffer";
|
||||
import { Redis } from "../../db.ts";
|
||||
import { generateRandomString } from "../../apiutils.ts";
|
||||
|
||||
export enum FileType {
|
||||
Unknown,
|
||||
RoomSave,
|
||||
Holotar,
|
||||
Image,
|
||||
Video,
|
||||
Invention
|
||||
}
|
||||
|
||||
export function getFileName(prefix: string, type: FileType) {
|
||||
switch (type) {
|
||||
case FileType.RoomSave:
|
||||
return `${prefix}.room`;
|
||||
case FileType.Holotar:
|
||||
return `${prefix}.holotar`;
|
||||
case FileType.Image:
|
||||
return `${prefix}.image`;
|
||||
case FileType.Video:
|
||||
return `${prefix}.video`;
|
||||
case FileType.Invention:
|
||||
return `${prefix}.invention`;
|
||||
default:
|
||||
return `${prefix}.unknown`
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFile(name: string) {
|
||||
const data = await Redis.Database.getBuffer(Redis.buildKey(
|
||||
Redis.KeyGroups.Content.Root,
|
||||
name
|
||||
));
|
||||
|
||||
if (!data) return null;
|
||||
else return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Name of the new file
|
||||
*/
|
||||
export async function setFile(data: Buffer<ArrayBufferLike>, type: FileType, name?: string) {
|
||||
let filename = generateRandomString(24);
|
||||
if (name) filename = name;
|
||||
const finalName = getFileName(filename, type);
|
||||
|
||||
await Redis.Database.set(Redis.buildKey(
|
||||
Redis.KeyGroups.Content.Root,
|
||||
finalName
|
||||
), data);
|
||||
await initFileMeta(filename, type);
|
||||
return filename;
|
||||
}
|
||||
|
||||
interface FileMeta {
|
||||
created: Date | string;
|
||||
fetchCount: number;
|
||||
type: FileType
|
||||
}
|
||||
|
||||
async function initFileMeta(filename: string, type: FileType) {
|
||||
const meta: FileMeta = {
|
||||
created: new Date(),
|
||||
fetchCount: 0,
|
||||
type: type
|
||||
}
|
||||
await Redis.Database.hset(Redis.buildKey(
|
||||
Redis.KeyGroups.Content.Files,
|
||||
getFileName(filename, type)
|
||||
), meta);
|
||||
}
|
||||
|
||||
export async function incrementFileFetches(name: string, type: FileType) {
|
||||
|
||||
}
|
||||
@@ -16,13 +16,12 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Redis } from "../../db.ts";
|
||||
import { RootPath } from "./baseimages.ts";
|
||||
import { Profile } from "../profiles.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomDetails, RoomState, WriteMode } from "./rooms/DataTypes.ts";
|
||||
import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomDataTypes, RoomDetails, RoomState, WriteMode } from "./rooms/DataTypes.ts";
|
||||
import { RoomFactory } from "./rooms/RoomFactory.ts";
|
||||
import { SubroomFactory } from "./rooms/SubroomFactory.ts";
|
||||
import { Image } from "https://deno.land/x/imagescript@1.3.0/ImageScript.js";
|
||||
import { RootPath } from "../../path.ts";
|
||||
|
||||
const log = new Logging("Rooms");
|
||||
|
||||
@@ -87,34 +86,27 @@ class RoomsBase {
|
||||
}
|
||||
|
||||
async cloneRoom(roomid: number, newname: string, newowner: Profile) {
|
||||
enum CloneResult {
|
||||
Success,
|
||||
DoesNotAllowCloning,
|
||||
CannotCloneDormRoom,
|
||||
NameIsTaken,
|
||||
Unknown
|
||||
}
|
||||
interface RoomClone {
|
||||
factory?: RoomFactory;
|
||||
result: CloneResult;
|
||||
result: RoomDataTypes.CreateModifyRoomStatus;
|
||||
}
|
||||
|
||||
const factory = await new RoomFactory({ id: roomid, factoryMode: FactoryMode.Fetch }).init();
|
||||
if (!factory || !factory.CloningAllowed) return { result: CloneResult.DoesNotAllowCloning } as RoomClone;
|
||||
if (factory.Name == 'DormRoom') return { result: CloneResult.CannotCloneDormRoom } as RoomClone;
|
||||
if (factory.Name == newname) return { result: CloneResult.NameIsTaken } as RoomClone;
|
||||
if (!factory || !factory.CloningAllowed) return { result: RoomDataTypes.CreateModifyRoomStatus.PermissionDenied } as RoomClone;
|
||||
if (factory.Name == 'DormRoom') return { result: RoomDataTypes.CreateModifyRoomStatus.ReservedName } as RoomClone;
|
||||
if (factory.Name == newname) return { result: RoomDataTypes.CreateModifyRoomStatus.DuplicateName } as RoomClone;
|
||||
|
||||
const newFactory = await new RoomFactory({ id: await Rooms.#getAvailableRoomId(), factoryMode: FactoryMode.Write }).init();
|
||||
if (!newFactory) return { result: CloneResult.Unknown } as RoomClone;
|
||||
if (!newFactory) return { result: RoomDataTypes.CreateModifyRoomStatus.Unknown } as RoomClone;
|
||||
|
||||
newFactory.CreatorPlayerId = newowner.getId();
|
||||
newFactory.Description = factory.Description;
|
||||
newFactory.Name = factory.Name;
|
||||
newFactory.ImageName = factory.Description;
|
||||
newFactory.State = factory.State;
|
||||
newFactory.RoomAccessibility = factory.RoomAccessibility;
|
||||
newFactory.Name = newname;
|
||||
newFactory.ImageName = factory.ImageName;
|
||||
newFactory.State = RoomState.Active;
|
||||
newFactory.RoomAccessibility = RoomAccessibility.Private;
|
||||
newFactory.SupportsLevelVoting = factory.SupportsLevelVoting;
|
||||
newFactory.IsAGRoom = factory.IsAGRoom;
|
||||
newFactory.IsAGRoom = false;
|
||||
newFactory.IsDormRoom = factory.IsDormRoom;
|
||||
newFactory.CloningAllowed = false; // new rooms cannot be cloned
|
||||
newFactory.AllowsJuniors = factory.AllowsJuniors;
|
||||
@@ -128,8 +120,8 @@ class RoomsBase {
|
||||
|
||||
const oldSubroomIds = await factory.getAllSubroomIds();
|
||||
const promises = oldSubroomIds.map(async (id) => {
|
||||
const newSubroomFactory = newFactory.getSubroom(id, FactoryMode.Write, WriteMode.Overwrite);
|
||||
const oldSubroomFactory = factory.getSubroom(id, FactoryMode.Fetch);
|
||||
const newSubroomFactory = await newFactory.getSubroom(id, FactoryMode.Write, WriteMode.Overwrite).init();
|
||||
const oldSubroomFactory = await factory.getSubroom(id, FactoryMode.Fetch).init();
|
||||
|
||||
newSubroomFactory.RoomSceneLocationId = oldSubroomFactory.RoomSceneLocationId;
|
||||
newSubroomFactory.Name = oldSubroomFactory.Name;
|
||||
@@ -143,10 +135,11 @@ class RoomsBase {
|
||||
await Promise.all(promises);
|
||||
|
||||
await newFactory.write();
|
||||
newFactory.factoryMode = FactoryMode.Fetch;
|
||||
|
||||
return {
|
||||
factory: newFactory,
|
||||
result: CloneResult.Success
|
||||
result: RoomDataTypes.CreateModifyRoomStatus.Success
|
||||
} as RoomClone
|
||||
}
|
||||
|
||||
@@ -200,7 +193,7 @@ class RoomsBase {
|
||||
|
||||
factory.addHardwareSupport('*');
|
||||
|
||||
const subroomFactory = factory.getSubroom(await this.#getAvailableSubRoomId(id), FactoryMode.Write, WriteMode.WriteIfFree);
|
||||
const subroomFactory = await factory.getSubroom(await this.#getAvailableSubRoomId(id), FactoryMode.Write, WriteMode.WriteIfFree).init();
|
||||
if (!subroomFactory) return null;
|
||||
|
||||
subroomFactory.RoomSceneLocationId = IntegratedRoomScene.DormRoom;
|
||||
|
||||
@@ -143,6 +143,7 @@ export enum IntegratedRoomScene {
|
||||
DiscGolfLake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
|
||||
DiscGolfPropulsion = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
|
||||
Dodgeball = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
|
||||
DodgeballVR = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
|
||||
Paddleball = "d89f74fa-d51e-477a-a425-025a891dd499",
|
||||
Paintball_River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
|
||||
Paintball_Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
|
||||
@@ -179,7 +180,6 @@ export enum IntegratedRoomScene {
|
||||
Stadium = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
|
||||
Hangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
|
||||
CyberJunkCity = "9d6456ce-6264-48b4-808d-2d96b3d91038",
|
||||
DodgeballVR = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
|
||||
Crescendo = "49cb8993-a956-43e2-86f4-1318f279b22a",
|
||||
Bowling = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
|
||||
BowlingAlley = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
|
||||
@@ -188,4 +188,31 @@ export enum IntegratedRoomScene {
|
||||
StuntRunnerBaseRoom = "882e9b96-7115-4b03-86f6-c0c9d8e22e00",
|
||||
}
|
||||
|
||||
export enum CreateModifyRoomStatus {
|
||||
Success,
|
||||
Unknown,
|
||||
PermissionDenied,
|
||||
RoomNotActive,
|
||||
RoomDoesNotExist,
|
||||
RoomHasNoDataBlob,
|
||||
DuplicateName = 10,
|
||||
ReservedName,
|
||||
InappropriateName,
|
||||
InappropriateDescription,
|
||||
TooManyRooms = 20,
|
||||
InvalidMaxPlayers = 30,
|
||||
DataHistoryDoesNotExist = 40,
|
||||
DataHistoryAlreadyActive,
|
||||
InvalidTags = 50,
|
||||
NoStartingRoomScene = 55,
|
||||
RoomUnderModerationReview = 100,
|
||||
PlayerHasRoomUnderModerationReview,
|
||||
AccessibilityUnderModerationLock,
|
||||
JuniorStatusFail = 200,
|
||||
InappropriateCustomWarning = 300,
|
||||
PartialBanSuccessBanPower = 400,
|
||||
TargetHasBanPower,
|
||||
PlayerIsRoomBanned = 410
|
||||
}
|
||||
|
||||
export * as RoomDataTypes from "./DataTypes.ts";
|
||||
@@ -60,14 +60,14 @@ class MatchmakingBase {
|
||||
const instance = Instances.getInstance(options.instanceId);
|
||||
if (instance) {
|
||||
|
||||
if (Instances.playerIsInInstance(options.profile, instance)) return { errorCode: MatchmakingErrorCode.AlreadyInTargetInstance };
|
||||
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 };
|
||||
|
||||
await Instances.setPlayerInstance(options.profile, instance);
|
||||
Instances.clearAllRoomEmptyInstances(instance.roomId);
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: instance };
|
||||
await instance.addPlayer(options.profile);
|
||||
Instances.clearEmptyInstances(instance.roomId);
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: instance.snapshot() };
|
||||
}
|
||||
|
||||
} else return { errorCode: MatchmakingErrorCode.NoSuchGame }
|
||||
@@ -82,7 +82,7 @@ class MatchmakingBase {
|
||||
if (targetRoom.Room.State !== RoomDataTypes.RoomState.Active) return { errorCode: MatchmakingErrorCode.RoomIsNotActive };
|
||||
const roomId = targetRoom.Room.RoomId;
|
||||
|
||||
Instances.clearAllRoomEmptyInstances(roomId);
|
||||
Instances.clearEmptyInstances(roomId);
|
||||
|
||||
// get all available instance
|
||||
let allInstances = Instances.getAllRoomInstances(roomId).values().toArray().filter(instance => !instance.isPrivate && !instance.isFull);
|
||||
@@ -108,9 +108,9 @@ class MatchmakingBase {
|
||||
});
|
||||
|
||||
allInstances = allInstances.sort((a, b) => {
|
||||
const pidsA = Instances.getInstancePlayers(a);
|
||||
const pidsB = Instances.getInstancePlayers(b);
|
||||
return pidsA.size - pidsB.size;
|
||||
const pidsA = a.getAllPlayers();
|
||||
const pidsB = b.getAllPlayers();
|
||||
return pidsA.length - pidsB.length;
|
||||
}).reverse(); // Largest instances first
|
||||
|
||||
const foundInstance = allInstances[0];
|
||||
@@ -125,8 +125,8 @@ class MatchmakingBase {
|
||||
IsDorm: options.roomName == 'DormRoom'
|
||||
});
|
||||
|
||||
Instances.clearAllRoomEmptyInstances(roomId);
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: newInstance };
|
||||
Instances.clearEmptyInstances(roomId);
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: newInstance.snapshot() };
|
||||
|
||||
} else {
|
||||
|
||||
@@ -134,11 +134,11 @@ class MatchmakingBase {
|
||||
if (currentInstance?.roomInstanceId == foundInstance.roomInstanceId)
|
||||
return { errorCode: MatchmakingErrorCode.AlreadyInBestInstance };
|
||||
|
||||
await Instances.setPlayerInstance(options.profile, foundInstance);
|
||||
await foundInstance.addPlayer(options.profile);
|
||||
|
||||
Instances.clearAllRoomEmptyInstances(roomId);
|
||||
Instances.clearEmptyInstances(roomId);
|
||||
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: foundInstance };
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: foundInstance.snapshot() };
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -21,84 +21,191 @@ import { RoomInstance, InstanceOptions } from "./types.ts";
|
||||
import { Config } from "../../config.ts";
|
||||
import Presence from "./presence.ts";
|
||||
import { RoomFactory } from "../content/rooms/RoomFactory.ts";
|
||||
import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
|
||||
import { PushNotificationId } from "../../socket/types.ts";
|
||||
|
||||
const log = new Logging("Instances");
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
const instancePlayers: Map<RoomInstance, Set<Profile>> = new Map();
|
||||
/**
|
||||
* `Map<roomId (number), RoomInstance>`
|
||||
* `Map<roomId (number), Instance>`
|
||||
*/
|
||||
const instanceMap: Map<number, Set<RoomInstance>> = new Map();
|
||||
const instanceSet: Set<Instance> = new Set();
|
||||
|
||||
export class Instance {
|
||||
|
||||
#players = new Set<Profile>();
|
||||
timeCreated = new Date().toISOString();
|
||||
|
||||
#id: number;
|
||||
#room: RoomDataTypes.RoomDetails | undefined;
|
||||
#subroom: RoomDataTypes.RoomScene | undefined;
|
||||
|
||||
#eventId?: number; // not yet implemented
|
||||
#name?: string;
|
||||
#priv?: boolean;
|
||||
#inProgress?: boolean;
|
||||
#blob?: string;
|
||||
|
||||
constructor(id: number) {
|
||||
this.#id = id;
|
||||
}
|
||||
|
||||
async init(options: InstanceOptions) {
|
||||
|
||||
const scene = options.Room.Scenes[options.SceneIndex];
|
||||
if (!scene) throw new Error("The specified scene did not exist.");
|
||||
|
||||
let instanceName;
|
||||
if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`;
|
||||
else instanceName = `^${options.Room.Room.Name}.${scene.Name}`;
|
||||
if (options.IsDorm) {
|
||||
const dormCreatorPlayer = UnifiedProfile.get(options.Room.Room.CreatorPlayerId);
|
||||
const player = await dormCreatorPlayer.export();
|
||||
if (player) instanceName = `@${player.displayName}'s Dorm`;
|
||||
}
|
||||
|
||||
this.#room = options.Room;
|
||||
this.#subroom = scene;
|
||||
this.#name = instanceName;
|
||||
this.#blob = scene.DataBlobName;
|
||||
this.#inProgress = false;
|
||||
this.#priv = options.Private ? options.Private : false;
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
equalInstance(instance: RoomInstance) {
|
||||
return instance.roomInstanceId == this.#id;
|
||||
}
|
||||
|
||||
getAllPlayers() {
|
||||
return this.#players.values().toArray();
|
||||
}
|
||||
|
||||
hasPlayer(player: Profile) {
|
||||
return this.getAllPlayers().includes(player);
|
||||
}
|
||||
|
||||
removePlayer(player: Profile) {
|
||||
if (!this.hasPlayer(player)) throw new Error(`Cannot remove player ${player.getId()} from instance ${this.#id} they are not in`);
|
||||
this.#players.delete(player);
|
||||
player.setInstance(null);
|
||||
}
|
||||
|
||||
async addPlayer(player: Profile) {
|
||||
const currentInstance = player.getInstance();
|
||||
if (currentInstance && currentInstance.equalInstance(this)) return;
|
||||
|
||||
if (currentInstance) currentInstance.removePlayer(player);
|
||||
|
||||
if (!this.isFull) {
|
||||
const instancePlayers = this.getAllPlayers();
|
||||
const profileExport = await player.export();
|
||||
log.i(`Player ${player.getId()} "${profileExport?.displayName}" went to '${this.name}' with ${instancePlayers.length} other players`);
|
||||
|
||||
this.#players.add(player);
|
||||
player.setInstance(this);
|
||||
|
||||
const pres = await Presence.get(player);
|
||||
pres.update();
|
||||
|
||||
const room = await new RoomFactory({ id: this.roomId }).init();
|
||||
await room?.addVisit();
|
||||
|
||||
// move some of this to a dedicated "onPlayerMove" function
|
||||
} else log.w(`Instance ${this.roomInstanceId} is full. Cannot add player ${player.getId()}`);
|
||||
|
||||
log.d(`Players in instance ${this.#id}: ${this.#players.values().toArray().map(prof => prof.getId()).join(',')}`);
|
||||
}
|
||||
|
||||
get roomInstanceId() { return this.#id }
|
||||
|
||||
get roomId() { return this.#room ? this.#room?.Room.RoomId : 0 }
|
||||
|
||||
get subRoomId() { return this.#subroom ? this.#subroom?.RoomSceneId : 0 }
|
||||
|
||||
get location() { return this.#subroom ? this.#subroom?.RoomSceneLocationId : "" }
|
||||
|
||||
get dataBlob() { return this.#blob ? this.#blob : undefined }
|
||||
set dataBlob(data) { this.#blob = data }
|
||||
|
||||
get eventId() { return this.#eventId }
|
||||
|
||||
get photonRegionId() { return config.public.photonRegionId }
|
||||
|
||||
get photonRoomId() { return `GC20200306-${this.#id}` }
|
||||
|
||||
get name() { return this.#name ? this.#name : "InstanceNameError" }
|
||||
|
||||
get maxCapacity() { return this.#subroom ? this.#subroom.MaxPlayers : 8 }
|
||||
|
||||
get isFull() { return this.#players.size >= this.maxCapacity }
|
||||
|
||||
get isPrivate() { return this.#priv ? this.#priv : false }
|
||||
set isPrivate(data) { this.#priv = data }
|
||||
|
||||
get isInProgress() { return this.#inProgress ? this.#inProgress : false }
|
||||
set isInProgress(data) { this.#inProgress = data }
|
||||
|
||||
snapshot() {
|
||||
const inst: RoomInstance = {
|
||||
roomInstanceId: this.roomInstanceId,
|
||||
roomId: this.roomId,
|
||||
subRoomId: this.subRoomId,
|
||||
location: this.location,
|
||||
dataBlob: this.dataBlob,
|
||||
eventId: this.eventId,
|
||||
photonRegionId: this.photonRegionId,
|
||||
photonRoomId: this.photonRoomId,
|
||||
name: this.name,
|
||||
maxCapacity: this.maxCapacity,
|
||||
isFull: this.isFull,
|
||||
isPrivate: this.isPrivate,
|
||||
isInProgress: this.isInProgress
|
||||
}
|
||||
return inst;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
instanceSet.delete(this);
|
||||
if (this.#players.size !== 0) for (const player of this.#players) player.getSocketHandler()?.sendNotification(PushNotificationId.Logout);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class InstancesBase {
|
||||
|
||||
getInstance(id: number) {
|
||||
const instances = instancePlayers.keys();
|
||||
const instances = instanceSet.values().toArray();
|
||||
const instance = instances.find(val => val.roomInstanceId == id);
|
||||
if (instance) return instance;
|
||||
else return null;
|
||||
}
|
||||
|
||||
getAllInstances() {
|
||||
return new Set([...instanceMap.values()].flatMap(set => [...set]));
|
||||
return new Set([...instanceSet.values().toArray()]);
|
||||
}
|
||||
|
||||
getAllRoomInstances(roomId: number) {
|
||||
let instances = instanceMap.get(roomId);
|
||||
if (!instances) {
|
||||
instances = new Set();
|
||||
instanceMap.set(roomId, instances);
|
||||
}
|
||||
return instances;
|
||||
return new Set([...this.getAllInstances().values().toArray().filter(val => val.roomId == roomId)]);
|
||||
}
|
||||
|
||||
getInstancePlayers(instance: RoomInstance): Set<Profile> {
|
||||
let players = instancePlayers.get(instance);
|
||||
if (!players) {
|
||||
players = new Set();
|
||||
instancePlayers.set(instance, players);
|
||||
}
|
||||
return players;
|
||||
}
|
||||
|
||||
clearEmptyInstances(instances: Set<RoomInstance>, roomId?: number) {
|
||||
const beforeCount = instances.size;
|
||||
for (const instance of instances) {
|
||||
const profiles = this.getInstancePlayers(instance);
|
||||
if (profiles.size === 0) {
|
||||
clearEmptyInstances(roomId?: number) {
|
||||
const beforeCount = instanceSet.size;
|
||||
for (const instance of instanceSet) {
|
||||
if (roomId && instance.roomId == roomId) continue;
|
||||
const profiles = instance.getAllPlayers();
|
||||
if (profiles.length === 0) {
|
||||
log.i(`Instance ${instance.roomInstanceId} empty, deleting`);
|
||||
instancePlayers.delete(instance);
|
||||
this.getAllRoomInstances(instance.roomId).delete(instance);
|
||||
instance.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const afterCount = instances.size;
|
||||
log.d(`Cleared ${roomId !== undefined ? `room ${roomId}` : 'all'} empty instances.\n Instances before: ${beforeCount}\n Instances after: ${afterCount}`);
|
||||
}
|
||||
|
||||
clearAllEmptyInstances() {
|
||||
this.clearEmptyInstances(this.getAllInstances());
|
||||
}
|
||||
|
||||
clearAllRoomEmptyInstances(roomId: number) {
|
||||
this.clearEmptyInstances(this.getAllRoomInstances(roomId), roomId);
|
||||
}
|
||||
|
||||
updateGlobalInstancesIsFull() {
|
||||
for (const instance of this.getAllInstances())
|
||||
instance.isFull = this.getInstancePlayers(instance).size >= instance.maxCapacity;
|
||||
}
|
||||
|
||||
updateSingleInstanceIsFull(instance: RoomInstance) {
|
||||
const profiles = this.getInstancePlayers(instance);
|
||||
if (profiles.size >= instance.maxCapacity) instance.isFull = true;
|
||||
else instance.isFull = false;
|
||||
}
|
||||
|
||||
instanceCanFitPlayer(instance: RoomInstance) {
|
||||
return this.getInstancePlayers(instance).size < instance.maxCapacity;
|
||||
const afterCount = instanceSet.size;
|
||||
log.d(`Cleared empty instances for roomId ${roomId ? roomId : "*"}.\n Instances before: ${beforeCount}\n Instances after: ${afterCount}`);
|
||||
}
|
||||
|
||||
#generateUniqueInstanceId() {
|
||||
@@ -112,97 +219,20 @@ class InstancesBase {
|
||||
* Create an instance with options.
|
||||
*
|
||||
* If `options.FirstPlayer` is not specified, the created instance will not contain any players and may be removed.
|
||||
*
|
||||
* If one is, the player will be automatically added to the instance and their `profile.getInstance()` will be synchronized.
|
||||
*/
|
||||
async createInstance(options: InstanceOptions) {
|
||||
|
||||
const scene = options.Room.Scenes[options.SceneIndex];
|
||||
const newId = this.#generateUniqueInstanceId();
|
||||
if (!scene) throw new Error("The specified scene did not exist.");
|
||||
const newInstance = await new Instance(newId).init(options);
|
||||
|
||||
let instanceName = scene.Name === "Home" || scene.Name === options.Room.Room.Name ? `^${options.Room.Room.Name}` : `^${options.Room.Room.Name}.${scene.Name}`;
|
||||
if (options.IsDorm) {
|
||||
const dormCreatorPlayer = UnifiedProfile.get(options.Room.Room.CreatorPlayerId);
|
||||
const player = await dormCreatorPlayer.export();
|
||||
if (player) instanceName = `@${player.displayName}'s Dorm`;
|
||||
}
|
||||
const newInstance: RoomInstance = {
|
||||
roomInstanceId: newId,
|
||||
roomId: options.Room.Room.RoomId,
|
||||
subRoomId: scene.RoomSceneId,
|
||||
location: scene.RoomSceneLocationId,
|
||||
dataBlob: scene.DataBlobName,
|
||||
eventId: options.EventId,
|
||||
photonRegionId: config.public.photonRegionId,
|
||||
photonRoomId: `20200306-GC${newId}`,
|
||||
name: instanceName,
|
||||
maxCapacity: scene.MaxPlayers,
|
||||
isFull: false,
|
||||
isPrivate: typeof options.Private !== 'boolean' ? false : options.Private,
|
||||
isInProgress: false
|
||||
};
|
||||
|
||||
this.getAllRoomInstances(options.Room.Room.RoomId).add(newInstance);
|
||||
if (options.FirstPlayer) {
|
||||
this.setPlayerInstance(options.FirstPlayer, newInstance);
|
||||
this.getInstancePlayers(newInstance).add(options.FirstPlayer);
|
||||
}
|
||||
instanceSet.add(newInstance);
|
||||
if (options.FirstPlayer)
|
||||
newInstance.addPlayer(options.FirstPlayer);
|
||||
|
||||
return newInstance;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Call only when the player is ready to be moved to an instance
|
||||
*
|
||||
* Synchronizes profile instance to `instance` and adds player to instance.
|
||||
*/
|
||||
async setPlayerInstance(player: Profile, instance: RoomInstance) {
|
||||
const currentInstance = player.getInstance();
|
||||
if (currentInstance === instance) return;
|
||||
|
||||
if (currentInstance) {
|
||||
this.getInstancePlayers(currentInstance).delete(player);
|
||||
this.updateSingleInstanceIsFull(currentInstance);
|
||||
}
|
||||
|
||||
if (this.instanceCanFitPlayer(instance)) {
|
||||
const instancePlayers = this.getInstancePlayers(instance);
|
||||
log.i(`Player ${player.getId()} went to '${instance.name}' with ${instancePlayers.size} other players`);
|
||||
instancePlayers.add(player);
|
||||
|
||||
player.setInstance(instance);
|
||||
const pres = await Presence.get(player);
|
||||
pres.update();
|
||||
|
||||
this.updateSingleInstanceIsFull(instance);
|
||||
} else log.w(`Instance ${instance.roomInstanceId} is full. Cannot add player ${player.getId()}`);
|
||||
|
||||
const room = await new RoomFactory({ id: instance.roomId }).init();
|
||||
await room?.addVisit();
|
||||
}
|
||||
|
||||
playerIsInInstance(player: Profile, instance: RoomInstance) {
|
||||
|
||||
const profiles = this.getInstancePlayers(instance);
|
||||
return profiles.has(player);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Call only when the player is ready to be removed (or when not responding)
|
||||
*
|
||||
* Synchronizes profile instance to `null` and removes player from instance.
|
||||
*/
|
||||
removePlayerFromCurrentInstance(player: Profile) {
|
||||
const instance = player.getInstance();
|
||||
if (!instance) return;
|
||||
this.getInstancePlayers(instance).delete(player);
|
||||
player.setInstance(null);
|
||||
this.updateSingleInstanceIsFull(instance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Instances = new InstancesBase();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { SettingKey } from "../content/settings.ts";
|
||||
import { Profile } from "../profiles.ts";
|
||||
import { DeviceClass, PlayerStatusVisibility, RoomInstance, VRMovementMode } from "./types.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { Instance } from "./instances.ts";
|
||||
|
||||
const log = new Logging("Presence");
|
||||
|
||||
@@ -31,6 +32,7 @@ export interface PresenceExport {
|
||||
vrMovementMode?: VRMovementMode;
|
||||
}
|
||||
|
||||
// This whole class needs rewritten .. probably
|
||||
class PlayerPresence {
|
||||
|
||||
intervalId: number;
|
||||
@@ -63,7 +65,7 @@ class PlayerPresence {
|
||||
statusVisibility: PlayerStatusVisibility;
|
||||
deviceClass: DeviceClass;
|
||||
vrMovementMode: VRMovementMode | undefined;
|
||||
roomInstance: RoomInstance | null;
|
||||
roomInstance: Instance | null;
|
||||
|
||||
lastSeen: Date;
|
||||
|
||||
@@ -102,9 +104,10 @@ class PlayerPresence {
|
||||
*/
|
||||
async export() {
|
||||
await this.update();
|
||||
const inst = this.roomInstance?.snapshot();
|
||||
const exp: PresenceExport = {
|
||||
playerId: this.playerId,
|
||||
roomInstance: this.roomInstance,
|
||||
roomInstance: inst ? inst : null,
|
||||
statusVisibility: this.statusVisibility,
|
||||
deviceClass: this.deviceClass,
|
||||
vrMovementMode: this.vrMovementMode
|
||||
|
||||
@@ -41,6 +41,7 @@ interface Relationship {
|
||||
Favorited?: ReciprocalStatus
|
||||
}
|
||||
|
||||
// Literally all of this is untested
|
||||
export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
|
||||
#baseRelationships: Relationship[] = [
|
||||
@@ -123,15 +124,15 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
return (await Redis.Database.smembers(this.#friendsKey)).map(val => parseInt(val)).filter(val => !isNaN(val));
|
||||
}
|
||||
|
||||
async #getIncomingRequests() {
|
||||
async getIncomingRequests() {
|
||||
return (await Redis.Database.smembers(this.#incomingFriends)).map(val => parseInt(val)).filter(val => !isNaN(val));
|
||||
}
|
||||
|
||||
async #getOutgoingRequests() {
|
||||
async getOutgoingRequests() {
|
||||
return (await Redis.Database.smembers(this.#outgoingFriends)).map(val => parseInt(val)).filter(val => !isNaN(val));
|
||||
}
|
||||
|
||||
createRemoteRootKey(remoteProfileId: number) {
|
||||
#createRemoteRootKey(remoteProfileId: number) {
|
||||
return Redis.buildKey(
|
||||
Redis.KeyGroups.Profiles.Root,
|
||||
remoteProfileId.toString(),
|
||||
@@ -140,7 +141,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
}
|
||||
|
||||
async #clearAssociationWithRemote(remoteProfileId: number) {
|
||||
const remoteRootKey = this.createRemoteRootKey(remoteProfileId);
|
||||
const remoteRootKey = this.#createRemoteRootKey(remoteProfileId);
|
||||
await Redis.Database.srem(this.#incomingFriends, remoteProfileId);
|
||||
await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.OutgoingFriendRequests), this.profileId);
|
||||
await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId);
|
||||
@@ -148,27 +149,27 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
}
|
||||
|
||||
async acceptRequest(remoteProfileId: number) {
|
||||
const requests = await this.#getIncomingRequests();
|
||||
const requests = await this.getIncomingRequests();
|
||||
if (!requests.includes(remoteProfileId)) return;
|
||||
|
||||
await this.#clearAssociationWithRemote(remoteProfileId);
|
||||
|
||||
const remoteRootKey = this.createRemoteRootKey(remoteProfileId);
|
||||
const remoteRootKey = this.#createRemoteRootKey(remoteProfileId);
|
||||
|
||||
await Redis.Database.sadd(this.#friendsKey, remoteProfileId);
|
||||
await Redis.Database.sadd(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.Friends), this.profileId);
|
||||
}
|
||||
|
||||
async denyRequest(remoteProfileId: number) {
|
||||
const requests = await this.#getIncomingRequests();
|
||||
const requests = await this.getIncomingRequests();
|
||||
if (!requests.includes(remoteProfileId)) return;
|
||||
await this.#clearAssociationWithRemote(remoteProfileId);
|
||||
}
|
||||
|
||||
async getRelationshipType(remoteProfileId: number) {
|
||||
const isFriendsWithRemote = (await Redis.Database.sismember(this.#friendsKey, remoteProfileId)) >= 1;
|
||||
const remoteSentRequest = (await this.#getIncomingRequests()).includes(remoteProfileId);
|
||||
const localSentRequest = (await this.#getOutgoingRequests()).includes(remoteProfileId);
|
||||
const remoteSentRequest = (await this.getIncomingRequests()).includes(remoteProfileId);
|
||||
const localSentRequest = (await this.getOutgoingRequests()).includes(remoteProfileId);
|
||||
|
||||
if (isFriendsWithRemote || (remoteSentRequest && localSentRequest)) return RelationshipType.Friend;
|
||||
else if (remoteSentRequest) return RelationshipType.FriendRequestReceived;
|
||||
@@ -177,7 +178,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
}
|
||||
|
||||
async getMutedReciprocal(remoteProfileId: number) {
|
||||
const remoteKey = this.createRemoteRootKey(remoteProfileId);
|
||||
const remoteKey = this.#createRemoteRootKey(remoteProfileId);
|
||||
|
||||
const localMuted = (await this.getAllMuted()).includes(remoteProfileId);
|
||||
const remoteMuted = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Muted), this.profileId);
|
||||
@@ -189,7 +190,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
}
|
||||
|
||||
async getIgnoredReciprocal(remoteProfileId: number) {
|
||||
const remoteKey = this.createRemoteRootKey(remoteProfileId);
|
||||
const remoteKey = this.#createRemoteRootKey(remoteProfileId);
|
||||
|
||||
const localIgnored = (await this.getAllMuted()).includes(remoteProfileId);
|
||||
const remoteIgnored = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Ignoring), this.profileId);
|
||||
@@ -211,12 +212,12 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
}
|
||||
|
||||
async sendPlayerFriendRequest(player: Profile) {
|
||||
await Redis.Database.sadd(Redis.buildKey(this.createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId);
|
||||
await Redis.Database.sadd(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId);
|
||||
await Redis.Database.sadd(this.#outgoingFriends, player.getId());
|
||||
}
|
||||
|
||||
async removePlayerFriendRequest(player: Profile) {
|
||||
await Redis.Database.srem(Redis.buildKey(this.createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId);
|
||||
await Redis.Database.srem(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId);
|
||||
await Redis.Database.srem(this.#outgoingFriends, player.getId());
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,15 @@ export class ProfileRoomsManager extends ProfileContentManager {
|
||||
#rootKey = Redis.buildKey(
|
||||
Redis.KeyGroups.Profiles.Root,
|
||||
this.profileId.toString(),
|
||||
Redis.KeyGroups.Profiles.Avatar.Root
|
||||
Redis.KeyGroups.Profiles.Rooms
|
||||
);
|
||||
|
||||
async getRooms() {
|
||||
// get player rooms
|
||||
async getOwnedRoomIds() {
|
||||
return (await Redis.Database.smembers(this.#rootKey)).map(val => parseInt(val)).filter(val => !isNaN(val));
|
||||
}
|
||||
|
||||
async addOwnedRoomId(id: number) {
|
||||
await Redis.Database.sadd(this.#rootKey, id.toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,23 +16,25 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Redis } from "../db.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import Dictionary from "./usernames.ts";
|
||||
import { Config } from "../config.ts";
|
||||
import { AuthType } from "./users.ts";
|
||||
import * as JsonWebToken from "@gz/jwt";
|
||||
import { TokenBaseFormat } from "../apiutils.ts";
|
||||
import { DeviceClass, RoomInstance, VRMovementMode } from "./live/types.ts";
|
||||
import { DeviceClass, VRMovementMode } from "./live/types.ts";
|
||||
import { SettingKey } from "./content/settings.ts";
|
||||
import { z } from "zod";
|
||||
import { SignalRSocketHandler } from "../socket/socket.ts";
|
||||
import { ProfileSettingsManager } from "./profile/settings.ts";
|
||||
import { ProfileProgressionManager } from "./profile/progression.ts";
|
||||
import { ProfileReputationManager } from "./profile/reputation.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { ProfileRelationshipManager } from "./profile/relationships.ts";
|
||||
import { ProfileAvatarManager } from "./profile/avatar.ts";
|
||||
import { EventManager } from "./baseevent.ts";
|
||||
import { ProfileEvents, ProfileUpdatedEvent } from "./profileevents.ts";
|
||||
import { Instance } from "./live/instances.ts";
|
||||
import { ProfileRoomsManager } from "./profile/rooms.ts";
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
@@ -149,7 +151,6 @@ class Profile extends EventManager {
|
||||
return profile;
|
||||
}
|
||||
|
||||
// surely this can be written better
|
||||
static getExportAccount(id: number): Promise<SelfAccountExport | null> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username)).then((val) => {
|
||||
@@ -177,6 +178,11 @@ class Profile extends EventManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get player account exports in bulk. Returns only profiles that exist.
|
||||
* @param ids Player IDs
|
||||
* @returns Promise - Array of Profiles
|
||||
*/
|
||||
static async getExportAccountsBulk(ids: number[]) {
|
||||
const accs = await Promise.all(
|
||||
ids.map((val) => this.getExportAccount(val)),
|
||||
@@ -186,7 +192,7 @@ class Profile extends EventManager {
|
||||
|
||||
#id: number;
|
||||
|
||||
#instance: RoomInstance | null = null;
|
||||
#instance: Instance | null = null;
|
||||
|
||||
#socket: SignalRSocketHandler | null = null;
|
||||
|
||||
@@ -195,6 +201,7 @@ class Profile extends EventManager {
|
||||
Reputation: ProfileReputationManager;
|
||||
Relationships: ProfileRelationshipManager;
|
||||
Avatar: ProfileAvatarManager;
|
||||
Rooms: ProfileRoomsManager;
|
||||
|
||||
constructor(id: number) {
|
||||
super();
|
||||
@@ -206,6 +213,7 @@ class Profile extends EventManager {
|
||||
this.Reputation = new ProfileReputationManager(this.#id);
|
||||
this.Relationships = new ProfileRelationshipManager(this.#id);
|
||||
this.Avatar = new ProfileAvatarManager(this.#id);
|
||||
this.Rooms = new ProfileRoomsManager(this.#id);
|
||||
}
|
||||
|
||||
#emitProfileUpdated() {
|
||||
@@ -216,7 +224,7 @@ class Profile extends EventManager {
|
||||
this.emit(ProfileEvents.BaseUpdated, ev);
|
||||
}
|
||||
|
||||
setInstance(instance: RoomInstance | null) {
|
||||
setInstance(instance: Instance | null) {
|
||||
this.#instance = instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import { TokenBaseFormat } from "../apiutils.ts";
|
||||
type UserInitOptions = {
|
||||
client_id: string;
|
||||
pubkey: string;
|
||||
captcha?: string;
|
||||
};
|
||||
|
||||
export enum AuthType {
|
||||
|
||||
@@ -71,7 +71,7 @@ export const KeyGroups = {
|
||||
},
|
||||
Content: {
|
||||
Root: "content",
|
||||
Files: "file-meta",
|
||||
Words: "charades"
|
||||
},
|
||||
Profile_Usernames: "profile-usernames",
|
||||
PlatformAssociations: "platforms",
|
||||
|
||||
17
src/main.ts
17
src/main.ts
@@ -27,15 +27,16 @@ import { decode } from "@gz/jwt";
|
||||
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts";
|
||||
import { SocketHandoff } from "./socket/handoff.ts";
|
||||
import { SignalRSocketHandler } from "./socket/socket.ts";
|
||||
import Rooms from "./data/content/rooms.ts";
|
||||
import { GameConfigs } from "./data/config.ts";
|
||||
import { RootPath } from "./data/content/baseimages.ts";
|
||||
import { getVersion } from "./ver.ts";
|
||||
import Rooms from "./data/content/rooms.ts";
|
||||
import { PushNotificationId } from "./socket/types.ts";
|
||||
|
||||
const instanceId = generateRandomString(64);
|
||||
|
||||
const log = new Log.default("Main");
|
||||
|
||||
log.i(`Starting Galvanic Corrosion..`);
|
||||
log.i(`Galvanic Corrosion '${await getVersion()}'`);
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
@@ -158,7 +159,7 @@ try {
|
||||
}}, async (req: Request, info: Deno.ServeHandlerInfo<Deno.NetAddr>) => {
|
||||
const path = new URL(req.url).pathname;
|
||||
const upgrade = req.headers.get('Upgrade') === 'websocket';
|
||||
log.n(`U:${upgrade}; ${info.remoteAddr.hostname}:${info.remoteAddr.port} ${req.method} ${path}`);
|
||||
log.n(`SOCK U:${upgrade}; ${info.remoteAddr.hostname}:${info.remoteAddr.port} ${req.method} ${path}`);
|
||||
|
||||
if (path === '/negotiate' && req.method == 'POST')
|
||||
return new Response(JSON.stringify({}));
|
||||
@@ -199,15 +200,17 @@ try {
|
||||
Deno.addSignalListener("SIGINT", () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
for (const handoff of SocketHandoff.all()) handoff.complete();
|
||||
for (const sock of UnifiedProfile.getAllSockets()) sock.sendNotification(PushNotificationId.ModerationQuitGame);
|
||||
});
|
||||
Deno.addSignalListener("SIGINT", () => {
|
||||
if (shuttingDown) return;
|
||||
log.i(`Shutting down`);
|
||||
|
||||
abort.abort(); // websockets
|
||||
http.close();
|
||||
http.closeAllConnections();
|
||||
});
|
||||
Deno.addSignalListener("SIGINT", () => {
|
||||
for (const handoff of SocketHandoff.all()) handoff.complete();
|
||||
});
|
||||
|
||||
if (!(await UnifiedProfile.existsByName("Coach"))) UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist
|
||||
if (!(await UnifiedProfile.existsByName("Server"))) UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 if they do not exist
|
||||
|
||||
20
src/path.ts
Normal file
20
src/path.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { platform } from "node:process";
|
||||
|
||||
export const RootPath = Deno.mainModule.substring(platform == 'win32' ? 8 : 7, Deno.mainModule.length - 11);
|
||||
@@ -15,14 +15,17 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import express from "express";
|
||||
import UnifiedProfile, { Profile } from "../../data/profiles.ts";
|
||||
import { z } from "zod";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
|
||||
export const route = APIUtils.createRouter("/account");
|
||||
|
||||
const log = new Logging("AccountRoute");
|
||||
|
||||
const CreateAccountRequestBodySchema = z.object({
|
||||
platform: z.string(),
|
||||
platformId: z.string(),
|
||||
@@ -70,11 +73,9 @@ route.router.get("/bulk",
|
||||
|
||||
} else if (typeof rq.query.id == "string") {
|
||||
|
||||
const id = parseInt(rq.query.id, 10);
|
||||
const id = parseInt(rq.query.id);
|
||||
if (isNaN(id)) {
|
||||
rs.json(
|
||||
APIUtils.genericResponseFormat(true, "Query data error"),
|
||||
);
|
||||
rs.json(APIUtils.genericResponseFormat(true, "Query data error"));
|
||||
return;
|
||||
} else {
|
||||
rs.json([await Profile.getExportAccount(id)].filter((val) => val !== null));
|
||||
@@ -116,7 +117,7 @@ route.router.put("/me/displayname",
|
||||
express.urlencoded({ extended: true }),
|
||||
APIUtils.validateRequestBody(DisplayNameUpdateSchema),
|
||||
|
||||
(rq: express.Request<{}, {}, DisplayNameUpdate>, rs: express.Response, nxt: express.NextFunction) => {
|
||||
(rq: express.Request<NoBody, NoBody, DisplayNameUpdate>, rs: express.Response, nxt: express.NextFunction) => {
|
||||
rs.locals.profile.setDisplayName(rq.body.displayName);
|
||||
nxt();
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ import { route as CommunityBoardRoute } from "./api/communityboard.ts";
|
||||
import { route as PlayerEventsRoute } from "./api/playerevents.ts";
|
||||
import { route as StorefrontsRoute } from "./api/storefronts.ts";
|
||||
import { route as AnnouncementRoute } from "./api/announcement.ts";
|
||||
import { route as ActivitiesRoute } from "./api/activities.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/api");
|
||||
|
||||
@@ -65,3 +66,4 @@ route.router.use(CommunityBoardRoute.path, CommunityBoardRoute.router);
|
||||
route.router.use(PlayerEventsRoute.path, PlayerEventsRoute.router);
|
||||
route.router.use(StorefrontsRoute.path, StorefrontsRoute.router);
|
||||
route.router.use(AnnouncementRoute.path, AnnouncementRoute.router);
|
||||
route.router.use(ActivitiesRoute.path, ActivitiesRoute.router);
|
||||
34
src/routes/api/activities.ts
Normal file
34
src/routes/api/activities.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { getWords } from "../../data/content/activities.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/activities");
|
||||
|
||||
const rateLimit = new APIUtils.RateLimiter();
|
||||
|
||||
const words = getWords();
|
||||
route.router.get('/charades/v1/words',
|
||||
|
||||
rateLimit.middle(),
|
||||
|
||||
(_rq, rs) => {
|
||||
rs.json(words);
|
||||
},
|
||||
|
||||
);
|
||||
@@ -16,6 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { ObjectiveType } from "../../data/objectives.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/checklist');
|
||||
@@ -25,6 +26,171 @@ route.router.get('/v1/current',
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
APIUtils.emptyArrayResponse
|
||||
(_rq, rs) => {
|
||||
rs.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
|
||||
}
|
||||
]);
|
||||
},
|
||||
|
||||
);
|
||||
@@ -36,6 +36,17 @@ route.router.get('/v1/myprogress',
|
||||
|
||||
);
|
||||
|
||||
route.router.post('/v1/updateobjective',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
(_rq, rs) => {
|
||||
rs.sendStatus(200);
|
||||
},
|
||||
|
||||
)
|
||||
|
||||
interface ClearGroupRequestBody {
|
||||
Group: number
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ route.router.post('/v1/bulk',
|
||||
express.urlencoded({ extended: true }),
|
||||
APIUtils.validateRequestBody(reputationBulkSchema),
|
||||
|
||||
async (rq: express.Request<NoBody, {}, ReputationBulkBody>, rs: express.Response) => {
|
||||
async (rq: express.Request<NoBody, NoBody, ReputationBulkBody>, rs: express.Response) => {
|
||||
if (typeof rq.body.Ids == 'object') {
|
||||
const reputations = rq.body.Ids
|
||||
.map(id => parseInt(id)).filter(id => !isNaN(id)) // parse as int[] and filter out non-numbers
|
||||
|
||||
@@ -68,7 +68,7 @@ route.router.post('/v1/progression/bulk',
|
||||
express.urlencoded({ extended: true }),
|
||||
APIUtils.validateRequestBody(progressionBulkSchema),
|
||||
|
||||
async (rq: express.Request<NoBody, {}, ProgressionBulkBody>, rs: express.Response) => {
|
||||
async (rq: express.Request<NoBody, NoBody, ProgressionBulkBody>, rs: express.Response) => {
|
||||
if (typeof rq.body.Ids == 'object') {
|
||||
const progressions = rq.body.Ids
|
||||
.map(id => parseInt(id)).filter(id => !isNaN(id)) // filter out non-numbers
|
||||
|
||||
@@ -15,11 +15,13 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { z } from "zod";
|
||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import Rooms from "../../data/content/rooms.ts";
|
||||
import { RoomDataTypes } from "../../data/content/rooms/DataTypes.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import express from "express";
|
||||
import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/rooms");
|
||||
|
||||
@@ -54,7 +56,19 @@ route.router.get('/v2/myrooms',
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
APIUtils.emptyArrayResponse
|
||||
async (_rq, rs) => {
|
||||
const ids = await rs.locals.profile.Rooms.getOwnedRoomIds();
|
||||
if (ids.length == 0) {
|
||||
rs.json([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const roomFactoriesPreInit = ids.map(id => new RoomFactory({ id: id }));
|
||||
const roomFactories = (await Promise.all(roomFactoriesPreInit.map(factory => factory.init()))).filter(val => val !== null);
|
||||
const detailsPromises = (await Promise.all(roomFactories.map(factory => factory.export())));
|
||||
|
||||
rs.json(detailsPromises.map(roomDetails => roomDetails.Room));
|
||||
},
|
||||
|
||||
);
|
||||
|
||||
@@ -119,8 +133,58 @@ route.router.post('/v1/roomRolePermissions',
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
(rq, rs) => {
|
||||
(_rq, rs) => {
|
||||
rs.sendStatus(200);
|
||||
},
|
||||
|
||||
);
|
||||
|
||||
route.router.get('/v1/agRoomIds',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
async (_rq, rs) => {
|
||||
|
||||
const rooms = await Rooms.getAllBuiltinRoomGenerations();
|
||||
rs.json(rooms.map(det => det.Room.RoomId));
|
||||
|
||||
},
|
||||
|
||||
);
|
||||
|
||||
const CloneRoomSchema = z.object({
|
||||
Name: z.string(),
|
||||
RoomId: z.number()
|
||||
});
|
||||
interface CloneRoomBody {
|
||||
Name: string,
|
||||
RoomId: number
|
||||
}
|
||||
|
||||
route.router.post('/v1/clone',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
express.json(),
|
||||
APIUtils.validateRequestBody(CloneRoomSchema),
|
||||
|
||||
async (rq: express.Request<NoBody, NoBody, CloneRoomBody>, rs: express.Response) => {
|
||||
|
||||
const room = await Rooms.cloneRoom(rq.body.RoomId, rq.body.Name, rs.locals.profile);
|
||||
|
||||
const masterRoomFactory = await new RoomFactory({ id: rq.body.RoomId }).init();
|
||||
|
||||
rs.json({
|
||||
Result: room.result,
|
||||
RoomDetails: room.result == RoomDataTypes.CreateModifyRoomStatus.Success ? await room.factory?.export() : await masterRoomFactory?.export()
|
||||
});
|
||||
|
||||
if (
|
||||
room.result == RoomDataTypes.CreateModifyRoomStatus.Success
|
||||
&& room.factory
|
||||
) rs.locals.profile.Rooms.addOwnedRoomId(room.factory.RoomId);
|
||||
|
||||
},
|
||||
|
||||
);
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import express from "express";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { CurrencyType, StorefrontBalanceType } from "../../data/content/storefronts.ts";
|
||||
import { StorefrontBalanceType } from "../../data/content/storefronts.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/storefronts');
|
||||
|
||||
|
||||
@@ -16,11 +16,12 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import Matchmaking from "../../data/live/base.ts";
|
||||
import { MatchmakingErrorCode } from "../../data/live/types.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import express from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
const log = new Logging("MatchGotoRoute");
|
||||
|
||||
@@ -31,13 +32,31 @@ interface MatchmakingParams {
|
||||
subRoomName?: string
|
||||
}
|
||||
|
||||
const ProperCaseBooleanSchema = z.preprocess((val) => {
|
||||
if (val === "True") return true;
|
||||
if (val === "False") return false;
|
||||
if (typeof val === "boolean") return val; // allow raw booleans too
|
||||
return val; // will fail validation
|
||||
}, z.boolean());
|
||||
|
||||
interface MatchmakingOptions {
|
||||
CreatePrivateInstance?: string,
|
||||
BypassMovementModeRestriction?: string
|
||||
}
|
||||
|
||||
route.router.post('/room/:roomName',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
APIUtils.startTimer,
|
||||
express.urlencoded({ extended: true }),
|
||||
APIUtils.validateRequestBody(z.object({
|
||||
CreatePrivateInstance: ProperCaseBooleanSchema.optional(),
|
||||
BypassMovementModeRestriction: ProperCaseBooleanSchema.optional()
|
||||
})),
|
||||
|
||||
async (rq: express.Request<MatchmakingParams>, rs: express.Response, nxt: express.NextFunction) => {
|
||||
async (rq: express.Request<MatchmakingParams, NoBody, MatchmakingOptions>, rs: express.Response, nxt: express.NextFunction) => {
|
||||
log.d(`Player ${rs.locals.profile.getId()} is requesting to matchmake\n Room: '${rq.params.roomName}'\n Body: ${JSON.stringify(rq.body)}`);
|
||||
if (!rq.params.roomName) {
|
||||
log.d('Matchmake failed: No room specified');
|
||||
rs.json({
|
||||
@@ -45,7 +64,11 @@ route.router.post('/room/:roomName',
|
||||
});
|
||||
return;
|
||||
}
|
||||
rs.json(await Matchmaking.matchmake({ profile: rs.locals.profile, roomName: rq.params.roomName }));
|
||||
rs.json(await Matchmaking.matchmake({
|
||||
profile: rs.locals.profile,
|
||||
roomName: rq.params.roomName,
|
||||
private: rq.body.CreatePrivateInstance ? rq.body.CreatePrivateInstance == 'True' : undefined
|
||||
}));
|
||||
nxt();
|
||||
},
|
||||
|
||||
@@ -57,8 +80,10 @@ route.router.post('/room/:roomName/:subRoomName',
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
APIUtils.startTimer,
|
||||
express.urlencoded({ extended: true }),
|
||||
|
||||
async (rq: express.Request<MatchmakingParams>, rs: express.Response, nxt: express.NextFunction) => {
|
||||
async (rq: express.Request<MatchmakingParams, NoBody, MatchmakingOptions>, rs: express.Response, nxt: express.NextFunction) => {
|
||||
log.d(`Player ${rs.locals.profile.getId()} is requesting to matchmake\n Room: '${rq.params.roomName}'\n Subroom: '${rq.params.subRoomName}'\n Body: ${JSON.stringify(rq.body)}`);
|
||||
if (!rq.params.roomName) {
|
||||
log.d('Matchmake failed: No room specified');
|
||||
rs.json({
|
||||
@@ -66,7 +91,12 @@ route.router.post('/room/:roomName/:subRoomName',
|
||||
});
|
||||
return;
|
||||
}
|
||||
rs.json(await Matchmaking.matchmake({ profile: rs.locals.profile, roomName: rq.params.roomName, subRoomName: rq.params.subRoomName }));
|
||||
rs.json(await Matchmaking.matchmake({
|
||||
profile: rs.locals.profile,
|
||||
roomName: rq.params.roomName,
|
||||
subRoomName: rq.params.subRoomName,
|
||||
private: rq.body.CreatePrivateInstance ? rq.body.CreatePrivateInstance == 'True' : undefined
|
||||
}));
|
||||
nxt();
|
||||
},
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import Logging from "@proxnet/undead-logging";
|
||||
import UnifiedProfile from "../../data/profiles.ts";
|
||||
import { PlayerStatusVisibility, VRMovementMode } from "../../data/live/types.ts";
|
||||
import { SettingKey } from "../../data/content/settings.ts";
|
||||
import Instances from "../../data/live/instances.ts";
|
||||
|
||||
const log = new Logging("MatchPlayerRoute");
|
||||
|
||||
@@ -86,7 +85,7 @@ route.router.post('/logout',
|
||||
APIUtils.validateRequestBody(LoginSchema),
|
||||
|
||||
(_rq, rs) => {
|
||||
Instances.removePlayerFromCurrentInstance(rs.locals.profile);
|
||||
rs.locals.profile.getInstance()?.removePlayer(rs.locals.profile);
|
||||
rs.sendStatus(200);
|
||||
}
|
||||
|
||||
@@ -129,10 +128,10 @@ route.router.put('/statusvisibility',
|
||||
);
|
||||
|
||||
interface VRMovementModeBody {
|
||||
vrMovementMode: PlayerStatusVisibility
|
||||
vrMovementMode: VRMovementMode
|
||||
}
|
||||
const VRMovementModeSchema = z.object({
|
||||
vrMovementMode: z.nativeEnum(VRMovementMode)
|
||||
vrMovementMode: z.enum(Object.values(VRMovementMode).map(String) as [string, ...string[]])
|
||||
});
|
||||
|
||||
route.router.put('/vrmovementmode',
|
||||
@@ -155,7 +154,7 @@ route.router.put('/photonregionpings',
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
(rq, rs) => {
|
||||
(_rq, rs) => {
|
||||
rs.sendStatus(200);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ route.router.get('/:id/instances',
|
||||
roomId: parsedId,
|
||||
subRoomId: instance.subRoomId,
|
||||
isFull: instance.isFull,
|
||||
createdAt: new Date().toISOString(), // TODO: rewrite instance - create instance class rather than using sets - include datetime when instance was created
|
||||
playerIds: Instances.getInstancePlayers(instance).values().toArray().map(profile => profile.getId())
|
||||
createdAt: instance.timeCreated,
|
||||
playerIds: instance.getAllPlayers().map(profile => profile.getId())
|
||||
})));
|
||||
},
|
||||
|
||||
|
||||
@@ -34,10 +34,9 @@ import {
|
||||
import { SocketTarget } from "./targets/targetbase.ts";
|
||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||
import Presence from "../data/live/presence.ts";
|
||||
import Instances from "../data/live/instances.ts";
|
||||
import Matchmaking from "../data/live/base.ts";
|
||||
|
||||
const logmessages = true;
|
||||
const logmessages = false;
|
||||
|
||||
export class SignalRSocketHandler {
|
||||
|
||||
@@ -50,6 +49,8 @@ export class SignalRSocketHandler {
|
||||
|
||||
#PeriodicalId: number;
|
||||
|
||||
#killed = false;
|
||||
|
||||
constructor(socket: WebSocket, player: Profile) {
|
||||
|
||||
this.#socket = socket;
|
||||
@@ -62,6 +63,7 @@ export class SignalRSocketHandler {
|
||||
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
|
||||
|
||||
this.#PeriodicalId = setInterval(async () => {
|
||||
if (this.#killed) return;
|
||||
if (this.#socket.readyState !== this.#socket.CLOSED) {
|
||||
const pres = await Presence.get(this.#profile);
|
||||
this.sendNotification("PresenceUpdate", await pres.export());
|
||||
@@ -69,9 +71,18 @@ export class SignalRSocketHandler {
|
||||
}
|
||||
}, 8000);
|
||||
|
||||
this.#socket.onclose = (ev) => {
|
||||
this.#log.d(`Close reason: ${ev.reason}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async #dispatchTarget<T = unknown>(target: string, args: unknown): Promise<TargetResult> {
|
||||
if (this.#killed) {
|
||||
const error = "Tried to dispatch socket target on dead socket";
|
||||
this.#log.w(error);
|
||||
return { type: TargetResultType.Failure, err: error };
|
||||
}
|
||||
const targetExec = this.#Targets.get(target);
|
||||
if (!targetExec) return { type: TargetResultType.NotATarget } as TargetResultNotATarget;
|
||||
else {
|
||||
@@ -152,6 +163,7 @@ export class SignalRSocketHandler {
|
||||
|
||||
destroy(sock: SignalRSocketHandler, internal: boolean | undefined = false) {
|
||||
return () => {
|
||||
sock.#killed = true;
|
||||
clearInterval(sock.#PeriodicalId);
|
||||
sock.sendRaw({ type: 7, error: "Socket closed" });
|
||||
if (!internal) sock.#socket.close();
|
||||
@@ -160,7 +172,7 @@ export class SignalRSocketHandler {
|
||||
|
||||
for (const target of sock.#Targets.values()) target.destroy();
|
||||
|
||||
Instances.removePlayerFromCurrentInstance(this.#profile);
|
||||
this.#profile.getInstance()?.removePlayer(this.#profile);
|
||||
Matchmaking.deleteLoginLock(this.#profile);
|
||||
}
|
||||
}
|
||||
@@ -174,13 +186,13 @@ export class SignalRSocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
sendNotification(id: PushNotificationId | string, args: object) {
|
||||
sendNotification(id: PushNotificationId | string, args?: object) {
|
||||
const msg: SignalRMessage = {
|
||||
type: SignalMessageType.Invocation,
|
||||
target: "Notification",
|
||||
arguments: [JSON.stringify({
|
||||
Id: id,
|
||||
Msg: args
|
||||
Msg: args ? args : {}
|
||||
})]
|
||||
}
|
||||
this.sendRaw(msg);
|
||||
|
||||
25
src/ver.ts
Normal file
25
src/ver.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
export async function getVersion(): Promise<'development' | 'unknown ver' | string> {
|
||||
try {
|
||||
const ver = await import('../ver.ts');
|
||||
return ver.Version;
|
||||
} catch {
|
||||
return "unknown ver"
|
||||
}
|
||||
}
|
||||
18
ver.ts
Normal file
18
ver.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
export const Version = 'development';
|
||||
Reference in New Issue
Block a user