This commit is contained in:
2025-08-30 16:01:43 -04:00
parent 391bf3d1f8
commit 2fbd09e43f
17 changed files with 382 additions and 36 deletions

View File

@@ -12,6 +12,7 @@ import { PushNotificationId } from "./server/socket/signalr/types.ts";
import { genericResponse } from "./util/api.ts";
import { getNetConfig } from "./net.ts";
import { TokenFormat, TokenType } from "./server/platforms/types.ts";
import { Context } from "node:vm";
LoggingConfiguration.resetTimeFormat = TimeFormat.Unix;
LoggingConfiguration.resetLogTiming = LogTiming.Microtask;
@@ -48,7 +49,7 @@ await routeImporter(AppRoot.app, 'src/', [
]);
// deno-lint-ignore require-await
AppRoot.app.use('*', async c => {
AppRoot.app.use('*', async (c: Context) => {
return c.json(genericResponse(false, "Resource Not Found"), 404);
});
@@ -60,7 +61,11 @@ const onListen = async () => {
await Deno.mkdir('persist');
await Promise.all(Object.values(Server).map(base => ((base as ServerContentBase).kvInit ? (base as ServerContentBase).kvInit() : undefined)));
Server.emit('server.start', undefined);
if (typeof Server == 'undefined') {
log.e(`SERVER LOCKUP: Server is undefined!`);
return;
}
Server.emit('server.start', { server: Server });
}
const netConfig = getNetConfig();
@@ -92,7 +97,7 @@ const server = Deno.serve({
const splitHeader = authHeader.split(' ')[1];
if (!splitHeader) return unauthRes;
const secret = Deno.env.get('secret');
const secret = Deno.env.get('SECRET');
if (!secret) {
log.w(`No secret set!`);
return unauthRes;
@@ -140,6 +145,7 @@ const server = Deno.serve({
formatHeader(req.headers, 'Content-Type'),
formatHeader(req.headers, 'Connection'),
formatHeader(req.headers, 'User-Agent'),
formatHeader(req.headers, 'Accept-Encoding')
]);
if (!logBlacklist.includes(url.pathname)) {
if (res.status === 404) log.e(netlog);
@@ -163,7 +169,7 @@ Deno.addSignalListener('SIGINT', () => {
for (const socket of consoleSockets) socket.destroy();
for (const socket of gameSockets) socket.sendNotification(PushNotificationId.ModerationQuitGame);
Server.emit('server.destroy', undefined);
Server.emit('server.destroy', { server: Server });
});
Server.Commands.addRootCommand(new Command({

View File

@@ -1,8 +1,8 @@
export function getNetConfig() {
return {
host: "10.0.1.39",
host: "127.0.0.1",
port: 13370,
publicHost: "10.0.1.39:13370",
publicHost: "127.0.0.1:13370",
securePublicHost: false
}
}

View File

@@ -1,3 +1,7 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/messages");
export const route = createHonoRoute("/messages");
route.app.get('/v2/get', c => {
return c.json([]);
});

View File

@@ -1,7 +1,17 @@
import z from "zod";
import { createHonoRoute } from "../../../util/import.ts";
import { authenticate } from "../../../util/api.ts";
import { typedZValidator } from "../../../util/validators.ts";
export const route = createHonoRoute("/players");
route.app.get('/v2/progression/bulk', c => {
return c.json([]); // todo: progression
});
const getProgParamSchema = z.object({
id: z.coerce.number()
});
route.app.get('/v1/progression/:id', authenticate, typedZValidator('param', getProgParamSchema), async c => {
return c.json(await c.get('profile').Reputation.export());
});

View File

@@ -1,8 +1,7 @@
import z from "zod";
import { authenticate, genericResponse } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
import { ProfileSetting } from "../../../server/profiles/content/Settings.ts";
import { typedZValidator } from "../../../util/validators.ts";
import { HonoEnv } from "../../../util/types.ts";
import { Context } from "@hono/hono";
@@ -19,7 +18,7 @@ route.app.get('/v2/', getSettingsMiddleware);
route.app.get('/v2', getSettingsMiddleware);
const settingsSetSchema = z.object({
Key: z.string().transform(transformStringToEnum<ProfileSetting>(ProfileSetting, true)),
Key: z.string(),
Value: z.string()
});
route.app.post('/v2/set', typedZValidator('json', settingsSetSchema), async c => {

View File

@@ -9,8 +9,12 @@ export class ServerContentBase {
this.server = server;
this.kv = new KV(id, kv);
server.on('server.start', this.start);
server.on('server.destroy', this.destroy);
server.on('server.start', _ev => {
this.start();
});
server.on('server.destroy', _ev => {
this.destroy();
});
}
async kvInit() {

View File

@@ -2,6 +2,14 @@ import { CloudRegionCode } from "../../util/photon.ts";
import type Profile from "../profiles/profile.ts";
import { RoomInstance, RoomLocation } from "./base.ts";
export interface InstanceCreationOptions {
roomId: number,
subRoomId: number,
name: string,
maxCapacity: number,
private?: boolean
}
export class Instance {
#createdAt = new Date();
@@ -9,40 +17,42 @@ export class Instance {
#players: Set<Profile> = new Set();
#instanceId: number;
//#roomId: number;
//#subRoomId: number;
#roomId: number;
#subRoomId: number;
#location: RoomLocation;
//#name: string;
//#maxCapacity: number;
#name: string;
#maxCapacity: number;
#isFull: boolean = false;
//#isPrivate: boolean;
#isPrivate: boolean;
#isInProgress: boolean = false;
#photonRegionId: string = CloudRegionCode.us;
//#photonRoomId: string;
#photonRoomId: string;
#dataBlob?: string;
#eventId?: string
#eventId?: number
constructor(options: {
id: number,
location: RoomLocation,
}
) {
this.#instanceId = options.id;
this.#location = options.location;
constructor(id: number, location: RoomLocation, options: InstanceCreationOptions) {
this.#instanceId = id;
this.#location = location;
this.#roomId = options.roomId;
this.#subRoomId = options.subRoomId;
this.#isPrivate = typeof options.private == 'boolean' ? options.private : false;
this.#name = options.name;
this.#maxCapacity = options.maxCapacity;
this.#photonRoomId = `GCR-${this.#instanceId}`;
}
getPlayers() {
return this.#players.values().toArray();
}
playerIsHere(profile: Profile) {
return this.getPlayers().find(prof => prof.same(profile)) ? true : false;
return Boolean(this.getPlayers().find(prof => prof.same(profile)));
}
removePlayer(profile: Profile) {
this.#players.delete(profile);
}
addPlayer(profile: Profile) {
this.#players.add(profile);
}
getCreatedAt() {
@@ -50,10 +60,22 @@ export class Instance {
}
export() {
/*const inst: RoomInstance = {
const inst: RoomInstance = {
roomInstanceId: this.#instanceId,
}*/
roomId: this.#roomId,
subRoomId: this.#subRoomId,
location: this.#location,
name: this.#name,
maxCapacity: this.#maxCapacity,
isFull: this.#isFull,
isPrivate: this.#isPrivate,
isInProgress: this.#isInProgress,
photonRegionId: this.#photonRegionId,
photonRoomId: this.#photonRoomId,
dataBlob: this.#dataBlob,
eventId: this.#eventId
};
return inst;
}
}

View File

@@ -58,6 +58,6 @@ export interface RoomInstance {
export class InstanceManager extends ServerContentBase {
}

View File

@@ -0,0 +1,5 @@
import ProfileContentManager from "./base.ts";
export class ProfileProgressionManager extends ProfileContentManager {
} // do this soon:tm:

View File

@@ -1,5 +1,33 @@
import ProfileContentManager from "./base.ts";
interface ProfileReputation {
AccountId: number,
Noteriety: number,
CheerGeneral: number,
CheerHelpful: number,
CheerGreatHost: number,
CheerSportsman: number,
CheerCreative: number,
CheerCredit: number,
SubscriberCount: number,
SubscribedCount: number,
}
export class ProfileReputationManager extends ProfileContentManager {
async export(): Promise<ProfileReputation> {
return {
AccountId: this.profile.getId(),
Noteriety: 0.0,
CheerGeneral: 0.0,
CheerHelpful: 0.0,
CheerGreatHost: 0.0,
CheerSportsman: 0.0,
CheerCreative: 0.0,
CheerCredit: 0.0,
SubscribedCount: 0,
SubscriberCount: 0
}
}
}

View File

@@ -34,7 +34,7 @@ export class ProfileSettingsManager extends ProfileContentManager {
await this.kv.getKv().set(this.#key, settings);
}
async setSetting(Key: ProfileSetting, Value: string) {
async setSetting(Key: string, Value: string) {
const settings = await this.getAllSettings();
const s = settings.find(setting => setting.Key === Key);
if (!s) settings.push({ Key, Value });

View File

@@ -93,7 +93,7 @@ class ProfileManagerBase extends ServerContentBase {
exec: async (id: number) => {
const prof = await this.get(id);
if (!prof) return prof;
else return await prof.export();
else return prof.export();
},
zod: z.tuple([
z.string().transform(Number)

26
src/server/rooms/base.ts Normal file
View File

@@ -0,0 +1,26 @@
import { ServerContentBase } from "../ContentBase.ts";
export class ServerRoomsBase extends ServerContentBase {
#roomsKey = "rooms";
#roomNamesKey = "room_names"
getKv() {
return this.kv;
}
async getIdFromName(name: string) {
const id = await this.kv.getKv().get<number>([this.#roomsKey, name]);
if (id.value == null) return null;
return id.value;
}
async getFromName(name: string) {
}
async get(id: number) {
}
}

View File

@@ -0,0 +1,178 @@
import { RoomLocation } from "../../instances/base.ts";
import { RoomFactory } from "./RoomFactory.ts";
import { SubroomFactory } from "./SubroomFactory.ts";
export enum WriteMode {
Overwrite = "overwrite",
WriteIfFree = "if_free"
}
export enum FactoryMode {
Fetch = 'fetch',
Write = 'write'
}
export type HardwareSupport = "screens" | "walk_vr" | "teleport_vr" | "low_vr" | "mobile";
export const HardwareSupportStrings = ["screens", "walk_vr", "teleport_vr", "low_vr", "mobile"];
export enum RoomState {
Active,
PendingJunior = 11,
Moderation_PendingReview = 100,
Moderation_Closed,
MarkedForDelete = 1000
}
export enum RoomAccessibility {
Private,
Public,
Unlisted
}
export interface BuiltinScene {
Name: string,
RoomSceneLocationId: RoomLocation,
IsSandbox: boolean,
CanMatchmakeInto: boolean,
SupportsJoinInProgress: boolean,
UseLevelBasedMatchmaking: boolean,
UseAgeBasedMatchmaking: boolean,
UseRecRoyaleMatchmaking: boolean,
MaxPlayers: number
}
export interface BuiltinRoom {
Name: string,
Description: string,
Accessibility: RoomAccessibility,
SupportsLevelVoting: boolean,
CloningAllowed: boolean,
SupportsScreens: boolean,
SupportsWalkVR: boolean,
SupportsTeleportVR: boolean,
Scenes: BuiltinScene[]
}
export interface RoomScene {
RoomSceneId: number,
RoomId: number,
RoomSceneLocationId: string,
Name: string,
IsSandbox: boolean,
DataBlobName: string,
MaxPlayers: number,
CanMatchmakeInto?: boolean,
DataModifiedAt: string
}
export interface Room {
RoomId: number,
Name: string,
Description: string,
CreatorPlayerId: number,
ImageName: string,
State: RoomState,
Accessibility: RoomAccessibility,
SupportsLevelVoting: boolean,
IsAGRoom: boolean,
IsDormRoom?: boolean,
CloningAllowed: boolean,
SupportsScreens: boolean,
SupportsWalkVR: boolean,
SupportsTeleportVR: boolean,
AllowsJuniors: boolean,
RoomWarningMask: number,
CustomRoomWarning: string,
DisableMicAutoMute?: boolean | null
}
export enum RoomWarningMask {
None,
Scary,
Mature = 2,
FlashingLights = 4,
IntenseMotion = 8,
Violence = 16,
Custom = 32
}
export enum TagType {
General,
Auto,
AGOnly,
Banned
}
export interface TagDTO {
Tag: string,
Type: TagType
}
export interface RoomDetails {
Room: Room,
Scenes: RoomScene[],
CoOwners: number[],
InvitedCoOwners: number[],
Moderators?: number[],
InvitedModerators?: number[],
Hosts: number[],
InvitedHosts: number[],
CheerCount: number,
FavoriteCount: number,
VisitCount: number,
Tags: TagDTO[]
}
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 interface DatabaseRoom {
Visits: number,
Hardware: Set<HardwareSupport>,
Subrooms: number[],
Tags: Set<string>,
CoOwners: Set<number>,
InvitedCoOwners: Set<number>,
Hosts: Set<number>,
InvitedHosts: Set<number>,
Favorites: Set<number>,
Cheers: Set<number>
}
export interface DatabaseSubroom {
}
export interface RoomUpdatedEvent {
room: RoomFactory
}
export interface SubroomUpdatedEvent {
subroom: SubroomFactory
}
export * as RoomDataTypes from "./DataTypes.ts";

View File

@@ -0,0 +1,53 @@
import type KV from "../../persistence/kv.ts";
import { type ServerBase } from "../../server.ts";
import { DatabaseRoom, RoomDataTypes } from "./DataTypes.ts";
export interface RoomFactoryOptions {
id?: number,
name?: string,
}
export class RoomFactory {
#server: ServerBase;
#kv: KV;
#roomId: number | undefined;
#name: string | undefined;
#description: string | undefined;
#creatorPlayerId: number | undefined;
#imageName: string | undefined;
#state: RoomDataTypes.RoomScene | undefined;
#accessibility: RoomDataTypes.RoomAccessibility | undefined;
#supportsLevelVoting: boolean | undefined;
#isAGRoom: boolean | undefined;
#isDormRoom: boolean | undefined;
#cloningAllowed: boolean | undefined;
#supportsScreens: boolean | undefined;
#supportsWalkVR: boolean | undefined;
#supportsTeleportVR: boolean | undefined;
#allowsJuniors: boolean | undefined;
#roomWarningMask: number | undefined;
#customRoomWarning: string | undefined;
#disableMicAutoMute: boolean | undefined;
#dbMeta: DatabaseRoom | null = null;
#visits: number | undefined;
#hardware
constructor(server: ServerBase, kv: KV) {
this.#server = server;
this.#kv = kv;
}
async init(options: RoomFactoryOptions) {
if (typeof options.id == 'undefined' && typeof options.name == 'undefined')
throw new Error("Must specify a room ID or a room name");
}
}

View File

@@ -1,3 +1,4 @@
import { ServerUpdateEvent } from "../serverevents.ts";
import { AvatarContentBase } from "./avatars/base.ts";
import { EventManager } from "./baseevent.ts";
import { CommandsBase } from "./commands/commands.ts";
@@ -9,13 +10,17 @@ import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.
import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts";
import { type RoleUpdateEvent } from "./profiles/events/RoleUpdate.ts";
import ProfileManagerBase from "./profiles/manager.ts";
import { ServerRoomsBase } from "./rooms/base.ts";
import { RoomUpdatedEvent, SubroomUpdatedEvent } from "./rooms/internal/DataTypes.ts";
interface ServerEvents {
'profile.roleupdate': RoleUpdateEvent,
'profile.update': ProfileUpdateEvent,
'presence.update': PresenceUpdateEvent,
'server.start': undefined,
'server.destroy': undefined,
'server.start': ServerUpdateEvent,
'server.destroy': ServerUpdateEvent,
'room.updated': RoomUpdatedEvent,
'room.subroom.updated': SubroomUpdatedEvent
}
class ServerBase extends EventManager<ServerEvents> {
@@ -26,6 +31,7 @@ class ServerBase extends EventManager<ServerEvents> {
Avatars = new AvatarContentBase(this, 'avatars');
Instances = new InstanceManager(this, 'instances');
Content = new ServerContentManager(this, "content");
Rooms = new ServerRoomsBase(this, 'rooms', true);
}
const Server = new ServerBase();

5
src/serverevents.ts Normal file
View File

@@ -0,0 +1,5 @@
import { ServerBase } from "./server/server.ts";
export interface ServerUpdateEvent {
server: ServerBase
}