@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,077 lines • 105 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.MaxTimeToWaitForServerToStart = exports.DedicatedServerBackupStatus = exports.DedicatedServerStatus = void 0;
/**
* ARCHITECTURE DOCUMENTATION: DedicatedServer - Minecraft Server Process Manager
* ===============================================================================
*
* DedicatedServer manages a single instance of Minecraft Bedrock Dedicated Server,
* handling process lifecycle, command execution, content deployment, and world backups.
*
* ## Core Responsibilities
*
* 1. **Process Lifecycle**: Start, stop, and monitor bedrock_server.exe
* 2. **Command Execution**: Send commands to the server via stdin
* 3. **Output Parsing**: Parse server stdout for events (player join/leave, test results)
* 4. **Content Deployment**: Deploy add-on packs to the running server
* 5. **World Backup**: Backup world files during runtime using save hold/resume
* 6. **Debug Client**: Connect to the Minecraft script debugger for profiling
*
* ## Server Folder Structure
*
* Each DedicatedServer instance operates in a runtime server folder:
*
* ```
* srv20260101120000/ (Runtime server folder)
* ├─ bedrock_server.exe (Copied from source)
* ├─ server.properties (Generated, configures ports, world name)
* ├─ allowlist.json (Symlink to source)
* ├─ permissions.json (Symlink to source)
* ├─ config/ (Generated config folder)
* │ ├─ default.json (Creator Tools server config)
* │ └─ ...
* ├─ behavior_packs/ (Junction to source vanilla packs)
* ├─ resource_packs/ (Junction to source vanilla packs)
* ├─ definitions/ (Junction to source definitions)
* ├─ development_behavior_packs/ (Writable - deployed add-ons go here)
* │ └─ my_addon_abc123_my_bp/ (Symlink to pack cache folder)
* ├─ development_resource_packs/ (Writable - deployed add-ons go here)
* │ └─ my_addon_abc123_my_rp/ (Symlink to pack cache folder)
* └─ worlds/
* └─ defaultWorld/ (Writable - active world data)
* ├─ level.dat (World metadata in NBT format)
* ├─ levelname.txt (World display name)
* ├─ world_behavior_packs.json (Active behavior packs for world)
* ├─ world_resource_packs.json (Active resource packs for world)
* └─ db/ (LevelDB world data)
* ├─ CURRENT
* ├─ MANIFEST-000001
* ├─ *.ldb (Immutable SSTable files)
* └─ *.log (Write-ahead log)
* ```
*
* ## Startup Sequence
*
* 1. **Signature Verification**: On Windows, verify bedrock_server.exe is Microsoft-signed
* 2. **Folder Setup**: Create development_*_packs and worlds/defaultWorld if needed
* 3. **Config Generation**: Write server.properties with port, world name, settings
* 4. **World Restoration**: Optionally restore from latest backup
* 5. **Process Spawn**: Launch server with stdin/stdout capture
* - Windows: spawn bedrock_server.exe directly
* - Linux: spawn bedrock_server with LD_LIBRARY_PATH set
* 6. **Ready Detection**: Parse stdout for "Server started" message
* 7. **Debugger Connection**: Connect to script debugger on port 19144
* 8. **Position Polling**: Start polling for player positions
*
* ## Linux Compatibility
*
* The server supports both Windows and Linux with these platform-specific behaviors:
*
* - **Executable**: `bedrock_server.exe` on Windows, `bedrock_server` on Linux
* - **Library Path**: On Linux, `LD_LIBRARY_PATH` is set to the server directory
* - **Signature Check**: Authenticode verification is Windows-only (skipped on Linux)
* - **Path Handling**: Uses platform-aware path delimiters throughout
*
* ## Restart Backoff Strategy
*
* If the server crashes unexpectedly, it will attempt to restart with exponential
* backoff to avoid resource exhaustion:
*
* | Crash # | Delay Before Restart |
* |---------|---------------------|
* | 1 | 1 second |
* | 2 | 2 seconds |
* | 3 | 4 seconds |
* | 4+ | Stop auto-restart |
*
* The crash counter resets after 60 seconds of stable operation.
*
* ## World Backup Strategy
*
* Backups are performed using the Bedrock server's safe backup protocol:
*
* ```
* Normal Operation Backup Sequence
* │ │
* │ ┌───── save hold ─────────────► │ (1) Suspend world writes
* │ │ │
* │ │ save query ─────────────► │ (2) Get list of modified files
* │ │ │
* │ │ ◄──── file list ──────────── │ (3) Server returns file paths & sizes
* │ │ │
* │ │ [copy files] ───────────► │ (4) Copy only modified files
* │ │ │
* │ │ save resume ────────────► │ (5) Resume world writes
* │ │ │
* │ └─────────────────────────────► │
* ```
*
* Backup folders are stored in a configurable location (default: user's Documents):
* ```
* Documents/mctools/worlds/
* └─ world/
* ├─ world20260101120000/ (Timestamped backup)
* │ ├─ files.json (File listing with sizes)
* │ ├─ level.dat
* │ └─ db/
* │ └─ <only modified .ldb files>
* └─ world20260101130000/ (Later backup)
* ```
*
* **Incremental Backup Optimization**:
* - The backup system tracks SHA hashes of LevelDB files
* - Only files that have changed since the last backup are copied
* - This is especially efficient for LevelDB's immutable SSTable (.ldb) files
* - The `backupWorldFileListings` in ServerManager tracks known files across backups
*
* **Backup Timeout Protection**:
* - A 60-second timeout prevents backup from getting stuck if server doesn't respond
* - If timeout fires, save is forcefully resumed and an error is logged
* - Timeout is cleared when backup completes successfully or server stops
*
* ## Content Deployment Strategy (Feb 2026)
*
* Hot-reload is ENABLED for **script-only changes** on subsequent deploys.
* The first deploy always restarts to register packs with the world.
*
* Decision logic (in `deploy()` method):
*
* 1. **First deploy** (`deployCount === 0`): Always restart — packs must be registered
* 2. **Subsequent deploys** with server running:
* a. Capture before/after thumbprints of behavior + resource packs
* b. If resource pack files changed → restart (textures/models can't hot-reload)
* c. If behavior pack changes are script-only (.js/.ts/.map, no deletions) → `/reload`
* d. Otherwise → restart
* 3. **Caller override**: `isReloadable=false` forces restart regardless
*
* The `MinecraftUtilities.isReloadableSetOfChanges()` function gates the decision:
* it returns true only when ALL file diffs are `.js`, `.ts`, or `.map` with no deletions.
*
* When deploying add-on content:
*
* **Hot-Reload Path** (script-only changes, subsequent deploys):
* 1. **Sync Files**: Copy new/modified files to development_*_packs folders
* 2. **Thumbprint Diff**: Compare before/after to determine what changed
* 3. **Run `/reload`**: Hot-reload scripts without server restart
*
* **Restart Path** (structural changes, first deploy, or caller override):
* 1. **Sync Files**: Copy new/modified files to development_*_packs folders
* 2. **Update Pack References**: Ensure world_behavior_packs.json has pack UUIDs
* 3. **Stop Server**: Graceful shutdown with backup
* 4. **Restart Server**: Fresh start picks up all changes
*
* ## Slot Sentinel File (ServerManager)
*
* Each slot folder contains a sentinel file (`slot_context.json`) that records:
* - Source server version and path
* - When the slot was provisioned
* - Deployed pack UUIDs and versions
* - World settings and experiments enabled
*
* On startup, ServerManager compares the current context against the sentinel:
* - If context matches: Reuse existing slot (fast startup)
* - If context differs: Backup world, rebuild slot, restore world, re-deploy
*
* ## Server Output Parsing
*
* The server's stdout is continuously parsed for significant events:
*
* | Log Message Pattern | Action |
* |-----------------------------|-------------------------------------------|
* | "Server started" | Mark as running, enable debug, poll positions |
* | "Player connected" | Extract player name/xuid, emit event |
* | "Player disconnected" | Extract player name/xuid, emit event |
* | "Data saved" | Backup sequence state machine trigger |
* | "Changes to the level are resumed" | Backup complete notification |
* | "Loaded test: ..." | GameTest started event |
* | "passed test: ..." | GameTest passed event |
* | "failed test: ..." | GameTest failed event |
*
* ## Script Debugger Integration
*
* After server start, DedicatedServer can connect to the Minecraft script debugger:
*
* 1. Send `script debugger listen 19144` command to server (if enableDebugger=true)
* 2. Wait for "Debugger listening" message from Minecraft (confirms listener is ready)
* 3. Connect MinecraftDebugClient to localhost:19144 (if enableDebuggerStreaming=true)
* 4. Receive profiling stats (tick timing, script execution times)
* 5. Forward debug events to ServerManager → HttpServer → WebSocket clients
*
* **Configuration (Jan 2026 Update)**:
* - `enableDebugger` (default: true): Whether BDS enables script debugger on port 19144
* - `enableDebuggerStreaming` (default: true for serve command): Whether we connect and stream debug stats
* The serve command enables streaming by default to provide real-time stats in the web UI.
*
* **Debug Client Connection Flow**:
* - 3 seconds after "Server started" message: send `script debugger listen` command
* - Wait for "Debugger listening" message from Minecraft stdout (or 10s timeout)
* - Connect MinecraftDebugClient to localhost:debugPort
* - Uses TCP keep-alive (30s) to detect dead connections
* - Retries up to 5 times with exponential backoff on connection failure
* - Has 10-second handshake timeout if server doesn't respond with ProtocolEvent
* - Session info exposed via HTTP API: /api/{slot}/status includes debugConnectionState
*
* ## Related Files
*
* - ServerManager.ts: Creates and orchestrates DedicatedServer instances
* - ServerConfigManager.ts: Manages server config/*.json files
* - ServerPropertiesManager.ts: Manages server.properties file
* - MCWorld.ts: World metadata parsing and modification
* - MinecraftDebugClient.ts: Script debugger WebSocket client
* - Thumbprint.ts: File tree hashing for change detection
*
* ## Key Methods
*
* - `startServer()`: Launch the server process with config
* - `stopServer()`: Gracefully stop the server with "stop" command
* - `deploy()`: Deploy add-on content to the running server
* - `doBackup()`: Perform incremental world backup
* - `runCommand()`: Execute a slash command on the server
* - `ensureWorld()`: Set up world with settings and templates
*
* ## State Machine
*
* ```
* stopped ──► deploying ──► launching ──► starting ──► started
* ▲ │
* └────────────────────── stopping ◄───────────────────┘
* ```
*/
const stringio_1 = require("@rauschma/stringio");
const child_process_1 = require("child_process");
const ste_events_1 = require("ste-events");
const Player_1 = __importDefault(require("./../minecraft/Player"));
const ServerConfigManager_1 = __importDefault(require("./ServerConfigManager"));
const SecurityUtilities_1 = __importDefault(require("../core/SecurityUtilities"));
const ServerMessage_1 = __importStar(require("./ServerMessage"));
const ServerPropertiesManager_1 = __importDefault(require("../minecraft/ServerPropertiesManager"));
const NodeStorage_1 = __importDefault(require("./NodeStorage"));
const Log_1 = __importDefault(require("../core/Log"));
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const Project_1 = __importDefault(require("../app/Project"));
const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities"));
const LocalUtilities_1 = __importDefault(require("./LocalUtilities"));
const MCWorld_1 = __importDefault(require("../minecraft/MCWorld"));
const MinecraftUtilities_1 = __importDefault(require("../minecraft/MinecraftUtilities"));
const Thumbprint_1 = __importDefault(require("../storage/Thumbprint"));
const ICreatorToolsData_1 = require("../app/ICreatorToolsData");
const Utilities_1 = __importDefault(require("../core/Utilities"));
const timers_1 = require("timers");
const IWorldSettings_1 = require("../minecraft/IWorldSettings");
const NodeFile_1 = __importDefault(require("./NodeFile"));
const ZipStorage_1 = __importDefault(require("../storage/ZipStorage"));
const MinecraftDebugClient_1 = __importDefault(require("../debugger/MinecraftDebugClient"));
const IWorldBackupData_1 = require("./IWorldBackupData");
var DedicatedServerStatus;
(function (DedicatedServerStatus) {
DedicatedServerStatus[DedicatedServerStatus["stopped"] = 1] = "stopped";
DedicatedServerStatus[DedicatedServerStatus["deploying"] = 2] = "deploying";
DedicatedServerStatus[DedicatedServerStatus["launching"] = 3] = "launching";
DedicatedServerStatus[DedicatedServerStatus["starting"] = 4] = "starting";
DedicatedServerStatus[DedicatedServerStatus["started"] = 5] = "started";
})(DedicatedServerStatus || (exports.DedicatedServerStatus = DedicatedServerStatus = {}));
var DedicatedServerBackupStatus;
(function (DedicatedServerBackupStatus) {
DedicatedServerBackupStatus[DedicatedServerBackupStatus["none"] = 0] = "none";
DedicatedServerBackupStatus[DedicatedServerBackupStatus["suspendingSaveCommandIssued"] = 1] = "suspendingSaveCommandIssued";
DedicatedServerBackupStatus[DedicatedServerBackupStatus["suspendingQueryCommandIssued"] = 2] = "suspendingQueryCommandIssued";
DedicatedServerBackupStatus[DedicatedServerBackupStatus["suspendingQueryResultsPending"] = 3] = "suspendingQueryResultsPending";
DedicatedServerBackupStatus[DedicatedServerBackupStatus["saveSuspended"] = 4] = "saveSuspended";
DedicatedServerBackupStatus[DedicatedServerBackupStatus["copyingFiles"] = 5] = "copyingFiles";
DedicatedServerBackupStatus[DedicatedServerBackupStatus["resumingSave"] = 6] = "resumingSave";
DedicatedServerBackupStatus[DedicatedServerBackupStatus["saveResumed"] = 7] = "saveResumed";
})(DedicatedServerBackupStatus || (exports.DedicatedServerBackupStatus = DedicatedServerBackupStatus = {}));
exports.MaxTimeToWaitForServerToStart = 5000; // in ticks of 5ms each = 25 seconds
// Player position polling interval in ms (5 seconds)
const PLAYER_POSITION_POLL_INTERVAL = 5000;
// Minimum distance (in blocks) to consider a "major" move worth reporting
const PLAYER_MOVE_THRESHOLD = 2;
class DedicatedServer {
#pendingCommands = [];
#pendingRequestIds = [];
#pendingCommandsInternal = []; // Track which commands are internal (don't log)
#worldBackupContainerFolder;
serverPath;
name;
version;
#backupInterval;
#backupTimeoutTimer; // Timeout to prevent backup from getting stuck
#playerPositionPollInterval;
#lastPlayerPositions = new Map();
updates = [];
#unexpectedStopLog = [];
#backupStatus = DedicatedServerBackupStatus.none;
#behaviorPacksStorage;
#defaultWorldStorage;
#resourcePacksStorage;
#activeStdIn = null;
#env;
#currentCommandId = 0;
#dsm;
#starts = 0;
#lastResult;
startConfigurationHash = undefined;
#port;
#activeProcess = null;
#status = DedicatedServerStatus.stopped;
// Debug client for connecting to the Minecraft script debugger
#debugClient;
// enableDebugger: Whether BDS enables script debugger listening
// The debug port is calculated dynamically based on the slot (base port + 12)
#enableDebugger = true;
// enableDebuggerStreaming: Whether we connect to the debug port and stream stats to web console
// Enabled by default. Set worldSettings.enableDebuggerStreaming=false to disable.
#enableDebuggerStreaming = true;
// Flag to track when we're waiting for the debug listener to be ready
// Set to true after sending 'script debugger listen', cleared when we receive 'Debugger listening' message
#awaitingDebuggerListening = false;
// Flag to track if the debug listener is ready but we haven't connected yet
// Used when delaying debug client connection until a player joins
#debugListenerReady = false;
// Whether to delay debug client connection until a player joins
// Note: This was used during debugging but the real fix was sending 'resume' after
// protocol handshake (see MinecraftDebugClient._handleProtocolEvent)
#delayDebugClientUntilPlayerJoins = false;
// Track if at least one player has joined since server start
// Used to know when it's safe to start debug streaming
#hasPlayerJoined = false;
// Whether to launch BDS in Minecraft Editor mode (passes Editor=true arg)
#editorMode = false;
// Associated managed world ID for the new backup system.
// If set, backups will be stored in the WorldBackupManager structure.
// If not set, backups use the legacy per-slot backup folder structure.
#managedWorldId;
// Last backup result for tracking what was backed up
#lastBackupResult;
outputLines = [];
#opList;
#gameTest;
get opList() {
return this.#opList;
}
set opList(newOps) {
this.#opList = newOps;
}
get port() {
return this.#port;
}
/**
* Get the script debugger port for this server instance.
* The debug port is the base port + 12, ensuring each slot has a unique debug port.
* Slot 0: port 19132 -> debug port 19144
* Slot 1: port 19164 -> debug port 19176
* etc.
*/
get debugPort() {
const basePort = this.#port ?? 19132;
return basePort + 12; // Debug port offset is 12 from base port
}
get lastResult() {
return this.#lastResult;
}
set port(newPort) {
this.#port = newPort;
}
get editorMode() {
return this.#editorMode;
}
set editorMode(value) {
this.#editorMode = value;
}
get defaultWorldFolder() {
if (!this.#defaultWorldStorage) {
return undefined;
}
return this.#defaultWorldStorage.rootFolder;
}
get behaviorPacksFolder() {
if (!this.#behaviorPacksStorage) {
return undefined;
}
return this.#behaviorPacksStorage.rootFolder;
}
get resourcePacksFolder() {
if (!this.#resourcePacksStorage) {
return undefined;
}
return this.#resourcePacksStorage.rootFolder;
}
/**
* Get the behavior packs storage (NodeStorage) for file watching purposes.
*/
get behaviorPacksStorage() {
return this.#behaviorPacksStorage;
}
/**
* Get the default world storage (NodeStorage) for file watching purposes.
*/
get defaultWorldStorage() {
return this.#defaultWorldStorage;
}
/**
* Get the resource packs storage (NodeStorage) for file watching purposes.
*/
get resourcePacksStorage() {
return this.#resourcePacksStorage;
}
config;
properties;
deployCount = 0;
#onServerOutput = new ste_events_1.EventDispatcher();
#onServerStarted = new ste_events_1.EventDispatcher();
#onServerRefreshed = new ste_events_1.EventDispatcher();
#onServerError = new ste_events_1.EventDispatcher();
#onServerStarting = new ste_events_1.EventDispatcher();
#onServerStopping = new ste_events_1.EventDispatcher();
#onServerStopped = new ste_events_1.EventDispatcher();
#onServerGameEvent = 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 client events
#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();
#updateIds = {};
constructor(name, dsm, env, serverPath, worldBackupContainerFolder) {
this.name = name;
this.serverPath = serverPath;
this.#worldBackupContainerFolder = worldBackupContainerFolder;
this.#dsm = dsm;
this.#env = env;
this.config = new ServerConfigManager_1.default();
this.config.ensureDefaultConfig();
this.config.addCartoConfig();
this.properties = new ServerPropertiesManager_1.default();
this.handleClose = this.handleClose.bind(this);
this.doRunningBackup = this.doRunningBackup.bind(this);
this.startServer = this.startServer.bind(this);
this.stopServer = this.stopServer.bind(this);
this.directOutput = this.directOutput.bind(this);
this.handleCommandRequest = this.handleCommandRequest.bind(this);
}
get worldStoragePath() {
return (NodeStorage_1.default.ensureEndsWithDelimiter(this.serverPath) +
"worlds" +
NodeStorage_1.default.platformFolderDelimiter +
"defaultWorld" +
NodeStorage_1.default.platformFolderDelimiter);
}
pushUpdates(additionalUpdates) {
for (let i = 0; i < additionalUpdates.length; i++) {
const update = additionalUpdates[i];
const updateId = update.eventId;
if (!updateId || !this.#updateIds[updateId]) {
if (updateId) {
this.#updateIds[updateId] = true;
}
this.#onServerGameEvent.dispatch(this, update);
this.updates.push(update);
}
}
}
get gameTest() {
return this.#gameTest;
}
set gameTest(newGameTest) {
this.#gameTest = newGameTest;
}
get status() {
return this.#status;
}
get onServerStarting() {
return this.#onServerStarting.asEvent();
}
get onServerStopping() {
return this.#onServerStopping.asEvent();
}
get onServerStopped() {
return this.#onServerStopped.asEvent();
}
get onServerRefreshed() {
return this.#onServerRefreshed.asEvent();
}
get onServerOutput() {
return this.#onServerOutput.asEvent();
}
get onServerGameEvent() {
return this.#onServerGameEvent.asEvent();
}
get onServerError() {
return this.#onServerError.asEvent();
}
get onServerStarted() {
return this.#onServerStarted.asEvent();
}
get onTestStarted() {
return this.#onTestStarted.asEvent();
}
get onTestFailed() {
return this.#onTestFailed.asEvent();
}
get onTestSucceeded() {
return this.#onTestSucceeded.asEvent();
}
get onPlayerConnected() {
return this.#onPlayerConnected.asEvent();
}
get onPlayerDisconnected() {
return this.#onPlayerDisconnected.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 debugClient() {
return this.#debugClient;
}
get debuggerEnabled() {
return this.#enableDebugger;
}
set debuggerEnabled(value) {
this.#enableDebugger = value;
}
get debuggerStreamingEnabled() {
return this.#enableDebuggerStreaming;
}
set debuggerStreamingEnabled(value) {
this.#enableDebuggerStreaming = value;
}
/**
* Get the managed world ID for this server.
* If set, backups will use the WorldBackupManager system.
*/
get managedWorldId() {
return this.#managedWorldId;
}
/**
* Set the managed world ID for this server.
* @param worldId The world ID from WorldBackupManager, or undefined to use legacy backups
*/
set managedWorldId(worldId) {
this.#managedWorldId = worldId;
}
/**
* Get the last backup result.
*/
get lastBackupResult() {
return this.#lastBackupResult;
}
handleCommandRequest(event, data) {
const slargs = Utilities_1.default.splitUntil(data, "|", 1);
this.runCommand(slargs[1], slargs[0]);
}
async runActionSet(actionSet, requestId) {
if (!requestId) {
requestId = "";
}
const actionData = JSON.stringify(actionSet);
await this.runCommand("mct:runactions " + actionData, requestId);
}
async waitUntilStarted() {
let waitTicks = 0;
while (this.status !== DedicatedServerStatus.started &&
this.status !== DedicatedServerStatus.stopped &&
waitTicks < exports.MaxTimeToWaitForServerToStart) {
await Utilities_1.default.sleep(5);
waitTicks++;
}
if (waitTicks >= exports.MaxTimeToWaitForServerToStart) {
Log_1.default.message("Timed out waiting for server to start.");
}
}
async runCommandImmediate(command, tokenId, maxWaitMs) {
Log_1.default.message("Running command: " + command);
let targetResultLine = this.outputLines.length;
await this.writeToServer(command);
// maxPolls / pollCount count 5 ms polling iterations, not Minecraft game ticks.
const maxPolls = maxWaitMs ? Math.ceil(maxWaitMs / 5) : 500;
let pollCount = 0;
if (tokenId) {
let foundLineIndex = -1;
while (foundLineIndex < 0 && pollCount < maxPolls) {
await Utilities_1.default.sleep(5);
for (let i = targetResultLine; i < this.outputLines.length; i++) {
if (this.outputLines[i].message.indexOf(tokenId) >= 0) {
foundLineIndex = i;
}
}
pollCount++;
}
if (foundLineIndex >= 0) {
let result = this.outputLines[foundLineIndex].message;
Log_1.default.message("Command run complete: " + command + "| Result: " + result);
return result;
}
}
else {
while (targetResultLine >= this.outputLines.length && pollCount < maxPolls) {
await Utilities_1.default.sleep(5);
pollCount++;
}
if (this.outputLines.length > targetResultLine) {
let result = this.outputLines[targetResultLine].message;
Log_1.default.message("Command run complete: " + command + "| Result: " + result);
return result;
}
}
return undefined;
}
async runCommand(command, requestId, isInternal) {
if (!requestId) {
requestId = "";
}
const newCommand = this.#pendingCommands.length;
this.#pendingCommands[newCommand] = command;
this.#pendingRequestIds[newCommand] = requestId;
this.#pendingCommandsInternal[newCommand] = isInternal === true;
if (newCommand === this.#currentCommandId) {
await this.executeNextCommand();
}
}
/**
* Run an internal command that doesn't show in logs.
* Used for implementation details like querytarget polling.
*/
async runInternalCommand(command, requestId) {
return this.runCommand(command, requestId, true);
}
async writeToServer(commandLine) {
if (this.#activeStdIn === null) {
Log_1.default.message("Could not find active stdin to run command '" + commandLine + "'.");
return;
}
// Security: Sanitize command to prevent injection
commandLine = SecurityUtilities_1.default.sanitizeCommand(commandLine);
if (!SecurityUtilities_1.default.isCommandSafe(commandLine)) {
Log_1.default.message("Command rejected as unsafe: " + commandLine);
return;
}
await (0, stringio_1.streamWrite)(this.#activeStdIn, commandLine + "\n");
}
async ensureServerFolders() {
if (!this.#behaviorPacksStorage) {
this.#behaviorPacksStorage = new NodeStorage_1.default(NodeStorage_1.default.ensureEndsWithDelimiter(this.serverPath) + "development_behavior_packs", "");
await this.#behaviorPacksStorage.rootFolder.ensureExists();
}
if (!this.#resourcePacksStorage) {
this.#resourcePacksStorage = new NodeStorage_1.default(NodeStorage_1.default.ensureEndsWithDelimiter(this.serverPath) + "development_resource_packs", "");
await this.#resourcePacksStorage.rootFolder.ensureExists();
}
if (!this.#defaultWorldStorage) {
this.#defaultWorldStorage = new NodeStorage_1.default(this.worldStoragePath, "");
await this.#defaultWorldStorage.rootFolder.ensureExists();
}
}
async restoreLatestBackupWorld() {
// If we have a managed world ID and the WorldBackupManager is available,
// use the new restore system. Otherwise, fall back to legacy restore.
if (this.#managedWorldId && this.#dsm.worldBackupManager && this.#defaultWorldStorage) {
return await this.restoreManagedWorld();
}
// Legacy restore system
const worldBackupContainerFolderExists = fs.existsSync(this.#worldBackupContainerFolder.fullPath);
if (!worldBackupContainerFolderExists) {
return false;
}
const folders = fs.readdirSync(this.#worldBackupContainerFolder.fullPath);
let latestWorldName;
let latestWorldDate = new Date(0, 0, 0);
const operId = await this.#dsm.creatorTools.notifyOperationStarted("Restoring world from '" + this.#worldBackupContainerFolder.fullPath + "'");
for (const folder of folders) {
if (folder.startsWith("world") && folder.length === 19) {
const dateStr = folder.substring(5);
if (Utilities_1.default.isNumeric(dateStr)) {
const fullPath = NodeStorage_1.default.ensureEndsWithDelimiter(this.#worldBackupContainerFolder.fullPath) +
folder +
NodeStorage_1.default.platformFolderDelimiter;
const filesJsonPath = fullPath + "files.json";
const filesJsonExists = fs.existsSync(filesJsonPath);
const worldDate = Utilities_1.default.getDateFromStr(dateStr);
if (filesJsonExists && worldDate.getTime() > latestWorldDate.getTime()) {
latestWorldName = folder;
latestWorldDate = worldDate;
}
}
}
}
if (latestWorldName) {
const lastBackupWorldFolder = this.#worldBackupContainerFolder.folders[latestWorldName];
if (lastBackupWorldFolder && this.#defaultWorldStorage) {
await this.#dsm.creatorTools.notifyStatusUpdate("Restoring world '" + lastBackupWorldFolder.name + "'");
await lastBackupWorldFolder.copyContentsOut(this.#defaultWorldStorage.rootFolder);
await this.#dsm.creatorTools.notifyOperationEnded(operId, "Completed restoring world '" + lastBackupWorldFolder.name + "'");
return true;
}
}
await this.#dsm.creatorTools.notifyOperationEnded(operId, "Was not able to restore world.");
return false;
}
/**
* Restore the latest backup using the new WorldBackupManager system.
*/
async restoreManagedWorld() {
if (!this.#managedWorldId || !this.#dsm.worldBackupManager || !this.#defaultWorldStorage) {
throw new Error("Managed restore requires managedWorldId and WorldBackupManager");
}
const world = this.#dsm.worldBackupManager.getWorld(this.#managedWorldId);
if (!world) {
Log_1.default.message(`No managed world found with ID ${this.#managedWorldId}`);
return false;
}
await world.loadBackups();
if (world.backups.length === 0) {
Log_1.default.message(`No backups found for world ${this.#managedWorldId}`);
return false;
}
// Get the latest backup
const latestBackup = world.backups[world.backups.length - 1];
const operId = await this.#dsm.creatorTools.notifyOperationStarted(`Restoring managed world '${world.friendlyName}' (${this.#managedWorldId})`);
try {
// Restore the latest backup
await this.#dsm.worldBackupManager.restoreBackup(this.#managedWorldId, latestBackup.id, this.#defaultWorldStorage.rootFolder.fullPath);
await this.#dsm.creatorTools.notifyOperationEnded(operId, `Completed restoring world '${world.friendlyName}'`);
return true;
}
catch (error) {
Log_1.default.error(`Failed to restore managed world: ${error.message}`);
await this.#dsm.creatorTools.notifyOperationEnded(operId, `Failed to restore world: ${error.message}`);
return false;
}
}
async applyWorldSettings(mcworld, startInfo) {
if (startInfo?.worldSettings && startInfo.worldSettings.packageReferences) {
for (let i = 0; i < startInfo.worldSettings.packageReferences.length; i++) {
const packRefSet = startInfo.worldSettings.packageReferences[i];
mcworld.ensurePackReferenceSet(packRefSet);
if (packRefSet.resourcePackReferences.length > 0) {
mcworld.deferredTechnicalPreviewExperiment = true;
}
if (packRefSet.behaviorPackReferences.length > 0) {
mcworld.betaApisExperiment = true;
}
}
}
if (startInfo && startInfo.worldSettings) {
// only apply world settings if the world has no world templates.
if (startInfo.worldSettings.worldTemplateReferences === undefined ||
startInfo.worldSettings.worldTemplateReferences.length <= 0) {
// console.log("Applying settings " + JSON.stringify(startInfo.worldSettings));
mcworld.applyWorldSettings(startInfo?.worldSettings);
}
}
await mcworld.save();
}
async getStorageFromPath(path) {
if (!fs.existsSync(path)) {
return undefined;
}
const content = await NodeStorage_1.default.createFromPath(path);
if (content instanceof NodeFile_1.default &&
(path.endsWith(".mcpack") ||
path.endsWith(".mcaddon") ||
path.endsWith(".mcworld") ||
path.endsWith(".zip") ||
path.endsWith(".mcproject"))) {
const zs = await ZipStorage_1.default.loadFromFile(content);
return zs;
}
return undefined;
}
async ensureWorld(startInfo) {
const worldServerStorage = new NodeStorage_1.default(this.worldStoragePath, "");
const worldSourcePath = startInfo?.worldSettings?.worldContentPath;
if (worldSourcePath) {
let folder = await NodeStorage_1.default.createFromPathIncludingZip(worldSourcePath);
if (folder) {
await StorageUtilities_1.default.syncFolderTo(folder, worldServerStorage.rootFolder, false, false, false);
}
}
const mcworld = new MCWorld_1.default();
mcworld.folder = worldServerStorage.rootFolder;
await mcworld.loadMetaFiles(false);
await this.applyWorldSettings(mcworld, startInfo);
}
async ensureContentDeployed(startInfo) {
if (startInfo?.additionalContentPath) {
let folder = await NodeStorage_1.default.createFromPathIncludingZip(startInfo.additionalContentPath);
if (folder) {
await this.deploy(folder, false, false);
}
}
}
async startServer(restartIfAlreadyRunning, start) {
if (start === undefined) {
start = {
worldSettings: this.#dsm.creatorTools.worldSettings,
mode: ICreatorToolsData_1.DedicatedServerMode.auto,
iagree: this.#env.iAgreeToTheMinecraftEndUserLicenseAgreementAndPrivacyStatementAtMinecraftDotNetSlashEula,
};
}
if (this.#status === DedicatedServerStatus.launching ||
this.#status === DedicatedServerStatus.started ||
this.#status === DedicatedServerStatus.starting) {
if (restartIfAlreadyRunning) {
await this.stopServer();
}
else {
return;
}
}
let rootPath = this.serverPath;
this.#onServerStarting.dispatch(this, "");
this.#status = DedicatedServerStatus.launching;
const ns = new NodeStorage_1.default(rootPath, "");
if (start.worldSettings?.backupType === IWorldSettings_1.BackupType.every2Minutes) {
this.#backupInterval = (0, timers_1.setInterval)(this.doRunningBackup, 120000);
}
else if (start.worldSettings?.backupType === IWorldSettings_1.BackupType.every5Minutes) {
this.#backupInterval = (0, timers_1.setInterval)(this.doRunningBackup, 300000);
}
if (this.#starts === 0) {
await this.ensureServerFolders();
this.properties.serverFolder = ns.rootFolder;
this.properties.levelName = "defaultWorld";
this.properties.contentLogFileEnabled = true;
if (this.#port) {
this.properties.serverPort = this.#port;
}
if (start && start.worldSettings) {
this.properties.applyFromWorldSettings(start.worldSettings);
// Apply debugger settings from worldSettings
if (start.worldSettings.enableDebugger !== undefined) {
this.#enableDebugger = start.worldSettings.enableDebugger;
}
if (start.worldSettings.enableDebuggerStreaming !== undefined) {
this.#enableDebuggerStreaming = start.worldSettings.enableDebuggerStreaming;
}
if (start.worldSettings.isEditor !== undefined) {
this.#editorMode = start.worldSettings.isEditor;
}
}
await this.properties.writeFile();
const configFolder = ns.rootFolder.ensureFolder("config");
await configFolder.ensureExists();
this.config.serverConfigFolder = configFolder;
this.config.writeFiles();
}
// Use platform-aware path delimiter instead of hardcoded backslash
rootPath = NodeStorage_1.default.ensureEndsWithDelimiter(rootPath);
this.#env.utilities.validateFolderPath(rootPath);
// Use platform-specific executable name
const executableName = os.platform() === "win32" ? "bedrock_server.exe" : "bedrock_server";
const fullPath = rootPath + executableName;
// Verify the executable exists
if (!fs.existsSync(fullPath)) {
const errorMsg = `Server executable not found at ${fullPath}`;
Log_1.default.fail(errorMsg);
this.#status = DedicatedServerStatus.stopped;
this.#onServerError.dispatch(this, errorMsg);
return;
}
// Verify digital signature on Windows before starting the server
if (os.platform() === "win32" && !start?.unsafeSkipSignatureValidation) {
Log_1.default.message("Verifying digital signature of " + fullPath + "...");
const sigResult = await LocalUtilities_1.default.verifyAuthenticodeSignature(fullPath);
if (!sigResult.isValid) {
const errorMsg = `Digital signature verification failed for ${fullPath}. ` +
`Status: ${sigResult.status}. ${sigResult.error || ""}\n` +
`This could indicate the file has been tampered with or corrupted.\n` +
`If you trust this file, you can skip signature verification with --unsafe-skip-signature-validation.`;
Log_1.default.fail(errorMsg);
this.#status = DedicatedServerStatus.stopped;
this.#onServerError.dispatch(this, errorMsg);
return;
}
if (!sigResult.isMicrosoftSigned) {
const errorMsg = `Digital signature verification: ${fullPath} is signed, but not by Microsoft/Mojang. ` +
`Signer: ${sigResult.signer || "unknown"}\n` +
`This could indicate the file is not an official Minecraft Dedicated Server.\n` +
`If you trust this file, you can skip signature verification with --unsafe-skip-signature-validation.`;
Log_1.default.fail(errorMsg);
this.#status = DedicatedServerStatus.stopped;
this.#onServerError.dispatch(this, errorMsg);
return;
}
Log_1.default.message(`Signature verified: ${sigResult.signer}`);
}
else if (start?.unsafeSkipSignatureValidation) {
Log_1.default.message("WARNING: Skipping digital signature verification as requested. This is unsafe.");
}
Log_1.default.message("Starting server from " + fullPath);
// Kill any stale bedrock_server processes that may hold file locks on this slot
this._killStaleProcesses(rootPath);
// Set up spawn options - on Linux, we need LD_LIBRARY_PATH to find shared libraries
const spawnOptions = {
cwd: rootPath,
};
if (os.platform() !== "win32") {
spawnOptions.env = {
...process.env,
LD_LIBRARY_PATH: rootPath,
};
}
const args = [];
if (this.#editorMode) {
args.push("Editor=true");
}
const childProcess = (0, child_process_1.spawn)(fullPath, args, spawnOptions);
this.#status = DedicatedServerStatus.starting;
childProcess.on("close", this.handleClose);
this.#activeStdIn = childProcess.stdin;
this.#activeProcess = childProcess;
// Write PID file so we can find stale processes after a crash
if (childProcess.pid) {
this._writePidFile(rootPath, childProcess.pid);
}
this.directOutput(childProcess.stdout);
this.directErrors(childProcess.stderr);
Log_1.default.verbose("Server '" +
this.name +
"' at '" +
fullPath +
"' launched" +
(this.#editorMode ? " (Editor mode)" : "") +
" (starts: " +
this.#starts +
").");
}
async executeNextCommand() {
if (this.#currentCommandId < this.#pendingCommands.length) {
this.#currentCommandId++;
const nextCommand = this.#currentCommandId - 1;
const commandLine = this.#pendingCommands[nextCommand];
const isInternal = this.#pendingCommandsInternal[nextCommand];
// Only log non-internal commands
if (!isInternal) {
Log_1.default.message("Command " + this.#currentCommandId + " sent:" + commandLine);
}
await this.writeToServer(commandLine);
await this.executeNextCommand();
}
}
async doRunningBackup() {
if (this.#backupStatus === DedicatedServerBackupStatus.none ||
this.#backupStatus === DedicatedServerBackupStatus.saveResumed) {
this.#backupStatus = DedicatedServerBackupStatus.suspendingSaveCommandIssued;
// Set a timeout to prevent backup from getting stuck if server doesn't respond
// If backup doesn't complete within 60 seconds, force resume save
if (this.#backupTimeoutTimer) {
(0, timers_1.clearTimeout)(this.#backupTimeoutTimer);
}
this.#backupTimeoutTimer = setTimeout(async () => {
if (this.#backupStatus !== DedicatedServerBackupStatus.none &&
this.#backupStatus !== DedicatedServerBackupStatus.saveResumed) {
Log_1.default.error("Backup timed out after 60 seconds - forcing save resume");
this.#backupStatus = DedicatedServerBackupStatus.none;
await this.runCommand("save resume");
}
}, 60000);
await this.runCommand("save hold");
}
}
async doBackup(backupFileLine) {
// If we have a managed world ID and the WorldBackupManager is available,
// use the new backup system. Otherwise, fall back to legacy backup.
if (this.#managedWorldId && this.#dsm.worldBackupManager && this.#defaultWorldStorage) {
await this.doManagedBackup(backupFileLine);
return;
}
// Legacy backup system
const worldPath = "world" + Utilities_1.default.getDateStr(new Date());
const backupFolder = this.#worldBackupContainerFolder.ensureFolder(worldPath);
await backupFolder.ensureExists();
const inclusionList = [];
if (backupFileLine) {
const items = backupFileLine.split(", ");
for (let i = 0; i < items.length; i++) {
const fileItem = items[i].split(":");
if (fileItem.length === 2) {
let size = undefined;
let path = fileItem[0];
const firstSlash = path.indexOf("/");
if (firstSlash > 0) {
path = path.substring(firstSlash + 1);
}
try {
size = parseInt(fileItem[1]);
}
catch (e) {
Log_1.default.verbose("Failed to parse backup file size: " + e);
}
if (size !== undefined && firstSlash > 0) {
inclusionList.push({ path: path, size: size });
}
}
}
}
if (this.#defaultWorldStorage) {
Log_1.default.message("Backing world up to '" + this.#defaultWorldStorage.rootFolder.fullPath + "' to '" + backupFolder.fullPath + "'");
await this.#defaultWorldStorage.rootFolder.copyContentsTo(backupFolder.fullPath, inclusionList, backupFileLine === undefined, this.#dsm.backupWorldFileListings, StorageUtilities_1.default.ensureStartsWithDelimiter(StorageUtilities_1.default.ensureEndsWithDelimiter(this.#worldBackupContainerFolder.name + StorageUtilities_1.default.standardFolderDelimiter + worldPath)));
}
if (inclusionList) {
await backupFolder.saveFilesList(worldPath, inclusionList);
}
}
/**
* Create a backup using the new WorldBackupManager system.
* This provides better organization, deduplication, and export capabilities.
*/
async doManagedBackup(backupFileLine) {
if (!this.#managedWorldId