1
0
mirror of https://github.com/GeyserMC/GeyserOptionalPack.git synced 2025-12-19 14:59:14 +00:00

Add vanilla json patching system (#68)

* Add json patching renderer

* Make minecraft:trial_spawner_detection particle have parity

* Oops meant to keep this

* Move Arrow, Rabbit and Illusioner entity to new json patches

* Move to .patch.json system

* Bump pack version

* Update title visibility condition to only hide blank text

* Use nicer json format

* Change to .kts, fix trial_spawner_detection_ominous particle

---------

Co-authored-by: chris <github@onechris.mozmail.com>
This commit is contained in:
rtm516
2025-09-17 13:52:59 +01:00
committed by GitHub
parent 730cf0cbc9
commit 37ac51459d
31 changed files with 435 additions and 338 deletions

View File

@@ -23,9 +23,8 @@ Download: [GeyserOptionalPack.mcpack](https://download.geysermc.org/v2/projects/
### Manually compiling the pack ### Manually compiling the pack
1. Clone the repo to your computer 1. Clone the repo to your computer
2. Run `gradlew build`. 2. Run `gradlew run`.
3. Run the pack compiler using `java -jar build/libs/GeyserOptionalPackCompiler.jar` 3. When it finishes compiling, it will output the `GeyserOptionalPack.mcpack`.
4. When it finishes compiling, it will output the `GeyserOptionalPack.mcpack`.
### Legal ### Legal

View File

@@ -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'
}

27
build.gradle.kts Normal file
View File

@@ -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")
}

View File

@@ -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. 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 ### Armor stands
#### Part visibility and rotation encoding #### Part visibility and rotation encoding

View File

@@ -1,2 +0,0 @@
rootProject.name = 'GeyserOptionalPackCompiler'

2
settings.gradle.kts Normal file
View File

@@ -0,0 +1,2 @@
rootProject.name = "GeyserOptionalPackCompiler"

View File

@@ -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));
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -27,6 +27,7 @@ package org.geysermc.optionalpack;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@@ -60,12 +61,15 @@ public class JavaResources {
String assetFileName = Path.of(jarAssetPath).toFile().getName(); String assetFileName = Path.of(jarAssetPath).toFile().getName();
Path destination = OptionalPack.WORKING_PATH.resolve(destinationPath).resolve(assetFileName); Path destination = OptionalPack.WORKING_PATH.resolve(destinationPath).resolve(assetFileName);
if (destination.toFile().mkdirs()) { File destinationFolder = OptionalPack.WORKING_PATH.resolve(destinationPath).toFile();
Files.copy(asset, destination, StandardCopyOption.REPLACE_EXISTING); if (!destinationFolder.exists()) {
} if (!destinationFolder.mkdirs()) {
else { OptionalPack.log("Could not make directories for copying " + jarAssetPath + " to " + destinationPath + "!");
OptionalPack.log("Could not make directories for copying " + jarAssetPath + " to " + destinationPath + "!"); continue;
}
} }
Files.copy(asset, destination, StandardCopyOption.REPLACE_EXISTING);
} }
} catch (IOException e) { } catch (IOException e) {

View File

@@ -1,7 +1,5 @@
package org.geysermc.optionalpack; package org.geysermc.optionalpack;
import com.google.gson.Gson;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -10,24 +8,21 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class LauncherMetaWrapper { 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 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 String LAUNCHER_META_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
private static final Gson GSON = new Gson();
public static Path getLatest() { 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()) { for (Version version : versionManifest.versions()) {
if (version.id().equals(TARGET_VERSION)) { if (version.id().equals(Constants.JAVA_TARGET_VERSION)) {
VersionInfo versionInfo = GSON.fromJson(HTTP.getAsString(version.url()), VersionInfo.class); VersionInfo versionInfo = Constants.GSON.fromJson(WebUtils.getAsString(version.url()), VersionInfo.class);
VersionDownload client = versionInfo.downloads().get("client"); VersionDownload client = versionInfo.downloads().get("client");
if (!Files.exists(CLIENT_JAR) || !client.sha1.equals(getSha1(CLIENT_JAR))) { if (!Files.exists(CLIENT_JAR) || !client.sha1.equals(getSha1(CLIENT_JAR))) {
// Download the 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); Files.copy(in, CLIENT_JAR);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Could not download client jar", e); throw new RuntimeException("Could not download client jar", e);
@@ -62,8 +57,6 @@ public class LauncherMetaWrapper {
} }
} }
public record VersionManifest( public record VersionManifest(
LatestVersion latest, LatestVersion latest,
List<Version> versions List<Version> versions

View File

@@ -27,6 +27,7 @@ package org.geysermc.optionalpack;
import org.geysermc.optionalpack.renderers.Renderer; import org.geysermc.optionalpack.renderers.Renderer;
import org.geysermc.optionalpack.renderers.SweepAttackRenderer; import org.geysermc.optionalpack.renderers.SweepAttackRenderer;
import org.reflections.Reflections;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.io.*; import java.io.*;
@@ -38,6 +39,8 @@ import java.text.DecimalFormat;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream; 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 TEMP_PATH = Path.of("temp");
public static final Path WORKING_PATH = TEMP_PATH.resolve("optionalpack"); public static final Path WORKING_PATH = TEMP_PATH.resolve("optionalpack");
/* private static final Renderer[] RENDERERS;
List of renderers that will be used to convert sprites for the resource pack.
They are executed in order from start to end. static {
*/ Reflections reflections = new Reflections("org.geysermc.optionalpack.renderers");
private static List<Renderer> renderers = List.of( Set<Class<? extends Renderer>> renderers = reflections.getSubTypesOf(Renderer.class);
new SweepAttackRenderer()
); 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) { public static void main(String[] args) {
Instant start = Instant.now(); Instant start = Instant.now();
@@ -64,7 +74,7 @@ public class OptionalPack {
log("Extracting pre-made optional pack data to folder..."); 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 // 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 // Step 2: Download the 1.21.8 client.jar and copy all files needed to working folder
File jarFile = LauncherMetaWrapper.getLatest().toFile(); File jarFile = LauncherMetaWrapper.getLatest().toFile();
@@ -73,22 +83,25 @@ public class OptionalPack {
JavaResources.extract(clientJar); JavaResources.extract(clientJar);
/* Step 3: Rendering sprites in a format that we use in the resource pack */ /* 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() + "..."); log("Rendering " + renderer.getName() + "...");
File imageFile = WORKING_PATH.resolve(renderer.getDestination()).toFile(); File destinationFolder = renderer.getDestinationPath().toFile().getParentFile();
if (imageFile.mkdirs()) { if (!destinationFolder.exists()) {
ImageIO.write(renderer.render(), "PNG", imageFile); if (!destinationFolder.mkdirs()) {
throw new IOException("Failed to create directory: " + destinationFolder);
}
} }
renderer.render();
} }
// Step 4: Compile pack folder into a mcpack. // Step 4: Compile pack folder into a mcpack.
log("Zipping as GeyserOptionalPack.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 // Step 5: Cleanup temporary folders and files
log("Clearing temporary files..."); log("Clearing temporary files...");
clientJar.close(); clientJar.close();
deleteDirectory(WORKING_PATH.toFile()); FileUtils.deleteDirectory(WORKING_PATH);
// Step 6: Finish!! // Step 6: Finish!!
DecimalFormat r3 = new DecimalFormat("0.000"); 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. * Prints a message to the console.
* *

View File

@@ -27,10 +27,21 @@ package org.geysermc.optionalpack;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.nio.charset.Charset; 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 { public class Resources {
/** /**
@@ -89,4 +100,64 @@ public class Resources {
is.close(); is.close();
return image; 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);
}
}
} }

View File

@@ -30,7 +30,7 @@ import java.io.InputStream;
import java.net.*; import java.net.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public class HTTP { public class WebUtils {
/** /**
* Requests a URL and returns the InputStream. * Requests a URL and returns the InputStream.

View File

@@ -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;
}
}

View File

@@ -25,8 +25,11 @@
package org.geysermc.optionalpack.renderers; package org.geysermc.optionalpack.renderers;
import org.geysermc.optionalpack.OptionalPack;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
public interface Renderer { 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. * @throws IOException If an error occurs during rendering.
*/ */
BufferedImage render() throws IOException; void render() throws IOException;
default void log(String message) {
OptionalPack.log(message);
}
} }

View File

@@ -27,7 +27,9 @@ package org.geysermc.optionalpack.renderers;
import org.geysermc.optionalpack.JavaResources; import org.geysermc.optionalpack.JavaResources;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -58,7 +60,7 @@ public class VerticalSpriteSheetRenderer implements Renderer {
} }
@Override @Override
public BufferedImage render() throws IOException { public void render() throws IOException {
List<BufferedImage> sprites = new ArrayList<>(); List<BufferedImage> sprites = new ArrayList<>();
for (String path : spritePaths) { for (String path : spritePaths) {
// Retrieve the image from the client jar // Retrieve the image from the client jar
@@ -72,6 +74,7 @@ public class VerticalSpriteSheetRenderer implements Renderer {
BufferedImage sprite = sprites.get(i); BufferedImage sprite = sprites.get(i);
canvas.getGraphics().drawImage(sprite, 0, i * sprite.getHeight(), null); canvas.getGraphics().drawImage(sprite, 0, i * sprite.getHeight(), null);
} }
return canvas;
ImageIO.write(canvas, "PNG", getDestinationPath().toFile());
} }
} }

View File

@@ -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" ]
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -4,7 +4,7 @@
"description": "Optional Bedrock resource pack to extend Geyser functionality", "description": "Optional Bedrock resource pack to extend Geyser functionality",
"name": "GeyserOptionalPack", "name": "GeyserOptionalPack",
"uuid": "e5f5c938-a701-11eb-b2a3-047d7bb283ba", "uuid": "e5f5c938-a701-11eb-b2a3-047d7bb283ba",
"version": [1, 0, 13], "version": [1, 0, 14],
"min_engine_version": [ 1, 16, 0 ] "min_engine_version": [ 1, 16, 0 ]
}, },
"modules": [ "modules": [
@@ -12,7 +12,7 @@
"description": "GeyserOptionalPack", "description": "GeyserOptionalPack",
"type": "resources", "type": "resources",
"uuid": "eebb4ea8-a701-11eb-95ba-047d7bb283ba", "uuid": "eebb4ea8-a701-11eb-95ba-047d7bb283ba",
"version": [1, 0, 13] "version": [1, 0, 14]
} }
] ]
} }

View File

@@ -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": {}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -8,7 +8,7 @@
}, },
{ {
"binding_type": "view", "binding_type": "view",
"source_property_name": "((#text - ' ') = #text)", "source_property_name": "((('%02s' * #text) = #text) or ((#text - ' ') = #text))",
"target_property_name": "#visible" "target_property_name": "#visible"
} }
] ]

View File

@@ -0,0 +1,9 @@
{
"minecraft:client_entity": {
"description": {
"textures": {
"spectral": "textures/geyser/entity/arrow/spectral_arrow"
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"minecraft:client_entity": {
"description": {
"textures": {
"illusioner": "textures/geyser/entity/illager/illusioner"
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"minecraft:client_entity": {
"description": {
"textures": {
"caerbannog": "textures/geyser/entity/rabbit/caerbannog"
}
}
}
}

View File

@@ -0,0 +1,7 @@
{
"particle_effect": {
"components": {
"minecraft:particle_expire_if_not_in_blocks": []
}
}
}

View File

@@ -0,0 +1,16 @@
{
"particle_effect": {
"components": {
"minecraft:emitter_rate_instant": {
"num_particles": "1"
},
"minecraft:emitter_shape_box": {
"offset": [
0,
0,
0
]
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"particle_effect": {
"components": {
"minecraft:emitter_rate_instant": {
"num_particles": "1"
},
"minecraft:emitter_shape_box": {
"offset": [
0,
0,
0
]
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"render_controllers": {
"controller.render.arrow": {
"textures": [
"q.is_bribed ? texture.spectral : texture.default"
]
}
}
}