From bcee414004b7829983e9c98e881067572ea2fa2b Mon Sep 17 00:00:00 2001 From: zombieb Date: Wed, 2 Apr 2025 14:10:01 -0400 Subject: [PATCH] Rooms and matchmaking debugging * Dorm is not properly cloned (fix this) * Basic matchmaking implementation * Goto route * Goto DormRoom --- src/data/content/rooms.ts | 205 ++++++++++++++++++++++++++-------- src/data/content/roomtypes.ts | 24 ++-- src/data/live/base.ts | 67 +++++++---- src/data/live/instances.ts | 4 - src/db.ts | 1 + src/routes/match/goto.ts | 22 +++- 6 files changed, 237 insertions(+), 86 deletions(-) diff --git a/src/data/content/rooms.ts b/src/data/content/rooms.ts index 174d678..ed097d9 100644 --- a/src/data/content/rooms.ts +++ b/src/data/content/rooms.ts @@ -15,11 +15,14 @@ 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 . */ -import { it } from "node:test"; import { Redis } from "../../db.ts"; import { RootPath } from "./baseimages.ts"; -import { BuiltinRoom, RoomState } from "./roomtypes.ts"; +import { BuiltinRoom, RoomDetails, RoomState } from "./roomtypes.ts"; import { RoomFetch } from "./room.ts"; +import { Profile } from "../profiles.ts"; +import Logging from "@proxnet/undead-logging"; + +const log = new Logging("Rooms"); const rooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as BuiltinRoom[]; @@ -119,58 +122,166 @@ class RoomsBase { return id; } + async #setRoom(details: RoomDetails) { + const rootKey = Redis.buildKey(Redis.KeyGroups.Rooms.Root, details.Room.RoomId.toString()); + + const roomMetaRootKey = Redis.buildKey(rootKey, this.roomRootKeys.Meta); + await Promise.all([ + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomId, details.Room.RoomId), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Name, details.Room.Name), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Description, details.Room.Description), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CreatorPlayerId, details.Room.CreatorPlayerId), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.ImageName, `DefaultProfileImage.png`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.State, details.Room.State), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Accessibility, details.Room.Accessibility), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsLevelVoting, `${details.Room.SupportsLevelVoting}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsAGRoom, `${details.Room.IsAGRoom}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsDormRoom, `${details.Room.IsDormRoom}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CloningAllowed, `${details.Room.CloningAllowed}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsScreens, `${details.Room.SupportsScreens}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsWalkVR, `${details.Room.SupportsWalkVR}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsTeleportVR, `${details.Room.SupportsTeleportVR}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.AllowsJuniors, `${details.Room.AllowsJuniors}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomWarningMask, details.Room.RoomWarningMask), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CustomRoomWarning, details.Room.CustomRoomWarning), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.DisableMicAutoMute, `${details.Room.DisableMicAutoMute}`), + ]); + + for (const subroom of details.Scenes) { + const newSubId = await this.#getAvailableSubRoomId(details.Room.RoomId); + const subRootMetaKey = Redis.buildKey( + Redis.KeyGroups.Rooms.Root, + details.Room.RoomId.toString(), + this.roomRootKeys.Subrooms, + newSubId.toString(), + this.subroomRootKeys.Meta + ); + + await Promise.all([ + Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.RoomSceneLocationId, subroom.RoomSceneLocationId), + Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.Name, subroom.RoomSceneLocationId), + Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.IsSandbox, `${subroom.IsSandbox}`), + Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataBlobName, subroom.DataBlobName), + Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.MaxPlayers, subroom.MaxPlayers), + Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.CanMatchmakeInto, `${subroom.CanMatchmakeInto}`), + Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataModifiedAt, new Date().toISOString()), + ]); + await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, details.Room.RoomId.toString(), this.roomRootKeys.Subrooms), newSubId); + } + } + + + + async cloneRoom(roomid: number, newname: string, newowner: Profile, dorm?: boolean) { + const canBeClonedRaw = await Redis.Database.hget(Redis.buildKey( + Redis.KeyGroups.Rooms.Root, + roomid.toString(), + Rooms.roomRootKeys.Meta + ), Rooms.roomMetaKeys.CloningAllowed); + if (!canBeClonedRaw) return null; + let canBeCloned = null; + try { + canBeCloned = JSON.parse(canBeClonedRaw) as boolean; + } catch { + return null; + } + if (!canBeCloned) return null; + const beforeRoom = await Rooms.get(roomid); // room must exist + if (!beforeRoom || !beforeRoom.Room.CloningAllowed) return null; // room must be cloneable + if (await Rooms.getByName(newname)) return null; // room name cannot be taken + + const newId = await this.#getAvailableRoomId(); + beforeRoom.Room.CreatorPlayerId = newowner.getId(); + beforeRoom.Room.RoomId = newId; + for (const subroom of beforeRoom.Scenes) subroom.RoomId = newId; + if (dorm) { + beforeRoom.Room.IsAGRoom = true; + beforeRoom.Room.IsDormRoom = true; + } + + await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, newname), newId); + await Rooms.#setRoom(beforeRoom); + return beforeRoom; + + } + + async getProfileDormDefault(player: Profile) { + const unparsedId = await Redis.Database.hget(Redis.buildKey( + Redis.KeyGroups.Rooms.Root, + Redis.KeyGroups.Rooms.PlayerDorms + ), player.getId().toString()); + if (unparsedId) { + const parsedId = parseInt(unparsedId); + if (!isNaN(parsedId)) return await Rooms.get(parsedId); + } + + const baseDorm = await Rooms.getByName("DormRoom"); + + log.d('got base dorm'); + if (!baseDorm) return null; + log.d('base dorm is not null'); + const newDorm = await this.cloneRoom(baseDorm.Room.RoomId, "DormRoom", player, true); + await Redis.Database.hset(Redis.buildKey( + Redis.KeyGroups.Rooms.Root, + Redis.KeyGroups.Rooms.PlayerDorms + ), player.getId().toString(), baseDorm.Room.RoomId); + log.d('got new dorm'); + if (!newDorm) return null; + log.d('new dorm is not null'); + return newDorm; + } + async generateBuiltinRooms() { if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated))) !== null) return true; for (const builtinRoom of rooms) { const newId = await this.#getAvailableRoomId(); await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.AGRooms), newId); - await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, builtinRoom.Name), newId); - const rootKey = Redis.buildKey(Redis.KeyGroups.Rooms.Root, newId.toString()); - - const roomMetaRootKey = Redis.buildKey(rootKey, this.roomRootKeys.Meta); - await Promise.all([ - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomId, newId), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Name, builtinRoom.Name), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Description, builtinRoom.Description), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CreatorPlayerId, `1`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.ImageName, `${builtinRoom.Name}.png`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.State, `${RoomState.Active}`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Accessibility, `${builtinRoom.Accessibility}`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsLevelVoting, `${builtinRoom.SupportsLevelVoting}`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsAGRoom, "true"), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsDormRoom, "false"), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CloningAllowed, `${builtinRoom.CloningAllowed}`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsScreens, `${builtinRoom.SupportsScreens}`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsWalkVR, `${builtinRoom.SupportsWalkVR}`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsTeleportVR, `${builtinRoom.SupportsTeleportVR}`), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.AllowsJuniors, "true"), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomWarningMask, 0), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CustomRoomWarning, ""), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.DisableMicAutoMute, "false"), - ]); - - for (const subroom of builtinRoom.Scenes) { - const newSubId = await this.#getAvailableSubRoomId(newId); - const subRootMetaKey = Redis.buildKey( - Redis.KeyGroups.Rooms.Root, - newId.toString(), - this.roomRootKeys.Subrooms, - newSubId.toString(), - this.subroomRootKeys.Meta - ); - - await Promise.all([ - Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.RoomSceneLocationId, subroom.RoomSceneLocationId), - Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.Name, subroom.RoomSceneLocationId), - Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.IsSandbox, `${subroom.IsSandbox}`), - Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataBlobName, ""), - Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.MaxPlayers, subroom.MaxPlayers), - Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.CanMatchmakeInto, `${subroom.CanMatchmakeInto}`), - Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataModifiedAt, new Date().toISOString()), - ]); - await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, newId.toString(), this.roomRootKeys.Subrooms), newSubId); + const roomDets: RoomDetails = { + Room: { + Name: builtinRoom.Name, + RoomId: newId, + Description: builtinRoom.Description, + CreatorPlayerId: 1, + ImageName: `${builtinRoom.Name}.png`, + State: RoomState.Active, + Accessibility: builtinRoom.Accessibility, + SupportsLevelVoting: builtinRoom.SupportsLevelVoting, + IsAGRoom: true, + IsDormRoom: builtinRoom.Name == 'DormRoom', + CloningAllowed: builtinRoom.CloningAllowed, + SupportsScreens: builtinRoom.SupportsScreens, + SupportsWalkVR: builtinRoom.SupportsWalkVR, + SupportsTeleportVR: builtinRoom.SupportsTeleportVR, + AllowsJuniors: true, + RoomWarningMask: 0, + CustomRoomWarning: "" + }, + Scenes: [], + CoOwners: [], + InvitedCoOwners: [], + Hosts: [], + InvitedHosts: [], + CheerCount: 0, + FavoriteCount: 0, + VisitCount: 0, + Tags: [] } + for (const subroom of builtinRoom.Scenes) { + const newSubroomId = await this.#getAvailableSubRoomId(newId); + roomDets.Scenes.push({ + RoomSceneId: newSubroomId, + RoomId: newId, + RoomSceneLocationId: subroom.RoomSceneLocationId, + Name: subroom.Name, + IsSandbox: subroom.IsSandbox, + DataBlobName: "", + MaxPlayers: subroom.MaxPlayers, + CanMatchmakeInto: subroom.CanMatchmakeInto ? subroom.CanMatchmakeInto : true, + DataModifiedAt: new Date().toISOString() + }); + } + await this.#setRoom(roomDets); } Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated), "1"); return false; diff --git a/src/data/content/roomtypes.ts b/src/data/content/roomtypes.ts index a2c75c2..854670a 100644 --- a/src/data/content/roomtypes.ts +++ b/src/data/content/roomtypes.ts @@ -101,6 +101,18 @@ export interface BuiltinRoom { 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, @@ -144,18 +156,6 @@ export interface TagDTO { Type: TagType } -export interface RoomScene { - RoomSceneId: number, - RoomId: number, - RoomSceneLocationId: string, - Name: string, - IsSandbox: boolean, - DataBlobName: string, - MaxPlayers: number, - CanMatchmakeInto?: boolean, - DataModifiedAt: string -} - export interface RoomDetails { Room: Room, Scenes: RoomScene[], diff --git a/src/data/live/base.ts b/src/data/live/base.ts index e197fbf..1cc4efd 100644 --- a/src/data/live/base.ts +++ b/src/data/live/base.ts @@ -15,6 +15,8 @@ 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 . */ +import Rooms from "../content/rooms.ts"; +import { RoomAccessibility, RoomState } from "../content/roomtypes.ts"; import { Profile } from "../profiles.ts"; import Instances from "./instances.ts"; import { MatchmakingErrorCode, RoomInstance } from "./types.ts"; @@ -25,7 +27,7 @@ interface MatchmakingOptions { roomName: string, subRoomName?: string, private?: boolean, - instanceId?: string, + instanceId?: number, profile: Profile } @@ -51,30 +53,55 @@ class MatchmakingBase { loginLocks.delete(prof.getId()); } - matchmake(options: MatchmakingOptions) { + async matchmake(options: MatchmakingOptions) { if (options.instanceId) { - // get instance - // if instance exists, check private - - } - /* - if roomname is dormroom, create the profile's dormroom if it does not exist - set the target room to the profile's dormroom id + const instance = Instances.getInstance(options.instanceId); + if (instance) { - get all public instances for roomname - filter out private and full instances - if a subroomname was specified, filter out subrooms that are not that subroom + if (Instances.playerIsInInstance(options.profile, instance)) return { errorCode: MatchmakingErrorCode.AlreadyInTargetInstance }; + else { + if (instance.isFull) return { errorCode: MatchmakingErrorCode.InsufficientSpace } + else if (instance.isPrivate) return { errorCode: MatchmakingErrorCode.RoomInstanceIsPrivate }; + + Instances.setPlayerInstance(options.profile, instance); + return { errorCode: MatchmakingErrorCode.Success, roomInstance: instance }; + } + + } else return { errorCode: MatchmakingErrorCode.NoSuchGame } + + } else { + + const targetRoom = options.roomName !== 'DormRoom' ? await Rooms.getByName(options.roomName) : await Rooms.getProfileDormDefault(options.profile); + if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom }; + if (targetRoom.Room.Accessibility == RoomAccessibility.Private) return { errorCode: MatchmakingErrorCode.RoomIsPrivate }; + if (targetRoom.Room.State !== RoomState.Active) return { errorCode: MatchmakingErrorCode.RoomIsNotActive }; + const roomId = targetRoom.Room.RoomId; + + let allInstances = Instances.getAllRoomInstances(roomId).values().toArray().filter(instance => !instance.isPrivate && !instance.isFull); + const subroomId = targetRoom.Scenes.find(scene => scene.Name == options.subRoomName)?.RoomSceneId; + if (subroomId) allInstances = allInstances.filter(instance => instance.subRoomId == subroomId); + + const foundInstance = allInstances[Math.floor(Math.random() * allInstances.length)]; + if (!foundInstance) { + + const matchmakeableSubrooms = targetRoom.Scenes.filter(scene => scene.CanMatchmakeInto); + const newInstance = Instances.createInstance({ + Room: targetRoom, + SceneIndex: Math.floor(Math.random() * matchmakeableSubrooms.length), + FirstPlayer: options.profile + }); + return { errorCode: MatchmakingErrorCode.Success, roomInstance: newInstance }; + + } else { + + Instances.setPlayerInstance(options.profile, foundInstance); + return { errorCode: MatchmakingErrorCode.Success, roomInstance: foundInstance }; + + } - pick an instance of the given instances at random - if none exist, create one - use only matchmakeable subrooms - if none are matchmakeable, go to "Home" - if "Home" does not exist, go to the first index - - go to that instance - */ + } } diff --git a/src/data/live/instances.ts b/src/data/live/instances.ts index 8eeb54d..96b3bab 100644 --- a/src/data/live/instances.ts +++ b/src/data/live/instances.ts @@ -29,10 +29,6 @@ const instancePlayers: Map> = new Map(); * `Map` */ const instanceMap: Map> = new Map(); -/** - * `Map` - */ -const instanceRoomMap: Map = new Map(); class InstancesBase { diff --git a/src/db.ts b/src/db.ts index 5bf661e..be5d084 100644 --- a/src/db.ts +++ b/src/db.ts @@ -106,6 +106,7 @@ export const KeyGroups = { Room_Names: "room-names", Rooms: { Root: "room", + PlayerDorms: "player-dormids" }, Operators: "operators", Users: { diff --git a/src/routes/match/goto.ts b/src/routes/match/goto.ts index 534e9a7..030288c 100644 --- a/src/routes/match/goto.ts +++ b/src/routes/match/goto.ts @@ -33,14 +33,30 @@ route.router.post('/room/:roomName', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), - (rq: express.Request, rs: express.Response) => { + async (rq: express.Request, rs: express.Response) => { if (!rq.params.roomName) { rs.json({ errorCode: MatchmakingErrorCode.NoSuchRoom }); return; } - rs.json(Matchmaking.matchmake({ profile: rs.locals.profile, roomName: rq.params.roomName })); + rs.json(await Matchmaking.matchmake({ profile: rs.locals.profile, roomName: rq.params.roomName })); }, -); \ No newline at end of file +); +route.router.post('/room/:roomName/:subRoomName', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + async (rq: express.Request, rs: express.Response) => { + if (!rq.params.roomName) { + rs.json({ + errorCode: MatchmakingErrorCode.NoSuchRoom + }); + return; + } + rs.json(await Matchmaking.matchmake({ profile: rs.locals.profile, roomName: rq.params.roomName, subRoomName: rq.params.subRoomName })); + }, + +) \ No newline at end of file