UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

733 lines (732 loc) 48 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. 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 }); exports.TexturePerformanceTierCount = exports.TextureImageInfoGeneratorTest = void 0; const ProjectInfoItem_1 = __importDefault(require("./ProjectInfoItem")); const IProjectItemData_1 = require("../app/IProjectItemData"); const IInfoItemData_1 = require("./IInfoItemData"); const Database_1 = __importDefault(require("../minecraft/Database")); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); const ProjectItemUtilities_1 = __importDefault(require("../app/ProjectItemUtilities")); const TextureDefinition_1 = __importDefault(require("../minecraft/TextureDefinition")); const ProjectUtilities_1 = __importStar(require("../app/ProjectUtilities")); const Utilities_1 = __importDefault(require("../core/Utilities")); const Pack_1 = require("../minecraft/Pack"); var TextureImageInfoGeneratorTest; (function (TextureImageInfoGeneratorTest) { TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["textureImages"] = 101] = "textureImages"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["textureImagesTier0"] = 200] = "textureImagesTier0"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["textureImagesTier1"] = 201] = "textureImagesTier1"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["textureImagesTier2"] = 202] = "textureImagesTier2"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["textureImagesTier3"] = 203] = "textureImagesTier3"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["textureImagesTier4"] = 204] = "textureImagesTier4"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["textureImagesTier5"] = 205] = "textureImagesTier5"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["pngJpgImageProcessingError"] = 401] = "pngJpgImageProcessingError"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["individualTextureMemoryExceedsBudget"] = 402] = "individualTextureMemoryExceedsBudget"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["totalTextureMemoryExceedsBudget"] = 403] = "totalTextureMemoryExceedsBudget"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["tgaImageProcessingError"] = 404] = "tgaImageProcessingError"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["individualAtlasTextureMemoryExceedsBudget"] = 405] = "individualAtlasTextureMemoryExceedsBudget"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["totalAtlasTextureMemoryExceedsBudgetWarn"] = 406] = "totalAtlasTextureMemoryExceedsBudgetWarn"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["totalAtlasTextureMemoryExceedsBudgetError"] = 407] = "totalAtlasTextureMemoryExceedsBudgetError"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["pngJpgImageProcessingNoResults"] = 408] = "pngJpgImageProcessingNoResults"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["invalidTieringConfiguration"] = 409] = "invalidTieringConfiguration"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["invalidTieringForVibrantVisuals"] = 410] = "invalidTieringForVibrantVisuals"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["totalTextureMemoryExceedsBudgetErrorBase"] = 420] = "totalTextureMemoryExceedsBudgetErrorBase"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["totalTextureMemoryExceedsBudgetWarningBase"] = 440] = "totalTextureMemoryExceedsBudgetWarningBase"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["texturePackDoesntOverrideVanillaGameTexture"] = 460] = "texturePackDoesntOverrideVanillaGameTexture"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["texturePackDoesntOverrideMostTextures"] = 461] = "texturePackDoesntOverrideMostTextures"; TextureImageInfoGeneratorTest[TextureImageInfoGeneratorTest["mashupPackDoesntOverrideMostTextures"] = 462] = "mashupPackDoesntOverrideMostTextures"; })(TextureImageInfoGeneratorTest || (exports.TextureImageInfoGeneratorTest = TextureImageInfoGeneratorTest = {})); exports.TexturePerformanceTierCount = 6; const TextureTiersBase = 200; const MashupResourcePackThresholdErrorPercent = 0.6; // for a global resource pack in content that also has a world (i.e., a mashup), return an error if it doesn't override at least 60% of vanilla game textures. const TextureOverrideThresholdPercent = 0.7; // if you override at least 70% of vanilla game textures, we assume you're trying to create a "texture pack" and should warn when you're not "covering" a vanilla texture. const TextureOverrideThresholdErrorPercent = 0.95; // if you override at least 95% of vanilla game textures, we assume you're trying to create a "texture pack" and should error if you don't have 95% coverage. const ExemptVanillaOverridePaths = [ "/entity/npc/", "/entity/banner/", "/entity/horse/", "/entity/horse/armor/", "/entity/zombie_villager/", "/entity/villager/", "/entity/zombie_villager2/professions/", "/colormap/", "/particle/", "/misc/", "/persona_thumbnails/", "/ui/", "/gui/", "/entity/shield_patterns/", "/textures/trims/", // Blocks "/blocks/glowing_obsidian", "/blocks/missing_tile", "/blocks/camera_back", "/blocks/camera_front", "/blocks/camera_side", "/blocks/camera_top", "/blocks/reactor_core_stage_0", "/blocks/reactor_core_stage_1", "/blocks/reactor_core_stage_2", "/blocks/bed_feet_end", "/blocks/bed_feet_side", "/blocks/bed_feet_top", "/blocks/bed_head_end", "/blocks/bed_head_side", "/blocks/bed_head_top", "/blocks/flower_rose_blue", "/blocks/flower_paeonia", "/blocks/llama", "/blocks/border", "/blocks/build_allow", "/blocks/build_deny", "/blocks/smithing_table_top", "/blocks/end_gateway", "/blocks/end_portal", "/blocks/water_flow", "/blocks/water_still", "/blocks/carrots_stage3", "/blocks/bell_side", "/blocks/bell_top", // Entity "/entity/agent", "/entity/alex", "/entity/camera_tripod", "/entity/char", "/entity/dummy", "/entity/screenshot_frame", "/entity/enchanting_table_book_shadow", "/entity/loyalty_rope", "/entity/egg_null", "/entity/egg_template", "/entity/dragon_exploding", "/entity/dragon_eyes", "/entity/llama", "/entity/pigzombie", "/entity/steve", "/entity/cape_invisible", // Entity subcategories "/entity/cat/blackcat", "/entity/cat/graytabby_tame", "/entity/cat/red", "/entity/cat/siamese", "/entity/fish/clownfish", "/entity/fish/fish", "/entity/villager2/professions/unskilled", "/entity/horse2/horse_markings_none", "/entity/horse2/armor/horse_armor_none", "/entity/llama/decor/decor_none", "/entity/llama/spit", "/entity/iron_golem/cracked_none", "/entity/wolf/wolf_armor_cracked_none", // Items "/items/camera", "/items/chalkboard_large", "/items/chalkboard_medium", "/items/chalkboard_small", "/items/egg_agent", "/items/egg_npc", "/items/quiver", "/items/ruby", "/items/spawn_egg_overlay", "/items/book_portfolio", "/items/boat", "/items/tipped_arrow_base", "/items/tipped_arrow_head", "/items/tipped_arrow_luck", "/items/potion_bottle_saturation", "/items/potion_overlay", "/items/egg_template", "/items/egg_mask", "/items/spawn_egg", "/items/hoglin_meat_cooked", "/items/hoglin_meat_raw", "/items/egg_fish", "/items/boat_dark_oak", "/items/light_block_0", "/items/light_block_1", "/items/light_block_10", "/items/light_block_11", "/items/light_block_12", "/items/light_block_13", "/items/light_block_14", "/items/light_block_15", "/items/light_block_2", "/items/light_block_3", "/items/light_block_4", "/items/light_block_5", "/items/light_block_6", "/items/light_block_7", "/items/light_block_8", "/items/light_block_9", // Models "models/armor/cloth_1", "models/armor/cloth_2", // Map "map/player_icon_background", "map/jungle_temple", "map/swamp_hut", "map/village_desert", "map/village_plains", "map/village_savanna", "map/village_snowy", "map/village_taiga", "map/trial_chambers", // Root level "forcefield_atlas", ]; /* export enum ProjectMetaCategory { mix = 0, worldTemplate = 1, texturePack = 2, addOn = 3, skinPack = 4, persona = 5, }*/ const TextureMemoryLimitsByTier = { 0 /*mix*/: { 0: 750, 1: 750, 2: 1000, 3: 1500, 4: 3000, 5: 4000 }, 1 /*world template*/: { 0: 750, 1: 750, 2: 1000, 3: 1500, 4: 3000, 5: 4000 }, 2 /*texture pack*/: { 0: 350, 1: 350, 2: 500, 3: 650, 4: 1250, 5: 1650 }, // added 50mb as a "discount" assuming texture packs override vanilla textures, and therefore save the 50mb of vanilla texture overhead 3 /*add-on*/: { 0: 150, 1: 150, 2: 225, 3: 300, 4: 600, 5: 800 }, 4 /*skin pack*/: { 0: 150, 1: 150, 2: 225, 3: 300, 4: 600, 5: 800 }, 5 /*persona*/: { 0: 150, 1: 150, 2: 225, 3: 300, 4: 600, 5: 800 }, }; /** * Validates texture images for size, format, and memory budget compliance. * * @see {@link ../../public/data/forms/mctoolsval/textureimage.form.json} for topic definitions */ class TextureImageInfoGenerator { id = "TEXTUREIMAGE"; title = "Texture Image Validation"; performAddOnValidations = false; _vanillaPathCache = new Map(); async _matchesVanillaPathCached(path) { const cached = this._vanillaPathCache.get(path); if (cached !== undefined) { return cached; } const result = await Database_1.default.matchesVanillaPath(path); this._vanillaPathCache.set(path, result); return result; } summarize(info, infoSet) { info.textureCount = infoSet.getSummedFeatureValue(this.id, TextureImageInfoGeneratorTest.textureImages, "texels", "instanceCount"); info.textureTexelSize = infoSet.getSummedFeatureValue(this.id, TextureImageInfoGeneratorTest.textureImages, "nonVanillaTexels", "total"); info.vanillaGameTextureCoverage = infoSet.getAverageFeatureValue(this.id, TextureImageInfoGeneratorTest.textureImages, "vanillaGameTextureCoverage", "percent"); info.vanillaGameTextureCount = infoSet.getAverageFeatureValue(this.id, TextureImageInfoGeneratorTest.textureImages, "vanillaGameTextureCoverage", "overrideCount"); let minimumSupportablePerformanceTier = 0; for (let i = 0; i < exports.TexturePerformanceTierCount; i++) { const overBudgetTierErrors = infoSet.getCount(this.id, TextureImageInfoGeneratorTest.totalTextureMemoryExceedsBudgetWarningBase + i); if (overBudgetTierErrors > 0) { minimumSupportablePerformanceTier = i + 1; } } // If the project uses subpacks, the minimum tier is at least the lowest tier // any subpack targets — Minecraft never loads a resource pack with subpacks // without one of them applied. const minSubpackTier = infoSet.getSummedFeatureValue(this.id, TextureImageInfoGeneratorTest.textureImages, "subpackTiering", "minimumEffectiveTier"); if (minSubpackTier > 0) { minimumSupportablePerformanceTier = Math.max(minimumSupportablePerformanceTier, minSubpackTier); } info.minimumSupportablePerformanceTier = minimumSupportablePerformanceTier; } static isGameTexturePath(path) { path = path.toLowerCase(); let result = path.startsWith("/resource_pack/textures/") && (path.endsWith(".png") || path.endsWith(".tga") || path.indexOf(".") < 0) && path.indexOf("_mers.") < 0 && path.indexOf("_mer.") < 0 && path.indexOf("_normal.") < 0 && path.indexOf("_mipmap.") < 0 && (path.indexOf("/textures/blocks") >= 0 || path.indexOf("/textures/entity") >= 0 || path.indexOf("/textures/items") >= 0); if (!result) { return false; } for (let exemptPath of ExemptVanillaOverridePaths) { if (path.toLowerCase().indexOf(exemptPath) >= 0) { return false; } } return true; } async generate(project, contentIndex) { const items = []; const textureImagePi = new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.featureAggregate, this.id, TextureImageInfoGeneratorTest.textureImages, "Texture Images"); items.push(textureImagePi); const vanillaPathList = await Database_1.default.getVanillaPathList(); const nonVanillaTextureMemoryByTier = []; const totalTextureMemoryByTier = []; const itemAtlasTextureMemoryByTier = []; const blockAtlasTextureMemoryByTier = []; const textureTierImagePi = []; const hasSupportForTier = []; const tierTotalMemorySizes = []; let isExplicitlyTargetingTiers = false; const vanillaTexturePathNonMers = {}; let vanillaTexturePathNonMersCount = 0; if (vanillaPathList) { for (const path of vanillaPathList) { if (TextureImageInfoGenerator.isGameTexturePath(path)) { vanillaTexturePathNonMersCount++; let extensionlessPath = StorageUtilities_1.default.stripExtension(path); vanillaTexturePathNonMers[extensionlessPath] = false; } } } for (let i = 0; i < exports.TexturePerformanceTierCount; i++) { hasSupportForTier[i] = false; nonVanillaTextureMemoryByTier.push({}); totalTextureMemoryByTier.push({}); itemAtlasTextureMemoryByTier.push({}); blockAtlasTextureMemoryByTier.push({}); textureTierImagePi[i] = new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.featureAggregate, this.id, TextureTiersBase + i, "Texture Images Tier " + i); items.push(textureTierImagePi[i]); } for (const projectVariant in project.variants) { const variant = project.variants[projectVariant]; if (!variant.isDefault && variant.effectiveUnifiedTier !== undefined) { hasSupportForTier[variant.effectiveUnifiedTier] = true; isExplicitlyTargetingTiers = true; } } // When a resource pack uses subpacks, Minecraft never loads the pack without // one of the subpacks applied. The lowest tier among all subpacks is therefore // the de-facto minimum device tier the creator supports, regardless of texture // budgets. Record it so that summarize() can incorporate it into // minimumSupportablePerformanceTier. if (isExplicitlyTargetingTiers) { let minSubpackTier = exports.TexturePerformanceTierCount; // start above max, will be reduced for (const projectVariant in project.variants) { const variant = project.variants[projectVariant]; if (!variant.isDefault && variant.effectiveUnifiedTier !== undefined) { minSubpackTier = Math.min(minSubpackTier, variant.effectiveUnifiedTier); } } if (minSubpackTier < exports.TexturePerformanceTierCount) { textureImagePi.setFeature("Subpack Tiering", "Minimum Effective Tier", minSubpackTier); } } const textureItems = project.getItemsByType(IProjectItemData_1.ProjectItemType.texture); for (const projectItem of textureItems) { if (projectItem.projectPath && !projectItem.projectPath.endsWith(".hdr") // ignore HDR files ) { if (!projectItem.isContentLoaded) { await projectItem.loadContent(); } const variantList = projectItem.getVariantList(); // we assume that the default (base) variant, with a label of "", must go // first. variantList.sort((a, b) => { return a.label.localeCompare(b.label); }); for (const variant of variantList) { const variantFile = variant.file; if (variantFile) { let nonVanillaTextureMemoryByTierToUpdate = []; let totalTextureMemoryByTierToUpdate = []; let itemAtlasTextureMemoryByTierToUpdate = []; let blockAtlasTextureMemoryByTierToUpdate = []; let textureTierItem = undefined; if (variant.projectVariant) { if (variant.projectVariant.isDefault) { // if this is the default, set texture sizes by path for all tiers nonVanillaTextureMemoryByTierToUpdate = nonVanillaTextureMemoryByTier; totalTextureMemoryByTierToUpdate = totalTextureMemoryByTier; itemAtlasTextureMemoryByTierToUpdate = itemAtlasTextureMemoryByTier; blockAtlasTextureMemoryByTierToUpdate = blockAtlasTextureMemoryByTier; } else if (variant.projectVariant.effectiveUnifiedTier !== undefined) { // otherwise, set the texture sizes by path for the effective tier textureTierItem = textureTierImagePi[variant.projectVariant.effectiveUnifiedTier]; nonVanillaTextureMemoryByTierToUpdate.push(nonVanillaTextureMemoryByTier[variant.projectVariant.effectiveUnifiedTier]); totalTextureMemoryByTierToUpdate.push(totalTextureMemoryByTier[variant.projectVariant.effectiveUnifiedTier]); itemAtlasTextureMemoryByTierToUpdate.push(itemAtlasTextureMemoryByTier[variant.projectVariant.effectiveUnifiedTier]); blockAtlasTextureMemoryByTierToUpdate.push(blockAtlasTextureMemoryByTier[variant.projectVariant.effectiveUnifiedTier]); } } let pathInRp = await projectItem.getPackRelativePath(); let isVanilla = false; // Log.assertDefined(pathInRp, "TIIGGP"); if (pathInRp) { pathInRp = StorageUtilities_1.default.getBaseFromName(StorageUtilities_1.default.ensureNotStartsWithDelimiter(pathInRp)); const vanillaRpPath = "/resource_pack/" + pathInRp; if (vanillaTexturePathNonMers[vanillaRpPath] === false) { vanillaTexturePathNonMers[vanillaRpPath] = true; } if (await this._matchesVanillaPathCached(pathInRp)) { textureImagePi.incrementFeature("Vanilla Override Texture"); if (textureTierItem) { textureTierItem.incrementFeature("Vanilla Override Texture"); } isVanilla = true; } else { textureImagePi.incrementFeature("Custom Texture"); if (textureTierItem) { textureTierItem.incrementFeature("Custom Texture"); } } } else { textureImagePi.incrementFeature("Custom Texture"); } let imageWidth = -1; let imageHeight = -1; if (variantFile.content && variantFile.content instanceof Uint8Array) { const textureDefinition = await TextureDefinition_1.default.ensureOnFile(variantFile); if (textureDefinition) { await textureDefinition.processContent(); if (textureDefinition.errorProcessing) { if (variantFile.type === "tga") { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.internalProcessingError, this.id, TextureImageInfoGeneratorTest.tgaImageProcessingError, `Error processing TGA image`, projectItem, textureDefinition.errorMessage)); } else { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, TextureImageInfoGeneratorTest.pngJpgImageProcessingNoResults, `Could not extract metadata from PNG/JPG/TIF/HEIC image`, projectItem, textureDefinition.errorMessage)); } } else if (textureDefinition.width !== undefined && textureDefinition.height !== undefined) { imageWidth = textureDefinition.width; imageHeight = textureDefinition.height; const textureMem = imageWidth * imageHeight * 4; if (variantFile.type === "tga") { textureImagePi.spectrumIntFeature("TGA Width", imageWidth); textureImagePi.spectrumIntFeature("TGA Height", imageHeight); textureImagePi.spectrumIntFeature("TGA Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("TGA Texture Memory", textureMem); } else { textureImagePi.spectrumIntFeature("PNGJPG Width", imageWidth); textureImagePi.spectrumIntFeature("PNGJPG Height", imageHeight); textureImagePi.spectrumIntFeature("PNGJPG Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("PNGJPG Texture Memory", textureMem); } } } } if (imageWidth >= 0 && imageHeight >= 0) { const textureMem = imageWidth * imageHeight * 4; let isAtlasTexture = false; textureImagePi.spectrumIntFeature("Width", imageWidth); textureImagePi.spectrumIntFeature("Height", imageHeight); textureImagePi.spectrumIntFeature("Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Texture Memory", textureMem); if (pathInRp) { for (const totalTextureMemoryByTier of totalTextureMemoryByTierToUpdate) { totalTextureMemoryByTier[pathInRp] = textureMem; } } if (!isVanilla) { textureImagePi.spectrumIntFeature("Non-Vanilla Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Non-Vanilla Texture Memory", textureMem); if (pathInRp) { for (const nonVanillaTextureMemoryByTier of nonVanillaTextureMemoryByTierToUpdate) { nonVanillaTextureMemoryByTier[pathInRp] = textureMem; } } } let relations = 0; if (ProjectItemUtilities_1.default.isUIRelated(projectItem)) { textureImagePi.spectrumIntFeature("UI Width", imageWidth); textureImagePi.spectrumIntFeature("UI Height", imageHeight); textureImagePi.spectrumIntFeature("UI Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("UI Texture Memory", textureMem); if (!isVanilla) { textureImagePi.spectrumIntFeature("UI Non-Vanilla Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("UI Non-Vanilla Texture Memory", textureMem); } relations++; } if (ProjectItemUtilities_1.default.isBlockRelated(projectItem)) { textureImagePi.spectrumIntFeature("Block Width", imageWidth); textureImagePi.spectrumIntFeature("Block Height", imageHeight); textureImagePi.spectrumIntFeature("Block Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Block Texture Memory", textureMem); if (!isVanilla) { textureImagePi.spectrumIntFeature("Block Non-Vanilla Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Block Non-Vanilla Texture Memory", textureMem); if (pathInRp) { for (const blockAtlasMemoryByTier of blockAtlasTextureMemoryByTierToUpdate) { blockAtlasMemoryByTier[pathInRp] = textureMem; } } } relations++; isAtlasTexture = true; } if (ProjectItemUtilities_1.default.isEntityRelated(projectItem)) { textureImagePi.spectrumIntFeature("Entity Width", imageWidth); textureImagePi.spectrumIntFeature("Entity Height", imageHeight); textureImagePi.spectrumIntFeature("Entity Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Entity Texture Memory", textureMem); if (!isVanilla) { textureImagePi.spectrumIntFeature("Entity Non-Vanilla Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Entity Non-Vanilla Texture Memory", textureMem); } relations++; } if (ProjectItemUtilities_1.default.isItemRelated(projectItem)) { textureImagePi.spectrumIntFeature("Item Width", imageWidth); textureImagePi.spectrumIntFeature("Item Height", imageHeight); textureImagePi.spectrumIntFeature("Item Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Item Texture Memory", textureMem); if (!isVanilla) { textureImagePi.spectrumIntFeature("Item Non-Vanilla Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Item Non-Vanilla Texture Memory", textureMem); if (pathInRp) { for (const itemAtlasMemoryByTier of itemAtlasTextureMemoryByTierToUpdate) { itemAtlasMemoryByTier[pathInRp] = textureMem; } } } relations++; isAtlasTexture = true; } if (ProjectItemUtilities_1.default.isParticleRelated(projectItem)) { textureImagePi.spectrumIntFeature("Particle Width", imageWidth); textureImagePi.spectrumIntFeature("Particle Height", imageHeight); textureImagePi.spectrumIntFeature("Particle Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Particle Texture Memory", textureMem); if (!isVanilla) { textureImagePi.spectrumIntFeature("Particle Non-Vanilla Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Particle Non-Vanilla Texture Memory", textureMem); } relations++; } if (relations === 0 && projectItem.projectPath) { const texturePath = TextureDefinition_1.default.getTexturePath(projectItem.projectPath); if (texturePath) { const isVanillaEx = await this._matchesVanillaPathCached(texturePath); if (isVanillaEx) { textureImagePi.spectrumIntFeature("Vanilla Override Width", imageWidth); textureImagePi.spectrumIntFeature("Vanilla Override Height", imageHeight); textureImagePi.spectrumIntFeature("Vanilla Override Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Vanilla Override Texture Memory", textureMem); } else { textureImagePi.spectrumIntFeature("Non-tied Width", imageWidth); textureImagePi.spectrumIntFeature("Non-tied Height", imageHeight); textureImagePi.spectrumIntFeature("Non-tied Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Non-tied Texture Memory", textureMem); if (!isVanilla) { textureImagePi.spectrumIntFeature("Non-tied Non-Vanilla Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Non-tied Non-Vanilla Texture Memory", textureMem); } } } } else if (relations > 1) { textureImagePi.spectrumIntFeature("Multi-tied Width", imageWidth); textureImagePi.spectrumIntFeature("Multi-tied Height", imageHeight); textureImagePi.spectrumIntFeature("Multi-tied Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Multi-tied Texture Memory", textureMem); if (!isVanilla) { textureImagePi.spectrumIntFeature("Multi-tied Non-Vanilla Texels", imageWidth * imageHeight); textureImagePi.spectrumIntFeature("Multi-tied Non-Vanilla Texture Memory", textureMem); } } let individualMemoryBudget = 2048 * 2048 * 4; if (isAtlasTexture) { individualMemoryBudget = 256 * 256 * 4; if (textureMem > individualMemoryBudget) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, TextureImageInfoGeneratorTest.individualAtlasTextureMemoryExceedsBudget, `Individual atlassed texture memory exceeds budget of ${individualMemoryBudget} bytes. Memory used`, projectItem, textureMem)); } } else { if (textureMem > individualMemoryBudget) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, TextureImageInfoGeneratorTest.individualTextureMemoryExceedsBudget, `Individual loose texture memory exceeds budget of ${individualMemoryBudget} bytes. Memory used`, projectItem, textureMem)); } } } } } } } let maxTotalTextureMemory = 0; let maxNonVanillaTextureMemory = 0; let maxBlockAtlasTextureMemory = 0; let maxItemAtlasTextureMemory = 0; const category = await ProjectUtilities_1.default.getMetaCategory(project); for (let curTier = 0; curTier < exports.TexturePerformanceTierCount; curTier++) { let tierNonVanillaTextureMemory = 0; let tierTotalTextureMemory = 0; let tierTotalBlockAtlasTextureMemory = 0; let tierTotalItemAtlasTextureMemory = 0; for (const texturePath in nonVanillaTextureMemoryByTier[curTier]) { tierNonVanillaTextureMemory += nonVanillaTextureMemoryByTier[curTier][texturePath]; } for (const texturePath in totalTextureMemoryByTier[curTier]) { tierTotalTextureMemory += totalTextureMemoryByTier[curTier][texturePath]; } for (const texturePath in blockAtlasTextureMemoryByTier[curTier]) { tierTotalBlockAtlasTextureMemory += blockAtlasTextureMemoryByTier[curTier][texturePath]; } for (const texturePath in itemAtlasTextureMemoryByTier[curTier]) { tierTotalItemAtlasTextureMemory += itemAtlasTextureMemoryByTier[curTier][texturePath]; } if (curTier > 0 && !hasSupportForTier[curTier]) { textureTierImagePi[curTier].spectrumIntFeature("Non-Vanilla Texture Memory Tier", maxNonVanillaTextureMemory); textureTierImagePi[curTier].spectrumIntFeature("Total Texture Memory Tier", maxTotalTextureMemory); } else { maxItemAtlasTextureMemory = Math.max(maxItemAtlasTextureMemory, tierTotalItemAtlasTextureMemory); maxBlockAtlasTextureMemory = Math.max(maxBlockAtlasTextureMemory, tierTotalBlockAtlasTextureMemory); maxNonVanillaTextureMemory = Math.max(maxNonVanillaTextureMemory, tierNonVanillaTextureMemory); maxTotalTextureMemory = Math.max(maxTotalTextureMemory, tierTotalTextureMemory); textureTierImagePi[curTier].spectrumIntFeature("Non-Vanilla Texture Memory Tier", tierNonVanillaTextureMemory); textureTierImagePi[curTier].spectrumIntFeature("Total Texture Memory Tier", tierTotalTextureMemory); } // Expand atlases to power of 2 tierTotalBlockAtlasTextureMemory = Math.pow(2, Math.ceil(Math.log2(tierTotalBlockAtlasTextureMemory))); tierTotalItemAtlasTextureMemory = Math.pow(2, Math.ceil(Math.log2(tierTotalItemAtlasTextureMemory))); textureTierImagePi[curTier].spectrumIntFeature("Block Atlas Texture Memory Tier", tierTotalBlockAtlasTextureMemory); textureTierImagePi[curTier].spectrumIntFeature("Item Atlas Texture Memory Tier", tierTotalItemAtlasTextureMemory); // 4K limit for warn, 8K as hard limit const totalAtlasTextureMemoryBudgetWarn = 4096 * 4096 * 4; const totalAtlasTextureMemoryBudgetError = totalAtlasTextureMemoryBudgetWarn * 4; if (tierTotalBlockAtlasTextureMemory > totalAtlasTextureMemoryBudgetError) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.totalAtlasTextureMemoryExceedsBudgetError, `Texture memory of block atlas exceeds hard limit of ${Utilities_1.default.addCommasToNumber(totalAtlasTextureMemoryBudgetError)} bytes. Total memory used`, undefined, tierTotalBlockAtlasTextureMemory)); } else if (tierTotalBlockAtlasTextureMemory > totalAtlasTextureMemoryBudgetWarn) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, TextureImageInfoGeneratorTest.totalAtlasTextureMemoryExceedsBudgetWarn, `Texture memory of block atlas exceeds budget of ${Utilities_1.default.addCommasToNumber(totalAtlasTextureMemoryBudgetWarn)} bytes. Total memory used`, undefined, tierTotalBlockAtlasTextureMemory)); } if (tierTotalItemAtlasTextureMemory > totalAtlasTextureMemoryBudgetError) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.totalAtlasTextureMemoryExceedsBudgetError, `Texture memory of item atlas exceeds hard limit of ${Utilities_1.default.addCommasToNumber(totalAtlasTextureMemoryBudgetError)} bytes. Total memory used`, undefined, tierTotalItemAtlasTextureMemory)); } else if (tierTotalItemAtlasTextureMemory > totalAtlasTextureMemoryBudgetWarn) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, TextureImageInfoGeneratorTest.totalAtlasTextureMemoryExceedsBudgetWarn, `Texture memory of item atlas exceeds budget of ${Utilities_1.default.addCommasToNumber(totalAtlasTextureMemoryBudgetWarn)} bytes. Total memory used`, undefined, tierTotalItemAtlasTextureMemory)); } tierNonVanillaTextureMemory += tierTotalBlockAtlasTextureMemory + tierTotalItemAtlasTextureMemory; tierTotalTextureMemory += tierTotalBlockAtlasTextureMemory + tierTotalItemAtlasTextureMemory; let curMemoryUsedInTier = tierTotalTextureMemory; if (curMemoryUsedInTier <= 0) { curMemoryUsedInTier = maxTotalTextureMemory; } if (curTier > 0 && !hasSupportForTier[curTier]) { curMemoryUsedInTier = maxTotalTextureMemory; } tierTotalMemorySizes[curTier] = curMemoryUsedInTier; // check for non linear tier scaling (e.g., tier 2 occupies more memory than tier 3) if (hasSupportForTier[curTier]) { for (let prevTier = 0; prevTier < curTier; prevTier++) { if (tierTotalMemorySizes[prevTier] && tierTotalMemorySizes[prevTier] > curMemoryUsedInTier) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.invalidTieringConfiguration, `Lower tier ${prevTier} has a higher memory requirement (${Utilities_1.default.addCommasToNumber(tierTotalMemorySizes[prevTier])}) than tier ${curTier}`, undefined, curMemoryUsedInTier)); } } } const tierTotalTextureMemoryBudget = 1024 * 1024 * TextureMemoryLimitsByTier[category][curTier]; if (curMemoryUsedInTier > tierTotalTextureMemoryBudget) { let warningMessage = `Total texture memory exceeds budget of ${Utilities_1.default.addCommasToNumber(tierTotalTextureMemoryBudget)} bytes for items of type ${ProjectUtilities_1.default.getMetaCategoryDescription(category)} at tier ${curTier}.`; warningMessage += " Total texture memory used"; items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, TextureImageInfoGeneratorTest.totalTextureMemoryExceedsBudgetWarningBase + curTier, warningMessage, undefined, curMemoryUsedInTier)); for (const pvLabel in project.variants) { const pv = project.variants[pvLabel]; if (pv && pv.effectiveUnifiedTier === curTier) { warningMessage += " Because this project specifically targets this tier, it is an error."; items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.totalTextureMemoryExceedsBudgetErrorBase + curTier, warningMessage, undefined, curMemoryUsedInTier)); break; } } // vibrant visuals content must support tier 2 memory limits. if (curTier === 2) { const isVibrantVisualsCompatible = await ProjectUtilities_1.default.isVibrantVisualsCompatible(project); if (isVibrantVisualsCompatible) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.invalidTieringForVibrantVisuals, `Project is marked to support vibrant visuals, but does not support texture limits at tiers ${curTier}. Texture memory`, undefined, curMemoryUsedInTier)); } } } } // if add-on content is not managing its texture memory across tiers and is exceeding a base limit, throw an error if (!isExplicitlyTargetingTiers && category === ProjectUtilities_1.ProjectMetaCategory.addOn && maxTotalTextureMemory > TextureMemoryLimitsByTier[category][0] * 1024 * 1024) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.totalTextureMemoryExceedsBudget, `Total texture memory exceeds base budget of ${Utilities_1.default.addCommasToNumber(TextureMemoryLimitsByTier[category][0] * 1024 * 1024)} bytes and is not using subpacks to target specific tiers. Total memory used`, undefined, maxTotalTextureMemory)); } const totalTextureMemoryBudget = 1024 * 1024 * TextureMemoryLimitsByTier[category][5]; // if content exceeds absolute limits, throw an error if (maxTotalTextureMemory > totalTextureMemoryBudget) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.totalTextureMemoryExceedsBudget, `Total texture memory exceeds absolute budget of ${Utilities_1.default.addCommasToNumber(totalTextureMemoryBudget)} bytes. Total memory used`, undefined, maxTotalTextureMemory)); } if (vanillaTexturePathNonMersCount > 0) { let vanillaOverrideTextureCount = 0; for (const path in vanillaTexturePathNonMers) { if (vanillaTexturePathNonMers[path] === true) { vanillaOverrideTextureCount++; } } textureImagePi.setFeature("Vanilla Game Texture Coverage", "Override Count", vanillaOverrideTextureCount); textureImagePi.setFeature("Vanilla Game Texture Coverage", "Vanilla Texture Count", vanillaTexturePathNonMersCount); textureImagePi.setFeature("Vanilla Game Texture Coverage", "Percent", vanillaOverrideTextureCount / vanillaTexturePathNonMersCount); const actualOverridePercent = vanillaOverrideTextureCount / vanillaTexturePathNonMersCount; if (actualOverridePercent < MashupResourcePackThresholdErrorPercent) { let hasGlobalResourcePack = false; for (const pack of project.packs) { if (pack.packType === Pack_1.PackType.resource && !pack.isInWorld) { hasGlobalResourcePack = true; } } const worldCount = project.getItemsByType(IProjectItemData_1.ProjectItemType.worldFolder).length; if (hasGlobalResourcePack && worldCount > 0) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.mashupPackDoesntOverrideMostTextures, `Content seems like a mashup pack, but the resource pack does not override >60% of vanilla textures.`, undefined, actualOverridePercent)); } } if (actualOverridePercent >= TextureOverrideThresholdPercent) { if (actualOverridePercent < TextureOverrideThresholdErrorPercent) { const nonRpItemCount = project.getItemsByType(IProjectItemData_1.ProjectItemType.behaviorPackManifestJson).length + project.getItemsByType(IProjectItemData_1.ProjectItemType.worldFolder).length; if (nonRpItemCount === 0) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, TextureImageInfoGeneratorTest.texturePackDoesntOverrideMostTextures, `Content seems like a texture pack (overrides >70% of textures), but does not override the vast majority of textures. This pack should override at least 95% of vanilla textures.`, undefined, actualOverridePercent)); } } for (const path in vanillaTexturePathNonMers) { if (vanillaTexturePathNonMers[path] === false) { items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, TextureImageInfoGeneratorTest.texturePackDoesntOverrideVanillaGameTexture, `Content seems like a texture pack, but does not override vanilla texture`, undefined, path)); } } } } return items; } } exports.default = TextureImageInfoGenerator;