Skip to content
This repository has been archived by the owner on Sep 17, 2024. It is now read-only.

Commit

Permalink
feat(lobbies): create lobbies module
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanFlurry committed Jul 2, 2024
1 parent 6fef75d commit ee6bab4
Show file tree
Hide file tree
Showing 18 changed files with 1,507 additions and 49 deletions.
578 changes: 578 additions & 0 deletions modules/lobbies/actors/lobby_manager.ts

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions modules/lobbies/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface Config {
lobbies: LobbyConfig,
lobbyRules: LobbyRule[],
players: {
maxPerIp?: number;
maxUnconnected?: number;
unconnectedExpireAfter: number;
autoDestroyAfter?: number;
}
}

export interface LobbyRule {
tags: Record<string, string>,
config: Partial<LobbyConfig>,
}

export interface LobbyConfig extends Record<PropertyKey, unknown> {
destroyOnEmptyAfter?: number | null;
unreadyExpireAfter: number;
maxPlayers: number;
maxPlayersDirect: number;
enableDynamicMaxPlayers?: PlayerRange,
enableDynamicMaxPlayersDirect?: PlayerRange,
enableCreate: boolean,
enableDestroy: boolean,
enableFind: boolean,
enableFindOrCreate: boolean,
enableJoin: boolean,
enableList: boolean,
}

export interface PlayerRange {
min: number,
max: number,
}
15 changes: 15 additions & 0 deletions modules/lobbies/docs/UNCONNECTED_PLAYERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Unconnected Players

## Why it exists?

- high load & low player caps
- preventing botting

## What happens when players fail to connect?

- Unconnected players stack up
- How lobbies API handles it
- Max players per IP: if creating another player and goes over ip limit, will
delete the old unconnected player for the same IP
- Maximum unconnected players: if too many unconnected players, we'll start
discarding the oldest unconnected player
128 changes: 128 additions & 0 deletions modules/lobbies/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
{
"name": "Lobbies",
"description": "Lobby & player management.",
"icon": "game-board",
"tags": [
"core", "multiplayer"
],
"authors": [
"NathanFlurry"
],
"status": "stable",
"scripts": {
"create": {
"name": "Create Lobby",
"description": "Creates a new lobby on-demand.",
"public": true
},
"destroy": {
"name": "Destroy Lobby",
"description": "Destroys an existing lobby.",
"public": true
},
"find_or_create": {
"name": "Find Or Create Lobby",
"description": "Finds a lobby or creates one if there are no available spots for players.",
"public": true
},
"join": {
"name": "Join Lobby",
"description": "Add a player to an existing lobby.",
"public": true
},
"list": {
"name": "List Lobbies",
"description": "List & query all lobbies.",
"public": true
},
"set_lobby_ready": {
"name": "Set Lobby Ready",
"description": "Called on lobby startup after initiation to notify it can start accepting player. This should be called after operations like loading maps are complete.",
"public": true
},
"set_player_connected": {
"name": "Set Player Connected",
"description": "Called when a player connects to the lobby.",
"public": true
},
"set_player_disconnected": {
"name": "Set Player Disconnected",
"description": "Called when a player disconnects from the lobby.",
"public": true
},
"find": {
"name": "Find Lobby",
"description": "Finds an existing lobby with a given query. This will not create a new lobby, see `find_or_create` instead.",
"public": true
},
"force_gc": {
"name": "Force Garbage Collection",
"description": "Rarely used. Forces the matchmaker to purge lobbies & players."
}
},
"actors": {
"lobby_manager": {}
},
"errors": {
"lobby_not_found": {
"name": "Lobby Not Found",
"description": "Lobby not found."
},
"lobby_create_missing_players": {
"name": "Lobby Create Missing Players",
"description": "When creating a lobby with `config.lobbies.autoDestroyWhenEmpty`, a lobby must be created with players in order to avoid creating an empty lobby."
},
"lobby_full": {
"name": "Lobby Full",
"description": "No more players can join this lobby."
},
"more_players_than_max": {
"name": "More Players Than Max",
"description": "More players were passed to the create lobby than the number of max players in a lobby."
},
"lobby_already_ready": {
"name": "Lobby Already Ready",
"description": "Lobby already set as ready."
},
"player_already_connected": {
"name": "Player Already Connected",
"description": "The player has already connected to this server. This error helps mitigate botting attacks by only allowing one scoket to connect to a game server for every player."
},
"player_disconnected": {
"name": "Player Disconnected",
"description": "The player has already disconnected from the server. Create a new player for the specified lobby using the `join` script."
},
"no_matching_lobbies": {
"name": "No Matching Lobbies",
"description": "No lobbies matched the given query."
},
"too_many_players_for_ip": {
"name": "Too Many Players For IP",
"description": "The player has too many existing players for the given IP."
}
},
"dependencies": {
"tokens": {}
},
"defaultConfig": {
"lobbies": {
"destroyOnEmptyAfter": 60000,
"unreadyExpireAfter": 300000,
"maxPlayers": 16,
"maxPlayersDirect": 16,
"enableCreate": false,
"enableDestroy": false,
"enableFind": true,
"enableFindOrCreate": true,
"enableJoin": true,
"enableList": true
},
"lobbyRules": [],
"players": {
"maxPerIp": 8,
"maxUnconnected": 128,
"unconnectedExpireAfter": 60000,
"autoDestroyAfter": 4147200000
}
}
}
75 changes: 75 additions & 0 deletions modules/lobbies/scripts/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
CreateLobbyRequest,
CreateLobbyResponse,
} from "../actors/lobby_manager.ts";
import { ScriptContext } from "../module.gen.ts";
import {
Lobby,
PlayerRequest,
PlayerWithToken,
} from "../utils/types.ts";

export interface Request {
version: string;
tags: Record<string, string>;
maxPlayers: number;
maxPlayersDirect: number;

players: PlayerRequest[];
}

export interface Response {
lobby: Lobby;
players: PlayerWithToken[];
}

// TODO: Doc why we create tokens on the script and not the DO

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
// Setup lobby
//
// This token will be disposed if the lobby is not created
const lobbyId = crypto.randomUUID();
const { token: lobbyToken } = await ctx.modules.tokens.create({
type: "lobby",
meta: { lobbyId: lobbyId },
});

// Setup players
const playerOpts: PlayerRequest[] = [];
const playerTokens: Record<string, string> = {};
for (const _player of req.players) {
const playerId = crypto.randomUUID();
const { token: playerToken } = await ctx.modules.tokens.create({
type: "player",
meta: { lobbyId: lobbyId, playerId: playerId },
});
playerOpts.push({ playerId });
playerTokens[playerId] = playerToken.token;
}

const { lobby, players }: CreateLobbyResponse = await ctx.actors.lobbyManager.getOrCreateAndCall(
"default",
undefined,
"createLobby",
{
lobby: {
lobbyId,
version: req.version,
tags: req.tags,
lobbyToken: lobbyToken.token,
maxPlayers: req.maxPlayers,
maxPlayersDirect: req.maxPlayersDirect,
},
players: playerOpts,
} as CreateLobbyRequest,
);

return {
lobby,
players: players.map((x) => ({ token: playerTokens[x.id], ...x })),
};
}
23 changes: 23 additions & 0 deletions modules/lobbies/scripts/destroy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DestroyLobbyRequest } from "../actors/lobby_manager.ts";
import { ScriptContext } from "../module.gen.ts";

export interface Request {
lobbyId: string;
}

export interface Response {
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.actors.lobbyManager.getOrCreateAndCall(
"default",
undefined,
"destroyLobby",
{ lobbyId: req.lobbyId } as DestroyLobbyRequest,
);

return {};
}
53 changes: 53 additions & 0 deletions modules/lobbies/scripts/find.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
FindLobbyRequest,
FindLobbyResponse,
} from "../actors/lobby_manager.ts";
import { ScriptContext } from "../module.gen.ts";
import { Lobby, PlayerRequest, PlayerWithToken } from "../utils/types.ts";

export interface Request {
version: string;
tags: Record<string, string>;
players: PlayerRequest[];
}

export interface Response {
lobby: Lobby;
players: PlayerWithToken[];
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
// Setup players
const playerOpts: PlayerRequest[] = [];
const playerTokens: Record<string, string> = {};
for (const _player of req.players) {
const playerId = crypto.randomUUID();
const { token: playerToken } = await ctx.modules.tokens.create({
type: "player",
meta: { playerId: playerId },
});
playerOpts.push({ playerId });
playerTokens[playerId] = playerToken.token;
}

const { lobby, players }: FindLobbyResponse = await ctx.actors.lobbyManager.getOrCreateAndCall(
"default",
undefined,
"findLobby",
{
query: {
version: req.version,
tags: req.tags,
},
players: playerOpts,
} as FindLobbyRequest,
);

return {
lobby,
players: players.map((x) => ({ token: playerTokens[x.id], ...x })),
};
}
Loading

0 comments on commit ee6bab4

Please sign in to comment.