Skip to content

Commit

Permalink
first p2p draft
Browse files Browse the repository at this point in the history
  • Loading branch information
IsaacThoman committed Jan 28, 2025
1 parent cec4653 commit 553b24a
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 0 deletions.
15 changes: 15 additions & 0 deletions src/server/DataValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'https://deno.land/x/[email protected]/mod.ts';
import { ChatMessage } from './models/ChatMessage.ts';
import { DamageRequest } from './models/DamageRequest.ts';
import { Player, PlayerData } from '../shared/Player.ts';
import { ServerInfo } from './models/ServerInfo.ts';

export class DataValidator {
private static SERVER_VERSION = '';
Expand Down Expand Up @@ -83,6 +84,20 @@ export class DataValidator {
damage: z.number(),
}).strict();

static serverInfoSchema = z.object({
name: z.string(),
maxPlayers: z.number(),
currentPlayers: z.number(),
mapName: z.string(),
tickRate: z.number(),
version: z.string(),
gameMode: z.string(),
playerMaxHealth: z.number(),
skyColor: z.string(),
tickComputeTime: z.number(),
cleanupComputeTime: z.number(),
}).strict().transform((data) => Object.assign(new ServerInfo(), data));

static validatePlayerData(data: PlayerData) {
return DataValidator.playerDataSchema.safeParse(data);
}
Expand Down
24 changes: 24 additions & 0 deletions src/server/GameServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DamageSystem } from './managers/DamageSystem.ts';
import { MapData } from './models/MapData.ts';
import { DataValidator } from './DataValidator.ts';
import { CustomServer } from '../shared/messages.ts';
import { PeerManager } from './managers/PeerManager.ts';

export class GameServer {
router: Router = new Router();
Expand All @@ -23,6 +24,7 @@ export class GameServer {
chatManager: ChatManager;
damageSystem: DamageSystem;
mapData: MapData;
peerManager: PeerManager;

constructor() {
this.mapData = this.loadMapData();
Expand All @@ -33,6 +35,8 @@ export class GameServer {

this.playerManager.setItemManager(this.itemManager);

this.peerManager = new PeerManager();

this.setupSocketIO();
this.setupRoutes();

Expand Down Expand Up @@ -130,6 +134,26 @@ export class GameServer {
}
});

this.router.get('/api/healthcheck', (context) => {
const secret = context.request.headers.get('X-Health-Secret');
if (secret === this.peerManager.healthSecret) {
context.response.status = 200;
} else {
context.response.status = 403;
}
});

this.router.post('/api/shareServerList', async (context) => {
try {
const body = await context.request.body.json();
const urls: string[] = Array.isArray(body) ? body : [];
this.peerManager.handleIncomingServers(urls);
context.response.status = 200;
} catch {
context.response.status = 400;
}
});

this.app.use(this.router.routes());
this.app.use(this.router.allowedMethods());
}
Expand Down
16 changes: 16 additions & 0 deletions src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ const defaults = {
SERVER_TICK_RATE: '15',
SERVER_CLEANUP_INTERVAL: '1000',

// Peer settings
PEER_UPDATE_INTERVAL: '5',
PEER_SHARE_INTERVAL: '5',
PEER_MAX_FAILED_ATTEMPTS: '3',
PEER_STALE_THRESHOLD: '60',
PEER_MAX_SERVERS: '200',
PEER_HEALTHCHECK_RETRIES: '3',

// Player settings
PLAYER_DISCONNECT_TIME: '10',
PLAYER_AFK_KICK_TIME: '600',
Expand Down Expand Up @@ -68,6 +76,14 @@ function parseConfig(env: Record<string, string>) {
tickRate: parseInt(env.SERVER_TICK_RATE),
cleanupInterval: parseInt(env.SERVER_CLEANUP_INTERVAL),
},
peer: {
updateInterval: parseInt(env.PEER_UPDATE_INTERVAL),
shareInterval: parseInt(env.PEER_SHARE_INTERVAL),
maxFailedAttempts: parseInt(env.PEER_MAX_FAILED_ATTEMPTS),
staleThreshold: parseInt(env.PEER_STALE_THRESHOLD),
maxServers: parseInt(env.PEER_MAX_SERVERS),
healthcheckRetries: parseInt(env.PEER_HEALTHCHECK_RETRIES),
},
game: {
mode: env.GAME_MODE,
maxPlayers: parseInt(env.GAME_MAX_PLAYERS),
Expand Down
156 changes: 156 additions & 0 deletions src/server/managers/PeerManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Peer } from '../models/Peer.ts';
import { DataValidator } from '../DataValidator.ts';
import config from '../config.ts';
import { ServerInfo } from '../models/ServerInfo.ts';

export class PeerManager {
private peers: Peer[] = [];
private updateQueue: string[] = [];
private shareQueue: string[] = [];
healthSecret: string;
private serversFilePath = './servers.txt';

constructor() {
this.healthSecret = crypto.randomUUID();
this.initialize();
}

private async initialize() {
await this.healthCheck();
await this.loadServersFile();
this.startQueueProcessors();
}

private async healthCheck() {
const retries = config.peer.healthcheckRetries;
const delay = config.server.cleanupInterval;

for (let i = 0; i < retries; i++) {
try {
const response = await fetch(`${config.server.url}/api/healthcheck`, {
headers: { 'X-Health-Secret': this.healthSecret },
});
if (response.ok) return;
} catch {
console.error('Healthcheck failed');
}

if (i < retries - 1) await new Promise((r) => setTimeout(r, delay));
}

console.error('Failed healthcheck after retries');
}

private async loadServersFile() {
try {
const content = await Deno.readTextFile(this.serversFilePath);
this.updateQueue = content.split('\n').filter((url) => url.trim());
} catch {
await Deno.writeTextFile(this.serversFilePath, 'https://bridge.candiru.xyz\n');
this.updateQueue = ['https://bridge.candiru.xyz'];
}
}

private startQueueProcessors() {
setInterval(() => this.processUpdateQueue(), config.peer.updateInterval * 1000);
setInterval(() => this.processShareQueue(), config.peer.shareInterval * 1000);
setInterval(() => this.checkStalePeers(), config.server.cleanupInterval);
}

private async processUpdateQueue() {
const url = this.updateQueue.shift();
if (!url) return;

try {
const response = await fetch(`${url}/api/getInfo`);
const data = await response.json();
const result = DataValidator.serverInfoSchema.safeParse(data);

if (result.success) {
let peer = this.peers.find((p) => p.url === url);
if (!peer) {
peer = new Peer(url);
this.peers.push(peer);
await this.addToServersFile(url);
}
peer.updateServerInfo(result.data);
} else {
this.handleFailedUpdate(url);
}
} catch {
this.handleFailedUpdate(url);
} finally {
this.updateQueue.push(url);
}
}

private async processShareQueue() {
const url = this.shareQueue.shift();
if (!url) return;

try {
const serverList = this.peers
.filter((p) => p.serverInfo)
.map((p) => p.url)
.slice(0, config.peer.maxServers);

await fetch(`${url}/api/shareServerList`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serverList),
});
} finally {
this.shareQueue.push(url);
}
}

private checkStalePeers() {
const stalePeers = this.peers.filter((p) =>
p.isStale(config.peer.staleThreshold) ||
(Date.now() / 1000 - p.lastShare) > config.peer.shareInterval * 1000
);

stalePeers.forEach((peer) => {
if (peer.isStale(config.peer.staleThreshold)) {
this.updateQueue.push(peer.url);
}
if ((Date.now() / 1000 - peer.lastShare) > config.peer.shareInterval * 1000) {
this.shareQueue.push(peer.url);
}
});

this.peers = this.peers.filter((p) => !p.hasExceededFailures(config.peer.maxFailedAttempts));
}

private handleFailedUpdate(url: string) {
const peer = this.peers.find((p) => p.url === url);
if (peer) {
peer.failedAttempts++;
if (peer.hasExceededFailures(config.peer.maxFailedAttempts)) {
this.peers = this.peers.filter((p) => p.url !== url);
}
}
}

private async addToServersFile(url: string) {
const current = await Deno.readTextFile(this.serversFilePath);
const urls = current.split('\n').filter((u) => u.trim());

if (!urls.includes(url)) {
urls.push(url);
if (urls.length > config.peer.maxServers) urls.shift();
await Deno.writeTextFile(this.serversFilePath, urls.join('\n'));
}
}

public handleIncomingServers(urls: string[]) {
urls.forEach((url) => {
if (
!this.updateQueue.includes(url) &&
!this.peers.some((p) => p.url === url)
) {
this.updateQueue.push(url);
}
});
}
}
27 changes: 27 additions & 0 deletions src/server/models/Peer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ServerInfo } from './ServerInfo.ts';

export class Peer {
public url: string;
public serverInfo?: ServerInfo;
public lastUpdate: number = 0;
public failedAttempts: number = 0;
public lastShare: number = 0;

constructor(url: string) {
this.url = url;
}

updateServerInfo(info: ServerInfo) {
this.serverInfo = info;
this.lastUpdate = Date.now() / 1000;
this.failedAttempts = 0;
}

isStale(threshold: number) {
return (Date.now() / 1000 - this.lastUpdate) > threshold;
}

hasExceededFailures(max: number) {
return this.failedAttempts >= max;
}
}

0 comments on commit 553b24a

Please sign in to comment.