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

Commit a6e177d

Browse files
committed
feat(lobbies): create lobbies module
1 parent 412d5c0 commit a6e177d

File tree

18 files changed

+1487
-49
lines changed

18 files changed

+1487
-49
lines changed

modules/lobbies/actors/lobby_manager.ts

Lines changed: 583 additions & 0 deletions
Large diffs are not rendered by default.

modules/lobbies/config.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export const DEFAULT_CONFIG: FullConfig = {
2+
lobbies: {
3+
destroyOnEmptyAfter: 60 * 1000,
4+
unreadyExpireAfter: 5 * 60 * 1000,
5+
maxPlayers: 16,
6+
maxPlayersDirect: 16,
7+
enableCreate: false,
8+
enableDestroy: false,
9+
enableFind: true,
10+
enableFindOrCreate: true,
11+
enableJoin: true,
12+
enableList: true,
13+
},
14+
lobbyRules: [],
15+
players: {
16+
maxPerIp: 8,
17+
maxUnconnected: 128,
18+
unconnectedExpireAfter: 60 * 1000,
19+
autoDestroyAfter: 48 * 24 * 60 * 60 * 1000,
20+
}
21+
};
22+
23+
export type Config = Partial<FullConfig>;
24+
25+
export interface FullConfig {
26+
lobbies: LobbyConfig,
27+
lobbyRules: LobbyRule[],
28+
players: {
29+
maxPerIp?: number;
30+
maxUnconnected?: number;
31+
unconnectedExpireAfter: number;
32+
autoDestroyAfter?: number;
33+
}
34+
}
35+
36+
export interface LobbyRule {
37+
tags: Record<string, string>,
38+
config: LobbyConfig,
39+
}
40+
41+
export interface LobbyConfig {
42+
destroyOnEmptyAfter?: number | null;
43+
unreadyExpireAfter: number;
44+
maxPlayers: number;
45+
maxPlayersDirect: number;
46+
enableDynamicMaxPlayers?: PlayerRange,
47+
enableDynamicMaxPlayersDirect?: PlayerRange,
48+
enableCreate: boolean,
49+
enableDestroy: boolean,
50+
enableFind: boolean,
51+
enableFindOrCreate: boolean,
52+
enableJoin: boolean,
53+
enableList: boolean,
54+
}
55+
56+
export interface PlayerRange {
57+
min: number,
58+
max: number,
59+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Unconnected Players
2+
3+
## Why it exists?
4+
5+
- high load & low player caps
6+
- preventing botting
7+
8+
## What happens when players fail to connect?
9+
10+
- Unconnected players stack up
11+
- How lobbies API handles it
12+
- Max players per IP: if creating another player and goes over ip limit, will
13+
delete the old unconnected player for the same IP
14+
- Maximum unconnected players: if too many unconnected players, we'll start
15+
discarding the oldest unconnected player

modules/lobbies/module.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"scripts": {
3+
"create": {
4+
"public": true
5+
},
6+
"destroy": {},
7+
"find_or_create": {},
8+
"join": {},
9+
"list": {},
10+
"set_lobby_ready": {},
11+
"set_player_connected": {},
12+
"set_player_disconnected": {},
13+
"find": {
14+
"description": "TODO: Doc connect to server asap afterwards."
15+
},
16+
"force_gc": {}
17+
},
18+
"actors": {
19+
"lobby_manager": {
20+
"storage_id": "lobby_manager"
21+
}
22+
},
23+
"errors": {
24+
"lobby_not_found": {},
25+
"lobby_create_missing_players": {
26+
"description": "When creating a lobby with `config.lobbies.autoDestroyWhenEmpty`, a lobby must be created with players in order to avoid creating an empty lobby."
27+
},
28+
"lobby_full": {},
29+
"more_players_than_max": {
30+
"description": "More players were passed to the create lobby than the number of max players in a lobby."
31+
},
32+
"lobby_already_ready": {},
33+
"player_already_connected": {
34+
"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."
35+
},
36+
"player_disconnected": {
37+
"description": "The player has already disconnected from the server. Create a new player for the specified lobby using the `join` script."
38+
},
39+
"no_matching_lobbies": {},
40+
"too_many_players_for_ip": {}
41+
},
42+
"dependencies": {
43+
"tokens": {}
44+
}
45+
}

modules/lobbies/scripts/create.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {
2+
CreateLobbyRequest,
3+
CreateLobbyResponse,
4+
} from "../actors/lobby_manager.ts";
5+
import { ScriptContext } from "../module.gen.ts";
6+
import {
7+
Lobby,
8+
Player,
9+
PlayerRequest,
10+
PlayerWithToken,
11+
} from "../utils/types.ts";
12+
13+
export interface Request {
14+
version: string;
15+
tags: Record<string, string>;
16+
maxPlayers: number;
17+
destroyOnEmptyAfter: number;
18+
19+
players: PlayerRequest[];
20+
}
21+
22+
export interface Response {
23+
lobby: Lobby;
24+
players: PlayerWithToken[];
25+
}
26+
27+
// TODO: Doc why we create tokens on the script and not the DO
28+
29+
export async function run(
30+
ctx: ScriptContext,
31+
req: Request,
32+
): Promise<Response> {
33+
let manager;
34+
if (!await ctx.actors.lobbies.lobbyManager.exists("default")) {
35+
manager = await ctx.actors.lobbies.lobbyManager.create("default", {});
36+
} else {
37+
manager = ctx.actors.lobbies.lobbyManager.get("default");
38+
}
39+
40+
// Setup lobby
41+
//
42+
// This token will be disposed if the lobby is not created
43+
const lobbyId = crypto.randomUUID();
44+
const { token: lobbyToken } = await ctx.modules.tokens.create({
45+
type: "lobby",
46+
meta: { lobbyId: lobbyId },
47+
});
48+
49+
// Setup players
50+
const playerOpts: PlayerRequest[] = [];
51+
const playerTokens: Record<string, string> = {};
52+
for (const _player of req.players) {
53+
const playerId = crypto.randomUUID();
54+
const { token: playerToken } = await ctx.modules.tokens.create({
55+
type: "player",
56+
meta: { lobbyId: lobbyId, playerId: playerId },
57+
});
58+
playerOpts.push({ playerId });
59+
playerTokens[playerId] = playerToken.token;
60+
}
61+
62+
const { lobby, players }: CreateLobbyResponse = await manager.call(
63+
"createLobby",
64+
{
65+
lobby: {
66+
lobbyId,
67+
version: req.version,
68+
tags: req.tags,
69+
lobbyToken: lobbyToken.token,
70+
maxPlayers: req.maxPlayers,
71+
destroyOnEmptyAfter: req.destroyOnEmptyAfter,
72+
},
73+
players: playerOpts,
74+
} as CreateLobbyRequest,
75+
);
76+
77+
return {
78+
lobby,
79+
players: players.map((x) => ({ token: playerTokens[x.id], ...x })),
80+
};
81+
}

modules/lobbies/scripts/destroy.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { DestroyLobbyRequest } from "../actors/lobby_manager.ts";
2+
import { ScriptContext } from "../module.gen.ts";
3+
4+
export interface Request {
5+
lobbyId: string;
6+
}
7+
8+
export interface Response {
9+
}
10+
11+
export async function run(
12+
ctx: ScriptContext,
13+
req: Request,
14+
): Promise<Response> {
15+
let manager;
16+
if (!await ctx.actors.lobbies.lobbyManager.exists("default")) {
17+
manager = await ctx.actors.lobbies.lobbyManager.create("default", {});
18+
} else {
19+
manager = ctx.actors.lobbies.lobbyManager.get("default");
20+
}
21+
22+
await manager.call(
23+
"destroyLobby",
24+
{ lobbyId: req.lobbyId } as DestroyLobbyRequest,
25+
);
26+
27+
return {};
28+
}

modules/lobbies/scripts/find.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
FindLobbyRequest,
3+
FindLobbyResponse,
4+
} from "../actors/lobby_manager.ts";
5+
import { ScriptContext } from "../module.gen.ts";
6+
import { Lobby, PlayerRequest, PlayerWithToken } from "../utils/types.ts";
7+
8+
export interface Request {
9+
version: string;
10+
tags: Record<string, string>;
11+
players: PlayerRequest[];
12+
}
13+
14+
export interface Response {
15+
lobby: Lobby;
16+
players: PlayerWithToken[];
17+
}
18+
19+
export async function run(
20+
ctx: ScriptContext,
21+
req: Request,
22+
): Promise<Response> {
23+
let manager;
24+
if (!await ctx.actors.lobbies.lobbyManager.exists("default")) {
25+
manager = await ctx.actors.lobbies.lobbyManager.create("default", {});
26+
} else {
27+
manager = ctx.actors.lobbies.lobbyManager.get("default");
28+
}
29+
30+
// Setup players
31+
const playerOpts: PlayerRequest[] = [];
32+
const playerTokens: Record<string, string> = {};
33+
for (const _player of req.players) {
34+
const playerId = crypto.randomUUID();
35+
const { token: playerToken } = await ctx.modules.tokens.create({
36+
type: "player",
37+
meta: { playerId: playerId },
38+
});
39+
playerOpts.push({ playerId });
40+
playerTokens[playerId] = playerToken.token;
41+
}
42+
43+
const { lobby, players }: FindLobbyResponse = await manager.call(
44+
"findLobby",
45+
{
46+
query: {
47+
version: req.version,
48+
tags: req.tags,
49+
},
50+
players: playerOpts,
51+
} as FindLobbyRequest,
52+
);
53+
54+
return {
55+
lobby,
56+
players: players.map((x) => ({ token: playerTokens[x.id], ...x })),
57+
};
58+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
FindOrCreateLobbyRequest,
3+
FindOrCreateLobbyResponse,
4+
} from "../actors/lobby_manager.ts";
5+
import { ScriptContext } from "../module.gen.ts";
6+
import { Lobby, PlayerRequest, PlayerWithToken } from "../utils/types.ts";
7+
8+
export interface Request {
9+
version: string,
10+
tags: Record<string, string>;
11+
players: PlayerRequest[];
12+
13+
createConfig: {
14+
tags: Record<string, string>;
15+
maxPlayers: number;
16+
destroyOnEmptyAfter: number;
17+
}
18+
}
19+
20+
export interface Response {
21+
lobby: Lobby;
22+
players: PlayerWithToken[];
23+
}
24+
25+
export async function run(
26+
ctx: ScriptContext,
27+
req: Request,
28+
): Promise<Response> {
29+
let manager;
30+
if (!await ctx.actors.lobbies.lobbyManager.exists("default")) {
31+
manager = await ctx.actors.lobbies.lobbyManager.create("default", {});
32+
} else {
33+
manager = ctx.actors.lobbies.lobbyManager.get("default");
34+
}
35+
36+
// Setup lobby
37+
const lobbyId = crypto.randomUUID();
38+
const { token: lobbyToken } = await ctx.modules.tokens.create({
39+
type: "lobby",
40+
meta: { lobbyId: lobbyId },
41+
});
42+
43+
// Setup players
44+
const playerOpts: PlayerRequest[] = [];
45+
const playerTokens: Record<string, string> = {};
46+
for (const _player of req.players) {
47+
const playerId = crypto.randomUUID();
48+
const { token: playerToken } = await ctx.modules.tokens.create({
49+
type: "player",
50+
meta: { playerId: playerId },
51+
});
52+
playerOpts.push({ playerId });
53+
playerTokens[playerId] = playerToken.token;
54+
}
55+
56+
const { lobby, players }: FindOrCreateLobbyResponse = await manager.call(
57+
"findOrCreateLobby",
58+
{
59+
query: {
60+
version: req.version,
61+
tags: req.tags,
62+
},
63+
lobby: {
64+
lobbyId,
65+
version: req.version,
66+
tags: req.createConfig.tags,
67+
lobbyToken: lobbyToken.token,
68+
maxPlayers: req.createConfig.maxPlayers,
69+
destroyOnEmptyAfter: req.createConfig.destroyOnEmptyAfter,
70+
},
71+
players: playerOpts,
72+
} as FindOrCreateLobbyRequest,
73+
);
74+
75+
return {
76+
lobby,
77+
players: players.map((x) => ({ token: playerTokens[x.id], ...x })),
78+
};
79+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
3+
export interface Request {
4+
}
5+
6+
export interface Response {
7+
}
8+
9+
export async function run(
10+
ctx: ScriptContext,
11+
req: Request,
12+
): Promise<Response> {
13+
let manager;
14+
if (!await ctx.actors.lobbies.lobbyManager.exists("default")) {
15+
manager = await ctx.actors.lobbies.lobbyManager.create("default", {});
16+
} else {
17+
manager = ctx.actors.lobbies.lobbyManager.get("default");
18+
}
19+
20+
await manager.call("gc");
21+
22+
return {};
23+
}

0 commit comments

Comments
 (0)