Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
package com.tiedup.remake.client.texture;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import javax.imageio.ImageIO;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Manager for dynamically loaded online textures.
|
||||
*
|
||||
* Features:
|
||||
* - Async HTTP download with thread pool
|
||||
* - Host whitelist validation
|
||||
* - Memory cache with LRU-style expiration
|
||||
* - Failed URL tracking to prevent retry spam
|
||||
* - Thread-safe operations
|
||||
*
|
||||
* Singleton pattern - access via getInstance().
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class DynamicTextureManager {
|
||||
|
||||
private static DynamicTextureManager INSTANCE;
|
||||
|
||||
// Configuration
|
||||
private static final int CONNECT_TIMEOUT_MS = 10_000; // 10 seconds
|
||||
private static final int READ_TIMEOUT_MS = 30_000; // 30 seconds
|
||||
private static final int MAX_CACHE_SIZE = 50;
|
||||
private static final long FAILED_URL_RETRY_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Default whitelist of allowed hosts
|
||||
private static final Set<String> DEFAULT_WHITELIST = Set.of(
|
||||
"i.imgur.com",
|
||||
"cdn.discordapp.com",
|
||||
"media.discordapp.net",
|
||||
"raw.githubusercontent.com",
|
||||
"user-images.githubusercontent.com",
|
||||
"64.media.tumblr.com",
|
||||
"www.minecraftskins.com",
|
||||
"minecraftskins.com",
|
||||
"textures.minecraft.net",
|
||||
"crafatar.com",
|
||||
"mc-heads.net",
|
||||
"minotar.net",
|
||||
"novaskin.me",
|
||||
"t.novaskin.me",
|
||||
"s.namemc.com",
|
||||
"namemc.com"
|
||||
);
|
||||
|
||||
// Thread pool for async downloads
|
||||
private final Executor downloadExecutor = Executors.newFixedThreadPool(
|
||||
2,
|
||||
r -> {
|
||||
Thread t = new Thread(r, "TiedUp-TextureDownloader");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
);
|
||||
|
||||
// Cache: URL -> loaded texture
|
||||
private final Map<String, DynamicOnlineTexture> textureCache =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// Pending downloads: URL -> future (to avoid duplicate downloads)
|
||||
private final Map<
|
||||
String,
|
||||
CompletableFuture<DynamicOnlineTexture>
|
||||
> pendingDownloads = new ConcurrentHashMap<>();
|
||||
|
||||
// Failed URLs: URL -> timestamp of failure (to avoid retry spam)
|
||||
private final Map<String, Long> failedUrls = new ConcurrentHashMap<>();
|
||||
|
||||
// Registered textures with Minecraft's texture manager
|
||||
private final Map<String, ResourceLocation> registeredTextures =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private DynamicTextureManager() {}
|
||||
|
||||
/**
|
||||
* Get the singleton instance.
|
||||
*/
|
||||
public static DynamicTextureManager getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new DynamicTextureManager();
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a texture for a URL, loading it asynchronously if needed.
|
||||
*
|
||||
* @param url The texture URL
|
||||
* @return The texture if cached, or null if loading/failed
|
||||
*/
|
||||
@Nullable
|
||||
public DynamicOnlineTexture getTexture(String url) {
|
||||
if (url == null || url.isEmpty()) return null;
|
||||
|
||||
// Check config
|
||||
if (!ModConfig.CLIENT.enableDynamicTextures.get()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check cache
|
||||
DynamicOnlineTexture cached = textureCache.get(url);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Check if failed recently
|
||||
Long failTime = failedUrls.get(url);
|
||||
if (
|
||||
failTime != null &&
|
||||
System.currentTimeMillis() - failTime < FAILED_URL_RETRY_MS
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Start async download if not already pending
|
||||
if (!pendingDownloads.containsKey(url)) {
|
||||
downloadAsync(url);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a texture is loaded and ready.
|
||||
*/
|
||||
public boolean isTextureReady(String url) {
|
||||
if (url == null || url.isEmpty()) return false;
|
||||
DynamicOnlineTexture tex = textureCache.get(url);
|
||||
return tex != null && tex.isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an async download for a URL.
|
||||
*/
|
||||
private void downloadAsync(String url) {
|
||||
CompletableFuture<DynamicOnlineTexture> future =
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
return downloadTexture(url);
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] Download failed for {}: {}",
|
||||
url,
|
||||
e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
downloadExecutor
|
||||
);
|
||||
|
||||
pendingDownloads.put(url, future);
|
||||
|
||||
future.thenAccept(texture -> {
|
||||
pendingDownloads.remove(url);
|
||||
|
||||
if (texture != null && texture.isValid()) {
|
||||
// Cache management - remove oldest if at capacity
|
||||
if (textureCache.size() >= MAX_CACHE_SIZE) {
|
||||
evictOldest();
|
||||
}
|
||||
textureCache.put(url, texture);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[DynamicTextureManager] Loaded texture from {}",
|
||||
url
|
||||
);
|
||||
} else {
|
||||
failedUrls.put(url, System.currentTimeMillis());
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] Failed to load texture from {}",
|
||||
url
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a texture from a URL.
|
||||
* Validates host whitelist and performs the HTTP download.
|
||||
*/
|
||||
@Nullable
|
||||
private DynamicOnlineTexture downloadTexture(String urlString) {
|
||||
try {
|
||||
// Validate URL format
|
||||
if (!urlString.startsWith("https://")) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] Rejected non-HTTPS URL: {}",
|
||||
urlString
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
URL url = URI.create(urlString).toURL();
|
||||
String host = url.getHost().toLowerCase();
|
||||
|
||||
// Validate host whitelist
|
||||
if (ModConfig.CLIENT.useTextureHostWhitelist.get()) {
|
||||
if (!isHostWhitelisted(host)) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] Host not whitelisted: {}",
|
||||
host
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Open connection with browser-like headers
|
||||
// Many CDNs (including Tumblr) block requests without proper headers
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
conn.setReadTimeout(READ_TIMEOUT_MS);
|
||||
conn.setRequestProperty(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
);
|
||||
conn.setRequestProperty(
|
||||
"Accept",
|
||||
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
|
||||
);
|
||||
conn.setRequestProperty("Accept-Language", "en-US,en;q=0.9");
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] HTTP {} for {}",
|
||||
responseCode,
|
||||
urlString
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
String contentType = conn.getContentType();
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[DynamicTextureManager] Content-Type: {} for {}",
|
||||
contentType,
|
||||
urlString
|
||||
);
|
||||
|
||||
// Read image - convert any format to PNG for NativeImage compatibility
|
||||
try (InputStream is = conn.getInputStream()) {
|
||||
// Buffer the stream so we can inspect/retry if needed
|
||||
byte[] imageBytes = is.readAllBytes();
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[DynamicTextureManager] Downloaded {} bytes (Content-Type: {}) from {}",
|
||||
imageBytes.length,
|
||||
contentType,
|
||||
urlString
|
||||
);
|
||||
|
||||
if (imageBytes.length == 0) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] Empty response from {}",
|
||||
urlString
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if response looks like HTML (error page) instead of image
|
||||
if (imageBytes.length > 0 && imageBytes[0] == '<') {
|
||||
String preview = new String(
|
||||
imageBytes,
|
||||
0,
|
||||
Math.min(200, imageBytes.length)
|
||||
);
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] Response looks like HTML, not an image: {}",
|
||||
preview
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
NativeImage image = readImageAsNative(
|
||||
new ByteArrayInputStream(imageBytes),
|
||||
urlString
|
||||
);
|
||||
if (image == null) {
|
||||
// Log first bytes to help debug
|
||||
StringBuilder hexPreview = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(16, imageBytes.length); i++) {
|
||||
hexPreview.append(
|
||||
String.format("%02X ", imageBytes[i])
|
||||
);
|
||||
}
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] First 16 bytes: {}",
|
||||
hexPreview
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return new DynamicOnlineTexture(image, urlString);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[DynamicTextureManager] Error downloading {}: {}",
|
||||
urlString,
|
||||
e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an image from InputStream, converting any format (JPG, PNG, etc.) to NativeImage.
|
||||
* NativeImage only supports PNG natively, so we use ImageIO to read other formats
|
||||
* and convert them to PNG bytes.
|
||||
*
|
||||
* @param is The input stream containing image data
|
||||
* @param urlString The URL (for logging)
|
||||
* @return NativeImage or null if failed
|
||||
*/
|
||||
@Nullable
|
||||
private NativeImage readImageAsNative(InputStream is, String urlString) {
|
||||
try {
|
||||
// First, try reading with ImageIO (supports JPG, PNG, GIF, BMP, etc.)
|
||||
BufferedImage bufferedImage = ImageIO.read(is);
|
||||
if (bufferedImage == null) {
|
||||
// Try to peek at the first bytes to see what we got
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DynamicTextureManager] ImageIO could not read image from {} - format may be unsupported or data is not an image",
|
||||
urlString
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[DynamicTextureManager] Read image {}x{} from {}",
|
||||
bufferedImage.getWidth(),
|
||||
bufferedImage.getHeight(),
|
||||
urlString
|
||||
);
|
||||
|
||||
// Convert BufferedImage to PNG bytes, then to NativeImage
|
||||
ByteArrayOutputStream pngOutput = new ByteArrayOutputStream();
|
||||
ImageIO.write(bufferedImage, "PNG", pngOutput);
|
||||
byte[] pngBytes = pngOutput.toByteArray();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[DynamicTextureManager] Converted to {} PNG bytes",
|
||||
pngBytes.length
|
||||
);
|
||||
|
||||
// Now read as NativeImage (PNG format is supported)
|
||||
try (
|
||||
ByteArrayInputStream pngInput = new ByteArrayInputStream(
|
||||
pngBytes
|
||||
)
|
||||
) {
|
||||
return NativeImage.read(pngInput);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[DynamicTextureManager] Failed to convert image from {}: {}",
|
||||
urlString,
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a host is in the whitelist.
|
||||
*/
|
||||
private boolean isHostWhitelisted(String host) {
|
||||
// Check default whitelist
|
||||
if (DEFAULT_WHITELIST.contains(host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check config additional whitelist
|
||||
for (String allowed : ModConfig.CLIENT.textureHostWhitelist.get()) {
|
||||
if (
|
||||
host.equals(allowed.toLowerCase()) ||
|
||||
host.endsWith("." + allowed.toLowerCase())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict the oldest texture from cache.
|
||||
*/
|
||||
private void evictOldest() {
|
||||
// Simple eviction: remove first entry
|
||||
var iterator = textureCache.entrySet().iterator();
|
||||
if (iterator.hasNext()) {
|
||||
var entry = iterator.next();
|
||||
entry.getValue().close();
|
||||
iterator.remove();
|
||||
registeredTextures.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a ResourceLocation for a dynamic texture.
|
||||
* Registers the texture with Minecraft's texture manager.
|
||||
*
|
||||
* @param url The texture URL
|
||||
* @param fullSkin Whether to use full-skin mode texture
|
||||
* @return The ResourceLocation, or null if not available
|
||||
*/
|
||||
@Nullable
|
||||
public ResourceLocation getTextureLocation(String url, boolean fullSkin) {
|
||||
DynamicOnlineTexture texture = getTexture(url);
|
||||
if (texture == null || !texture.isValid()) return null;
|
||||
|
||||
String key = url + (fullSkin ? "_fullskin" : "_clothes");
|
||||
|
||||
return registeredTextures.computeIfAbsent(key, k -> {
|
||||
var dynTex = fullSkin
|
||||
? texture.getFullSkinTexture()
|
||||
: texture.getClothesTexture();
|
||||
if (dynTex == null) return null;
|
||||
|
||||
// Create unique ResourceLocation
|
||||
int hash = Math.abs(key.hashCode());
|
||||
ResourceLocation loc = ResourceLocation.fromNamespaceAndPath(
|
||||
TiedUpMod.MOD_ID,
|
||||
"dynamic/clothes_" + hash
|
||||
);
|
||||
|
||||
// Register with Minecraft
|
||||
Minecraft.getInstance().getTextureManager().register(loc, dynTex);
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a specific URL from cache.
|
||||
*/
|
||||
public void invalidate(String url) {
|
||||
DynamicOnlineTexture tex = textureCache.remove(url);
|
||||
if (tex != null) {
|
||||
tex.close();
|
||||
}
|
||||
pendingDownloads.remove(url);
|
||||
failedUrls.remove(url);
|
||||
registeredTextures.remove(url + "_clothes");
|
||||
registeredTextures.remove(url + "_fullskin");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches. Called on world unload.
|
||||
*/
|
||||
public void clearAll() {
|
||||
for (DynamicOnlineTexture tex : textureCache.values()) {
|
||||
tex.close();
|
||||
}
|
||||
textureCache.clear();
|
||||
pendingDownloads.clear();
|
||||
failedUrls.clear();
|
||||
registeredTextures.clear();
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[DynamicTextureManager] Cleared all texture caches"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up stale failed URL entries.
|
||||
*/
|
||||
public void cleanupStale() {
|
||||
long now = System.currentTimeMillis();
|
||||
failedUrls
|
||||
.entrySet()
|
||||
.removeIf(e -> now - e.getValue() > FAILED_URL_RETRY_MS * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics for debugging.
|
||||
*/
|
||||
public String getCacheStats() {
|
||||
return String.format(
|
||||
"Cache: %d textures, %d pending, %d failed",
|
||||
textureCache.size(),
|
||||
pendingDownloads.size(),
|
||||
failedUrls.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user