import { MinecraftConfig } from '../types'; import { FileUtils } from '../utils/FileUtils'; import { Logger } from '../utils/Logger'; import * as path from 'path'; import * as fs from 'fs-extra'; import * as yaml from 'js-yaml'; export interface GeyserConfig { bedrock: { port: number; address: string; }; remote: { address: string; port: number; authType: string; }; floodgateKeyFile: string; passthroughMotd: boolean; passthroughPlayerCounts: boolean; passthroughProtocolName: boolean; } export class GeyserBridge { private logger = Logger.getInstance(); public async setup(config: MinecraftConfig, isPterodactyl: boolean = false): Promise { const pluginsDir = config.folders.plugins; await FileUtils.ensureDir(pluginsDir); this.logger.info('Setting up Geyser & Floodgate...'); await this.cleanupRemappedFolders(pluginsDir); await this.downloadGeyser(pluginsDir); await this.downloadFloodgate(pluginsDir); if (isPterodactyl) { await this.configureGeyserForPterodactyl(config, pluginsDir); } else { await this.configureGeyser(config, pluginsDir); } this.logger.success('Geyser & Floodgate installed'); } private async cleanupRemappedFolders(pluginsDir: string): Promise { try { const files = await fs.readdir(pluginsDir); for (const file of files) { if (file.includes('.paper-remapped')) { const remappedPath = path.join(pluginsDir, file); const stat = await fs.stat(remappedPath); if (stat.isDirectory()) { this.logger.warning(`Removing corrupt remapped folder: ${file}`); await fs.remove(remappedPath); } } } } catch (error) { } } private async downloadGeyser(pluginsDir: string): Promise { const geyserJar = path.join(pluginsDir, 'Geyser-Spigot.jar'); const possibleNames = ['geyser.jar', 'Geyser.jar', 'Geyser-Spigot.jar']; for (const name of possibleNames) { const oldPath = path.join(pluginsDir, name); if (await FileUtils.fileExists(oldPath)) { await fs.remove(oldPath); } } this.logger.info('Downloading Geyser...'); try { const response = await require('axios')({ method: 'GET', url: 'https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest/downloads/spigot', responseType: 'stream', timeout: 60000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); const writer = fs.createWriteStream(geyserJar); response.data.pipe(writer); await new Promise((resolve, reject) => { writer.on('finish', () => resolve()); writer.on('error', (err) => reject(err)); }); const stats = await fs.stat(geyserJar); if (stats.size < 100000) { throw new Error(`Downloaded file too small: ${stats.size} bytes`); } this.logger.success(`Geyser downloaded (${(stats.size / 1024 / 1024).toFixed(2)} MB)`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to download Geyser: ${errorMessage}`); throw error; } } private async downloadFloodgate(pluginsDir: string): Promise { const floodgateJar = path.join(pluginsDir, 'floodgate.jar'); if (await FileUtils.fileExists(floodgateJar)) { await fs.remove(floodgateJar); } this.logger.info('Downloading Floodgate...'); try { const response = await require('axios')({ method: 'GET', url: 'https://download.geysermc.org/v2/projects/floodgate/versions/latest/builds/latest/downloads/spigot', responseType: 'stream', timeout: 60000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); const writer = fs.createWriteStream(floodgateJar); response.data.pipe(writer); await new Promise((resolve, reject) => { writer.on('finish', () => resolve()); writer.on('error', (err) => reject(err)); }); const stats = await fs.stat(floodgateJar); if (stats.size < 100000) { throw new Error(`Downloaded file too small: ${stats.size} bytes`); } this.logger.success(`Floodgate downloaded (${(stats.size / 1024 / 1024).toFixed(2)} MB)`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to download Floodgate: ${errorMessage}`); throw error; } } private async configureGeyser(config: MinecraftConfig, pluginsDir: string): Promise { const geyserConfigPath = path.join(pluginsDir, 'Geyser-Spigot', 'config.yml'); await fs.ensureDir(path.dirname(geyserConfigPath)); const geyserConfig: GeyserConfig = { bedrock: { port: config.network.bedrockPort || 19132, address: config.network.ip }, remote: { address: '127.0.0.1', port: config.network.port, authType: 'floodgate' }, floodgateKeyFile: 'floodgate/key.pem', passthroughMotd: true, passthroughPlayerCounts: true, passthroughProtocolName: true }; await fs.writeFile(geyserConfigPath, yaml.dump(geyserConfig)); this.logger.debug('Geyser configured for standalone mode'); } private async configureGeyserForPterodactyl(config: MinecraftConfig, pluginsDir: string): Promise { const geyserConfigPath = path.join(pluginsDir, 'Geyser-Spigot', 'config.yml'); await fs.ensureDir(path.dirname(geyserConfigPath)); const pterodactylPort = parseInt(process.env.SERVER_PORT || config.network.port.toString()); const hasSeparateBedrockPort = !!(process.env.BEDROCK_PORT || config.network.bedrockPort); let geyserConfig: GeyserConfig; if (hasSeparateBedrockPort) { geyserConfig = { bedrock: { port: parseInt(process.env.BEDROCK_PORT || config.network.bedrockPort?.toString() || '19132'), address: '0.0.0.0' }, remote: { address: '127.0.0.1', port: pterodactylPort, authType: 'floodgate' }, floodgateKeyFile: 'floodgate/key.pem', passthroughMotd: true, passthroughPlayerCounts: true, passthroughProtocolName: true }; this.logger.info(`Geyser configured with separate Bedrock port: ${geyserConfig.bedrock.port}`); } else { geyserConfig = { bedrock: { port: pterodactylPort, address: '0.0.0.0' }, remote: { address: '127.0.0.1', port: pterodactylPort, authType: 'floodgate' }, floodgateKeyFile: 'floodgate/key.pem', passthroughMotd: true, passthroughPlayerCounts: true, passthroughProtocolName: true }; this.logger.info(`Geyser configured for port sharing on ${pterodactylPort} (TCP+UDP required)`); this.logger.warning('Make sure Pterodactyl panel has UDP enabled for this port'); } await fs.writeFile(geyserConfigPath, yaml.dump(geyserConfig)); this.logger.debug('Geyser configured for Pterodactyl mode'); } public stop(): void { } }