UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

389 lines (385 loc) 23.4 kB
"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 }); const Versioning_1 = __importDefault(require("../../../app/Versioning")); const ArrayUtilities_1 = require("../../../core/ArrayUtilities"); const ObjectUtilities_1 = require("../../../core/ObjectUtilities"); const Utilities_1 = __importDefault(require("../../../core/Utilities")); const SemanticVersion_1 = __importDefault(require("../../../core/versioning/SemanticVersion")); const Pack_1 = require("../../../minecraft/Pack"); const TestDefinition_1 = require("../../tests/TestDefinition"); const Manifest_1 = require("../../../minecraft/manifests/Manifest"); const CheckManifestData_1 = require("./CheckManifestData"); const ValidationData = __importStar(require("./CheckManifestData")); const IProjectItemData_1 = require("../../../app/IProjectItemData"); /** * Validates pack manifest files for structure, required fields, and format compliance. * * @see {@link ../../../../public/data/forms/mctoolsval/chkmanif.form.json} for topic definitions */ class CheckManifestGenerator { id = "CHKMANIF"; title = "Manifest Validation"; canAlwaysProcess = true; async generate(project, contentIndex) { const packs = project.packs; const invalidManifests = packs .filter((pack) => { const manifests = pack.getPackItems().filter((item) => item.name === "manifest.json"); return manifests.length === 0 || manifests.length > 1; }) .map(() => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidNumberOfManifests, { id: this.id })); if (invalidManifests.length > 0) { return invalidManifests; } const packManifests = packs.map((pack) => [pack.getManifest(), pack]); const worldManifests = project.items .filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.worldTemplateManifestJson) .map((manifest) => [manifest, { isWorld: true }]); const allManifests = [...worldManifests, ...packManifests]; const results = await Promise.all(allManifests.map(([desc, manifest]) => this.validateManifest(desc, manifest))); return results.flat(); } async validateManifest(manifestItem, pack) { const json = await manifestItem.getContentAsJson(); const [manifest, parseErrors] = (0, Manifest_1.parseManifest)(json); if (parseErrors) { return parseErrors .map((error) => ({ id: this.id, message: error.message, item: manifestItem, data: error.propertyName })) .map((resultData) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidManifestSchema, resultData)); } const results = [ ...this.validateFormatVersion(pack, manifest, manifestItem), ...this.validateIds(pack, manifest, manifestItem), ...this.validateHeader(pack, manifest, manifestItem), ...this.validateModules(pack, manifest, manifestItem), ...this.validateDependencies(pack, manifest, manifestItem), ...this.validateSubpacks(pack, manifest, manifestItem), ...this.validateCapabilities(pack, manifest, manifestItem), ...this.validateCapabilitiesForMinEngineVersionPlusPbr(pack, manifest, manifestItem), ...this.validateSettings(pack, manifest, manifestItem), ]; return results; } validateIds(pack, manifest, manifestItem) { const allIds = [ manifest.header.uuid, ...(manifest.modules?.map((module) => module.uuid) || []), ...(manifest.dependencies?.map((dep) => dep.uuid).filter(ObjectUtilities_1.notEmpty) || []), ]; const [validIds, invalidIds] = (0, ArrayUtilities_1.filterAndSeparate)(allIds, (id) => Utilities_1.default.isValidUuid(id)); const invalidIdResults = invalidIds .map((invalid) => ({ id: this.id, item: manifestItem, data: invalid })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidId, data)); const dupIdResults = (0, ArrayUtilities_1.findDuplicates)(validIds) .map((dupId) => ({ id: this.id, item: manifestItem, data: dupId })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.DuplicateId, data)); return [...invalidIdResults, ...dupIdResults]; } validateSettings(pack, manifest, manifestItem) { const settings = manifest.settings; if (!settings) { return []; } const dupNameResults = (0, ArrayUtilities_1.findDuplicates)(settings.map((setting) => setting.name)) .map((dup) => ({ id: this.id, data: dup, item: manifestItem })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.DuplicateSettingsName, data)); const noNamespaces = settings .filter((setting) => setting.name && ValidationData.NamespaceFormat.test(setting.name)) .map((setting) => ({ id: this.id, item: manifestItem, data: setting.name })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.SettingsNamespaceRequired, data)); const toggles = settings.filter((setting) => setting.type === "toggle"); const sliders = settings.filter((setting) => setting.type === "slider"); const dropdowns = settings.filter((setting) => setting.type === "dropdown"); const knownTypes = new Set(["label", "toggle", "slider", "dropdown"]); const invalidTypes = settings .filter((settings) => !knownTypes.has(settings.type)) .map((settings) => ({ id: this.id, data: settings.type, item: manifestItem })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidSettingType, data)); //note: type and text are always required, so we use the schema to validate that //for other properties that are only conditionally required, we'll confirm post parsing const missingPropResults = [ ...toggles .map((toggle) => [toggle, (0, ObjectUtilities_1.findMissingProperty)(toggle, ["name", "default"])]) .filter(([, missingProperty]) => !!missingProperty) .map(([, missingProperty]) => ({ id: this.id, item: manifestItem, data: missingProperty })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.MissingSettingsProperty, data)), ...sliders .map((slider) => [slider, (0, ObjectUtilities_1.findMissingProperty)(slider, ["name", "min", "max", "step", "default"])]) .filter(([, missingProperty]) => !!missingProperty) .map(([, missingProperty]) => ({ id: this.id, item: manifestItem, data: missingProperty })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.MissingSettingsProperty, data)), ...dropdowns .map((dropdown) => [dropdown, (0, ObjectUtilities_1.findMissingProperty)(dropdown, ["name", "default", "options"])]) .filter(([, missingProperty]) => !!missingProperty) .map(([, missingProperty]) => ({ id: this.id, item: manifestItem, data: missingProperty })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.MissingSettingsProperty, data)), ]; const invalidMins = sliders // undefined values are reported by the missing property result, ignore for now .filter((slider) => !!slider.min && !!slider.max) // we've already filtered undefined values, '!' is safe .filter((slider) => slider.min > slider.max) .map((slider) => ({ id: this.id, item: manifestItem, data: slider.min })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidSettingsMin, data)); const invalidDefaults = sliders .filter((slider) => !!slider.default && !!slider.min && !!slider.max) // we've already filtered undefined values, '!' is safe .filter((slider) => typeof slider.default !== "number" || slider.default > slider.max || slider.default < slider.min) .map((slider) => ({ id: this.id, item: manifestItem, data: slider.default })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidSliderDefault, data)); const invalidSteps = sliders .filter((slider) => !!slider.step && !!slider.min && !!slider.max) // we've already filtered undefined values, '!' is safe .filter((slider) => slider.step <= 0 || slider.step > slider.max - slider.min) .map((slider) => ({ id: this.id, item: manifestItem, data: slider.step })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidSettingsStep, data)); const notEnoughOptions = dropdowns .filter((dropdown) => !dropdown.options || dropdown.options.length < ValidationData.MinDropDownOptions) .map(() => ({ id: this.id, item: manifestItem })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.NotEnoughSettingsOptions, data)); const duplicateOptions = dropdowns .flatMap((dropdown) => (dropdown.options ? (0, ArrayUtilities_1.findDuplicates)(dropdown.options.map((opt) => opt.name)) : [])) .map((duplicate) => ({ id: this.id, item: manifestItem, data: duplicate })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.DuplicateOptions, data)); const badDefaults = dropdowns .filter((dropdown) => dropdown.options && typeof dropdown.default === "string" && dropdown.options.map((opt) => opt.name).includes(dropdown.default)) .map((dropdown) => ({ id: this.id, item: manifestItem, data: dropdown.default })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidSliderDefault, data)); return [ ...noNamespaces, ...dupNameResults, ...invalidTypes, ...missingPropResults, ...invalidMins, ...invalidDefaults, ...invalidSteps, ...notEnoughOptions, ...duplicateOptions, ...badDefaults, ]; } validateCapabilitiesForMinEngineVersionPlusPbr(pack, manifest, manifestItem) { let results = []; if (manifestItem.itemType !== IProjectItemData_1.ProjectItemType.resourcePackManifestJson) { return results; } // Check for VV files scoped to THIS pack only, not the entire project. // A project may have multiple resource packs where only some contain PBR content. const owningPack = manifestItem.project.packs.find((p) => p.projectItem.projectPath && manifestItem.projectPath?.startsWith(p.projectItem.projectPath)); const hasVVFilesInPack = owningPack?.hasVibrantVisualsContent() ?? false; const capabilities = manifest.capabilities; let hasPbr = false; if (capabilities) { for (const cap of capabilities) { if (cap.toLowerCase() === "pbr") { hasPbr = true; if (manifest.header?.minEngineVersion) { const minVersion = SemanticVersion_1.default.parse(manifest.header.minEngineVersion); if (minVersion && minVersion.compareTo(ValidationData.TargetMevForVV) < 0) { results.push((0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.MinEngineVersionForVV, { id: this.id, item: manifestItem, data: minVersion.asString(), })); } } } } } if (hasVVFilesInPack && !hasPbr && !manifestItem.project.isVanillaEditSession) { results.push((0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.HasPBRFilesButNoManifestCapability, { id: this.id, item: manifestItem, })); } return results; } validateCapabilities(pack, manifest, manifestItem) { const capabilities = manifest.capabilities; if (!capabilities) { return []; } return capabilities .filter((capability) => !ValidationData.AllowedCapabilities.has(capability.toLowerCase())) .map((capability) => ({ id: this.id, item: manifestItem, data: capability })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidCapability, data)); } validateSubpacks(pack, manifest, manifestItem) { const subpacks = manifest.subpacks; if (!subpacks) { return []; } const dupFolderResults = (0, ArrayUtilities_1.findDuplicates)(subpacks.map((subp) => subp.folderName)) .map((folder) => ({ id: this.id, item: manifestItem, data: folder })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.DuplicateSubpackFolder, data)); const dupNameResults = (0, ArrayUtilities_1.findDuplicates)(subpacks.map((subp) => subp.name)) .map((name) => ({ id: this.id, item: manifestItem, data: name })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.DuplicateSubpackName, data)); /* Disable name checks and .memory tier checks for const validNames = new Set(Object.values(SubpackTypes).map((type) => type.name)); const invalidNameResults = subpacks .filter((subp) => !validNames.has(subp.name)) .map((subp) => ({ id: this.id, item: manifestItem, data: subp.name })) .map((data) => resultFromTest(Tests.InvalidSubpackName, data)); const invalidMemoryTierResults = subpacks .map((subp) => [subp, SubpackTypes[subp.name]?.minTier] as const) .filter(([, minTier]) => !!minTier) .filter(([subp, minTier]) => subp.memoryTier < minTier) .map(([subp]) => ({ id: this.id, item: manifestItem, data: subp.memoryTier })) .map((data) => resultFromTest(Tests.InvalidSubpackMemoryTier, data)); */ return [...dupFolderResults, ...dupNameResults]; //, ...invalidNameResults, ...invalidMemoryTierResults]; } validateDependencies(pack, manifest, manifestItem) { const dependencies = manifest.dependencies; if (!dependencies) { return []; } const noIdResults = dependencies .filter((dependency) => !dependency.uuid && !dependency.moduleName) .map(() => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.NoDependencyIdentifier, { id: this.id, item: manifestItem })); const multipleIdResults = dependencies .filter((dependency) => !!dependency.uuid && !!dependency.moduleName) .map(() => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.MultipleDependencyIdentifier, { id: this.id, item: manifestItem })); const allowedDependencyModulesResults = dependencies .filter((dependency) => !!dependency.moduleName && !ValidationData.AllowedDependencyModules[dependency.moduleName]) .map((dependency) => ({ id: this.id, item: manifestItem, data: dependency.moduleName })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.ModuleNameNotAllowed, data)); const dependencyVersions = dependencies.map((dep) => [dep, SemanticVersion_1.default.parse(dep.version)]); const invalidVersionResults = dependencyVersions .filter(([, version]) => !version) .map(([dep]) => ({ id: this.id, item: manifestItem, data: dep.version })) .map((data) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.UnableToParseVersion, data)); const versionsBelowMinResults = dependencyVersions // we only care about dependencies that use module name, non-allowed modules are handled by a different check .filter(([dep, version]) => !!dep.moduleName && !!ValidationData.AllowedDependencyModules[dep.moduleName] && !!version) // '!' is safe here, we've already filtered falsey moduleNames and versions .filter(([dep, version]) => version.compareTo(ValidationData.AllowedDependencyModules[dep.moduleName]) <= 0) .map(([dep]) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.BelowMinVersion, { id: this.id, item: manifestItem, data: dep.version })); return [ ...noIdResults, ...multipleIdResults, ...allowedDependencyModulesResults, ...invalidVersionResults, ...versionsBelowMinResults, ]; } validateModules(pack, manifest, manifestItem) { const modules = manifest.modules; if (!modules) { return []; } if (modules.filter((module) => module.type === ValidationData.WorldTemplateModuleName).length > 1) { return [(0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.TooManyWorldTemplates, { id: this.id, item: manifestItem })]; } return modules .filter((module) => !ValidationData.KnownModuleTypes.has(module.type)) .map((module) => (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidModuleType, { id: this.id, item: manifestItem, data: module.type })); } validateHeader(pack, manifest, manifestItem) { const header = manifest.header; const results = []; const isemptyDescriptionAllowed = pack.type === Pack_1.PackType.skin; if (!header.description && !isemptyDescriptionAllowed) { results.push((0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.MissingHeaderProperty, { id: this.id, item: manifestItem, data: "description" })); } if (pack.isWorld) { if (!!header.baseGameVersion && manifest.formatVersion < ValidationData.FormatVersion2) { const data = { id: this.id, item: manifestItem, data: header.baseGameVersion }; results.push((0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidBaseGameVersion, data)); } if (header.baseGameVersion === "*") { const data = { id: this.id, item: manifestItem, data: header.baseGameVersion }; results.push((0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.WildCardGameVersion, data)); } if (manifest.formatVersion > ValidationData.FormatVersion1 && header.lockTemplateOptions === undefined) { const data = { id: this.id, item: manifestItem, data: "lock_template_options" }; results.push((0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.HeaderPropertyRequiredV2, data)); } } if (pack.type === Pack_1.PackType.resource) { const infoItem = this.validateMinEngineVersion(header.minEngineVersion, pack, manifest.formatVersion, manifestItem); if (infoItem) { results.push(infoItem); } } if (header.packscope && ValidationData.AllowedPackScopes.has(header.packscope)) { results.push((0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidPackScope, { id: this.id, item: manifestItem, data: header.packscope })); } return results; } validateMinEngineVersion(mev, pack, formatVersion, manifestItem) { const minVersion = SemanticVersion_1.default.parse(mev); if (!minVersion && formatVersion > ValidationData.FormatVersion1) { const data = { id: this.id, item: manifestItem, data: "min_engine_version" }; return (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.HeaderPropertyRequiredV2, data); } const requiredVer = this.getMinimalVersionThatRequiresV2(pack.isEDUOffer === true); if (minVersion && minVersion.compareTo(requiredVer) >= 0 && formatVersion < ValidationData.FormatVersion2) { const data = { id: this.id, item: manifestItem, data: mev }; return (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.MinEngineVersionTooHigh, data); } return null; } getMinimalVersionThatRequiresV2(isEDUOffer) { return isEDUOffer ? Versioning_1.default.FirstMinEngineVersionForFormatV2EDU : Versioning_1.default.FirstMinEngineVersionForFormatV2; } validateFormatVersion(pack, manifest, manifestItem) { const format = manifest.formatVersion; if (!ValidationData.ValidFormatVersions.has(format)) { return [(0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidFormatVersion, { id: this.id, data: format })]; } if (!(pack.type === Pack_1.PackType.skin || pack.type === Pack_1.PackType.persona) && manifest.formatVersion < ValidationData.FormatVersion2) { return [ (0, TestDefinition_1.resultFromTest)(CheckManifestData_1.Tests.InvalidFormatVersion, { id: this.id, data: format, message: `All new content targeting published client version must conform to format version [${ValidationData.FormatVersion2}].`, }), ]; } return []; } summarize() { } } exports.default = CheckManifestGenerator;