refactor server structure

This commit is contained in:
Elizabeth Hunt 2023-08-25 18:10:09 -06:00
parent 773ce84f4b
commit 2fbe0f0595
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
7 changed files with 208 additions and 141 deletions

6
server/src/constants.ts Normal file
View File

@ -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;
}

32
server/src/main.ts Normal file
View File

@ -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();

View File

@ -0,0 +1,8 @@
import { MessageProcessor } from '@engine/network';
import { ServerMessage } from '.';
export class ServerMessageProcessor implements MessageProcessor {
constructor() {}
public process(_message: ServerMessage) {}
}

View File

@ -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 = [];
}
}
}

View File

@ -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 = [];
}
}

View File

@ -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<string>;
};
export interface ServerMessage extends Message {
sessionData: SessionData;
}

View File

@ -1,110 +1,124 @@
import { Game } from '@engine/Game'; import { Game } from '@engine/Game';
import { EntityNames, Player } from '@engine/entities'; import { EntityNames, Player } from '@engine/entities';
import { WallBounds, Physics, Collision, NetworkUpdate } from '@engine/systems'; import { MessageType } from '@engine/network';
import { Constants } from './constants';
import { import {
type MessageQueueProvider, ServerSocketMessageReceiver,
type MessagePublisher, ServerSocketMessagePublisher,
MessageType, SessionData,
type MessageProcessor, ServerMessage,
type Message Session
} from '@engine/network'; } from './network';
import { stringify, parse } from '@engine/utils'; import { parse } from '@engine/utils';
import { Grid } from '@engine/structures'; import { Server, ServerWebSocket } from 'bun';
import { Miscellaneous } from '@engine/config';
import { Server } from 'bun';
const SERVER_PORT = 8080; export class GameServer {
const SERVER_TICK_RATE = (1 / 60) * 1000; private sessions: Map<string, Session>;
const GAME_TOPIC = 'game';
const MAX_PLAYERS = 8;
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 server?: Server;
private messages: Message[]; private game: Game;
private messageReceiver: ServerSocketMessageReceiver;
private messagePublisher: ServerSocketMessagePublisher;
constructor(server?: Server) { constructor(
if (server) { game: Game,
this.server = server; messageReceiver: ServerSocketMessageReceiver,
} messagePublisher: ServerSocketMessagePublisher
) {
this.sessions = new Map();
this.messages = []; this.game = game;
this.messageReceiver = messageReceiver;
this.messagePublisher = messagePublisher;
} }
public setServer(server: Server) { public serve() {
this.server = server; 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)
}
});
this.messagePublisher.setServer(this.server);
console.log(`Listening on ${this.server.hostname}:${this.server.port}`);
} }
public addMessage(message: Message) { private websocketMessage(
this.messages.push(message); websocket: ServerWebSocket<SessionData>,
} message: string | Uint8Array
) {
if (typeof message == 'string') {
const receivedMessage = parse<ServerMessage>(message);
receivedMessage.sessionData = websocket.data;
public publish() { this.messageReceiver.addMessage(receivedMessage);
if (this.messages.length) {
this.server?.publish(GAME_TOPIC, stringify(this.messages));
this.messages = [];
} }
} }
}
const game = new Game(); private closeWebsocket(websocket: ServerWebSocket<SessionData>) {
const { sessionId } = websocket.data;
const messageReceiver = new ServerSocketMessageReceiver(); const sessionEntities = this.sessions.get(sessionId)!.controllableEntities;
const messagePublisher = new ServerSocketMessagePublisher();
const messageProcessor = new ServerMessageProcessor();
const sessionControllableEntities: Map<string, Set<string>> = new Map();
const sessions = new Set<string>(); this.sessions.delete(sessionId);
const server = Bun.serve<SessionData>({ if (!sessionEntities) return;
port: SERVER_PORT, this.messagePublisher.addMessage({
fetch: async (req, server): Promise<Response> => { type: MessageType.REMOVE_ENTITIES,
body: Array.from(sessionEntities)
});
}
private openWebsocket(websocket: ServerWebSocket<SessionData>) {
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> | Response {
const url = new URL(req.url); const url = new URL(req.url);
const headers = new Headers(); const headers = new Headers();
headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Origin', '*');
if (url.pathname == '/assign') { 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 }); return new Response('too many players', { headers, status: 400 });
const sessionId = crypto.randomUUID(); const sessionId = crypto.randomUUID();
headers.set('Set-Cookie', `SessionId=${sessionId};`); headers.set('Set-Cookie', `SessionId=${sessionId};`);
sessions.add(sessionId);
return new Response(sessionId, { headers }); return new Response(sessionId, { headers });
} }
@ -127,7 +141,7 @@ const server = Bun.serve<SessionData>({
} }
}); });
return new Response('upgraded', { headers }); return new Response('upgraded to ws', { headers });
} }
if (url.pathname == '/me') { if (url.pathname == '/me') {
@ -135,67 +149,5 @@ const server = Bun.serve<SessionData>({
} }
return new Response('Not found', { headers, status: 404 }); 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<ServerMessage>(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}`);