@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
389 lines (385 loc) • 23.4 kB
JavaScript
;
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;