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

Commit 8cdc6fe

Browse files
committed
feat(lobbies): create lobbies module
1 parent 6d0c35b commit 8cdc6fe

Some content is hidden

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

48 files changed

+6252
-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: 1153 additions & 0 deletions
Large diffs are not rendered by default.

modules/lobbies/config.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
regions: string[];
23+
destroyOnEmptyAfter?: number | null;
24+
unreadyExpireAfter: number;
25+
maxPlayers: number;
26+
maxPlayersDirect: number;
27+
enableDynamicMaxPlayers?: PlayerRange;
28+
enableDynamicMaxPlayersDirect?: PlayerRange;
29+
enableCreate: boolean;
30+
enableDestroy: boolean;
31+
enableFind: boolean;
32+
enableFindOrCreate: boolean;
33+
enableJoin: boolean;
34+
enableList: boolean;
35+
backend: LobbyBackend;
36+
}
37+
38+
export interface PlayerRange {
39+
min: number;
40+
max: number;
41+
}
42+
43+
export type LobbyBackend = { test: BackendTestConfig } | { localDevelopment: BackendLocalDevelopmentConfig } | { server: BackendServerConfig };
44+
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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
"list_regions": {
65+
"public": false
66+
}
67+
},
68+
"actors": {
69+
"lobby_manager": {}
70+
},
71+
"errors": {
72+
"lobby_not_found": {
73+
"name": "Lobby Not Found",
74+
"description": "Lobby not found."
75+
},
76+
"lobby_create_missing_players": {
77+
"name": "Lobby Create Missing Players",
78+
"description": "When creating a lobby with `config.lobbies.autoDestroyWhenEmpty`, a lobby must be created with players in order to avoid creating an empty lobby."
79+
},
80+
"lobby_full": {
81+
"name": "Lobby Full",
82+
"description": "No more players can join this lobby."
83+
},
84+
"more_players_than_max": {
85+
"name": "More Players Than Max",
86+
"description": "More players were passed to the create lobby than the number of max players in a lobby."
87+
},
88+
"lobby_already_ready": {
89+
"name": "Lobby Already Ready",
90+
"description": "Lobby already set as ready."
91+
},
92+
"player_already_connected": {
93+
"name": "Player Already Connected",
94+
"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."
95+
},
96+
"player_disconnected": {
97+
"name": "Player Disconnected",
98+
"description": "The player has already disconnected from the server. Create a new player for the specified lobby using the `join` script."
99+
},
100+
"no_matching_lobbies": {
101+
"name": "No Matching Lobbies",
102+
"description": "No lobbies matched the given query."
103+
},
104+
"too_many_players_for_ip": {
105+
"name": "Too Many Players For IP",
106+
"description": "The player has too many existing players for the given IP."
107+
},
108+
"cannot_mutate_lobbies": {
109+
"name": "Cannot Mutate Lobbies",
110+
"description": "This backend doesn't let you create or destroy lobbies."
111+
},
112+
"lobby_token_required": {
113+
"name": "Lobby Token Required",
114+
"description": "A lobby token was not provided when required for authentication."
115+
},
116+
"region_not_found": {
117+
"name": "Region Not Found",
118+
"description": "Region not found."
119+
}
120+
},
121+
"dependencies": {
122+
"tokens": {},
123+
"rivet": {}
124+
},
125+
"defaultConfig": {
126+
"lobbies": {
127+
"destroyOnEmptyAfter": 60000,
128+
"unreadyExpireAfter": 300000,
129+
"maxPlayers": 16,
130+
"maxPlayersDirect": 16,
131+
"enableCreate": false,
132+
"enableDestroy": false,
133+
"enableFind": true,
134+
"enableFindOrCreate": true,
135+
"enableJoin": true,
136+
"enableList": true
137+
},
138+
"lobbyRules": [],
139+
"players": {
140+
"maxPerIp": 8,
141+
"maxUnconnected": 128,
142+
"unconnectedExpireAfter": 60000,
143+
"autoDestroyAfter": 4147200000
144+
}
145+
}
146+
}

modules/lobbies/scripts/create.ts

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

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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
regions?: string[];
16+
tags?: Record<string, string>;
17+
players: PlayerRequest[];
18+
noWait?: boolean;
19+
}
20+
21+
export interface Response {
22+
lobby: LobbyResponse;
23+
players: PlayerResponseWithToken[];
24+
}
25+
26+
export async function run(
27+
ctx: ScriptContext,
28+
req: Request,
29+
): Promise<Response> {
30+
const { lobby, players } = await ctx.actors.lobbyManager
31+
.getOrCreateAndCall<undefined, FindLobbyRequest, FindLobbyResponse>(
32+
"default",
33+
undefined,
34+
"rpcFindLobby",
35+
{
36+
query: {
37+
version: req.version,
38+
regions: req.regions,
39+
tags: req.tags,
40+
},
41+
players: req.players,
42+
noWait: req.noWait ?? false,
43+
}
44+
);
45+
46+
const playerResponses = [];
47+
for (const player of players) {
48+
playerResponses.push(await buildPlayerResponseWithToken(ctx, player));
49+
}
50+
51+
return {
52+
lobby,
53+
players: playerResponses,
54+
};
55+
}

0 commit comments

Comments
 (0)