diff options
Diffstat (limited to 'server')
-rwxr-xr-x | server/bun.lockb | bin | 1650 -> 1270 bytes | |||
-rw-r--r-- | server/package.json | 3 | ||||
-rw-r--r-- | server/src/constants.ts | 6 | ||||
-rw-r--r-- | server/src/main.ts | 59 | ||||
-rw-r--r-- | server/src/network/MessageProcessor.ts | 36 | ||||
-rw-r--r-- | server/src/network/MessagePublisher.ts | 31 | ||||
-rw-r--r-- | server/src/network/MessageReceiver.ts | 22 | ||||
-rw-r--r-- | server/src/network/SessionInputSystem.ts | 32 | ||||
-rw-r--r-- | server/src/network/SessionManager.ts | 33 | ||||
-rw-r--r-- | server/src/network/index.ts | 29 | ||||
-rw-r--r-- | server/src/server.ts | 197 | ||||
-rw-r--r-- | server/tsconfig.json | 28 |
12 files changed, 433 insertions, 43 deletions
diff --git a/server/bun.lockb b/server/bun.lockb Binary files differindex 7f8b5ce..28b67ce 100755 --- a/server/bun.lockb +++ b/server/bun.lockb diff --git a/server/package.json b/server/package.json index 17d3c25..388cff2 100644 --- a/server/package.json +++ b/server/package.json @@ -8,6 +8,5 @@ "peerDependencies": { "typescript": "^5.0.0" }, - "dependencies": { - } + "dependencies": {} } 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..0e47491 --- /dev/null +++ b/server/src/main.ts @@ -0,0 +1,59 @@ +import { Grid } from '@engine/structures'; +import { + ServerMessageProcessor, + ServerSocketMessagePublisher, + ServerSocketMessageReceiver, + MemorySessionManager, + SessionInputSystem +} from './network'; +import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems'; +import { Game } from '@engine/Game'; +import { Constants } from './constants'; +import { GameServer } from './server'; +import { Floor } from '@engine/entities'; +import { BoundingBox } from '@engine/components'; +import { Miscellaneous } from '@engine/config'; + +const game = new Game(); + +const sessionManager = new MemorySessionManager(); + +const messageReceiver = new ServerSocketMessageReceiver(); +const messagePublisher = new ServerSocketMessagePublisher(); +const messageProcessor = new ServerMessageProcessor(game, sessionManager); + +const server = new GameServer( + game, + messageReceiver, + messagePublisher, + sessionManager +); + +[ + new SessionInputSystem(sessionManager), + new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor), + new Physics(), + new Collision(new Grid()), + new WallBounds() +].forEach((system) => game.addSystem(system)); + +const floor = new Floor(160); +const floorHeight = 200; + +floor.addComponent( + new BoundingBox( + { + x: Miscellaneous.WIDTH / 2, + y: Miscellaneous.HEIGHT + floorHeight / 2 + }, + { width: Miscellaneous.WIDTH, height: floorHeight } + ) +); +game.addEntity(floor); + +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..2d9f11f --- /dev/null +++ b/server/src/network/MessageProcessor.ts @@ -0,0 +1,36 @@ +import { + EntityUpdateBody, + MessageProcessor, + MessageType +} from '@engine/network'; +import { ServerMessage, SessionManager } from '.'; +import { Game } from '@engine/Game'; + +export class ServerMessageProcessor implements MessageProcessor { + private game: Game; + private sessionManager: SessionManager; + + constructor(game: Game, sessionManager: SessionManager) { + this.game = game; + this.sessionManager = sessionManager; + } + + public process(message: ServerMessage) { + switch (message.type) { + case MessageType.NEW_INPUT: { + const { sessionId } = message.sessionData; + const session = this.sessionManager.getSession(sessionId); + session?.inputSystem.keyPressed(message.body as string); + break; + } + case MessageType.REMOVE_INPUT: { + const { sessionId } = message.sessionData; + const session = this.sessionManager.getSession(sessionId); + session?.inputSystem.keyReleased(message.body as string); + break; + } + default: + break; + } + } +} 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/SessionInputSystem.ts b/server/src/network/SessionInputSystem.ts new file mode 100644 index 0000000..44fba54 --- /dev/null +++ b/server/src/network/SessionInputSystem.ts @@ -0,0 +1,32 @@ +import { Game } from '@engine/Game'; +import { SessionManager } from '.'; +import { System } from '@engine/systems'; +import { BoundingBox, ComponentNames, Control } from '@engine/components'; + +export class SessionInputSystem extends System { + private sessionManager: SessionManager; + + constructor(sessionManager: SessionManager) { + super('SessionInputSystem'); + + this.sessionManager = sessionManager; + } + + public update(_dt: number, game: Game) { + this.sessionManager.getSessions().forEach((sessionId) => { + const session = this.sessionManager.getSession(sessionId); + + if (!session) return; + + const { inputSystem } = session; + session.controllableEntities.forEach((entityId) => { + const entity = game.getEntity(entityId); + if (!entity) return; + + if (entity.hasComponent(ComponentNames.Control)) { + inputSystem.handleInput(entity); + } + }); + }); + } +} diff --git a/server/src/network/SessionManager.ts b/server/src/network/SessionManager.ts new file mode 100644 index 0000000..dbd4364 --- /dev/null +++ b/server/src/network/SessionManager.ts @@ -0,0 +1,33 @@ +import { Session, SessionManager } from '.'; + +export class MemorySessionManager implements SessionManager { + private sessions: Map<string, Session>; + + constructor() { + this.sessions = new Map(); + } + + public getSessions() { + return Array.from(this.sessions.keys()); + } + + public uniqueSessionId() { + return crypto.randomUUID(); + } + + public getSession(id: string) { + return this.sessions.get(id); + } + + public putSession(id: string, session: Session) { + return this.sessions.set(id, session); + } + + public numSessions() { + return this.sessions.size; + } + + public removeSession(id: string) { + this.sessions.delete(id); + } +} diff --git a/server/src/network/index.ts b/server/src/network/index.ts new file mode 100644 index 0000000..3cbf0ac --- /dev/null +++ b/server/src/network/index.ts @@ -0,0 +1,29 @@ +import { Message } from '@engine/network'; +import { Input } from '@engine/systems'; + +export * from './MessageProcessor'; +export * from './MessagePublisher'; +export * from './MessageReceiver'; +export * from './SessionManager'; +export * from './SessionInputSystem'; + +export type SessionData = { sessionId: string }; + +export type Session = { + sessionId: string; + controllableEntities: Set<string>; + inputSystem: Input; +}; + +export interface ServerMessage extends Message { + sessionData: SessionData; +} + +export interface SessionManager { + uniqueSessionId(): string; + getSession(id: string): Session | undefined; + getSessions(): string[]; + putSession(id: string, session: Session): void; + removeSession(id: string): void; + numSessions(): number; +} diff --git a/server/src/server.ts b/server/src/server.ts index 74d901b..575e916 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,37 +1,174 @@ -import { Game } from "../../engine/Game"; -import { Floor, Player } from "../../engine/entities"; -import { WallBounds, Physics, Collision } from "../../engine/systems"; -import { Miscellaneous } from "../../engine/config"; +import { Game } from '@engine/Game'; +import { Player } from '@engine/entities'; +import { Message, MessageType } from '@engine/network'; +import { Constants } from './constants'; +import { + ServerSocketMessageReceiver, + ServerSocketMessagePublisher, + SessionData, + ServerMessage, + Session, + SessionManager +} from './network'; +import { parse } from '@engine/utils'; +import { Server, ServerWebSocket } from 'bun'; +import { Input } from '@engine/systems'; +import { Control, NetworkUpdateable } from '@engine/components'; +import { stringify } from '@engine/utils'; -const TICK_RATE = 60 / 1000; +export class GameServer { + private server?: Server; + private game: Game; + private messageReceiver: ServerSocketMessageReceiver; + private messagePublisher: ServerSocketMessagePublisher; + private sessionManager: SessionManager; -const game = new Game(); + constructor( + game: Game, + messageReceiver: ServerSocketMessageReceiver, + messagePublisher: ServerSocketMessagePublisher, + sessionManager: SessionManager + ) { + this.game = game; + this.messageReceiver = messageReceiver; + this.messagePublisher = messagePublisher; + this.sessionManager = sessionManager; + } -[ - new Physics(), - new Collision({ width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }), - new WallBounds(Miscellaneous.WIDTH), -].forEach((system) => game.addSystem(system)); + public serve() { + if (!this.server) + this.server = Bun.serve<SessionData>({ + 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) + } + }); -[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity)); + this.messagePublisher.setServer(this.server); -game.start(); -setInterval(() => { - game.doGameLoop(performance.now()); -}, TICK_RATE); + console.log(`Listening on ${this.server.hostname}:${this.server.port}`); + } -const server = Bun.serve<>({ - port: 8080, - fetch(req, server) { - server.upgrade(req, { - data: {}, + private websocketMessage( + websocket: ServerWebSocket<SessionData>, + message: string | Uint8Array + ) { + if (typeof message == 'string') { + const receivedMessage = parse<ServerMessage>(message); + receivedMessage.sessionData = websocket.data; + + this.messageReceiver.addMessage(receivedMessage); + } + } + + private closeWebsocket(websocket: ServerWebSocket<SessionData>) { + const { sessionId } = websocket.data; + + const sessionEntities = + this.sessionManager.getSession(sessionId)!.controllableEntities; + this.sessionManager.removeSession(sessionId); + + if (!sessionEntities) return; + sessionEntities.forEach((id) => this.game.removeEntity(id)); + + this.messagePublisher.addMessage({ + type: MessageType.REMOVE_ENTITIES, + body: Array.from(sessionEntities) }); - }, - websocket: { - // handler called when a message is received - async message(ws, message) { - console.log(`Received ${message}`); - }, - }, -}); -console.log(`Listening on localhost:${server.port}`); + } + + private openWebsocket(websocket: ServerWebSocket<SessionData>) { + websocket.subscribe(Constants.GAME_TOPIC); + + const { sessionId } = websocket.data; + if (this.sessionManager.getSession(sessionId)) { + return; + } + + const newSession: Session = { + sessionId, + controllableEntities: new Set(), + inputSystem: new Input(sessionId) + }; + + const player = new Player(); + player.addComponent(new Control(sessionId)); + player.addComponent(new NetworkUpdateable()); + this.game.addEntity(player); + + newSession.controllableEntities.add(player.id); + this.sessionManager.putSession(sessionId, newSession); + + const addCurrentEntities: Message[] = [ + { + type: MessageType.NEW_ENTITIES, + body: Array.from(this.game.entities.values()) + .filter((entity) => entity.id != player.id) + .map((entity) => { + return { + id: entity.id, + entityName: entity.name, + args: entity.serialize() + }; + }) + } + ]; + websocket.sendText(stringify(addCurrentEntities)); + + const addNewPlayer: Message = { + type: MessageType.NEW_ENTITIES, + body: [ + { + id: player.id, + entityName: player.name, + args: player.serialize() + } + ] + }; + this.messagePublisher.addMessage(addNewPlayer); + } + + private fetchHandler(req: Request, server: Server): Response { + const url = new URL(req.url); + + const headers = new Headers(); + headers.set('Access-Control-Allow-Origin', '*'); + + if (url.pathname == '/assign') { + if (this.sessionManager.numSessions() > Constants.MAX_PLAYERS) + return new Response('too many players', { headers, status: 400 }); + + const sessionId = crypto.randomUUID(); + headers.set('Set-Cookie', `SessionId=${sessionId};`); + + return new Response(sessionId, { headers }); + } + + const cookie = req.headers.get('cookie'); + if (!cookie) { + return new Response('No session', { headers, status: 401 }); + } + + const sessionId = cookie.split(';').at(0)!.split('SessionId=').at(1); + + if (url.pathname == '/game') { + server.upgrade(req, { + headers, + data: { + sessionId + } + }); + + return new Response('upgraded to ws', { headers }); + } + + if (url.pathname == '/me') { + return new Response(sessionId, { headers }); + } + + return new Response('Not found', { headers, status: 404 }); + } +} diff --git a/server/tsconfig.json b/server/tsconfig.json index 29f8aa0..52f0ddc 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,21 +1,27 @@ { + "extends": "../tsconfig.engine.json", "compilerOptions": { - "lib": ["ESNext"], + // add Bun type definitions + "types": ["bun-types"], + + // enable latest features + "lib": ["esnext"], "module": "esnext", "target": "esnext", + + // if TS 5.x+ "moduleResolution": "bundler", - "moduleDetection": "force", + "noEmit": true, "allowImportingTsExtensions": true, + "moduleDetection": "force", + + "jsx": "react-jsx", // support JSX + "allowJs": true, // allow importing `.js` from `.ts` + "esModuleInterop": true, // allow default imports for CommonJS modules + + // best practices "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "preserve", - "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, - "allowJs": true, - "noEmit": true, - "types": [ - "bun-types" // add Bun global - ] + "skipLibCheck": true } } |