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 { 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<string, Session>;
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<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) {
this.messages.push(message);
}
private websocketMessage(
websocket: ServerWebSocket<SessionData>,
message: string | Uint8Array
) {
if (typeof message == 'string') {
const receivedMessage = parse<ServerMessage>(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<SessionData>) {
const { sessionId } = websocket.data;
const messageReceiver = new ServerSocketMessageReceiver();
const messagePublisher = new ServerSocketMessagePublisher();
const messageProcessor = new ServerMessageProcessor();
const sessionControllableEntities: Map<string, Set<string>> = new Map();
const sessionEntities = this.sessions.get(sessionId)!.controllableEntities;
const sessions = new Set<string>();
this.sessions.delete(sessionId);
const server = Bun.serve<SessionData>({
port: SERVER_PORT,
fetch: async (req, server): Promise<Response> => {
if (!sessionEntities) return;
this.messagePublisher.addMessage({
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 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<SessionData>({
}
});
return new Response('upgraded', { headers });
return new Response('upgraded to ws', { headers });
}
if (url.pathname == '/me') {
@ -135,67 +149,5 @@ const server = Bun.serve<SessionData>({
}
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}`);
}