summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rwxr-xr-xserver/bun.lockbbin1650 -> 1270 bytes
-rw-r--r--server/package.json3
-rw-r--r--server/src/constants.ts6
-rw-r--r--server/src/main.ts59
-rw-r--r--server/src/network/MessageProcessor.ts36
-rw-r--r--server/src/network/MessagePublisher.ts31
-rw-r--r--server/src/network/MessageReceiver.ts22
-rw-r--r--server/src/network/SessionInputSystem.ts32
-rw-r--r--server/src/network/SessionManager.ts33
-rw-r--r--server/src/network/index.ts29
-rw-r--r--server/src/server.ts197
-rw-r--r--server/tsconfig.json28
12 files changed, 433 insertions, 43 deletions
diff --git a/server/bun.lockb b/server/bun.lockb
index 7f8b5ce..28b67ce 100755
--- a/server/bun.lockb
+++ b/server/bun.lockb
Binary files differ
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
}
}