From 1058f55d323e78a13a3f54dce07971e3bf988691 Mon Sep 17 00:00:00 2001 From: Liam Sage Date: Wed, 4 Nov 2026 12:03:42 +0100 Subject: [PATCH 1/3] test: cover Minecraft 26.3 dimension migration in PR builds --- .github/workflows/build.yml | 28 +++++++ .../cc/modlabs/worldengine/WorldEngineTest.kt | 80 ++++++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e560b18 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,28 @@ +name: Build + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build: + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 25 + - uses: gradle/actions/setup-gradle@v4 + - name: Build and test against published dependencies + run: bash gradlew clean build --refresh-dependencies --no-daemon + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports + path: build/reports/tests/test/ + if-no-files-found: ignore diff --git a/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt b/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt index 202d00f..f468bf1 100644 --- a/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt +++ b/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt @@ -70,10 +70,14 @@ class WorldEngineTest { val snippet = JsonObject().apply { addProperty("id", "example123") addProperty("type", "dimension_type") - addProperty("version", "26.2") + addProperty("version", "26.3") addProperty("data", LZString.compressToBase64(shareJson.toString())) } val decoded = DimensionDatapack.decodeSnippet(snippet.toString(), "example123") + val outdatedSnippet = snippet.deepCopy().apply { addProperty("version", "26.2") } + assertFailsWith { + DimensionDatapack.decodeSnippet(outdatedSnippet.toString(), "example123") + } assertEquals(1024, decoded["height"].asInt) assertEquals("none", decoded["skybox"].asString) val sound = decoded["attributes"].asJsonObject["minecraft:audio/background_music"] @@ -84,7 +88,10 @@ class WorldEngineTest { try { DimensionDatapack.stage(directory, "void", decoded) val pack = directory.resolve("dimension-datapack") - assertTrue(Files.isRegularFile(pack.resolve("pack.mcmeta"))) + val metadata = JsonParser.parseString(Files.readString(pack.resolve("pack.mcmeta"))).asJsonObject["pack"].asJsonObject + assertEquals(121, metadata["pack_format"].asInt) + assertEquals("[121,0]", metadata["min_format"].toString()) + assertEquals("[121,0]", metadata["max_format"].toString()) val stagedType = JsonParser.parseString( Files.readString(pack.resolve("data/worldengine/dimension_type/void.json")) ).asJsonObject @@ -109,4 +116,73 @@ class WorldEngineTest { directory.toFile().deleteRecursively() } } + + @Test + fun `existing dimension packs migrate to 26_3 without losing custom metadata`() { + val directory = Files.createTempDirectory("worldengine-legacy-dimension-test") + try { + val pack = directory.resolve("dimension-datapack") + val types = pack.resolve("data/worldengine/dimension_type") + Files.createDirectories(types) + Files.writeString(pack.resolve("pack.mcmeta"), """ + {"pack":{"description":"Custom description","pack_format":107, + "min_format":[107,1],"max_format":[107,1]},"custom":{"keep":true}} + """.trimIndent()) + Files.writeString(types.resolve("legacy.json"), """ + {"min_y":0,"height":256,"attributes":{ + "minecraft:gameplay/bed_rule":{"can_sleep":"never","explodes":true} + }} + """.trimIndent()) + Files.writeString(types.resolve("explicit.json"), """ + {"min_y":0,"height":256,"attributes":{ + "minecraft:gameplay/bed_rule":{"explodes":true,"destroy_on_use":false} + }} + """.trimIndent()) + + assertEquals(2, DimensionDatapack.normalizeStagedDimensions(directory)) + val metadata = JsonParser.parseString(Files.readString(pack.resolve("pack.mcmeta"))).asJsonObject + assertEquals(121, metadata["pack"].asJsonObject["pack_format"].asInt) + assertEquals("[121,0]", metadata["pack"].asJsonObject["min_format"].toString()) + assertEquals("[121,0]", metadata["pack"].asJsonObject["max_format"].toString()) + assertEquals("Custom description", metadata["pack"].asJsonObject["description"].asString) + assertTrue(metadata["custom"].asJsonObject["keep"].asBoolean) + val legacy = JsonParser.parseString(Files.readString(types.resolve("legacy.json"))) + .asJsonObject["attributes"].asJsonObject["minecraft:gameplay/bed_rule"].asJsonObject + assertTrue(legacy["destroy_on_use"].asBoolean) + assertEquals("never", legacy["can_sleep"].asString) + assertFalse(legacy.has("explodes")) + val explicit = JsonParser.parseString(Files.readString(types.resolve("explicit.json"))) + .asJsonObject["attributes"].asJsonObject["minecraft:gameplay/bed_rule"].asJsonObject + assertFalse(explicit["destroy_on_use"].asBoolean) + assertFalse(explicit.has("explodes")) + val migratedMetadata = Files.readString(pack.resolve("pack.mcmeta")) + assertEquals(0, DimensionDatapack.normalizeStagedDimensions(directory)) + assertEquals(migratedMetadata, Files.readString(pack.resolve("pack.mcmeta"))) + } finally { + directory.toFile().deleteRecursively() + } + } + + @Test + fun `staging a dimension refreshes existing pack metadata`() { + val directory = Files.createTempDirectory("worldengine-staging-migration-test") + try { + val pack = directory.resolve("dimension-datapack") + Files.createDirectories(pack) + Files.writeString(pack.resolve("pack.mcmeta"), """ + {"pack":{"description":"WorldEngine dimensions","pack_format":107, + "min_format":[107,1],"max_format":[107,1]}} + """.trimIndent()) + DimensionDatapack.stage(directory, "new_void", JsonObject().apply { + addProperty("min_y", 0) + addProperty("height", 256) + }) + val metadata = JsonParser.parseString(Files.readString(pack.resolve("pack.mcmeta"))).asJsonObject + assertEquals(121, metadata["pack"].asJsonObject["pack_format"].asInt) + assertEquals("[121,0]", metadata["pack"].asJsonObject["min_format"].toString()) + assertEquals("[121,0]", metadata["pack"].asJsonObject["max_format"].toString()) + } finally { + directory.toFile().deleteRecursively() + } + } } From 1acb5d1fc9a189bdc6af8f4008e0f4ed2d134618 Mon Sep 17 00:00:00 2001 From: Liam Sage Date: Wed, 4 Nov 2026 12:07:30 +0100 Subject: [PATCH 2/3] fix: restore canonical repositories for build and runtime dependencies --- build.gradle.kts | 6 ++-- settings.gradle.kts | 2 +- .../cc/modlabs/worldengine/WorldEngineTest.kt | 36 ++++++++++++++++--- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ace938b..814fb90 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,8 @@ plugins { kotlin("jvm") version "2.4.10" `java-library` id("io.papermc.paperweight.userdev") version "2.0.0-beta.22" - id("cc.modlabs.kpaper-gradle") version "2026.9.26.1231+kpaper.2026.9.23.1637" + // Keep the loader on canonical repositories; the newer plugin hardcodes the mirror. + id("cc.modlabs.kpaper-gradle") version "2026.9.23.1656+kpaper.2026.9.23.1637" id("maven-publish") } @@ -31,11 +32,12 @@ val mcCoroutineVersion: String by project repositories { mavenLocal() - maven("https://repo-api.modlabs.cc/repo/maven/maven-mirror/") + mavenCentral() maven("https://repo.extendedclip.com/releases/") maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.codemc.io/repository/maven-releases/") maven("https://repo.codemc.io/repository/maven-snapshots/") + maven("https://repo-api.modlabs.cc/repo/maven/maven-public/") } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0c8d168..32fb642 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,7 +4,7 @@ rootProject.name = "WorldEngine" pluginManagement { repositories { gradlePluginPortal() - maven("https://repo-api.modlabs.cc/repo/maven/maven-mirror/") + maven("https://repo-api.modlabs.cc/repo/maven/maven-public/") maven("https://repo.papermc.io/repository/maven-public/") mavenLocal() } diff --git a/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt b/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt index f468bf1..01c8d47 100644 --- a/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt +++ b/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt @@ -134,18 +134,21 @@ class WorldEngineTest { }} """.trimIndent()) Files.writeString(types.resolve("explicit.json"), """ - {"min_y":0,"height":256,"attributes":{ + {"height":256,"attributes":{ "minecraft:gameplay/bed_rule":{"explodes":true,"destroy_on_use":false} }} """.trimIndent()) - assertEquals(2, DimensionDatapack.normalizeStagedDimensions(directory)) + Files.writeString(types.resolve("non_explosive.json"), """ + {"attributes":{"minecraft:gameplay/bed_rule":{"explodes":false}}} + """.trimIndent()) + assertEquals(3, DimensionDatapack.normalizeStagedDimensions(directory)) val metadata = JsonParser.parseString(Files.readString(pack.resolve("pack.mcmeta"))).asJsonObject + assertTrue(metadata["custom"].asJsonObject["keep"].asBoolean) assertEquals(121, metadata["pack"].asJsonObject["pack_format"].asInt) assertEquals("[121,0]", metadata["pack"].asJsonObject["min_format"].toString()) assertEquals("[121,0]", metadata["pack"].asJsonObject["max_format"].toString()) assertEquals("Custom description", metadata["pack"].asJsonObject["description"].asString) - assertTrue(metadata["custom"].asJsonObject["keep"].asBoolean) val legacy = JsonParser.parseString(Files.readString(types.resolve("legacy.json"))) .asJsonObject["attributes"].asJsonObject["minecraft:gameplay/bed_rule"].asJsonObject assertTrue(legacy["destroy_on_use"].asBoolean) @@ -155,6 +158,10 @@ class WorldEngineTest { .asJsonObject["attributes"].asJsonObject["minecraft:gameplay/bed_rule"].asJsonObject assertFalse(explicit["destroy_on_use"].asBoolean) assertFalse(explicit.has("explodes")) + val nonExplosive = JsonParser.parseString(Files.readString(types.resolve("non_explosive.json"))) + .asJsonObject["attributes"].asJsonObject["minecraft:gameplay/bed_rule"].asJsonObject + assertFalse(nonExplosive["destroy_on_use"].asBoolean) + assertFalse(nonExplosive.has("explodes")) val migratedMetadata = Files.readString(pack.resolve("pack.mcmeta")) assertEquals(0, DimensionDatapack.normalizeStagedDimensions(directory)) assertEquals(migratedMetadata, Files.readString(pack.resolve("pack.mcmeta"))) @@ -171,13 +178,14 @@ class WorldEngineTest { Files.createDirectories(pack) Files.writeString(pack.resolve("pack.mcmeta"), """ {"pack":{"description":"WorldEngine dimensions","pack_format":107, - "min_format":[107,1],"max_format":[107,1]}} + "min_format":[107,1],"max_format":[107,1]},"custom":{"keep":true}} """.trimIndent()) DimensionDatapack.stage(directory, "new_void", JsonObject().apply { addProperty("min_y", 0) addProperty("height", 256) }) val metadata = JsonParser.parseString(Files.readString(pack.resolve("pack.mcmeta"))).asJsonObject + assertTrue(metadata["custom"].asJsonObject["keep"].asBoolean) assertEquals(121, metadata["pack"].asJsonObject["pack_format"].asInt) assertEquals("[121,0]", metadata["pack"].asJsonObject["min_format"].toString()) assertEquals("[121,0]", metadata["pack"].asJsonObject["max_format"].toString()) @@ -185,4 +193,24 @@ class WorldEngineTest { directory.toFile().deleteRecursively() } } + + @Test + fun `metadata-only packs migrate before dimension discovery`() { + val directory = Files.createTempDirectory("worldengine-metadata-migration-test") + try { + val pack = directory.resolve("dimension-datapack") + Files.createDirectories(pack) + Files.writeString(pack.resolve("pack.mcmeta"), """ + {"pack":{"description":"WorldEngine dimensions","pack_format":107, + "min_format":[107,1],"max_format":[107,1]}} + """.trimIndent()) + assertEquals(0, DimensionDatapack.normalizeStagedDimensions(directory)) + val metadata = JsonParser.parseString(Files.readString(pack.resolve("pack.mcmeta"))).asJsonObject["pack"].asJsonObject + assertEquals(121, metadata["pack_format"].asInt) + assertEquals("[121,0]", metadata["min_format"].toString()) + assertEquals("[121,0]", metadata["max_format"].toString()) + } finally { + directory.toFile().deleteRecursively() + } + } } From dc35648408b012582a6f98274588bba2ad7aff0a Mon Sep 17 00:00:00 2001 From: Liam Sage Date: Wed, 4 Nov 2026 12:09:54 +0100 Subject: [PATCH 3/3] fix: migrate managed dimensions and document Minecraft 26.3 validation --- README.md | 22 +++++-- .../dimensions/DimensionDatapack.kt | 62 ++++++++++++++----- .../cc/modlabs/worldengine/WorldEngineTest.kt | 4 +- 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8b6fa02..9d86630 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,23 @@ ## Compatibility -- Paper 26.2 +- Paper 26.3 (compiled against `26.3.build.42-alpha`) - Java 25 - KPaper lifecycle with all optional KPaper features disabled -Jenkins builds with the jdk25 tool and exposes the successful JAR as a downloadable build artifact. +### Build and validate + +With JDK 25 selected, run the checked-in Gradle wrapper: + +```sh +bash gradlew clean build --refresh-dependencies --no-daemon +``` + +The build compiles against Paper 26.3 and runs the unit tests. The plugin JAR is written to `build/libs/`. Pull requests run this same clean build without publishing a release. Jenkins uses its `jdk25` tool and exposes the successful JAR as a downloadable artifact. + +KPaperGradle is pinned to the last version that resolves both build and runtime libraries through Maven Central, ModLabs' hosted repository, and CodeMC rather than depending on the broad ModLabs mirror. The KPaper library version remains `2026.9.23.1637`. Malformed mirror responses caused the failed release build; do not restore the mirror-only plugin until its repository routing is fixed. + +Before releasing, smoke-test the JAR on an isolated Paper 26.3 server: start with an existing managed dimension, import a new 26.3 Misode share, restart, and verify world loading, teleportation, and generators. Unit tests do not replace this server check. ## PlotSquared compatibility @@ -32,7 +44,7 @@ WorldEngine intentionally does not register itself as a `/plot setup` generator. ## Custom dimensions -Stage a void dimension from a Minecraft 26.2 Misode Dimension Type share link: +Stage a void dimension from a Minecraft 26.3 Misode Dimension Type share link: ```text /worldengine dimension create @@ -40,6 +52,8 @@ Stage a void dimension from a Minecraft 26.2 Misode Dimension Type share link: Restart the server once. WorldEngine registers the generated datapack during bootstrap and manages the loaded `worldengine:` dimension afterward. Only HTTPS links from `misode.github.io/dimension-type/` are accepted. +Managed dimension datapacks use Minecraft 26.3 data pack format 121.0. Existing WorldEngine 26.2 packs are upgraded on bootstrap: pack metadata is refreshed and the bed-rule `explodes` field is migrated to `destroy_on_use`. New imports must use a 26.3 Misode share. Back up your worlds and `plugins/WorldEngine/dimension-datapack` before upgrading. + For Geyser compatibility, imported dimension types with a negative `min_y` are mapped to Bedrock's Overworld height model. Existing WorldEngine dimension types are migrated automatically during the next server bootstrap and loaded during that same start. ## API usage @@ -116,7 +130,7 @@ WorldEngine is published to ModLabs Nexus. ```kotlin repositories { - maven("https://repo-api.modlabs.cc/repo/maven/maven-mirror/") + maven("https://repo-api.modlabs.cc/repo/maven/maven-public/") } dependencies { diff --git a/src/main/kotlin/cc/modlabs/worldengine/dimensions/DimensionDatapack.kt b/src/main/kotlin/cc/modlabs/worldengine/dimensions/DimensionDatapack.kt index 6a34d75..3437332 100644 --- a/src/main/kotlin/cc/modlabs/worldengine/dimensions/DimensionDatapack.kt +++ b/src/main/kotlin/cc/modlabs/worldengine/dimensions/DimensionDatapack.kt @@ -28,7 +28,7 @@ object DimensionDatapack { private const val PACK_DIRECTORY = "dimension-datapack" private const val SHARE_HOST = "misode.github.io" private const val SNIPPET_HOST = "snippets.misode.workers.dev" - private const val MINECRAFT_VERSION = "26.2" + private const val MINECRAFT_VERSION = "26.3" private const val MAX_RESPONSE_BYTES = 32 * 1024 private const val MAX_JSON_CHARS = 64 * 1024 private val shareIdPattern = Regex("[A-Za-z0-9_-]{6,64}") @@ -44,7 +44,7 @@ object DimensionDatapack { if (!Files.isRegularFile(pack.resolve("pack.mcmeta"))) return val migrated = normalizeStagedDimensions(context.dataDirectory) if (migrated > 0) { - context.logger.info("Made $migrated WorldEngine dimension type compatible with negative Y on Geyser") + context.logger.info("Migrated $migrated WorldEngine dimension types for Minecraft $MINECRAFT_VERSION and Geyser") } registrar.discoverPack(pack, "dimensions") { configurer -> configurer.title(Component.text("WorldEngine dimensions")) @@ -115,8 +115,14 @@ object DimensionDatapack { return inlineCustomSounds(dimensionType) } - private fun normalizeForGeyser(dimensionType: JsonObject): JsonObject { + private fun normalizeDimensionType(dimensionType: JsonObject): JsonObject { val copy = dimensionType.deepCopy() + // Minecraft 26.3 renamed this field. Keep an explicit new value if both are present. + val bedRule = copy.getAsJsonObject("attributes")?.getAsJsonObject("minecraft:gameplay/bed_rule") + bedRule?.remove("explodes")?.let { legacyValue -> + if (!bedRule.has("destroy_on_use")) bedRule.add("destroy_on_use", legacyValue) + } + val minY = runCatching { copy["min_y"]?.asInt }.getOrNull() ?: return copy if (minY < 0 && copy["skybox"]?.asString in setOf("end", "none")) { copy.remove("skybox") @@ -156,8 +162,9 @@ object DimensionDatapack { val dimensionFile = pack.resolve("data/worldengine/dimension/$worldKey.json") require(!Files.exists(dimensionFile)) { "Dimension '$worldKey' is already staged" } + normalizeStagedDimensions(dataDirectory) writeIfMissing(pack.resolve("pack.mcmeta"), PACK_META) - writeAtomically(typeFile, gson.toJson(normalizeForGeyser(dimensionType)), replace = true) + writeAtomically(typeFile, gson.toJson(normalizeDimensionType(dimensionType)), replace = true) writeAtomically(dimensionFile, gson.toJson(voidDimension(worldKey)), replace = false) return dimensionFile } @@ -165,22 +172,43 @@ object DimensionDatapack { @Synchronized internal fun normalizeStagedDimensions(dataDirectory: Path): Int { val typeDirectory = packDirectory(dataDirectory).resolve("data/worldengine/dimension_type") - if (!Files.isDirectory(typeDirectory)) return 0 var changed = 0 - Files.newDirectoryStream(typeDirectory, "*.json").use { files -> - for (file in files) { - if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) continue - val original = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).asJsonObject - val normalized = normalizeForGeyser(original) - if (normalized != original) { - writeAtomically(file, gson.toJson(normalized), replace = true) - changed++ + if (Files.isDirectory(typeDirectory)) { + Files.newDirectoryStream(typeDirectory, "*.json").use { files -> + for (file in files) { + if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) continue + val original = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).asJsonObject + val normalized = normalizeDimensionType(original) + if (normalized != original) { + writeAtomically(file, gson.toJson(normalized), replace = true) + changed++ + } } } } + // Advertise the new format only after every dimension has migrated successfully. + upgradePackMetadata(packDirectory(dataDirectory).resolve("pack.mcmeta")) return changed } + private fun upgradePackMetadata(path: Path) { + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) return + val metadata = JsonParser.parseString(Files.readString(path, StandardCharsets.UTF_8)).asJsonObject + val pack = metadata.getAsJsonObject("pack") ?: return + // Only upgrade the format produced by WorldEngine's 26.2 implementation. + // Leave current, unknown, or newer packs alone rather than silently downgrading them. + if (pack["pack_format"]?.asInt != 107) return + val currentPack = JsonParser.parseString(PACK_META).asJsonObject.getAsJsonObject("pack") + for (field in listOf("pack_format", "min_format", "max_format")) { + pack.add(field, currentPack[field].deepCopy()) + } + val description = pack["description"]?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString } + if (description?.asString == "WorldEngine managed dimensions for Minecraft Java 26.2") { + pack.add("description", currentPack["description"].deepCopy()) + } + writeAtomically(path, gson.toJson(metadata), replace = true) + } + internal fun hasDimension(dataDirectory: Path, worldKey: String): Boolean { if (!worldKeyPattern.matches(worldKey)) return false return Files.isRegularFile(packDirectory(dataDirectory).resolve("data/worldengine/dimension/$worldKey.json")) @@ -244,10 +272,10 @@ object DimensionDatapack { private val PACK_META = """ { "pack": { - "description": "WorldEngine managed dimensions for Minecraft Java 26.2", - "pack_format": 107, - "min_format": [107, 1], - "max_format": [107, 1] + "description": "WorldEngine managed dimensions for Minecraft Java $MINECRAFT_VERSION", + "pack_format": 121, + "min_format": [121, 0], + "max_format": [121, 0] } } """.trimIndent() diff --git a/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt b/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt index 01c8d47..1cc9dcc 100644 --- a/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt +++ b/src/test/kotlin/cc/modlabs/worldengine/WorldEngineTest.kt @@ -177,7 +177,7 @@ class WorldEngineTest { val pack = directory.resolve("dimension-datapack") Files.createDirectories(pack) Files.writeString(pack.resolve("pack.mcmeta"), """ - {"pack":{"description":"WorldEngine dimensions","pack_format":107, + {"pack":{"description":{"text":"Custom","color":"gold"},"pack_format":107, "min_format":[107,1],"max_format":[107,1]},"custom":{"keep":true}} """.trimIndent()) DimensionDatapack.stage(directory, "new_void", JsonObject().apply { @@ -185,6 +185,8 @@ class WorldEngineTest { addProperty("height", 256) }) val metadata = JsonParser.parseString(Files.readString(pack.resolve("pack.mcmeta"))).asJsonObject + assertEquals("Custom", metadata["pack"].asJsonObject["description"].asJsonObject["text"].asString) + assertEquals("gold", metadata["pack"].asJsonObject["description"].asJsonObject["color"].asString) assertTrue(metadata["custom"].asJsonObject["keep"].asBoolean) assertEquals(121, metadata["pack"].asJsonObject["pack_format"].asInt) assertEquals("[121,0]", metadata["pack"].asJsonObject["min_format"].toString())