@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
374 lines (373 loc) • 12.1 kB
JavaScript
"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 });
const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities"));
const Utilities_1 = __importDefault(require("../core/Utilities"));
const Log_1 = __importDefault(require("../core/Log"));
const IWorldBackupData_1 = require("./IWorldBackupData");
const WorldBackup_1 = __importDefault(require("./WorldBackup"));
const NodeFolder_1 = __importDefault(require("./NodeFolder"));
class ManagedWorld {
_data;
_folder;
_backups = [];
_backupsLoaded = false;
/**
* Get the unique world ID.
*/
get id() {
return this._data.id;
}
/**
* Alias for id (for consistency with some API usage).
*/
get worldId() {
return this._data.id;
}
/**
* Get the user-visible friendly name.
*/
get friendlyName() {
return this._data.friendlyName;
}
/**
* Set the user-visible friendly name.
*/
set friendlyName(value) {
this._data.friendlyName = value;
this._data.lastModified = new Date().toISOString();
}
/**
* Get the description.
*/
get description() {
return this._data.description;
}
/**
* Set the description.
*/
set description(value) {
this._data.description = value;
this._data.lastModified = new Date().toISOString();
}
/**
* Get the creation timestamp.
*/
get createdAt() {
return new Date(this._data.createdAt);
}
/**
* Get the last modified timestamp.
*/
get lastModified() {
return new Date(this._data.lastModified);
}
/**
* Get the initial configuration hash.
*/
get initialConfigurationHash() {
return this._data.initialConfigurationHash;
}
/**
* Set the initial configuration hash.
*/
set initialConfigurationHash(value) {
this._data.initialConfigurationHash = value;
this._data.lastModified = new Date().toISOString();
}
/**
* Get optional notes.
*/
get notes() {
return this._data.notes;
}
/**
* Set optional notes.
*/
set notes(value) {
this._data.notes = value;
this._data.lastModified = new Date().toISOString();
}
/**
* Get tags for organization.
*/
get tags() {
return this._data.tags || [];
}
/**
* Set tags for organization.
*/
set tags(value) {
this._data.tags = value;
this._data.lastModified = new Date().toISOString();
}
/**
* Get the folder containing this world's data.
*/
get folder() {
return this._folder;
}
/**
* Get the raw data object.
*/
get data() {
return this._data;
}
/**
* Get the list of backups (must call loadBackups first).
*/
get backups() {
return this._backups;
}
/**
* Private constructor - use static factory methods.
*/
constructor(data, folder) {
this._data = data;
this._folder = folder;
}
/**
* Generate a random 8-character world ID.
*/
static generateId() {
let id = "";
for (let i = 0; i < IWorldBackupData_1.WORLD_ID_LENGTH; i++) {
id += IWorldBackupData_1.WORLD_ID_CHARS.charAt(Math.floor(Math.random() * IWorldBackupData_1.WORLD_ID_CHARS.length));
}
return id;
}
/**
* Validate a world ID format.
*/
static isValidId(id) {
if (id.length !== IWorldBackupData_1.WORLD_ID_LENGTH) {
return false;
}
for (const char of id) {
if (!IWorldBackupData_1.WORLD_ID_CHARS.includes(char)) {
return false;
}
}
return true;
}
/**
* Create a new managed world.
*
* @param friendlyName User-visible name for the world
* @param parentFolder The worlds container folder
* @param configurationHash Optional initial configuration hash
* @returns The newly created ManagedWorld
*/
static async create(friendlyName, parentFolder, configurationHash) {
const id = ManagedWorld.generateId();
const now = new Date().toISOString();
const data = {
id,
friendlyName,
initialConfigurationHash: configurationHash,
createdAt: now,
lastModified: now,
};
const worldFolder = parentFolder.ensureFolder(id);
await worldFolder.ensureExists();
const world = new ManagedWorld(data, worldFolder);
await world.save();
Log_1.default.message(`Created new managed world: ${friendlyName} (${id})`);
return world;
}
/**
* Load an existing managed world from a folder.
*
* @param folder The world folder (e.g., worlds/a3k9m2p1/)
* @returns The loaded ManagedWorld, or undefined if invalid
*/
static async load(folder) {
await folder.load(false);
const worldFile = folder.files["world.json"];
if (!worldFile) {
Log_1.default.debug(`No world.json found in ${folder.fullPath}`);
return undefined;
}
await worldFile.loadContent(false);
const data = StorageUtilities_1.default.getJsonObject(worldFile);
if (!data || !data.id) {
Log_1.default.debug(`Invalid world.json in ${folder.fullPath}`);
return undefined;
}
return new ManagedWorld(data, folder);
}
/**
* Save the world metadata to world.json.
*/
async save() {
const worldFile = this._folder.ensureFile("world.json");
worldFile.setContent(JSON.stringify(this._data, null, 2));
await worldFile.saveContent();
}
/**
* Load all backups for this world.
*/
async loadBackups() {
if (this._backupsLoaded) {
return this._backups;
}
// Clear existing folder references to ensure fresh objects when loading
// This prevents stale in-memory folder objects from being reused
for (const folderName in this._folder.folders) {
delete this._folder.folders[folderName];
}
await this._folder.load(true);
this._backups = [];
for (const folderName in this._folder.folders) {
if (folderName.startsWith("world") && folderName.length === 19) {
const dateStr = folderName.substring(5);
if (Utilities_1.default.isNumeric(dateStr)) {
const backupFolder = this._folder.folders[folderName];
if (backupFolder) {
const backup = await WorldBackup_1.default.load(backupFolder, this._data.id);
if (backup) {
this._backups.push(backup);
}
}
}
}
}
// Sort by creation date, newest first
this._backups.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
this._backupsLoaded = true;
return this._backups;
}
/**
* Get the latest backup for this world.
*/
async getLatestBackup() {
await this.loadBackups();
return this._backups.length > 0 ? this._backups[0] : undefined;
}
/**
* Get a specific backup by ID.
*/
async getBackup(backupId) {
await this.loadBackups();
return this._backups.find((b) => b.id === backupId);
}
/**
* Get summary info for the manifest.
*/
getSummary() {
return {
id: this._data.id,
friendlyName: this._data.friendlyName,
createdAt: this._data.createdAt,
lastBackupAt: this._backups.length > 0 ? this._backups[0].createdAt.toISOString() : undefined,
backupCount: this._backups.length,
worldId: this._data.id,
lastModified: this._backups.length > 0
? this._backups[0].createdAt.toISOString()
: this._data.lastModified || this._data.createdAt,
};
}
/**
* Delete a backup.
*
* @param backupId The backup ID to delete
* @returns True if deleted, false if not found
*/
async deleteBackup(backupId) {
await this.loadBackups();
const backupIndex = this._backups.findIndex((b) => b.id === backupId);
if (backupIndex < 0) {
return false;
}
const backup = this._backups[backupIndex];
await backup.delete();
this._backups.splice(backupIndex, 1);
Log_1.default.message(`Deleted backup ${backupId} from world ${this._data.id}`);
return true;
}
/**
* Prune old backups, keeping only the specified count.
* Before deleting, consolidates any remaining backups that depend on files
* in the backups being deleted.
*
* @param keepCount Number of backups to keep
* @returns Number of backups deleted
*/
async pruneBackups(keepCount) {
await this.loadBackups();
if (this._backups.length <= keepCount) {
return 0;
}
const toKeep = this._backups.slice(0, keepCount);
const toDelete = this._backups.slice(keepCount);
// Build a set of backup paths that will be deleted
const deletingPaths = new Set();
for (const backup of toDelete) {
// Format: /worldId/backupId/
deletingPaths.add(`/${this._data.id}/${backup.id}/`);
}
// Consolidate any backups being kept that depend on backups being deleted
for (const backup of toKeep) {
const dependencies = backup.getDependencies();
for (const dep of dependencies) {
if (deletingPaths.has(dep)) {
// This backup depends on a backup being deleted - consolidate it
await backup.consolidate(this._folder.parentFolder);
break; // Only need to consolidate once per backup
}
}
}
let deleted = 0;
for (const backup of toDelete) {
try {
await backup.delete();
deleted++;
}
catch (e) {
Log_1.default.error(`Failed to delete backup ${backup.id}: ${e}`);
}
}
// Invalidate cache to ensure next access reloads from disk with fresh folder objects
this.invalidateBackupCache();
Log_1.default.message(`Pruned ${deleted} old backups from world ${this._data.id}`);
return deleted;
}
/**
* Delete this world and all its backups.
*/
async delete() {
try {
if (this._folder instanceof NodeFolder_1.default) {
await this._folder.deleteThisFolder();
}
Log_1.default.message(`Deleted managed world ${this._data.id}`);
return true;
}
catch (e) {
Log_1.default.error(`Failed to delete world ${this._data.id}: ${e}`);
return false;
}
}
/**
* Register a new backup folder (called by WorldBackupManager).
* Note: This invalidates the backup cache so the next loadBackups() reloads from disk
* to ensure the in-memory folder structure is synchronized.
*/
registerBackup(backup) {
// Invalidate cache to ensure next load reads from disk with proper file structure
this.invalidateBackupCache();
this._data.lastModified = new Date().toISOString();
}
/**
* Invalidate the backup cache to force reload.
*/
invalidateBackupCache() {
this._backupsLoaded = false;
this._backups = [];
}
}
exports.default = ManagedWorld;