diff --git a/server/src/constants.ts b/server/src/constants.ts new file mode 100644 index 0000000..a2b3d12 --- /dev/null +++ b/server/src/constants.ts @@ -0,0 +1,6 @@ +export namespace Constants { + export const SERVER_PORT = 8080; + export const SERVER_TICK_RATE = (1 / 60) * 1000; + export const GAME_TOPIC = 'game'; + export const MAX_PLAYERS = 8; +} diff --git a/server/src/main.ts b/server/src/main.ts new file mode 100644 index 0000000..965e0d7 --- /dev/null +++ b/server/src/main.ts @@ -0,0 +1,32 @@ +import { Grid } from '@engine/structures'; +import { + ServerMessageProcessor, + ServerSocketMessagePublisher, + ServerSocketMessageReceiver +} from './network'; +import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems'; +import { Game } from '@engine/Game'; +import { Constants } from './constants'; +import { GameServer } from './server'; + +const messageReceiver = new ServerSocketMessageReceiver(); +const messagePublisher = new ServerSocketMessagePublisher(); +const messageProcessor = new ServerMessageProcessor(); + +const game = new Game(); + +const server = new GameServer(game, messageReceiver, messagePublisher); + +[ + new Physics(), + new Collision(new Grid()), + new WallBounds(), + new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor) +].forEach((system) => game.addSystem(system)); + +game.start(); +setInterval(() => { + game.doGameLoop(performance.now()); +}, Constants.SERVER_TICK_RATE); + +server.serve(); diff --git a/server/src/network/MessageProcessor.ts b/server/src/network/MessageProcessor.ts new file mode 100644 index 0000000..de42459 --- /dev/null +++ b/server/src/network/MessageProcessor.ts @@ -0,0 +1,8 @@ +import { MessageProcessor } from '@engine/network'; +import { ServerMessage } from '.'; + +export class ServerMessageProcessor implements MessageProcessor { + constructor() {} + + public process(_message: ServerMessage) {} +} diff --git a/server/src/network/MessagePublisher.ts b/server/src/network/MessagePublisher.ts new file mode 100644 index 0000000..9c6011f --- /dev/null +++ b/server/src/network/MessagePublisher.ts @@ -0,0 +1,31 @@ +import { Message, MessagePublisher } from '@engine/network'; +import { Server } from 'bun'; +import { Constants } from '../constants'; +import { stringify } from '@engine/utils'; + +export class ServerSocketMessagePublisher implements MessagePublisher { + private server?: Server; + private messages: Message[]; + + constructor(server?: Server) { + this.messages = []; + + if (server) this.setServer(server); + } + + public setServer(server: Server) { + this.server = server; + } + + public addMessage(message: Message) { + this.messages.push(message); + } + + public publish() { + if (this.messages.length) { + this.server?.publish(Constants.GAME_TOPIC, stringify(this.messages)); + + this.messages = []; + } + } +} diff --git a/server/src/network/MessageReceiver.ts b/server/src/network/MessageReceiver.ts new file mode 100644 index 0000000..fcac0a4 --- /dev/null +++ b/server/src/network/MessageReceiver.ts @@ -0,0 +1,22 @@ +import { MessageQueueProvider } from '@engine/network'; +import type { ServerMessage } from '.'; + +export class ServerSocketMessageReceiver implements MessageQueueProvider { + private messages: ServerMessage[]; + + constructor() { + this.messages = []; + } + + public addMessage(message: ServerMessage) { + this.messages.push(message); + } + + public getNewMessages() { + return this.messages; + } + + public clearMessages() { + this.messages = []; + } +} diff --git a/server/src/network/index.ts b/server/src/network/index.ts new file mode 100644 index 0000000..8ffa689 --- /dev/null +++ b/server/src/network/index.ts @@ -0,0 +1,16 @@ +import { Message } from '@engine/network'; + +export * from './MessageProcessor'; +export * from './MessagePublisher'; +export * from './MessageReceiver'; + +export type SessionData = { sessionId: string }; + +export type Session = { + sessionId: string; + controllableEntities: Set; +}; + +export interface ServerMessage extends Message { + sessionData: SessionData; +} diff --git a/server/src/server.ts b/server/src/server.ts index c77bfef..6acbe74 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,110 +1,124 @@ import { Game } from '@engine/Game'; import { EntityNames, Player } from '@engine/entities'; -import { WallBounds, Physics, Collision, NetworkUpdate } from '@engine/systems'; +import { MessageType } from '@engine/network'; +import { Constants } from './constants'; import { - type MessageQueueProvider, - type MessagePublisher, - MessageType, - type MessageProcessor, - type Message -} from '@engine/network'; -import { stringify, parse } from '@engine/utils'; -import { Grid } from '@engine/structures'; -import { Miscellaneous } from '@engine/config'; -import { Server } from 'bun'; + ServerSocketMessageReceiver, + ServerSocketMessagePublisher, + SessionData, + ServerMessage, + Session +} from './network'; +import { parse } from '@engine/utils'; +import { Server, ServerWebSocket } from 'bun'; -const SERVER_PORT = 8080; -const SERVER_TICK_RATE = (1 / 60) * 1000; -const GAME_TOPIC = 'game'; -const MAX_PLAYERS = 8; +export class GameServer { + private sessions: Map; -type SessionData = { sessionId: string }; - -interface ServerMessage extends Message { - sessionData: SessionData; -} - -class ServerSocketMessageReceiver implements MessageQueueProvider { - private messages: ServerMessage[]; - - constructor() { - this.messages = []; - } - - public addMessage(message: ServerMessage) { - this.messages.push(message); - } - - public getNewMessages() { - return this.messages; - } - - public clearMessages() { - this.messages = []; - } -} - -class ServerMessageProcessor implements MessageProcessor { - constructor() {} - - public process(_message: ServerMessage) {} -} - -class ServerSocketMessagePublisher implements MessagePublisher { private server?: Server; - private messages: Message[]; + private game: Game; + private messageReceiver: ServerSocketMessageReceiver; + private messagePublisher: ServerSocketMessagePublisher; - constructor(server?: Server) { - if (server) { - this.server = server; - } + constructor( + game: Game, + messageReceiver: ServerSocketMessageReceiver, + messagePublisher: ServerSocketMessagePublisher + ) { + this.sessions = new Map(); - this.messages = []; + this.game = game; + this.messageReceiver = messageReceiver; + this.messagePublisher = messagePublisher; } - public setServer(server: Server) { - this.server = server; + public serve() { + if (!this.server) + this.server = Bun.serve({ + port: Constants.SERVER_PORT, + fetch: (req, srv) => this.fetchHandler(req, srv), + websocket: { + open: (ws) => this.openWebsocket(ws), + message: (ws, msg) => this.websocketMessage(ws, msg), + close: (ws) => this.closeWebsocket(ws) + } + }); + + this.messagePublisher.setServer(this.server); + + console.log(`Listening on ${this.server.hostname}:${this.server.port}`); } - public addMessage(message: Message) { - this.messages.push(message); - } + private websocketMessage( + websocket: ServerWebSocket, + message: string | Uint8Array + ) { + if (typeof message == 'string') { + const receivedMessage = parse(message); + receivedMessage.sessionData = websocket.data; - public publish() { - if (this.messages.length) { - this.server?.publish(GAME_TOPIC, stringify(this.messages)); - - this.messages = []; + this.messageReceiver.addMessage(receivedMessage); } } -} -const game = new Game(); + private closeWebsocket(websocket: ServerWebSocket) { + const { sessionId } = websocket.data; -const messageReceiver = new ServerSocketMessageReceiver(); -const messagePublisher = new ServerSocketMessagePublisher(); -const messageProcessor = new ServerMessageProcessor(); -const sessionControllableEntities: Map> = new Map(); + const sessionEntities = this.sessions.get(sessionId)!.controllableEntities; -const sessions = new Set(); + this.sessions.delete(sessionId); -const server = Bun.serve({ - port: SERVER_PORT, - fetch: async (req, server): Promise => { + if (!sessionEntities) return; + this.messagePublisher.addMessage({ + type: MessageType.REMOVE_ENTITIES, + body: Array.from(sessionEntities) + }); + } + + private openWebsocket(websocket: ServerWebSocket) { + websocket.subscribe(Constants.GAME_TOPIC); + + const { sessionId } = websocket.data; + if (this.sessions.has(sessionId)) { + return; + } + + this.sessions.set(sessionId, { + sessionId, + controllableEntities: new Set() + }); + + const player = new Player(sessionId); + this.game.addEntity(player); + this.messagePublisher.addMessage({ + type: MessageType.NEW_ENTITIES, + body: [ + { + entityName: EntityNames.Player, + args: { playerId: sessionId, id: player.id } + } + ] + }); + + this.sessions.get(sessionId)!.controllableEntities.add(player.id); + } + + private fetchHandler( + req: Request, + server: Server + ): Promise | Response { const url = new URL(req.url); const headers = new Headers(); headers.set('Access-Control-Allow-Origin', '*'); if (url.pathname == '/assign') { - if (sessions.size > MAX_PLAYERS) + if (this.sessions.size > Constants.MAX_PLAYERS) return new Response('too many players', { headers, status: 400 }); const sessionId = crypto.randomUUID(); headers.set('Set-Cookie', `SessionId=${sessionId};`); - sessions.add(sessionId); - return new Response(sessionId, { headers }); } @@ -127,7 +141,7 @@ const server = Bun.serve({ } }); - return new Response('upgraded', { headers }); + return new Response('upgraded to ws', { headers }); } if (url.pathname == '/me') { @@ -135,67 +149,5 @@ const server = Bun.serve({ } return new Response('Not found', { headers, status: 404 }); - }, - websocket: { - open(ws) { - const { sessionId } = ws.data; - - if (sessionControllableEntities.has(sessionId)) { - return; - } - - const player = new Player(sessionId); - game.addEntity(player); - sessionControllableEntities.set(sessionId, new Set([player.id])); - - messagePublisher.addMessage({ - type: MessageType.NEW_ENTITIES, - body: [ - { - entityName: EntityNames.Player, - args: { playerId: sessionId, id: player.id } - } - ] - }); - - ws.subscribe(GAME_TOPIC); - }, - message(ws, message) { - if (typeof message == 'string') { - const receivedMessage = parse(message); - receivedMessage.sessionData = ws.data; - - messageReceiver.addMessage(receivedMessage); - } - }, - close(ws) { - const { sessionId } = ws.data; - - sessions.delete(sessionId); - - const sessionEntities = sessionControllableEntities.get(sessionId); - if (!sessionEntities) return; - - messagePublisher.addMessage({ - type: MessageType.REMOVE_ENTITIES, - body: Array.from(sessionEntities) - }); - } } -}); - -messagePublisher.setServer(server); - -[ - new Physics(), - new Collision(new Grid()), - new WallBounds(), - new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor) -].forEach((system) => game.addSystem(system)); - -game.start(); -setInterval(() => { - game.doGameLoop(performance.now()); -}, SERVER_TICK_RATE); - -console.log(`Listening on ${server.hostname}:${server.port}`); +}