-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlobby.ts
110 lines (100 loc) · 3.9 KB
/
lobby.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { throttle } from "vendor/sindresorhus/throttleit/index.ts"
import { generateProjectName as generateWorldName } from "vendor/withastro/cli-kit"
import { ServerWorld } from "game/world.server.ts"
import { Player } from "game/player.ts"
import type { Receiver } from "game/channel.d.ts"
import type { JoinWorld, MessageRegistry, NewWorld, Disconnected, Messages, CursorMove, CursorSync } from "game/messages.d.ts"
/**
* The lobby is responsible for creating new worlds where games can be played,
* and adding newly-connected players to those worlds.
*/
export const lobby = new class Lobby implements Receiver {
/**
* Worlds keyed by their names.
*/
#worlds = new Map<string, ServerWorld>
/**
* Cursors keyed by the players they belong to.
*/
#cursors = new Map<Player, [ x: number, y: number ]>
/**
* Players currently in the lobby.
*/
#players = new Set<Player>
/**
* Creates a new player for the given connection and
* starts listening for messages from it.
*/
enter(weboscket: WebSocket) {
const player = new Player(weboscket)
this.#players.add(player)
player.subscribe(this)
}
#exit(player: Player) {
player.unsubscribe(this)
this.#players.delete(player)
this.#cursors.delete(player)
this.#broadcastCursors()
}
/**
* There are only three messages that the lobby is interested in.
*
* - `NewWorld` is sent by a player when they click on "New Game".
*
* - `JoinWorld` is sent by a player when they want to join an existing world
* whose name they have.
*
* - `Disconnected` is sent implicitly when the connection is closed or is severed.
*/
receive<Message extends Messages>(message: Message, data: MessageRegistry[Message]) {
if (message === "Connected") {
const player = Player.get(data)
if (player) {
const cursors: CursorSync = [...this.#cursors].map(([ { id }, [ x, y ]]) => [ id.slice(0, 8), x, y ])
player.send("CursorSync", cursors)
}
} else if (message === "Disconnected") {
const { player }: Disconnected = data
this.#exit(player!)
} else if (message === "NewWorld") {
this.#newWorld(data)
} else if (message === "JoinWorld") {
this.#joinWorld(data)
} else if (message === "CursorMove") {
const player = Player.get(data)
if (player) {
this.#cursors.set(player, data as CursorMove)
this.#broadcastCursors()
}
}
}
#newWorld(data: NewWorld) {
const player = Player.get(data)
if (!player) return Player.notFound("NewWorld", data)
let worldName: string
// security: possible world names are finite - an attack could
// create them all, and this line would then freeze the server
while (this.#worlds.has(worldName = generateWorldName())) {}
const world = new ServerWorld(worldName)
this.#worlds.set(world.name, world)
world.update("AddPlayer", { player })
this.#exit(player)
}
#joinWorld(data: JoinWorld) {
const player = Player.get(data)
if (!player) return Player.notFound("JoinWorld", data)
const world = this.#worlds.get(data.world)
if (world === undefined) {
return player.send("WorldNotFound", { world: data.world })
}
world.update("AddPlayer", { player })
this.#exit(player)
}
#broadcastCursors = throttle(() => {
const cursors: CursorSync = [...this.#cursors].map(([ { id }, [ x, y ]]) => [ id.slice(0, 8), x, y ])
for (const player of this.#players) {
const otherPlayers = cursors.filter(p => p[0] !== player.id.slice(0, 8))
if (otherPlayers.length > 0) player.send("CursorSync", otherPlayers)
}
}, 50)
}