Rooms and matchmaking debugging

* Dorm is not properly cloned (fix this)
* Basic matchmaking implementation
* Goto route
* Goto DormRoom
This commit is contained in:
2025-04-02 14:10:01 -04:00
parent 27b3754330
commit bcee414004
6 changed files with 237 additions and 86 deletions

View File

@@ -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 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/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { it } from "node:test";
import { Redis } from "../../db.ts"; import { Redis } from "../../db.ts";
import { RootPath } from "./baseimages.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 { 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[]; const rooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as BuiltinRoom[];
@@ -119,58 +122,166 @@ class RoomsBase {
return id; 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() { async generateBuiltinRooms() {
if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated))) !== null) return true; if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated))) !== null) return true;
for (const builtinRoom of rooms) { for (const builtinRoom of rooms) {
const newId = await this.#getAvailableRoomId(); const newId = await this.#getAvailableRoomId();
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.AGRooms), newId); 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 roomDets: RoomDetails = {
const rootKey = Redis.buildKey(Redis.KeyGroups.Rooms.Root, newId.toString()); Room: {
Name: builtinRoom.Name,
const roomMetaRootKey = Redis.buildKey(rootKey, this.roomRootKeys.Meta); RoomId: newId,
await Promise.all([ Description: builtinRoom.Description,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomId, newId), CreatorPlayerId: 1,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Name, builtinRoom.Name), ImageName: `${builtinRoom.Name}.png`,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Description, builtinRoom.Description), State: RoomState.Active,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CreatorPlayerId, `1`), Accessibility: builtinRoom.Accessibility,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.ImageName, `${builtinRoom.Name}.png`), SupportsLevelVoting: builtinRoom.SupportsLevelVoting,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.State, `${RoomState.Active}`), IsAGRoom: true,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Accessibility, `${builtinRoom.Accessibility}`), IsDormRoom: builtinRoom.Name == 'DormRoom',
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsLevelVoting, `${builtinRoom.SupportsLevelVoting}`), CloningAllowed: builtinRoom.CloningAllowed,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsAGRoom, "true"), SupportsScreens: builtinRoom.SupportsScreens,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsDormRoom, "false"), SupportsWalkVR: builtinRoom.SupportsWalkVR,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CloningAllowed, `${builtinRoom.CloningAllowed}`), SupportsTeleportVR: builtinRoom.SupportsTeleportVR,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsScreens, `${builtinRoom.SupportsScreens}`), AllowsJuniors: true,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsWalkVR, `${builtinRoom.SupportsWalkVR}`), RoomWarningMask: 0,
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsTeleportVR, `${builtinRoom.SupportsTeleportVR}`), CustomRoomWarning: ""
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.AllowsJuniors, "true"), },
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomWarningMask, 0), Scenes: [],
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CustomRoomWarning, ""), CoOwners: [],
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.DisableMicAutoMute, "false"), InvitedCoOwners: [],
]); Hosts: [],
InvitedHosts: [],
for (const subroom of builtinRoom.Scenes) { CheerCount: 0,
const newSubId = await this.#getAvailableSubRoomId(newId); FavoriteCount: 0,
const subRootMetaKey = Redis.buildKey( VisitCount: 0,
Redis.KeyGroups.Rooms.Root, Tags: []
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);
} }
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"); Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated), "1");
return false; return false;

View File

@@ -101,6 +101,18 @@ export interface BuiltinRoom {
Scenes: BuiltinScene[] 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 { export interface Room {
RoomId: number, RoomId: number,
Name: string, Name: string,
@@ -144,18 +156,6 @@ export interface TagDTO {
Type: TagType 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 { export interface RoomDetails {
Room: Room, Room: Room,
Scenes: RoomScene[], Scenes: RoomScene[],

View File

@@ -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 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/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import Rooms from "../content/rooms.ts";
import { RoomAccessibility, RoomState } from "../content/roomtypes.ts";
import { Profile } from "../profiles.ts"; import { Profile } from "../profiles.ts";
import Instances from "./instances.ts"; import Instances from "./instances.ts";
import { MatchmakingErrorCode, RoomInstance } from "./types.ts"; import { MatchmakingErrorCode, RoomInstance } from "./types.ts";
@@ -25,7 +27,7 @@ interface MatchmakingOptions {
roomName: string, roomName: string,
subRoomName?: string, subRoomName?: string,
private?: boolean, private?: boolean,
instanceId?: string, instanceId?: number,
profile: Profile profile: Profile
} }
@@ -51,30 +53,55 @@ class MatchmakingBase {
loginLocks.delete(prof.getId()); loginLocks.delete(prof.getId());
} }
matchmake(options: MatchmakingOptions) { async matchmake(options: MatchmakingOptions) {
if (options.instanceId) { if (options.instanceId) {
// get instance const instance = Instances.getInstance(options.instanceId);
// if instance exists, check private if (instance) {
}
/*
if roomname is dormroom, create the profile's dormroom if it does not exist
set the target room to the profile's dormroom id
get all public instances for roomname if (Instances.playerIsInInstance(options.profile, instance)) return { errorCode: MatchmakingErrorCode.AlreadyInTargetInstance };
filter out private and full instances else {
if a subroomname was specified, filter out subrooms that are not that subroom 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
*/
} }

View File

@@ -29,10 +29,6 @@ const instancePlayers: Map<RoomInstance, Set<Profile>> = new Map();
* `Map<roomId (number), RoomInstance>` * `Map<roomId (number), RoomInstance>`
*/ */
const instanceMap: Map<number, Set<RoomInstance>> = new Map(); const instanceMap: Map<number, Set<RoomInstance>> = new Map();
/**
* `Map<instanceId (number), roomId (number)>`
*/
const instanceRoomMap: Map<number, number> = new Map();
class InstancesBase { class InstancesBase {

View File

@@ -106,6 +106,7 @@ export const KeyGroups = {
Room_Names: "room-names", Room_Names: "room-names",
Rooms: { Rooms: {
Root: "room", Root: "room",
PlayerDorms: "player-dormids"
}, },
Operators: "operators", Operators: "operators",
Users: { Users: {

View File

@@ -33,14 +33,30 @@ route.router.post('/room/:roomName',
APIUtils.Authentication, APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game), APIUtils.AuthenticationType(AuthType.Game),
(rq: express.Request<MatchmakingParams>, rs: express.Response) => { async (rq: express.Request<MatchmakingParams>, rs: express.Response) => {
if (!rq.params.roomName) { if (!rq.params.roomName) {
rs.json({ rs.json({
errorCode: MatchmakingErrorCode.NoSuchRoom errorCode: MatchmakingErrorCode.NoSuchRoom
}); });
return; 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 }));
}, },
); );
route.router.post('/room/:roomName/:subRoomName',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
async (rq: express.Request<MatchmakingParams>, 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 }));
},
)