diff --git a/README.md b/README.md index 8237601..4739176 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,8 @@ Download: [GeyserOptionalPack.mcpack](https://download.geysermc.org/v2/projects/ ### Manually compiling the pack 1. Clone the repo to your computer -2. Run `gradlew build`. -3. Run the pack compiler using `java -jar build/libs/GeyserOptionalPackCompiler.jar` -4. When it finishes compiling, it will output the `GeyserOptionalPack.mcpack`. +2. Run `gradlew run`. +3. When it finishes compiling, it will output the `GeyserOptionalPack.mcpack`. ### Legal diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 3ae256f..0000000 --- a/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id 'java' - id 'application' -} - -group = 'org.geysermc.optionalpack' -version = '1.0-SNAPSHOT' - -repositories { - mavenCentral() -} - -dependencies { - implementation 'com.google.code.gson:gson:2.13.1' -} - -jar { - archiveFileName = 'GeyserOptionalPackCompiler.jar' - manifest { - attributes 'Main-Class': application.mainClass - } -} - -application { - mainClass = 'org.geysermc.optionalpack.OptionalPack' -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..da3efce --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + java + application +} + +group = "org.geysermc.optionalpack" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.google.code.gson:gson:2.13.1") + implementation("org.reflections:reflections:0.10.2") +} + +tasks { + jar { + archiveFileName = "GeyserOptionalPackCompiler.jar" + manifest.attributes["Main-Class"] = application.mainClass + } +} + +application { + mainClass.set("org.geysermc.optionalpack.OptionalPack") +} diff --git a/developer_documentation.md b/developer_documentation.md index b1a38c4..35c9adf 100644 --- a/developer_documentation.md +++ b/developer_documentation.md @@ -26,6 +26,11 @@ The GeyserOptionalPack is compiled using a program written in Java. It contains Entity data and entity flags (known as queries in Molang) are pieces of metadata that store various pieces of information about an entity on the Bedrock Edition of Minecraft. You can query for an entity's health, for example (a number query or an entity data), and can query for if an entity is angry (an entity flag, which is either 1.0 or 0.0 in Molang). Not all entities use every query, but every entity has access to most queries, though Bedrock by default ignores these. These queries can be sent by Geyser and change how an entity looks. We use this to our advantage in this resource pack. +### Patches +There is a system within the compiler to apply patches to the vanilla Bedrock json files. This is done by placing a `.patch.json` in the `patches` resource folder, with the same path as the file you want to patch. The patch file will be merged with the original file, replacing any existing keys. This is useful for small changes to vanilla files, such as adding an extra texture to an entity. + +The source for these files is https://github.com/Mojang/bedrock-samples/tree/main/resource_pack with each patch being the same name and path as the original file but with `.json` replaced with `.patch.json`. + ### Armor stands #### Part visibility and rotation encoding diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 8daad83..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'GeyserOptionalPackCompiler' - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..435010a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "GeyserOptionalPackCompiler" + diff --git a/src/main/java/org/geysermc/optionalpack/BedrockResourcesWrapper.java b/src/main/java/org/geysermc/optionalpack/BedrockResourcesWrapper.java new file mode 100644 index 0000000..4b361bf --- /dev/null +++ b/src/main/java/org/geysermc/optionalpack/BedrockResourcesWrapper.java @@ -0,0 +1,15 @@ +package org.geysermc.optionalpack; + +import java.io.InputStream; + +public class BedrockResourcesWrapper { + private static final String BEDROCK_RESOURCES_URL = "https://raw.githubusercontent.com/Mojang/bedrock-samples/refs/tags/v" + Constants.BEDROCK_TARGET_VERSION + "/resource_pack/%s"; + + public static String getResourceAsString(String path) { + return WebUtils.getAsString(BEDROCK_RESOURCES_URL.formatted(path)); + } + + public static InputStream getResource(String path) { + return WebUtils.request(BEDROCK_RESOURCES_URL.formatted(path)); + } +} diff --git a/src/main/java/org/geysermc/optionalpack/Constants.java b/src/main/java/org/geysermc/optionalpack/Constants.java new file mode 100644 index 0000000..43d499e --- /dev/null +++ b/src/main/java/org/geysermc/optionalpack/Constants.java @@ -0,0 +1,14 @@ +package org.geysermc.optionalpack; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class Constants { + public static final String JAVA_TARGET_VERSION = "1.21.8"; + public static final String BEDROCK_TARGET_VERSION = "1.21.100.6"; + + public static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .create(); +} diff --git a/src/main/java/org/geysermc/optionalpack/FileUtils.java b/src/main/java/org/geysermc/optionalpack/FileUtils.java new file mode 100644 index 0000000..b67be90 --- /dev/null +++ b/src/main/java/org/geysermc/optionalpack/FileUtils.java @@ -0,0 +1,62 @@ +package org.geysermc.optionalpack; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class FileUtils { + /** + * Delete a directory and all files within it + * From: https://www.geeksforgeeks.org/java/java-program-to-delete-a-directory/ + * + * @param directory The directory to remove + */ + public static void deleteDirectory(File directory) { + File[] files = directory.listFiles(); + if (files != null) { + for (File subfile : directory.listFiles()) { + if (subfile.isDirectory()) { + deleteDirectory(subfile); + } + subfile.delete(); + } + } + + directory.delete(); + } + + /** + * @see #deleteDirectory(File) + */ + public static void deleteDirectory(Path directory) { + deleteDirectory(directory.toFile()); + } + + /** + * Zip a folder + * From: https://stackoverflow.com/a/57997601 + * + * @param sourceFolderPath Folder to zip + * @param zipPath Output path for the zip + */ + public static void zipFolder(Path sourceFolderPath, Path zipPath) throws Exception { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipPath.toFile())); + Files.walkFileTree(sourceFolderPath, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + zos.putNextEntry(new ZipEntry(sourceFolderPath.relativize(file).toString())); + Files.copy(file, zos); + zos.closeEntry(); + return FileVisitResult.CONTINUE; + } + }); + zos.close(); + } +} diff --git a/src/main/java/org/geysermc/optionalpack/JavaResources.java b/src/main/java/org/geysermc/optionalpack/JavaResources.java index ae0ca13..e5787d8 100644 --- a/src/main/java/org/geysermc/optionalpack/JavaResources.java +++ b/src/main/java/org/geysermc/optionalpack/JavaResources.java @@ -27,6 +27,7 @@ package org.geysermc.optionalpack; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; @@ -60,12 +61,15 @@ public class JavaResources { String assetFileName = Path.of(jarAssetPath).toFile().getName(); Path destination = OptionalPack.WORKING_PATH.resolve(destinationPath).resolve(assetFileName); - if (destination.toFile().mkdirs()) { - Files.copy(asset, destination, StandardCopyOption.REPLACE_EXISTING); - } - else { - OptionalPack.log("Could not make directories for copying " + jarAssetPath + " to " + destinationPath + "!"); + File destinationFolder = OptionalPack.WORKING_PATH.resolve(destinationPath).toFile(); + if (!destinationFolder.exists()) { + if (!destinationFolder.mkdirs()) { + OptionalPack.log("Could not make directories for copying " + jarAssetPath + " to " + destinationPath + "!"); + continue; + } } + + Files.copy(asset, destination, StandardCopyOption.REPLACE_EXISTING); } } catch (IOException e) { diff --git a/src/main/java/org/geysermc/optionalpack/LauncherMetaWrapper.java b/src/main/java/org/geysermc/optionalpack/LauncherMetaWrapper.java index 6e295b1..4ce601a 100644 --- a/src/main/java/org/geysermc/optionalpack/LauncherMetaWrapper.java +++ b/src/main/java/org/geysermc/optionalpack/LauncherMetaWrapper.java @@ -1,7 +1,5 @@ package org.geysermc.optionalpack; -import com.google.gson.Gson; - import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -10,24 +8,21 @@ import java.util.List; import java.util.Map; public class LauncherMetaWrapper { - private static final String TARGET_VERSION = "1.21.8"; - private static final Path CLIENT_JAR = OptionalPack.TEMP_PATH.resolve("client.jar"); private static final String LAUNCHER_META_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; - private static final Gson GSON = new Gson(); public static Path getLatest() { - OptionalPack.log("Downloading " + TARGET_VERSION + " client.jar from Mojang..."); + OptionalPack.log("Downloading " + Constants.JAVA_TARGET_VERSION + " client.jar from Mojang..."); - VersionManifest versionManifest = GSON.fromJson(HTTP.getAsString(LAUNCHER_META_URL), VersionManifest.class); + VersionManifest versionManifest = Constants.GSON.fromJson(WebUtils.getAsString(LAUNCHER_META_URL), VersionManifest.class); for (Version version : versionManifest.versions()) { - if (version.id().equals(TARGET_VERSION)) { - VersionInfo versionInfo = GSON.fromJson(HTTP.getAsString(version.url()), VersionInfo.class); + if (version.id().equals(Constants.JAVA_TARGET_VERSION)) { + VersionInfo versionInfo = Constants.GSON.fromJson(WebUtils.getAsString(version.url()), VersionInfo.class); VersionDownload client = versionInfo.downloads().get("client"); if (!Files.exists(CLIENT_JAR) || !client.sha1.equals(getSha1(CLIENT_JAR))) { // Download the client jar - try (InputStream in = HTTP.request(client.url())) { + try (InputStream in = WebUtils.request(client.url())) { Files.copy(in, CLIENT_JAR); } catch (Exception e) { throw new RuntimeException("Could not download client jar", e); @@ -62,8 +57,6 @@ public class LauncherMetaWrapper { } } - - public record VersionManifest( LatestVersion latest, List versions diff --git a/src/main/java/org/geysermc/optionalpack/OptionalPack.java b/src/main/java/org/geysermc/optionalpack/OptionalPack.java index 03ab539..20a5c02 100644 --- a/src/main/java/org/geysermc/optionalpack/OptionalPack.java +++ b/src/main/java/org/geysermc/optionalpack/OptionalPack.java @@ -27,6 +27,7 @@ package org.geysermc.optionalpack; import org.geysermc.optionalpack.renderers.Renderer; import org.geysermc.optionalpack.renderers.SweepAttackRenderer; +import org.reflections.Reflections; import javax.imageio.ImageIO; import java.io.*; @@ -38,6 +39,8 @@ import java.text.DecimalFormat; import java.time.Duration; import java.time.Instant; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; @@ -47,13 +50,20 @@ public class OptionalPack { public static final Path TEMP_PATH = Path.of("temp"); public static final Path WORKING_PATH = TEMP_PATH.resolve("optionalpack"); - /* - List of renderers that will be used to convert sprites for the resource pack. - They are executed in order from start to end. - */ - private static List renderers = List.of( - new SweepAttackRenderer() - ); + private static final Renderer[] RENDERERS; + + static { + Reflections reflections = new Reflections("org.geysermc.optionalpack.renderers"); + Set> renderers = reflections.getSubTypesOf(Renderer.class); + + RENDERERS = renderers.stream().map(rendererClass -> { + try { + return rendererClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + return null; + } + }).filter(Objects::nonNull).toArray(Renderer[]::new); + } public static void main(String[] args) { Instant start = Instant.now(); @@ -64,7 +74,7 @@ public class OptionalPack { log("Extracting pre-made optional pack data to folder..."); // there are probably better ways to do this, but this is the way im doing it - unzipPack(Resources.get("optionalpack"), WORKING_PATH); + Resources.extractFolder("optionalpack", WORKING_PATH); // Step 2: Download the 1.21.8 client.jar and copy all files needed to working folder File jarFile = LauncherMetaWrapper.getLatest().toFile(); @@ -73,22 +83,25 @@ public class OptionalPack { JavaResources.extract(clientJar); /* Step 3: Rendering sprites in a format that we use in the resource pack */ - for (Renderer renderer : renderers) { + for (Renderer renderer : RENDERERS) { log("Rendering " + renderer.getName() + "..."); - File imageFile = WORKING_PATH.resolve(renderer.getDestination()).toFile(); - if (imageFile.mkdirs()) { - ImageIO.write(renderer.render(), "PNG", imageFile); + File destinationFolder = renderer.getDestinationPath().toFile().getParentFile(); + if (!destinationFolder.exists()) { + if (!destinationFolder.mkdirs()) { + throw new IOException("Failed to create directory: " + destinationFolder); + } } + renderer.render(); } // Step 4: Compile pack folder into a mcpack. log("Zipping as GeyserOptionalPack.mcpack..."); - zipFolder(WORKING_PATH, Path.of("GeyserOptionalPack.mcpack")); + FileUtils.zipFolder(WORKING_PATH, Path.of("GeyserOptionalPack.mcpack")); // Step 5: Cleanup temporary folders and files log("Clearing temporary files..."); clientJar.close(); - deleteDirectory(WORKING_PATH.toFile()); + FileUtils.deleteDirectory(WORKING_PATH); // Step 6: Finish!! DecimalFormat r3 = new DecimalFormat("0.000"); @@ -100,106 +113,6 @@ public class OptionalPack { } } - /** - * Delete a directory and all files within it - * From: https://www.geeksforgeeks.org/java/java-program-to-delete-a-directory/ - * - * @param directory The directory to remove - */ - public static void deleteDirectory(File directory) { - File[] files = directory.listFiles(); - if (files != null) { - for (File subfile : directory.listFiles()) { - if (subfile.isDirectory()) { - deleteDirectory(subfile); - } - subfile.delete(); - } - } - - directory.delete(); - } - - /** - * Zip a folder - * From: https://stackoverflow.com/a/57997601 - * - * @param sourceFolderPath Folder to zip - * @param zipPath Output path for the zip - */ - private static void zipFolder(Path sourceFolderPath, Path zipPath) throws Exception { - ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipPath.toFile())); - Files.walkFileTree(sourceFolderPath, new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - zos.putNextEntry(new ZipEntry(sourceFolderPath.relativize(file).toString())); - Files.copy(file, zos); - zos.closeEntry(); - return FileVisitResult.CONTINUE; - } - }); - zos.close(); - } - - /** - * Extract a zip to a given directory - * - * @param file The zip to extract - * @param destDir THe destination to put all the files - */ - private static void unzipPack(URL file, Path destDir) { - File dir = destDir.toFile(); - // create output directory if it doesn't exist - if (!dir.exists()) dir.mkdirs(); - - try { - if (file.getProtocol().equals("file")) { - Path resourceDir = Paths.get(file.toURI()); - Files.walk(resourceDir) - .filter(Files::isRegularFile) - .forEach(source -> { - try { - Path relative = resourceDir.relativize(source); - Path target = destDir.resolve(relative); - Files.createDirectories(target.getParent()); - Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } else { - byte[] buffer = new byte[1024]; - FileInputStream fileStream = new FileInputStream(new File(file.toURI())); - ZipInputStream zipStream = new ZipInputStream(fileStream); - ZipEntry entry = zipStream.getNextEntry(); - while (entry != null) { - if (!entry.isDirectory()) { - String fileName = entry.getName(); - File newFile = new File(destDir + File.separator + fileName); - // create directories for subdirectories in zip - new File(newFile.getParent()).mkdirs(); - FileOutputStream extractedFile = new FileOutputStream(newFile); - int len; - while ((len = zipStream.read(buffer)) > 0) { - extractedFile.write(buffer, 0, len); - } - extractedFile.close(); - } - // close this ZipEntry - - zipStream.closeEntry(); - entry = zipStream.getNextEntry(); - } - // close the last ZipEntry - zipStream.closeEntry(); - zipStream.close(); - fileStream.close(); - } - } catch (IOException | URISyntaxException e) { - throw new RuntimeException("Unable to unzip pack!", e); - } - } - /** * Prints a message to the console. * diff --git a/src/main/java/org/geysermc/optionalpack/Resources.java b/src/main/java/org/geysermc/optionalpack/Resources.java index f364e4c..d5c99f4 100644 --- a/src/main/java/org/geysermc/optionalpack/Resources.java +++ b/src/main/java/org/geysermc/optionalpack/Resources.java @@ -27,10 +27,21 @@ package org.geysermc.optionalpack; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class Resources { /** @@ -89,4 +100,64 @@ public class Resources { is.close(); return image; } + + /** + * Extract a resource folder to a given directory + * + * @param folder The resource folder to extract + * @param destDir The destination to put all the files + */ + public static void extractFolder(String folder, Path destDir) { + URL file = get(folder); + File dir = destDir.toFile(); + // create output directory if it doesn't exist + if (!dir.exists()) dir.mkdirs(); + + try { + if (file.getProtocol().equals("file")) { + Path resourceDir = Paths.get(file.toURI()); + Files.walk(resourceDir) + .filter(Files::isRegularFile) + .forEach(source -> { + try { + Path relative = resourceDir.relativize(source); + Path target = destDir.resolve(relative); + Files.createDirectories(target.getParent()); + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } else { + byte[] buffer = new byte[1024]; + FileInputStream fileStream = new FileInputStream(new File(file.toURI())); + ZipInputStream zipStream = new ZipInputStream(fileStream); + ZipEntry entry = zipStream.getNextEntry(); + while (entry != null) { + if (!entry.isDirectory()) { + String fileName = entry.getName(); + File newFile = new File(destDir + File.separator + fileName); + // create directories for subdirectories in zip + new File(newFile.getParent()).mkdirs(); + FileOutputStream extractedFile = new FileOutputStream(newFile); + int len; + while ((len = zipStream.read(buffer)) > 0) { + extractedFile.write(buffer, 0, len); + } + extractedFile.close(); + } + // close this ZipEntry + + zipStream.closeEntry(); + entry = zipStream.getNextEntry(); + } + // close the last ZipEntry + zipStream.closeEntry(); + zipStream.close(); + fileStream.close(); + } + } catch (IOException | URISyntaxException e) { + throw new RuntimeException("Unable to unzip pack!", e); + } + } } diff --git a/src/main/java/org/geysermc/optionalpack/HTTP.java b/src/main/java/org/geysermc/optionalpack/WebUtils.java similarity index 99% rename from src/main/java/org/geysermc/optionalpack/HTTP.java rename to src/main/java/org/geysermc/optionalpack/WebUtils.java index 62d5138..1d6065e 100644 --- a/src/main/java/org/geysermc/optionalpack/HTTP.java +++ b/src/main/java/org/geysermc/optionalpack/WebUtils.java @@ -30,7 +30,7 @@ import java.io.InputStream; import java.net.*; import java.nio.charset.StandardCharsets; -public class HTTP { +public class WebUtils { /** * Requests a URL and returns the InputStream. diff --git a/src/main/java/org/geysermc/optionalpack/renderers/JsonPatchRenderer.java b/src/main/java/org/geysermc/optionalpack/renderers/JsonPatchRenderer.java new file mode 100644 index 0000000..fa47e66 --- /dev/null +++ b/src/main/java/org/geysermc/optionalpack/renderers/JsonPatchRenderer.java @@ -0,0 +1,93 @@ +package org.geysermc.optionalpack.renderers; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.geysermc.optionalpack.BedrockResourcesWrapper; +import org.geysermc.optionalpack.Constants; +import org.geysermc.optionalpack.FileUtils; +import org.geysermc.optionalpack.OptionalPack; +import org.geysermc.optionalpack.Resources; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class JsonPatchRenderer implements Renderer { + private static final Path PATCHES_PATH = OptionalPack.TEMP_PATH.resolve("patches"); + + @Override + public String getName() { + return "Json Patcher"; + } + + @Override + public String getDestination() { + return ""; + } + + @Override + public void render() throws IOException { + log("Extracting JSON patches..."); + Resources.extractFolder("patches", PATCHES_PATH); + + log("Patching JSON files..."); + try (var stream = Files.walk(PATCHES_PATH)) { + for (var path : stream.filter(Files::isRegularFile).toList()) { + String patchFile = PATCHES_PATH.relativize(path).toString().replace("\\", "/"); + + log("Applying patch: " + patchFile); + patchJsonFile(patchFile); + } + } + + // Clean up patches folder + FileUtils.deleteDirectory(PATCHES_PATH); + } + + private void patchJsonFile(String patchFile) throws IOException { + String realPath = patchFile.replace(".patch.json", ".json"); + + JsonObject sourceJson = JsonParser.parseString(BedrockResourcesWrapper.getResourceAsString(realPath)).getAsJsonObject(); + JsonObject patchJson = JsonParser.parseString(Files.readString(PATCHES_PATH.resolve(patchFile), StandardCharsets.UTF_8)).getAsJsonObject(); + + JsonObject merged = mergeJsonObjects(sourceJson, patchJson); + + try (FileWriter writer = new FileWriter(OptionalPack.WORKING_PATH.resolve(realPath).toFile())) { + writer.write(Constants.GSON.toJson(merged)); + } + } + + /** + * Merges two JsonObjects. In case of conflicts, values from obj2 take precedence. + * If both values are JsonObjects, they are merged recursively. + * + * @param obj1 The first JsonObject + * @param obj2 The second JsonObject + * @return The merged JsonObject + */ + private static JsonObject mergeJsonObjects(JsonObject obj1, JsonObject obj2) { + JsonObject merged = obj1.deepCopy(); // Start with a copy of the first + + for (String key : obj2.keySet()) { + JsonElement value2 = obj2.get(key); + if (merged.has(key)) { + JsonElement value1 = merged.get(key); + + // If both are JsonObjects, recursively merge + if (value1.isJsonObject() && value2.isJsonObject()) { + merged.add(key, mergeJsonObjects(value1.getAsJsonObject(), value2.getAsJsonObject())); + } else { + // Override with value from obj2 + merged.add(key, value2); + } + } else { + merged.add(key, value2); + } + } + + return merged; + } +} diff --git a/src/main/java/org/geysermc/optionalpack/renderers/Renderer.java b/src/main/java/org/geysermc/optionalpack/renderers/Renderer.java index 25e4857..c430fd4 100644 --- a/src/main/java/org/geysermc/optionalpack/renderers/Renderer.java +++ b/src/main/java/org/geysermc/optionalpack/renderers/Renderer.java @@ -25,8 +25,11 @@ package org.geysermc.optionalpack.renderers; +import org.geysermc.optionalpack.OptionalPack; + import java.awt.image.BufferedImage; import java.io.IOException; +import java.nio.file.Path; public interface Renderer { /** @@ -46,10 +49,23 @@ public interface Renderer { } /** - * Draws the image as a BufferedImage. + * Gets the destination path as a Path object, or null if no destination is set. + * + * @return The destination path + */ + default Path getDestinationPath() { + String destination = getDestination(); + return OptionalPack.WORKING_PATH.resolve(destination); + } + + /** + * Renders the output of the renderer. * - * @return The rendered output as a BufferedImage. * @throws IOException If an error occurs during rendering. */ - BufferedImage render() throws IOException; + void render() throws IOException; + + default void log(String message) { + OptionalPack.log(message); + } } diff --git a/src/main/java/org/geysermc/optionalpack/renderers/VerticalSpriteSheetRenderer.java b/src/main/java/org/geysermc/optionalpack/renderers/VerticalSpriteSheetRenderer.java index 670321f..ab383e9 100644 --- a/src/main/java/org/geysermc/optionalpack/renderers/VerticalSpriteSheetRenderer.java +++ b/src/main/java/org/geysermc/optionalpack/renderers/VerticalSpriteSheetRenderer.java @@ -27,7 +27,9 @@ package org.geysermc.optionalpack.renderers; import org.geysermc.optionalpack.JavaResources; +import javax.imageio.ImageIO; import java.awt.image.BufferedImage; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -58,7 +60,7 @@ public class VerticalSpriteSheetRenderer implements Renderer { } @Override - public BufferedImage render() throws IOException { + public void render() throws IOException { List sprites = new ArrayList<>(); for (String path : spritePaths) { // Retrieve the image from the client jar @@ -72,6 +74,7 @@ public class VerticalSpriteSheetRenderer implements Renderer { BufferedImage sprite = sprites.get(i); canvas.getGraphics().drawImage(sprite, 0, i * sprite.getHeight(), null); } - return canvas; + + ImageIO.write(canvas, "PNG", getDestinationPath().toFile()); } } diff --git a/src/main/resources/optionalpack/entity/arrow.entity.json b/src/main/resources/optionalpack/entity/arrow.entity.json deleted file mode 100644 index 04cc161..0000000 --- a/src/main/resources/optionalpack/entity/arrow.entity.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "format_version": "1.10.0", - "minecraft:client_entity": { - "description": { - "identifier": "minecraft:arrow", - "materials": { - "default": "arrow" - }, - "textures": { - "default": "textures/entity/arrows", - "spectral": "textures/geyser/entity/arrow/spectral_arrow" - }, - "geometry": { - "default": "geometry.arrow" - }, - "animations": { - "move": "animation.arrow.move" - }, - "scripts": { - "pre_animation": [ - "variable.shake = query.shake_time - query.frame_alpha;", - "variable.shake_power = variable.shake > 0.0 ? -Math.sin(variable.shake * 200.0) * variable.shake : 0.0;" - ], - "animate": [ - "move" - ] - }, - "render_controllers": [ "controller.render.arrow" ] - } - } -} diff --git a/src/main/resources/optionalpack/entity/evocation_illager.entity.json b/src/main/resources/optionalpack/entity/evocation_illager.entity.json deleted file mode 100644 index 68a0f9f..0000000 --- a/src/main/resources/optionalpack/entity/evocation_illager.entity.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "format_version": "1.10.0", - "minecraft:client_entity": { - "description": { - "identifier": "minecraft:evocation_illager", - "min_engine_version": "1.8.0", - "materials": { - "default": "evoker" - }, - "textures": { - "default": "textures/entity/illager/evoker", - "illusioner": "textures/geyser/entity/illager/illusioner" - }, - "geometry": { - "default": "geometry.evoker.v1.8" - }, - "scripts": { - "scale": "0.9375", - "animate": [ - "controller_general", - "controller_move" - ] - }, - "animations": { - "general": "animation.evoker.general", - "casting": "animation.evoker.casting", - "look_at_target": "animation.common.look_at_target", - "move": "animation.villager.move", - "celebrating": "animation.humanoid.celebrating", - "controller_general": "controller.animation.evoker.general", - "controller_move": "controller.animation.villager.move" - }, - "particle_effects": { - "spell": "minecraft:evoker_spell" - }, - "render_controllers": [ "controller.render.evoker" ], - "spawn_egg": { - "texture": "spawn_egg", - "texture_index": 40 - } - } - } -} diff --git a/src/main/resources/optionalpack/entity/rabbit.entity.json b/src/main/resources/optionalpack/entity/rabbit.entity.json deleted file mode 100644 index 6c07624..0000000 --- a/src/main/resources/optionalpack/entity/rabbit.entity.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "format_version": "1.8.0", - "minecraft:client_entity": { - "description": { - "identifier": "minecraft:rabbit", - "min_engine_version": "1.8.0", - "materials": { "default": "rabbit" }, - "textures": { - "brown": "textures/entity/rabbit/brown", - "white": "textures/entity/rabbit/white", - "black": "textures/entity/rabbit/blackrabbit", - "white_splotched": "textures/entity/rabbit/white_splotched", - "gold": "textures/entity/rabbit/gold", - "salt": "textures/entity/rabbit/salt", - "toast": "textures/entity/rabbit/toast", - "caerbannog": "textures/geyser/entity/rabbit/caerbannog" - }, - "geometry": { - "default": "geometry.rabbit.v1.8" - }, - "animations": { - "move": "animation.rabbit.move", - "baby_transform": "animation.rabbit.baby_transform", - "look_at_target": "animation.common.look_at_target" - }, - "animation_controllers": [ - { "general": "controller.animation.rabbit.general" }, - { "baby": "controller.animation.rabbit.baby" } - ], - "render_controllers": [ "controller.render.rabbit" ], - "spawn_egg": { - "texture": "spawn_egg", - "texture_index": 24 - } - } - } -} \ No newline at end of file diff --git a/src/main/resources/optionalpack/manifest.json b/src/main/resources/optionalpack/manifest.json index 25ecc92..58e4bdd 100644 --- a/src/main/resources/optionalpack/manifest.json +++ b/src/main/resources/optionalpack/manifest.json @@ -4,7 +4,7 @@ "description": "Optional Bedrock resource pack to extend Geyser functionality", "name": "GeyserOptionalPack", "uuid": "e5f5c938-a701-11eb-b2a3-047d7bb283ba", - "version": [1, 0, 13], + "version": [1, 0, 14], "min_engine_version": [ 1, 16, 0 ] }, "modules": [ @@ -12,7 +12,7 @@ "description": "GeyserOptionalPack", "type": "resources", "uuid": "eebb4ea8-a701-11eb-95ba-047d7bb283ba", - "version": [1, 0, 13] + "version": [1, 0, 14] } ] } diff --git a/src/main/resources/optionalpack/particles/basic_bubble_manual.json b/src/main/resources/optionalpack/particles/basic_bubble_manual.json deleted file mode 100644 index 2ad0aed..0000000 --- a/src/main/resources/optionalpack/particles/basic_bubble_manual.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "format_version": "1.10.0", - "particle_effect": { - "description": { - "identifier": "minecraft:basic_bubble_particle_manual", - "basic_render_parameters": { - "material": "particles_alpha", - "texture": "textures/particle/particles" - } - }, - "components": { - "minecraft:emitter_rate_manual": { - "max_particles": 100 - }, - "minecraft:emitter_lifetime_expression": { - "activation_expression": 1, - "expiration_expression": 0 - }, - "minecraft:emitter_shape_point": { - }, - "minecraft:particle_lifetime_expression": { - "max_lifetime": "2 / ((Math.Random(0.0, 1.0) * 0.8 + 0.2) * 5)" - }, - "minecraft:particle_expire_if_not_in_blocks": [], - "minecraft:particle_motion_dynamic": { - "linear_acceleration": [ 0, 0.8, 0 ], - "linear_drag_coefficient": 5.25 - }, - "minecraft:particle_appearance_billboard": { - "size": [ "(0.05*variable.particle_random_1+0.1)*(variable.particle_random_2*0.9+0.2)", "(0.05*variable.particle_random_1+0.1)*(variable.particle_random_2*0.9+0.2)" ], - "facing_camera_mode": "lookat_xyz", - "uv": { - "texture_width": 128, - "texture_height": 128, - "uv": [ 0, 16 ], - "uv_size": [ 8, 8 ] - } - }, - "minecraft:particle_appearance_lighting": {} - } - } -} - diff --git a/src/main/resources/optionalpack/render_controllers/arrow.render_controllers.json b/src/main/resources/optionalpack/render_controllers/arrow.render_controllers.json deleted file mode 100644 index a8c2e44..0000000 --- a/src/main/resources/optionalpack/render_controllers/arrow.render_controllers.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "format_version": "1.10.0", - "render_controllers": { - "controller.render.arrow": { - "geometry": "geometry.default", - "materials": [ { "*": "Material.default" } ], - "textures": [ - "q.is_bribed ? texture.spectral : texture.default" - ], - "filter_lighting": true - } - } -} diff --git a/src/main/resources/optionalpack/ui/hud_screen.json b/src/main/resources/optionalpack/ui/hud_screen.json index ee2d7a0..9b26be5 100644 --- a/src/main/resources/optionalpack/ui/hud_screen.json +++ b/src/main/resources/optionalpack/ui/hud_screen.json @@ -8,7 +8,7 @@ }, { "binding_type": "view", - "source_property_name": "((#text - ' ') = #text)", + "source_property_name": "((('%02s' * #text) = #text) or ((#text - ' ') = #text))", "target_property_name": "#visible" } ] diff --git a/src/main/resources/patches/entity/arrow.entity.patch.json b/src/main/resources/patches/entity/arrow.entity.patch.json new file mode 100644 index 0000000..ae1398d --- /dev/null +++ b/src/main/resources/patches/entity/arrow.entity.patch.json @@ -0,0 +1,9 @@ +{ + "minecraft:client_entity": { + "description": { + "textures": { + "spectral": "textures/geyser/entity/arrow/spectral_arrow" + } + } + } +} diff --git a/src/main/resources/patches/entity/evocation_illager.entity.patch.json b/src/main/resources/patches/entity/evocation_illager.entity.patch.json new file mode 100644 index 0000000..3406854 --- /dev/null +++ b/src/main/resources/patches/entity/evocation_illager.entity.patch.json @@ -0,0 +1,9 @@ +{ + "minecraft:client_entity": { + "description": { + "textures": { + "illusioner": "textures/geyser/entity/illager/illusioner" + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/patches/entity/rabbit.entity.patch.json b/src/main/resources/patches/entity/rabbit.entity.patch.json new file mode 100644 index 0000000..5144150 --- /dev/null +++ b/src/main/resources/patches/entity/rabbit.entity.patch.json @@ -0,0 +1,9 @@ +{ + "minecraft:client_entity": { + "description": { + "textures": { + "caerbannog": "textures/geyser/entity/rabbit/caerbannog" + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/patches/particles/basic_bubble_manual.patch.json b/src/main/resources/patches/particles/basic_bubble_manual.patch.json new file mode 100644 index 0000000..7cd6360 --- /dev/null +++ b/src/main/resources/patches/particles/basic_bubble_manual.patch.json @@ -0,0 +1,7 @@ +{ + "particle_effect": { + "components": { + "minecraft:particle_expire_if_not_in_blocks": [] + } + } +} \ No newline at end of file diff --git a/src/main/resources/patches/particles/trial_spawner_detection.particle.patch.json b/src/main/resources/patches/particles/trial_spawner_detection.particle.patch.json new file mode 100644 index 0000000..b255c6d --- /dev/null +++ b/src/main/resources/patches/particles/trial_spawner_detection.particle.patch.json @@ -0,0 +1,16 @@ +{ + "particle_effect": { + "components": { + "minecraft:emitter_rate_instant": { + "num_particles": "1" + }, + "minecraft:emitter_shape_box": { + "offset": [ + 0, + 0, + 0 + ] + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/patches/particles/trial_spawner_detection_ominous.particle.patch.json b/src/main/resources/patches/particles/trial_spawner_detection_ominous.particle.patch.json new file mode 100644 index 0000000..b255c6d --- /dev/null +++ b/src/main/resources/patches/particles/trial_spawner_detection_ominous.particle.patch.json @@ -0,0 +1,16 @@ +{ + "particle_effect": { + "components": { + "minecraft:emitter_rate_instant": { + "num_particles": "1" + }, + "minecraft:emitter_shape_box": { + "offset": [ + 0, + 0, + 0 + ] + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/patches/render_controllers/arrow.render_controllers.patch.json b/src/main/resources/patches/render_controllers/arrow.render_controllers.patch.json new file mode 100644 index 0000000..c191494 --- /dev/null +++ b/src/main/resources/patches/render_controllers/arrow.render_controllers.patch.json @@ -0,0 +1,9 @@ +{ + "render_controllers": { + "controller.render.arrow": { + "textures": [ + "q.is_bribed ? texture.spectral : texture.default" + ] + } + } +} \ No newline at end of file