From af46b5f0edcf5b8a6e83c23cac3165f3f2e86201 Mon Sep 17 00:00:00 2001 From: TBPig <147127248+TBPig@users.noreply.github.com> Date: Fri, 8 May 2026 10:01:11 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(structure):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=B8=B2=E6=9F=93.snbt=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 6 +- README.md | 6 +- docs/en/03-markdown-syntax.md | 10 +- docs/en/10-structure-preview-rendering.md | 4 +- docs/zh/03-markdown-syntax.md | 9 +- docs/zh/10-structure-preview-rendering.md | 4 +- .../extend/MDNBTStructureComponent.java | 201 +++++++++++++++++- .../assets/ageratum/ageratum/en_us/index.md | 2 + .../assets/ageratum/ageratum/en_us/test.snbt | 18 ++ .../assets/ageratum/ageratum/zh_cn/index.md | 2 + .../assets/ageratum/ageratum/zh_cn/test.snbt | 18 ++ 11 files changed, 253 insertions(+), 27 deletions(-) create mode 100644 src/main/resources/assets/ageratum/ageratum/en_us/test.snbt create mode 100644 src/main/resources/assets/ageratum/ageratum/zh_cn/test.snbt diff --git a/README.en.md b/README.en.md index 17775ca..144ae5c 100644 --- a/README.en.md +++ b/README.en.md @@ -194,19 +194,21 @@ public static final DeferredHolder, ### Structure NBT Component (``) -Use the structure extension to render a summary, top-down block preview, and bounded NBT tree for `.nbt` structure files directly inside +Use the structure extension to render a summary, top-down block preview, and bounded NBT tree for `.nbt` / `.snbt` structure files directly inside documents: ```markdown + + ``` - `id` / `path`: required, target structure file `ResourceLocation` - `maxDepth`: optional, maximum expansion depth, default `2` - `maxEntries`: optional, maximum number of keys/list entries shown per level, default `12` -- Relative paths are supported and resolved against the current document directory, including `.nbt` files placed next to the Markdown +- Relative paths are supported and resolved against the current document directory, including `.nbt` / `.snbt` files placed next to the Markdown document inside the resource pack; in preview mode the matching file is loaded from `run/ageratum_review/` - The component first shows structure metadata such as size, palette, block count, and entity count, then renders a top-down block preview followed by a depth-limited NBT tree diff --git a/README.md b/README.md index ebbb94d..b6d3a9e 100644 --- a/README.md +++ b/README.md @@ -365,18 +365,20 @@ public static final DeferredHolder, ### 结构 NBT 组件(``) -使用结构扩展可以在文档中直接渲染 `.nbt` 结构文件的摘要、俯视方块预览与 NBT 树: +使用结构扩展可以在文档中直接渲染 `.nbt` / `.snbt` 结构文件的摘要、俯视方块预览与 NBT 树: ```markdown + + ``` - `id` / `path`:必填,目标结构文件的 `ResourceLocation` - `maxDepth`:可选,最大展开深度,默认 `2` - `maxEntries`:可选,每层最多显示的键/列表项数量,默认 `12` -- 支持相对路径,按当前文档目录解析;可直接读取资源包中与 Markdown 同目录的 `.nbt` 文件,预览模式下则会从 `run/ageratum_review/` 读取对应文件 +- 支持相对路径,按当前文档目录解析;可直接读取资源包中与 Markdown 同目录的 `.nbt` / `.snbt` 文件,预览模式下则会从 `run/ageratum_review/` 读取对应文件 - 组件会优先显示结构尺寸、调色板、方块数、实体数等摘要,其后渲染俯视方块预览,并在下方展示受限深度的 NBT 树 - 将鼠标悬停到预览区域的方块上时,可查看方块 ID、结构坐标、palette 索引及是否带块实体数据 diff --git a/docs/en/03-markdown-syntax.md b/docs/en/03-markdown-syntax.md index ad087cc..483b8b5 100644 --- a/docs/en/03-markdown-syntax.md +++ b/docs/en/03-markdown-syntax.md @@ -283,21 +283,23 @@ Block content here — supports Markdown syntax. ### Structure NBT Component Use the `structure` extension to render a summary, top-down block preview, and bounded NBT tree view for -structure files stored under `data//structure/*.nbt`: +structure files (`.nbt` / `.snbt`): ```markdown + + ``` - `id` / `path`: required, target structure file `ResourceLocation` - `maxDepth`: optional, maximum expansion depth, default `2` - `maxEntries`: optional, maximum number of keys/list entries shown per level, default `12` -- Relative paths are supported, for example `./test.nbt` or `../shared/demo.nbt`, resolved against the current document directory and able - to point to `.nbt` files placed next to the Markdown document inside the resource pack -- In preview mode, relative paths are loaded from the matching location under `run/ageratum_review/` +- Relative paths are supported, for example `./test.nbt`, `./test.snbt` or `../shared/demo.nbt`, resolved against the current document directory and able + to point to `.nbt` / `.snbt` files placed next to the Markdown document inside the resource pack +- In preview mode, relative paths are loaded from the matching location under `run/ageratum_review/` (both `.nbt` and `.snbt`) - Rendered content: structure size, palette/block/entity counts, a top-down block preview, and a depth-limited NBT tree - Hovering a block in the preview shows its block ID, structure coordinates, palette index, and whether it carries block-entity NBT - Fallback behavior: if the structure file is missing or cannot be read, the component shows an inline error message diff --git a/docs/en/10-structure-preview-rendering.md b/docs/en/10-structure-preview-rendering.md index a0b3b4d..7a96909 100644 --- a/docs/en/10-structure-preview-rendering.md +++ b/docs/en/10-structure-preview-rendering.md @@ -1,6 +1,6 @@ # Structure Preview Rendering -This document explains how `MDNBTStructureComponent` renders `.nbt` structures inside the guide UI, including the core pipeline and class responsibilities. +This document explains how `MDNBTStructureComponent` renders `.nbt` / `.snbt` (SNBT) structures inside the guide UI, including the core pipeline and class responsibilities. --- @@ -9,7 +9,7 @@ This document explains how `MDNBTStructureComponent` renders `.nbt` structures i The structure preview pipeline is: 1. Resolve the structure target path (including relative paths) -2. Open NBT data from resource packs or preview files +2. Open NBT/SNBT data from resource packs or preview files 3. Place the template into `SandboxRenderLevel` 4. Render it through `StructurePreviewRenderer` into the markdown UI diff --git a/docs/zh/03-markdown-syntax.md b/docs/zh/03-markdown-syntax.md index 8eff209..797ba01 100644 --- a/docs/zh/03-markdown-syntax.md +++ b/docs/zh/03-markdown-syntax.md @@ -282,21 +282,22 @@ CommonMark 可转义标点符号(`!`、`"`、`#`、`$`、`%`、`&`、`'`、`(` ### 结构 NBT 组件 -使用 `structure` 扩展,可以在文档中渲染 `data//structure/*.nbt` -结构文件的摘要、俯视方块预览与 NBT 树状视图: +使用 `structure` 扩展,可以在文档中渲染结构文件(`.nbt` / `.snbt`)的摘要、俯视方块预览与 NBT 树状视图: ```markdown + + ``` - `id` / `path`:必填,目标结构文件的 `ResourceLocation` - `maxDepth`:可选,最大展开深度,默认 `2` - `maxEntries`:可选,每层最多显示的键/列表项数量,默认 `12` -- 支持相对路径:如 `./test.nbt`、`../shared/demo.nbt`,相对于当前文档所在目录解析;可直接指向资源包中与 Markdown 同目录的 `.nbt` 文件 -- 预览模式下,相对路径会从 `run/ageratum_review/` 下的对应位置读取 `.nbt` 文件 +- 支持相对路径:如 `./test.nbt`、`./test.snbt`、`../shared/demo.nbt`,相对于当前文档所在目录解析;可直接指向资源包中与 Markdown 同目录的 `.nbt` / `.snbt` 文件 +- 预览模式下,相对路径会从 `run/ageratum_review/` 下的对应位置读取 `.nbt` / `.snbt` 文件 - 渲染内容:结构尺寸、调色板/方块/实体统计、俯视方块预览,以及受限深度的 NBT 树 - 将鼠标悬停在预览区的方块上时,可查看方块 ID、结构坐标、palette 索引及是否带块实体数据 - 回退行为:当结构文件不存在或读取失败时,组件会在文档中显示错误信息 diff --git a/docs/zh/10-structure-preview-rendering.md b/docs/zh/10-structure-preview-rendering.md index 54801c7..fb0870b 100644 --- a/docs/zh/10-structure-preview-rendering.md +++ b/docs/zh/10-structure-preview-rendering.md @@ -1,6 +1,6 @@ # 结构预览渲染说明 -本文档说明 `MDNBTStructureComponent` 在 GUI 中渲染 `.nbt` 结构时的主要流程与关键类职责,帮助后续维护与扩展。 +本文档说明 `MDNBTStructureComponent` 在 GUI 中渲染 `.nbt` / `.snbt` 结构时的主要流程与关键类职责,帮助后续维护与扩展。 --- @@ -9,7 +9,7 @@ 结构预览是一条“资源解析 -> 沙盒世界装配 -> 渲染输出”的链路: 1. 组件解析目标结构路径(支持相对路径) -2. 从资源包或 preview 目录读取 NBT +2. 从资源包或 preview 目录读取 NBT / SNBT 3. 将结构放置到 `SandboxRenderLevel` 4. 使用 `StructurePreviewRenderer` 在文档 UI 内绘制 diff --git a/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java b/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java index a8196e7..45018ca 100644 --- a/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java +++ b/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java @@ -21,8 +21,12 @@ import net.minecraft.core.Vec3i; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.TagParser; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.Resource; import net.minecraft.util.Mth; @@ -30,13 +34,19 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; import net.minecraft.world.phys.BlockHitResult; +import com.mojang.brigadier.StringReader; import org.lwjgl.glfw.GLFW; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; /** @@ -334,7 +344,27 @@ public int getHeight(Minecraft minecraft, int maxX, int maxY) { StructureTemplate template = new StructureTemplate(); HolderLookup.RegistryLookup blocks = clientLevel.registryAccess().registryOrThrow(Registries.BLOCK).asLookup(); - CompoundTag root = NbtIo.readCompressed(inputStream, NbtAccounter.unlimitedHeap()); + byte[] bytes = inputStream.readAllBytes(); + CompoundTag root; + try { + root = NbtIo.readCompressed(new ByteArrayInputStream(bytes), NbtAccounter.unlimitedHeap()); + } catch (Exception compressedException) { + try { + String snbt = new String(bytes, StandardCharsets.UTF_8); + if (!snbt.isEmpty() && snbt.charAt(0) == '\ufeff') { + snbt = snbt.substring(1); + } + root = new TagParser(new StringReader(snbt)).readStruct(); + } catch (Exception snbtException) { + log.warn( + "Failed to parse structure file '{}' as compressed NBT or SNBT", + target.displayPath(), + snbtException + ); + return null; + } + } + root = normalizeStructureRoot(root); template.load(blocks, root); Vec3i size = template.getSize(); BlockPos pos = StructureSandboxFactory.centeredPlacement(template); @@ -347,6 +377,131 @@ public int getHeight(Minecraft minecraft, int maxX, int maxY) { } } + /** + * Normalize SNBT variants into the vanilla StructureTemplate NBT format. + * + *

In addition to vanilla structure NBT (compressed or SNBT), we also support a simplified SNBT format: + *

+     * {
+     *   size: [x, y, z],
+     *   palette: ["minecraft:stone", "minecraft:barrier{waterlogged:false}"],
+     *   data: [{pos:[0,0,0], state:"minecraft:stone"}, ...]
+     * }
+     * 
+ * This method converts it to {@code palette: [{Name:"...", Properties:{...}}, ...]} and + * {@code blocks: [{pos:[...], state:}, ...]}. + */ + private static CompoundTag normalizeStructureRoot(CompoundTag root) { + Tag paletteTag = root.get("palette"); + boolean paletteIsStringList = paletteTag instanceof ListTag list && list.getElementType() == Tag.TAG_STRING; + Tag dataTag = root.get("data"); + boolean hasSimplifiedData = dataTag instanceof ListTag list && list.getElementType() == Tag.TAG_COMPOUND; + + if (!paletteIsStringList && !hasSimplifiedData) { + return root; + } + + CompoundTag converted = root.copy(); + + // Build palette entries in a deterministic order. + List paletteStates = new ArrayList<>(); + if (paletteIsStringList) { + ListTag paletteStrings = (ListTag) converted.get("palette"); + for (int i = 0; i < paletteStrings.size(); i++) { + String state = paletteStrings.getString(i); + if (!state.isBlank()) { + paletteStates.add(state); + } + } + } + + ListTag dataList = hasSimplifiedData ? (ListTag) converted.get("data") : null; + if (paletteStates.isEmpty() && dataList != null) { + for (int i = 0; i < dataList.size(); i++) { + CompoundTag entry = dataList.getCompound(i); + String state = entry.getString("state"); + if (!state.isBlank() && !paletteStates.contains(state)) { + paletteStates.add(state); + } + } + } + + ListTag palette = new ListTag(); + Map paletteIndex = new LinkedHashMap<>(); + for (String state : paletteStates) { + paletteIndex.put(state, palette.size()); + palette.add(toStructurePaletteEntry(state)); + } + converted.put("palette", palette); + + if (dataList != null && !converted.contains("blocks")) { + ListTag blocks = new ListTag(); + for (int i = 0; i < dataList.size(); i++) { + CompoundTag entry = dataList.getCompound(i); + String state = entry.getString("state"); + + Integer index = paletteIndex.get(state); + if (index == null) { + index = palette.size(); + paletteIndex.put(state, index); + palette.add(toStructurePaletteEntry(state)); + } + + CompoundTag block = new CompoundTag(); + Tag pos = entry.get("pos"); + if (pos != null) { + block.put("pos", pos.copy()); + } + block.putInt("state", index); + + Tag nbt = entry.get("nbt"); + if (nbt != null) { + block.put("nbt", nbt.copy()); + } + blocks.add(block); + } + converted.remove("data"); + converted.put("blocks", blocks); + } + + return converted; + } + + private static CompoundTag toStructurePaletteEntry(String stateString) { + String raw = stateString.trim(); + String name = raw; + String props = ""; + + int braceIndex = raw.indexOf('{'); + if (braceIndex >= 0 && raw.endsWith("}")) { + name = raw.substring(0, braceIndex).trim(); + props = raw.substring(braceIndex + 1, raw.length() - 1).trim(); + } + + CompoundTag entry = new CompoundTag(); + entry.putString("Name", name); + + if (!props.isEmpty()) { + CompoundTag properties = new CompoundTag(); + String[] pairs = props.split(","); + for (String pair : pairs) { + int colon = pair.indexOf(':'); + if (colon < 0) { + continue; + } + String key = pair.substring(0, colon).trim(); + String value = pair.substring(colon + 1).trim(); + if (!key.isEmpty() && !value.isEmpty()) { + properties.put(key, StringTag.valueOf(value)); + } + } + if (!properties.isEmpty()) { + entry.put("Properties", properties); + } + } + return entry; + } + /** * 按优先级打开结构输入流:先尝试 preview 工作区,再尝试资源管理器, * 最后回退到 classpath 路径。 @@ -361,9 +516,11 @@ public int getHeight(Minecraft minecraft, int maxX, int maxY) { } } - Resource directResource = Minecraft.getInstance().getResourceManager().getResource(target.location()).orElse(null); - if (directResource != null) { - return directResource.open(); + for (ResourceLocation candidate : candidateResourceLocations(target.location())) { + Resource directResource = Minecraft.getInstance().getResourceManager().getResource(candidate).orElse(null); + if (directResource != null) { + return directResource.open(); + } } for (String candidate : candidateResourcePaths(target.location())) { @@ -375,6 +532,18 @@ public int getHeight(Minecraft minecraft, int maxX, int maxY) { return null; } + private static List candidateResourceLocations(ResourceLocation location) { + String path = location.getPath(); + if (endsWithStructureExtension(path)) { + return List.of(location); + } + return List.of( + location, + ResourceLocation.fromNamespaceAndPath(location.getNamespace(), path + ".nbt"), + ResourceLocation.fromNamespaceAndPath(location.getNamespace(), path + ".snbt") + ); + } + /** * 生成结构文件在 classpath 中的回退搜索路径。 */ @@ -382,7 +551,9 @@ private static List candidateResourcePaths(ResourceLocation location) { String normalizedPath = normalizeStructurePath(location.getPath()); return List.of( "data/" + location.getNamespace() + "/structure/" + normalizedPath + ".nbt", - "data/" + location.getNamespace() + "/structures/" + normalizedPath + ".nbt" + "data/" + location.getNamespace() + "/structures/" + normalizedPath + ".nbt", + "data/" + location.getNamespace() + "/structure/" + normalizedPath + ".snbt", + "data/" + location.getNamespace() + "/structures/" + normalizedPath + ".snbt" ); } @@ -391,14 +562,22 @@ private static String normalizeStructurePath(String path) { while (normalized.startsWith("/")) { normalized = normalized.substring(1); } - if (normalized.endsWith(".nbt")) { - normalized = normalized.substring(0, normalized.length() - 4); + if (endsWithStructureExtension(normalized)) { + int dotIndex = normalized.lastIndexOf('.'); + normalized = dotIndex >= 0 ? normalized.substring(0, dotIndex) : normalized; } return normalized; } - private static String ensureNbtExtension(String path) { - return path.endsWith(".nbt") ? path : path + ".nbt"; + private static boolean endsWithStructureExtension(String path) { + return path.endsWith(".nbt") || path.endsWith(".snbt"); + } + + private static List expandStructureExtensions(String path) { + String normalized = path.replace('\\', '/'); + return endsWithStructureExtension(normalized) + ? List.of(normalized) + : List.of(normalized + ".nbt", normalized + ".snbt"); } private static String getCurrentDirectoryPath(ResourceLocation location) { @@ -419,7 +598,7 @@ public static StructureTarget resolve(ResourceLocation sourceLocation, String ra if (trimmed.contains(":")) { ResourceLocation location = ResourceLocation.parse(trimmed); List previewPaths = AgeratumClient.isPreviewLocation(location) - ? List.of(ensureNbtExtension(RelativePathResolver.resolveWithinBase("", location.getPath()))) + ? expandStructureExtensions(RelativePathResolver.resolveWithinBase("", location.getPath())) : List.of(); return new StructureTarget(location, trimmed, previewPaths); } @@ -427,7 +606,7 @@ public static StructureTarget resolve(ResourceLocation sourceLocation, String ra String resolvedPath = RelativePathResolver.resolveWithinBase(getCurrentDirectoryPath(sourceLocation), trimmed); ResourceLocation location = ResourceLocation.fromNamespaceAndPath(sourceLocation.getNamespace(), resolvedPath); List previewPaths = AgeratumClient.isPreviewLocation(sourceLocation) - ? List.of(ensureNbtExtension(resolvedPath)) + ? expandStructureExtensions(resolvedPath) : List.of(); return new StructureTarget(location, trimmed, previewPaths); } diff --git a/src/main/resources/assets/ageratum/ageratum/en_us/index.md b/src/main/resources/assets/ageratum/ageratum/en_us/index.md index 858405a..2864a7c 100644 --- a/src/main/resources/assets/ageratum/ageratum/en_us/index.md +++ b/src/main/resources/assets/ageratum/ageratum/en_us/index.md @@ -241,6 +241,8 @@ Tilde fenced code block is now supported. + + --- ## Implemented: Image (line-only, namespace:path) diff --git a/src/main/resources/assets/ageratum/ageratum/en_us/test.snbt b/src/main/resources/assets/ageratum/ageratum/en_us/test.snbt new file mode 100644 index 0000000..9fca397 --- /dev/null +++ b/src/main/resources/assets/ageratum/ageratum/en_us/test.snbt @@ -0,0 +1,18 @@ +{ + DataVersion: 3955, + size: [3, 4, 1], + data: [ + {pos: [0, 0, 0], state: "minecraft:stonecutter{facing:north}"}, + {pos: [2, 0, 0], state: "minecraft:stonecutter{facing:north}"}, + {pos: [0, 1, 0], state: "minecraft:cobblestone"}, + {pos: [0, 2, 0], state: "minecraft:barrier{waterlogged:false}"}, + {pos: [0, 3, 0], state: "minecraft:anvil{facing:west}"} + ], + entities: [], + palette: [ + "minecraft:cobblestone", + "minecraft:barrier{waterlogged:false}", + "minecraft:stonecutter{facing:north}", + "minecraft:anvil{facing:west}" + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/ageratum/ageratum/zh_cn/index.md b/src/main/resources/assets/ageratum/ageratum/zh_cn/index.md index 58e34d6..44c4765 100644 --- a/src/main/resources/assets/ageratum/ageratum/zh_cn/index.md +++ b/src/main/resources/assets/ageratum/ageratum/zh_cn/index.md @@ -226,6 +226,8 @@ _ 被转义 + + --- ## 已实现:图片(独占一行,namespace:path) diff --git a/src/main/resources/assets/ageratum/ageratum/zh_cn/test.snbt b/src/main/resources/assets/ageratum/ageratum/zh_cn/test.snbt new file mode 100644 index 0000000..9fca397 --- /dev/null +++ b/src/main/resources/assets/ageratum/ageratum/zh_cn/test.snbt @@ -0,0 +1,18 @@ +{ + DataVersion: 3955, + size: [3, 4, 1], + data: [ + {pos: [0, 0, 0], state: "minecraft:stonecutter{facing:north}"}, + {pos: [2, 0, 0], state: "minecraft:stonecutter{facing:north}"}, + {pos: [0, 1, 0], state: "minecraft:cobblestone"}, + {pos: [0, 2, 0], state: "minecraft:barrier{waterlogged:false}"}, + {pos: [0, 3, 0], state: "minecraft:anvil{facing:west}"} + ], + entities: [], + palette: [ + "minecraft:cobblestone", + "minecraft:barrier{waterlogged:false}", + "minecraft:stonecutter{facing:north}", + "minecraft:anvil{facing:west}" + ] +} \ No newline at end of file From a8c0f9810b75fddfb50178e3a8d1d759d5a365f3 Mon Sep 17 00:00:00 2001 From: Gugle Date: Fri, 8 May 2026 17:19:28 +0800 Subject: [PATCH 2/3] feat(structure): enhance structure loading with improved parsing and error handling --- .../extend/MDNBTStructureComponent.java | 129 ++++++++++++++---- 1 file changed, 104 insertions(+), 25 deletions(-) diff --git a/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java b/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java index 45018ca..558db05 100644 --- a/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java +++ b/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java @@ -37,7 +37,8 @@ import com.mojang.brigadier.StringReader; import org.lwjgl.glfw.GLFW; -import java.io.ByteArrayInputStream; +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -57,6 +58,7 @@ */ @Slf4j public final class MDNBTStructureComponent extends MDComponent { + private static final int MAX_SNBT_BYTES = 8 * 1024 * 1024; private static final float MIN_ZOOM = AgeratumConstants.Structure.Camera.MIN_ZOOM; private static final float MAX_ZOOM = AgeratumConstants.Structure.Camera.MAX_ZOOM; private static final float ROTATE_YAW_SENSITIVITY = AgeratumConstants.Structure.Sensitivity.ROTATE_YAW; @@ -337,32 +339,12 @@ public int getHeight(Minecraft minecraft, int maxX, int maxY) { */ private @Nullable SandboxRenderLevel prepare(@Nullable Level clientLevel, StructureTarget target) { if (clientLevel == null) return null; - try (InputStream inputStream = MDNBTStructureComponent.openStructureStream(target)) { - if (inputStream == null) { - return null; - } - + try { StructureTemplate template = new StructureTemplate(); HolderLookup.RegistryLookup blocks = clientLevel.registryAccess().registryOrThrow(Registries.BLOCK).asLookup(); - byte[] bytes = inputStream.readAllBytes(); - CompoundTag root; - try { - root = NbtIo.readCompressed(new ByteArrayInputStream(bytes), NbtAccounter.unlimitedHeap()); - } catch (Exception compressedException) { - try { - String snbt = new String(bytes, StandardCharsets.UTF_8); - if (!snbt.isEmpty() && snbt.charAt(0) == '\ufeff') { - snbt = snbt.substring(1); - } - root = new TagParser(new StringReader(snbt)).readStruct(); - } catch (Exception snbtException) { - log.warn( - "Failed to parse structure file '{}' as compressed NBT or SNBT", - target.displayPath(), - snbtException - ); - return null; - } + CompoundTag root = readStructureRoot(target); + if (root == null) { + return null; } root = normalizeStructureRoot(root); template.load(blocks, root); @@ -373,10 +355,107 @@ public int getHeight(Minecraft minecraft, int maxX, int maxY) { this.structureTemplateCache = template; return StructureSandboxFactory.create(clientLevel, template, pos); } catch (Exception exception) { + log.warn("Failed to load structure preview from '{}'", target.displayPath(), exception); + return null; + } + } + + private static @Nullable CompoundTag readStructureRoot(StructureTarget target) { + ParseMode preferredMode; + try (InputStream stream = MDNBTStructureComponent.openStructureStream(target)) { + if (stream == null) { + return null; + } + preferredMode = detectParseMode(stream); + } catch (IOException exception) { + log.warn("Failed to open structure file '{}'", target.displayPath(), exception); + return null; + } + + CompoundTag parsed = parseStructureRoot(target, preferredMode); + if (parsed != null) { + return parsed; + } + + ParseMode fallbackMode = preferredMode == ParseMode.COMPRESSED_NBT ? ParseMode.SNBT : ParseMode.COMPRESSED_NBT; + parsed = parseStructureRoot(target, fallbackMode); + if (parsed != null) { + log.warn( + "Structure file '{}' failed {} parsing and was loaded as {}", + target.displayPath(), + preferredMode.description, + fallbackMode.description + ); + return parsed; + } + + log.warn( + "Failed to parse structure file '{}' as {} or {}", + target.displayPath(), + preferredMode.description, + fallbackMode.description + ); + return null; + } + + private static @Nullable CompoundTag parseStructureRoot(StructureTarget target, ParseMode mode) { + try (InputStream stream = MDNBTStructureComponent.openStructureStream(target)) { + if (stream == null) { + return null; + } + return switch (mode) { + case COMPRESSED_NBT -> NbtIo.readCompressed(stream, NbtAccounter.unlimitedHeap()); + case SNBT -> readSnbtRoot(stream); + }; + } catch (Exception exception) { + log.debug("Failed to parse structure '{}' as {}", target.displayPath(), mode.description, exception); return null; } } + private static CompoundTag readSnbtRoot(InputStream stream) throws Exception { + String snbt = readUtf8WithLimit(stream, MAX_SNBT_BYTES); + if (!snbt.isEmpty() && snbt.charAt(0) == '\ufeff') { + snbt = snbt.substring(1); + } + return new TagParser(new StringReader(snbt)).readStruct(); + } + + private static ParseMode detectParseMode(InputStream stream) throws IOException { + BufferedInputStream buffered = stream instanceof BufferedInputStream b ? b : new BufferedInputStream(stream); + buffered.mark(2); + int first = buffered.read(); + int second = buffered.read(); + buffered.reset(); + return first == 0x1f && second == 0x8b ? ParseMode.COMPRESSED_NBT : ParseMode.SNBT; + } + + private static String readUtf8WithLimit(InputStream stream, int maxBytes) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(Math.min(maxBytes, 8192)); + byte[] buffer = new byte[8192]; + int total = 0; + int read; + while ((read = stream.read(buffer)) != -1) { + total += read; + if (total > maxBytes) { + throw new IOException("SNBT payload exceeds " + maxBytes + " bytes limit"); + } + output.write(buffer, 0, read); + } + return output.toString(StandardCharsets.UTF_8); + } + + private enum ParseMode { + COMPRESSED_NBT("compressed NBT"), + SNBT("SNBT"); + + private final String description; + + ParseMode(String description) { + this.description = description; + } + } + /** * Normalize SNBT variants into the vanilla StructureTemplate NBT format. * From 9c3cb12114a853d3ebaea4f5548b1c4bc6221086 Mon Sep 17 00:00:00 2001 From: Gugle Date: Fri, 8 May 2026 17:21:06 +0800 Subject: [PATCH 3/3] refactor(MDNBTStructureComponent): streamline method signatures and enhance readability --- .../extend/MDNBTStructureComponent.java | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java b/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java index 558db05..80f88df 100644 --- a/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java +++ b/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/markdown/component/extend/MDNBTStructureComponent.java @@ -1,5 +1,6 @@ package dev.anvilcraft.resource.ageratum.client.feat.markdown.component.extend; +import com.mojang.brigadier.StringReader; import dev.anvilcraft.resource.ageratum.client.AgeratumClient; import dev.anvilcraft.resource.ageratum.client.constants.AgeratumConstants; import dev.anvilcraft.resource.ageratum.client.feat.markdown.MDExtensionContext; @@ -34,7 +35,6 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; import net.minecraft.world.phys.BlockHitResult; -import com.mojang.brigadier.StringReader; import org.lwjgl.glfw.GLFW; import java.io.BufferedInputStream; @@ -45,9 +45,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.annotation.Nullable; /** @@ -126,13 +128,14 @@ public void render(MDRenderContext context) { this.cameraRig.setZoom(2.0f); this.cameraRig.setOffsetX(this.panOffsetX); this.cameraRig.setOffsetY(context.screenHeight() / 2.0f - this.contentHeight + this.bottomHeight / 2.0f - context.offsetY() + this.panOffsetY); - StructurePreviewRenderer.getInstance().render( - this.previewLevel, - this.cameraRig, - graphics.bufferSource(), - this.visibleMinY, - this.visibleMinY + this.visibleLayerCount - ); + StructurePreviewRenderer.getInstance() + .render( + this.previewLevel, + this.cameraRig, + graphics.bufferSource(), + this.visibleMinY, + this.visibleMinY + this.visibleLayerCount + ); this.renderLayerIndicator(context, graphics); this.renderButton(context); context.disableScissor(); @@ -160,15 +163,7 @@ private void renderButton(MDRenderContext context) { } @Override - public boolean keyPressed( - Minecraft minecraft, - double mouseX, - double mouseY, - int keyCode, - int scanCode, - int modifiers, - int maxX - ) { + public boolean keyPressed(Minecraft minecraft, double mouseX, double mouseY, int keyCode, int scanCode, int modifiers, int maxX) { if (this.previewLevel == null) { return false; } @@ -234,15 +229,7 @@ public boolean mouseClicked(Minecraft minecraft, double mouseX, double mouseY, i } @Override - public boolean mouseDragged( - Minecraft minecraft, - double mouseX, - double mouseY, - int button, - double dragX, - double dragY, - int maxX - ) { + public boolean mouseDragged(Minecraft minecraft, double mouseX, double mouseY, int button, double dragX, double dragY, int maxX) { if (button != this.dragButton) { return false; } @@ -446,8 +433,7 @@ private static String readUtf8WithLimit(InputStream stream, int maxBytes) throws } private enum ParseMode { - COMPRESSED_NBT("compressed NBT"), - SNBT("SNBT"); + COMPRESSED_NBT("compressed NBT"), SNBT("SNBT"); private final String description; @@ -484,11 +470,12 @@ private static CompoundTag normalizeStructureRoot(CompoundTag root) { // Build palette entries in a deterministic order. List paletteStates = new ArrayList<>(); + Set seenPaletteStates = new HashSet<>(); if (paletteIsStringList) { ListTag paletteStrings = (ListTag) converted.get("palette"); for (int i = 0; i < paletteStrings.size(); i++) { String state = paletteStrings.getString(i); - if (!state.isBlank()) { + if (!state.isBlank() && seenPaletteStates.add(state)) { paletteStates.add(state); } } @@ -499,7 +486,7 @@ private static CompoundTag normalizeStructureRoot(CompoundTag root) { for (int i = 0; i < dataList.size(); i++) { CompoundTag entry = dataList.getCompound(i); String state = entry.getString("state"); - if (!state.isBlank() && !paletteStates.contains(state)) { + if (!state.isBlank() && seenPaletteStates.add(state)) { paletteStates.add(state); } } @@ -654,9 +641,7 @@ private static boolean endsWithStructureExtension(String path) { private static List expandStructureExtensions(String path) { String normalized = path.replace('\\', '/'); - return endsWithStructureExtension(normalized) - ? List.of(normalized) - : List.of(normalized + ".nbt", normalized + ".snbt"); + return endsWithStructureExtension(normalized) ? List.of(normalized) : List.of(normalized + ".nbt", normalized + ".snbt"); } private static String getCurrentDirectoryPath(ResourceLocation location) {