diff --git a/Update Process.md b/Update Process.md
index 29e060fca..7ae6ac535 100644
--- a/Update Process.md
+++ b/Update Process.md
@@ -92,6 +92,9 @@ Proposals come from three strategies, and the report says which one produced eac
3. Update manual enums.
4. Extract the client resource packs with [brarchive-extractor](https://github.com/LPaicen/brarchive-extractor). The tools read the extracted packs from `run/client-data/resource_packs_unpacked` by
default.
+ Use matching [BDS schemas](https://github.com/bedrock-apis/bds-docs) or a client schema export to decode compiled MCB resources.
+ Pass the schema directory with `--schema=/path/to/schemas`.
+ Keep the extracted `__brarchive` directories inside each pack. The pack generator merges their definitions into the pack root.
5. Update `assets/viabedrock/data` (see `Data Asset Sources.md`) and `BedrockMappingData`.
`./gradlew generateBedrockSoundLists` and `./gradlew generateBedrockParticleList` generate the sound and particle files from the client. New block state upgrade schemas are applied to the mappings
with
diff --git a/build.gradle b/build.gradle
index 8a16054d7..1ff718496 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,6 +19,10 @@ sourceSets {
tool.runtimeClasspath += tool.compileClasspath
}
+configurations {
+ testRuntimeOnly.extendsFrom compileOnly
+}
+
repositories {
//mavenLocal() // Uncomment when updating
maven {
@@ -101,6 +105,14 @@ dependencies {
toolImplementation "org.jsoup:jsoup:1.23.2"
toolImplementation "org.ow2.asm:asm-tree:9.11.1"
toolImplementation "net.lenni0451.commons:asm:1.9.2"
+
+ testImplementation sourceSets.tool.output
+ testImplementation "org.junit.jupiter:junit-jupiter:6.1.3"
+ testRuntimeOnly "org.junit.platform:junit-platform-launcher:6.1.3"
+}
+
+test {
+ useJUnitPlatform()
}
def registerToolTask(String name, String toolMainClass, String toolDescription) {
diff --git a/src/test/java/net/raphimc/viabedrock/tool/generator/client/BedrockPacksGeneratorTest.java b/src/test/java/net/raphimc/viabedrock/tool/generator/client/BedrockPacksGeneratorTest.java
new file mode 100644
index 000000000..bb2405bd7
--- /dev/null
+++ b/src/test/java/net/raphimc/viabedrock/tool/generator/client/BedrockPacksGeneratorTest.java
@@ -0,0 +1,91 @@
+/*
+ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock
+ * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package net.raphimc.viabedrock.tool.generator.client;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.ZipFile;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+final class BedrockPacksGeneratorTest {
+
+ @TempDir
+ Path directory;
+
+ @Test
+ void includesExtractedDefinitionsAtThePackRoot() throws Exception {
+ final Path input = Files.createDirectory(this.directory.resolve("input"));
+ final byte[] payload = {1, 2, 3};
+ for (String folder : new String[]{"biomes", "fogs", "render_controllers"}) {
+ write(input.resolve("__brarchive/" + folder + "/nested/definition.json"), payload);
+ }
+ final Path output = this.directory.resolve("output.mcpack");
+
+ BedrockPacksGenerator.writeResourcePack(input.toFile(), output);
+
+ try (ZipFile pack = new ZipFile(output.toFile())) {
+ for (String folder : new String[]{"biomes", "fogs", "render_controllers"}) {
+ assertArrayEquals(payload, pack.getInputStream(pack.getEntry(folder + "/nested/definition.json")).readAllBytes());
+ assertNull(pack.getEntry("__brarchive/" + folder + "/nested/definition.json"));
+ }
+ }
+ }
+
+ @Test
+ void mergesBothLayoutsAndPreservesDirectFilesOnConflicts() throws Exception {
+ final Path input = Files.createDirectory(this.directory.resolve("input"));
+ final byte[] direct = {1, 2, 3};
+ final byte[] extracted = {4, 5, 6};
+ write(input.resolve("biomes/direct.json"), direct);
+ write(input.resolve("__brarchive/biomes/extracted.json"), extracted);
+ write(input.resolve("__brarchive/biomes/direct.json"), extracted);
+ final Path output = this.directory.resolve("output.mcpack");
+
+ BedrockPacksGenerator.writeResourcePack(input.toFile(), output);
+
+ try (ZipFile pack = new ZipFile(output.toFile())) {
+ assertArrayEquals(direct, pack.getInputStream(pack.getEntry("biomes/direct.json")).readAllBytes());
+ assertArrayEquals(extracted, pack.getInputStream(pack.getEntry("biomes/extracted.json")).readAllBytes());
+ }
+ }
+
+ @Test
+ void preservesDirectDefinitionsWithoutAnExtractedDirectory() throws Exception {
+ final Path input = Files.createDirectory(this.directory.resolve("input"));
+ final byte[] payload = {7, 8, 9};
+ write(input.resolve("fogs/direct.json"), payload);
+ final Path output = this.directory.resolve("output.mcpack");
+
+ BedrockPacksGenerator.writeResourcePack(input.toFile(), output);
+
+ try (ZipFile pack = new ZipFile(output.toFile())) {
+ assertArrayEquals(payload, pack.getInputStream(pack.getEntry("fogs/direct.json")).readAllBytes());
+ assertNull(pack.getEntry("biomes"));
+ }
+ }
+
+ private static void write(final Path path, final byte[] payload) throws Exception {
+ Files.createDirectories(path.getParent());
+ Files.write(path, payload);
+ }
+
+}
diff --git a/src/tool/java/net/raphimc/viabedrock/tool/generator/client/BedrockPacksGenerator.java b/src/tool/java/net/raphimc/viabedrock/tool/generator/client/BedrockPacksGenerator.java
index 7dc6be460..7963784f8 100644
--- a/src/tool/java/net/raphimc/viabedrock/tool/generator/client/BedrockPacksGenerator.java
+++ b/src/tool/java/net/raphimc/viabedrock/tool/generator/client/BedrockPacksGenerator.java
@@ -65,21 +65,7 @@ public static void main(final String[] args) throws Throwable {
packKeys.add(readPackKey(new File(packDir, "manifest.json")));
final File outputFile = new File(resourcePacksOutputDir, packDir.getName() + ".mcpack");
-
- try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + outputFile.toURI()), Map.of("create", "true"))) {
- final Path fsRoot = fs.getRootDirectories().iterator().next();
-
- addLicense(fsRoot);
- copyFileIfExists(packDir, fsRoot, "manifest.json");
- copyFileIfExists(packDir, fsRoot, "texts/en_US.lang");
- copyFileIfExists(packDir, fsRoot, "font/glyph_E0.png");
- copyFileIfExists(packDir, fsRoot, "font/glyph_E1.png");
- copyFolder(packDir, fsRoot, "biomes");
- copyFolder(packDir, fsRoot, "fogs");
- copyFolder(packDir, fsRoot, "render_controllers");
-
- removeTimestamps(fsRoot);
- }
+ writeResourcePack(packDir, outputFile.toPath());
}
final File skinPacksOutputDir = ToolPaths.SKIN_PACKS.toFile();
@@ -95,6 +81,23 @@ public static void main(final String[] args) throws Throwable {
reportMissingPackKeys(packKeys);
}
+ static void writeResourcePack(final File packDir, final Path outputFile) throws IOException {
+ try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + outputFile.toUri()), Map.of("create", "true"))) {
+ final Path fsRoot = fs.getRootDirectories().iterator().next();
+
+ addLicense(fsRoot);
+ copyFileIfExists(packDir, fsRoot, "manifest.json");
+ copyFileIfExists(packDir, fsRoot, "texts/en_US.lang");
+ copyFileIfExists(packDir, fsRoot, "font/glyph_E0.png");
+ copyFileIfExists(packDir, fsRoot, "font/glyph_E1.png");
+ copyFolder(packDir, fsRoot, "biomes");
+ copyFolder(packDir, fsRoot, "fogs");
+ copyFolder(packDir, fsRoot, "render_controllers");
+
+ removeTimestamps(fsRoot);
+ }
+ }
+
/**
* The load order of the vanilla packs is hand maintained, so new packs are only reported instead of being added blindly.
*/
@@ -153,22 +156,23 @@ private static void copyFileIfExists(final File packDir, final Path targetRoot,
}
private static void copyFolder(final File packDir, final Path targetRoot, final String folderPath) throws IOException {
- final File folder = new File(packDir, folderPath);
- if (folder.exists()) {
+ // Direct definitions take precedence when both layouts contain the same path.
+ for (File folder : List.of(new File(packDir, folderPath), new File(packDir, "__brarchive/" + folderPath))) {
+ if (!folder.isDirectory()) {
+ continue;
+ }
final Path sourcePath = folder.toPath();
final Path targetPath = targetRoot.resolve(folderPath);
- Files.walk(sourcePath).forEach(path -> {
- try {
+ try (Stream paths = Files.walk(sourcePath)) {
+ for (Path path : paths.toList()) {
final Path resolvedTargetPath = targetPath.resolve(sourcePath.relativize(path).toString());
if (Files.isDirectory(path)) {
Files.createDirectories(resolvedTargetPath);
- } else {
+ } else if (!Files.exists(resolvedTargetPath)) {
Files.copy(path, resolvedTargetPath);
}
- } catch (final IOException e) {
- e.printStackTrace();
}
- });
+ }
}
}