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

Commit 43b2b4a

Browse files
committed
feat(lobbies): create lobbies module
1 parent 770c6b0 commit 43b2b4a

24 files changed

+1984
-47
lines changed

deno.lock

Lines changed: 236 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: 646 additions & 0 deletions
Large diffs are not rendered by default.

modules/lobbies/config.ts

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

modules/lobbies/scripts/create.ts

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

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(
16+
"default",
17+
undefined,
18+
"destroyLobby",
19+
{ lobbyId: req.lobbyId } as DestroyLobbyRequest,
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 { 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+
// Setup players
24+
const playerOpts: PlayerRequest[] = [];
25+
const playerTokens: Record<string, string> = {};
26+
for (const _player of req.players) {
27+
const playerId = crypto.randomUUID();
28+
const { token: playerToken } = await ctx.modules.tokens.create({
29+
type: "player",
30+
meta: { playerId: playerId },
31+
});
32+
playerOpts.push({ playerId });
33+
playerTokens[playerId] = playerToken.token;
34+
}
35+
36+
const { lobby, players }: FindLobbyResponse = await ctx.actors.lobbyManager.getOrCreateAndCall(
37+
"default",
38+
undefined,
39+
"findLobby",
40+
{
41+
query: {
42+
version: req.version,
43+
tags: req.tags,
44+
},
45+
players: playerOpts,
46+
} as FindLobbyRequest,
47+
);
48+
49+
return {
50+
lobby,
51+
players: players.map((x) => ({ token: playerTokens[x.id], ...x })),
52+
};
53+
}

0 commit comments

Comments
 (0)