From 36f517cb46d58a84d0e1e8611eec61dfda9be4ef Mon Sep 17 00:00:00 2001 From: danyadev Date: Fri, 25 Sep 2026 02:02:04 +0300 Subject: [PATCH] new approach: make each ctm variation sprite a real resource --- gradle.properties | 6 +- .../mixins/MixinFallbackResourceManager.java | 62 +++++ .../team/chisel/ctmlib/TextureSubmap.java | 213 +----------------- .../chisel/ctmlib/TextureSubmapResource.java | 190 ++++++++++++++++ 4 files changed, 259 insertions(+), 212 deletions(-) create mode 100644 src/main/java/com/cricketcraft/chisel/mixins/MixinFallbackResourceManager.java create mode 100644 src/main/java/team/chisel/ctmlib/TextureSubmapResource.java diff --git a/gradle.properties b/gradle.properties index b3adbf1d..a5b61fa5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -107,7 +107,7 @@ apiPackage = api accessTransformersFile = chisel_compile_at.cfg # Provides setup for Mixins if enabled. If you don't know what mixins are: Keep it disabled! -usesMixins = false +usesMixins = true # Set to a non-empty string to configure mixins in a separate source set under src/VALUE, instead of src/main. # This can speed up compile times thanks to not running the mixin annotation processor on all input sources. @@ -115,7 +115,7 @@ usesMixins = false separateMixinSourceSet = # Adds some debug arguments like verbose output and class export. -usesMixinDebug = false +usesMixinDebug = true # Specify the location of your implementation of IMixinConfigPlugin. Leave it empty otherwise. mixinPlugin = @@ -123,7 +123,7 @@ mixinPlugin = # Specify the package that contains all of your Mixins. The package must exist or # the build will fail. If you have a package property defined in your mixins..json, # it must match with this or the build will fail. -mixinsPackage = +mixinsPackage = mixins # Specify the core mod entry class if you use a core mod. This class must implement IFMLLoadingPlugin! # This parameter is for legacy compatibility only diff --git a/src/main/java/com/cricketcraft/chisel/mixins/MixinFallbackResourceManager.java b/src/main/java/com/cricketcraft/chisel/mixins/MixinFallbackResourceManager.java new file mode 100644 index 00000000..839a30a7 --- /dev/null +++ b/src/main/java/com/cricketcraft/chisel/mixins/MixinFallbackResourceManager.java @@ -0,0 +1,62 @@ +package com.cricketcraft.chisel.mixins; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.resources.FallbackResourceManager; +import net.minecraft.client.resources.IResource; +import net.minecraft.util.ResourceLocation; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import team.chisel.ctmlib.TextureSubmapResource; + +/** + * Provides generated resources for independently stitched CTM submap sprites. + *

+ * For example, a request for chisel:textures/blocks/__chisel_submap/4x4/2_1/foo.png is resolved from + * chisel:textures/blocks/foo.png and cropped to the requested cell. This lets Minecraft and other mods treat each + * submap sprite like an ordinary texture resource. + */ +@Mixin(FallbackResourceManager.class) +public abstract class MixinFallbackResourceManager { + + @Shadow + public abstract IResource getResource(ResourceLocation location) throws IOException; + + @Shadow + public abstract List getAllResources(ResourceLocation location) throws IOException; + + @Inject(method = "getResource", at = @At("HEAD"), cancellable = true) + private void chisel$getSubmapResource(ResourceLocation location, CallbackInfoReturnable cir) + throws IOException { + + TextureSubmapResource.SubmapLocation submap = TextureSubmapResource.parse(location); + if (submap == null) return; + + IResource source = getResource(submap.source()); + cir.setReturnValue(TextureSubmapResource.create(location, source, submap)); + } + + @Inject(method = "getAllResources", at = @At("HEAD"), cancellable = true) + private void chisel$getAllSubmapResources(ResourceLocation location, CallbackInfoReturnable> cir) + throws IOException { + + TextureSubmapResource.SubmapLocation submap = TextureSubmapResource.parse(location); + if (submap == null) return; + + List sources = getAllResources(submap.source()); + List resources = new ArrayList<>(sources.size()); + + for (IResource source : sources) { + resources.add(TextureSubmapResource.create(location, source, submap)); + } + + cir.setReturnValue(resources); + } +} diff --git a/src/main/java/team/chisel/ctmlib/TextureSubmap.java b/src/main/java/team/chisel/ctmlib/TextureSubmap.java index 0bc24ab0..0c83f40f 100644 --- a/src/main/java/team/chisel/ctmlib/TextureSubmap.java +++ b/src/main/java/team/chisel/ctmlib/TextureSubmap.java @@ -1,37 +1,18 @@ package team.chisel.ctmlib; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import javax.imageio.ImageIO; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureMap; -import net.minecraft.client.resources.IResource; -import net.minecraft.client.resources.IResourceManager; -import net.minecraft.client.resources.data.AnimationMetadataSection; -import net.minecraft.client.resources.data.TextureMetadataSection; import net.minecraft.util.IIcon; -import net.minecraft.util.MathHelper; import net.minecraft.util.ResourceLocation; import net.minecraftforge.client.event.TextureStitchEvent; import net.minecraftforge.common.MinecraftForge; import org.apache.commons.lang3.ArrayUtils; -import com.github.bsideup.jabel.Desugar; - import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import lombok.experimental.Delegate; -import team.chisel.Chisel; /** * Splits a texture sheet into independently stitched sub-icons used for CTM and other texture manipulation. @@ -113,22 +94,16 @@ public class TextureSubmap implements IIcon, ISubmap { MinecraftForge.EVENT_BUS.register(new StitchHandler()); } - private static final class StitchHandler { + public static final class StitchHandler { @SubscribeEvent public void onTextureStitchPre(TextureStitchEvent.Pre event) { if (event.map.getTextureType() != 0) return; - TextureSubmapSprite.clearSourceCache(); for (TextureSubmap submap : submaps) { submap.registerSubIcons(event.map); } submaps.clear(); } - - @SubscribeEvent - public void onTextureStitchPost(TextureStitchEvent.Post event) { - TextureSubmapSprite.clearSourceCache(); - } } /** @@ -139,192 +114,12 @@ public class TextureSubmap implements IIcon, ISubmap { return; } - ResourceLocation sourceIcon = new ResourceLocation(baseIcon.getIconName()); + ResourceLocation sourceLocation = new ResourceLocation(baseIcon.getIconName()); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { - String name = getSubIconName(sourceIcon, width, height, x, y); - TextureAtlasSprite registered = textureMap.getTextureExtry(name); - if (registered == null) { - registered = new TextureSubmapSprite(name, sourceIcon, width, height, x, y); - textureMap.setTextureEntry(name, registered); - } - icons[x][y] = registered; + String name = TextureSubmapResource.getSubIconName(sourceLocation, width, height, x, y); + icons[x][y] = textureMap.registerIcon(name); } } } - - private static String getSubIconName(ResourceLocation source, int width, int height, int x, int y) { - return source.getResourceDomain() + ":__chisel_submap/" - + width - + "x" - + height - + "/" - + x - + "_" - + y - + "/" - + source.getResourcePath(); - } - - @SideOnly(Side.CLIENT) - private static final class TextureSubmapSprite extends TextureAtlasSprite { - - @Desugar - private record SourceTexture(BufferedImage[] images, AnimationMetadataSection animation) {} - - private static final Map sourceCache = new ConcurrentHashMap<>(); - - private final ResourceLocation sourceLocation; - private final int columns; - private final int rows; - private final int cellX; - private final int cellY; - - private TextureSubmapSprite(String name, ResourceLocation sourceLocation, int columns, int rows, int cellX, - int cellY) { - - super(name); - this.sourceLocation = sourceLocation; - this.columns = columns; - this.rows = rows; - this.cellX = cellX; - this.cellY = cellY; - } - - @Override - public boolean hasCustomLoader(IResourceManager manager, ResourceLocation location) { - return true; - } - - @Override - public boolean load(IResourceManager manager, ResourceLocation location) { - try { - int mipmapLevels = Minecraft.getMinecraft().gameSettings.mipmapLevels; - boolean useAnisotropicFiltering = Minecraft.getMinecraft().gameSettings.anisotropicFiltering > 1; - - SourceTexture sourceTexture = getSourceTexture(manager, sourceLocation, mipmapLevels); - BufferedImage[] images = new BufferedImage[sourceTexture.images().length]; - boolean animated = sourceTexture.animation() != null; - - for (int level = 0; level < images.length; level++) { - BufferedImage sourceImage = sourceTexture.images()[level]; - if (sourceImage == null) continue; - // A mip level can be too small to split into the submap grid - if (sourceImage.getWidth() < columns) continue; - images[level] = extractSubImage(sourceImage, animated); - } - - loadSprite(images, sourceTexture.animation(), useAnisotropicFiltering); - return false; - } catch (IOException | RuntimeException e) { - Chisel.logger.warn("Unable to create submap sprite {} from {}", getIconName(), sourceLocation, e); - return true; - } - } - - private static SourceTexture getSourceTexture(IResourceManager manager, ResourceLocation sourceLocation, - int mipmapLevels) throws IOException { - - SourceTexture cached = sourceCache.get(sourceLocation); - if (cached != null) { - return cached; - } - - ResourceLocation location = getSourceResource(sourceLocation, 0); - IResource resource = manager.getResource(location); - - BufferedImage[] images = new BufferedImage[mipmapLevels + 1]; - images[0] = readImage(resource, location); - - TextureMetadataSection textureMetadata = (TextureMetadataSection) resource.getMetadata("texture"); - if (textureMetadata != null && !textureMetadata.getListMipmaps() - .isEmpty()) { - - int width = images[0].getWidth(); - int height = images[0].getHeight(); - - if (MathHelper.roundUpToPowerOfTwo(width) != width - || MathHelper.roundUpToPowerOfTwo(height) != height) { - throw new RuntimeException("Unable to load extra miplevels, source texture is not power of two"); - } - - for (int mipLevel : textureMetadata.getListMipmaps()) { - if (mipLevel <= 0 || mipLevel >= images.length || images[mipLevel] != null) { - continue; - } - - ResourceLocation mipLocation = getSourceResource(sourceLocation, mipLevel); - try { - images[mipLevel] = readImage(manager.getResource(mipLocation), mipLocation); - } catch (IOException e) { - Chisel.logger.warn("Unable to load mip level {} from {}", mipLevel, mipLocation, e); - } - } - } - - AnimationMetadataSection animation = (AnimationMetadataSection) resource.getMetadata("animation"); - SourceTexture sourceTexture = new SourceTexture(images, animation); - - SourceTexture existing = sourceCache.putIfAbsent(sourceLocation, sourceTexture); - return existing != null ? existing : sourceTexture; - } - - private static BufferedImage readImage(IResource resource, ResourceLocation location) throws IOException { - BufferedImage image; - try (InputStream stream = resource.getInputStream()) { - image = ImageIO.read(stream); - } - if (image == null) { - throw new IOException("ImageIO could not decode " + location); - } - return image; - } - - private static void clearSourceCache() { - sourceCache.clear(); - } - - private static ResourceLocation getSourceResource(ResourceLocation sourceLocation, int mipLevel) { - String domain = sourceLocation.getResourceDomain(); - String path = sourceLocation.getResourcePath(); - if (mipLevel == 0) return new ResourceLocation(domain, "textures/blocks/" + path + ".png"); - return new ResourceLocation(domain, "textures/blocks/mipmaps/" + path + "." + mipLevel + ".png"); - } - - private BufferedImage extractSubImage(BufferedImage source, boolean animated) { - int frameSize = source.getWidth(); - int sourceHeight = source.getHeight(); - - if (frameSize % columns != 0 || frameSize % rows != 0) { - throw new IllegalArgumentException( - "Texture size " + frameSize + " is not divisible by submap " + columns + "x" + rows); - } - - if (animated) { - if (sourceHeight % frameSize != 0) { - throw new IllegalArgumentException( - "Animated texture height " + sourceHeight + " is not divisible by frame size " + frameSize); - } - } else if (sourceHeight != frameSize) { - throw new IllegalArgumentException( - "Non-animated texture is not square: " + frameSize + "x" + sourceHeight); - } - - int cellWidth = frameSize / columns; - int cellHeight = frameSize / rows; - int frameCount = animated ? sourceHeight / frameSize : 1; - - BufferedImage result = new BufferedImage(cellWidth, cellHeight * frameCount, BufferedImage.TYPE_INT_ARGB); - int[] pixels = new int[cellWidth * cellHeight]; - - for (int frame = 0; frame < frameCount; frame++) { - int sourceX = cellX * cellWidth; - int sourceY = frame * frameSize + cellY * cellHeight; - source.getRGB(sourceX, sourceY, cellWidth, cellHeight, pixels, 0, cellWidth); - result.setRGB(0, frame * cellHeight, cellWidth, cellHeight, pixels, 0, cellWidth); - } - - return result; - } - } } diff --git a/src/main/java/team/chisel/ctmlib/TextureSubmapResource.java b/src/main/java/team/chisel/ctmlib/TextureSubmapResource.java new file mode 100644 index 00000000..4d55db20 --- /dev/null +++ b/src/main/java/team/chisel/ctmlib/TextureSubmapResource.java @@ -0,0 +1,190 @@ +package team.chisel.ctmlib; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +import net.minecraft.client.resources.IResource; +import net.minecraft.client.resources.data.AnimationFrame; +import net.minecraft.client.resources.data.AnimationMetadataSection; +import net.minecraft.client.resources.data.IMetadataSection; +import net.minecraft.util.ResourceLocation; + +import com.github.bsideup.jabel.Desugar; + +public final class TextureSubmapResource implements IResource { + + @Desugar + public record SubmapLocation(ResourceLocation source, int columns, int rows, int cellX, int cellY) {} + + private static final String PREFIX = "__chisel_submap/"; + + private final IResource source; + private final byte[] imageData; + private final int columns; + private final int rows; + + private TextureSubmapResource(IResource source, byte[] imageData, int columns, int rows) { + this.source = source; + this.imageData = imageData; + this.columns = columns; + this.rows = rows; + } + + public static String getSubIconName(ResourceLocation source, int columns, int rows, int cellX, int cellY) { + return source.getResourceDomain() + ":" + + PREFIX + + columns + + "x" + + rows + + "/" + + cellX + + "_" + + cellY + + "/" + + source.getResourcePath(); + } + + public static IResource create(ResourceLocation location, IResource source, SubmapLocation submap) + throws IOException { + BufferedImage sourceImage; + try (InputStream stream = source.getInputStream()) { + sourceImage = ImageIO.read(stream); + } + if (sourceImage == null) { + throw new IOException("Unable to decode " + submap.source); + } + + BufferedImage cropped = crop(sourceImage, submap); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + if (!ImageIO.write(cropped, "png", output)) { + throw new IOException("Unable to encode submap resource " + location); + } + + return new TextureSubmapResource(source, output.toByteArray(), submap.columns, submap.rows); + } + + private static BufferedImage crop(BufferedImage source, SubmapLocation submap) throws IOException { + int frameSize = source.getWidth(); + int sourceHeight = source.getHeight(); + + if (frameSize < submap.columns || frameSize < submap.rows + || frameSize % submap.columns != 0 + || frameSize % submap.rows != 0 + || sourceHeight % frameSize != 0) { + + throw new IOException( + "Texture size " + frameSize + + "x" + + sourceHeight + + " cannot be split into submap " + + submap.columns + + "x" + + submap.rows); + } + + int cellWidth = frameSize / submap.columns; + int cellHeight = frameSize / submap.rows; + int frameCount = sourceHeight / frameSize; + + BufferedImage result = new BufferedImage(cellWidth, cellHeight * frameCount, BufferedImage.TYPE_INT_ARGB); + int[] pixels = new int[cellWidth * cellHeight]; + + for (int frame = 0; frame < frameCount; frame++) { + int sourceX = submap.cellX * cellWidth; + int sourceY = frame * frameSize + submap.cellY * cellHeight; + + source.getRGB(sourceX, sourceY, cellWidth, cellHeight, pixels, 0, cellWidth); + result.setRGB(0, frame * cellHeight, cellWidth, cellHeight, pixels, 0, cellWidth); + } + + return result; + } + + public static SubmapLocation parse(ResourceLocation location) { + String path = location.getResourcePath(); + + int prefix = path.indexOf(PREFIX); + if (prefix < 0 || prefix > 0 && path.charAt(prefix - 1) != '/') return null; + + int gridStart = prefix + PREFIX.length(); + int gridEnd = path.indexOf('/', gridStart); + if (gridEnd < 0) return null; + + int cellEnd = path.indexOf('/', gridEnd + 1); + if (cellEnd < 0) return null; + + String grid = path.substring(gridStart, gridEnd); + String cell = path.substring(gridEnd + 1, cellEnd); + + int gridSeparator = grid.indexOf('x'); + int cellSeparator = cell.indexOf('_'); + if (gridSeparator <= 0 || cellSeparator <= 0) return null; + + try { + int columns = Integer.parseInt(grid.substring(0, gridSeparator)); + int rows = Integer.parseInt(grid.substring(gridSeparator + 1)); + int cellX = Integer.parseInt(cell.substring(0, cellSeparator)); + int cellY = Integer.parseInt(cell.substring(cellSeparator + 1)); + + if (cellX < 0 || cellY < 0 || cellX >= columns || cellY >= rows) { + return null; + } + + String sourcePath = path.substring(0, prefix) + path.substring(cellEnd + 1); + ResourceLocation sourceLocation = new ResourceLocation(location.getResourceDomain(), sourcePath); + return new SubmapLocation(sourceLocation, columns, rows, cellX, cellY); + } catch (NumberFormatException ignored) { + return null; + } + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(imageData); + } + + @Override + public boolean hasMetadata() { + return source.hasMetadata(); + } + + @Override + public IMetadataSection getMetadata(String sectionName) { + IMetadataSection metadata = source.getMetadata(sectionName); + if (!(metadata instanceof AnimationMetadataSection animation)) { + return metadata; + } + + List frames = new ArrayList<>(animation.getFrameCount()); + for (int i = 0; i < animation.getFrameCount(); i++) { + if (animation.frameHasTime(i)) { + frames.add(new AnimationFrame(animation.getFrameIndex(i), animation.getFrameTimeSingle(i))); + } else { + frames.add(new AnimationFrame(animation.getFrameIndex(i))); + } + } + + int frameWidth = scaleMetadataDimension(animation.getFrameWidth(), columns); + int frameHeight = scaleMetadataDimension(animation.getFrameHeight(), rows); + return new AnimationMetadataSection(frames, frameWidth, frameHeight, animation.getFrameTime()); + } + + private static int scaleMetadataDimension(int dimension, int divisions) { + if (dimension < 0) { + return dimension; + } + if (dimension % divisions != 0) { + throw new IllegalArgumentException( + "Animation frame size " + dimension + " is not divisible by submap size " + divisions); + } + return dimension / divisions; + } +}