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

Commit 5c89242

Browse files
committed
feat(lobbies): create lobbies module
1 parent 6d0c35b commit 5c89242

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+5777
-1367
lines changed

deno.lock

Lines changed: 1552 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

modules/lobbies/actors/lobby_manager.ts

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

modules/lobbies/config.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { BackendLocalDevelopmentConfig, BackendLocalDevelopmentConfigPort } from "./utils/lobby/backend/local_development.ts";
2+
import { BackendServerConfig } from "./utils/lobby/backend/server.ts";
3+
import { BackendTestConfig } from "./utils/lobby/backend/test.ts";
4+
5+
export interface Config {
6+
lobbies: LobbyConfig;
7+
lobbyRules: LobbyRule[];
8+
players: {
9+
maxPerIp?: number;
10+
maxUnconnected?: number;
11+
unconnectedExpireAfter: number;
12+
autoDestroyAfter?: number;
13+
};
14+
}
15+
16+
export interface LobbyRule {
17+
tags: Record<string, string>;
18+
config: Partial<LobbyConfig>;
19+
}
20+
21+
export interface LobbyConfig extends Record<PropertyKey, unknown> {
22+
destroyOnEmptyAfter?: number | null;
23+
unreadyExpireAfter: number;
24+
maxPlayers: number;
25+
maxPlayersDirect: number;
26+
enableDynamicMaxPlayers?: PlayerRange;
27+
enableDynamicMaxPlayersDirect?: PlayerRange;
28+
enableCreate: boolean;
29+
enableDestroy: boolean;
30+
enableFind: boolean;
31+
enableFindOrCreate: boolean;
32+
enableJoin: boolean;
33+
enableList: boolean;
34+
backend: LobbyBackend;
35+
}
36+
37+
export interface PlayerRange {
38+
min: number;
39+
max: number;
40+
}
41+
42+
type LobbyBackend = { test: BackendTestConfig } | { localDevelopment: BackendLocalDevelopmentConfig } | { server: BackendServerConfig };
43+
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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
{
2+
"status": "stable",
3+
"name": "Lobbies",
4+
"description": "Lobby & player management.",
5+
"icon": "game-board",
6+
"tags": [
7+
"core",
8+
"multiplayer"
9+
],
10+
"authors": [
11+
"NathanFlurry"
12+
],
13+
"scripts": {
14+
"create": {
15+
"name": "Create Lobby",
16+
"description": "Creates a new lobby on-demand.",
17+
"public": true
18+
},
19+
"destroy": {
20+
"name": "Destroy Lobby",
21+
"description": "Destroys an existing lobby.",
22+
"public": true
23+
},
24+
"find_or_create": {
25+
"name": "Find Or Create Lobby",
26+
"description": "Finds a lobby or creates one if there are no available spots for players.",
27+
"public": true
28+
},
29+
"join": {
30+
"name": "Join Lobby",
31+
"description": "Add a player to an existing lobby.",
32+
"public": true
33+
},
34+
"list": {
35+
"name": "List Lobbies",
36+
"description": "List & query all lobbies.",
37+
"public": true
38+
},
39+
"set_lobby_ready": {
40+
"name": "Set Lobby Ready",
41+
"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.",
42+
"public": true
43+
},
44+
"set_player_connected": {
45+
"name": "Set Player Connected",
46+
"description": "Called when a player connects to the lobby.",
47+
"public": true
48+
},
49+
"set_player_disconnected": {
50+
"name": "Set Player Disconnected",
51+
"description": "Called when a player disconnects from the lobby.",
52+
"public": true
53+
},
54+
"find": {
55+
"name": "Find Lobby",
56+
"description": "Finds an existing lobby with a given query. This will not create a new lobby, see `find_or_create` instead.",
57+
"public": true
58+
},
59+
"force_gc": {
60+
"name": "Force Garbage Collection",
61+
"description": "Rarely used. Forces the matchmaker to purge lobbies & players.",
62+
"public": false
63+
}
64+
},
65+
"actors": {
66+
"lobby_manager": {}
67+
},
68+
"errors": {
69+
"lobby_not_found": {
70+
"name": "Lobby Not Found",
71+
"description": "Lobby not found."
72+
},
73+
"lobby_create_missing_players": {
74+
"name": "Lobby Create Missing Players",
75+
"description": "When creating a lobby with `config.lobbies.autoDestroyWhenEmpty`, a lobby must be created with players in order to avoid creating an empty lobby."
76+
},
77+
"lobby_full": {
78+
"name": "Lobby Full",
79+
"description": "No more players can join this lobby."
80+
},
81+
"more_players_than_max": {
82+
"name": "More Players Than Max",
83+
"description": "More players were passed to the create lobby than the number of max players in a lobby."
84+
},
85+
"lobby_already_ready": {
86+
"name": "Lobby Already Ready",
87+
"description": "Lobby already set as ready."
88+
},
89+
"player_already_connected": {
90+
"name": "Player Already Connected",
91+
"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."
92+
},
93+
"player_disconnected": {
94+
"name": "Player Disconnected",
95+
"description": "The player has already disconnected from the server. Create a new player for the specified lobby using the `join` script."
96+
},
97+
"no_matching_lobbies": {
98+
"name": "No Matching Lobbies",
99+
"description": "No lobbies matched the given query."
100+
},
101+
"too_many_players_for_ip": {
102+
"name": "Too Many Players For IP",
103+
"description": "The player has too many existing players for the given IP."
104+
},
105+
"cannot_mutate_lobbies": {
106+
"name": "Cannot Mutate Lobbies",
107+
"description": "This backend doesn't let you create or destroy lobbies."
108+
},
109+
"lobby_token_required": {
110+
"name": "Lobby Token Required",
111+
"description": "A lobby token was not provided when required for authentication."
112+
}
113+
},
114+
"dependencies": {
115+
"tokens": {},
116+
"rivet": {}
117+
},
118+
"defaultConfig": {
119+
"lobbies": {
120+
"destroyOnEmptyAfter": 60000,
121+
"unreadyExpireAfter": 300000,
122+
"maxPlayers": 16,
123+
"maxPlayersDirect": 16,
124+
"enableCreate": false,
125+
"enableDestroy": false,
126+
"enableFind": true,
127+
"enableFindOrCreate": true,
128+
"enableJoin": true,
129+
"enableList": true
130+
},
131+
"lobbyRules": [],
132+
"players": {
133+
"maxPerIp": 8,
134+
"maxUnconnected": 128,
135+
"unconnectedExpireAfter": 60000,
136+
"autoDestroyAfter": 4147200000
137+
}
138+
}
139+
}

modules/lobbies/scripts/create.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
CreateLobbyRequest,
3+
CreateLobbyResponse,
4+
} from "../actors/lobby_manager.ts";
5+
import { ScriptContext } from "../module.gen.ts";
6+
import { LobbyResponse } from "../utils/lobby/mod.ts";
7+
import {
8+
buildPlayerResponseWithToken,
9+
PlayerRequest,
10+
PlayerResponseWithToken,
11+
} from "../utils/player.ts";
12+
13+
export interface Request {
14+
version: string;
15+
tags: Record<string, string>;
16+
maxPlayers: number;
17+
maxPlayersDirect: number;
18+
19+
players: PlayerRequest[];
20+
21+
noWait?: boolean;
22+
}
23+
24+
export interface Response {
25+
lobby: LobbyResponse;
26+
players: PlayerResponseWithToken[];
27+
}
28+
29+
// TODO: Doc why we create tokens on the script and not the DO
30+
31+
export async function run(
32+
ctx: ScriptContext,
33+
req: Request,
34+
): Promise<Response> {
35+
const lobbyId = crypto.randomUUID();
36+
37+
const { lobby, players } = await ctx.actors.lobbyManager
38+
.getOrCreateAndCall<undefined, CreateLobbyRequest, CreateLobbyResponse>(
39+
"default",
40+
undefined,
41+
"rpcCreateLobby",
42+
{
43+
lobby: {
44+
lobbyId,
45+
version: req.version,
46+
tags: req.tags,
47+
maxPlayers: req.maxPlayers,
48+
maxPlayersDirect: req.maxPlayersDirect,
49+
},
50+
players: req.players,
51+
noWait: req.noWait ?? false,
52+
},
53+
);
54+
55+
const playerResponses = [];
56+
for (const player of players) {
57+
playerResponses.push(await buildPlayerResponseWithToken(ctx, player));
58+
}
59+
60+
return {
61+
lobby,
62+
players: playerResponses,
63+
};
64+
}

modules/lobbies/scripts/destroy.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
await ctx.actors.lobbyManager.getOrCreateAndCall<undefined, DestroyLobbyRequest, undefined>(
16+
"default",
17+
undefined,
18+
"rpcDestroyLobby",
19+
{ lobbyId: req.lobbyId }
20+
);
21+
22+
return {};
23+
}

modules/lobbies/scripts/find.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
FindLobbyRequest,
3+
FindLobbyResponse,
4+
} from "../actors/lobby_manager.ts";
5+
import { ScriptContext } from "../module.gen.ts";
6+
import { LobbyResponse } from "../utils/lobby/mod.ts";
7+
import {
8+
buildPlayerResponseWithToken,
9+
PlayerRequest,
10+
PlayerResponseWithToken,
11+
} from "../utils/player.ts";
12+
13+
export interface Request {
14+
version: string;
15+
tags: Record<string, string>;
16+
players: PlayerRequest[];
17+
noWait?: boolean;
18+
}
19+
20+
export interface Response {
21+
lobby: LobbyResponse;
22+
players: PlayerResponseWithToken[];
23+
}
24+
25+
export async function run(
26+
ctx: ScriptContext,
27+
req: Request,
28+
): Promise<Response> {
29+
const { lobby, players } = await ctx.actors.lobbyManager
30+
.getOrCreateAndCall<undefined, FindLobbyRequest, FindLobbyResponse>(
31+
"default",
32+
undefined,
33+
"rpcFindLobby",
34+
{
35+
query: {
36+
version: req.version,
37+
tags: req.tags,
38+
},
39+
players: req.players,
40+
noWait: req.noWait ?? false,
41+
}
42+
);
43+
44+
const playerResponses = [];
45+
for (const player of players) {
46+
playerResponses.push(await buildPlayerResponseWithToken(ctx, player));
47+
}
48+
49+
return {
50+
lobby,
51+
players: playerResponses,
52+
};
53+
}

0 commit comments

Comments
 (0)