new approach: make each ctm variation sprite a real resource

This commit is contained in:
danyadev committed 2026-09-25 02:02:04 +03:00
1 parent 68d65d23ed
commit 36f517cb46
4 files changed
+259 -212

No files matched your search

+3 -3
View File
@@ -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.<modid>.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
@@ -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.
* <p>
* 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<IResource> getAllResources(ResourceLocation location) throws IOException;
@Inject(method = "getResource", at = @At("HEAD"), cancellable = true)
private void chisel$getSubmapResource(ResourceLocation location, CallbackInfoReturnable<IResource> 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<List<IResource>> cir)
throws IOException {
TextureSubmapResource.SubmapLocation submap = TextureSubmapResource.parse(location);
if (submap == null) return;
List<IResource> sources = getAllResources(submap.source());
List<IResource> resources = new ArrayList<>(sources.size());
for (IResource source : sources) {
resources.add(TextureSubmapResource.create(location, source, submap));
}
cir.setReturnValue(resources);
}
}
@@ -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<ResourceLocation, SourceTexture> 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;
}
}
}
@@ -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<AnimationFrame> 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;
}
}