UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

323 lines (311 loc) 18 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const IProjectItemData_1 = require("../../app/IProjectItemData"); const IInfoItemData_1 = require("../IInfoItemData"); const Skin_1 = require("../../minecraft/skins/Skin"); const StorageUtilities_1 = __importDefault(require("../../storage/StorageUtilities")); const TestDefinition_1 = require("../tests/TestDefinition"); const ProjectItemUtilities_1 = require("../../app/ProjectItemUtilities"); const SkinPack_1 = require("../../minecraft/skins/SkinPack"); const TextureUtilities_1 = require("../../minecraft/textures/TextureUtilities"); const TextureDefinition_1 = __importDefault(require("../../minecraft/TextureDefinition")); const MaxFreeSkins = 2; const MaxSkinsInAPack = 80; const LeadingOrTrailingSpaceRegex = /^\s+|\s+$/; const Tests = { JsonNotFoundFile: { id: 101, title: "Skin Pack Json File Not Found", severity: IInfoItemData_1.InfoItemType.error, defaultMessage: "skins.json file not found.", }, InvalidJsonFile: { id: 102, title: "Invalid Json File" }, InvalidPackLocName: { id: 103, title: "Invalid Localization Name", severity: IInfoItemData_1.InfoItemType.error, defaultMessage: "skins.json localization_name and serialize_name must be the same.", }, TooManyFreeSkins: { id: 104, title: "More Free Skins Than Allowed" }, DuplicateTextures: { id: 105, title: "Duplicate Textures Found", severity: IInfoItemData_1.InfoItemType.warning }, CapeTextureNotAllowed: { id: 106, title: "Cape Texture Not Allowed" }, InvalidTextureSize: { id: 107, title: "Texture Invalid Size" }, MCCreatorPropertyNotAllowed: { id: 108, title: "Minecraft Creator Property Not Allowed" }, FailedToReadFile: { id: 109, title: "File Read Failed" }, OrphanedTexture: { id: 110, title: "Texture Not Found in skins.json" }, OrphanedLocKey: { id: 111, title: "Loc Key Not Found in Lang File" }, LocalizedKeyNotFoundInSkinsJson: { id: 112, title: "Localized Key Not Found In skins.json" }, InvalidSpacingOnLocalizedKey: { id: 113, title: "Localized Key Cannot Have Leading Or Trailing Spaces" }, InvalidSkinType: { id: 114, title: "Skin Purchase Type Not Allowed" }, InvalidSkinModelTarget: { id: 115, title: "Invalid Skin Model Target" }, InvalidNumberOfSkins: { id: 116, title: "Invalid Number Of Skins", defaultMessage: `Maximum Allowable skins is: ${MaxSkinsInAPack}`, }, OuterAreaIsBlank: { id: 117, title: "Outer Area Blank" }, ModelInvisible: { id: 118, title: "Model Invisible From Some Angles" }, ModelPartiallyInvisible: { id: 119, title: "Model Partially Invisible", severity: IInfoItemData_1.InfoItemType.warning }, CouldNotFindRelatedPack: { id: 120, title: "Could Not Find Related Skin Pack", defaultMessage: "Could not read skin pack manifest pack", }, }; /** * Validates skin pack JSON files including skins.json structure and texture references. * * @see {@link ../../../public/data/forms/mctoolsval/cspj.form.json} for topic definitions */ class CheckSkinPackJsonGenerator { id = "CSPJ"; title = "Skin Pack Validation"; canAlwaysProcess = true; async generate(project) { const skinPackManifestItems = project.getItemsByType(IProjectItemData_1.ProjectItemType.skinPackManifestJson); const allResults = []; for (const skinPackManifestItem of skinPackManifestItems) { if (!skinPackManifestItems.length) { return (0, TestDefinition_1.notApplicable)(); } const skinPack = await skinPackManifestItem.getPack(); if (!skinPack) { allResults.push((0, TestDefinition_1.resultFromTest)(Tests.CouldNotFindRelatedPack, { id: this.id, item: skinPackManifestItem, data: skinPackManifestItem.name, })); continue; } const packItems = skinPack.getPackItems(); const skinCatalogJsonFile = await (0, ProjectItemUtilities_1.getEnsuredFileOfType)(packItems, IProjectItemData_1.ProjectItemType.skinCatalogJson); if (!skinCatalogJsonFile) { return [ (0, TestDefinition_1.resultFromTestWithMessage)(Tests.JsonNotFoundFile, this.id, "Could not find skins.json file", skinPackManifestItem), ]; } // read skin pack from json and return error results if it can't be read or validated const skinCatalogJson = await StorageUtilities_1.default.getJsonObject(skinCatalogJsonFile); const [skinPackManifestObj, errors] = (0, SkinPack_1.validateSkinPackJson)(skinCatalogJson); if (errors) { return errors.map((error) => (0, TestDefinition_1.resultFromTestWithMessage)(Tests.InvalidJsonFile, this.id, error.message, skinPackManifestItem)); } if (!hasValidLocalizationNames(skinPackManifestObj.localization_name, skinPackManifestObj.serialize_name)) { return [(0, TestDefinition_1.resultFromTest)(Tests.InvalidPackLocName, { id: this.id, item: skinPackManifestItem })]; } allResults.push(...this.validateSkins(skinPackManifestObj.skins, project.isMinecraftCreator)); allResults.push(...(await this.validateTextures(packItems, skinPackManifestObj.skins, project.isMinecraftCreator))); allResults.push(...this.checkSkinLocalizations(project.loc, skinPackManifestObj)); } return allResults; } summarize() { } validateSkins(skins, isMCCreator) { const results = []; const invalidSkins = skins .filter((skin) => !(0, Skin_1.isValidSkinPurchaseType)(skin.type)) .map(() => (0, TestDefinition_1.resultFromTestWithMessage)(Tests.InvalidSkinType, this.id)); results.push(...invalidSkins); const freeSkins = skins.filter((skin) => skin.type === "free"); if (freeSkins.length > MaxFreeSkins) { const message = `${freeSkins.length} free skins found. Only ${MaxFreeSkins} allowed.`; results.push((0, TestDefinition_1.resultFromTestWithMessage)(Tests.TooManyFreeSkins, this.id, message)); } if (!isMCCreator) { const nonFPResults = this.validateNonMCRestrictionsForSkin(skins); results.push(...nonFPResults); } return results; } async validateTextures(packItems, skins, isMCCreator) { //check for duplicate capes const capeTextureNames = skins.map((skin) => skin.cape).filter((cape) => !!cape); // CSPJ105 const duplicateCapeTexturesResults = capeTextureNames .filter((name, index) => capeTextureNames.indexOf(name) !== index) .map((duplicate) => (0, TestDefinition_1.resultFromTestWithMessage)(Tests.DuplicateTextures, this.id, `Duplicate cape texture found: ${duplicate}`)); //check cape textures const capeTextureSet = new Set(capeTextureNames); const capeTextures = await (0, ProjectItemUtilities_1.findEnsuredFiles)(packItems, (item) => capeTextureSet.has(item.name)); const capeCheck = await Promise.all(capeTextures.map((cape) => this.validateCapeTextureImage(cape))); const capeResults = capeCheck.filter(TestDefinition_1.isResult); //check for duplicate skin textures const skinTextureNames = skins.map((skin) => skin.texture).filter((texture) => !!texture); const duplicateTextureResults = skinTextureNames .filter((name, index) => skinTextureNames.indexOf(name) !== index) .map((duplicate) => (0, TestDefinition_1.resultFromTestWithMessage)(Tests.DuplicateTextures, this.id, `Duplicate skin texture found: ${duplicate}`)); //check skin textures const textureCheck = await Promise.all(skins.map((skin) => this.validateTextureImage(skin, packItems, isMCCreator))); const textureResults = textureCheck.flatMap((result) => result); //check orphaned textures const knownTextures = new Set([...skinTextureNames, ...capeTextureSet]); const allTexturesInProject = packItems.filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.texture); const orphaned = allTexturesInProject.filter((texItem) => !knownTextures.has(texItem.name) && !texItem.name.startsWith("pack_icon")); // CSPJ110 const orphanResults = orphaned.map((orphan) => (0, TestDefinition_1.resultFromTestWithMessage)(Tests.OrphanedTexture, this.id, `${orphan.name} in skin pack not found in skins.json`)); // combine and return results return [ ...duplicateCapeTexturesResults, ...capeResults, ...duplicateTextureResults, ...textureResults, ...orphanResults, ]; } validateNonMCRestrictionsForSkin(skins) { const results = []; if (skins.length > MaxSkinsInAPack) { results.push((0, TestDefinition_1.resultFromTestWithMessage)(Tests.InvalidNumberOfSkins, this.id)); } if (skins.find((skin) => hasMCOnlyProperties(skin))) { const message = "animations and enable_attachables not allowed if not Minecraft Creator"; results.push((0, TestDefinition_1.resultFromTestWithMessage)(Tests.MCCreatorPropertyNotAllowed, this.id, message)); } if (skins.find((skin) => !!skin.cape)) { results.push((0, TestDefinition_1.resultFromTestWithMessage)(Tests.CapeTextureNotAllowed, this.id)); } return results; } async validateCapeTextureImage(textureFile) { if (!textureFile) { return null; } const texture = await TextureDefinition_1.default.ensureOnFile(textureFile); if (!texture) { const message = `Failed to read file: ${textureFile.name}`; return (0, TestDefinition_1.resultFromTestWithMessage)(Tests.FailedToReadFile, this.id, message); } const width = texture.width; const height = texture.height; if (!width || !height || texture.errorMessage) { const message = `Failed to read dimensions from texture: ${texture.errorMessage}`; return (0, TestDefinition_1.resultFromTestWithMessage)(Tests.FailedToReadFile, this.id, message); } if (!(0, Skin_1.isValidCapeSize)([width, height])) { const message = `Texture: ${textureFile.name} is invalid size (${width}x${height})`; return (0, TestDefinition_1.resultFromTestWithMessage)(Tests.InvalidTextureSize, this.id, message); } return null; } async validateTextureImage(skin, items, isMCCreator) { const textureFile = await (0, ProjectItemUtilities_1.getEnsuredFile)(items, (item) => item.name === skin.texture); if (!textureFile) { return []; } const textureName = textureFile.name; const texture = await TextureDefinition_1.default.ensureOnFile(textureFile); if (!texture) { const message = `Failed to read file: ${textureName}`; return [(0, TestDefinition_1.resultFromTestWithMessage)(Tests.FailedToReadFile, this.id, message)]; } if (!texture.isContentProcessed) { await texture.processContent(); } const width = texture.width; const height = texture.height; if (!width || !height || texture.errorMessage) { const message = `Failed to read dimensions from texture: ${textureName} ${texture.errorMessage}`; return [(0, TestDefinition_1.resultFromTestWithMessage)(Tests.FailedToReadFile, this.id, message)]; } if (!(0, Skin_1.isValidSkinModelTarget)([width, height])) { const message = `Texture: ${textureName} is invalid size (${width}x${height})`; return [(0, TestDefinition_1.resultFromTestWithMessage)(Tests.InvalidTextureSize, this.id, message)]; } // remaining checks do not apply to MC creator if (isMCCreator) { return []; } const skinTarget = (0, TextureUtilities_1.getSkinTargetFromName)(textureName); //CSPJ115 if (!skinTarget) { return [ (0, TestDefinition_1.resultFromTestWithMessage)(Tests.InvalidSkinModelTarget, this.id, `No intended model tag in name. Slim model skins must have the prefix or suffix of 'a', 'alex', 'slim', 'customSlim' separated by an '_' ex. <name>_customSlim. Custom model skins must have the prefix or suffix of 's', 'steve', 'custom' separated by an '_' ex. <name>_custom.`), ]; } const geoSkinSize = (0, Skin_1.getModelTargetGeometry)(skin); //CSPJ115 if (!geoSkinSize) { return [ (0, TestDefinition_1.resultFromTestWithMessage)(Tests.InvalidSkinModelTarget, this.id, `geometry property: ${skin.geometry} not allowed`), ]; } /* // requires pixel data for PNG, which we don't have const pixelSkinSize = getSkinTargetByUniquePixelLocations(texture); const isSizeConsistent = skinTarget === geoSkinSize && skinTarget === pixelSkinSize; if (!isSizeConsistent) { const message = `Model size indicators are inconsistent. name: ${skinTarget}, geometry: ${geoSkinSize}, image data: ${pixelSkinSize}`; return [resultFromTestWithMessage(Tests.InvalidSkinModelTarget, this.id, message)]; } if (!(await isOuterAreaIsBlank(texture, skinTarget))) { const message = `[${textureFile.name}]: Ensure that no pixels not visible on the model are filled in with an alpha greater than 0`; return [resultFromTestWithMessage(Tests.OuterAreaIsBlank, this.id, message)]; } const segmentVisibilities = getSegmentsVisibilities(texture, skinTarget); const visErrors = segmentVisibilities .flatMap((segment) => { const messages = []; if (!segment.visibilities.top && !segment.visibilities.bottom) { messages.push( `The ${segment.segmentName} is not visible from the top and the bottom. This could cause this part of the model to be completely invisible from certain angles.` ); } if (!segment.visibilities.front && segment.visibilities.back) { messages.push( `The ${segment.segmentName} is not visible from the front and the back. This could cause this part of the model to be completely invisible from certain angles.` ); } if (!segment.visibilities.left && !segment.visibilities.right) { messages.push( `The ${segment.segmentName} is not visible from the right and the left. This could cause this part of the model to be completely invisible from certain angles.` ); } return messages; }) .map((message) => resultFromTestWithMessage(Tests.ModelInvisible, this.id, message)); const visWarnings = segmentVisibilities .flatMap((segment) => Object.entries(segment.visibilities) .filter(([_side, isVisible]) => !isVisible) .map(([side]) => `Side: [${side}] of segment: [${segment.segmentName}] is not visible!`) ) .map((message) => resultFromTestWithMessage(Tests.ModelPartiallyInvisible, this.id, message)); return [...visErrors, ...visWarnings];*/ return []; } checkSkinLocalizations(locManager, skinPack) { const locKeysFromSkinPack = (0, SkinPack_1.getLocKeysFromSkinPack)(skinPack); const knownKeys = new Set(locManager.getAllTokenKeys()); const orphanLocKeyResults = locKeysFromSkinPack .filter((key) => !knownKeys.has(key)) .map((key) => (0, TestDefinition_1.resultFromTestWithMessage)(Tests.OrphanedLocKey, this.id, `Loc key [${key}] in skins.json not found in en_US.lang file`)); const knownSkinsKeys = new Set(locKeysFromSkinPack); const localizedOrphansResults = []; const invalidSpaceResults = []; for (const lang of locManager.getAllLanguages()) { const orphansResultsForLang = lang .getLocKeys() .filter((key) => !knownSkinsKeys.has(key) && (key.startsWith("skin.") || key.startsWith("skinpack."))) .map((orphanedKey) => `Loc Key: [${orphanedKey}] found in ${lang.language}.lang not found in skins.json`) .map((message) => (0, TestDefinition_1.resultFromTestWithMessage)(Tests.LocalizedKeyNotFoundInSkinsJson, this.id, message)); const invalidSpaceResultsForLang = lang .getLocKeys() .filter((key) => LeadingOrTrailingSpaceRegex.test(key)) .map((invalidSpacedKey) => `Loc string for key [${invalidSpacedKey}] in ${lang.language}.lang must not contain leading or trailing spaces.`) .map((message) => (0, TestDefinition_1.resultFromTestWithMessage)(Tests.InvalidSpacingOnLocalizedKey, this.id, message)); localizedOrphansResults.push(...orphansResultsForLang); invalidSpaceResults.push(...invalidSpaceResultsForLang); } return [...orphanLocKeyResults, ...localizedOrphansResults, ...invalidSpaceResults]; } } exports.default = CheckSkinPackJsonGenerator; function hasValidLocalizationNames(localizationName, serializeName) { return !!localizationName && !!serializeName && localizationName === serializeName; } function hasMCOnlyProperties(skin) { return !!skin.animations || !!skin.enable_attachables; }