UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

467 lines (466 loc) 22 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HostType = exports.CreatorToolsThemeStyle = void 0; /* * ========================================================================================== * CREATORTOOLS HOST - PLATFORM ABSTRACTION LAYER NOTES * ========================================================================================== * * OVERVIEW: * --------- * CreatorToolsHost is the central platform abstraction layer that allows MCT to run across * multiple environments: web browser, NodeJS CLI, Electron app, and VS Code extension. * It provides static methods and properties for platform detection, storage initialization, * and cross-platform thunks for functionality that differs by platform. * * HOST TYPES (HostType enum): * --------------------------- * - web (0): Pure browser, no backend services (uses BrowserStorage/localStorage) * - toolsNodejs (1): NodeJS command line tool (uses NodeStorage/fs) * - electronNodeJs (2): Electron main process (NodeJS side) * - electronWeb (3): Electron renderer process (browser side, uses ElectronStorage via IPC) * - vsCodeMainNodeJs (4): VS Code extension host (NodeJS side) * - vsCodeMainWeb (5): VS Code webview connected to extension host * - vsCodeWebService (6): VS Code web extension service worker * - vsCodeWebWeb (7): VS Code web extension webview * - webPlusServices (8): Browser with AppServiceProxy backend * - testLocal (9): Local testing environment * * STORAGE INITIALIZATION: * ----------------------- * The init() method sets up storage based on hostType: * - prefsStorage: User preferences (BrowserStorage in web, ElectronStorage in Electron) * - projectsStorage: Project files and metadata * - deploymentStorage[]: Array of deployment targets indexed by DeploymentTargetType * - worldStorage: Minecraft worlds * - packStorage: Downloaded/cached packs * - workingStorage: Temporary working files * * CROSS-PLATFORM THUNKS: * ---------------------- * These function properties are set based on platform capabilities: * - generateCryptoRandomNumber: Secure random number generation * - localFolderExists / localFileExists: File system checks (only in NodeJS contexts) * - ensureLocalFolder: Create local folder (only in NodeJS contexts) * - createMinecraft / canCreateMinecraft: Minecraft instance creation (Electron/VSCode) * * CONTENT ROOTS: * -------------- * - contentWebRoot: Base URL for APP data (forms, catalogs, snippets, typedefs, mccat.json). * Set to "/" for local HTTP server, or full URL for web hosting. * Used for: data/forms/, data/mccat.json, data/typedefs.*.json, data/mci/, data/mch/ * * - vanillaContentRoot: Base URL for VANILLA MINECRAFT assets (textures, models, resource packs). * These are large files (~500MB+) not bundled with CLI tool, hosted at mctools.dev. * Used for: res/latest/van/release/, res/latest/van/preview/, res/latest/van/serve/ * * - getVanillaContentRoot(): Returns vanillaContentRoot if set, else contentWebRoot. * Use this for all vanilla resource loading (textures, blocks.json, terrain_texture.json). * * INITIALIZATION FLOW: * -------------------- * 1. Global variables (g_contentRoot, g_isVsCodeMain, etc.) are injected by build system * 2. init() reads globals and sets hostType * 3. AppServiceProxy.init() establishes IPC if in Electron/VSCode * 4. Database.loadVanillaCatalog() loads mccat.json * 5. Storage objects are created based on detected platform * 6. CreatorTools singleton is instantiated with storage references * * RELATED FILES: * -------------- * - CreatorTools.ts: Main application object, instantiated by CreatorToolsHost * - AppServiceProxy.ts: IPC communication layer for Electron/VSCode * - BrowserStorage.ts, ElectronStorage.ts, NodeStorage.ts: Storage implementations * - Database.ts: Loads vanilla catalog and form definitions * * COMMON PATTERNS: * ---------------- * - Check platform: CreatorToolsHost.isNodeJs, .isWeb, .isVsCode, .isAppServiceWeb * - Get main app: CreatorToolsHost.getCreatorTools() * - Listen for init: CreatorToolsHost.onInitialized.subscribe(handler) * - Theme changes: CreatorToolsHost.onThemeChanged.subscribe(handler) * * ========================================================================================== */ const CreatorTools_1 = __importDefault(require("./CreatorTools")); const Database_1 = __importDefault(require("../minecraft/Database")); const BrowserStorage_1 = __importDefault(require("../storage/BrowserStorage")); const ElectronStorage_1 = __importDefault(require("../electronclient/ElectronStorage")); const ste_events_1 = require("ste-events"); const AppServiceProxy_1 = __importDefault(require("../core/AppServiceProxy")); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); const CreatorToolsCommands_1 = __importDefault(require("./CreatorToolsCommands")); const DeploymentTarget_1 = require("./DeploymentTarget"); var CreatorToolsThemeStyle; (function (CreatorToolsThemeStyle) { CreatorToolsThemeStyle[CreatorToolsThemeStyle["dark"] = 0] = "dark"; CreatorToolsThemeStyle[CreatorToolsThemeStyle["light"] = 1] = "light"; CreatorToolsThemeStyle[CreatorToolsThemeStyle["highContrastDark"] = 2] = "highContrastDark"; CreatorToolsThemeStyle[CreatorToolsThemeStyle["highContrastLight"] = 3] = "highContrastLight"; })(CreatorToolsThemeStyle || (exports.CreatorToolsThemeStyle = CreatorToolsThemeStyle = {})); var HostType; (function (HostType) { HostType[HostType["web"] = 0] = "web"; HostType[HostType["toolsNodejs"] = 1] = "toolsNodejs"; HostType[HostType["electronNodeJs"] = 2] = "electronNodeJs"; HostType[HostType["electronWeb"] = 3] = "electronWeb"; HostType[HostType["vsCodeMainNodeJs"] = 4] = "vsCodeMainNodeJs"; HostType[HostType["vsCodeMainWeb"] = 5] = "vsCodeMainWeb"; HostType[HostType["vsCodeWebService"] = 6] = "vsCodeWebService"; HostType[HostType["vsCodeWebWeb"] = 7] = "vsCodeWebWeb"; HostType[HostType["webPlusServices"] = 8] = "webPlusServices"; HostType[HostType["testLocal"] = 9] = "testLocal"; })(HostType || (exports.HostType = HostType = {})); /// CreatorToolsHost mostly serves as a static app loader for the really-core Carto app object. class CreatorToolsHost { static _creatorTools; static _onInitialized = new ste_events_1.EventDispatcher(); static _onThemeChanged = new ste_events_1.EventDispatcher(); static _initializing = false; static _initialized = false; static isLocalNode = false; static fullLocalStorage = false; static hostType = HostType.web; static hostManager = undefined; static _theme = CreatorToolsThemeStyle.dark; /** * True when the OS/browser is in a forced-colors (high contrast) mode. * This is orthogonal to dark/light — HC Dark sets theme=dark + isHighContrast, * HC Light sets theme=light + isHighContrast. */ static isHighContrast = false; static retrieveDataFromWebContentRoot = false; static contentWebRoot = ""; static vanillaContentRoot = ""; // URL root for vanilla textures, samples, definitions (e.g., https://mctools.dev/) static projectPath = ""; static focusPath = undefined; static baseUrl = ""; /** * Gets the effective root URL for vanilla content (textures, samples, definitions). * Returns vanillaContentRoot if set, otherwise falls back to contentRoot. */ static getVanillaContentRoot() { return this.vanillaContentRoot || this.contentWebRoot || ""; } /** * Gets the current theme style */ static get theme() { return this._theme; } /** * Sets the theme style and notifies listeners */ static set theme(value) { if (this._theme !== value) { this._theme = value; this._onThemeChanged.dispatch(CreatorToolsHost, value); } } /** * Event fired when theme changes. Use this to update FluentUI components * that are initialized outside of React's render cycle. */ static get onThemeChanged() { return this._onThemeChanged.asEvent(); } static initialMode = null; static modeParameter = null; static contentUrl = null; static readOnly = false; static postMessage; static prefsStorage = null; static projectsStorage = null; static deploymentStorage = []; static worldStorage = null; static packStorage = null; static workingStorage = null; static generateCryptoRandomNumber; static generateUuid; static localFolderExists; static localFileExists; static ensureLocalFolder; static createMinecraft; static canCreateMinecraft; // Image codec thunks - set by Node.js environments to provide platform-specific implementations static decodePng; static encodeToPng; static _onMessage = new ste_events_1.EventDispatcher(); static get isNodeJs() { return (this.hostType === HostType.electronNodeJs || this.hostType === HostType.toolsNodejs || this.hostType === HostType.vsCodeMainNodeJs); } static get isVsCode() { return (this.hostType === HostType.vsCodeMainNodeJs || this.hostType === HostType.vsCodeWebService || this.hostType === HostType.vsCodeMainWeb || this.hostType === HostType.vsCodeWebWeb); } static get isWeb() { return (this.hostType === HostType.webPlusServices || this.hostType === HostType.web || this.hostType === HostType.electronWeb || this.hostType === HostType.vsCodeMainWeb || this.hostType === HostType.vsCodeWebWeb); } static get isAppServiceWeb() { return this.hostType === HostType.electronWeb || this.hostType === HostType.vsCodeMainWeb; } static get onMessage() { return CreatorToolsHost._onMessage.asEvent(); } static get onInitialized() { return CreatorToolsHost._onInitialized.asEvent(); } static getCreatorTools() { if (!this._initialized) { this.init(); } return CreatorToolsHost._creatorTools; } static notifyNewMessage(source, message) { CreatorToolsHost._onMessage.dispatch(source, message); } static setHostType(type) { this.hostType = type; } static init() { if (CreatorToolsHost._initializing || CreatorToolsHost._initialized) { return; } CreatorToolsHost._initializing = true; //@ts-ignore if (typeof g_contentRoot !== "undefined") { //@ts-ignore CreatorToolsHost.contentWebRoot = StorageUtilities_1.default.ensureEndsWithDelimiter(g_contentRoot); } //@ts-ignore if (typeof g_vanillaContentRoot !== "undefined") { //@ts-ignore CreatorToolsHost.vanillaContentRoot = StorageUtilities_1.default.ensureEndsWithDelimiter(g_vanillaContentRoot); } //@ts-ignore if (typeof g_initialMode !== "undefined") { //@ts-ignore CreatorToolsHost.initialMode = g_initialMode; } //@ts-ignore if (typeof g_modeParameter !== "undefined") { //@ts-ignore CreatorToolsHost.modeParameter = g_modeParameter; } //@ts-ignore if (typeof g_isVsCodeMain !== "undefined") { //@ts-ignore if (g_isVsCodeMain) { CreatorToolsHost.hostType = HostType.vsCodeMainWeb; } } //@ts-ignore if (typeof g_isVsCodeWeb !== "undefined") { //@ts-ignore if (g_isVsCodeWeb) { CreatorToolsHost.hostType = HostType.vsCodeWebWeb; } } //@ts-ignore if (typeof g_projectPath !== "undefined") { //@ts-ignore CreatorToolsHost.projectPath = g_projectPath; } //@ts-ignore if (typeof g_baseUrl !== "undefined") { //@ts-ignore CreatorToolsHost.baseUrl = g_baseUrl; } //@ts-ignore if (typeof g_contentUrl !== "undefined") { //@ts-ignore CreatorToolsHost.contentUrl = g_contentUrl; } //@ts-ignore if (typeof g_readOnly !== "undefined") { //@ts-ignore CreatorToolsHost.readOnly = g_readOnly === true; } AppServiceProxy_1.default.init(); Database_1.default.loadVanillaCatalog(); CreatorToolsCommands_1.default.registerCommonCommands(); CreatorToolsHost.generateCryptoRandomNumber = (toVal) => { const ct = CreatorToolsHost.getCreatorTools(); if (!ct || !ct.local) { throw new Error("Could not generate key."); } return ct.local.generateCryptoRandomNumber(toVal); }; CreatorToolsHost.generateUuid = () => { const ct = CreatorToolsHost.getCreatorTools(); if (!ct || !ct.local) { throw new Error("Could not generate UUID."); } return ct.local.generateUuid(); }; // Set up image codec thunks for Node.js environments CreatorToolsHost.decodePng = (data) => { const ct = CreatorToolsHost.getCreatorTools(); if (!ct || !ct.local) { return undefined; } return ct.local.decodePng(data); }; CreatorToolsHost.encodeToPng = (pixels, width, height) => { const ct = CreatorToolsHost.getCreatorTools(); if (!ct || !ct.local) { return undefined; } return ct.local.encodeToPng(pixels, width, height); }; // @ts-ignore if (typeof window !== "undefined" && window.crypto) { CreatorToolsHost.generateCryptoRandomNumber = (toVal) => { // Use rejection sampling to avoid modulo bias when generating random numbers // from a cryptographically secure source const maxUint32 = 0xffffffff; const limit = maxUint32 - (maxUint32 % toVal); let randomValue; do { // @ts-ignore randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0]; } while (randomValue >= limit); return randomValue % toVal; }; // @ts-ignore if (window.crypto.randomUUID) { CreatorToolsHost.generateUuid = () => { // @ts-ignore return window.crypto.randomUUID(); }; } else { // Fallback for older browsers using crypto.getRandomValues CreatorToolsHost.generateUuid = () => { // @ts-ignore const bytes = window.crypto.getRandomValues(new Uint8Array(16)); // Set version 4 (random) UUID bits bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; }; } } if (CreatorToolsHost.projectsStorage !== null && CreatorToolsHost.prefsStorage !== null) { } else if (AppServiceProxy_1.default.hasAppService) { let ls = new ElectronStorage_1.default("<DOCP>", "prefs"); ls.rootFolder.ensureExists(); CreatorToolsHost.prefsStorage = ls; ls = new ElectronStorage_1.default("<DOCP>", "working"); ls.rootFolder.ensureExists(); CreatorToolsHost.workingStorage = ls; ls = new ElectronStorage_1.default("<DOCP>", "projects"); ls.rootFolder.ensureExists(); CreatorToolsHost.projectsStorage = ls; ls = new ElectronStorage_1.default("<DOCP>", "worlds"); ls.rootFolder.ensureExists(); CreatorToolsHost.worldStorage = ls; ls = new ElectronStorage_1.default("<DOCP>", "packs"); ls.rootFolder.ensureExists(); CreatorToolsHost.packStorage = ls; // @ts-ignore if (typeof window !== "undefined") { // @ts-ignore let basePath = window.location.href; const lastSlash = basePath.lastIndexOf("/"); if (lastSlash >= 0) { CreatorToolsHost.contentWebRoot = basePath.substring(0, lastSlash + 1); } } let minecraftPath = "<BDRK>"; ls = new ElectronStorage_1.default(minecraftPath, ""); ls.getAvailable(); CreatorToolsHost.deploymentStorage[DeploymentTarget_1.DeploymentTargetType.bedrock] = ls; let minecraftPreviewPath = "<BDPV>"; ls = new ElectronStorage_1.default(minecraftPreviewPath, ""); ls.getAvailable(); CreatorToolsHost.deploymentStorage[DeploymentTarget_1.DeploymentTargetType.bedrockPreview] = ls; //@ts-ignore } else if (typeof window !== "undefined") { // Guard: If we're in Electron context but AppServiceProxy isn't ready, that's an initialization error. // Electron renderer should NEVER use BrowserStorage - it should use ElectronStorage which proxies to NodeJS. // @ts-ignore if (typeof window.api !== "undefined") { throw new Error("BrowserStorage initialization attempted in Electron context. " + "Electron renderer must use ElectronStorage. " + "Ensure AppServiceProxy.hasAppService is true before CreatorToolsHost.init() is called."); } CreatorToolsHost.prefsStorage = new BrowserStorage_1.default("mctprefs"); CreatorToolsHost.projectsStorage = new BrowserStorage_1.default("mctprojects"); CreatorToolsHost.deploymentStorage[DeploymentTarget_1.DeploymentTargetType.bedrock] = new BrowserStorage_1.default("mctdeploy"); CreatorToolsHost.workingStorage = new BrowserStorage_1.default("mctworking"); CreatorToolsHost.worldStorage = new BrowserStorage_1.default("mctworlds"); CreatorToolsHost.packStorage = new BrowserStorage_1.default("mctpacks"); } if (CreatorToolsHost.prefsStorage === null || CreatorToolsHost.projectsStorage === null) { throw new Error("Unexpected uninitialized storage."); } CreatorToolsHost._creatorTools = new CreatorTools_1.default(CreatorToolsHost.prefsStorage, CreatorToolsHost.projectsStorage, CreatorToolsHost.deploymentStorage, CreatorToolsHost.worldStorage, CreatorToolsHost.packStorage, CreatorToolsHost.workingStorage, CreatorToolsHost.contentWebRoot); if (CreatorToolsHost.ensureLocalFolder) { CreatorToolsHost._creatorTools.ensureLocalFolder = CreatorToolsHost.ensureLocalFolder; CreatorToolsHost._creatorTools.localFolderExists = CreatorToolsHost.localFolderExists; CreatorToolsHost._creatorTools.localFileExists = CreatorToolsHost.localFileExists; } else if (CreatorToolsHost.hostType === HostType.electronWeb) { CreatorToolsHost._creatorTools.ensureLocalFolder = CreatorToolsHost._ensureElectronLocalFolder; CreatorToolsHost._creatorTools.localFolderExists = CreatorToolsHost._localFolderExists; CreatorToolsHost._creatorTools.localFileExists = CreatorToolsHost._localFileExists; } CreatorToolsHost._creatorTools.createMinecraft = CreatorToolsHost.createMinecraft; CreatorToolsHost._creatorTools.canCreateMinecraft = CreatorToolsHost.canCreateMinecraft; CreatorToolsHost._initialized = true; this._onInitialized.dispatch(CreatorToolsHost._creatorTools, CreatorToolsHost._creatorTools); } static async _localFolderExists(path) { const ls = new ElectronStorage_1.default(path, ""); let result = ls.available; if (!result) { result = await ls.getAvailable(); if (!result) { return false; } } return await ls.rootFolder.exists(); } static async _localFileExists(path) { const folderPath = StorageUtilities_1.default.getFolderPath(path); const fileName = StorageUtilities_1.default.getLeafName(path); if (!fileName || fileName.length < 2 || !folderPath || folderPath.length < 2) { throw new Error("Could not process file with path: `" + path + "`"); } const ls = new ElectronStorage_1.default(folderPath, ""); const file = ls.rootFolder.ensureFile(fileName); return await file.exists(); } static _ensureElectronLocalFolder(path) { // Reuse existing storage if one already exists for this path // (e.g., from a prior _localFolderExists call that set up the file watcher). // Creating a new ElectronStorage overwrites electronStorages[path], which could // orphan an existing storage that has its file watcher configured. let ls = ElectronStorage_1.default.electronStorages[path]; if (!ls) { ls = new ElectronStorage_1.default(path, ""); // Kick off getAvailable() to set up the fs.watch file watcher in the main process. // Without this, externally created files (e.g., by MCP tools) won't be detected. ls.getAvailable(); } return ls.rootFolder; } } exports.default = CreatorToolsHost;