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