Skip to content

Commit

Permalink
initial p2p test
Browse files Browse the repository at this point in the history
  • Loading branch information
IsaacThoman committed Jan 28, 2025
1 parent 9fbfaca commit 1f48d82
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 5 deletions.
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"build": "deno run -A --node-modules-dir npm:vite build",
"preview": "deno run -A --node-modules-dir npm:vite preview",
"serve": "deno run --allow-net --allow-read jsr:@std/http@1/file-server dist/",
"start": "deno task build && deno run --allow-read --allow-env --allow-net --allow-write main.ts"
"start": "deno task build && deno run --allow-read --allow-env --allow-net --allow-write --unstable-kv main.ts"
},
"compilerOptions": {
"lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"]
Expand Down
36 changes: 36 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 { PeerListManager } from './managers/PeerListManager.ts';

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

constructor() {
this.mapData = this.loadMapData();
Expand All @@ -32,6 +34,7 @@ export class GameServer {
this.damageSystem = new DamageSystem(this.playerManager, this.chatManager);

this.playerManager.setItemManager(this.itemManager);
this.peerListManager = new PeerListManager();

this.setupSocketIO();
this.setupRoutes();
Expand Down Expand Up @@ -130,6 +133,39 @@ export class GameServer {
}
});

this.router.post('/api/shareServerList', async (ctx) => {
try {
const urls = await ctx.request.body.json() as string[];

if (Array.isArray(urls)) {
for (const url of urls) {
if (url.startsWith('http')) {
await this.peerListManager.addToQueue(url);
}
}
}

ctx.response.status = 200;
} catch (error) {
console.error('Error handling /api/shareServerList:', error);
ctx.response.status = 400;
ctx.response.body = 'Invalid request body';
}
});

this.router.post('/api/selfCheck', async (ctx) => {
try {
const { secret } = await ctx.request.body.json() as { secret: number };
const isValid = this.peerListManager.validateSelfCheck(secret);

ctx.response.status = isValid ? 200 : 400;
} catch (error) {
console.error('Error handling /api/selfCheck:', error);
ctx.response.status = 400;
ctx.response.body = 'Invalid request body';
}
});

this.app.use(this.router.routes());
this.app.use(this.router.allowedMethods());
}
Expand Down
11 changes: 7 additions & 4 deletions src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ const defaults = {
SERVER_URL: 'https://example.com',
SERVER_DEFAULT_MAP: 'crackhouse_1',
SERVER_TICK_RATE: '15',
SERVER_CLEANUP_INTERVAL: '1000',
SERVER_CLEANUP_INTERVAL: '1', // 1 second
SERVER_PEER_UPDATE_INTERVAL: '1800', // 30 minutes in seconds
SERVER_SELF_CHECK_INTERVAL: '30', // 30 seconds

// Player settings
PLAYER_DISCONNECT_TIME: '10',
PLAYER_AFK_KICK_TIME: '600',
PLAYER_MAX_HEALTH: '100',
PLAYER_BASE_INVENTORY: '[]',

//Game settings
// Game settings
GAME_MODE: 'ffa',
GAME_MAX_PLAYERS: '20',
RESPAWN_DELAY: '10',
Expand All @@ -23,7 +25,7 @@ const defaults = {
HEALTH_REGEN_DELAY: '6',
HEALTH_REGEN_RATE: '5',

//Item settings
// Item settings
MAX_ITEMS_IN_WORLD: '10',
ITEM_RESPAWN_TIME: '7',
};
Expand Down Expand Up @@ -57,7 +59,6 @@ async function updateEnvFile(defaults: Record<string, string>) {
return finalEnv;
}

// Parse specific types from string values
function parseConfig(env: Record<string, string>) {
return {
server: {
Expand All @@ -67,6 +68,8 @@ function parseConfig(env: Record<string, string>) {
defaultMap: env.SERVER_DEFAULT_MAP,
tickRate: parseInt(env.SERVER_TICK_RATE),
cleanupInterval: parseInt(env.SERVER_CLEANUP_INTERVAL),
peerUpdateInterval: parseInt(env.SERVER_PEER_UPDATE_INTERVAL),
selfCheckInterval: parseInt(env.SERVER_SELF_CHECK_INTERVAL),
},
game: {
mode: env.GAME_MODE,
Expand Down
202 changes: 202 additions & 0 deletions src/server/managers/PeerListManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/// <reference lib="deno.unstable" />

import { ServerInfo } from '../models/ServerInfo.ts';
import config from '../config.ts';

interface ServerEntry {
url: string;
info: ServerInfo;
lastUpdated: number;
}

export class PeerListManager {
private kv!: Deno.Kv;
private queue: Set<string> = new Set();
private staleUrls: Map<string, number> = new Map(); // Track stale URLs for re-checking
private processing = false;
private secret?: number;
private secretExpiration = 0;
private healthCheckAttempts = 0;
private isHealthy = false;

constructor() {
this.initializeKv().then(() => {
this.initializeFromFile();
this.runHealthChecks(); // Start health checks
});
}

private async initializeKv() {
this.kv = await Deno.openKv();
}

private async initializeFromFile() {
try {
// Use async file operations instead of sync
const urls = (await Deno.readTextFile('./servers.txt'))
.split('\n')
.map((url) => this.normalizeUrl(url))
.filter((url) => url && url.startsWith('http'));

// Load existing KV entries on startup
const existingEntries = this.kv.list<ServerEntry>({ prefix: ['servers'] });
for await (const entry of existingEntries) {
this.queue.add(entry.value.url);
}

// Add URLs from the file to the queue
for (const url of urls) {
await this.addToQueue(url);
}
} catch {
await Deno.writeTextFile('./servers.txt', 'https://bridge.candiru.xyz\n');
}
}

private normalizeUrl(url: string): string {
try {
return new URL(url).href.replace(/\/$/, ''); // Normalize URL and remove trailing slashes
} catch {
return ''; // Invalid URL, return empty string
}
}

private startQueueProcessor() {
this.processQueue(); // Process immediately once
return setInterval(() => this.processQueue(), config.server.peerUpdateInterval * 1000);
}

private async processQueue() {
if (!this.isHealthy || this.processing) return;
this.processing = true;

// Process stale entries first
const now = Date.now();
for (const [url, expiry] of this.staleUrls) {
if (now >= expiry) {
this.queue.add(url);
this.staleUrls.delete(url);
}
}

// Process the queue
for (const url of this.queue) {
const entry = await this.kv.get<ServerEntry>(['servers', url]);
const needsUpdate = !entry.value ||
Date.now() - entry.value.lastUpdated > config.server.peerUpdateInterval * 1000;
if (needsUpdate) {
await this.checkServer(url);
}
}

this.processing = false;
}

private async checkServer(url: string) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
console.log(`Checking ${url}`);
const response = await fetch(`${url}/api/getInfo`, {
signal: controller.signal,
});

// Validate response format
const info: ServerInfo = await response.json();
if (!this.validateServerInfo(info)) {
throw new Error('Invalid server response');
}

// Update lastUpdated even if info hasn't changed
await this.kv.set(['servers', url], {
url,
info,
lastUpdated: Date.now(),
});

await this.shareWithServer(url);
} catch (error) {
console.error(`Failed to check ${url}:`, error);
// Retry logic with backoff
const retryTime = Date.now() + (30 * 60 * 1000); // 30 min default
this.staleUrls.set(url, retryTime);
this.queue.delete(url);
} finally {
clearTimeout(timeout);
}
}

private validateServerInfo(obj: any): obj is ServerInfo {
return obj && typeof obj === 'object' &&
'version' in obj &&
'playerCount' in obj;
}

private async shareWithServer(url: string) {
const entries = this.kv.list<ServerEntry>({ prefix: ['servers'] });
const urls: string[] = [];
for await (const entry of entries) {
if (entry.value.url !== url) { // Exclude self from sharing
urls.push(entry.value.url);
}
}
try {
console.log(`Sharing with ${url}`);
await fetch(`${url}/api/shareServerList`, {
method: 'POST',
body: JSON.stringify(urls),
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error(`Failed to share with ${url}:`, error);
}
}

public async addToQueue(url: string) {
const normalized = this.normalizeUrl(url);
if (normalized === this.normalizeUrl(config.server.url)) return; // Skip self

const existing = await this.kv.get(['servers', normalized]);
if (!existing.value && !this.queue.has(normalized)) {
console.log(`Adding ${normalized} to queue`);
this.queue.add(normalized);
}
}

private async runHealthChecks() {
const checkHealth = async () => {
const success = await this.performHealthCheck();
if (success) {
this.isHealthy = true;
this.startQueueProcessor();
}
// Continue checking periodically regardless of success
setTimeout(checkHealth, config.server.selfCheckInterval * 1000);
};

await checkHealth();
}

private async performHealthCheck() {
this.secret = Math.random();
this.secretExpiration = Date.now() + 5000; // 5-second window for secret validity
try {
await fetch(`${config.server.url}/api/selfCheck`, {
method: 'POST',
body: JSON.stringify({ secret: this.secret }),
headers: { 'Content-Type': 'application/json' },
});
console.log('Health check passed');
return true;
} catch (error) {
console.error('Health check failed:', error);
return false;
}
}

public validateSelfCheck(secret: number) {
const isValid = secret === this.secret && Date.now() < this.secretExpiration;
this.secret = undefined; // Invalidate after use
return isValid;
}
}

0 comments on commit 1f48d82

Please sign in to comment.