diff --git a/client/src/main/java/org/geysermc/rainbow/client/MinecraftAssetResolver.java b/client/src/main/java/org/geysermc/rainbow/client/MinecraftAssetResolver.java index d3e2f7c..7a4acab 100644 --- a/client/src/main/java/org/geysermc/rainbow/client/MinecraftAssetResolver.java +++ b/client/src/main/java/org/geysermc/rainbow/client/MinecraftAssetResolver.java @@ -8,20 +8,25 @@ import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.world.item.equipment.EquipmentAsset; import org.geysermc.rainbow.client.accessor.ResolvedModelAccessor; import org.geysermc.rainbow.client.mixin.EntityRenderDispatcherAccessor; import org.geysermc.rainbow.mapping.AssetResolver; +import java.io.IOException; +import java.io.InputStream; import java.util.Optional; public class MinecraftAssetResolver implements AssetResolver { private final ModelManager modelManager; private final EquipmentAssetManager equipmentAssetManager; + private final ResourceManager resourceManager; public MinecraftAssetResolver(Minecraft minecraft) { modelManager = minecraft.getModelManager(); equipmentAssetManager = ((EntityRenderDispatcherAccessor) minecraft.getEntityRenderDispatcher()).getEquipmentAssets(); + resourceManager = minecraft.getResourceManager(); } @Override @@ -38,4 +43,9 @@ public class MinecraftAssetResolver implements AssetResolver { public Optional getEquipmentInfo(ResourceKey key) { return Optional.of(equipmentAssetManager.get(key)); } + + @Override + public InputStream openAsset(ResourceLocation location) throws IOException { + return resourceManager.open(location); + } } diff --git a/client/src/main/java/org/geysermc/rainbow/client/MinecraftPackSerializer.java b/client/src/main/java/org/geysermc/rainbow/client/MinecraftPackSerializer.java index 6ee5895..61c1e39 100644 --- a/client/src/main/java/org/geysermc/rainbow/client/MinecraftPackSerializer.java +++ b/client/src/main/java/org/geysermc/rainbow/client/MinecraftPackSerializer.java @@ -16,7 +16,6 @@ import org.geysermc.rainbow.mapping.PackSerializer; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; import java.util.Objects; @@ -24,11 +23,9 @@ import java.util.concurrent.CompletableFuture; public class MinecraftPackSerializer implements PackSerializer { private final HolderLookup.Provider registries; - private final ResourceManager resourceManager; public MinecraftPackSerializer(Minecraft minecraft) { registries = Objects.requireNonNull(minecraft.level).registryAccess(); - resourceManager = minecraft.getResourceManager(); } @Override @@ -44,13 +41,12 @@ public class MinecraftPackSerializer implements PackSerializer { } @Override - public CompletableFuture saveTexture(ResourceLocation texture, Path path) { + public CompletableFuture saveTexture(byte[] texture, Path path) { return CompletableFuture.runAsync(() -> { - ResourceLocation texturePath = texture.withPath(p -> "textures/" + p + ".png"); - try (InputStream inputTexture = resourceManager.open(texturePath)) { + try { CodecUtil.ensureDirectoryExists(path.getParent()); try (OutputStream outputTexture = new FileOutputStream(path.toFile())) { - IOUtils.copy(inputTexture, outputTexture); + outputTexture.write(texture); } } catch (IOException exception) { // TODO log diff --git a/client/src/main/java/org/geysermc/rainbow/client/PackManager.java b/client/src/main/java/org/geysermc/rainbow/client/PackManager.java index e81b348..f6d9a52 100644 --- a/client/src/main/java/org/geysermc/rainbow/client/PackManager.java +++ b/client/src/main/java/org/geysermc/rainbow/client/PackManager.java @@ -88,8 +88,8 @@ public final class PackManager { Set bedrockItems = pack.getBedrockItems(); long attachables = bedrockItems.stream().filter(item -> item.attachable().isPresent()).count(); - long geometries = bedrockItems.stream().filter(item -> item.geometry().isPresent()).count(); - long animations = bedrockItems.stream().filter(item -> item.animation().isPresent()).count(); + long geometries = bedrockItems.stream().filter(item -> item.geometry().geometry().isPresent()).count(); + long animations = bedrockItems.stream().filter(item -> item.geometry().animation().isPresent()).count(); return """ -- PACK GENERATION REPORT -- diff --git a/client/src/main/java/org/geysermc/rainbow/client/RainbowClient.java b/client/src/main/java/org/geysermc/rainbow/client/RainbowClient.java index 1ef7cea..f004099 100644 --- a/client/src/main/java/org/geysermc/rainbow/client/RainbowClient.java +++ b/client/src/main/java/org/geysermc/rainbow/client/RainbowClient.java @@ -1,14 +1,33 @@ package org.geysermc.rainbow.client; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.brigadier.arguments.StringArgumentType; import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.SpriteLoader; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.animation.FrameSize; import net.minecraft.commands.synchronization.SingletonArgumentInfo; +import net.minecraft.data.AtlasIds; +import net.minecraft.resources.ResourceLocation; import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.client.command.CommandSuggestionsArgumentType; import org.geysermc.rainbow.client.command.PackGeneratorCommand; import org.geysermc.rainbow.client.mapper.PackMapper; +import org.geysermc.rainbow.mixin.SpriteContentsAccessor; +import org.geysermc.rainbow.mixin.SpriteLoaderAccessor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; public class RainbowClient implements ClientModInitializer { @@ -18,10 +37,53 @@ public class RainbowClient implements ClientModInitializer { // TODO export language overrides @Override public void onInitializeClient() { - ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> PackGeneratorCommand.register(dispatcher, packManager, packMapper)); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> { + PackGeneratorCommand.register(dispatcher, packManager, packMapper); + + dispatcher.register( + ClientCommandManager.literal("DEBUGTEST") + .then(ClientCommandManager.argument("textures", StringArgumentType.greedyString()) + .executes(context -> { + SpriteLoader spriteLoader = new SpriteLoader(AtlasIds.BLOCKS, 1024, 16, 16); + List sprites = Arrays.stream(StringArgumentType.getString(context, "textures").split(" ")) + .map(ResourceLocation::tryParse) + .map(RainbowClient::readSpriteContents) + .toList(); + SpriteLoader.Preparations preparations = ((SpriteLoaderAccessor) spriteLoader).invokeStitch(sprites, 0, Util.backgroundExecutor()); + + try (NativeImage stitched = stitchTextureAtlas(preparations)) { + stitched.writeToFile(FabricLoader.getInstance().getGameDir().resolve("test.png")); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + return 0; + }) + ) + ); + }); ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick); ArgumentTypeRegistry.registerArgumentType(Rainbow.getModdedLocation("command_suggestions"), CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new)); } + + private static SpriteContents readSpriteContents(ResourceLocation location) { + try (InputStream textureStream = Minecraft.getInstance().getResourceManager().open(location.withPath(path -> "textures/" + path + ".png"))) { + NativeImage texture = NativeImage.read(textureStream); + return new SpriteContents(location, new FrameSize(texture.getWidth(), texture.getHeight()), texture); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + + private static NativeImage stitchTextureAtlas(SpriteLoader.Preparations preparations) { + NativeImage stitched = new NativeImage(preparations.width(), preparations.height(), false); + for (TextureAtlasSprite sprite : preparations.regions().values()) { + try (SpriteContents contents = sprite.contents()) { + ((SpriteContentsAccessor) contents).getOriginalImage().copyRect(stitched, 0, 0, + sprite.getX(), sprite.getY(), contents.width(), contents.height(), false, false); + } + } + return stitched; + } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java b/rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java index 3aa247c..aa3352d 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java @@ -14,7 +14,16 @@ public class Rainbow { return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); } + // TODO rename remove file public static String fileSafeResourceLocation(ResourceLocation location) { return location.toString().replace(':', '.').replace('/', '_'); } + + public static ResourceLocation decorateResourceLocation(ResourceLocation location, String type, String extension) { + return location.withPath(path -> type + "/" + path + "." + extension); + } + + public static ResourceLocation decorateTextureLocation(ResourceLocation location) { + return decorateResourceLocation(location, "textures", "png"); + } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/image/NativeImageUtil.java b/rainbow/src/main/java/org/geysermc/rainbow/image/NativeImageUtil.java new file mode 100644 index 0000000..e79fbd8 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/image/NativeImageUtil.java @@ -0,0 +1,44 @@ +package org.geysermc.rainbow.image; + +import com.mojang.blaze3d.platform.NativeImage; +import org.geysermc.rainbow.mixin.NativeImageAccessor; +import org.lwjgl.stb.STBImage; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Pipe; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; + +public class NativeImageUtil { + + // Adjusted NativeImage#writeToFile + @SuppressWarnings("DataFlowIssue") + public static byte[] writeToByteArray(NativeImage image) throws IOException { + if (!image.format().supportedByStb()) { + throw new UnsupportedOperationException("Don't know how to write format " + image.format()); + } else { + ((NativeImageAccessor) (Object) image).invokeCheckAllocated(); + Pipe pipe = Pipe.open(); + try (WritableByteChannel outputChannel = pipe.sink()) { + if (!((NativeImageAccessor) (Object) image).invokeWriteToChannel(outputChannel)) { + throw new IOException("Could not write image to pipe: " + STBImage.stbi_failure_reason()); + } + } + + try (ReadableByteChannel inputChannel = pipe.source()) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ByteBuffer buffer = ByteBuffer.allocate(4096); + while (inputChannel.read(buffer) != -1) { + buffer.flip(); + while (buffer.hasRemaining()) { + bytes.write(buffer.get()); + } + buffer.clear(); + } + return bytes.toByteArray(); + } + } + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java index 53ee42b..922017e 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java @@ -7,6 +7,8 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.equipment.EquipmentAsset; +import java.io.IOException; +import java.io.InputStream; import java.util.Optional; public interface AssetResolver { @@ -16,4 +18,6 @@ public interface AssetResolver { Optional getClientItem(ResourceLocation location); Optional getEquipmentInfo(ResourceKey key); + + InputStream openAsset(ResourceLocation location) throws IOException; } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java index 8e1e7d9..02e12f2 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java @@ -21,8 +21,6 @@ import net.minecraft.client.renderer.item.properties.select.Charge; import net.minecraft.client.renderer.item.properties.select.ContextDimension; import net.minecraft.client.renderer.item.properties.select.DisplayContext; import net.minecraft.client.renderer.item.properties.select.TrimMaterialProperty; -import net.minecraft.client.resources.model.Material; -import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.core.component.DataComponents; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -37,11 +35,8 @@ import net.minecraft.world.item.component.ItemAttributeModifiers; import net.minecraft.world.item.equipment.trim.TrimMaterial; import net.minecraft.world.level.Level; import org.apache.commons.lang3.ArrayUtils; -import org.geysermc.rainbow.mapping.animation.AnimationMapper; -import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext; import org.geysermc.rainbow.mapping.attachable.AttachableMapper; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; -import org.geysermc.rainbow.mapping.geometry.GeometryMapper; import org.geysermc.rainbow.definition.GeyserBaseDefinition; import org.geysermc.rainbow.definition.GeyserItemDefinition; import org.geysermc.rainbow.definition.GeyserLegacyDefinition; @@ -52,9 +47,7 @@ import org.geysermc.rainbow.definition.predicate.GeyserPredicate; import org.geysermc.rainbow.definition.predicate.GeyserRangeDispatchPredicate; import org.geysermc.rainbow.mixin.LateBoundIdMapperAccessor; import org.geysermc.rainbow.mixin.RangeSelectItemModelAccessor; -import org.geysermc.rainbow.mixin.TextureSlotsAccessor; import org.geysermc.rainbow.pack.BedrockItem; -import org.geysermc.rainbow.pack.BedrockTextures; import java.util.List; import java.util.Optional; @@ -62,9 +55,6 @@ import java.util.function.Function; import java.util.stream.Stream; public class BedrockItemMapper { - private static final List HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace") - .map(ResourceLocation::withDefaultNamespace) - .toList(); private static final List TRIMMABLE_ARMOR_TAGS = Stream.of("is_armor", "trimmable_armors") .map(ResourceLocation::withDefaultNamespace) .toList(); @@ -117,7 +107,7 @@ public class BedrockItemMapper { case ConditionalItemModel.Unbaked conditional -> mapConditionalModel(conditional, context.child("condition model ")); case RangeSelectItemModel.Unbaked rangeSelect -> mapRangeSelectModel(rangeSelect, context.child("range select model ")); case SelectItemModel.Unbaked select -> mapSelectModel(select, context.child("select model ")); - default -> context.reporter.report(() -> "unsupported item model " + getModelId(model)); + default -> context.report("unsupported item model " + getModelId(model)); } } @@ -126,10 +116,6 @@ public class BedrockItemMapper { context.packContext().assetResolver().getResolvedModel(itemModelLocation) .ifPresentOrElse(itemModel -> { - ResolvedModel parentModel = itemModel.parent(); - // debugName() returns the resource location of the model as a string - boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName())); - ResourceLocation bedrockIdentifier; if (itemModelLocation.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { bedrockIdentifier = ResourceLocation.fromNamespaceAndPath("geyser_mc", itemModelLocation.getPath()); @@ -137,29 +123,13 @@ public class BedrockItemMapper { bedrockIdentifier = itemModelLocation; } - Material layer0Texture = itemModel.getTopTextureSlots().getMaterial("layer0"); - Optional texture; - Optional customGeometry; - if (layer0Texture != null) { - texture = Optional.of(layer0Texture.texture()); - customGeometry = Optional.empty(); - } else { - // We can't stitch multiple textures together yet, so we just grab the first one we see - // This will only work properly for models with just one texture - texture = ((TextureSlotsAccessor) itemModel.getTopTextureSlots()).getResolvedValues().values().stream() - .map(Material::texture) - .findAny(); - // Unknown texture (doesn't use layer0), so we immediately assume the geometry is custom - // This check should probably be done differently - customGeometry = Optional.of(itemModel); - } - - texture.ifPresentOrElse(itemTexture -> { + BedrockGeometryContext geometry = BedrockGeometryContext.create(itemModel); + if (context.packContext.reportSuccesses()) { // Not a problem, but just report to get the model printed in the report file - context.reporter.report(() -> "creating mapping for block model " + itemModelLocation); - context.create(bedrockIdentifier, itemTexture, handheld, customGeometry); - }, () -> context.reporter.report(() -> "not mapping block model " + itemModelLocation + " because it has no texture")); - }, () -> context.reporter.report(() -> "missing block model " + itemModelLocation)); + context.report("creating mapping for block model " + itemModelLocation); + } + context.create(bedrockIdentifier, geometry); + }, () -> context.report("missing block model " + itemModelLocation)); } private static void mapConditionalModel(ConditionalItemModel.Unbaked model, MappingContext context) { @@ -176,7 +146,7 @@ public class BedrockItemMapper { ItemModel.Unbaked onFalse = model.onFalse(); if (predicateProperty == null) { - context.reporter.report(() -> "unsupported conditional model property " + property + ", only mapping on_false"); + context.report("unsupported conditional model property " + property + ", only mapping on_false"); mapItem(onFalse, context.child("condition on_false (unsupported property)")); return; } @@ -197,7 +167,7 @@ public class BedrockItemMapper { }; if (predicateProperty == null) { - context.reporter.report(() -> "unsupported range dispatch model property " + property + ", only mapping fallback, if it is present"); + context.report("unsupported range dispatch model property " + property + ", only mapping fallback, if it is present"); } else { for (RangeSelectItemModel.Entry entry : model.entries()) { mapItem(entry.model(), context.with(new GeyserRangeDispatchPredicate(predicateProperty, entry.threshold(), model.scale()), "threshold " + entry.threshold())); @@ -223,7 +193,7 @@ public class BedrockItemMapper { if (dataConstructor == null) { if (unbakedSwitch.property() instanceof DisplayContext) { - context.reporter.report(() -> "unsupported select model property display_context, only mapping \"gui\" case, if it exists"); + context.report("unsupported select model property display_context, only mapping \"gui\" case, if it exists"); for (SelectItemModel.SwitchCase switchCase : cases) { if (switchCase.values().contains(ItemDisplayContext.GUI)) { mapItem(switchCase.model(), context.child("select GUI display_context case (unsupported property) ")); @@ -231,7 +201,7 @@ public class BedrockItemMapper { } } } - context.reporter.report(() -> "unsupported select model property " + unbakedSwitch.property() + ", only mapping fallback, if present"); + context.report("unsupported select model property " + unbakedSwitch.property() + ", only mapping fallback, if present"); model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("select fallback case (unsupported property) "))); return; } @@ -255,17 +225,11 @@ public class BedrockItemMapper { return new MappingContext(predicateStack, stack, reporter.forChild(() -> childName), definitionCreator, packContext); } - public void create(ResourceLocation bedrockIdentifier, ResourceLocation texture, boolean displayHandheld, - Optional customModel) { - List tags; - if (stack.is(ItemTags.TRIMMABLE_ARMOR)) { - tags = TRIMMABLE_ARMOR_TAGS; - } else { - tags = List.of(); - } + public void create(ResourceLocation bedrockIdentifier, BedrockGeometryContext geometry) { + List tags = stack.is(ItemTags.TRIMMABLE_ARMOR) ? TRIMMABLE_ARMOR_TAGS : List.of(); GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.ofNullable(stack.getHoverName().tryCollapseToString()), predicateStack, - new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, displayHandheld, calculateProtectionValue(stack), tags), + new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, geometry.handheld(), calculateProtectionValue(stack), tags), stack.getComponentsPatch()); try { packContext.mappings().map(stack.getItemHolder(), definitionCreator.apply(base)); @@ -274,26 +238,13 @@ public class BedrockItemMapper { return; } - // TODO Should probably get a better way to get geometry texture - String safeIdentifier = base.textureName(); - String bone = "bone"; - ResourceLocation geometryTexture = texture; - Optional bedrockGeometry = customModel.flatMap(model -> GeometryMapper.mapGeometry(safeIdentifier, bone, model, geometryTexture)); - Optional bedrockAnimation = customModel.map(model -> AnimationMapper.mapAnimation(safeIdentifier, bone, model.getTopTransforms())); + // TODO move attachable mapping somewhere else for cleaner code? + packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), geometry, + AttachableMapper.mapItem(stack.getComponentsPatch(), bedrockIdentifier, geometry, packContext.assetResolver(), packContext.additionalTextureConsumer()))); + } - boolean exportTexture = true; - if (customModel.isPresent()) { - texture = texture.withPath(path -> path + "_icon"); - // FIXME Bit of a hack, preferably render geometry at a later stage - exportTexture = !packContext.geometryRenderer().render(stack, packContext.paths().packRoot().resolve(BedrockTextures.TEXTURES_FOLDER + texture.getPath() + ".png")); - packContext.additionalTextureConsumer().accept(geometryTexture); - } - - packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), texture, exportTexture, - AttachableMapper.mapItem(stack.getComponentsPatch(), bedrockIdentifier, bedrockGeometry, bedrockAnimation, - packContext.assetResolver(), - packContext.additionalTextureConsumer()), - bedrockGeometry.map(BedrockGeometryContext::geometry), bedrockAnimation.map(BedrockAnimationContext::animation))); + public void report(String problem) { + reporter.report(() -> problem); } private static int calculateProtectionValue(ItemStack stack) { diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackContext.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackContext.java index e32196a..d581eb3 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackContext.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackContext.java @@ -1,13 +1,13 @@ package org.geysermc.rainbow.mapping; -import net.minecraft.resources.ResourceLocation; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.definition.GeyserMappings; +import org.geysermc.rainbow.mapping.geometry.TextureHolder; import org.geysermc.rainbow.pack.PackPaths; import java.util.function.Consumer; public record PackContext(GeyserMappings mappings, PackPaths paths, BedrockItemConsumer itemConsumer, AssetResolver assetResolver, - GeometryRenderer geometryRenderer, Consumer additionalTextureConsumer, + GeometryRenderer geometryRenderer, Consumer additionalTextureConsumer, boolean reportSuccesses) { } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java index 5f74525..ef1b9e2 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java @@ -1,7 +1,6 @@ package org.geysermc.rainbow.mapping; import com.mojang.serialization.Codec; -import net.minecraft.resources.ResourceLocation; import java.nio.file.Path; import java.util.concurrent.CompletableFuture; @@ -10,5 +9,5 @@ public interface PackSerializer { CompletableFuture saveJson(Codec codec, T object, Path path); - CompletableFuture saveTexture(ResourceLocation texture, Path path); + CompletableFuture saveTexture(byte[] texture, Path path); } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java index 85a1487..c5fc157 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java @@ -9,8 +9,8 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.item.equipment.Equippable; import org.geysermc.rainbow.mapping.AssetResolver; -import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; +import org.geysermc.rainbow.mapping.geometry.TextureHolder; import org.geysermc.rainbow.pack.attachable.BedrockAttachable; import java.util.List; @@ -19,12 +19,12 @@ import java.util.function.Consumer; public class AttachableMapper { - public static Optional mapItem(DataComponentPatch components, ResourceLocation bedrockIdentifier, Optional customGeometry, - Optional customAnimation, AssetResolver assetResolver, Consumer textureConsumer) { + public static Optional mapItem(DataComponentPatch components, ResourceLocation bedrockIdentifier, BedrockGeometryContext geometryContext, + AssetResolver assetResolver, Consumer textureConsumer) { // Crazy optional statement // Unfortunately we can't have both equippables and custom models, so we prefer the latter :( - return customGeometry - .map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.geometry().definitions().getFirst(), geometry.texture().getPath())) + return geometryContext.geometry() + .map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.definitions().getFirst(), geometryContext.texture().location().getPath())) .or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE)) .flatMap(optional -> (Optional) optional) .flatMap(equippable -> equippable.assetId().flatMap(assetResolver::getEquipmentInfo).map(info -> Pair.of(equippable.slot(), info))) @@ -33,14 +33,14 @@ public class AttachableMapper { .mapSecond(info -> info.getLayers(getLayer(assetInfo.getFirst())))) .filter(assetInfo -> !assetInfo.getSecond().isEmpty()) .map(assetInfo -> { - ResourceLocation texture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst())); - textureConsumer.accept(texture); - return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), texture.getPath()); + ResourceLocation equipmentTexture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst())); + textureConsumer.accept(new TextureHolder(equipmentTexture)); + return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), equipmentTexture.getPath()); })) .map(attachable -> { - customAnimation.ifPresent(context -> { - attachable.withAnimation("first_person", context.firstPerson()); - attachable.withAnimation("third_person", context.thirdPerson()); + geometryContext.animation().ifPresent(animation -> { + attachable.withAnimation("first_person", animation.firstPerson()); + attachable.withAnimation("third_person", animation.thirdPerson()); attachable.withScript("animate", "first_person", "context.is_first_person == 1.0"); attachable.withScript("animate", "third_person", "context.is_first_person == 0.0"); }); diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java index 2e26ac2..026a606 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java @@ -1,6 +1,53 @@ package org.geysermc.rainbow.mapping.geometry; +import net.minecraft.client.renderer.block.model.TextureSlots; +import net.minecraft.client.resources.model.Material; +import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.resources.ResourceLocation; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.mapping.animation.AnimationMapper; +import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext; import org.geysermc.rainbow.pack.geometry.BedrockGeometry; -public record BedrockGeometryContext(BedrockGeometry geometry, ResourceLocation texture) {} +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public record BedrockGeometryContext(Optional geometry, Optional animation, TextureHolder texture, + boolean handheld) { + private static final List HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace") + .map(ResourceLocation::withDefaultNamespace) + .toList(); + + public static BedrockGeometryContext create(ResolvedModel model) { + ResolvedModel parentModel = model.parent(); + // debugName() returns the resource location of the model as a string + boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName())); + + TextureSlots textures = model.getTopTextureSlots(); + Material layer0Texture = textures.getMaterial("layer0"); + Optional geometry; + Optional animation; + TextureHolder texture; + + if (layer0Texture != null) { + geometry = Optional.empty(); + animation = Optional.empty(); + texture = new TextureHolder(layer0Texture.texture()); + } else { + // Unknown model (doesn't use layer0), so we immediately assume the geometry is custom + // This check should probably be done differently (actually check if the model is 2D or 3D) + + ResourceLocation modelLocation = ResourceLocation.parse(model.debugName()); + String safeIdentifier = Rainbow.fileSafeResourceLocation(modelLocation); + + StitchedTextures stitchedTextures = StitchedTextures.stitchModelTextures(textures); + geometry = GeometryMapper.mapGeometry(safeIdentifier, "bone", model, stitchedTextures); + animation = Optional.of(AnimationMapper.mapAnimation(safeIdentifier, "bone", model.getTopTransforms())); + texture = new TextureHolder(modelLocation.withSuffix("_stitched"), Optional.of(stitchedTextures.stitched())); + // TODO geometry rendering + } + + return new BedrockGeometryContext(geometry, animation, texture, handheld); + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java index 550095b..a2794ba 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java @@ -7,7 +7,7 @@ import net.minecraft.client.renderer.block.model.SimpleUnbakedGeometry; import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.UnbakedGeometry; import net.minecraft.core.Direction; -import net.minecraft.resources.ResourceLocation; +import org.geysermc.rainbow.mixin.FaceBakeryAccessor; import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import org.joml.Vector2f; import org.joml.Vector3f; @@ -19,7 +19,7 @@ import java.util.Optional; public class GeometryMapper { private static final Vector3fc CENTRE_OFFSET = new Vector3f(8.0F, 0.0F, 8.0F); - public static Optional mapGeometry(String identifier, String boneName, ResolvedModel model, ResourceLocation texture) { + public static Optional mapGeometry(String identifier, String boneName, ResolvedModel model, StitchedTextures textures) { UnbakedGeometry top = model.getTopGeometry(); if (top == UnbakedGeometry.EMPTY) { return Optional.empty(); @@ -31,9 +31,8 @@ public class GeometryMapper { builder.withVisibleBoundsHeight(4.0F); builder.withVisibleBoundsOffset(new Vector3f(0.0F, 0.75F, 0.0F)); - // TODO proper texture size - builder.withTextureWidth(16); - builder.withTextureHeight(16); + builder.withTextureWidth(textures.width()); + builder.withTextureHeight(textures.height()); BedrockGeometry.Bone.Builder bone = BedrockGeometry.bone(boneName); @@ -43,7 +42,7 @@ public class GeometryMapper { SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) top; for (BlockElement element : geometry.elements()) { // TODO the origin here is wrong, some models seem to be mirrored weirdly in blockbench - BedrockGeometry.Cube cube = mapBlockElement(element).build(); + BedrockGeometry.Cube cube = mapBlockElement(element, textures).build(); bone.withCube(cube); min.min(cube.origin()); max.max(cube.origin().add(cube.size(), new Vector3f())); @@ -55,35 +54,36 @@ public class GeometryMapper { // Bind to the bone of the current item slot bone.withBinding("q.item_slot_to_bone_name(context.item_slot)"); - return Optional.of(new BedrockGeometryContext(builder.withBone(bone).build(), texture)); + return Optional.of(builder.withBone(bone).build()); } - private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element) { + private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element, StitchedTextures textures) { // The centre of the model is back by 8 in the X and Z direction on Java, so move the origin of the cube and the pivot like that BedrockGeometry.Cube.Builder builder = BedrockGeometry.cube(element.from().sub(CENTRE_OFFSET, new Vector3f()), element.to().sub(element.from(), new Vector3f())); for (Map.Entry faceEntry : element.faces().entrySet()) { - // TODO texture key Direction direction = faceEntry.getKey(); BlockElementFace face = faceEntry.getValue(); Vector2f uvOrigin; Vector2f uvSize; BlockElementFace.UVs uvs = face.uvs(); - if (uvs != null) { - // Up and down faces are special - if (direction.getAxis() == Direction.Axis.Y) { - uvOrigin = new Vector2f(uvs.maxU(), uvs.maxV()); - uvSize = new Vector2f(uvs.minU() - uvs.maxU(), uvs.minV() - uvs.maxV()); - } else { - uvOrigin = new Vector2f(uvs.minU(), uvs.minV()); - uvSize = new Vector2f(uvs.maxU() - uvs.minU(), uvs.maxV() - uvs.minV()); - } - } else { - uvOrigin = new Vector2f(); - uvSize = new Vector2f(); + if (uvs == null) { + // Java defaults to a set of UV values determined by the position of the face if no UV values were specified + uvs = FaceBakeryAccessor.invokeDefaultFaceUV(element.from(), element.to(), direction); } + // Up and down faces are special and have their UVs flipped + if (direction.getAxis() == Direction.Axis.Y) { + uvOrigin = new Vector2f(uvs.maxU(), uvs.maxV()); + uvSize = new Vector2f(uvs.minU() - uvs.maxU(), uvs.minV() - uvs.maxV()); + } else { + uvOrigin = new Vector2f(uvs.minU(), uvs.minV()); + uvSize = new Vector2f(uvs.maxU() - uvs.minU(), uvs.maxV() - uvs.minV()); + } + + // If the texture was stitched (which it should have been, unless it doesn't exist), offset the UVs by the texture's starting UV + textures.getSprite(face.texture()).ifPresent(sprite -> uvOrigin.add(sprite.getX(), sprite.getY())); builder.withFace(direction, uvOrigin, uvSize, face.rotation()); } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedTextures.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedTextures.java new file mode 100644 index 0000000..90dc0b6 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedTextures.java @@ -0,0 +1,77 @@ +package org.geysermc.rainbow.mapping.geometry; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.block.model.TextureSlots; +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.SpriteLoader; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.animation.FrameSize; +import net.minecraft.client.resources.model.Material; +import net.minecraft.data.AtlasIds; +import net.minecraft.resources.ResourceLocation; +import org.geysermc.rainbow.mixin.SpriteContentsAccessor; +import org.geysermc.rainbow.mixin.SpriteLoaderAccessor; +import org.geysermc.rainbow.mixin.TextureSlotsAccessor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public record StitchedTextures(Map sprites, Supplier stitched, int width, int height) { + + public Optional getSprite(String key) { + if (TextureSlotsAccessor.invokeIsTextureReference(key)) { + key = key.substring(1); + } + return Optional.ofNullable(sprites.get(key)); + } + + public static StitchedTextures stitchModelTextures(TextureSlots textures) { + Map materials = ((TextureSlotsAccessor) textures).getResolvedValues(); + SpriteLoader.Preparations preparations = prepareStitching(materials.values().stream().map(Material::texture)); + + Map sprites = new HashMap<>(); + for (Map.Entry material : materials.entrySet()) { + sprites.put(material.getKey(), preparations.getSprite(material.getValue().texture())); + } + return new StitchedTextures(Map.copyOf(sprites), () -> stitchTextureAtlas(preparations), preparations.width(), preparations.height()); + } + + private static SpriteLoader.Preparations prepareStitching(Stream textures) { + // Atlas ID doesn't matter much here, but BLOCKS is the most appropriate + // Not sure if 1024 should be the max supported texture size, but it seems to work + SpriteLoader spriteLoader = new SpriteLoader(AtlasIds.BLOCKS, 1024, 16, 16); + List sprites = textures.map(StitchedTextures::readSpriteContents).toList(); + return ((SpriteLoaderAccessor) spriteLoader).invokeStitch(sprites, 0, Util.backgroundExecutor()); + } + + private static SpriteContents readSpriteContents(ResourceLocation location) { + // TODO decorate path util + // TODO don't use ResourceManager + // TODO IO is on main thread here? + try (InputStream textureStream = Minecraft.getInstance().getResourceManager().open(location.withPath(path -> "textures/" + path + ".png"))) { + NativeImage texture = NativeImage.read(textureStream); + return new SpriteContents(location, new FrameSize(texture.getWidth(), texture.getHeight()), texture); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + + private static NativeImage stitchTextureAtlas(SpriteLoader.Preparations preparations) { + NativeImage stitched = new NativeImage(preparations.width(), preparations.height(), false); + for (TextureAtlasSprite sprite : preparations.regions().values()) { + try (SpriteContents contents = sprite.contents()) { + ((SpriteContentsAccessor) contents).getOriginalImage().copyRect(stitched, 0, 0, + sprite.getX(), sprite.getY(), contents.width(), contents.height(), false, false); + } + } + return stitched; + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/TextureHolder.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/TextureHolder.java new file mode 100644 index 0000000..cef5482 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/TextureHolder.java @@ -0,0 +1,14 @@ +package org.geysermc.rainbow.mapping.geometry; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.resources.ResourceLocation; + +import java.util.Optional; +import java.util.function.Supplier; + +public record TextureHolder(ResourceLocation location, Optional> supplier) { + + public TextureHolder(ResourceLocation location) { + this(location, Optional.empty()); + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/FaceBakeryAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/FaceBakeryAccessor.java new file mode 100644 index 0000000..2fa94a4 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/FaceBakeryAccessor.java @@ -0,0 +1,17 @@ +package org.geysermc.rainbow.mixin; + +import net.minecraft.client.renderer.block.model.BlockElementFace; +import net.minecraft.client.renderer.block.model.FaceBakery; +import net.minecraft.core.Direction; +import org.joml.Vector3fc; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(FaceBakery.class) +public interface FaceBakeryAccessor { + + @Invoker + static BlockElementFace.UVs invokeDefaultFaceUV(Vector3fc posFrom, Vector3fc posTo, Direction facing) { + throw new AssertionError(); + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/NativeImageAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/NativeImageAccessor.java new file mode 100644 index 0000000..6840312 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/NativeImageAccessor.java @@ -0,0 +1,18 @@ +package org.geysermc.rainbow.mixin; + +import com.mojang.blaze3d.platform.NativeImage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.io.IOException; +import java.nio.channels.WritableByteChannel; + +@Mixin(NativeImage.class) +public interface NativeImageAccessor { + + @Invoker + void invokeCheckAllocated(); + + @Invoker + boolean invokeWriteToChannel(WritableByteChannel channel) throws IOException; +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteContentsAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteContentsAccessor.java new file mode 100644 index 0000000..e2793de --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteContentsAccessor.java @@ -0,0 +1,13 @@ +package org.geysermc.rainbow.mixin; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.renderer.texture.SpriteContents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SpriteContents.class) +public interface SpriteContentsAccessor { + + @Accessor + NativeImage getOriginalImage(); +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteLoaderAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteLoaderAccessor.java new file mode 100644 index 0000000..4401cac --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteLoaderAccessor.java @@ -0,0 +1,16 @@ +package org.geysermc.rainbow.mixin; + +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.SpriteLoader; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.List; +import java.util.concurrent.Executor; + +@Mixin(SpriteLoader.class) +public interface SpriteLoaderAccessor { + + @Invoker + SpriteLoader.Preparations invokeStitch(List contents, int mipLevel, Executor executor); +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java index c957971..6c5af2f 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java @@ -4,6 +4,7 @@ import net.minecraft.client.renderer.block.model.TextureSlots; import net.minecraft.client.resources.model.Material; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; import java.util.Map; @@ -12,4 +13,9 @@ public interface TextureSlotsAccessor { @Accessor Map getResolvedValues(); + + @Invoker + static boolean invokeIsTextureReference(String name) { + throw new AssertionError(); + } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java index 929260a..2742206 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java @@ -3,9 +3,8 @@ package org.geysermc.rainbow.pack; import net.minecraft.resources.ResourceLocation; import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.mapping.PackSerializer; -import org.geysermc.rainbow.pack.animation.BedrockAnimation; +import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; import org.geysermc.rainbow.pack.attachable.BedrockAttachable; -import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import java.nio.file.Path; import java.util.List; @@ -13,15 +12,14 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; -public record BedrockItem(ResourceLocation identifier, String textureName, ResourceLocation texture, boolean exportTexture, Optional attachable, - Optional geometry, Optional animation) { +public record BedrockItem(ResourceLocation identifier, String textureName, BedrockGeometryContext geometry, Optional attachable) { public List> save(PackSerializer serializer, Path attachableDirectory, Path geometryDirectory, Path animationDirectory) { return Stream.concat( attachable.stream().map(present -> present.save(serializer, attachableDirectory)), Stream.concat( - geometry.stream().map(present -> present.save(serializer, geometryDirectory)), - animation.stream().map(present -> present.save(serializer, animationDirectory, Rainbow.fileSafeResourceLocation(identifier))) + geometry.geometry().stream().map(present -> present.save(serializer, geometryDirectory)), + geometry.animation().stream().map(context -> context.animation().save(serializer, animationDirectory, Rainbow.fileSafeResourceLocation(identifier))) ) ).toList(); } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java index 4252d29..5455726 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java @@ -12,6 +12,8 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.CustomModelData; import org.geysermc.rainbow.CodecUtil; import org.geysermc.rainbow.PackConstants; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.image.NativeImageUtil; import org.geysermc.rainbow.mapping.AssetResolver; import org.geysermc.rainbow.mapping.BedrockItemMapper; import org.geysermc.rainbow.mapping.PackContext; @@ -19,9 +21,11 @@ import org.geysermc.rainbow.mapping.PackSerializer; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.mapping.geometry.NoopGeometryRenderer; import org.geysermc.rainbow.definition.GeyserMappings; +import org.geysermc.rainbow.mapping.geometry.TextureHolder; import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; @@ -41,7 +45,7 @@ public class BedrockPack { private final BedrockTextures.Builder itemTextures = BedrockTextures.builder(); private final Set bedrockItems = new HashSet<>(); - private final Set texturesToExport = new HashSet<>(); + private final Set texturesToExport = new HashSet<>(); private final Set modelsMapped = new HashSet<>(); private final IntSet customModelDataMapped = new IntOpenHashSet(); @@ -59,9 +63,7 @@ public class BedrockPack { // Not reading existing item mappings/texture atlas for now since that doesn't work all that well yet this.context = new PackContext(new GeyserMappings(), paths, item -> { itemTextures.withItemTexture(item); - if (item.exportTexture()) { - texturesToExport.add(item.texture()); - } + texturesToExport.add(item.geometry().texture()); bedrockItems.add(item); }, assetResolver, geometryRenderer, texturesToExport::add, reportSuccesses); this.reporter = reporter; @@ -130,8 +132,27 @@ public class BedrockPack { futures.addAll(item.save(serializer, paths.attachables(), paths.geometry(), paths.animation())); } - for (ResourceLocation texture : texturesToExport) { - futures.add(serializer.saveTexture(texture, paths.packRoot().resolve(BedrockTextures.TEXTURES_FOLDER + texture.getPath() + ".png"))); + for (TextureHolder texture : texturesToExport) { + ResourceLocation textureLocation = Rainbow.decorateTextureLocation(texture.location()); + texture.supplier() + .flatMap(image -> { + try { + return Optional.of(NativeImageUtil.writeToByteArray(image.get())); + } catch (IOException exception) { + // TODO log + return Optional.empty(); + } + }) + .or(() -> { + try (InputStream textureStream = context.assetResolver().openAsset(textureLocation)) { + return Optional.of(textureStream.readAllBytes()); + } catch (IOException exception) { + // TODO log + return Optional.empty(); + } + }) + .map(bytes -> serializer.saveTexture(bytes, paths.packRoot().resolve(textureLocation.getPath()))) + .ifPresent(futures::add); } if (paths.zipOutput().isPresent()) { diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java index 030da51..c14aae6 100644 --- a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java @@ -33,7 +33,7 @@ public record BedrockTextures(Map textures) { private final Map textures = new HashMap<>(); public Builder withItemTexture(BedrockItem item) { - return withTexture(item.textureName(), TEXTURES_FOLDER + item.texture().getPath()); + return withTexture(item.textureName(), TEXTURES_FOLDER + item.geometry().texture().location().getPath()); } public Builder withTexture(String name, String texture) { diff --git a/rainbow/src/main/resources/rainbow.mixins.json b/rainbow/src/main/resources/rainbow.mixins.json index 5e7f32d..c3f5d53 100644 --- a/rainbow/src/main/resources/rainbow.mixins.json +++ b/rainbow/src/main/resources/rainbow.mixins.json @@ -5,10 +5,16 @@ "compatibilityLevel": "JAVA_21", "client": [ "LateBoundIdMapperAccessor", + "NativeImageAccessor", "RangeSelectItemModelAccessor", + "SpriteContentsAccessor", + "SpriteLoaderAccessor", "TextureSlotsAccessor" ], "injectors": { "defaultRequire": 1 - } + }, + "mixins": [ + "FaceBakeryAccessor" + ] }