package com.tiedup.remake.client.texture; import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.systems.RenderSystem; import com.tiedup.remake.core.TiedUpMod; import org.jetbrains.annotations.Nullable; import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; /** * Handles a dynamically loaded online texture for clothes. * Supports standard skins (64x32/64x64) and scales to proper format. * * Creates multiple texture variants: * - Base texture: Original image as uploaded * - Clothes texture: Resized for 64x64 format if needed (old 64x32 format) * - Full-skin texture: For full-skin mode (covers entire player) * * Thread-safe: Texture uploads are scheduled on render thread. */ @OnlyIn(Dist.CLIENT) public class DynamicOnlineTexture { private static final int MAX_DIMENSION = 1280; // Texture objects managed by Minecraft's texture manager private DynamicTexture baseTexture; private DynamicTexture clothesTexture; private DynamicTexture fullSkinTexture; private boolean needsResize = false; private boolean valid = false; private final String sourceUrl; /** * Create a new dynamic texture from a downloaded image. * * @param image The downloaded image * @param sourceUrl The source URL (for logging) */ public DynamicOnlineTexture(NativeImage image, String sourceUrl) { this.sourceUrl = sourceUrl; if (image != null) { this.valid = processImage(image); } } /** * Process the downloaded image and create texture variants. * * @param image The source image * @return true if processing succeeded */ private boolean processImage(NativeImage image) { int width = image.getWidth(); int height = image.getHeight(); // Validate dimensions if (width == 0 || height == 0) { TiedUpMod.LOGGER.warn( "[DynamicOnlineTexture] Invalid image dimensions: {}x{} from {}", width, height, sourceUrl ); return false; } // Check for valid skin format (must be divisible by 64 width, 32 height) if (height % 32 != 0 || width % 64 != 0) { TiedUpMod.LOGGER.warn( "[DynamicOnlineTexture] Invalid skin dimensions: {}x{} (must be 64x32 or 64x64 multiples) from {}", width, height, sourceUrl ); return false; } // Check maximum size if (width > MAX_DIMENSION || height > MAX_DIMENSION) { TiedUpMod.LOGGER.warn( "[DynamicOnlineTexture] Image too large: {}x{} (max {}) from {}", width, height, MAX_DIMENSION, sourceUrl ); return false; } try { // Create base texture (original image) NativeImage baseImage = copyImage(image); // Create clothes texture (resized if needed for old 64x32 format) NativeImage clothesImage; if (height * 2 == width) { // New format (64x64), use as-is clothesImage = copyImage(image); } else { // Old format (64x32), needs conversion to 64x64 needsResize = true; clothesImage = convertOldFormat(image); } // Create full-skin texture (square format for full coverage) NativeImage fullSkinImage = createFullSkinImage( clothesImage, width ); // Schedule texture upload on render thread RenderSystem.recordRenderCall(() -> { this.baseTexture = new DynamicTexture(baseImage); this.clothesTexture = needsResize ? new DynamicTexture(clothesImage) : this.baseTexture; this.fullSkinTexture = new DynamicTexture(fullSkinImage); }); return true; } catch (Exception e) { TiedUpMod.LOGGER.error( "[DynamicOnlineTexture] Failed to process image from {}: {}", sourceUrl, e.getMessage() ); return false; } } /** * Create a copy of a NativeImage. */ private NativeImage copyImage(NativeImage source) { NativeImage copy = new NativeImage( source.format(), source.getWidth(), source.getHeight(), false ); copy.copyFrom(source); return copy; } /** * Convert old 64x32 format to modern 64x64 format. * This copies the arm and leg textures to the correct locations. */ private NativeImage convertOldFormat(NativeImage source) { int width = source.getWidth(); int height = source.getHeight(); int scale = width / 64; // Create new 64x64 (scaled) image NativeImage result = new NativeImage( source.format(), width, width, false ); // Copy top half (head, body, arms - same as before) for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { result.setPixelRGBA(x, y, source.getPixelRGBA(x, y)); } } // Copy left arm from right arm location (mirrored) // Old format has arms in row 2, col 10 (44-47, 16-32) // New format left arm goes to row 3-4, col 8 (32-48, 48-64) copyRegion( source, result, 44 * scale, 16 * scale, 32 * scale, 48 * scale, 4 * scale, 16 * scale ); // Copy left leg from right leg location (mirrored) // Old format has leg in row 0-1 (0-16, 16-32) // New format left leg goes to row 3-4, col 4 (16-32, 48-64) copyRegion( source, result, 0 * scale, 16 * scale, 16 * scale, 48 * scale, 16 * scale, 16 * scale ); return result; } /** * Copy a region from source to destination image. */ private void copyRegion( NativeImage src, NativeImage dst, int srcX, int srcY, int dstX, int dstY, int width, int height ) { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if ( srcX + x < src.getWidth() && srcY + y < src.getHeight() && dstX + x < dst.getWidth() && dstY + y < dst.getHeight() ) { dst.setPixelRGBA( dstX + x, dstY + y, src.getPixelRGBA(srcX + x, srcY + y) ); } } } } /** * Create full-skin image (square format, opaque in key areas). */ private NativeImage createFullSkinImage(NativeImage source, int width) { NativeImage fullSkin = new NativeImage( source.format(), width, width, false ); fullSkin.copyFrom(source); // Make key areas fully opaque for full-skin mode int quarter = width / 4; setAreaOpaque(fullSkin, 0, 0, quarter, quarter); setAreaOpaque(fullSkin, 0, quarter, width, quarter * 2); setAreaOpaque(fullSkin, quarter, quarter * 3, quarter * 3, width); return fullSkin; } /** * Make an area of the image fully opaque. */ private void setAreaOpaque( NativeImage image, int x1, int y1, int x2, int y2 ) { for (int x = x1; x < x2 && x < image.getWidth(); x++) { for (int y = y1; y < y2 && y < image.getHeight(); y++) { int pixel = image.getPixelRGBA(x, y); // Set alpha to 255 (fully opaque) pixel |= 0xFF000000; image.setPixelRGBA(x, y, pixel); } } } /** * Get the base texture ID (original image). */ public int getBaseTextureId() { return baseTexture != null ? baseTexture.getId() : -1; } /** * Get the clothes texture ID (resized for modern format). */ public int getClothesTextureId() { return clothesTexture != null ? clothesTexture.getId() : getBaseTextureId(); } /** * Get the full-skin texture ID. */ public int getFullSkinTextureId() { return fullSkinTexture != null ? fullSkinTexture.getId() : -1; } /** * Bind the clothes texture for rendering. */ public void bindClothes() { int id = getClothesTextureId(); if (id != -1) { RenderSystem.setShaderTexture(0, id); } } /** * Bind the full-skin texture for rendering. */ public void bindFullSkin() { int id = getFullSkinTextureId(); if (id != -1) { RenderSystem.setShaderTexture(0, id); } } /** * Bind the base texture for rendering. */ public void bindBase() { int id = getBaseTextureId(); if (id != -1) { RenderSystem.setShaderTexture(0, id); } } /** * Check if the texture was successfully loaded. */ public boolean isValid() { return valid; } /** * Check if the image needed resizing (old 64x32 format). */ public boolean wasResized() { return needsResize; } /** * Get the source URL for debugging. */ public String getSourceUrl() { return sourceUrl; } /** * Release texture resources. */ public void close() { if (baseTexture != null) { baseTexture.close(); baseTexture = null; } if (clothesTexture != null && clothesTexture != baseTexture) { clothesTexture.close(); clothesTexture = null; } if (fullSkinTexture != null) { fullSkinTexture.close(); fullSkinTexture = null; } valid = false; } /** * Get the DynamicTexture for clothes (for ResourceLocation registration). */ @Nullable public DynamicTexture getClothesTexture() { return clothesTexture; } /** * Get the DynamicTexture for full-skin mode. */ @Nullable public DynamicTexture getFullSkinTexture() { return fullSkinTexture; } }