Strip all Phase references, TODO/FUTURE roadmap notes, and internal planning comments from the codebase. Run Prettier for consistent formatting across all Java files.
385 lines
11 KiB
Java
385 lines
11 KiB
Java
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 net.minecraft.client.renderer.texture.DynamicTexture;
|
|
import net.minecraftforge.api.distmarker.Dist;
|
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|