@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,066 lines • 116 kB
JavaScript
"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