diff --git a/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/BootstrapLogListener.java b/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/BootstrapLogListener.java index b7b1a4b..7e69d00 100644 --- a/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/BootstrapLogListener.java +++ b/bootstrap/src/main/java/org/geysermc/pack/converter/bootstrap/BootstrapLogListener.java @@ -26,6 +26,7 @@ package org.geysermc.pack.converter.bootstrap; +import org.geysermc.pack.converter.util.DefaultLogListener; import org.geysermc.pack.converter.util.LogListener; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -33,7 +34,7 @@ import org.jetbrains.annotations.Nullable; import java.io.PrintWriter; import java.io.StringWriter; -public class BootstrapLogListener implements LogListener { +public class BootstrapLogListener extends DefaultLogListener { private final ThunderGUI gui; public BootstrapLogListener(ThunderGUI gui) { @@ -45,21 +46,26 @@ public class BootstrapLogListener implements LogListener { if (gui.debugMode.get()) { appendText("DEBUG: " + message); } + + super.debug(message); } @Override public void info(@NotNull String message) { appendText(message); + super.info(message); } @Override public void warn(@NotNull String message) { appendText("WARNING: " + message); + super.warn(message); } @Override public void error(@NotNull String message) { appendText("ERROR: " + message); + super.error(message); } @Override @@ -73,6 +79,7 @@ public class BootstrapLogListener implements LogListener { appendText(writer.toString()); } + super.error(message, exception); } private void appendText(String text) { diff --git a/converter/src/main/java/org/geysermc/pack/converter/pipeline/AssetConverters.java b/converter/src/main/java/org/geysermc/pack/converter/pipeline/AssetConverters.java index a6ed4fd..d7d6dac 100644 --- a/converter/src/main/java/org/geysermc/pack/converter/pipeline/AssetConverters.java +++ b/converter/src/main/java/org/geysermc/pack/converter/pipeline/AssetConverters.java @@ -80,7 +80,7 @@ public final class AssetConverters { public static final ConverterPipeline MODEL = create(ModelConverter.INSTANCE); public static final ConverterPipeline> SOUND_REGISTRY = create( (pack, context) -> pack.soundRegistries(), SoundRegistryConverter.INSTANCE); - public static final ConverterPipeline SOUND = create(extractor(SoundSerializer.CATEGORY), SoundConverter.INSTANCE); + public static final ConverterPipeline SOUND = create(SoundConverter.INSTANCE); public static final ConverterPipeline TEXTURE = create(TextureConverter.INSTANCE); private static ConverterPipeline createSingle(BiFunction extractor, diff --git a/converter/src/main/java/org/geysermc/pack/converter/type/sound/SoundConverter.java b/converter/src/main/java/org/geysermc/pack/converter/type/sound/SoundConverter.java index 367cfc3..f59e275 100644 --- a/converter/src/main/java/org/geysermc/pack/converter/type/sound/SoundConverter.java +++ b/converter/src/main/java/org/geysermc/pack/converter/type/sound/SoundConverter.java @@ -27,11 +27,10 @@ package org.geysermc.pack.converter.type.sound; import org.geysermc.pack.bedrock.resource.BedrockResourcePack; -import org.geysermc.pack.converter.pipeline.AssetCombiner; -import org.geysermc.pack.converter.pipeline.AssetConverter; -import org.geysermc.pack.converter.pipeline.CombineContext; -import org.geysermc.pack.converter.pipeline.ConversionContext; +import org.geysermc.pack.converter.pipeline.*; +import org.geysermc.pack.converter.util.JsonMappings; import org.jetbrains.annotations.Nullable; +import team.unnamed.creative.ResourcePack; import team.unnamed.creative.sound.Sound; import java.io.FileOutputStream; @@ -40,11 +39,17 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -public class SoundConverter implements AssetConverter, AssetCombiner { +public class SoundConverter implements AssetExtractor, AssetConverter, AssetCombiner { public static final SoundConverter INSTANCE = new SoundConverter(); + @Override + public Collection extract(ResourcePack pack, ExtractionContext context) { + return pack.sounds(); + } + @Override public @Nullable Sound convert(Sound sound, ConversionContext context) throws Exception { return sound; @@ -52,27 +57,32 @@ public class SoundConverter implements AssetConverter, AssetCombin @Override public void include(BedrockResourcePack pack, List sounds, CombineContext context) { + JsonMappings mappings = JsonMappings.getMapping("sounds"); + List exported = new ArrayList<>(); Path output = pack.directory().resolve(SoundRegistryConverter.BEDROCK_SOUNDS_LOCATION); for (Sound sound : sounds) { - String path = sound.key().value(); - if (exported.contains(path)) { - context.warn("Conflicting sound file " + sound.key() + "!"); - continue; - } - Path file = output.resolve(path + ".ogg"); - Path directory = file.getParent(); - try { - Files.createDirectories(directory); - try (OutputStream outputStream = new FileOutputStream(file.toFile())) { - sound.data().write(outputStream); + String javaPath = sound.key().value(); + List paths = mappings.map(javaPath); + for (String path : paths) { + if (exported.contains(path)) { + context.warn("Conflicting sound file " + sound.key() + "!"); + continue; } - } catch (IOException exception) { - context.error("Failed to write sound file " + sound.key() + "!", exception); - continue; + Path file = output.resolve(path + ".ogg"); + Path directory = file.getParent(); + try { + Files.createDirectories(directory); + try (OutputStream outputStream = new FileOutputStream(file.toFile())) { + sound.data().write(outputStream); + } + } catch (IOException exception) { + context.error("Failed to write sound file " + sound.key() + "!", exception); + continue; + } + exported.add(path); } - exported.add(path); } } } diff --git a/converter/src/main/java/org/geysermc/pack/converter/type/texture/TextureConverter.java b/converter/src/main/java/org/geysermc/pack/converter/type/texture/TextureConverter.java index b6f17be..bac67c2 100644 --- a/converter/src/main/java/org/geysermc/pack/converter/type/texture/TextureConverter.java +++ b/converter/src/main/java/org/geysermc/pack/converter/type/texture/TextureConverter.java @@ -37,6 +37,8 @@ import org.geysermc.pack.converter.type.texture.transformer.TextureTransformer; import org.geysermc.pack.converter.type.texture.transformer.TransformContext; import org.geysermc.pack.converter.type.texture.transformer.TransformedTexture; import org.geysermc.pack.converter.util.ImageUtil; +import org.geysermc.pack.converter.util.JsonMappings; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import team.unnamed.creative.ResourcePack; import team.unnamed.creative.texture.Texture; @@ -78,7 +80,7 @@ public class TextureConverter implements AssetExtractor, AssetConverter // TODO ideally textures should be transformed individually in the convert process, and not together in the extraction process, but this is hard to achieve, // TODO and will need another big refactor to the texture transformation code // TODO for now this will work, but for library users it might be nice to be able to properly convert singular textures with transformations - TextureMappings mappings = TextureMappings.textureMappings(); + JsonMappings mappings = JsonMappings.getMapping("textures"); List textures = new ArrayList<>(pack.textures()); context.info("Transforming textures..."); @@ -103,8 +105,8 @@ public class TextureConverter implements AssetExtractor, AssetConverter } @Override - public @Nullable TransformedTexture convert(Texture texture, ConversionContext context) throws Exception { - TextureMappings mappings = TextureMappings.textureMappings(); + public @NotNull TransformedTexture convert(Texture texture, ConversionContext context) throws Exception { + JsonMappings mappings = JsonMappings.getMapping("textures"); TransformedTexture transformed = new TransformedTexture(texture); String input = texture.key().value(); @@ -113,36 +115,13 @@ public class TextureConverter implements AssetExtractor, AssetConverter String rootPath = relativePath.substring(0, relativePath.indexOf('/')); String bedrockRoot = DIRECTORY_LOCATIONS.getOrDefault(rootPath, rootPath); - Object mappingObject = mappings.textures(relativePath); - - if (mappingObject == null) { - mappingObject = mappings.textures(rootPath); + List mapping = mappings.map(relativePath); + List transformedOutputs = new ArrayList<>(); + for (String item : mapping) { + transformedOutputs.add(bedrockRoot + item.substring(item.indexOf('/')) + ".png"); } - String fallbackPath = bedrockRoot + "/" + relativePath.substring(relativePath.indexOf('/') + 1) + ".png"; - if (mappingObject instanceof Map keyMappings) { // Handles common subdirectories - String sanitizedName = input.substring(input.indexOf('/') + 1); - if (sanitizedName.endsWith(".png")) sanitizedName = sanitizedName.substring(0, sanitizedName.length() - 4); - - Object bedrockOutput = keyMappings.get(sanitizedName); - if (bedrockOutput instanceof String bedrockPath) { - transformed.output(bedrockRoot + "/" + bedrockPath + ".png"); - } else if (bedrockOutput instanceof List paths) { - for (String bedrockPath : (List) paths) { - transformed.output(bedrockRoot + "/" + bedrockPath + ".png"); - } - } else { // Fallback - transformed.output(fallbackPath); - } - } else if (mappingObject instanceof String str) { // Direct mappings - transformed.output(str + ".png"); - } else if (mappingObject instanceof List paths) { // Mappings where duplicate code paths exist - for (String path : (List) paths) { - transformed.output(path + ".png"); - } - } else { // Fallback - transformed.output(fallbackPath); - } + transformed.output(transformedOutputs); return transformed; } @@ -206,6 +185,8 @@ public class TextureConverter implements AssetExtractor, AssetConverter } } + Files.createDirectories(output.getParent()); + try (OutputStream stream = Files.newOutputStream(output)) { ImageIO.write(bedrockImage, "png", stream); } diff --git a/converter/src/main/java/org/geysermc/pack/converter/type/texture/TextureMappings.java b/converter/src/main/java/org/geysermc/pack/converter/type/texture/TextureMappings.java deleted file mode 100644 index 4ddff9a..0000000 --- a/converter/src/main/java/org/geysermc/pack/converter/type/texture/TextureMappings.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/PackConverter - * - */ - -package org.geysermc.pack.converter.type.texture; - -import com.google.gson.Gson; -import lombok.ToString; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.LinkedHashMap; - -@ToString -public class TextureMappings extends LinkedHashMap { - private static TextureMappings INSTANCE; - - @Nullable - public Object textures(@NotNull String key) { - return this.get(key); - } - - public static TextureMappings textureMappings() { - if (INSTANCE != null) { - return INSTANCE; - } - InputStream mappingsStream = TextureMappings.class.getResourceAsStream("/mappings/textures.json"); - if (mappingsStream == null) { - throw new RuntimeException("Could not find textures.json mappings file!"); - } - - return INSTANCE = new Gson().fromJson(new InputStreamReader(mappingsStream), TextureMappings.class); - } -} diff --git a/converter/src/main/java/org/geysermc/pack/converter/type/texture/transformer/TransformContext.java b/converter/src/main/java/org/geysermc/pack/converter/type/texture/transformer/TransformContext.java index 6397c07..4484b34 100644 --- a/converter/src/main/java/org/geysermc/pack/converter/type/texture/transformer/TransformContext.java +++ b/converter/src/main/java/org/geysermc/pack/converter/type/texture/transformer/TransformContext.java @@ -28,8 +28,8 @@ package org.geysermc.pack.converter.type.texture.transformer; import net.kyori.adventure.key.Key; import org.geysermc.pack.bedrock.resource.BedrockResourcePack; -import org.geysermc.pack.converter.type.texture.TextureMappings; import org.geysermc.pack.converter.util.ImageUtil; +import org.geysermc.pack.converter.util.JsonMappings; import org.geysermc.pack.converter.util.LogListener; import org.geysermc.pack.converter.util.LogListenerHelper; import org.jetbrains.annotations.NotNull; @@ -46,7 +46,7 @@ import java.util.Map; import java.util.Optional; public class TransformContext implements LogListenerHelper { - private final TextureMappings mappings; + private final JsonMappings mappings; private final Collection textures; // TODO figure out how to handle this, this is executed in the extraction phase and ideally bedrock pack wouldn't be accessed then @Deprecated(forRemoval = true) @@ -57,7 +57,7 @@ public class TransformContext implements LogListenerHelper { private final Map byKey = new HashMap<>(); public TransformContext( - TextureMappings mappings, + JsonMappings mappings, Collection textures, BedrockResourcePack bedrockPack, ResourcePack javaPack, @@ -76,7 +76,7 @@ public class TransformContext implements LogListenerHelper { } } - public TextureMappings mappings() { + public JsonMappings mappings() { return this.mappings; } diff --git a/converter/src/main/java/org/geysermc/pack/converter/type/texture/transformer/TransformedTexture.java b/converter/src/main/java/org/geysermc/pack/converter/type/texture/transformer/TransformedTexture.java index 5387c5f..3c1e92e 100644 --- a/converter/src/main/java/org/geysermc/pack/converter/type/texture/transformer/TransformedTexture.java +++ b/converter/src/main/java/org/geysermc/pack/converter/type/texture/transformer/TransformedTexture.java @@ -34,7 +34,7 @@ import java.util.List; public class TransformedTexture { private final Texture texture; - private final List outputs = new ArrayList<>(); + private List outputs = new ArrayList<>(); public TransformedTexture(@NotNull Texture texture) { this.texture = texture; @@ -50,7 +50,7 @@ public class TransformedTexture { return outputs; } - public void output(@NotNull String output) { - outputs.add(output); + public void output(@NotNull List output) { + outputs = new ArrayList<>(output); } } diff --git a/converter/src/main/java/org/geysermc/pack/converter/util/JsonMappings.java b/converter/src/main/java/org/geysermc/pack/converter/util/JsonMappings.java new file mode 100644 index 0000000..43ca76f --- /dev/null +++ b/converter/src/main/java/org/geysermc/pack/converter/util/JsonMappings.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/PackConverter + * + */ + +package org.geysermc.pack.converter.util; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JsonMappings { + private static final Map CACHE = new HashMap<>(); + + public static JsonMappings getMapping(String name) { + if (CACHE.containsKey(name)) { + return CACHE.get(name); + } + InputStream mappingsStream = JsonMappings.class.getResourceAsStream("/mappings/%s.json".formatted(name)); + if (mappingsStream == null) { + throw new RuntimeException("Could not find %s.json mappings file!".formatted(name)); + } + + JsonObject jsonMappings = JsonParser.parseReader(new InputStreamReader(mappingsStream)).getAsJsonObject(); + + Map> mappings = new HashMap<>(); + + for (Map.Entry entry : jsonMappings.entrySet()) { + mappings.putAll(extractMapping(entry.getValue(), entry.getKey(), List.of())); + } + + JsonMappings instance = new JsonMappings(mappings); + CACHE.put(name, instance); + + return instance; + } + + private static Map> extractMapping(JsonElement element, String key, List parents) { + if (element.isJsonObject()) { + Map> mappings = new HashMap<>(); + + for (Map.Entry entry : element.getAsJsonObject().entrySet()) { + List newParents = new ArrayList<>(parents); + newParents.add(key); + mappings.putAll(extractMapping(entry.getValue(), key + "/" + entry.getKey(), newParents)); + } + + return mappings; + } else if (element.isJsonArray()) { + List paths = new ArrayList<>(); + + for (JsonElement arrayElement : element.getAsJsonArray()) { + if (arrayElement.isJsonPrimitive()) { + paths.add(arrayElement.getAsString()); + } else { + throw new RuntimeException("Invalid item found within mapping file, items in an array must be primitives."); + } + } + + return Map.of(key, paths); + } else if (element.isJsonPrimitive()) { + String prefix = ""; + if (!parents.isEmpty()) prefix = String.join("/", parents) + "/"; + return Map.of(key, List.of(prefix + element.getAsString())); + } + + return Map.of(); + } + + private final Map> mappings; + + private JsonMappings(Map> mappings) { + this.mappings = mappings; + } + + public List map(String input) { + return mappings.getOrDefault(input, List.of(input)); + } +} diff --git a/converter/src/main/resources/mappings/sounds.json b/converter/src/main/resources/mappings/sounds.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/converter/src/main/resources/mappings/sounds.json @@ -0,0 +1 @@ +{}