UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,066 lines 116 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServerType = exports.ServerManagerFeatures = exports.ServerVersionVariants = void 0; /** * ARCHITECTURE DOCUMENTATION: ServerManager - Dedicated Server Orchestration * =========================================================================== * * ServerManager is the central orchestrator for Minecraft Bedrock Dedicated Server * instances. It handles downloading, provisioning, and managing the lifecycle of * one or more DedicatedServer instances. * * ## Core Responsibilities * * 1. **Server Version Management**: Download and track Bedrock Dedicated Server versions * 2. **Server Provisioning**: Create runtime server instances from source downloads * 3. **Multi-Slot Support**: Manage multiple concurrent server instances on different ports * 4. **Pack Cache Management**: Extract and cache add-on packages for deployment * 5. **HTTP Server Integration**: Host web API and WebSocket notifications via HttpServer * 6. **Event Aggregation**: Bubble up events from individual DedicatedServer instances * * ## Server Download & Source Architecture * * The architecture separates "source" servers from "runtime" servers: * * ``` * minecraft.net Source Servers Runtime Servers * │ │ │ * │ downloadLatestSourceServer │ │ * ├───────────────────────────►│ bwv1.21.50.24/ │ * │ │ ├─ bedrock_server.exe │ * │ │ ├─ behavior_packs/ │ prepareTempServerNameAndPath * │ │ ├─ resource_packs/ ├──────────────────────────► * │ │ ├─ definitions/ │ srv20260101120000/ * │ │ └─ ... │ ├─ bedrock_server.exe (copy) * │ │ │ ├─ behavior_packs/ (symlink) * │ │ │ ├─ resource_packs/ (symlink) * │ │ │ ├─ development_behavior_packs/ * │ │ │ ├─ development_resource_packs/ * │ │ │ └─ worlds/defaultWorld/ * ``` * * **Source Servers** (stored in `%LOCALAPPDATA%/mctools_server/serverSources/`): * - Downloaded once per version from minecraft.net * - Named with format: `<type>v<version>/` (e.g., `bwv1.21.50.24/` for Bedrock Windows) * - Type prefixes: `bw` (Bedrock Windows), `bl` (Bedrock Linux), `pw`/`pl` (Preview) * - Contains the original, unmodified server files * * **Runtime Servers** (stored in `%LOCALAPPDATA%/mctools_server/servers/`): * - Created per-session with timestamp names: `srv<YYYYMMDDHHMMSS>/` * - Use symbolic links (junctions on Windows) to share static content with source * - Have their own writable folders for worlds, config, and development packs * * ## Symbolic Link Strategy * * To minimize disk usage and allow quick server provisioning, runtime servers use * symbolic links to reference static content from source servers: * * | Folder/File | Strategy | Reason | * |--------------------------|--------------|------------------------------------------| * | bedrock_server[.exe] | File COPY | Symlinks don't work reliably for files | * | behavior_packs/ | Symlink | Read-only vanilla content | * | resource_packs/ | Symlink | Read-only vanilla content | * | definitions/ | Symlink | Read-only vanilla definitions | * | structures/ | Symlink | Read-only vanilla structures | * | world_templates/ | Symlink | Read-only vanilla world templates | * | development_behavior_packs/ | Real folder | Writable, for deployed add-ons | * | development_resource_packs/ | Real folder | Writable, for deployed add-ons | * | worlds/ | Real folder | Writable, for world data | * | config/ | Real folder | Writable, for server config | * * **Platform-specific symlink handling:** * - Windows: Uses "junction" symlinks (work without admin/developer mode) * - Linux/macOS: Uses "dir" symlinks (work without elevated privileges) * - Fallback: If symlink creation fails, directories are copied instead * * ## Smart Reprovisioning & Backup Discipline * * To optimize stop/start cycles and protect world data, the system implements: * * **Smart Reprovisioning Detection** (`needsReprovisioning()`): * - Tracks provisioning info per slot: source path, version, timestamp * - On start, compares requested source with last-provisioned source * - If same source and folder exists: SKIP file operations (fast restart) * - If different source: REPROVISION with backup first * * **Backup Before Destructive Operations** (`backupSlotWorldData()`): * Before replacing/reprovisioning a slot's server files: * 1. Locate the default world folder (e.g., `slot0/worlds/defaultWorld/`) * 2. Create timestamped backup: `worldBackups/slot0/backup_YYYYMMDD_HHMMSS/` * 3. Copy world data to backup folder * 4. Only then proceed with destructive file operations * * **Slot Update Helpers**: * - `updateDedicatedServerSymLinkFolder()`: Removes old symlink/dir, creates fresh symlink * - `updateDedicatedServerFile()`: Overwrites file in place (critical for firewall rules) * * This discipline ensures: * - No world data is ever lost during version updates * - Users can recover from failed updates via backups * - Stop/start cycles are fast (no unnecessary file operations) * - Source server version changes are handled gracefully * * ## Linux Compatibility * * The server management system supports both Windows and Linux: * * | Feature | Windows | Linux | * |--------------------------|--------------------------|------------------------------| * | Executable name | bedrock_server.exe | bedrock_server | * | Symlink type | junction | dir | * | Library path | (not needed) | LD_LIBRARY_PATH set | * | Executable permissions | (not needed) | chmod +x applied | * | Signature verification | Authenticode check | Skipped (not supported) | * * ## Pack Cache System * * Add-on packages (.mcaddon, .mcpack) are extracted to a cache folder to avoid * repeated extraction. The cache uses content hashes to identify unique versions: * * ``` * %LOCALAPPDATA%/mctools_pack_cache/ * └─ my_addon_<hash>/ * ├─ behavior_pack/ * └─ resource_pack/ * ``` * * During server provisioning, pack cache folders are referenced via symbolic links * in the runtime server's development_*_packs folders. * * ## Multi-Slot Architecture * * ServerManager supports running multiple server instances simultaneously using * a slot-based port allocation scheme: * * | Slot | Base Port | Use Case | * |------|-----------|---------------------------------------------| * | 0 | 19132 | Default Minecraft port | * | 1 | 19164 | Second server instance | * | 2 | 19196 | Third server instance | * | ... | +32/slot | Additional instances (up to slot 79) | * * Each slot has its own DedicatedServer instance, world backup folder, and * independent lifecycle. * * ### Slot Prefix for Context Isolation * * To prevent different commands/contexts from interfering with each other's * server instances, ServerManager supports a `slotPrefix` property. This prefix * is prepended to slot folder names: * * | Context | Prefix | Folder Names | * |----------------|----------|----------------------------------------| * | MCP command | "mcp" | mcp0/, mcp1/, ... | * | Serve command | "serve" | serve0/, serve1/, ... | * | VS Code ext | "vscode" | vscode0/, vscode1/, ... | * | Default | "" | slot0/, slot1/, ... (backward compat) | * * This ensures that running `mct mcp` doesn't reuse or interfere with servers * from `mct serve` or the VS Code extension, and vice versa. * * ## Version Detection * * Server versions are retrieved from two sources: * 1. Primary: minecraft.net version service API * 2. Fallback: Mojang bedrock-samples GitHub repository version.json * * Both retail and preview tracks are supported, with version info including: * - Version string (e.g., "1.21.50.24") * - Version index for comparison (derived from version components) * - Download URL prefix for each platform * * ## Related Files * * - DedicatedServer.ts: Individual server instance management * - HttpServer.ts: Web API and WebSocket server for remote management * - LocalEnvironment.ts: Environment configuration (paths, EULA acceptance) * - LocalUtilities.ts: Platform-specific path utilities * - ServerConfigManager.ts: Manages server config JSON files * - ServerPropertiesManager.ts: Manages server.properties file * - Package.ts: Represents cached add-on packages * * ## Key Methods * * - `downloadLatestSourceServer()`: Download Bedrock server from minecraft.net * - `prepareSlotServerPath()`: Create/update slot-based runtime server (smart reprovisioning) * - `needsReprovisioning()`: Check if a slot needs file operations * - `backupSlotWorldData()`: Backup world data before destructive operations * - `ensureActiveServer()`: Get or create a DedicatedServer for a slot * - `deployPackCache()`: Extract add-on packages to cache folder * - `preparePacksAndTemplates()`: Set up pack references for a world * - `updateDedicatedServerSymLinkFolder()`: Update symlink for a slot folder * - `updateDedicatedServerFile()`: Update a file in a slot folder (overwrite in place) * * ## Event Flow * * ServerManager aggregates events from all DedicatedServer instances and forwards * them to listeners and the HttpServer for WebSocket broadcast: * * ``` * DedicatedServer ──► ServerManager ──► HttpServer ──► WebSocket Clients * │ │ │ * │ onServerStarted │ bubbleServerStarted * │ onPlayerConnected│ bubblePlayerConnected ──► notifyPlayerJoined * │ onServerOutput │ pushStatusNotification ──► notifyStatusUpdate * ``` */ const ste_events_1 = require("ste-events"); const fs = __importStar(require("fs")); const os = __importStar(require("os")); const NodeStorage_1 = __importDefault(require("./NodeStorage")); const axios_1 = __importDefault(require("axios")); const ZipStorage_1 = __importDefault(require("../storage/ZipStorage")); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); const Log_1 = __importDefault(require("../core/Log")); const DedicatedServer_1 = __importStar(require("./DedicatedServer")); const Utilities_1 = __importDefault(require("../core/Utilities")); const ContentLogWatcher_1 = __importDefault(require("./ContentLogWatcher")); const HttpServer_1 = __importDefault(require("./HttpServer")); const ICreatorToolsData_1 = require("../app/ICreatorToolsData"); const Package_1 = __importDefault(require("../app/Package")); const Database_1 = __importDefault(require("../minecraft/Database")); const ServerMessage_1 = require("./ServerMessage"); const MinecraftUtilities_1 = __importDefault(require("../minecraft/MinecraftUtilities")); const Constants_1 = require("../core/Constants"); const ILocalUtilities_1 = require("../core/ILocalUtilities"); const WorldBackupManager_1 = __importDefault(require("./WorldBackupManager")); exports.ServerVersionVariants = 5; var ServerManagerFeatures; (function (ServerManagerFeatures) { ServerManagerFeatures[ServerManagerFeatures["all"] = 0] = "all"; ServerManagerFeatures[ServerManagerFeatures["allWebServices"] = 1] = "allWebServices"; ServerManagerFeatures[ServerManagerFeatures["basicWebServices"] = 2] = "basicWebServices"; ServerManagerFeatures[ServerManagerFeatures["dedicatedServerOnly"] = 3] = "dedicatedServerOnly"; })(ServerManagerFeatures || (exports.ServerManagerFeatures = ServerManagerFeatures = {})); var ServerType; (function (ServerType) { ServerType[ServerType["bedrockWindows"] = 0] = "bedrockWindows"; ServerType[ServerType["bedrockLinux"] = 1] = "bedrockLinux"; ServerType[ServerType["bedrockWindowsPreview"] = 2] = "bedrockWindowsPreview"; ServerType[ServerType["bedrockLinuxPreview"] = 3] = "bedrockLinuxPreview"; ServerType[ServerType["java"] = 4] = "java"; })(ServerType || (exports.ServerType = ServerType = {})); /** Filename for the slot sentinel/context file */ const SLOT_CONTEXT_FILENAME = "slot_context.json"; class ServerManager { #servers = {}; #activeServersByPort = {}; #quiescentServersByPort = {}; #activeDirectServer; #contentLogWatcher; #usePreview; #httpServer; #creatorTools; #utilities; #env; dataStorage; runOnce; maxServerIndex = 0; #features = ServerManagerFeatures.all; #isPrepared = false; primaryServerPort = 19132; backupWorldFileListings = {}; /** * Central backup manager for all world backups. * Handles world identity, backup creation, restoration, and deduplication. */ #worldBackupManager; /** * Get the world backup manager. */ get worldBackupManager() { return this.#worldBackupManager; } /** * Tracks provisioning state per slot. * Key is the slot number (0-79), value is the provisioning info. * Used to avoid unnecessary reprovisioning when source hasn't changed. */ #slotProvisioningInfo = {}; /** * Prefix for slot folder names to avoid conflicts between different contexts. * Different commands/contexts use different prefixes to keep their server instances separate: * - MCP command uses "mcp" prefix → "mcp0", "mcp1", etc. * - Serve command uses "serve" prefix → "serve0", "serve1", etc. * - VS Code extension uses "vscode" prefix → "vscode0", "vscode1", etc. * - Default (empty string) uses no prefix → "slot0", "slot1", etc. */ #slotPrefix = ""; serverVersions = [ { downloadPrefix: "https://www.minecraft.net/bedrockdedicatedserver/bin-win/bedrock-server-", }, { downloadPrefix: "https://www.minecraft.net/bedrockdedicatedserver/bin-linux/bedrock-server-", }, { downloadPrefix: "https://www.minecraft.net/bedrockdedicatedserver/bin-win-preview/bedrock-server-", }, { downloadPrefix: "https://www.minecraft.net/bedrockdedicatedserver/bin-linux-preview/bedrock-server-", }, {}, ]; #onServerOutput = new ste_events_1.EventDispatcher(); #onServerError = new ste_events_1.EventDispatcher(); #onServerStarted = new ste_events_1.EventDispatcher(); #onServerRefreshed = new ste_events_1.EventDispatcher(); #onServerStarting = new ste_events_1.EventDispatcher(); #onServerStopped = new ste_events_1.EventDispatcher(); #onServerStopping = new ste_events_1.EventDispatcher(); #onShutdown = new ste_events_1.EventDispatcher(); #onPlayerConnected = new ste_events_1.EventDispatcher(); #onPlayerDisconnected = new ste_events_1.EventDispatcher(); #onTestStarted = new ste_events_1.EventDispatcher(); #onTestFailed = new ste_events_1.EventDispatcher(); #onTestSucceeded = new ste_events_1.EventDispatcher(); // Debug events from the script debugger #onDebugConnected = new ste_events_1.EventDispatcher(); #onDebugDisconnected = new ste_events_1.EventDispatcher(); #onDebugStats = new ste_events_1.EventDispatcher(); #onDebugPaused = new ste_events_1.EventDispatcher(); #onDebugResumed = new ste_events_1.EventDispatcher(); #onProfilerCapture = new ste_events_1.EventDispatcher(); get isAnyServerRunning() { for (const serverName in this.#servers) { const server = this.#servers[serverName]; if (server.status === DedicatedServer_1.DedicatedServerStatus.started) { return true; } } return false; } get creatorTools() { return this.#creatorTools; } get effectiveAutoSourceServerPath() { if (this.#utilities.platform === ILocalUtilities_1.Platform.linux) { if (this.effectiveIsUsingPreview) { return this.serverVersions[ServerType.bedrockLinuxPreview].downloadedPath; } return this.serverVersions[ServerType.bedrockLinux].downloadedPath; } if (this.effectiveIsUsingPreview) { return this.serverVersions[ServerType.bedrockWindowsPreview].downloadedPath; } return this.serverVersions[ServerType.bedrockWindows].downloadedPath; } get activeDirectServer() { return this.#activeDirectServer; } get primaryActiveServer() { if (this.#activeDirectServer) { return this.#activeDirectServer; } return this.#activeServersByPort[this.primaryServerPort]; } /** * Get all active servers as an array. */ get activeServers() { return Object.values(this.#activeServersByPort); } get features() { return this.#features; } set features(featuresIn) { this.#features = featuresIn; } get onServerOutput() { return this.#onServerOutput.asEvent(); } get onServerError() { return this.#onServerError.asEvent(); } get onServerStarted() { return this.#onServerStarted.asEvent(); } get onServerRefreshed() { return this.#onServerRefreshed.asEvent(); } get onServerStarting() { return this.#onServerStarting.asEvent(); } get onServerStopped() { return this.#onServerStopped.asEvent(); } get onServerStopping() { return this.#onServerStopping.asEvent(); } get onTestStarted() { return this.#onTestStarted.asEvent(); } get onShutdown() { return this.#onShutdown.asEvent(); } get onTestFailed() { return this.#onTestFailed.asEvent(); } get onTestSucceeded() { return this.#onTestSucceeded.asEvent(); } get onDebugConnected() { return this.#onDebugConnected.asEvent(); } get onDebugDisconnected() { return this.#onDebugDisconnected.asEvent(); } get onDebugStats() { return this.#onDebugStats.asEvent(); } get onDebugPaused() { return this.#onDebugPaused.asEvent(); } get onDebugResumed() { return this.#onDebugResumed.asEvent(); } get onProfilerCapture() { return this.#onProfilerCapture.asEvent(); } get onPlayerConnected() { return this.#onPlayerConnected.asEvent(); } get onPlayerDisconnected() { return this.#onPlayerDisconnected.asEvent(); } get usePreview() { return this.#usePreview; } set usePreview(newUsePreview) { this.#usePreview = newUsePreview; } /** * Get the slot prefix used for folder naming. * This allows different contexts (MCP, serve, VS Code) to have isolated server slots. */ get slotPrefix() { return this.#slotPrefix; } /** * Set the slot prefix for folder naming. * Different contexts should use different prefixes: * - "mcp" for MCP command → "mcp0", "mcp1", etc. * - "serve" for serve command → "serve0", "serve1", etc. * - "vscode" for VS Code extension → "vscode0", "vscode1", etc. * - "" (empty) for default/backward compatibility → "slot0", "slot1", etc. */ set slotPrefix(prefix) { this.#slotPrefix = prefix; } /** * Gets the folder name for a given slot number, including the context prefix. * Examples: * - slotPrefix="" → "slot0", "slot1", etc. * - slotPrefix="mcp" → "mcp0", "mcp1", etc. * - slotPrefix="serve" → "serve0", "serve1", etc. * - slotPrefix="vscode" → "vscode0", "vscode1", etc. */ getSlotFolderName(slotNumber) { if (this.#slotPrefix) { return `${this.#slotPrefix}${slotNumber}`; } return `slot${slotNumber}`; } constructor(env, creatorTools) { this.#utilities = env.utilities; this.#env = env; this.#creatorTools = creatorTools; this.dataStorage = new NodeStorage_1.default(this.getRootPath() + "data/", ""); this.bubbleServerOutput = this.bubbleServerOutput.bind(this); this.bubblePlayerConnected = this.bubblePlayerConnected.bind(this); this.bubblePlayerDisconnected = this.bubblePlayerDisconnected.bind(this); this.bubbleServerError = this.bubbleServerError.bind(this); this.bubbleServerStarted = this.bubbleServerStarted.bind(this); this.bubbleServerStopped = this.bubbleServerStopped.bind(this); this.bubbleServerStarting = this.bubbleServerStarting.bind(this); this.bubbleServerStopping = this.bubbleServerStopping.bind(this); this.bubbleTestFailed = this.bubbleTestFailed.bind(this); this.bubbleTestStarted = this.bubbleTestStarted.bind(this); this.bubbleTestFailed = this.bubbleTestFailed.bind(this); this.bubbleServerRefreshed = this.bubbleServerRefreshed.bind(this); this.bubbleServerGameEvent = this.bubbleServerGameEvent.bind(this); this.bubbleDebugConnected = this.bubbleDebugConnected.bind(this); this.bubbleDebugDisconnected = this.bubbleDebugDisconnected.bind(this); this.bubbleDebugStats = this.bubbleDebugStats.bind(this); // Register process signal handlers for graceful shutdown // This ensures child server processes are stopped when the parent is terminated this.registerProcessSignalHandlers(); } /** * Register signal handlers to gracefully shutdown all servers when the process is terminated. * This prevents orphaned bedrock_server processes that could hold ports. */ registerProcessSignalHandlers() { const gracefulShutdown = async (signal) => { Log_1.default.message(`Received ${signal} signal - initiating graceful shutdown...`); try { await this.shutdown(`Process terminated by ${signal}`); Log_1.default.message("Graceful shutdown complete."); process.exit(0); } catch (e) { Log_1.default.error(`Error during graceful shutdown: ${e.message}`); process.exit(1); } }; // Handle common termination signals process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); process.on("SIGINT", () => gracefulShutdown("SIGINT")); // On Windows, handle the CTRL+BREAK and window close events if (os.platform() === "win32") { process.on("SIGBREAK", () => gracefulShutdown("SIGBREAK")); } // Handle uncaught exceptions - try to clean up before crashing process.on("uncaughtException", async (error) => { Log_1.default.error(`Uncaught exception: ${error.message}`); Log_1.default.error(error.stack || "No stack trace available"); try { await this.stopAllDedicatedServers(); } catch (e) { // Ignore cleanup errors during crash } process.exit(1); }); } async stopWebServer(reason) { if (this.#httpServer) { await this.#httpServer.stop(reason); } } async shutdown(message) { await this.stopWebServer(message); await this.stopAllDedicatedServers(); if (this.#onShutdown) { this.#onShutdown.dispatch(this, message); } } ensureHttpServer(port) { if (!this.#httpServer) { this.#httpServer = new HttpServer_1.default(this.#env, this); if (port) { this.#httpServer.port = port; } // Pass experimental SSL config if available (not persisted - command line only) if (this.#env.sslConfig) { this.#httpServer.sslConfig = this.#env.sslConfig; } this.#httpServer.creatorTools = this.#creatorTools; this.#httpServer.init(); } return this.#httpServer; } get environment() { return this.#env; } getRootPath() { let fullPath = __dirname; const lastSlash = Math.max(fullPath.lastIndexOf("\\", fullPath.length - 2), fullPath.lastIndexOf("/", fullPath.length - 2)); if (lastSlash >= 0) { fullPath = fullPath.substring(0, lastSlash + 1); } return fullPath; } ensureDedicatedServerForPath(name, serverPath) { let server = this.#servers[name]; if (server) { return server; } server = new DedicatedServer_1.default(name, this, this.#env, serverPath, this.#env.worldContainerStorage.rootFolder.ensureFolder("world")); server.onPlayerConnected.subscribe(this.bubblePlayerConnected); server.onPlayerDisconnected.subscribe(this.bubblePlayerDisconnected); server.onServerError.subscribe(this.bubbleServerError); server.onServerOutput.subscribe(this.bubbleServerOutput); server.onServerStarted.subscribe(this.bubbleServerStarted); server.onServerRefreshed.subscribe(this.bubbleServerRefreshed); server.onServerStarting.subscribe(this.bubbleServerStarting); server.onServerStopped.subscribe(this.bubbleServerStopped); server.onServerStopping.subscribe(this.bubbleServerStopping); server.onTestFailed.subscribe(this.bubbleTestFailed); server.onTestStarted.subscribe(this.bubbleTestStarted); server.onTestSucceeded.subscribe(this.bubbleTestSucceeded); server.onServerGameEvent.subscribe(this.bubbleServerGameEvent); // Subscribe to debug events server.onDebugConnected.subscribe(this.bubbleDebugConnected); server.onDebugDisconnected.subscribe(this.bubbleDebugDisconnected); server.onDebugStats.subscribe(this.bubbleDebugStats); server.onDebugPaused.subscribe(this.bubbleDebugPaused); server.onDebugResumed.subscribe(this.bubbleDebugResumed); server.onProfilerCapture.subscribe(this.bubbleProfilerCapture); this.#servers[name] = server; return server; } async stopAllDedicatedServers() { for (const serverName in this.#servers) { const server = this.#servers[serverName]; if (server.status === DedicatedServer_1.DedicatedServerStatus.started || server.status === DedicatedServer_1.DedicatedServerStatus.launching || server.status === DedicatedServer_1.DedicatedServerStatus.starting) { await server.stopServer(); } } return false; } bubblePlayerConnected(dedicatedServer, player) { this.#onPlayerConnected.dispatch(dedicatedServer, player); // Push WebSocket notification if (this.#httpServer) { const slot = this.getSlotForServer(dedicatedServer); this.#httpServer.notify({ eventName: "playerJoined", timestamp: Date.now(), slot: slot, playerName: player.id ?? "unknown", xuid: player.xuid, }); } } bubblePlayerDisconnected(dedicatedServer, player) { this.#onPlayerDisconnected.dispatch(dedicatedServer, player); // Push WebSocket notification if (this.#httpServer) { const slot = this.getSlotForServer(dedicatedServer); this.#httpServer.notify({ eventName: "playerLeft", timestamp: Date.now(), slot: slot, playerName: player.id ?? "unknown", xuid: player.xuid, }); } } /** * Handle game events from the dedicated server (e.g., PlayerTravelled, BlockBroken, etc.) * and relay them to WebSocket clients. */ bubbleServerGameEvent(dedicatedServer, event) { if (!this.#httpServer) { Log_1.default.verbose("bubbleServerGameEvent: No httpServer available"); return; } const slot = this.getSlotForServer(dedicatedServer); const eventWithHeader = event; Log_1.default.verbose("bubbleServerGameEvent received: " + eventWithHeader.header?.eventName); // Check if this is a PlayerTravelled event and send a more specific notification if (eventWithHeader.header?.eventName === "PlayerTravelled" && eventWithHeader.body?.player) { const player = eventWithHeader.body.player; if (player.position) { Log_1.default.message("Sending playerMoved notification for " + player.name + " to slot " + slot); this.#httpServer.notifyPlayerMoved(slot, player.name ?? "unknown", player.position, undefined, eventWithHeader.body.dimension !== undefined ? `dimension_${eventWithHeader.body.dimension}` : undefined); } } // Also send the full game event for clients that want detailed event data if (eventWithHeader.header?.eventName) { this.#httpServer.notifyGameEvent(slot, eventWithHeader.header.eventName, event); } } bubbleServerError(dedicatedServer, message) { this.#onServerError.dispatch(dedicatedServer, message); this.pushStatusNotification(dedicatedServer); } bubbleServerOutput(dedicatedServer, message) { // Don't forward internal system messages (e.g., querytarget polling output) to the web UI if (message.category === ServerMessage_1.ServerMessageCategory.internalSystemMessage) { return; } this.#onServerOutput.dispatch(dedicatedServer, message); // Push status update for new messages this.pushStatusNotification(dedicatedServer); } bubbleServerStarted(dedicatedServer, message) { this.#onServerStarted.dispatch(dedicatedServer, message); this.pushStatusNotification(dedicatedServer); // Start watching the server's storage folders for real-time file updates this.startWatchingServerStorage(dedicatedServer); } /** * Start watching the server's storage folders (world, behavior_packs, resource_packs) * for file changes and broadcast notifications to WebSocket clients. */ async startWatchingServerStorage(dedicatedServer) { if (!this.#httpServer) { return; } // Get the slot number for this server const slot = this.getSlotForServer(dedicatedServer); // Ensure server folders are initialized await dedicatedServer.ensureServerFolders(); // Start watching world storage const worldStorage = dedicatedServer.defaultWorldStorage; if (worldStorage) { this.#httpServer.startWatchingStorage(worldStorage, slot, "world"); Log_1.default.message(`[ServerManager] Started watching world storage for slot ${slot}: ${worldStorage.rootFolder.fullPath}`); } else { Log_1.default.debug(`[ServerManager] No world storage available for slot ${slot}`); } // Start watching behavior packs storage const behaviorPacksStorage = dedicatedServer.behaviorPacksStorage; if (behaviorPacksStorage) { this.#httpServer.startWatchingStorage(behaviorPacksStorage, slot, "behavior_packs"); Log_1.default.message(`[ServerManager] Started watching behavior_packs storage for slot ${slot}`); } // Start watching resource packs storage const resourcePacksStorage = dedicatedServer.resourcePacksStorage; if (resourcePacksStorage) { this.#httpServer.startWatchingStorage(resourcePacksStorage, slot, "resource_packs"); Log_1.default.message(`[ServerManager] Started watching resource_packs storage for slot ${slot}`); } Log_1.default.message(`[ServerManager] Completed storage watching setup for slot ${slot}`); } /** * Stop watching the server's storage folders when the server stops. */ stopWatchingServerStorage(dedicatedServer) { if (!this.#httpServer) { return; } const worldStorage = dedicatedServer.defaultWorldStorage; if (worldStorage) { this.#httpServer.stopWatchingStorage(worldStorage); } const behaviorPacksStorage = dedicatedServer.behaviorPacksStorage; if (behaviorPacksStorage) { this.#httpServer.stopWatchingStorage(behaviorPacksStorage); } const resourcePacksStorage = dedicatedServer.resourcePacksStorage; if (resourcePacksStorage) { this.#httpServer.stopWatchingStorage(resourcePacksStorage); } } bubbleServerRefreshed(dedicatedServer, message) { this.#onServerRefreshed.dispatch(dedicatedServer, message); this.pushStatusNotification(dedicatedServer); } bubbleServerStarting(dedicatedServer, message) { this.#onServerStarting.dispatch(dedicatedServer, message); this.pushStatusNotification(dedicatedServer); } bubbleServerStopping(dedicatedServer, message) { this.#onServerStopping.dispatch(dedicatedServer, message); this.pushStatusNotification(dedicatedServer); } bubbleServerStopped(dedicatedServer, message) { // Stop watching storage when the server stops this.stopWatchingServerStorage(dedicatedServer); this.#onServerStopped.dispatch(dedicatedServer, message); this.pushStatusNotification(dedicatedServer); } /** * Get the slot number for a given DedicatedServer. * Returns 0 if the server is not found in active servers. */ getSlotForServer(dedicatedServer) { for (const portStr in this.#activeServersByPort) { const port = parseInt(portStr); if (this.#activeServersByPort[port] === dedicatedServer) { return MinecraftUtilities_1.default.getSlotFromPort(port); } } return 0; // Default to slot 0 } /** * Push a status update notification via WebSocket. * This replaces the need for clients to poll /api/{slot}/status/ */ pushStatusNotification(dedicatedServer) { if (!this.#httpServer) { return; } const slot = this.getSlotForServer(dedicatedServer); const recentMessages = []; // Get recent messages, excluding internal system messages (e.g., querytarget polling) let lastIndex = dedicatedServer.outputLines.length; while (lastIndex >= 1 && recentMessages.length < 10) { lastIndex--; const line = dedicatedServer.outputLines[lastIndex]; if (!line.isInternal) { recentMessages.push({ message: line.message, received: line.received, }); } } this.#httpServer.notifyStatusUpdate(slot, dedicatedServer.status, recentMessages); } bubbleTestFailed(dedicatedServer, message) { this.#onTestFailed.dispatch(dedicatedServer, message); } bubbleTestStarted(dedicatedServer, message) { this.#onTestStarted.dispatch(dedicatedServer, message); } bubbleTestSucceeded(dedicatedServer, message) { this.#onTestSucceeded.dispatch(dedicatedServer, message); } bubbleDebugConnected(dedicatedServer, sessionInfo) { this.#onDebugConnected.dispatch(dedicatedServer, sessionInfo); // Push WebSocket notification if (this.#httpServer) { const slot = this.getSlotForServer(dedicatedServer); this.#httpServer.notify({ eventName: "debugConnected", timestamp: Date.now(), slot: slot, protocolVersion: sessionInfo.protocolVersion, }); } } bubbleDebugDisconnected(dedicatedServer, reason) { this.#onDebugDisconnected.dispatch(dedicatedServer, reason); // Push WebSocket notification if (this.#httpServer) { const slot = this.getSlotForServer(dedicatedServer); this.#httpServer.notify({ eventName: "debugDisconnected", timestamp: Date.now(), slot: slot, reason: reason, }); } } bubbleDebugStats(dedicatedServer, statsData) { Log_1.default.verbose(`[DebugStats] ServerManager: Received stats tick ${statsData.tick} with ${statsData.stats.length} stat items`); this.#onDebugStats.dispatch(dedicatedServer, statsData); // Push WebSocket notification if (this.#httpServer) { const slot = this.getSlotForServer(dedicatedServer); Log_1.default.verbose(`[DebugStats] Notifying WebSocket clients for slot ${slot}`); this.#httpServer.notify({ eventName: "debugStats", timestamp: Date.now(), slot: slot, tick: statsData.tick, stats: statsData.stats.map((s) => ({ name: s.name, values: s.values, parent: s.parent_name || undefined, })), }); } else { Log_1.default.verbose(`[DebugStats] No HTTP server to notify`); } } bubbleDebugPaused(dedicatedServer, reason) { this.#onDebugPaused.dispatch(dedicatedServer, reason); // Push WebSocket notification if (this.#httpServer) { const slot = this.getSlotForServer(dedicatedServer); this.#httpServer.notify({ eventName: "debugPaused", timestamp: Date.now(), slot: slot, reason: reason, }); } } bubbleDebugResumed(dedicatedServer) { this.#onDebugResumed.dispatch(dedicatedServer); // Push WebSocket notification if (this.#httpServer) { const slot = this.getSlotForServer(dedicatedServer); this.#httpServer.notify({ eventName: "debugResumed", timestamp: Date.now(), slot: slot, }); } } bubbleProfilerCapture(dedicatedServer, captureEvent) { this.#onProfilerCapture.dispatch(dedicatedServer, captureEvent); // Push WebSocket notification with profiler data if (this.#httpServer) { const slot = this.getSlotForServer(dedicatedServer); this.#httpServer.notify({ eventName: "profilerCapture", timestamp: Date.now(), slot: slot, captureBasePath: captureEvent.capture_base_path, captureData: captureEvent.capture_data, // Base64 encoded profiler data }); } } get effectiveIsUsingPreview() { return this.#usePreview || (this.#creatorTools && this.#creatorTools.track === ICreatorToolsData_1.MinecraftTrack.preview); } async getLatestVersionInfo(force) { let minecraftInfoResponse = undefined; if (this.#utilities.platform === ILocalUtilities_1.Platform.linux && ((this.serverVersions[ServerType.bedrockLinux].version && !this.effectiveIsUsingPreview) || (this.serverVersions[ServerType.bedrockLinuxPreview].version && this.effectiveIsUsingPreview)) && !force) { return true; } if (this.#utilities.platform === ILocalUtilities_1.Platform.windows && ((this.serverVersions[ServerType.bedrockWindows].version && !this.effectiveIsUsingPreview) || (this.serverVersions[ServerType.bedrockWindowsPreview].version && this.effectiveIsUsingPreview)) && !force) { return true; } let successfullyRetrievedVersions = false; let serverVersionUrl = "https://net-secondary.web.minecraft-services.net/api/v1.0/download/links"; try { minecraftInfoResponse = (await axios_1.default.get(serverVersionUrl)); if (minecraftInfoResponse && minecraftInfoResponse.data && minecraftInfoResponse.data.result && minecraftInfoResponse.data.result.links) { const links = minecraftInfoResponse.data.result.links; for (const link of links) { if (link.downloadType && link.downloadUrl) { const lastDash = link.downloadUrl.lastIndexOf("-"); const lastDot = link.downloadUrl.lastIndexOf(".zip"); if (lastDash > 0 && lastDot > lastDash) { let version = link.downloadUrl.substring(lastDash + 1, lastDot); // re-constitute the version number ourselves const verNums = version.split("."); if (verNums.length === 4) { const verStr = String(String(parseInt(verNums[0])) + "." + String(parseInt(verNums[1])) + "." + String(parseInt(verNums[2])) + "." + String(parseInt(verNums[3]))); const versionIndex = Database_1.default.getVersionIndexFromVersionStr(version); let serverType = undefined; switch (link.downloadType) { case "serverBedrockWindows": serverType = ServerType.bedrockWindows; break; case "serverBedrockLinux": serverType = ServerType.bedrockLinux; break; case "serverBedrockPreviewWindows": serverType = ServerType.bedrockWindowsPreview; break; case "serverBedrockPreviewLinux": serverType = ServerType.bedrockLinuxPreview; break; case "serverJar": serverType = ServerType.java; break; } if (serverType !== undefined) { this.serverVersions[serverType].version = verStr; this.serverVersions[serverType].versionIndex = versionIndex; } } } } } successfullyRetrievedVersions = true; } } catch (e) { console.log("Could not access Bedrock Dedicated Server details." + e.toString()); return false; } // fallback: use version.json from githubusercontent.com if (!successfullyRetrievedVersions) { let versionUrl = "https://raw.githubusercontent.com/Mojang/bedrock-samples/main/version.json"; if (this.effectiveIsUsingPreview) { versionUrl = "https://raw.githubusercontent.com/Mojang/bedrock-samples/preview/version.json"; } await this.creatorTools.notifyStatusUpdate("Retrieving " + (this.effectiveIsUsingPreview ? "preview" : "retail") + " version data."); try { minecraftInfoResponse = await axios_1.default.get(versionUrl); } catch (e) { console.log("Could not access Bedrock Dedicated Server details." + e); throw new Error(e.toString()); } let latestVersionIndex = 0; try { if (minecraftInfoResponse === undefined || minecraftInfoResponse.data === undefined) { return false; } const responseJson = JSON.parse(JSON.stringify(minecraftInfoResponse.data)); if (!responseJson) { return false; } for (const verId in responseJson) { const ver = responseJson[verId].version; if (ver) { const isPreview = Database_1.default.getVersionIsPreview(ver); // version ends with .20 or higher const versionIndex = Database_1.default.getVersionIndexFromVersionStr(ver); if (versionIndex > 0 && versionIndex > latestVersionIndex && isPreview === this.effectiveIsUsingPreview) { latestVersionIndex = versionIndex; // re-constitute the version number ourselves const verNums = ver.split("."); const verStr = String(String(parseInt(verNums[0])) + "." + String(parseInt(verNums[1])) + "." + String(parseInt(verNums[2])) + "." + String(parseInt(verNums[3]))); // Set version for both Windows and Linux server types // The version.json fallback only gives us the version number, which is the same // for both platforms - only the download URL differs const windowsServerType = isPreview ? ServerType.bedrockWindowsPreview : ServerType.bedrockWindows; const linuxServerType = isPreview ? ServerType.bedrockLinuxPreview : ServerType.bedrockLinux; this.serverVersions[windowsServerType].version = verStr; this.serverVersions[windowsServerType].versionIndex = latestVersionIndex; this.serverVersions[linuxServerType].version = verStr; this.serverVersions[linuxServerType].versionIndex = latestVersionIndex; } } } } catch (e) { Log_1.default.error("Could not access Bedrock Dedicated Server details." + e); return false; } } if (this.#utilities.platform === ILocalUtilities_1.Platform.linux && ((!this.effectiveIsUsingPreview && !this.serverVersions[ServerType.bedrockLinux]?.version) || (this.effectiveIsUsingPreview && !this.serverVersions[ServerType.bedrockLinuxPreview]?.version))) { Log_1.default.error("Could not determine latest Bedrock Dedicated Server version for Linux."); return false; } if (this.#utilities.platform === ILocalUtilities_1.Platform.windows && ((!this.effectiveIsUsingPreview && !this.serverVersions[ServerType.bedrockWindows]?.version) || (this.effectiveIsUsingPreview && !this.serverVersions[ServerType.bedrockWindowsPreview]?.version))) { Log_1.default.error("Could not determine latest Bedrock Dedicated Server version for Windows."); return false; } return true; } static getServerTypeStr(serv