diff --git a/.gitignore b/.gitignore index fe2c8c6..ac00352 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,6 @@ gradle-app.setting # JDT-specific (Eclipse Java Development Tools) .classpath .gitsync.json + +# NixOS flake.nix +flake.* diff --git a/common/src/main/java/com/elfmcys/yesstevemodel/client/ClientModelManager.java b/common/src/main/java/com/elfmcys/yesstevemodel/client/ClientModelManager.java index b0f0099..c93309f 100644 --- a/common/src/main/java/com/elfmcys/yesstevemodel/client/ClientModelManager.java +++ b/common/src/main/java/com/elfmcys/yesstevemodel/client/ClientModelManager.java @@ -193,6 +193,22 @@ private static void processServerData(ByteBuffer data) { } private static void handlePacket01(byte[] decryptedBuffer) throws Exception { + // Plaintext shape (server nativeSyncModels + encrypt appendNextKey): + // [garbageLen|0x80] [0x00] [garbage] [0x01] [56-byte nextKey] + // garbageLen fits in 7 bits, so total = 2 + garbageLen + 1 + 56 ∈ [59, 186]. + // A late packet 05 chunk decrypted with the wrong key shows up here as + // kilobytes of structurally-invalid bytes — drop it instead of accepting + // it as a key exchange, which would corrupt lastKey and cascade into a + // handlePacket03 crash on the next inbound chunk. + if (decryptedBuffer == null || decryptedBuffer.length < 59 || decryptedBuffer.length > 186) { + return; + } + int rxGarbageLen = decryptedBuffer[0] & 0x7F; + if (decryptedBuffer.length != 2 + rxGarbageLen + 1 + 56 + || decryptedBuffer[2 + rxGarbageLen] != 0x01) { + return; + } + key1 = new byte[56]; System.arraycopy(decryptedBuffer, decryptedBuffer.length - 56, key1, 0, 56); syncStep = 2; @@ -223,7 +239,11 @@ private record ModelHash(long hash1, long hash2) { private static void handlePacket03(YSMByteBuf buf) throws Exception { buf.skipGarbageHeader(); - int type = buf.readVarInt(); // expect 3 + int type = buf.readVarInt(); + // Defense in depth: a wrong-key decryption here would have lastKey pointing + // at junk and `type` reading as anything. handlePacket05 already does the + // same guard with `if (type != 5) return`. + if (type != 3) return; long folderHash = buf.readVarLong(); currentCacheFolderName = Long.toHexString(folderHash); diff --git a/common/src/main/java/com/elfmcys/yesstevemodel/client/texture/OuterFileTexture.java b/common/src/main/java/com/elfmcys/yesstevemodel/client/texture/OuterFileTexture.java index b9b4284..1662c1b 100644 --- a/common/src/main/java/com/elfmcys/yesstevemodel/client/texture/OuterFileTexture.java +++ b/common/src/main/java/com/elfmcys/yesstevemodel/client/texture/OuterFileTexture.java @@ -16,11 +16,21 @@ public class OuterFileTexture extends AbstractTexture implements ITextureMap { private final byte[] data; + private final int rawRgbaWidth; + private final int rawRgbaHeight; private Map suffixTextures = Reference2ReferenceMaps.emptyMap(); public OuterFileTexture(byte[] data) { this.data = data; + this.rawRgbaWidth = 0; + this.rawRgbaHeight = 0; + } + + public OuterFileTexture(byte[] rawRgbaData, int width, int height) { + this.data = rawRgbaData; + this.rawRgbaWidth = width; + this.rawRgbaHeight = height; } @Override @@ -34,7 +44,7 @@ public void load(@NotNull ResourceManager resourceManager) { public void doLoad() { try { - NativeImage imageIn = NativeImage.read(new ByteArrayInputStream(data)); + NativeImage imageIn = isRawRgba() ? readRawRgba() : NativeImage.read(new ByteArrayInputStream(data)); int width = imageIn.getWidth(); int height = imageIn.getHeight(); TextureUtil.prepareImage(this.getId(), 0, width, height); @@ -44,6 +54,31 @@ public void doLoad() { } } + private boolean isRawRgba() { + return rawRgbaWidth > 0 && rawRgbaHeight > 0; + } + + private NativeImage readRawRgba() throws IOException { + long expectedLength = (long) rawRgbaWidth * (long) rawRgbaHeight * 4L; + if (data == null || data.length < expectedLength) { + throw new IOException("Invalid raw RGBA texture data"); + } + + NativeImage image = new NativeImage(rawRgbaWidth, rawRgbaHeight, false); + for (int y = 0; y < rawRgbaHeight; y++) { + int row = y * rawRgbaWidth * 4; + for (int x = 0; x < rawRgbaWidth; x++) { + int offset = row + x * 4; + int r = data[offset] & 0xFF; + int g = data[offset + 1] & 0xFF; + int b = data[offset + 2] & 0xFF; + int a = data[offset + 3] & 0xFF; + image.setPixelRGBA(x, y, (a << 24) | (b << 16) | (g << 8) | r); + } + } + return image; + } + public void setSuffixTextures(Map map) { this.suffixTextures = Reference2ReferenceMaps.unmodifiable(new Reference2ReferenceOpenHashMap<>(map)); } @@ -51,4 +86,4 @@ public void setSuffixTextures(Map map) { public Map getSuffixTextures() { return this.suffixTextures; } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/elfmcys/yesstevemodel/resource/YSMBinaryDeserializer.java b/common/src/main/java/com/elfmcys/yesstevemodel/resource/YSMBinaryDeserializer.java index db8ce7f..39d3486 100644 --- a/common/src/main/java/com/elfmcys/yesstevemodel/resource/YSMBinaryDeserializer.java +++ b/common/src/main/java/com/elfmcys/yesstevemodel/resource/YSMBinaryDeserializer.java @@ -1,5 +1,6 @@ package com.elfmcys.yesstevemodel.resource; +import com.elfmcys.yesstevemodel.YesSteveModel; import com.elfmcys.yesstevemodel.resource.pojo.RawYsmModel; import io.netty.buffer.Unpooled; import rip.ysm.security.YSMByteBuf; @@ -28,7 +29,7 @@ public YSMBinaryDeserializer(byte[] decompressedData, int format) { private RawYsmModel deserializeInternal(boolean closeOnExit) { - System.out.println("deserializing format " + format + " file..."); + YesSteveModel.LOGGER.debug("deserializing format {} file...", format); if (format < 4) { deserializeLegacyV1(); } else if (format <= 15) { @@ -46,7 +47,7 @@ private RawYsmModel deserializeInternal(boolean closeOnExit) { if (closeOnExit) { this.reader.close(); } - System.out.println("end offset: 0x" + Integer.toHexString(offset)); + YesSteveModel.LOGGER.debug("end offset: 0x{}", Integer.toHexString(offset)); return model; } @@ -82,8 +83,8 @@ public void parseYSMFooter(RawYsmModel footer) { } } catch (Throwable t) { - System.out.println("ERROR"); - t.printStackTrace(System.out); + YesSteveModel.LOGGER.debug("Failed to parse YSM footer (format={}, offset=0x{})", + format, Integer.toHexString(reader.getOffset()), t); } } @@ -400,7 +401,7 @@ private void deserializeModern() { geoRef.sha256 = hash; geoRef.modelType = modelType; tempMainModels.add(geoRef); - System.out.println("Model Table Entry: ID=" + modelType + ", Hash=" + hash); + YesSteveModel.LOGGER.debug("Model Table Entry: ID={}, Hash={}", modelType, hash); } assignMainModels(tempMainModels); diff --git a/common/src/main/java/com/elfmcys/yesstevemodel/resource/YSMClientMapper.java b/common/src/main/java/com/elfmcys/yesstevemodel/resource/YSMClientMapper.java index 993cb72..d90cd26 100644 --- a/common/src/main/java/com/elfmcys/yesstevemodel/resource/YSMClientMapper.java +++ b/common/src/main/java/com/elfmcys/yesstevemodel/resource/YSMClientMapper.java @@ -1,6 +1,7 @@ package com.elfmcys.yesstevemodel.resource; import com.elfmcys.yesstevemodel.NativeLibLoader; +import com.elfmcys.yesstevemodel.YesSteveModel; import com.elfmcys.yesstevemodel.audio.AudioCodec; import com.elfmcys.yesstevemodel.audio.AudioTrackData; import com.elfmcys.yesstevemodel.client.ClientModelInfo; @@ -61,29 +62,21 @@ public class YSMClientMapper { public static class TranslucencyScanner { - private final BufferedImage[] images; + private final TranslucencyAtlas atlas; private final boolean[] results; -// private int remaining; public static final int STATE_INVISIBLE = 0; public static final int STATE_OPAQUE = 1; public static final int STATE_TRANSLUCENT = 2; public TranslucencyScanner(BufferedImage[] images, int expectedCount) { - this.images = images; - this.results = new boolean[Math.max(expectedCount, images.length)]; -// this.remaining = images.length; -// -// for (BufferedImage image : images) { -// if (image == null) { -// remaining--; -// } -// } + this(new TranslucencyAtlas(images), expectedCount); } -// public boolean isFinished() { -// return remaining <= 0; -// } + private TranslucencyScanner(TranslucencyAtlas atlas, int expectedCount) { + this.atlas = atlas; + this.results = new boolean[Math.max(expectedCount, atlas.size())]; + } public boolean[] getResults() { return results; @@ -103,13 +96,13 @@ public int scan(RawYsmModel.RawFace face) { boolean faceHasVisiblePixel = false; boolean faceHasTransparentPixel = false; - for (int i = 0; i < images.length; i++) { - if (images[i] == null) continue; + for (int i = 0; i < atlas.size(); i++) { + AlphaIndex index = atlas.get(i); + if (index == null) continue; hasValidImage = true; - BufferedImage img = images[i]; - int imgW = img.getWidth(); - int imgH = img.getHeight(); + int imgW = index.width; + int imgH = index.height; int startX = (int) Math.floor(minU * imgW + 0.01f); int endX = (int) Math.floor(maxU * imgW - 0.01f); @@ -124,53 +117,271 @@ public int scan(RawYsmModel.RawFace face) { startY = Math.max(0, Math.min(startY, imgH - 1)); endY = Math.max(0, Math.min(endY, imgH - 1)); - boolean imageHasVisiblePixel = false; - boolean imageHasTransparentPixel = false; - boolean imageHasColoredTranslucentPixel = false; + AlphaScanResult scanResult = index.scan(startX, endX, startY, endY); - for (int x = startX; x <= endX; x++) { - for (int y = startY; y <= endY; y++) { - int alpha = (img.getRGB(x, y) >>> 24) & 0xFF; + if (scanResult.hasVisiblePixel) { + faceHasVisiblePixel = true; + if (scanResult.hasTransparentPixel) { + faceHasTransparentPixel = true; + } + if (scanResult.hasColoredTranslucentPixel) { + results[i] = true; + } + } + } - if (alpha > 0) { - imageHasVisiblePixel = true; + if (!hasValidImage) return STATE_OPAQUE; + if (!faceHasVisiblePixel) return STATE_INVISIBLE; + if (faceHasTransparentPixel) return STATE_TRANSLUCENT; + return STATE_OPAQUE; + } + } - if (alpha < 255) { - imageHasColoredTranslucentPixel = true; - } - } + private static class TranslucencyAtlas { + private final AlphaIndex[] indexes; - if (alpha < 255) { - imageHasTransparentPixel = true; - } + TranslucencyAtlas(BufferedImage[] images) { + this.indexes = new AlphaIndex[images.length]; + for (int i = 0; i < images.length; i++) { + BufferedImage image = images[i]; + if (image != null) { + indexes[i] = new AlphaIndex(image); + } + } + } - if (imageHasVisiblePixel && imageHasTransparentPixel && imageHasColoredTranslucentPixel) { - break; - } - } + int size() { + return indexes.length; + } - if (imageHasVisiblePixel && imageHasTransparentPixel && imageHasColoredTranslucentPixel) { - break; + AlphaIndex get(int index) { + return indexes[index]; + } + } + + private static class AlphaIndex { + // Prefix sums are only worth their full-image build cost after repeated + // scans. The current model corpus regresses when this threshold is lower. + private static final long PREFIX_BUILD_AREA_MULTIPLIER = 8L; + + private final BufferedImage image; + final int width; + final int height; + private final int stride; + private final int[] intPixels; + private final int intBaseOffset; + private final int intScanlineStride; + private final byte[] bytePixels; + private final int byteAlphaOffset; + private final int bytePixelStride; + private final int byteScanlineStride; + private long directScanArea; + private int[] visible; + private int[] transparent; + private int[] coloredTranslucent; + + AlphaIndex(BufferedImage image) { + this.image = image; + this.width = image.getWidth(); + this.height = image.getHeight(); + this.stride = width + 1; + + java.awt.image.Raster raster = image.getRaster(); + java.awt.image.SampleModel sampleModel = raster.getSampleModel(); + java.awt.image.DataBuffer buffer = raster.getDataBuffer(); + + int[] resolvedIntPixels = null; + int resolvedIntBaseOffset = 0; + int resolvedIntScanlineStride = 0; + byte[] resolvedBytePixels = null; + int resolvedByteAlphaOffset = 0; + int resolvedBytePixelStride = 0; + int resolvedByteScanlineStride = 0; + + if ((image.getType() == BufferedImage.TYPE_INT_ARGB || image.getType() == BufferedImage.TYPE_INT_ARGB_PRE) + && sampleModel instanceof java.awt.image.SinglePixelPackedSampleModel + && buffer instanceof java.awt.image.DataBufferInt + && buffer.getNumBanks() == 1) { + java.awt.image.SinglePixelPackedSampleModel sm = (java.awt.image.SinglePixelPackedSampleModel) sampleModel; + java.awt.image.DataBufferInt db = (java.awt.image.DataBufferInt) buffer; + int baseOffset = db.getOffset() + sm.getOffset(-raster.getSampleModelTranslateX(), -raster.getSampleModelTranslateY()); + int scanlineStride = sm.getScanlineStride(); + int lastOffset = baseOffset + (height - 1) * scanlineStride + (width - 1); + int[] data = db.getData(); + if (baseOffset >= 0 && scanlineStride > 0 && lastOffset < data.length) { + resolvedIntPixels = data; + resolvedIntBaseOffset = baseOffset; + resolvedIntScanlineStride = scanlineStride; + } + } else if ((image.getType() == BufferedImage.TYPE_4BYTE_ABGR || image.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE) + && sampleModel instanceof java.awt.image.ComponentSampleModel + && buffer instanceof java.awt.image.DataBufferByte + && buffer.getNumBanks() == 1) { + java.awt.image.ComponentSampleModel sm = (java.awt.image.ComponentSampleModel) sampleModel; + java.awt.image.DataBufferByte db = (java.awt.image.DataBufferByte) buffer; + int[] bandOffsets = sm.getBandOffsets(); + if (bandOffsets.length >= 4) { + int pixelBaseOffset = db.getOffset() + + sm.getOffset(-raster.getSampleModelTranslateX(), -raster.getSampleModelTranslateY()) + - bandOffsets[0]; + int alphaOffset = pixelBaseOffset + bandOffsets[3]; + int pixelStride = sm.getPixelStride(); + int scanlineStride = sm.getScanlineStride(); + int lastOffset = alphaOffset + (height - 1) * scanlineStride + (width - 1) * pixelStride; + byte[] data = db.getData(); + if (alphaOffset >= 0 && lastOffset < data.length && pixelStride > 0 && scanlineStride > 0) { + resolvedBytePixels = data; + resolvedByteAlphaOffset = alphaOffset; + resolvedBytePixelStride = pixelStride; + resolvedByteScanlineStride = scanlineStride; } } + } - if (imageHasVisiblePixel) { - faceHasVisiblePixel = true; + this.intPixels = resolvedIntPixels; + this.intBaseOffset = resolvedIntBaseOffset; + this.intScanlineStride = resolvedIntScanlineStride; + this.bytePixels = resolvedBytePixels; + this.byteAlphaOffset = resolvedByteAlphaOffset; + this.bytePixelStride = resolvedBytePixelStride; + this.byteScanlineStride = resolvedByteScanlineStride; + } - if (imageHasTransparentPixel) { - faceHasTransparentPixel = true; + AlphaScanResult scan(int startX, int endX, int startY, int endY) { + if (visible != null) { + return scanPrefix(startX, endX, startY, endY); + } + + long area = (long) (endX - startX + 1) * (long) (endY - startY + 1); + directScanArea += area; + if (directScanArea >= (long) width * (long) height * PREFIX_BUILD_AREA_MULTIPLIER) { + buildPrefix(); + return scanPrefix(startX, endX, startY, endY); + } + + return scanDirect(startX, endX, startY, endY); + } + + private AlphaScanResult scanPrefix(int startX, int endX, int startY, int endY) { + return new AlphaScanResult( + sum(visible, startX, endX, startY, endY) > 0, + sum(transparent, startX, endX, startY, endY) > 0, + sum(coloredTranslucent, startX, endX, startY, endY) > 0 + ); + } + + private AlphaScanResult scanDirect(int startX, int endX, int startY, int endY) { + boolean imageHasVisiblePixel = false; + boolean imageHasTransparentPixel = false; + boolean imageHasColoredTranslucentPixel = false; + + scan: + if (intPixels != null) { + for (int y = startY; y <= endY; y++) { + int row = intBaseOffset + y * intScanlineStride; + for (int x = startX; x <= endX; x++) { + int alpha = (intPixels[row + x] >>> 24) & 0xFF; + if (alpha > 0) { + imageHasVisiblePixel = true; + if (alpha < 255) imageHasColoredTranslucentPixel = true; + } + if (alpha < 255) imageHasTransparentPixel = true; + if (imageHasVisiblePixel && imageHasTransparentPixel && imageHasColoredTranslucentPixel) break scan; + } + } + } else if (bytePixels != null) { + for (int y = startY; y <= endY; y++) { + int row = byteAlphaOffset + y * byteScanlineStride; + for (int x = startX; x <= endX; x++) { + int alpha = bytePixels[row + x * bytePixelStride] & 0xFF; + if (alpha > 0) { + imageHasVisiblePixel = true; + if (alpha < 255) imageHasColoredTranslucentPixel = true; + } + if (alpha < 255) imageHasTransparentPixel = true; + if (imageHasVisiblePixel && imageHasTransparentPixel && imageHasColoredTranslucentPixel) break scan; } + } + } else { + for (int x = startX; x <= endX; x++) { + for (int y = startY; y <= endY; y++) { + int alpha = (image.getRGB(x, y) >>> 24) & 0xFF; + if (alpha > 0) { + imageHasVisiblePixel = true; + if (alpha < 255) imageHasColoredTranslucentPixel = true; + } + if (alpha < 255) imageHasTransparentPixel = true; + if (imageHasVisiblePixel && imageHasTransparentPixel && imageHasColoredTranslucentPixel) break scan; + } + } + } - if (imageHasColoredTranslucentPixel) { - results[i] = true; + return new AlphaScanResult(imageHasVisiblePixel, imageHasTransparentPixel, imageHasColoredTranslucentPixel); + } + + private void buildPrefix() { + int size = stride * (height + 1); + visible = new int[size]; + transparent = new int[size]; + coloredTranslucent = new int[size]; + int[] rowPixels = intPixels == null && bytePixels == null ? new int[width] : null; + + for (int y = 0; y < height; y++) { + if (rowPixels != null) { + image.getRGB(0, y, width, 1, rowPixels, 0, width); + } + int row = (y + 1) * stride; + int previousRow = y * stride; + int visibleRunning = 0; + int transparentRunning = 0; + int coloredTranslucentRunning = 0; + int intRow = intBaseOffset + y * intScanlineStride; + int byteRow = byteAlphaOffset + y * byteScanlineStride; + + for (int x = 0; x < width; x++) { + int alpha; + if (intPixels != null) { + alpha = (intPixels[intRow + x] >>> 24) & 0xFF; + } else if (bytePixels != null) { + alpha = bytePixels[byteRow + x * bytePixelStride] & 0xFF; + } else { + alpha = (rowPixels[x] >>> 24) & 0xFF; } + + if (alpha > 0) visibleRunning++; + if (alpha < 255) transparentRunning++; + if (alpha > 0 && alpha < 255) coloredTranslucentRunning++; + + int idx = row + x + 1; + int above = previousRow + x + 1; + visible[idx] = visible[above] + visibleRunning; + transparent[idx] = transparent[above] + transparentRunning; + coloredTranslucent[idx] = coloredTranslucent[above] + coloredTranslucentRunning; } } + } - if (!hasValidImage) return STATE_OPAQUE; - if (!faceHasVisiblePixel) return STATE_INVISIBLE; - if (faceHasTransparentPixel) return STATE_TRANSLUCENT; - return STATE_OPAQUE; + private int sum(int[] prefix, int startX, int endX, int startY, int endY) { + int x1 = startX; + int y1 = startY; + int x2 = endX + 1; + int y2 = endY + 1; + return prefix[y2 * stride + x2] + - prefix[y1 * stride + x2] + - prefix[y2 * stride + x1] + + prefix[y1 * stride + x1]; + } + } + + private static class AlphaScanResult { + final boolean hasVisiblePixel; + final boolean hasTransparentPixel; + final boolean hasColoredTranslucentPixel; + + AlphaScanResult(boolean hasVisiblePixel, boolean hasTransparentPixel, boolean hasColoredTranslucentPixel) { + this.hasVisiblePixel = hasVisiblePixel; + this.hasTransparentPixel = hasTransparentPixel; + this.hasColoredTranslucentPixel = hasColoredTranslucentPixel; } } @@ -188,9 +399,9 @@ private static BufferedImage decodeToImage(byte[] data, int imageFormat, int wid try { if (imageFormat == -1) { - if (width > 0 && height > 0 && data.length >= width * height * 4) { + if (width > 0 && height > 0 && data.length >= (long) width * height * 4L) { BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - int[] pixels = new int[width * height]; + int[] pixels = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData(); for (int i = 0; i < pixels.length; i++) { int r = data[i * 4] & 0xFF; int g = data[i * 4 + 1] & 0xFF; @@ -198,7 +409,6 @@ private static BufferedImage decodeToImage(byte[] data, int imageFormat, int wid int a = data[i * 4 + 3] & 0xFF; pixels[i] = (a << 24) | (r << 16) | (g << 8) | b; } - img.setRGB(0, 0, width, height, pixels, 0, width); return img; } else throw new RuntimeException("Invalid RGBA texture"); } else { @@ -212,7 +422,7 @@ private static BufferedImage decodeToImage(byte[] data, int imageFormat, int wid } } } catch (Exception e) { - e.printStackTrace(); + YesSteveModel.LOGGER.debug("decodeToImage failed (format={}, {}x{})", imageFormat, width, height, e); } return null; } @@ -224,7 +434,7 @@ private static byte[] encodeToPng(BufferedImage img, byte[] fallbackData) { ImageIO.write(img, "png", baos); return baos.toByteArray(); } catch (Exception e) { - e.printStackTrace(); + YesSteveModel.LOGGER.debug("encodeToPng failed", e); } } return fallbackData; @@ -238,6 +448,30 @@ public static byte[] toPng(byte[] data, int imageFormat, int width, int height) return encodeToPng(img, data); } + private static OuterFileTexture buildTexture(byte[] data, int imageFormat, int width, int height) { + return buildTexture(data, imageFormat, width, height, null); + } + + private static OuterFileTexture buildTexture(byte[] data, int imageFormat, int width, int height, BufferedImage decodedImage) { + if (data == null || data.length == 0) { + return new OuterFileTexture(data); + } + if (imageFormat == -1 && width > 0 && height > 0 && data.length >= (long) width * height * 4L) { + return new OuterFileTexture(data, width, height); + } + + int resolvedFormat = imageFormat; + if (resolvedFormat == 0) { + resolvedFormat = YSMFolderDeserializer.detectFormat(data); + } + if (resolvedFormat == 1 || resolvedFormat == 2 || resolvedFormat == 3) { + return new OuterFileTexture(data); + } + + BufferedImage img = decodedImage != null ? decodedImage : decodeToImage(data, imageFormat, width, height); + return new OuterFileTexture(encodeToPng(img, data)); + } + public static ClientModelInfo buildParsedBundle(RawYsmModel raw, String modelId) { Map mainTextures = new LinkedHashMap<>(); int textureCount = Math.max(1, raw.mainEntity.textures.size()); @@ -248,17 +482,15 @@ public static ClientModelInfo buildParsedBundle(RawYsmModel raw, String modelId) BufferedImage img = decodeToImage(rt.data, rt.imageFormat, rt.width, rt.height); imagesList.add(img); - byte[] processedData = (rt.imageFormat == 2) ? rt.data : encodeToPng(img, rt.data); - OuterFileTexture tex = new OuterFileTexture(processedData); + OuterFileTexture tex = buildTexture(rt.data, rt.imageFormat, rt.width, rt.height, img); Map suffixTextures = new LinkedHashMap<>(); for (RawYsmModel.RawTexture.SubTexture sub : rt.subTextures) { if (sub.data == null) continue; - byte[] processedSubData = toPng(sub.data, sub.imageFormat, sub.width, sub.height); if (sub.specularType == 1) { - suffixTextures.put(ShadersTextureType.NORMAL, new OuterFileTexture(processedSubData)); + suffixTextures.put(ShadersTextureType.NORMAL, buildTexture(sub.data, sub.imageFormat, sub.width, sub.height)); } else if (sub.specularType == 2) { - suffixTextures.put(ShadersTextureType.SPECULAR, new OuterFileTexture(processedSubData)); + suffixTextures.put(ShadersTextureType.SPECULAR, buildTexture(sub.data, sub.imageFormat, sub.width, sub.height)); } } tex.setSuffixTextures(suffixTextures); @@ -267,8 +499,7 @@ public static ClientModelInfo buildParsedBundle(RawYsmModel raw, String modelId) Map avatarTextures = new LinkedHashMap<>(); for (RawYsmModel.RawMetadata.Author author : raw.metadata.authors) { if (author.avatarImage == null) continue; - byte[] processedAvatarData = toPng(author.avatarImage.data, author.avatarImage.format, author.avatarImage.width, author.avatarImage.height); - OuterFileTexture tex = new OuterFileTexture(processedAvatarData); + OuterFileTexture tex = buildTexture(author.avatarImage.data, author.avatarImage.format, author.avatarImage.width, author.avatarImage.height); avatarTextures.put(author.avatarImage.name, tex); } OrderedStringMap textureMap = buildTextureMap(mainTextures); @@ -276,10 +507,11 @@ public static ClientModelInfo buildParsedBundle(RawYsmModel raw, String modelId) GeometryDescription context = buildContext(raw.mainEntity.mainModel); BufferedImage[] imagesArray = imagesList.toArray(new BufferedImage[0]); + TranslucencyAtlas mainAtlas = new TranslucencyAtlas(imagesArray); TranslucencyScanner mainScanner = raw.mainEntity.mainModel != null ? - new TranslucencyScanner(imagesArray, textureCount) : null; + new TranslucencyScanner(mainAtlas, textureCount) : null; TranslucencyScanner armScanner = raw.mainEntity.armModel != null ? - new TranslucencyScanner(imagesArray, textureCount) : null; + new TranslucencyScanner(mainAtlas, textureCount) : null; GeoModel mainMesh = buildMesh(raw.mainEntity.mainModel, context, textureCount, mainScanner, raw.properties.allCutout); GeoModel armMesh = raw.mainEntity.armModel != null ? buildMesh(raw.mainEntity.armModel, context, textureCount, armScanner, raw.properties.allCutout) : mainMesh; @@ -753,9 +985,8 @@ private static ProjectileModelFiles buildSubEntityHolder(RawYsmModel.RawSubEntit for(RawYsmModel.RawTexture rt : sub.textures.values()) { BufferedImage img = decodeToImage(rt.data, rt.imageFormat, rt.width, rt.height); imgList.add(img); - byte[] processedData = (rt.imageFormat == 2) ? rt.data : encodeToPng(img, rt.data); if (texture == null) { - texture = new OuterFileTexture(processedData); + texture = buildTexture(rt.data, rt.imageFormat, rt.width, rt.height, img); } } if (sub.model != null) { @@ -795,9 +1026,8 @@ private static VehicleModelFiles buildSubEntityWrapper(RawYsmModel.RawSubEntity for(RawYsmModel.RawTexture rt : sub.textures.values()) { BufferedImage img = decodeToImage(rt.data, rt.imageFormat, rt.width, rt.height); imgList.add(img); - byte[] processedData = (rt.imageFormat == 2) ? rt.data : encodeToPng(img, rt.data); if (texture == null) { - texture = new OuterFileTexture(processedData); + texture = buildTexture(rt.data, rt.imageFormat, rt.width, rt.height, img); } } if (sub.model != null) { @@ -832,8 +1062,7 @@ private static Map buildExtraTextures(RawYsmModel raw) Map result = new LinkedHashMap<>(); for (RawYsmModel.RawImage img : raw.properties.backgroundImages) { if (img.name != null && !img.name.isEmpty()) { - byte[] processedData = toPng(img.data, img.format, img.width, img.height); - result.put(img.name, new OuterFileTexture(processedData)); + result.put(img.name, buildTexture(img.data, img.format, img.width, img.height)); } } return result; @@ -1005,4 +1234,4 @@ private static boolean isNegativeSizedFace(RawYsmModel.RawFace f) { float dot = nx * f.normal[0] + ny * f.normal[1] + nz * f.normal[2]; return dot < -1e-5f; } -} \ No newline at end of file +}