UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

368 lines (367 loc) 16 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.serverCommand = exports.ServerCommand = void 0; const IToolCommand_1 = require("../IToolCommand"); /** * DedicatedServerStatus.started value. Imported dynamically along with other * server dependencies to avoid pulling in the full DedicatedServer module * (which has heavy transitive dependencies) at module initialization time. */ // These value imports are lazy-loaded at runtime to avoid pulling in heavy // module dependency chains (WorldLevelDat → NbtBinary → FileBase, etc.) // at module initialization time — which causes issues in test runners. async function getServerDependencies() { const [{ DedicatedServerMode }, { GameType, Generator, Difficulty, PlayerPermissionsLevel }, PackageModule, { DedicatedServerStatus },] = await Promise.all([ Promise.resolve().then(() => __importStar(require("../../ICreatorToolsData"))), Promise.resolve().then(() => __importStar(require("../../../minecraft/WorldLevelDat"))), Promise.resolve().then(() => __importStar(require("../../Package"))), Promise.resolve().then(() => __importStar(require("../../../local/DedicatedServer"))), ]); return { DedicatedServerMode, GameType, Generator, Difficulty, PlayerPermissionsLevel, Package: PackageModule.default, DedicatedServerStatus, }; } class ServerCommand extends IToolCommand_1.ToolCommandBase { metadata = { name: "server", description: "Manage Bedrock Dedicated Server (start, stop, status)", aliases: ["srv", "bds"], category: "Server", arguments: [ { name: "action", description: "Action to perform: start, stop, or status", type: "string", required: true, autocompleteProvider: async (partial, _context) => { const actions = ["start", "stop", "status"]; if (!partial) return actions; return actions.filter((a) => a.startsWith(partial.toLowerCase())); }, }, ], flags: [ { name: "slot", shortName: "l", description: "Server slot number (default: 0). Each slot runs on a separate port.", type: "string", required: false, }, { name: "project", shortName: "p", description: "Path to project to deploy (default: current project if available)", type: "string", required: false, }, { name: "session", shortName: "s", description: "Session name (required for MCP/API scope)", type: "string", required: false, }, { name: "fresh", shortName: "f", description: "Force a fresh world (discard existing world data)", type: "boolean", required: false, }, { name: "json", shortName: "j", description: "Output results in JSON format for scripts and CI/CD pipelines", type: "boolean", required: false, }, { name: "wait-ready", shortName: "w", description: "Wait for server to be fully ready before returning", type: "boolean", required: false, }, { name: "timeout", shortName: "t", description: "Timeout in seconds for --wait-ready (default: 60)", type: "string", required: false, }, { name: "editor", shortName: "e", description: "Launch BDS in Minecraft Editor mode", type: "boolean", required: false, }, ], scopes: [IToolCommand_1.ToolCommandScope.ui, IToolCommand_1.ToolCommandScope.serveTerminal, IToolCommand_1.ToolCommandScope.mcp, IToolCommand_1.ToolCommandScope.serverApi], examples: [ "/server start", "/server start --project ./myAddon", "/server start --slot 1 --fresh", "/server start --wait-ready --timeout 120", "/server start --editor", "/server start --json", "/server stop", "/server status", "/server status --json", ], }; async execute(context, args, flags) { const action = (args[0] || "").toLowerCase(); const slot = parseInt(flags.slot, 10) || 0; if (!action || !["start", "stop", "status"].includes(action)) { return this.error("INVALID_ACTION", 'Invalid action. Use "start", "stop", or "status".'); } let result; switch (action) { case "start": result = await this._startServer(context, slot, flags); break; case "stop": result = await this._stopServer(context, slot, flags); break; case "status": result = await this._getStatus(context, slot, flags); break; default: result = this.error("INVALID_ACTION", `Unknown action: ${action}`); break; } if (flags.json === true) { const jsonOutput = result.success ? { ...result.data, message: result.message } : { status: "error", code: result.error?.code, message: result.error?.message }; context.output.info(JSON.stringify(jsonOutput)); } return result; } async _startServer(context, slot, flags) { const serverManager = this._getServerManager(context); if (!serverManager) { return this.error("NO_SERVER_MANAGER", "No ServerManager available. This command requires a serve-mode or Electron context with BDS support."); } context.output.info("Preparing Bedrock Dedicated Server..."); try { // Prepare ServerManager (downloads BDS if needed) await serverManager.prepare(); // Lazy-load heavy dependencies to avoid circular import issues at module init time const deps = await getServerDependencies(); // Build world settings with creator tools infrastructure addon const packRefs = []; deps.Package.ensureMinecraftCreatorToolsPackageReference(packRefs); const worldSettings = { gameType: deps.GameType.creative, generator: deps.Generator.flat, cheatsEnabled: true, difficulty: deps.Difficulty.peaceful, playerPermissionLevel: deps.PlayerPermissionsLevel.operator, permissionLevel: deps.PlayerPermissionsLevel.operator, randomSeed: "2000", packageReferences: packRefs, worldTemplateReferences: undefined, isEditor: flags.editor === true, }; const forceNewWorld = flags.fresh === true || flags.fresh === "true"; const startMessage = { mode: deps.DedicatedServerMode.auto, path: undefined, forceStartNewWorld: forceNewWorld, worldSettings, transientWorld: true, }; // If a project path or current project is available, add it as additional content const projectPath = flags.project; if (projectPath) { startMessage.additionalContentPath = projectPath; } else if (context.project?.projectFolder) { // Deploy current project's content if available const folderPath = context.project.projectFolder.fullPath; if (folderPath) { startMessage.additionalContentPath = folderPath; } } context.output.progress(1, 4, "Provisioning server slot..."); if (flags.editor === true) { context.output.info("Editor mode enabled — BDS will launch with Editor=true"); } const server = await serverManager.ensureActiveServer(slot, startMessage); if (!server) { return this.error("SERVER_FAILED", "Failed to create server instance"); } context.output.progress(2, 4, "Starting Bedrock Dedicated Server..."); await server.startServer(false, startMessage); context.output.progress(3, 4, "Waiting for server to be ready..."); await server.waitUntilStarted(); context.output.progress(4, 4, "Server started!"); const port = serverManager.getBasePortForSlot(slot); // --wait-ready: additional verification that the server is fully ready if (flags["wait-ready"] === true) { const startTime = Date.now(); const timeoutSec = parseInt(flags.timeout, 10) || 60; const deadline = startTime + timeoutSec * 1000; while (server.status !== deps.DedicatedServerStatus.started && Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 250)); } if (server.status !== deps.DedicatedServerStatus.started) { return { ...this.error("TIMEOUT", `Server did not reach ready state within ${timeoutSec}s`), exitCode: IToolCommand_1.ToolCommandExitCode.Timeout, }; } const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); context.output.info(`Server ready after ${elapsed}s`); } context.output.success(`Server started on slot ${slot} (port ${port})`); return this.success(`Server started on slot ${slot} (port ${port})`, { slot, port, status: "started", }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { ...this.error("START_FAILED", `Failed to start server: ${message}`), exitCode: this._categorizeError(message), }; } } async _stopServer(context, slot, _flags) { const serverManager = this._getServerManager(context); if (!serverManager) { return this.error("NO_SERVER_MANAGER", "No ServerManager available."); } try { const server = serverManager.getActiveServer(slot); if (!server) { return this.error("NO_SERVER", `No active server on slot ${slot}`); } context.output.info(`Stopping server on slot ${slot}...`); await server.stopServer(); context.output.success(`Server stopped on slot ${slot}`); return this.success(`Server stopped on slot ${slot}`, { slot, status: "stopped", }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { ...this.error("STOP_FAILED", `Failed to stop server: ${message}`), exitCode: this._categorizeError(message), }; } } async _getStatus(context, slot, _flags) { const serverManager = this._getServerManager(context); if (!serverManager) { return this.error("NO_SERVER_MANAGER", "No ServerManager available."); } const server = serverManager.getActiveServer(slot); if (!server) { context.output.info(`No active server on slot ${slot}`); return this.success(`No active server on slot ${slot}`, { slot, status: "none", running: false, }); } const status = server.status; const port = serverManager.getBasePortForSlot(slot); const deps = await getServerDependencies(); context.output.info(`Server on slot ${slot}: status=${status}, port=${port}`); return this.success(`Server status: ${status}`, { slot, port, status: String(status), running: status === deps.DedicatedServerStatus.started, }); } /** * Categorize an error message into a standard exit code. */ _categorizeError(message) { const lower = message.toLowerCase(); if ((lower.includes("port") && lower.includes("in use")) || lower.includes("eaddrinuse")) { return IToolCommand_1.ToolCommandExitCode.PortConflict; } if (lower.includes("eula")) { return IToolCommand_1.ToolCommandExitCode.EulaNotAccepted; } if (lower.includes("download") || lower.includes("network") || lower.includes("econnrefused")) { return IToolCommand_1.ToolCommandExitCode.NetworkError; } if (lower.includes("crash") || lower.includes("unexpected exit")) { return IToolCommand_1.ToolCommandExitCode.CrashOnStartup; } if (lower.includes("timeout") || lower.includes("timed out")) { return IToolCommand_1.ToolCommandExitCode.Timeout; } return IToolCommand_1.ToolCommandExitCode.GenericError; } /** * Get the ServerManager from whichever context is available. * Priority: session.serverManager > minecraft (if it wraps a server) > undefined */ _getServerManager(context) { // Session-based (MCP/API mode) if (context.session?.serverManager) { return context.session.serverManager; } // For UI/serve modes, we need to get or create a ServerManager. // The ServerManager is typically created by the hosting environment // (Electron DedicatedServerCommandHandler, MinecraftMcpServer, etc.) // We can create one on-demand for the creatorTools instance. return undefined; } } exports.ServerCommand = ServerCommand; exports.serverCommand = new ServerCommand();