package net.minecraft.client.renderer.texture; import com.mojang.logging.LogUtils; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.stream.Collectors; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; import net.minecraft.client.Minecraft; import net.minecraft.client.Options; import net.minecraft.client.TextureFilteringMethod; import net.minecraft.client.renderer.texture.atlas.SpriteResourceLoader; import net.minecraft.client.renderer.texture.atlas.SpriteSource; import net.minecraft.client.renderer.texture.atlas.SpriteSourceList; import net.minecraft.resources.Identifier; import net.minecraft.server.packs.metadata.MetadataSectionType; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.Mth; import net.minecraft.util.Util; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.Zone; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class SpriteLoader { private static final Logger LOGGER = LogUtils.getLogger(); private final Identifier location; private final int maxSupportedTextureSize; public SpriteLoader(final Identifier location, final int maxSupportedTextureSize) { this.location = location; this.maxSupportedTextureSize = maxSupportedTextureSize; } public static SpriteLoader create(final TextureAtlas atlas) { return new SpriteLoader(atlas.location(), atlas.maxSupportedTextureSize()); } private SpriteLoader.Preparations stitch(final List sprites, final int maxMipmapLevels, final Executor executor) { Zone ignored = Profiler.get().zone(() -> "stitch " + this.location); SpriteLoader.Preparations var19; try { int maxTextureSize = this.maxSupportedTextureSize; int minTexelSize = Integer.MAX_VALUE; int lowestOneBit = 1 << maxMipmapLevels; for (SpriteContents spriteInfo : sprites) { minTexelSize = Math.min(minTexelSize, Math.min(spriteInfo.width(), spriteInfo.height())); int lowestTextureBit = Math.min(Integer.lowestOneBit(spriteInfo.width()), Integer.lowestOneBit(spriteInfo.height())); if (lowestTextureBit < lowestOneBit) { LOGGER.warn( "Texture {} with size {}x{} limits mip level from {} to {}", spriteInfo.name(), spriteInfo.width(), spriteInfo.height(), Mth.log2(lowestOneBit), Mth.log2(lowestTextureBit) ); lowestOneBit = lowestTextureBit; } } int minSize = Math.min(minTexelSize, lowestOneBit); int minPowerOfTwo = Mth.log2(minSize); int mipLevel; if (minPowerOfTwo < maxMipmapLevels) { LOGGER.warn("{}: dropping miplevel from {} to {}, because of minimum power of two: {}", this.location, maxMipmapLevels, minPowerOfTwo, minSize); mipLevel = minPowerOfTwo; } else { mipLevel = maxMipmapLevels; } Options options = Minecraft.getInstance().options; int anisotropyBit = options.textureFiltering().get() != TextureFilteringMethod.ANISOTROPIC ? 0 : options.maxAnisotropyBit().get(); Stitcher stitcher = new Stitcher<>(maxTextureSize, maxTextureSize, mipLevel, anisotropyBit); for (SpriteContents spriteInfox : sprites) { stitcher.registerSprite(spriteInfox); } try { stitcher.stitch(); } catch (StitcherException var21) { CrashReport report = CrashReport.forThrowable(var21, "Stitching"); CrashReportCategory category = report.addCategory("Stitcher"); category.setDetail( "Sprites", var21.getAllSprites().stream().map(s -> String.format(Locale.ROOT, "%s[%dx%d]", s.name(), s.width(), s.height())).collect(Collectors.joining(",")) ); category.setDetail("Max Texture Size", maxTextureSize); throw new ReportedException(report); } int width = stitcher.getWidth(); int height = stitcher.getHeight(); Map result = this.getStitchedSprites(stitcher, width, height); TextureAtlasSprite missingSprite = (TextureAtlasSprite)result.get(MissingTextureAtlasSprite.getLocation()); CompletableFuture readyForUpload = CompletableFuture.runAsync(() -> result.values().forEach(s -> s.contents().increaseMipLevel(mipLevel)), executor); var19 = new SpriteLoader.Preparations(width, height, mipLevel, missingSprite, result, readyForUpload); } catch (Throwable var22) { if (ignored != null) { try { ignored.close(); } catch (Throwable var20) { var22.addSuppressed(var20); } } throw var22; } if (ignored != null) { ignored.close(); } return var19; } private static CompletableFuture> runSpriteSuppliers( final SpriteResourceLoader resourceLoader, final List sprites, final Executor executor ) { List> spriteFutures = sprites.stream() .map(supplier -> CompletableFuture.supplyAsync(() -> supplier.get(resourceLoader), executor)) .toList(); return Util.sequence(spriteFutures).thenApply(l -> l.stream().filter(Objects::nonNull).toList()); } public CompletableFuture loadAndStitch( final ResourceManager manager, final Identifier atlasInfoLocation, final int maxMipmapLevels, final Executor taskExecutor, final Set> additionalMetadata ) { SpriteResourceLoader spriteResourceLoader = SpriteResourceLoader.create(additionalMetadata); return CompletableFuture.supplyAsync(() -> SpriteSourceList.load(manager, atlasInfoLocation).list(manager), taskExecutor) .thenCompose(sprites -> runSpriteSuppliers(spriteResourceLoader, sprites, taskExecutor)) .thenApply(resources -> this.stitch(resources, maxMipmapLevels, taskExecutor)); } private Map getStitchedSprites(final Stitcher stitcher, final int atlasWidth, final int atlasHeight) { Map result = new HashMap(); stitcher.gatherSprites( (contents, x, y, padding) -> result.put(contents.name(), new TextureAtlasSprite(this.location, contents, atlasWidth, atlasHeight, x, y, padding)) ); return result; } public record Preparations( int width, int height, int mipLevel, TextureAtlasSprite missing, Map regions, CompletableFuture readyForUpload ) { @Nullable public TextureAtlasSprite getSprite(final Identifier id) { return (TextureAtlasSprite)this.regions.get(id); } } }