From 7c8958bb7c23a2643a9be60835c1598585550e16 Mon Sep 17 00:00:00 2001 From: Fenad2 Date: Fri, 7 Aug 2026 00:35:00 +0800 Subject: [PATCH 1/7] feat --- gradle.properties | 3 +- .../com/listmore/config/ListMoreConfigs.java | 8 +- .../GuiSchematicBrowserBaseMixin.java | 41 ++ .../schematic/SchematicPreviewDirection.java | 20 + .../schematic/SchematicPreviewLayout.java | 70 +++ .../schematic/SchematicPreviewLoader.java | 42 ++ .../schematic/SchematicPreviewModel.java | 162 +++++ .../schematic/SchematicPreviewOverlay.java | 135 +++++ .../SchematicPreviewRenderBackend.java | 21 + .../schematic/SchematicPreviewRenderer.java | 86 +++ .../schematic/SchematicPreviewSession.java | 119 ++++ .../schematic/SchematicPreviewTransform.java | 66 +++ .../resources/assets/listmore/lang/en_us.json | 10 +- .../resources/assets/listmore/lang/zh_cn.json | 10 +- src/main/resources/fabric.mod.json | 1 + src/main/resources/listmore.mixins.json | 3 +- versions/1.21.10-fabric/gradle.properties | 1 + versions/1.21.11-fabric/gradle.properties | 1 + versions/1.21.8-fabric/build.gradle | 6 + versions/1.21.8-fabric/gradle.properties | 3 +- versions/26.1-fabric/gradle.properties | 1 + versions/26.2-fabric/gradle.properties | 1 + .../schematic/ListMoreSchematicBrowser.java | 155 +++++ .../schematic/SchematicPreviewRenderer26.java | 446 ++++++++++++++ .../schematic/SchematicPreviewWorld26.java | 556 ++++++++++++++++++ 25 files changed, 1961 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/listmore/mixin/litematica/GuiSchematicBrowserBaseMixin.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewDirection.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewLayout.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewLoader.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewModel.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewOverlay.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewRenderBackend.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewRenderer.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewSession.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewTransform.java create mode 100644 versions/26.2-fabric/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java create mode 100644 versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewRenderer26.java create mode 100644 versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewWorld26.java diff --git a/gradle.properties b/gradle.properties index 8866ed3..903e110 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,8 +3,9 @@ org.gradle.jvmargs=-Xmx1G org.gradle.configuration-cache=false -mod_version=1.0.0 +mod_version=1.0.1 archives_base_name=ListMore-Fabric maven_group=com.listmore loom_version=1.17-SNAPSHOT preprocessor_version=c5abb4fb12 +conditionalmixin_version=0.6.4 diff --git a/src/main/java/com/listmore/config/ListMoreConfigs.java b/src/main/java/com/listmore/config/ListMoreConfigs.java index 1a65950..f34920d 100644 --- a/src/main/java/com/listmore/config/ListMoreConfigs.java +++ b/src/main/java/com/listmore/config/ListMoreConfigs.java @@ -49,6 +49,7 @@ public static class Generic { public static final ConfigBoolean TNT_EXPLOSION_PREVIEW = createTntExplosionPreview(); public static final ConfigOptionList TNT_EXPLOSION_PREVIEW_MODE = createTntExplosionPreviewMode(); public static final ConfigBoolean PROJECTILE_LANDING_PREDICTION = createProjectileLandingPrediction(); + public static final ConfigBoolean SCHEMATIC_DYNAMIC_PREVIEW = createSchematicDynamicPreview(); public static final ImmutableList OPTIONS = ImmutableList.of( COPY_TARGET_ID, @@ -62,7 +63,8 @@ public static class Generic { INVALID_FURNACE_INPUT_HIGHLIGHTER_RANGE, TNT_EXPLOSION_PREVIEW, TNT_EXPLOSION_PREVIEW_MODE, - PROJECTILE_LANDING_PREDICTION + PROJECTILE_LANDING_PREDICTION, + SCHEMATIC_DYNAMIC_PREVIEW ); } @@ -120,6 +122,10 @@ private static ConfigBoolean createProjectileLandingPrediction() { return createConfig(new ConfigBoolean("projectileLandingPrediction", false)); } + private static ConfigBoolean createSchematicDynamicPreview() { + return createConfig(new ConfigBoolean("schematicDynamicPreview", false)); + } + private static > T createConfig(T config) { return createConfig(config, value -> INSTANCE.save()); } diff --git a/src/main/java/com/listmore/mixin/litematica/GuiSchematicBrowserBaseMixin.java b/src/main/java/com/listmore/mixin/litematica/GuiSchematicBrowserBaseMixin.java new file mode 100644 index 0000000..1a59c7f --- /dev/null +++ b/src/main/java/com/listmore/mixin/litematica/GuiSchematicBrowserBaseMixin.java @@ -0,0 +1,41 @@ +package com.listmore.mixin.litematica; + +import com.listmore.config.ListMoreConfigs; +import fi.dy.masa.litematica.gui.GuiSchematicBrowserBase; +import fi.dy.masa.litematica.gui.widgets.WidgetSchematicBrowser; +import fi.dy.masa.malilib.gui.interfaces.ISelectionListener; +import fi.dy.masa.malilib.gui.widgets.WidgetFileBrowserBase.DirectoryEntry; +import me.fallenbreath.conditionalmixin.api.annotation.Condition; +import me.fallenbreath.conditionalmixin.api.annotation.Restriction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Pseudo +@Restriction(require = @Condition("litematica")) +@Mixin(value = GuiSchematicBrowserBase.class, remap = false) +public abstract class GuiSchematicBrowserBaseMixin { + @Shadow protected abstract ISelectionListener getSelectionListener(); + + @Inject(method = "createListWidget(II)Lfi/dy/masa/litematica/gui/widgets/WidgetSchematicBrowser;", + at = @At("HEAD"), cancellable = true, remap = false) + private void listmore$createPreviewBrowser(int listX, int listY, + CallbackInfoReturnable cir) { + if (!ListMoreConfigs.Generic.SCHEMATIC_DYNAMIC_PREVIEW.getBooleanValue()) { + return; + } + try { + Class browserClass = Class.forName("com.listmore.schematic.ListMoreSchematicBrowser"); + Object browser = browserClass.getConstructor(int.class, int.class, int.class, int.class, + GuiSchematicBrowserBase.class, ISelectionListener.class) + .newInstance(listX, listY, 100, 100, (GuiSchematicBrowserBase) (Object) this, + this.getSelectionListener()); + cir.setReturnValue((WidgetSchematicBrowser) browser); + } catch (ReflectiveOperationException ignored) { + // The version-specific preview browser is intentionally optional. + } + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewDirection.java b/src/main/java/com/listmore/schematic/SchematicPreviewDirection.java new file mode 100644 index 0000000..6f91a06 --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewDirection.java @@ -0,0 +1,20 @@ +package com.listmore.schematic; + +/** 预览区域中的八个固定观察方向。 */ +public enum SchematicPreviewDirection { + NORTH("↑"), + WEST("←"), + CENTER("↻"), + EAST("→"), + SOUTH("↓"); + + private final String label; + + SchematicPreviewDirection(String label) { + this.label = label; + } + + public String label() { + return this.label; + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewLayout.java b/src/main/java/com/listmore/schematic/SchematicPreviewLayout.java new file mode 100644 index 0000000..f4d6fb2 --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewLayout.java @@ -0,0 +1,70 @@ +package com.listmore.schematic; + +/** 原理图浏览器右侧预览区域及方向按钮的纯布局计算。 */ +public final class SchematicPreviewLayout { + private static final int BUTTON_SIZE = 16; + private static final int BUTTON_GAP = 2; + private static final int EDGE_OFFSET = 4; + + private final int x; + private final int y; + private final int width; + private final int height; + + public SchematicPreviewLayout(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = Math.max(0, width); + this.height = Math.max(0, height); + } + + public int x() { return this.x; } + public int y() { return this.y; } + public int width() { return this.width; } + public int height() { return this.height; } + + /** 模型实际可绘制的区域,底部方向按钮不覆盖这个区域。 */ + public int contentX() { return this.x + 1; } + public int contentY() { return this.y + 1; } + public int contentWidth() { return Math.max(0, this.width - 2); } + public int contentHeight() { return Math.max(0, this.height - 2); } + + public boolean contains(double mouseX, double mouseY) { + return mouseX >= this.x && mouseX < this.x + this.width + && mouseY >= this.y && mouseY < this.y + this.height; + } + + public ButtonBounds buttonBounds(SchematicPreviewDirection direction) { + int centerX = this.x + this.width / 2; + int centerY = this.y + this.height / 2; + return switch (direction) { + case NORTH -> new ButtonBounds(centerX - BUTTON_SIZE / 2, this.y + EDGE_OFFSET, + BUTTON_SIZE, BUTTON_SIZE); + case WEST -> new ButtonBounds(this.x + EDGE_OFFSET, centerY - BUTTON_SIZE / 2, + BUTTON_SIZE, BUTTON_SIZE); + case CENTER -> new ButtonBounds(this.x + this.width - BUTTON_SIZE - EDGE_OFFSET, + this.y + this.height - BUTTON_SIZE - EDGE_OFFSET, BUTTON_SIZE, BUTTON_SIZE); + case EAST -> new ButtonBounds(this.x + this.width - BUTTON_SIZE - EDGE_OFFSET, + centerY - BUTTON_SIZE / 2, BUTTON_SIZE, BUTTON_SIZE); + case SOUTH -> new ButtonBounds(centerX - BUTTON_SIZE / 2, + this.y + this.height - BUTTON_SIZE - EDGE_OFFSET, BUTTON_SIZE, BUTTON_SIZE); + }; + } + + public ButtonBounds zoomOutBounds() { + return new ButtonBounds(this.x + this.width - BUTTON_SIZE * 2 - BUTTON_GAP - EDGE_OFFSET, + this.y + EDGE_OFFSET, BUTTON_SIZE, BUTTON_SIZE); + } + + public ButtonBounds zoomInBounds() { + return new ButtonBounds(this.x + this.width - BUTTON_SIZE - EDGE_OFFSET, + this.y + EDGE_OFFSET, BUTTON_SIZE, BUTTON_SIZE); + } + + public record ButtonBounds(int x, int y, int width, int height) { + public boolean contains(double mouseX, double mouseY) { + return mouseX >= this.x && mouseX < this.x + this.width + && mouseY >= this.y && mouseY < this.y + this.height; + } + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewLoader.java b/src/main/java/com/listmore/schematic/SchematicPreviewLoader.java new file mode 100644 index 0000000..8501e30 --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewLoader.java @@ -0,0 +1,42 @@ +package com.listmore.schematic; + +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.listmore.ListMore; +import fi.dy.masa.litematica.schematic.LitematicaSchematic; + +/** 负责在后台读取 litematic 文件,不持有也不修改 Litematica 的全局投影状态。 */ +public final class SchematicPreviewLoader { + private static final ConcurrentMap> CACHE = new ConcurrentHashMap<>(); + + private SchematicPreviewLoader() { + } + + public static CompletableFuture load(Path file) { + Path normalizedFile = file.toAbsolutePath().normalize(); + CompletableFuture task = CACHE.computeIfAbsent(normalizedFile, SchematicPreviewLoader::startLoad); + task.whenComplete((schematic, throwable) -> { + if (throwable != null || schematic == null) { + CACHE.remove(normalizedFile, task); + } + }); + return task; + } + + private static CompletableFuture startLoad(Path file) { + return CompletableFuture.supplyAsync(() -> { + ListMore.LOGGER.info("Schematic preview: loading {}", file); + try { + LitematicaSchematic schematic = LitematicaSchematic.createFromFile(file.getParent(), file.getFileName().toString()); + ListMore.LOGGER.info("Schematic preview: loaded {} (regions={})", file, schematic != null ? schematic.getAreas().size() : 0); + return schematic; + } catch (Throwable throwable) { + ListMore.LOGGER.error("Schematic preview: failed to load {}", file, throwable); + throw throwable; + } + }); + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewModel.java b/src/main/java/com/listmore/schematic/SchematicPreviewModel.java new file mode 100644 index 0000000..b786cab --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewModel.java @@ -0,0 +1,162 @@ +package com.listmore.schematic; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import fi.dy.masa.litematica.schematic.container.LitematicaBlockStateContainer; +import fi.dy.masa.litematica.selection.Box; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +/** + * 原理图预览使用的只读方块快照。 + * 方块坐标相对于整个原理图的最小角,center 始终是完整包围盒的中心。 + */ +public final class SchematicPreviewModel { + private final Vec3i size; + private final float centerX; + private final float centerY; + private final float centerZ; + private final List blocks; + private final Map statesByPosition; + private final Map blockEntitiesByPosition; + + private SchematicPreviewModel(Vec3i size, float centerX, float centerY, float centerZ, List blocks, + Map statesByPosition, Map blockEntitiesByPosition) { + this.size = size; + this.centerX = centerX; + this.centerY = centerY; + this.centerZ = centerZ; + this.blocks = List.copyOf(blocks); + this.statesByPosition = Map.copyOf(statesByPosition); + this.blockEntitiesByPosition = Map.copyOf(blockEntitiesByPosition); + } + + public Vec3i size() { return this.size; } + public float centerX() { return this.centerX; } + public float centerY() { return this.centerY; } + public float centerZ() { return this.centerZ; } + public List blocks() { return this.blocks; } + + public Object blockEntityDataAt(int x, int y, int z) { + return this.blockEntitiesByPosition.get(packPosition(x, y, z)); + } + + /** + * 查询预览坐标中的方块状态。 + * 坐标以原理图整体包围盒的最小角为原点,范围外和空气位置均返回空气, + * 因此渲染后端不需要读取客户端当前世界。 + */ + public BlockState blockStateAt(int x, int y, int z) { + if (x < 0 || y < 0 || z < 0 || x >= this.size.getX() || y >= this.size.getY() || z >= this.size.getZ()) { + return Blocks.AIR.defaultBlockState(); + } + return this.statesByPosition.getOrDefault(packPosition(x, y, z), Blocks.AIR.defaultBlockState()); + } + + /** 从 Litematica 已解析的数据中提取非空气方块,不创建或修改投影世界。 */ + public static SchematicPreviewModel from(LitematicaSchematic schematic) { + Map areas = schematic.getAreas(); + if (areas.isEmpty()) { + return empty(); + } + + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxXExclusive = Integer.MIN_VALUE; + int maxYExclusive = Integer.MIN_VALUE; + int maxZExclusive = Integer.MIN_VALUE; + for (Map.Entry entry : areas.entrySet()) { + LitematicaBlockStateContainer container = schematic.getSubRegionContainer(entry.getKey()); + Box area = entry.getValue(); + BlockPos first = area.getPos1(); + BlockPos second = area.getPos2(); + if (container == null || first == null || second == null) { + continue; + } + int originX = Math.min(first.getX(), second.getX()); + int originY = Math.min(first.getY(), second.getY()); + int originZ = Math.min(first.getZ(), second.getZ()); + Vec3i regionSize = container.getSize(); + minX = Math.min(minX, originX); + minY = Math.min(minY, originY); + minZ = Math.min(minZ, originZ); + maxXExclusive = Math.max(maxXExclusive, originX + regionSize.getX()); + maxYExclusive = Math.max(maxYExclusive, originY + regionSize.getY()); + maxZExclusive = Math.max(maxZExclusive, originZ + regionSize.getZ()); + } + if (minX == Integer.MAX_VALUE) { + return empty(); + } + + List blocks = new ArrayList<>(); + Map statesByPosition = new HashMap<>(); + Map blockEntitiesByPosition = new HashMap<>(); + for (Map.Entry entry : areas.entrySet()) { + LitematicaBlockStateContainer container = schematic.getSubRegionContainer(entry.getKey()); + BlockPos first = entry.getValue().getPos1(); + BlockPos second = entry.getValue().getPos2(); + if (container == null || first == null || second == null) { + continue; + } + Vec3i regionSize = container.getSize(); + int originX = Math.min(first.getX(), second.getX()); + int originY = Math.min(first.getY(), second.getY()); + int originZ = Math.min(first.getZ(), second.getZ()); + for (int y = 0; y < regionSize.getY(); y++) { + for (int z = 0; z < regionSize.getZ(); z++) { + for (int x = 0; x < regionSize.getX(); x++) { + BlockState state = container.get(x, y, z); + if (!state.isAir()) { + int relativeX = x + originX - minX; + int relativeY = y + originY - minY; + int relativeZ = z + originZ - minZ; + long position = packPosition(relativeX, relativeY, relativeZ); + if (statesByPosition.putIfAbsent(position, state) == null) { + blocks.add(new Block(relativeX, relativeY, relativeZ, state)); + } + } + } + } + } + Map blockEntities = schematic.getBlockEntityMapForRegion(entry.getKey()); + if (blockEntities != null) { + for (Map.Entry blockEntity : blockEntities.entrySet()) { + if (blockEntity.getValue() == null) { + continue; + } + BlockPos position = blockEntity.getKey(); + int relativeX = position.getX() + originX - minX; + int relativeY = position.getY() + originY - minY; + int relativeZ = position.getZ() + originZ - minZ; + blockEntitiesByPosition.putIfAbsent(packPosition(relativeX, relativeY, relativeZ), blockEntity.getValue()); + } + } + } + + Vec3i size = new Vec3i(maxXExclusive - minX, maxYExclusive - minY, maxZExclusive - minZ); + return new SchematicPreviewModel(size, size.getX() * 0.5F, size.getY() * 0.5F, size.getZ() * 0.5F, + blocks, statesByPosition, blockEntitiesByPosition); + } + + private static SchematicPreviewModel empty() { + return new SchematicPreviewModel(BlockPos.ZERO, 0.0F, 0.0F, 0.0F, List.of(), Map.of(), Map.of()); + } + + /** + * 每个轴保留 21 位;原理图尺寸远小于该上限,同时避免为每次邻接方块查询创建 BlockPos 对象。 + */ + private static long packPosition(int x, int y, int z) { + return ((long) x << 42) | ((long) y << 21) | z; + } + + /** 单个非空气方块及其相对原理图坐标。 */ + public record Block(int x, int y, int z, BlockState state) { + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewOverlay.java b/src/main/java/com/listmore/schematic/SchematicPreviewOverlay.java new file mode 100644 index 0000000..c35c826 --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewOverlay.java @@ -0,0 +1,135 @@ +package com.listmore.schematic; + +import fi.dy.masa.malilib.render.RenderUtils; +import fi.dy.masa.malilib.util.StringUtils; + +//#if MC >= 1.21.11 +//$$ import fi.dy.masa.malilib.render.GuiContext; +//#else +import net.minecraft.client.gui.GuiGraphics; +//#endif + +/** 原理图浏览器右侧区域的 GUI 覆盖层。实际方块渲染由后续独立渲染器负责。 */ +public final class SchematicPreviewOverlay { + private static final int BACKGROUND_COLOR = 0xEE111820; + private static final int BORDER_COLOR = 0xFF5C7185; + private static final int BUTTON_COLOR = 0xFF263846; + private static final int BUTTON_HOVER_COLOR = 0xFF3E6572; + + private SchematicPreviewOverlay() { + } + + /** 绘制预览背景、加载状态和八方向操作按钮。 */ + public static void draw( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + SchematicPreviewLayout layout, SchematicPreviewSession session, int mouseX, int mouseY) { + RenderUtils.drawRect(context, layout.x(), layout.y(), layout.width(), layout.height(), BACKGROUND_COLOR); + drawBorder(context, layout); + + if (session.isLoading()) { + drawCentered(context, layout, StringUtils.translate("listmore.schematic_preview.loading")); + } else if (session.hasFailure()) { + drawCentered(context, layout, StringUtils.translate("listmore.schematic_preview.failed")); + } else if (session.model() != null) { + boolean rendered = session.renderer().render(context, layout, session.transform()); + if (!rendered) { + drawModelPlaceholder(context, layout, session.model().blocks().size()); + } + } else { + drawCentered(context, layout, StringUtils.translate("listmore.schematic_preview.empty")); + } + + for (SchematicPreviewDirection direction : SchematicPreviewDirection.values()) { + SchematicPreviewLayout.ButtonBounds button = layout.buttonBounds(direction); + drawButton(context, button, direction.label(), mouseX, mouseY); + } + drawButton(context, layout.zoomOutBounds(), "-", mouseX, mouseY); + drawButton(context, layout.zoomInBounds(), "+", mouseX, mouseY); + } + + private static void drawButton( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + SchematicPreviewLayout.ButtonBounds button, String label, int mouseX, int mouseY) { + int color = button.contains(mouseX, mouseY) ? BUTTON_HOVER_COLOR : BUTTON_COLOR; + RenderUtils.drawRect(context, button.x(), button.y(), button.width(), button.height(), color); + drawText(context, button.x() + 5, button.y() + 4, 0xFFFFFFFF, label); + } + + private static void drawBorder( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + SchematicPreviewLayout layout) { + RenderUtils.drawRect(context, layout.x(), layout.y(), layout.width(), 1, BORDER_COLOR); + RenderUtils.drawRect(context, layout.x(), layout.y() + layout.height() - 1, layout.width(), 1, BORDER_COLOR); + RenderUtils.drawRect(context, layout.x(), layout.y(), 1, layout.height(), BORDER_COLOR); + RenderUtils.drawRect(context, layout.x() + layout.width() - 1, layout.y(), 1, layout.height(), BORDER_COLOR); + } + + private static void drawModelPlaceholder( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + SchematicPreviewLayout layout, int blockCount) { + int centerX = layout.x() + layout.width() / 2; + int centerY = layout.y() + Math.max(20, (layout.height() - 56) / 2); + int halfWidth = Math.min(36, Math.max(12, layout.width() / 4)); + int halfHeight = Math.min(25, Math.max(8, layout.height() / 6)); + RenderUtils.drawRect(context, centerX - halfWidth, centerY - halfHeight, halfWidth * 2, 1, 0xFF8CC6A2); + RenderUtils.drawRect(context, centerX - halfWidth, centerY + halfHeight, halfWidth * 2, 1, 0xFF8CC6A2); + RenderUtils.drawRect(context, centerX - halfWidth, centerY - halfHeight, 1, halfHeight * 2, 0xFF8CC6A2); + RenderUtils.drawRect(context, centerX + halfWidth, centerY - halfHeight, 1, halfHeight * 2, 0xFF8CC6A2); + drawCenteredAt(context, centerX, centerY - 4, 0xFFBFD9C7, + StringUtils.translate("listmore.schematic_preview.building")); + drawCenteredAt(context, centerX, centerY + 8, 0xFF9FB4C2, + StringUtils.translate("listmore.schematic_preview.blocks", blockCount)); + } + + private static void drawCentered( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + SchematicPreviewLayout layout, String text) { + int x = layout.x() + Math.max(4, (layout.width() - text.length() * 6) / 2); + int y = layout.y() + Math.max(8, layout.height() / 2 - 4); + drawText(context, x, y, 0xFFB9C6D0, text); + } + + private static void drawCenteredAt( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + int centerX, int y, int color, String text) { + drawText(context, centerX - text.length() * 3, y, color, text); + } + + private static void drawText( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + int x, int y, int color, String text) { + //#if MC >= 1.21.11 + //$$ StringUtils.drawString(context, x, y, color, text); + //#else + StringUtils.drawString(x, y, color, text, context); + //#endif + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewRenderBackend.java b/src/main/java/com/listmore/schematic/SchematicPreviewRenderBackend.java new file mode 100644 index 0000000..b7b71d8 --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewRenderBackend.java @@ -0,0 +1,21 @@ +package com.listmore.schematic; + +/** + * 原理图预览的版本专属渲染后端。 + * + *

实现类位于各 Minecraft 子版本的源码目录中。公共预览逻辑只通过这个接口调用, + * 因此不需要在共同源码内引用某个版本独有的图形 API。

+ */ +public interface SchematicPreviewRenderBackend extends AutoCloseable { + /** Supplies the parsed schematic to version-specific read-only world adapters. */ + default void setSchematic(Object schematic) { + } + + /** 将当前快照绘制到预览区域;无法绘制时返回 {@code false}。 */ + boolean render(Object context, SchematicPreviewLayout layout, SchematicPreviewTransform transform, + SchematicPreviewModel model, long revision); + + @Override + default void close() { + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewRenderer.java b/src/main/java/com/listmore/schematic/SchematicPreviewRenderer.java new file mode 100644 index 0000000..d1cb804 --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewRenderer.java @@ -0,0 +1,86 @@ +package com.listmore.schematic; + +//#if MC >= 1.21.11 +//$$ import fi.dy.masa.malilib.render.GuiContext; +//#else +import net.minecraft.client.gui.GuiGraphics; +//#endif + +/** + * 原理图预览渲染器的公共生命周期入口。 + * + *

它只持有 ListMore 自己生成的只读模型快照,不读取或修改 Litematica 的全局投影世界、 + * 投影摆放和世界渲染器。不同 Minecraft 图形管线的具体实现放在独立适配器中。

+ */ +public final class SchematicPreviewRenderer implements AutoCloseable { + private SchematicPreviewModel model; + private Object schematic; + private long modelRevision; + private final SchematicPreviewRenderBackend backend = createBackend(); + + /** + * 提交新的原理图快照。 + * 模型只会在 GUI 线程调用此方法,因此适配器可以在下一帧安全地重建自己的网格和帧缓冲资源。 + */ + public void setModel(SchematicPreviewModel model) { + if (this.model == model) { + return; + } + this.model = model; + this.modelRevision++; + } + + public void setSchematic(Object schematic) { + this.schematic = schematic; + if (this.backend != null) { + this.backend.setSchematic(schematic); + } + } + + /** 返回当前快照的递增版本号,供版本渲染适配器判断是否需要重建网格。 */ + public long modelRevision() { + return this.modelRevision; + } + + /** + * 将预览模型绘制到右侧的信息面板中。 + * 渲染后端和 GUI 上下文的 API 差异被限制在这里,公共会话层不需要了解 Minecraft 的图形管线。 + */ + public boolean render( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + SchematicPreviewLayout layout, SchematicPreviewTransform transform) { + return this.backend != null && this.backend.render(context, layout, transform, this.model, this.modelRevision); + } + + public SchematicPreviewModel model() { + return this.model; + } + + /** + * 预览浏览器关闭时释放渲染器持有的快照。 + * GPU 帧缓冲和网格资源会由对应版本的渲染适配器在这里一并释放。 + */ + @Override + public void close() { + if (this.backend != null) { + this.backend.close(); + } + this.model = null; + this.schematic = null; + this.modelRevision++; + } + + private static SchematicPreviewRenderBackend createBackend() { + try { + Class backendClass = Class.forName("com.listmore.schematic.SchematicPreviewRenderer26"); + return backendClass.asSubclass(SchematicPreviewRenderBackend.class).getConstructor().newInstance(); + } catch (ReflectiveOperationException ignored) { + // 当前版本尚未提供专属后端时保留 GUI 占位预览。 + return null; + } + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewSession.java b/src/main/java/com/listmore/schematic/SchematicPreviewSession.java new file mode 100644 index 0000000..0704b51 --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewSession.java @@ -0,0 +1,119 @@ +package com.listmore.schematic; + +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; + +import com.listmore.ListMore; +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import net.minecraft.core.Vec3i; + +/** + * 单个原理图浏览器的预览生命周期。 + * 异步加载完成后会检查 generation,避免之前选择的文件覆盖当前预览。 + */ +public final class SchematicPreviewSession { + private final AtomicLong generation = new AtomicLong(); + private final SchematicPreviewTransform transform = new SchematicPreviewTransform(); + private final SchematicPreviewRenderer renderer = new SchematicPreviewRenderer(); + private Path file; + private CompletableFuture loadingTask; + private SchematicPreviewModel model; + private Throwable loadFailure; + /** 后台加载线程写入,GUI 绘制线程读取。 */ + private volatile LoadResult pendingResult; + private boolean closed; + + public SchematicPreviewTransform transform() { + return this.transform; + } + + /** 返回当前浏览器专属的预览渲染器。 */ + public SchematicPreviewRenderer renderer() { + return this.renderer; + } + + public SchematicPreviewModel model() { + return this.model; + } + + public Vec3i size() { + return this.model != null ? this.model.size() : null; + } + + public boolean isLoading() { + return this.loadingTask != null && !this.loadingTask.isDone(); + } + + public boolean hasFailure() { + return this.loadFailure != null; + } + + /** 仅在用户在原理图列表中选中新的 litematic 文件时启动新的加载任务。 */ + public void setFile(Path file) { + Path normalizedFile = file.toAbsolutePath().normalize(); + if (this.closed || normalizedFile.equals(this.file)) { + return; + } + + this.file = normalizedFile; + ListMore.LOGGER.info("Schematic preview: selected {}", normalizedFile); + this.model = null; + this.renderer.close(); + this.loadFailure = null; + this.pendingResult = null; + this.transform.reset(); + long requestGeneration = this.generation.incrementAndGet(); + CompletableFuture task = SchematicPreviewLoader.load(normalizedFile); + this.loadingTask = task; + task.whenComplete((loaded, throwable) -> { + if (this.closed || requestGeneration != this.generation.get() || task != this.loadingTask) { + return; + } + this.pendingResult = new LoadResult(requestGeneration, loaded, throwable); + }); + } + + /** 在 GUI 绘制线程应用后台加载结果,避免后台线程修改渲染状态。 */ + public void update() { + LoadResult result = this.pendingResult; + if (result == null || result.generation() != this.generation.get()) { + return; + } + + this.pendingResult = null; + if (result.throwable() != null) { + ListMore.LOGGER.error("Schematic preview: load result failed for {}", this.file, result.throwable()); + this.loadFailure = result.throwable(); + return; + } + if (result.schematic() == null) { + this.loadFailure = new IllegalStateException("Litematica returned no schematic"); + return; + } + + try { + this.model = SchematicPreviewModel.from(result.schematic()); + ListMore.LOGGER.info("Schematic preview: model ready for {} (size={}x{}x{}, nonAirBlocks={})", this.file, + this.model.size().getX(), this.model.size().getY(), this.model.size().getZ(), this.model.blocks().size()); + this.renderer.setSchematic(result.schematic()); + this.renderer.setModel(this.model); + } catch (Throwable throwable) { + ListMore.LOGGER.error("Schematic preview: model creation failed for {}", this.file, throwable); + this.loadFailure = throwable; + } + } + + /** 关闭浏览器时取消仍在等待的任务,并使所有回调失效。 */ + public void close() { + this.closed = true; + this.generation.incrementAndGet(); + this.loadingTask = null; + this.pendingResult = null; + this.model = null; + this.renderer.close(); + } + + private record LoadResult(long generation, LitematicaSchematic schematic, Throwable throwable) { + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewTransform.java b/src/main/java/com/listmore/schematic/SchematicPreviewTransform.java new file mode 100644 index 0000000..902715c --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewTransform.java @@ -0,0 +1,66 @@ +package com.listmore.schematic; + +/** Camera state for the fixed preview directions. */ +public final class SchematicPreviewTransform { + private static final float DEFAULT_YAW = 45.0F; + private static final float DEFAULT_PITCH = -25.0F; + private static final float ROTATION_STEP = 45.0F; + private static final float MAX_PITCH = 89.0F; + private static final float ZOOM_STEP = 1.25F; + private static final float MIN_ZOOM = 0.5F; + private static final float MAX_ZOOM = 2.0F; + + private float yaw = DEFAULT_YAW; + private float pitch = DEFAULT_PITCH; + private float zoom = 1.0F; + + public float yaw() { + return this.yaw; + } + + public float pitch() { + return this.pitch; + } + + public float distance(float sizeX, float sizeY, float sizeZ) { + float radius = (float) Math.sqrt(sizeX * sizeX + sizeY * sizeY + sizeZ * sizeZ) * 0.5F; + return Math.max(4.0F, radius * 2.4F) / this.zoom; + } + + public void reset() { + this.yaw = DEFAULT_YAW; + this.pitch = DEFAULT_PITCH; + this.zoom = 1.0F; + } + + public void zoomIn() { + this.zoom = Math.min(MAX_ZOOM, this.zoom * ZOOM_STEP); + } + + public void zoomOut() { + this.zoom = Math.max(MIN_ZOOM, this.zoom / ZOOM_STEP); + } + + public void applyDirection(SchematicPreviewDirection direction) { + if (direction == SchematicPreviewDirection.CENTER) { + this.reset(); + return; + } + + switch (direction) { + case NORTH -> this.rotate(0.0F, -ROTATION_STEP); + case WEST -> this.rotate(ROTATION_STEP, 0.0F); + case EAST -> this.rotate(-ROTATION_STEP, 0.0F); + case SOUTH -> this.rotate(0.0F, ROTATION_STEP); + case CENTER -> { } + } + } + + private void rotate(float yawDelta, float pitchDelta) { + this.yaw = (this.yaw + yawDelta) % 360.0F; + if (this.yaw < 0.0F) { + this.yaw += 360.0F; + } + this.pitch = Math.max(-MAX_PITCH, Math.min(MAX_PITCH, this.pitch + pitchDelta)); + } +} diff --git a/src/main/resources/assets/listmore/lang/en_us.json b/src/main/resources/assets/listmore/lang/en_us.json index bad3f23..83dbbae 100644 --- a/src/main/resources/assets/listmore/lang/en_us.json +++ b/src/main/resources/assets/listmore/lang/en_us.json @@ -41,5 +41,13 @@ "listmore.config.generic.prettyName.tntExplosionPreviewMode": "TNT Explosion Preview - Display Mode", "listmore.config.generic.comment.tntExplosionPreviewMode": "Choose whether to display possible destruction blocks", "listmore.label.tntExplosionPreviewMode.full": "Full Preview", - "listmore.label.tntExplosionPreviewMode.partial": "Partial Preview" + "listmore.label.tntExplosionPreviewMode.partial": "Partial Preview", + "listmore.config.generic.name.schematicDynamicPreview": "Schematic Dynamic Preview", + "listmore.config.generic.prettyName.schematicDynamicPreview": "Schematic Dynamic Preview", + "listmore.config.generic.comment.schematicDynamicPreview": "Replace Litematica's static schematic thumbnail with a real-time 3D preview", + "listmore.schematic_preview.loading": "Loading preview...", + "listmore.schematic_preview.failed": "Preview load failed", + "listmore.schematic_preview.empty": "Select a litematic file", + "listmore.schematic_preview.building": "Building model...", + "listmore.schematic_preview.blocks": "%d blocks" } diff --git a/src/main/resources/assets/listmore/lang/zh_cn.json b/src/main/resources/assets/listmore/lang/zh_cn.json index a4aa27e..0ed2ef2 100644 --- a/src/main/resources/assets/listmore/lang/zh_cn.json +++ b/src/main/resources/assets/listmore/lang/zh_cn.json @@ -41,5 +41,13 @@ "listmore.config.generic.prettyName.tntExplosionPreviewMode": "TNT爆炸预览-显示模式", "listmore.config.generic.comment.tntExplosionPreviewMode": "选择是否显示可能被炸坏的方块", "listmore.label.tntExplosionPreviewMode.full": "全预览", - "listmore.label.tntExplosionPreviewMode.partial": "半预览" + "listmore.label.tntExplosionPreviewMode.partial": "半预览", + "listmore.config.generic.name.schematicDynamicPreview": "投影实时预览", + "listmore.config.generic.prettyName.schematicDynamicPreview": "投影实时预览", + "listmore.config.generic.comment.schematicDynamicPreview": "将 Litematica 的静态投影缩略图替换为实时 3D 预览", + "listmore.schematic_preview.loading": "正在加载预览...", + "listmore.schematic_preview.failed": "预览加载失败", + "listmore.schematic_preview.empty": "请选择一个 litematic 文件", + "listmore.schematic_preview.building": "正在构建模型...", + "listmore.schematic_preview.blocks": "%d 个方块" } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index ace3e13..41fc1eb 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -33,6 +33,7 @@ "malilib": "*" }, "custom": { + "conditional-mixin": ">=0.2.0", "modmenu:clientsideOnly": true } } diff --git a/src/main/resources/listmore.mixins.json b/src/main/resources/listmore.mixins.json index 0791700..9378b0e 100644 --- a/src/main/resources/listmore.mixins.json +++ b/src/main/resources/listmore.mixins.json @@ -5,7 +5,8 @@ "client": [ "MinecraftMixin", "EntityMixin", - "EntityRenderDispatcherMixin" + "EntityRenderDispatcherMixin", + "litematica.GuiSchematicBrowserBaseMixin" ], "mixins": [ "WidgetConfigOptionMixin", diff --git a/versions/1.21.10-fabric/gradle.properties b/versions/1.21.10-fabric/gradle.properties index b6dd07b..7130583 100644 --- a/versions/1.21.10-fabric/gradle.properties +++ b/versions/1.21.10-fabric/gradle.properties @@ -7,3 +7,4 @@ fabric_api_version=0.138.4+1.21.10 dependencies.api.malilib_version=com.github.sakura-ryoko:malilib:1.21.10-0.26.6 dependencies.api.malilib_version.use_sakura_fork=true dependencies.api.modmenu_version=17.0.0 +litematica_version=com.github.sakura-ryoko:litematica:1.21.10-0.24.5 diff --git a/versions/1.21.11-fabric/gradle.properties b/versions/1.21.11-fabric/gradle.properties index 4f9452a..28a2f47 100644 --- a/versions/1.21.11-fabric/gradle.properties +++ b/versions/1.21.11-fabric/gradle.properties @@ -7,3 +7,4 @@ fabric_api_version=0.141.4+1.21.11 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-1.21.11:0.27.10 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=17.0.0-beta.2 +litematica_version=fi.dy.masa.litematica:litematica-fabric-1.21.11:0.26.12 diff --git a/versions/1.21.8-fabric/build.gradle b/versions/1.21.8-fabric/build.gradle index 3609c99..a0d3cc3 100644 --- a/versions/1.21.8-fabric/build.gradle +++ b/versions/1.21.8-fabric/build.gradle @@ -61,6 +61,7 @@ repositories { dependencies { def autoImplementation = { Object... args -> unobfuscated ? implementation(*args) : modImplementation(*args) } + def autoCompileOnly = { Object... args -> unobfuscated ? compileOnly(*args) : modCompileOnly(*args) } def useSakuraMalilib = project.findProperty('dependencies.api.malilib_version.use_sakura_fork')?.toString()?.toBoolean() ?: false def malilibFabricGa = useSakuraMalilib ? 'com.github.sakura-ryoko:malilib' : 'maven.modrinth:malilib' def malilibVersion = project.findProperty('dependencies.api.malilib_version') @@ -75,6 +76,11 @@ dependencies { autoImplementation "net.fabricmc:fabric-loader:${project.loader_version}" autoImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" autoImplementation malilibCoordinate + autoCompileOnly(project.litematica_version) { + // Litematica 的旧 POM 会传递一个不可用的 malilib 版本,使用项目已配置的版本。 + exclude group: 'com.github.sakura-ryoko', module: 'malilib' + } + autoCompileOnly "me.fallenbreath:conditional-mixin-fabric:${project.conditionalmixin_version}" def modMenuVersion = project.findProperty('dependencies.api.modmenu_version') if (modMenuVersion != null) { diff --git a/versions/1.21.8-fabric/gradle.properties b/versions/1.21.8-fabric/gradle.properties index b477043..3d628cb 100644 --- a/versions/1.21.8-fabric/gradle.properties +++ b/versions/1.21.8-fabric/gradle.properties @@ -7,8 +7,9 @@ fabric_api_version=0.136.1+1.21.8 dependencies.api.malilib_version=com.github.sakura-ryoko:malilib:1.21.8-0.25.7 dependencies.api.malilib_version.use_sakura_fork=true dependencies.api.modmenu_version=15.0.0 +litematica_version=com.github.sakura-ryoko:litematica:1.21.8-0.23.5 # https://github.com/sakura-ryoko/malilib/releases # https://masa.dy.fi/maven/fi/dy/masa/malilib/ -# https://maven.terraformersmc.com/releases/com/terraformersmc/modmenu/ \ No newline at end of file +# https://maven.terraformersmc.com/releases/com/terraformersmc/modmenu/ diff --git a/versions/26.1-fabric/gradle.properties b/versions/26.1-fabric/gradle.properties index 9739aa8..f01b2bf 100644 --- a/versions/26.1-fabric/gradle.properties +++ b/versions/26.1-fabric/gradle.properties @@ -7,3 +7,4 @@ fabric_api_version=0.148.2+26.1.2 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-26.1.2:0.28.4 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=20.0.0-beta.4 +litematica_version=fi.dy.masa.litematica:litematica-fabric-26.1.2:0.27.4 diff --git a/versions/26.2-fabric/gradle.properties b/versions/26.2-fabric/gradle.properties index 917e6d7..f4a2e8a 100644 --- a/versions/26.2-fabric/gradle.properties +++ b/versions/26.2-fabric/gradle.properties @@ -7,3 +7,4 @@ fabric_api_version=0.154.2+26.2 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-26.2:0.29.0 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=20.0.0-beta.4 +litematica_version=fi.dy.masa.litematica:litematica-fabric-26.2:0.28.0 diff --git a/versions/26.2-fabric/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java b/versions/26.2-fabric/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java new file mode 100644 index 0000000..0931959 --- /dev/null +++ b/versions/26.2-fabric/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java @@ -0,0 +1,155 @@ +package com.listmore.schematic; + +import java.util.Date; +import org.jetbrains.annotations.Nullable; + +import fi.dy.masa.litematica.Litematica; +import fi.dy.masa.litematica.gui.GuiSchematicBrowserBase; +import fi.dy.masa.litematica.gui.widgets.WidgetSchematicBrowser; +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import fi.dy.masa.litematica.schematic.SchematicMetadata; +import fi.dy.masa.litematica.schematic.SchematicSchema; +import fi.dy.masa.malilib.gui.interfaces.ISelectionListener; +import fi.dy.masa.malilib.gui.widgets.WidgetFileBrowserBase.DirectoryEntry; +import fi.dy.masa.malilib.render.GuiContext; +import fi.dy.masa.malilib.render.RenderUtils; +import fi.dy.masa.malilib.util.StringUtils; +import fi.dy.masa.malilib.util.data.Schema; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.core.Vec3i; +import org.apache.commons.lang3.tuple.Pair; + +/** 26.2 implementation of the Litematica browser preview extension. */ +public final class ListMoreSchematicBrowser extends WidgetSchematicBrowser { + private final SchematicPreviewSession previewSession = new SchematicPreviewSession(); + @Nullable private SchematicPreviewLayout previewLayout; + private int lastMouseX; + private int lastMouseY; + + public ListMoreSchematicBrowser(int x, int y, int width, int height, GuiSchematicBrowserBase parent, + @Nullable ISelectionListener selectionListener) { + super(x, y, width, height, parent, selectionListener); + } + + @Override + protected void drawAdditionalContents(GuiContext context, int mouseX, int mouseY) { + this.lastMouseX = mouseX; + this.lastMouseY = mouseY; + super.drawAdditionalContents(context, mouseX, mouseY); + } + + @Override + protected void drawSelectedSchematicInfo(GuiContext context, @Nullable DirectoryEntry entry) { + if (!isLitematic(entry)) { + this.previewLayout = null; + super.drawSelectedSchematicInfo(context, entry); + return; + } + + Pair metaPair = this.getSchematicVersionAndMetadata(entry); + if (metaPair == null || metaPair.getRight() == null) { + this.previewLayout = null; + super.drawSelectedSchematicInfo(context, entry); + return; + } + + int x = this.posX + this.totalWidth - this.infoWidth; + int y = this.posY; + int height = Math.min(this.infoHeight, this.parent.getMaxInfoHeight()); + RenderUtils.drawOutlinedBox(context, x, y, this.infoWidth, height, 0xA0000000, COLOR_HORIZONTAL_BAR); + x += 3; + y += 3; + int textColor = 0xC0C0C0C0; + int valueColor = 0xFFFFFFFF; + SchematicMetadata meta = metaPair.getRight(); + SchematicSchema version = metaPair.getLeft(); + + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.name"), x, y, textColor); + y += 12; + this.drawString(context, meta.getName(), x + 4, y, valueColor); + y += 12; + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.schematic_author", meta.getAuthor()), x, y, textColor); + y += 12; + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.time_created", DATE_FORMAT.format(new Date(meta.getTimeCreated()))), x, y, textColor); + y += 12; + if (meta.hasBeenModified()) { + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.time_modified", DATE_FORMAT.format(new Date(meta.getTimeModified()))), x, y, textColor); + y += 12; + } + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.region_count", meta.getRegionCount()), x, y, textColor); + y += 12; + if (this.parent.getScreenHeight() >= 340) { + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.total_volume", meta.getTotalVolume()), x, y, textColor); + y += 12; + if (meta.getTotalBlocks() > 0) { + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.total_blocks", meta.getTotalBlocks()), x, y, textColor); + y += 12; + } + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.enclosing_size"), x, y, textColor); + y += 12; + Vec3i areaSize = meta.getEnclosingSize(); + this.drawString(context, String.format("%d x %d x %d", areaSize.getX(), areaSize.getY(), areaSize.getZ()), x + 4, y, valueColor); + y += 12; + } else { + String size = String.format("%d x %d x %d", meta.getEnclosingSize().getX(), meta.getEnclosingSize().getY(), meta.getEnclosingSize().getZ()); + String total = meta.getTotalBlocks() > 0 + ? StringUtils.translate("litematica.gui.label.schematic_info.total_blocks_and_volume", meta.getTotalBlocks(), meta.getTotalVolume()) + : StringUtils.translate("litematica.gui.label.schematic_info.total_volume", meta.getTotalVolume()); + this.drawString(context, total, x, y, textColor); + y += 12; + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.enclosing_size_value", size), x, y, textColor); + y += 12; + } + if (version != null) { + this.drawString(context, StringUtils.translate("litematica.gui.label.schematic_info.version", version.litematicVersion()), x, y, textColor); + y += 12; + Schema schema = Schema.getSchemaByDataVersion(version.minecraftDataVersion()); + if (schema != null) { + String key = version.minecraftDataVersion() - LitematicaSchematic.MINECRAFT_DATA_VERSION > 100 + ? "litematica.gui.label.schematic_info.schema.newer" : "litematica.gui.label.schematic_info.schema"; + this.drawString(context, StringUtils.translate(key, schema.getString(), version.minecraftDataVersion()), x, y, textColor); + y += 12; + } + } + + y += 12; + int previewHeight = height - (y - this.posY) - 6; + this.previewLayout = previewHeight >= 64 ? new SchematicPreviewLayout(x + 4, y, this.infoWidth - 10, previewHeight) : null; + if (this.previewLayout != null) { + this.previewSession.setFile(entry.getFullPath()); + this.previewSession.update(); + SchematicPreviewOverlay.draw(context, this.previewLayout, this.previewSession, this.lastMouseX, this.lastMouseY); + } + } + + @Override + public boolean onMouseClicked(MouseButtonEvent click, boolean doubleClick) { + if (click.input() == 0 && this.previewLayout != null) { + if (this.previewLayout.zoomOutBounds().contains(click.x(), click.y())) { + this.previewSession.transform().zoomOut(); + return true; + } + if (this.previewLayout.zoomInBounds().contains(click.x(), click.y())) { + this.previewSession.transform().zoomIn(); + return true; + } + for (SchematicPreviewDirection direction : SchematicPreviewDirection.values()) { + if (this.previewLayout.buttonBounds(direction).contains(click.x(), click.y())) { + this.previewSession.transform().applyDirection(direction); + return true; + } + } + } + return super.onMouseClicked(click, doubleClick); + } + + @Override + public void onClose() { + this.previewSession.close(); + super.onClose(); + } + + private static boolean isLitematic(@Nullable DirectoryEntry entry) { + return entry != null && entry.getName().endsWith(LitematicaSchematic.FILE_EXTENSION); + } +} diff --git a/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewRenderer26.java b/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewRenderer26.java new file mode 100644 index 0000000..5f2fe93 --- /dev/null +++ b/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewRenderer26.java @@ -0,0 +1,446 @@ +package com.listmore.schematic; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; + +import com.listmore.ListMore; +import com.mojang.blaze3d.IndexType; +import com.mojang.blaze3d.PrimitiveTopology; +import com.mojang.blaze3d.GpuFormat; +import com.mojang.blaze3d.ProjectionType; +import com.mojang.blaze3d.buffers.GpuBuffer; +import com.mojang.blaze3d.buffers.GpuBufferSlice; +import com.mojang.blaze3d.buffers.Std140Builder; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderPass; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.textures.AddressMode; +import com.mojang.blaze3d.textures.FilterMode; +import com.mojang.blaze3d.textures.GpuSampler; +import com.mojang.blaze3d.textures.GpuTextureView; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.MeshData; +import fi.dy.masa.litematica.render.schematic.BlockModelRendererSchematic; +import fi.dy.masa.litematica.render.schematic.IBlockOutputSchematic; +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import fi.dy.masa.malilib.render.GuiContext; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.render.TextureSetup; +import net.minecraft.client.renderer.block.FluidRenderer; +import net.minecraft.client.renderer.DynamicUniforms; +import net.minecraft.client.renderer.GlobalSettingsUniform; +import net.minecraft.client.renderer.Projection; +import net.minecraft.client.renderer.ProjectionMatrixBuffer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.chunk.ChunkSectionLayer; +import net.minecraft.client.renderer.chunk.ChunkSectionLayerGroup; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.state.gui.BlitRenderState; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix4f; +import org.joml.Matrix4fc; +import org.joml.Matrix4fStack; +import org.joml.Matrix3x2f; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.lwjgl.system.MemoryStack; + +/** 26.x 鐨勭鏈?framebuffer 鏂瑰潡棰勮鍚庣銆?*/ +public final class SchematicPreviewRenderer26 implements SchematicPreviewRenderBackend { + private final List meshes = new ArrayList<>(); + private long builtRevision = Long.MIN_VALUE; + private LitematicaSchematic schematic; + private SchematicPreviewWorld26 world; + private TextureTarget target; + private GpuSampler sampler; + private ProjectionMatrixBuffer projection; + private final Projection previewProjection = new Projection(); + private GpuBuffer globalUniform; + private long loggedRenderRevision = Long.MIN_VALUE; + private long loggedDrawRevision = Long.MIN_VALUE; + private long loggedFrustumRevision = Long.MIN_VALUE; + private final FluidRenderer fluidRenderer = new FluidRenderer(Minecraft.getInstance().getModelManager().getFluidStateModelSet()); + private final CameraRenderState cameraRenderState = new CameraRenderState(); + + @Override + public void setSchematic(Object schematic) { + this.schematic = schematic instanceof LitematicaSchematic loaded ? loaded : null; + } + + public boolean render(Object rawContext, SchematicPreviewLayout layout, SchematicPreviewTransform transform, + SchematicPreviewModel model, long revision) { + GuiContext context = (GuiContext) rawContext; + if (model == null || model.blocks().isEmpty() || layout.contentWidth() <= 0 || layout.contentHeight() <= 0) { + return false; + } + this.ensureTarget(layout.contentWidth(), layout.contentHeight()); + if (this.builtRevision != revision) { + try { + this.rebuild(model, revision); + } catch (Throwable throwable) { + ListMore.LOGGER.error("Schematic preview: mesh rebuild failed (revision={})", revision, throwable); + return false; + } + } + if (this.meshes.isEmpty()) { + return false; + } + + RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures( + this.target.getColorTexture(), new org.joml.Vector4f(0.105F, 0.125F, 0.157F, 1.0F), + this.target.getDepthTexture(), 0.0D); + try { + this.renderTarget(model, transform); + } catch (Throwable throwable) { + ListMore.LOGGER.error("Schematic preview: framebuffer render failed (revision={})", revision, throwable); + return false; + } + context.addSimpleElement(new BlitRenderState( + RenderPipelines.GUI_TEXTURED, + TextureSetup.singleTexture(this.target.getColorTextureView(), this.sampler), + new Matrix3x2f(context.pose()), + layout.contentX(), layout.contentY(), + layout.contentX() + layout.contentWidth(), layout.contentY() + layout.contentHeight(), + 0.0F, 1.0F, 1.0F, 0.0F, 0xFFFFFFFF, context.scissorStack.peek())); + return true; + } + + private void ensureTarget(int width, int height) { + int scaledWidth = Math.max(1, Math.round(width * Minecraft.getInstance().getWindow().getGuiScale())); + int scaledHeight = Math.max(1, Math.round(height * Minecraft.getInstance().getWindow().getGuiScale())); + if (this.target == null) { + this.target = new TextureTarget("ListMore schematic preview", scaledWidth, scaledHeight, true, GpuFormat.RGBA8_UNORM); + } else if (this.target.width != scaledWidth || this.target.height != scaledHeight) { + this.target.resize(scaledWidth, scaledHeight); + } + if (this.sampler == null) { + this.sampler = RenderSystem.getDevice().createSampler(AddressMode.CLAMP_TO_EDGE, AddressMode.CLAMP_TO_EDGE, + FilterMode.LINEAR, FilterMode.LINEAR, 1, OptionalDouble.empty()); + } + } + + private void rebuild(SchematicPreviewModel model, long revision) { + this.closeMeshes(); + this.builtRevision = revision; + this.loggedRenderRevision = Long.MIN_VALUE; + this.loggedDrawRevision = Long.MIN_VALUE; + this.loggedFrustumRevision = Long.MIN_VALUE; + if (this.schematic == null) { + return; + } + if (this.world == null) { + this.world = new SchematicPreviewWorld26(Minecraft.getInstance()); + } + this.world.setSchematic(this.schematic); + SchematicPreviewWorld26 view = this.world; + ModelManager modelManager = Minecraft.getInstance().getModelManager(); + Map chunks = new HashMap<>(); + int modelBlocks = 0; + int fluidBlocks = 0; + BlockModelRendererSchematic renderer = new BlockModelRendererSchematic(); + renderer.enableCache(); + try { + for (SchematicPreviewModel.Block block : model.blocks()) { + BlockState state = block.state(); + BlockPos position = new BlockPos(block.x(), block.y(), block.z()); + ChunkMeshBuilder chunk = chunks.computeIfAbsent(new ChunkPos(block.x() >> 4, block.z() >> 4), ignored -> new ChunkMeshBuilder()); + if (!state.getFluidState().isEmpty()) { + fluidBlocks++; + this.fluidRenderer.tesselate(view, position, chunk::builder, state, state.getFluidState()); + } + if (state.getRenderShape() != RenderShape.MODEL) { + continue; + } + modelBlocks++; + IBlockOutputSchematic output = (x, y, z, quad, instance) -> chunk.builder(quad.materialInfo().layer()) + .putBlockBakedQuad(x, y, z, quad, instance); + renderer.tessellateBlock(view, state, position, new Vec3(block.x() & 15, block.y(), block.z() & 15), + modelManager.getBlockStateModelSet().get(state), state.getSeed(position), output); + } + } finally { + renderer.disableCache(); + } + chunks.forEach((position, chunk) -> { + ChunkMesh built = chunk.build(position); + if (built != null) { + this.meshes.add(built); + } + }); + int layers = this.meshes.stream().mapToInt(mesh -> mesh.buffers().size()).sum(); + int indices = this.meshes.stream().flatMap(mesh -> mesh.buffers().values().stream()).mapToInt(SectionBuffers::indexCount).sum(); + ListMore.LOGGER.info("Schematic preview: mesh rebuild complete (revision={}, sourceBlocks={}, modelBlocks={}, fluidBlocks={}, chunks={}, layers={}, indices={})", + revision, model.blocks().size(), modelBlocks, fluidBlocks, this.meshes.size(), layers, indices); + } + + private void renderTarget(SchematicPreviewModel model, SchematicPreviewTransform transform) { + float distance = transform.distance(model.size().getX(), model.size().getY(), model.size().getZ()); + float yaw = transform.yaw() * Mth.DEG_TO_RAD; + float pitch = transform.pitch() * Mth.DEG_TO_RAD; + float horizontal = Mth.cos(pitch); + Vector3f camera = new Vector3f( + model.centerX() - Mth.sin(yaw) * horizontal * distance, + model.centerY() - Mth.sin(pitch) * distance, + model.centerZ() + Mth.cos(yaw) * horizontal * distance); + if (this.loggedRenderRevision != this.builtRevision) { + this.loggedRenderRevision = this.builtRevision; + ListMore.LOGGER.info("Schematic preview: rendering revision={} (framebuffer={}x{}, camera={}, yaw={}, pitch={}, distance={})", + this.builtRevision, this.target.width, this.target.height, camera, transform.yaw(), transform.pitch(), distance); + } + Matrix4fStack modelView = RenderSystem.getModelViewStack(); + modelView.pushMatrix(); + // Match SchematicPreview's orbit convention: UI yaw describes the viewing + // direction, while the terrain view matrix uses the inverse camera yaw. + Quaternionf rotation = new Quaternionf().rotationYXZ(-yaw, pitch, 0.0F).conjugate(); + modelView.set(new Matrix4f().rotation(rotation)); + this.previewProjection.setupPerspective(0.05F, 4096.0F, 70.0F, + this.target.width, this.target.height); + Matrix4f projectionMatrix = this.previewProjection.getMatrix(new Matrix4f()); + this.logFrustum(model, camera, modelView, projectionMatrix); + this.cameraRenderState.initialized = true; + this.cameraRenderState.orientation = rotation; + this.cameraRenderState.pos = new Vec3(camera.x, camera.y, camera.z); + this.cameraRenderState.blockPos = BlockPos.containing(this.cameraRenderState.pos); + this.cameraRenderState.xRot = transform.pitch(); + this.cameraRenderState.yRot = -transform.yaw(); + RenderSystem.backupProjectionMatrix(); + if (this.projection == null) { + this.projection = new ProjectionMatrixBuffer("ListMore schematic preview"); + } + RenderSystem.setProjectionMatrix(this.projection.getBuffer(projectionMatrix), ProjectionType.PERSPECTIVE); + GpuBuffer previousGlobalUniform = RenderSystem.getGlobalSettingsUniform(); + this.writeGlobalUniform(camera); + try { + Minecraft.getInstance().gameRenderer.lighting().setupFor(Lighting.Entry.LEVEL); + this.renderMeshes(camera); + } finally { + RenderSystem.setGlobalSettingsUniform(previousGlobalUniform); + RenderSystem.restoreProjectionMatrix(); + modelView.popMatrix(); + } + } + + private void writeGlobalUniform(Vector3f camera) { + if (this.globalUniform == null) { + this.globalUniform = RenderSystem.getDevice().createBuffer(() -> "ListMore preview global settings", + GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_COPY_DST, GlobalSettingsUniform.UBO_SIZE); + } + int floorX = Mth.floor(camera.x); + int floorY = Mth.floor(camera.y); + int floorZ = Mth.floor(camera.z); + try (MemoryStack stack = MemoryStack.stackPush()) { + RenderSystem.getDevice().createCommandEncoder().writeToBuffer(this.globalUniform.slice(), + Std140Builder.onStack(stack, GlobalSettingsUniform.UBO_SIZE) + .putIVec3(floorX, floorY, floorZ) + .putVec3(floorX - camera.x, floorY - camera.y, floorZ - camera.z) + .putVec2(Minecraft.getInstance().getWindow().getWidth(), Minecraft.getInstance().getWindow().getHeight()) + .putFloat(1.0F).putFloat(0.0F).putInt(0).putInt(0).get()); + } + RenderSystem.setGlobalSettingsUniform(this.globalUniform); + } + + /** Logs the terrain shader's world-to-clip transform once for each preview. */ + private void logFrustum(SchematicPreviewModel model, Vector3f camera, Matrix4fc modelView, + Matrix4fc projectionMatrix) { + if (this.loggedFrustumRevision == this.builtRevision) { + return; + } + this.loggedFrustumRevision = this.builtRevision; + Matrix4f transform = new Matrix4f(projectionMatrix).mul(modelView); + int visibleCorners = 0; + float minX = Float.POSITIVE_INFINITY; + float minY = Float.POSITIVE_INFINITY; + float maxX = Float.NEGATIVE_INFINITY; + float maxY = Float.NEGATIVE_INFINITY; + for (int x : new int[] { 0, model.size().getX() }) { + for (int y : new int[] { 0, model.size().getY() }) { + for (int z : new int[] { 0, model.size().getZ() }) { + org.joml.Vector4f clip = transform.transform(new org.joml.Vector4f( + x - camera.x, y - camera.y, z - camera.z, 1.0F)); + if (clip.w > 0.0F) { + float ndcX = clip.x / clip.w; + float ndcY = clip.y / clip.w; + minX = Math.min(minX, ndcX); + minY = Math.min(minY, ndcY); + maxX = Math.max(maxX, ndcX); + maxY = Math.max(maxY, ndcY); + if (Math.abs(ndcX) <= 1.0F && Math.abs(ndcY) <= 1.0F) { + visibleCorners++; + } + } + } + } + } + ListMore.LOGGER.info("Schematic preview: CPU frustum revision={} visibleCorners={}/8 ndcX=[{}, {}] ndcY=[{}, {}]", + this.builtRevision, visibleCorners, minX, maxX, minY, maxY); + } + + private void renderMeshes(Vector3f camera) { + var atlas = Minecraft.getInstance().getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(); + List orderedMeshes = new ArrayList<>(this.meshes); + orderedMeshes.sort(Comparator.comparingDouble(mesh -> -mesh.distanceTo(camera))); + DynamicUniforms.ChunkSectionInfo[] infos = new DynamicUniforms.ChunkSectionInfo[orderedMeshes.size()]; + for (int i = 0; i < orderedMeshes.size(); i++) { + ChunkMesh mesh = orderedMeshes.get(i); + infos[i] = new DynamicUniforms.ChunkSectionInfo(RenderSystem.getModelViewMatrixCopy(), mesh.chunk().getMinBlockX(), 0, + mesh.chunk().getMinBlockZ(), 1.0F, atlas.getWidth(0), atlas.getHeight(0)); + } + GpuBufferSlice[] sectionUniforms = RenderSystem.getDynamicUniforms().writeChunkSections(infos); + RenderSystem.AutoStorageIndexBuffer sequential = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS); + int maxSequentialIndices = 0; + for (ChunkMesh chunk : orderedMeshes) { + for (SectionBuffers mesh : chunk.buffers().values()) { + if (mesh.indexBuffer() == null) { + maxSequentialIndices = Math.max(maxSequentialIndices, mesh.indexCount()); + } + } + } + GpuBuffer sharedIndex = maxSequentialIndices == 0 ? null : sequential.getBuffer(maxSequentialIndices); + IndexType sharedIndexType = maxSequentialIndices == 0 ? null : sequential.type(); + this.renderLayerGroup(orderedMeshes, sectionUniforms, atlas, sharedIndex, sharedIndexType, + ChunkSectionLayerGroup.OPAQUE, false); + this.renderLayerGroup(orderedMeshes, sectionUniforms, atlas, sharedIndex, sharedIndexType, + ChunkSectionLayerGroup.TRANSLUCENT, true); + } + + private void renderLayerGroup(List orderedMeshes, GpuBufferSlice[] sectionUniforms, + GpuTextureView atlas, GpuBuffer sharedIndex, IndexType sharedIndexType, + ChunkSectionLayerGroup group, boolean reverse) { + int drawCount = 0; + try (RenderPass pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass( + () -> "ListMore schematic preview " + group.label(), this.target.getColorTextureView(), Optional.empty(), + this.target.getDepthTextureView(), OptionalDouble.empty())) { + RenderSystem.bindDefaultUniforms(pass); + pass.bindTexture("Sampler2", Minecraft.getInstance().gameRenderer.lightmap(), + RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR)); + for (ChunkSectionLayer layer : group.layers()) { + pass.setPipeline(layer.pipeline()); + pass.bindTexture("Sampler0", atlas, this.sampler); + List> draws = new ArrayList<>(); + for (int i = 0; i < orderedMeshes.size(); i++) { + SectionBuffers mesh = orderedMeshes.get(i).buffers().get(layer); + if (mesh == null || mesh.indexCount() <= 0 || mesh.vertexBuffer().isClosed()) continue; + int uniformIndex = i; + draws.add(new RenderPass.Draw<>(0, mesh.vertexBuffer(), mesh.indexBuffer(), + mesh.indexBuffer() == null ? null : mesh.indexType(), 0, mesh.indexCount(), 0, + (slices, uploader) -> uploader.upload("ChunkSection", slices[uniformIndex]))); + } + if (!draws.isEmpty()) { + if (reverse) draws = draws.reversed(); + drawCount += draws.size(); + if (this.loggedDrawRevision != this.builtRevision) { + int layerIndices = draws.stream().mapToInt(RenderPass.Draw::indexCount).sum(); + ListMore.LOGGER.info("Schematic preview: layer {} has {} draws and {} indices (group={})", + layer, draws.size(), layerIndices, group.label()); + } + pass.drawMultipleIndexed(draws, sharedIndex, sharedIndexType, List.of("ChunkSection"), sectionUniforms); + } + } + } + if (this.loggedDrawRevision != this.builtRevision) { + this.loggedDrawRevision = this.builtRevision; + ListMore.LOGGER.info("Schematic preview: submitted {} draws for {} (sections={}, sharedIndex={})", + drawCount, group.label(), sectionUniforms.length, sharedIndex != null); + } + } + + private void closeMeshes() { + this.meshes.forEach(ChunkMesh::close); + this.meshes.clear(); + } + + /** 姣忔蹇収鍙樺寲鏃跺缓绔嬩竴娆?GPU 缂撳啿锛屽抚闂寸洿鎺ュ鐢ㄣ€?*/ + private record SectionBuffers(GpuBuffer vertexBuffer, GpuBuffer indexBuffer, int indexCount, + IndexType indexType) implements AutoCloseable { + @Override + public void close() { + this.vertexBuffer.close(); + if (this.indexBuffer != null) { + this.indexBuffer.close(); + } + } + } + + private static final class ChunkMeshBuilder implements AutoCloseable { + private final Map allocators = new EnumMap<>(ChunkSectionLayer.class); + private final Map builders = new EnumMap<>(ChunkSectionLayer.class); + + private BufferBuilder builder(ChunkSectionLayer layer) { + return this.builders.computeIfAbsent(layer, ignored -> new BufferBuilder( + this.allocators.computeIfAbsent(layer, unused -> new ByteBufferBuilder(4096)), + layer.pipeline().getPrimitiveTopology(), layer.vertexFormat())); + } + + private ChunkMesh build(ChunkPos position) { + Map buffers = new EnumMap<>(ChunkSectionLayer.class); + List meshData = new ArrayList<>(); + this.builders.forEach((layer, builder) -> { + MeshData mesh = builder.build(); + if (mesh == null) { + return; + } + meshData.add(mesh); + GpuBuffer vertex = RenderSystem.getDevice().createBuffer(() -> "ListMore preview vertex buffer", + GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, mesh.vertexBuffer()); + GpuBuffer index = mesh.indexBuffer() == null ? null : RenderSystem.getDevice().createBuffer( + () -> "ListMore preview index buffer", GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, + mesh.indexBuffer()); + buffers.put(layer, new SectionBuffers(vertex, index, mesh.drawState().indexCount(), mesh.drawState().indexType())); + }); + if (buffers.isEmpty()) { + meshData.forEach(MeshData::close); + this.close(); + return null; + } + return new ChunkMesh(position, buffers, meshData, new ArrayList<>(this.allocators.values())); + } + + @Override + public void close() { + this.allocators.values().forEach(ByteBufferBuilder::close); + this.allocators.clear(); + } + } + + private record ChunkMesh(ChunkPos chunk, Map buffers, + List meshData, List allocators) implements AutoCloseable { + private double distanceTo(Vector3f camera) { + float x = this.chunk.getMinBlockX() + 8.0F; + float z = this.chunk.getMinBlockZ() + 8.0F; + return Mth.square(x - camera.x) + Mth.square(z - camera.z); + } + + @Override + public void close() { + this.allocators.forEach(ByteBufferBuilder::close); + this.meshData.forEach(MeshData::close); + this.buffers.values().forEach(SectionBuffers::close); + } + } + + @Override + public void close() { + this.closeMeshes(); + this.builtRevision = Long.MIN_VALUE; + this.schematic = null; + this.world = null; + if (this.target != null) { this.target.destroyBuffers(); this.target = null; } + if (this.sampler != null) { this.sampler.close(); this.sampler = null; } + if (this.projection != null) { this.projection.close(); this.projection = null; } + if (this.globalUniform != null) { this.globalUniform.close(); this.globalUniform = null; } + } +} diff --git a/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewWorld26.java b/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewWorld26.java new file mode 100644 index 0000000..06988fd --- /dev/null +++ b/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewWorld26.java @@ -0,0 +1,556 @@ +package com.listmore.schematic; + +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; +import com.listmore.ListMore; +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import fi.dy.masa.litematica.schematic.container.LitematicaBlockStateContainer; +import net.minecraft.client.ClientClockManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.SectionPos; +import net.minecraft.core.Vec3i; +import net.minecraft.core.particles.ExplosionParticleInfo; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.ProblemReporter; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.*; +import net.minecraft.world.attribute.EnvironmentAttributeSystem; +import net.minecraft.world.clock.ClockManager; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.boss.enderdragon.EnderDragonPart; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.item.alchemy.PotionBrewing; +import net.minecraft.world.item.crafting.RecipeAccess; +import net.minecraft.world.level.*; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.FuelValues; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.lighting.LayerLightEventListener; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.saveddata.maps.MapId; +import net.minecraft.world.level.saveddata.maps.MapItemSavedData; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.level.storage.TagValueInput; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.ticks.BlackholeTickAccess; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.TickContainerAccess; +import org.jetbrains.annotations.Nullable; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +final class SchematicPreviewWorld26 extends Level implements LightChunkGetter, BlockAndTintGetter { + + private final LevelLightEngine lightingProvider = new FakeLightingProvider(this); + private final FakeChunkManager fakeChunkManager = new FakeChunkManager(); + private final WorldBorder worldBorder = new WorldBorder(); + private final PalettedContainerFactory palettesFactory; + private final Map areaPoses = new HashMap<>(); + private final Biome biome; + private LitematicaSchematic schematic; + private Vec3i areasOrigin = new Vec3i(0, 0, 0); + private Vec3i size = new Vec3i(0, 0, 0); + private BlockState[] blocksData; + private Map> blockEntities; + + SchematicPreviewWorld26(Minecraft mc) { + super( + new ClientLevel.ClientLevelData(Difficulty.PEACEFUL, false, true), + Level.OVERWORLD, + mc.level.registryAccess(), + mc.level.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(BuiltinDimensionTypes.OVERWORLD), + true, + false, + 0L, + 0 + ); + palettesFactory = mc.level.palettedContainerFactory(); + biome = registryAccess().lookupOrThrow(Registries.BIOME).getValue(Biomes.PLAINS); + } + + public void setSchematic(LitematicaSchematic schematic) { + this.schematic = schematic; + areaPoses.clear(); + schematic.getAreas().forEach((region, box) -> areaPoses.put(region, BlockPos.min(box.getPos1(), box.getPos2()))); + BlockPos.MutableBlockPos areasOrigin = new BlockPos.MutableBlockPos(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + BlockPos.MutableBlockPos areasEnd = new BlockPos.MutableBlockPos(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + areaPoses.forEach((region, areaPos) -> { + areasOrigin.set( + Math.min(areasOrigin.getX(), areaPos.getX()), + Math.min(areasOrigin.getY(), areaPos.getY()), + Math.min(areasOrigin.getZ(), areaPos.getZ()) + ); + Vec3i areaSize = schematic.getSubRegionContainer(region).getSize(); + areasEnd.set( + Math.max(areasEnd.getX(), areaPos.getX() + areaSize.getX()), + Math.max(areasEnd.getY(), areaPos.getY() + areaSize.getY()), + Math.max(areasEnd.getZ(), areaPos.getZ() + areaSize.getZ()) + ); + }); + this.areasOrigin = areasOrigin; + size = areasEnd.subtract(areasOrigin); + ListMore.LOGGER.info("Schematic preview: virtual world initialized (regions={}, origin={}, size={}x{}x{})", + areaPoses.size(), this.areasOrigin, size.getX(), size.getY(), size.getZ()); + blocksData = new BlockState[size.getX() * size.getY() * size.getZ()]; + ImmutableMap.Builder> blockEntitiesBuilder = ImmutableMap.builder(); + areaPoses.forEach((region, areaPos) -> { + BlockPos shift = areaPos.subtract(areasOrigin); + schematic.getBlockEntityMapForRegion(region).forEach((relPos, nbtCompound) -> { + BlockPos pos = relPos.offset(shift); + blockEntitiesBuilder.put(pos, Suppliers.memoize(() -> { + BlockEntity blockEntity = silentCreateTileFromNbt(pos, getBlockState(pos), nbtCompound, registryAccess()); + if (blockEntity != null) { + blockEntity.setLevel(this); + } + return blockEntity; + })); + }); + }); + blockEntities = blockEntitiesBuilder.buildKeepingLast(); + } + + @Nullable + private static BlockEntity silentCreateTileFromNbt(BlockPos pos, BlockState state, CompoundTag nbt, HolderLookup.Provider registries) { + String string = nbt.getStringOr("id", ""); + Identifier identifier = Identifier.tryParse(string); + if (identifier == null) { + return null; + } else { + return BuiltInRegistries.BLOCK_ENTITY_TYPE.getOptional(identifier).map((type) -> { + try { + return type.create(pos, state); + } catch (Exception e) { + return null; + } + }).map((blockEntity) -> { + try (ProblemReporter.ScopedCollector logging = new ProblemReporter.ScopedCollector(blockEntity.problemPath(), LoggerFactory.getLogger("ListMore schematic preview"))) { + blockEntity.loadWithComponents(TagValueInput.create(logging, registries, nbt)); + return blockEntity; + } catch (Exception e) { + return null; + } + }).orElse(null); + } + } + + public Vec3i getSize() { + return size; + } + + @Override + public LevelLightEngine getLightEngine() { + return lightingProvider; + } + + @Override + public CardinalLighting cardinalLighting() { + return CardinalLighting.DEFAULT; + } + + @Override + public int getBlockTint(BlockPos pos, ColorResolver colorResolver) { + return colorResolver.getColor(biome, pos.getX(), pos.getZ()); + } + + @Override + public Holder getUncachedNoiseBiome(int biomeX, int biomeY, int biomeZ) { + return this.registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS); + } + + @Override + public int getSeaLevel() { + return 0; + } + + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { + Supplier supplier = blockEntities.get(pos); + return supplier != null ? supplier.get() : null; + } + + @Override + public void setRespawnData(LevelData.RespawnData spawnPoint) { + + } + + @Override + public LevelData.RespawnData getRespawnData() { + return null; + } + + @Nullable + @Override + public Entity getEntity(int id) { + return null; + } + + @Override + public Collection dragonParts() { + return List.of(); + } + + @Override + public TickRateManager tickRateManager() { + return null; + } + + @Nullable + @Override + public MapItemSavedData getMapData(MapId id) { + return null; + } + + @Override + public void destroyBlockProgress(int entityId, BlockPos pos, int progress) { + + } + + @Override + public Scoreboard getScoreboard() { + return null; + } + + @Override + public RecipeAccess recipeAccess() { + return null; + } + + @Override + protected LevelEntityGetter getEntities() { + return null; + } + + @Override + public ClockManager clockManager() { + return new ClientClockManager(); + } + + @Override + public EnvironmentAttributeSystem environmentAttributes() { + return EnvironmentAttributeSystem.builder().addDefaultLayers(this).build(); + } + + @Override + public PotionBrewing potionBrewing() { + return null; + } + + @Override + public FuelValues fuelValues() { + return null; + } + + public Map> getBlockEntities() { + return blockEntities; + } + + @Override + public BlockState getBlockState(BlockPos pos) { + if (blocksData == null || isOutOfSize(pos.getX(), pos.getY(), pos.getZ(), size)) { + return Blocks.AIR.defaultBlockState(); + } + int index = getBlocksDataIndex(pos); + BlockState state = blocksData[index]; + if (state == null) { + for (Map.Entry areaEntry : areaPoses.entrySet()) { + LitematicaBlockStateContainer container = schematic.getSubRegionContainer(areaEntry.getKey()); + int x = pos.getX() - areaEntry.getValue().getX() + areasOrigin.getX(); + int y = pos.getY() - areaEntry.getValue().getY() + areasOrigin.getY(); + int z = pos.getZ() - areaEntry.getValue().getZ() + areasOrigin.getZ(); + if (!isOutOfSize(x, y, z, container.getSize())) { + return blocksData[index] = container.get(x, y, z); + } + } + } + return state != null ? state : Blocks.AIR.defaultBlockState(); + } + + private static boolean isOutOfSize(int x, int y, int z, Vec3i size) { + return x < 0 || x >= size.getX() || y < 0 || y >= size.getY() || z < 0 || z >= size.getZ(); + } + + private int getBlocksDataIndex(BlockPos pos) { + return pos.getY() * size.getX() * size.getZ() + pos.getZ() * size.getX() + pos.getX(); + } + + @Override + public FluidState getFluidState(BlockPos pos) { + return getBlockState(pos).getFluidState(); + } + + @Override + public void playSeededSound(@Nullable Entity source, double x, double y, double z, Holder sound, SoundSource category, float volume, float pitch, long seed) { + + } + + @Override + public void playSeededSound(@Nullable Entity source, Entity entity, Holder sound, SoundSource category, float volume, float pitch, long seed) { + + } + + @Override + public void explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, ExplosionInteraction explosionSourceType, ParticleOptions smallParticle, ParticleOptions largeParticle, WeightedList blockParticles, Holder soundEvent) { + + } + + @Override + public String gatherChunkSourceStats() { + return getClass().getSimpleName(); + } + + @Override + public int getHeight() { + return 384; + } + + @Override + public int getMinY() { + return 0; + } + + @Override + public FeatureFlagSet enabledFeatures() { + return FeatureFlags.DEFAULT_FLAGS; + } + + @Override + public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) {} + + @Override + public @org.jspecify.annotations.Nullable LightChunk getChunkForLighting(int i, int j) { + return null; + } + + @Override + public BlockGetter getLevel() { + return this; + } + + @Override + public LevelTickAccess getBlockTicks() { + return BlackholeTickAccess.emptyLevelList(); + } + + @Override + public LevelTickAccess getFluidTicks() { + return BlackholeTickAccess.emptyLevelList(); + } + + @Override + public ChunkSource getChunkSource() { + return fakeChunkManager; + } + + @Override + public void levelEvent(@Nullable Entity source, int eventId, BlockPos pos, int data) { + + } + + @Override + public void gameEvent(Holder event, Vec3 emitterPos, GameEvent.Context emitter) {} + + @Override + public List players() { + return List.of(); + } + + @Override + public WorldBorder getWorldBorder() { + return worldBorder; + } + + private static class FakeLightingProvider extends LevelLightEngine { + + private final LayerLightEventListener FULL_BRIGHT_VIEW = new LayerLightEventListener() { + @Nullable + @Override + public DataLayer getDataLayerData(SectionPos pos) { + return null; + } + @Override + public int getLightValue(BlockPos pos) { + return 15; + } + @Override + public void checkBlock(BlockPos pos) {} + @Override + public boolean hasLightWork() { + return false; + } + @Override + public int runLightUpdates() { + return 15; + } + @Override + public void updateSectionStatus(SectionPos pos, boolean notReady) {} + @Override + public void setLightEnabled(ChunkPos pos, boolean retainData) {} + @Override + public void propagateLightSources(ChunkPos chunkPos) {} + }; + + public FakeLightingProvider(LightChunkGetter chunkProvider) { + super(chunkProvider, false, false); + } + + @Override + public LayerLightEventListener getLayerListener(LightLayer lightType) { + return FULL_BRIGHT_VIEW; + } + + } + + private class FakeChunkManager extends ChunkSource { + + private final Map fakeChunks = new HashMap<>(); + + @Nullable + @Override + public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) { + ChunkPos chunkPos = new ChunkPos(x, z); + return fakeChunks.computeIfAbsent(chunkPos, FakeChunk::new); + } + + @Override + public void tick(BooleanSupplier shouldKeepTicking, boolean tickChunks) { + + } + + @Override + public String gatherStats() { + return "FakeChunkManager"; + } + + @Override + public int getLoadedChunksCount() { + return 0; + } + + @Override + public LevelLightEngine getLightEngine() { + return lightingProvider; + } + + @Override + public BlockGetter getLevel() { + return SchematicPreviewWorld26.this; + } + + } + + private class FakeChunk extends ChunkAccess { + + private final BlockPos chunkOrigin; + + public FakeChunk(ChunkPos pos) { + super(pos, new UpgradeData(new CompoundTag(), SchematicPreviewWorld26.this), SchematicPreviewWorld26.this, palettesFactory, 0, null, null); + chunkOrigin = pos.getWorldPosition(); + } + + @Nullable + @Override + public BlockState setBlockState(BlockPos pos, BlockState state, int flags) { + return null; + } + + @Override + public void setBlockEntity(BlockEntity blockEntity) { + + } + + @Override + public void addEntity(Entity entity) { + + } + + @Override + public ChunkStatus getPersistedStatus() { + return ChunkStatus.FULL; + } + + @Override + public void removeBlockEntity(BlockPos pos) { + + } + + @Nullable + @Override + public CompoundTag getBlockEntityNbtForSaving(BlockPos pos, HolderLookup.Provider registries) { + BlockEntity blockEntity = getBlockEntity(pos); + if (blockEntity != null) { + CompoundTag nbtCompound = blockEntity.saveWithFullMetadata(registries); + nbtCompound.putBoolean("keepPacked", false); + return nbtCompound; + } + return null; + } + + @Override + public TickContainerAccess getBlockTicks() { + return BlackholeTickAccess.emptyContainer(); + } + + @Override + public TickContainerAccess getFluidTicks() { + return BlackholeTickAccess.emptyContainer(); + } + + @Override + public PackedTicks getTicksForSerialization(long time) { + return new PackedTicks(List.of(), List.of()); + } + + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { + return SchematicPreviewWorld26.this.getBlockEntity(chunkOrigin.offset(pos)); + } + + @Override + public BlockState getBlockState(BlockPos pos) { + return SchematicPreviewWorld26.this.getBlockState(chunkOrigin.offset(pos)); + } + + @Override + public FluidState getFluidState(BlockPos pos) { + return SchematicPreviewWorld26.this.getFluidState(chunkOrigin.offset(pos)); + } + + } + +} + From e1fc938559a9085cc6175bb3784d6681bd2916b0 Mon Sep 17 00:00:00 2001 From: Fenad2 Date: Fri, 7 Aug 2026 15:30:07 +0800 Subject: [PATCH 2/7] feat:Added schematic preview feature --- .../listmore/config/ListMoreConfigGui.java | 21 +- .../com/listmore/config/ListMoreConfigs.java | 19 + .../schematic/ListMoreSchematicBrowser.java | 62 +- .../schematic/SchematicPreviewDirection.java | 11 +- .../schematic/SchematicPreviewLayout.java | 14 +- .../schematic/SchematicPreviewLoader.java | 16 +- .../schematic/SchematicPreviewModel.java | 28 +- .../schematic/SchematicPreviewOverlay.java | 6 +- .../SchematicPreviewRenderBackend.java | 10 +- .../SchematicPreviewRenderManager.java | 74 ++ .../schematic/SchematicPreviewRenderer.java | 648 ++++++++++++++++-- .../schematic/SchematicPreviewSession.java | 32 +- .../schematic/SchematicPreviewTransform.java | 20 +- .../schematic/SchematicPreviewWorld.java | 90 +++ .../resources/assets/listmore/lang/en_us.json | 1 + .../resources/assets/listmore/lang/zh_cn.json | 1 + .../schematic/SchematicPreviewRenderer26.java | 446 ------------ .../schematic/SchematicPreviewWorld26.java | 556 --------------- 18 files changed, 907 insertions(+), 1148 deletions(-) rename {versions/26.2-fabric/src => src}/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java (76%) create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewRenderManager.java create mode 100644 src/main/java/com/listmore/schematic/SchematicPreviewWorld.java delete mode 100644 versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewRenderer26.java delete mode 100644 versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewWorld26.java diff --git a/src/main/java/com/listmore/config/ListMoreConfigGui.java b/src/main/java/com/listmore/config/ListMoreConfigGui.java index 9029cfe..e2af650 100644 --- a/src/main/java/com/listmore/config/ListMoreConfigGui.java +++ b/src/main/java/com/listmore/config/ListMoreConfigGui.java @@ -9,6 +9,7 @@ import fi.dy.masa.malilib.gui.button.ButtonGeneric; import fi.dy.masa.malilib.gui.button.IButtonActionListener; import fi.dy.masa.malilib.util.StringUtils; +import net.fabricmc.loader.api.FabricLoader; public class ListMoreConfigGui extends GuiConfigsBase { private static ConfigTab tab = ConfigTab.GENERIC; @@ -24,6 +25,9 @@ public void initGui() { int x = 10; for (ConfigTab currentTab : ConfigTab.values()) { + if (!currentTab.isAvailable()) { + continue; + } x += this.createButton(x, 26, -1, currentTab) + 2; } } @@ -56,7 +60,15 @@ protected boolean useKeybindSearch() { @Override public List getConfigs() { - return ConfigOptionWrapper.createFor(ListMoreConfigs.Generic.OPTIONS); + return ConfigOptionWrapper.createFor(switch (tab) { + case ALL -> FabricLoader.getInstance().isModLoaded("litematica") + ? ListMoreConfigs.Generic.OPTIONS + : ListMoreConfigs.Generic.GENERIC_OPTIONS; + case GENERIC -> ListMoreConfigs.Generic.GENERIC_OPTIONS; + case LITEMATICA -> FabricLoader.getInstance().isModLoaded("litematica") + ? ListMoreConfigs.Generic.LITEMATICA_OPTIONS + : List.of(); + }); } private record ButtonListener(ConfigTab tab, ListMoreConfigGui parent) implements IButtonActionListener { @@ -73,7 +85,8 @@ public void actionPerformedWithButton(ButtonBase button, int mouseButton) { private enum ConfigTab { ALL("malilib.gui.title.all"), - GENERIC("listmore.gui.tab.generic"); + GENERIC("listmore.gui.tab.generic"), + LITEMATICA("listmore.gui.tab.litematica"); private final String translationKey; @@ -84,5 +97,9 @@ private enum ConfigTab { public String getDisplayName() { return StringUtils.translate(this.translationKey); } + + public boolean isAvailable() { + return this != LITEMATICA || FabricLoader.getInstance().isModLoaded("litematica"); + } } } diff --git a/src/main/java/com/listmore/config/ListMoreConfigs.java b/src/main/java/com/listmore/config/ListMoreConfigs.java index f34920d..c575a55 100644 --- a/src/main/java/com/listmore/config/ListMoreConfigs.java +++ b/src/main/java/com/listmore/config/ListMoreConfigs.java @@ -51,6 +51,25 @@ public static class Generic { public static final ConfigBoolean PROJECTILE_LANDING_PREDICTION = createProjectileLandingPrediction(); public static final ConfigBoolean SCHEMATIC_DYNAMIC_PREVIEW = createSchematicDynamicPreview(); + public static final ImmutableList GENERIC_OPTIONS = ImmutableList.of( + COPY_TARGET_ID, + ENTITY_HIGHLIGHT_OUTLINE, + ENTITY_HIGHLIGHT_OUTLINE_LIST, + ENTITY_HIGHLIGHT_OUTLINE_COLOR, + ENTITY_RENDERING_BLACKLIST, + ENTITY_RENDERING_BLACKLIST_LIST, + ENTITY_RENDERING_BLACKLIST_RANGE, + INVALID_FURNACE_INPUT_HIGHLIGHTER, + INVALID_FURNACE_INPUT_HIGHLIGHTER_RANGE, + TNT_EXPLOSION_PREVIEW, + TNT_EXPLOSION_PREVIEW_MODE, + PROJECTILE_LANDING_PREDICTION + ); + + public static final ImmutableList LITEMATICA_OPTIONS = ImmutableList.of( + SCHEMATIC_DYNAMIC_PREVIEW + ); + public static final ImmutableList OPTIONS = ImmutableList.of( COPY_TARGET_ID, ENTITY_HIGHLIGHT_OUTLINE, diff --git a/versions/26.2-fabric/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java b/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java similarity index 76% rename from versions/26.2-fabric/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java rename to src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java index 0931959..adab9f2 100644 --- a/versions/26.2-fabric/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java +++ b/src/main/java/com/listmore/schematic/ListMoreSchematicBrowser.java @@ -3,7 +3,6 @@ import java.util.Date; import org.jetbrains.annotations.Nullable; -import fi.dy.masa.litematica.Litematica; import fi.dy.masa.litematica.gui.GuiSchematicBrowserBase; import fi.dy.masa.litematica.gui.widgets.WidgetSchematicBrowser; import fi.dy.masa.litematica.schematic.LitematicaSchematic; @@ -11,15 +10,20 @@ import fi.dy.masa.litematica.schematic.SchematicSchema; import fi.dy.masa.malilib.gui.interfaces.ISelectionListener; import fi.dy.masa.malilib.gui.widgets.WidgetFileBrowserBase.DirectoryEntry; -import fi.dy.masa.malilib.render.GuiContext; +//#if MC >= 1.21.11 +//$$ import fi.dy.masa.malilib.render.GuiContext; +//#else +import net.minecraft.client.gui.GuiGraphics; +//#endif import fi.dy.masa.malilib.render.RenderUtils; import fi.dy.masa.malilib.util.StringUtils; import fi.dy.masa.malilib.util.data.Schema; -import net.minecraft.client.input.MouseButtonEvent; +//#if MC >= 1.21.10 +//$$ import net.minecraft.client.input.MouseButtonEvent; +//#endif import net.minecraft.core.Vec3i; import org.apache.commons.lang3.tuple.Pair; -/** 26.2 implementation of the Litematica browser preview extension. */ public final class ListMoreSchematicBrowser extends WidgetSchematicBrowser { private final SchematicPreviewSession previewSession = new SchematicPreviewSession(); @Nullable private SchematicPreviewLayout previewLayout; @@ -32,14 +36,26 @@ public ListMoreSchematicBrowser(int x, int y, int width, int height, GuiSchemati } @Override - protected void drawAdditionalContents(GuiContext context, int mouseX, int mouseY) { + protected void drawAdditionalContents( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + int mouseX, int mouseY) { this.lastMouseX = mouseX; this.lastMouseY = mouseY; super.drawAdditionalContents(context, mouseX, mouseY); } @Override - protected void drawSelectedSchematicInfo(GuiContext context, @Nullable DirectoryEntry entry) { + protected void drawSelectedSchematicInfo( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + @Nullable DirectoryEntry entry) { if (!isLitematic(entry)) { this.previewLayout = null; super.drawSelectedSchematicInfo(context, entry); @@ -123,25 +139,47 @@ protected void drawSelectedSchematicInfo(GuiContext context, @Nullable Directory } @Override - public boolean onMouseClicked(MouseButtonEvent click, boolean doubleClick) { - if (click.input() == 0 && this.previewLayout != null) { - if (this.previewLayout.zoomOutBounds().contains(click.x(), click.y())) { + //#if MC >= 1.21.10 + //$$ public boolean onMouseClicked(MouseButtonEvent click, boolean doubleClick) { + //$$ if (click.input() == 0 && this.previewLayout != null) { + //$$ if (this.previewLayout.zoomOutBounds().contains(click.x(), click.y())) { + //$$ this.previewSession.transform().zoomOut(); + //$$ return true; + //$$ } + //$$ if (this.previewLayout.zoomInBounds().contains(click.x(), click.y())) { + //$$ this.previewSession.transform().zoomIn(); + //$$ return true; + //$$ } + //$$ for (SchematicPreviewDirection direction : SchematicPreviewDirection.values()) { + //$$ if (this.previewLayout.buttonBounds(direction).contains(click.x(), click.y())) { + //$$ this.previewSession.transform().applyDirection(direction); + //$$ return true; + //$$ } + //$$ } + //$$ } + //$$ return super.onMouseClicked(click, doubleClick); + //$$ } + //#else + public boolean onMouseClicked(int mouseX, int mouseY, int button) { + if (button == 0 && this.previewLayout != null) { + if (this.previewLayout.zoomOutBounds().contains(mouseX, mouseY)) { this.previewSession.transform().zoomOut(); return true; } - if (this.previewLayout.zoomInBounds().contains(click.x(), click.y())) { + if (this.previewLayout.zoomInBounds().contains(mouseX, mouseY)) { this.previewSession.transform().zoomIn(); return true; } for (SchematicPreviewDirection direction : SchematicPreviewDirection.values()) { - if (this.previewLayout.buttonBounds(direction).contains(click.x(), click.y())) { + if (this.previewLayout.buttonBounds(direction).contains(mouseX, mouseY)) { this.previewSession.transform().applyDirection(direction); return true; } } } - return super.onMouseClicked(click, doubleClick); + return super.onMouseClicked(mouseX, mouseY, button); } + //#endif @Override public void onClose() { diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewDirection.java b/src/main/java/com/listmore/schematic/SchematicPreviewDirection.java index 6f91a06..227ebb7 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewDirection.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewDirection.java @@ -1,12 +1,11 @@ package com.listmore.schematic; -/** 预览区域中的八个固定观察方向。 */ public enum SchematicPreviewDirection { - NORTH("↑"), - WEST("←"), - CENTER("↻"), - EAST("→"), - SOUTH("↓"); + UP("↑"), + LEFT("←"), + RESET("↻"), + RIGHT("→"), + DOWN("↓"); private final String label; diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewLayout.java b/src/main/java/com/listmore/schematic/SchematicPreviewLayout.java index f4d6fb2..bff7c09 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewLayout.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewLayout.java @@ -1,6 +1,6 @@ package com.listmore.schematic; -/** 原理图浏览器右侧预览区域及方向按钮的纯布局计算。 */ +// 原理图浏览器右侧预览区域及方向按钮的布局计算 public final class SchematicPreviewLayout { private static final int BUTTON_SIZE = 16; private static final int BUTTON_GAP = 2; @@ -23,7 +23,7 @@ public SchematicPreviewLayout(int x, int y, int width, int height) { public int width() { return this.width; } public int height() { return this.height; } - /** 模型实际可绘制的区域,底部方向按钮不覆盖这个区域。 */ + // 实际可绘制的区域 public int contentX() { return this.x + 1; } public int contentY() { return this.y + 1; } public int contentWidth() { return Math.max(0, this.width - 2); } @@ -38,15 +38,15 @@ public ButtonBounds buttonBounds(SchematicPreviewDirection direction) { int centerX = this.x + this.width / 2; int centerY = this.y + this.height / 2; return switch (direction) { - case NORTH -> new ButtonBounds(centerX - BUTTON_SIZE / 2, this.y + EDGE_OFFSET, + case UP -> new ButtonBounds(centerX - BUTTON_SIZE / 2, this.y + EDGE_OFFSET, BUTTON_SIZE, BUTTON_SIZE); - case WEST -> new ButtonBounds(this.x + EDGE_OFFSET, centerY - BUTTON_SIZE / 2, + case LEFT -> new ButtonBounds(this.x + EDGE_OFFSET, centerY - BUTTON_SIZE / 2, BUTTON_SIZE, BUTTON_SIZE); - case CENTER -> new ButtonBounds(this.x + this.width - BUTTON_SIZE - EDGE_OFFSET, + case RESET -> new ButtonBounds(this.x + this.width - BUTTON_SIZE - EDGE_OFFSET, this.y + this.height - BUTTON_SIZE - EDGE_OFFSET, BUTTON_SIZE, BUTTON_SIZE); - case EAST -> new ButtonBounds(this.x + this.width - BUTTON_SIZE - EDGE_OFFSET, + case RIGHT -> new ButtonBounds(this.x + this.width - BUTTON_SIZE - EDGE_OFFSET, centerY - BUTTON_SIZE / 2, BUTTON_SIZE, BUTTON_SIZE); - case SOUTH -> new ButtonBounds(centerX - BUTTON_SIZE / 2, + case DOWN -> new ButtonBounds(centerX - BUTTON_SIZE / 2, this.y + this.height - BUTTON_SIZE - EDGE_OFFSET, BUTTON_SIZE, BUTTON_SIZE); }; } diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewLoader.java b/src/main/java/com/listmore/schematic/SchematicPreviewLoader.java index 8501e30..cda5ae7 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewLoader.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewLoader.java @@ -5,10 +5,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import com.listmore.ListMore; import fi.dy.masa.litematica.schematic.LitematicaSchematic; -/** 负责在后台读取 litematic 文件,不持有也不修改 Litematica 的全局投影状态。 */ +// 后台读取 litematic 文件 public final class SchematicPreviewLoader { private static final ConcurrentMap> CACHE = new ConcurrentHashMap<>(); @@ -27,16 +26,7 @@ public static CompletableFuture load(Path file) { } private static CompletableFuture startLoad(Path file) { - return CompletableFuture.supplyAsync(() -> { - ListMore.LOGGER.info("Schematic preview: loading {}", file); - try { - LitematicaSchematic schematic = LitematicaSchematic.createFromFile(file.getParent(), file.getFileName().toString()); - ListMore.LOGGER.info("Schematic preview: loaded {} (regions={})", file, schematic != null ? schematic.getAreas().size() : 0); - return schematic; - } catch (Throwable throwable) { - ListMore.LOGGER.error("Schematic preview: failed to load {}", file, throwable); - throw throwable; - } - }); + return CompletableFuture.supplyAsync(() -> + LitematicaSchematic.createFromFile(file.getParent(), file.getFileName().toString())); } } diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewModel.java b/src/main/java/com/listmore/schematic/SchematicPreviewModel.java index b786cab..729c8ed 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewModel.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewModel.java @@ -13,10 +13,6 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; -/** - * 原理图预览使用的只读方块快照。 - * 方块坐标相对于整个原理图的最小角,center 始终是完整包围盒的中心。 - */ public final class SchematicPreviewModel { private final Vec3i size; private final float centerX; @@ -47,11 +43,7 @@ public Object blockEntityDataAt(int x, int y, int z) { return this.blockEntitiesByPosition.get(packPosition(x, y, z)); } - /** - * 查询预览坐标中的方块状态。 - * 坐标以原理图整体包围盒的最小角为原点,范围外和空气位置均返回空气, - * 因此渲染后端不需要读取客户端当前世界。 - */ + // 查询预览坐标中的方块状态 public BlockState blockStateAt(int x, int y, int z) { if (x < 0 || y < 0 || z < 0 || x >= this.size.getX() || y >= this.size.getY() || z >= this.size.getZ()) { return Blocks.AIR.defaultBlockState(); @@ -59,13 +51,15 @@ public BlockState blockStateAt(int x, int y, int z) { return this.statesByPosition.getOrDefault(packPosition(x, y, z), Blocks.AIR.defaultBlockState()); } - /** 从 Litematica 已解析的数据中提取非空气方块,不创建或修改投影世界。 */ + // 从 Litematica 已解析的数据中提取非空气方块 + // 流程:遍历所有区域 -> 计算包围盒 -> 遍历容器提取非空气方块并转为相对坐标 -> 收集方块实体数据 public static SchematicPreviewModel from(LitematicaSchematic schematic) { Map areas = schematic.getAreas(); if (areas.isEmpty()) { return empty(); } + // 遍历所有区域计算包围盒 int minX = Integer.MAX_VALUE; int minY = Integer.MAX_VALUE; int minZ = Integer.MAX_VALUE; @@ -80,10 +74,12 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) { if (container == null || first == null || second == null) { continue; } + // 区域原点取两角点中较小者 int originX = Math.min(first.getX(), second.getX()); int originY = Math.min(first.getY(), second.getY()); int originZ = Math.min(first.getZ(), second.getZ()); Vec3i regionSize = container.getSize(); + // 更新全局包围盒:原点取最小,远端取 origin+size 的最大值 minX = Math.min(minX, originX); minY = Math.min(minY, originY); minZ = Math.min(minZ, originZ); @@ -95,6 +91,7 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) { return empty(); } + // 从每个区域中提取非空气方块,转换为相对坐标 List blocks = new ArrayList<>(); Map statesByPosition = new HashMap<>(); Map blockEntitiesByPosition = new HashMap<>(); @@ -109,15 +106,18 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) { int originX = Math.min(first.getX(), second.getX()); int originY = Math.min(first.getY(), second.getY()); int originZ = Math.min(first.getZ(), second.getZ()); + // 遍历容器内所有方块,只保留非空气方块 for (int y = 0; y < regionSize.getY(); y++) { for (int z = 0; z < regionSize.getZ(); z++) { for (int x = 0; x < regionSize.getX(); x++) { BlockState state = container.get(x, y, z); if (!state.isAir()) { + // 绝对坐标 -> 相对坐标:减去全局包围盒原点 int relativeX = x + originX - minX; int relativeY = y + originY - minY; int relativeZ = z + originZ - minZ; long position = packPosition(relativeX, relativeY, relativeZ); + // putIfAbsent 防止重叠区域重复添加同一位置的方块 if (statesByPosition.putIfAbsent(position, state) == null) { blocks.add(new Block(relativeX, relativeY, relativeZ, state)); } @@ -125,6 +125,7 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) { } } } + // 收集方块实体数据(如箱子、告示牌等) Map blockEntities = schematic.getBlockEntityMapForRegion(entry.getKey()); if (blockEntities != null) { for (Map.Entry blockEntity : blockEntities.entrySet()) { @@ -140,6 +141,7 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) { } } + // 包围盒尺寸 = 远端 - 原点,中心 = 尺寸 * 0.5(几何中心) Vec3i size = new Vec3i(maxXExclusive - minX, maxYExclusive - minY, maxZExclusive - minZ); return new SchematicPreviewModel(size, size.getX() * 0.5F, size.getY() * 0.5F, size.getZ() * 0.5F, blocks, statesByPosition, blockEntitiesByPosition); @@ -149,14 +151,12 @@ private static SchematicPreviewModel empty() { return new SchematicPreviewModel(BlockPos.ZERO, 0.0F, 0.0F, 0.0F, List.of(), Map.of(), Map.of()); } - /** - * 每个轴保留 21 位;原理图尺寸远小于该上限,同时避免为每次邻接方块查询创建 BlockPos 对象。 - */ + // 将坐标打包为 long private static long packPosition(int x, int y, int z) { return ((long) x << 42) | ((long) y << 21) | z; } - /** 单个非空气方块及其相对原理图坐标。 */ + // 单个非空气方块及其相对原理图坐标 public record Block(int x, int y, int z, BlockState state) { } } diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewOverlay.java b/src/main/java/com/listmore/schematic/SchematicPreviewOverlay.java index c35c826..4896edd 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewOverlay.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewOverlay.java @@ -9,7 +9,7 @@ import net.minecraft.client.gui.GuiGraphics; //#endif -/** 原理图浏览器右侧区域的 GUI 覆盖层。实际方块渲染由后续独立渲染器负责。 */ +// 原理图浏览器右侧区域的GUI覆盖层 -> 渲染层 public final class SchematicPreviewOverlay { private static final int BACKGROUND_COLOR = 0xEE111820; private static final int BORDER_COLOR = 0xFF5C7185; @@ -19,7 +19,8 @@ public final class SchematicPreviewOverlay { private SchematicPreviewOverlay() { } - /** 绘制预览背景、加载状态和八方向操作按钮。 */ + // 绘制预览背景、加载状态和方向操作按钮 + // 组件层级:背景 -> 边框 -> 状态提示/3D 模型/占位符 -> 方向按钮 -> 缩放按钮 public static void draw( //#if MC >= 1.21.11 //$$ GuiContext context, @@ -76,6 +77,7 @@ private static void drawBorder( RenderUtils.drawRect(context, layout.x() + layout.width() - 1, layout.y(), 1, layout.height(), BORDER_COLOR); } + // 当渲染后端不可用时显示的占位符,绘制一个绿色方框和方块数量 private static void drawModelPlaceholder( //#if MC >= 1.21.11 //$$ GuiContext context, diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewRenderBackend.java b/src/main/java/com/listmore/schematic/SchematicPreviewRenderBackend.java index b7b71d8..7899711 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewRenderBackend.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewRenderBackend.java @@ -1,17 +1,11 @@ package com.listmore.schematic; -/** - * 原理图预览的版本专属渲染后端。 - * - *

实现类位于各 Minecraft 子版本的源码目录中。公共预览逻辑只通过这个接口调用, - * 因此不需要在共同源码内引用某个版本独有的图形 API。

- */ +// 原理图预览的版本专属渲染后端,公共预览逻辑只通过这个接口调用 public interface SchematicPreviewRenderBackend extends AutoCloseable { - /** Supplies the parsed schematic to version-specific read-only world adapters. */ default void setSchematic(Object schematic) { } - /** 将当前快照绘制到预览区域;无法绘制时返回 {@code false}。 */ + // 将当前快照绘制到预览区域;无法绘制时返回false boolean render(Object context, SchematicPreviewLayout layout, SchematicPreviewTransform transform, SchematicPreviewModel model, long revision); diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewRenderManager.java b/src/main/java/com/listmore/schematic/SchematicPreviewRenderManager.java new file mode 100644 index 0000000..c02e6a1 --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewRenderManager.java @@ -0,0 +1,74 @@ +package com.listmore.schematic; + +//#if MC >= 1.21.11 +//$$ import fi.dy.masa.malilib.render.GuiContext; +//#else +import net.minecraft.client.gui.GuiGraphics; +//#endif + +public final class SchematicPreviewRenderManager implements AutoCloseable { + private SchematicPreviewModel model; + private Object schematic; + private long modelRevision; + private final SchematicPreviewRenderBackend backend = createBackend(); + + // 提交新的原理图快照,模型只会在 GUI 线程调用此方法 + public void setModel(SchematicPreviewModel model) { + if (this.model == model) { + return; + } + this.model = model; + this.modelRevision++; + } + + public void setSchematic(Object schematic) { + this.schematic = schematic; + if (this.backend != null) { + this.backend.setSchematic(schematic); + } + } + + // 返回当前快照的版本号 + public long modelRevision() { + return this.modelRevision; + } + + // 将预览模型绘制到右侧信息面板 + // 版本差异由渲染后端处理:通过接口委托给版本专属的 SchematicPreviewRenderer 实现 + public boolean render( + //#if MC >= 1.21.11 + //$$ GuiContext context, + //#else + GuiGraphics context, + //#endif + SchematicPreviewLayout layout, SchematicPreviewTransform transform) { + return this.backend != null && this.backend.render(context, layout, transform, this.model, this.modelRevision); + } + + public SchematicPreviewModel model() { + return this.model; + } + + // 关闭预览时释放渲染资源 + @Override + public void close() { + if (this.backend != null) { + this.backend.close(); + } + this.model = null; + this.schematic = null; + this.modelRevision++; + } + + // 通过反射加载版本专属的渲染后端,避免编译时硬依赖 + // 如果当前版本没有对应的 SchematicPreviewRenderer 类,回退到 null(GUI 占位预览) + private static SchematicPreviewRenderBackend createBackend() { + try { + Class backendClass = Class.forName("com.listmore.schematic.SchematicPreviewRenderer"); + return backendClass.asSubclass(SchematicPreviewRenderBackend.class).getConstructor().newInstance(); + } catch (ReflectiveOperationException ignored) { + // 当前版本尚未提供专属后端时保留 GUI 占位预览 + return null; + } + } +} diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewRenderer.java b/src/main/java/com/listmore/schematic/SchematicPreviewRenderer.java index d1cb804..1eae4ef 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewRenderer.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewRenderer.java @@ -1,86 +1,618 @@ package com.listmore.schematic; +import com.listmore.ListMore; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; + +//#if MC >= 26.2 +//$$ import com.mojang.blaze3d.IndexType; +//$$ import com.mojang.blaze3d.PrimitiveTopology; +//$$ import com.mojang.blaze3d.GpuFormat; +//#else +import com.mojang.blaze3d.vertex.VertexFormat.IndexType; +//#endif +import com.mojang.blaze3d.ProjectionType; +import com.mojang.blaze3d.buffers.GpuBuffer; +import com.mojang.blaze3d.buffers.GpuBufferSlice; +import com.mojang.blaze3d.buffers.Std140Builder; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderPass; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.textures.AddressMode; +import com.mojang.blaze3d.textures.FilterMode; +//#if MC >= 1.21.11 +//$$ import com.mojang.blaze3d.textures.GpuSampler; +//#endif +import com.mojang.blaze3d.textures.GpuTextureView; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.MeshData; +//#if MC < 26.2 +import com.mojang.blaze3d.vertex.VertexFormat; +//#endif +//#if MC >= 26.1 +//$$ import fi.dy.masa.litematica.render.schematic.BlockModelRendererSchematic; +//$$ import fi.dy.masa.litematica.render.schematic.IBlockOutputSchematic; +//$$ import fi.dy.masa.litematica.schematic.LitematicaSchematic; +//#else +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.ItemBlockRenderTypes; +import net.minecraft.util.RandomSource; +//#endif //#if MC >= 1.21.11 //$$ import fi.dy.masa.malilib.render.GuiContext; //#else import net.minecraft.client.gui.GuiGraphics; //#endif +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.render.TextureSetup; +import net.minecraft.client.renderer.DynamicUniforms; +import net.minecraft.client.renderer.GlobalSettingsUniform; +//#if MC >= 26.1 +//$$ import net.minecraft.client.renderer.Projection; +//#endif +//#if MC >= 26.1 +//$$ import net.minecraft.client.renderer.block.FluidRenderer; +//$$ import net.minecraft.client.renderer.ProjectionMatrixBuffer; +//#else +import net.minecraft.client.renderer.PerspectiveProjectionMatrixBuffer; +//#endif +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.chunk.ChunkSectionLayer; +import net.minecraft.client.renderer.chunk.ChunkSectionLayerGroup; +//#if MC >= 26.1 +//$$ import net.minecraft.client.renderer.state.gui.BlitRenderState; +//#else +import net.minecraft.client.gui.render.state.BlitRenderState; +//#endif +import net.minecraft.client.renderer.texture.TextureAtlas; +//#if MC >= 26.1 +//$$ import net.minecraft.world.phys.Vec3; +//#endif +import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import org.joml.Matrix4f; +import org.joml.Matrix4fStack; +import org.joml.Matrix3x2f; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.lwjgl.system.MemoryStack; -/** - * 原理图预览渲染器的公共生命周期入口。 - * - *

它只持有 ListMore 自己生成的只读模型快照,不读取或修改 Litematica 的全局投影世界、 - * 投影摆放和世界渲染器。不同 Minecraft 图形管线的具体实现放在独立适配器中。

- */ -public final class SchematicPreviewRenderer implements AutoCloseable { - private SchematicPreviewModel model; - private Object schematic; - private long modelRevision; - private final SchematicPreviewRenderBackend backend = createBackend(); - - /** - * 提交新的原理图快照。 - * 模型只会在 GUI 线程调用此方法,因此适配器可以在下一帧安全地重建自己的网格和帧缓冲资源。 - */ - public void setModel(SchematicPreviewModel model) { - if (this.model == model) { - return; - } - this.model = model; - this.modelRevision++; +public final class SchematicPreviewRenderer implements SchematicPreviewRenderBackend { + private final List meshes = new ArrayList<>(); + private long builtRevision = Long.MIN_VALUE; + //#if MC >= 26.1 + //$$ private LitematicaSchematic schematic; + //#endif + private SchematicPreviewWorld world; + private TextureTarget target; + //#if MC >= 1.21.11 + //$$ private GpuSampler sampler; + //#endif + //#if MC >= 26.1 + //$$ private ProjectionMatrixBuffer projection; + //#else + private PerspectiveProjectionMatrixBuffer projection; + //#endif +//#if MC >= 26.1 + //$$ private final Projection previewProjection = new Projection(); +//#endif + private GpuBuffer globalUniform; + private boolean rebuildFailureLogged; + private boolean renderFailureLogged; + //#if MC >= 26.1 + //$$ private final FluidRenderer fluidRenderer = new FluidRenderer(Minecraft.getInstance().getModelManager().getFluidStateModelSet()); + //#endif + @Override + public void setSchematic(Object schematic) { + //#if MC >= 26.1 + //$$ this.schematic = schematic instanceof LitematicaSchematic loaded ? loaded : null; + //#endif } - public void setSchematic(Object schematic) { - this.schematic = schematic; - if (this.backend != null) { - this.backend.setSchematic(schematic); + // 将原理图模型渲染到离屏帧缓冲,再将结果贴图绘制到 GUI 面板 + // 流程:校验模型 -> 确保帧缓冲尺寸 -> 增量重建网格 -> 清空帧缓冲 -> 3D 渲染 -> 贴图到 GUI + public boolean render(Object rawContext, SchematicPreviewLayout layout, SchematicPreviewTransform transform, + SchematicPreviewModel model, long revision) { + //#if MC >= 1.21.11 + //$$ GuiContext context = (GuiContext) rawContext; + //#else + GuiGraphics context = (GuiGraphics) rawContext; + //#endif + if (model == null || model.blocks().isEmpty() || layout.contentWidth() <= 0 || layout.contentHeight() <= 0) { + return false; } + // 确保离屏帧缓冲尺寸与当前预览区域匹配 + this.ensureTarget(layout.contentWidth(), layout.contentHeight()); + // 仅当模型快照版本号变化时才重建网格,避免每帧重建 + if (this.builtRevision != revision) { + try { + this.rebuild(model, revision); + this.rebuildFailureLogged = false; + } catch (Throwable throwable) { + if (!this.rebuildFailureLogged) { + ListMore.LOGGER.error("Failed to build schematic preview meshes", throwable); + this.rebuildFailureLogged = true; + } + return false; + } + } + if (this.meshes.isEmpty()) { + return false; + } + + // 清空帧缓冲的颜色和深度,背景色为0xFF1B2028 + //#if MC >= 26.2 + //$$ RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures( + //$$ this.target.getColorTexture(), new org.joml.Vector4f(0.105F, 0.125F, 0.157F, 1.0F), + //$$ this.target.getDepthTexture(), 0.0D); + //#else + RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures( + this.target.getColorTexture(), 0xFF1B2028, this.target.getDepthTexture(), 1.0D); + //#endif + try { + // 3D渲染阶段:设置相机投影并绘制所有网格到离屏帧缓冲 + this.renderTarget(model, transform); + this.renderFailureLogged = false; + } catch (Throwable throwable) { + if (!this.renderFailureLogged) { + ListMore.LOGGER.error("Failed to render schematic preview", throwable); + this.renderFailureLogged = true; + } + return false; + } + // 将离屏帧缓冲的纹理贴图到 GUI 面板的指定区域 + BlitRenderState blit = new BlitRenderState( + RenderPipelines.GUI_TEXTURED, + //#if MC >= 1.21.11 + //$$ TextureSetup.singleTexture(this.target.getColorTextureView(), this.sampler), + //#else + TextureSetup.singleTexture(this.target.getColorTextureView()), + //#endif + new Matrix3x2f(context.pose()), + layout.contentX(), layout.contentY(), + layout.contentX() + layout.contentWidth(), layout.contentY() + layout.contentHeight(), + 0.0F, 1.0F, 1.0F, 0.0F, 0xFFFFFFFF, context.scissorStack.peek()); + //#if MC >= 1.21.11 + //$$ context.addSimpleElement(blit); + //#else + context.guiRenderState.submitBlitToCurrentLayer(blit); + //#endif + return true; + } + + private void ensureTarget(int width, int height) { + int scaledWidth = Math.max(1, Math.round(width * Minecraft.getInstance().getWindow().getGuiScale())); + int scaledHeight = Math.max(1, Math.round(height * Minecraft.getInstance().getWindow().getGuiScale())); + if (this.target == null) { + //#if MC >= 26.2 + //$$ this.target = new TextureTarget("ListMore schematic preview", scaledWidth, scaledHeight, true, GpuFormat.RGBA8_UNORM); + //#else + this.target = new TextureTarget("ListMore schematic preview", scaledWidth, scaledHeight, true); + //#endif + } else if (this.target.width != scaledWidth || this.target.height != scaledHeight) { + this.target.resize(scaledWidth, scaledHeight); + } + //#if MC >= 1.21.11 + //$$ if (this.sampler == null) { + //$$ this.sampler = RenderSystem.getDevice().createSampler(AddressMode.CLAMP_TO_EDGE, AddressMode.CLAMP_TO_EDGE, + //$$ FilterMode.LINEAR, FilterMode.LINEAR, 1, OptionalDouble.empty()); + //$$ } + //#endif } - /** 返回当前快照的递增版本号,供版本渲染适配器判断是否需要重建网格。 */ - public long modelRevision() { - return this.modelRevision; + // 从原理图模型重建所有网格数据并上传到 GPU + // 流程:释放旧网格 -> 创建虚拟世界视图 -> 遍历方块按区块分组 -> 调用原版渲染器生成网格 -> 构建 GPU 缓冲区 + private void rebuild(SchematicPreviewModel model, long revision) { + this.closeMeshes(); + this.builtRevision = revision; + //#if MC >= 26.1 + //$$ if (this.schematic == null) { + //$$ return; + //$$ } + //#endif + if (this.world == null) { + this.world = new SchematicPreviewWorld(Minecraft.getInstance()); + } + this.world.setModel(model); + SchematicPreviewWorld view = this.world; + //#if MC >= 26.1 + //$$ ModelManager modelManager = Minecraft.getInstance().getModelManager(); + //#endif + // 按区块位置分组构建网格,每个 ChunkPos 对应一个 ChunkMeshBuilder + Map chunks = new HashMap<>(); + //#if MC >= 26.1 + //$$ BlockModelRendererSchematic renderer = new BlockModelRendererSchematic(); + //$$ renderer.enableCache(); + //#endif + try { + // 遍历所有非空气方块,分别渲染流体和固体模型 + for (SchematicPreviewModel.Block block : model.blocks()) { + BlockState state = block.state(); + BlockPos position = new BlockPos(block.x(), block.y(), block.z()); + // >> 4 将方块坐标转为区块坐标,computeIfAbsent 保证每个区块只有一个 Builder + ChunkMeshBuilder chunk = chunks.computeIfAbsent(new ChunkPos(block.x() >> 4, block.z() >> 4), ignored -> new ChunkMeshBuilder()); + // 先渲染流体(水、岩浆等),再渲染固体方块模型 + //#if MC >= 26.1 + //$$ if (!state.getFluidState().isEmpty()) { + //$$ this.fluidRenderer.tesselate(view, position, chunk::builder, state, state.getFluidState()); + //$$ } + //#else + if (!state.getFluidState().isEmpty()) { + Minecraft.getInstance().getBlockRenderer().renderLiquid(position, view, + chunk.builder(ItemBlockRenderTypes.getRenderLayer(state.getFluidState())), state, state.getFluidState()); + } + //#endif + if (state.getRenderShape() != RenderShape.MODEL) { + continue; + } + //#if MC >= 26.1 + //$$ IBlockOutputSchematic output = (x, y, z, quad, instance) -> chunk.builder(quad.materialInfo().layer()) + //$$ .putBlockBakedQuad(x, y, z, quad, instance); + //$$ renderer.tessellateBlock(view, state, position, new Vec3(block.x() & 15, block.y(), block.z() & 15), + //$$ modelManager.getBlockStateModelSet().get(state), state.getSeed(position), output); + //#else + PoseStack pose = new PoseStack(); + // & 15 取区块内局部坐标(0-15),模型顶点需要相对区块原点的偏移 + pose.translate(block.x() & 15, block.y(), block.z() & 15); + Minecraft.getInstance().getBlockRenderer().renderBatched(state, position, view, pose, + chunk.builder(ItemBlockRenderTypes.getChunkRenderType(state)), true, + Minecraft.getInstance().getBlockRenderer().getBlockModel(state) + .collectParts(RandomSource.create(state.getSeed(position)))); + //#endif + } + } finally { + //#if MC >= 26.1 + //$$ renderer.disableCache(); + //#endif + } + // 所有方块遍历完毕后,将每个区块的网格数据上传到 GPU + chunks.forEach((position, chunk) -> { + ChunkMesh built = chunk.build(position); + if (built != null) { + this.meshes.add(built); + } + }); + } + + // 设置相机、投影矩阵和全局 Uniform,然后将所有网格渲染到离屏帧缓冲 + // 相机位置根据偏航角/俯仰角/缩放计算,围绕模型包围盒的中心旋转 + private void renderTarget(SchematicPreviewModel model, SchematicPreviewTransform transform) { + // 计算相机在模型包围球上的位置 + // 球坐标 -> 空间坐标:x = centerX - sin(yaw)*cos(pitch)*dist, y = centerY - sin(pitch)*dist, z = centerZ + cos(yaw)*cos(pitch)*dist + float distance = transform.distance(model.size().getX(), model.size().getY(), model.size().getZ()); + float yaw = transform.yaw() * Mth.DEG_TO_RAD; + float pitch = transform.pitch() * Mth.DEG_TO_RAD; + float horizontal = Mth.cos(pitch); + Vector3f camera = new Vector3f( + model.centerX() - Mth.sin(yaw) * horizontal * distance, + model.centerY() - Mth.sin(pitch) * distance, + model.centerZ() + Mth.cos(yaw) * horizontal * distance); + Matrix4fStack modelView = RenderSystem.getModelViewStack(); + modelView.pushMatrix(); + // 与 SchematicPreview 的轨道约定一致:UI 偏航角描述观察方向 + // 而地形视图矩阵使用相机偏航角的逆,因此取 -yaw 并共轭 + Quaternionf rotation = new Quaternionf().rotationYXZ(-yaw, pitch, 0.0F).conjugate(); + modelView.set(new Matrix4f().rotation(rotation)); + // 构建透视投影矩阵,近平面 0.05,远平面 4096,FOV 70° + //#if MC >= 26.1 + //$$ this.previewProjection.setupPerspective(0.05F, 4096.0F, 70.0F, + //$$ this.target.width, this.target.height); + //$$ Matrix4f projectionMatrix = this.previewProjection.getMatrix(new Matrix4f()); + //#else + Matrix4f projectionMatrix = new Matrix4f().perspective(70.0F * Mth.DEG_TO_RAD, + (float) this.target.width / this.target.height, 0.05F, 4096.0F); + //#endif + // 备份原版投影矩阵和全局 Uniform,渲染完成后恢复 + RenderSystem.backupProjectionMatrix(); + if (this.projection == null) { + //#if MC >= 26.1 + //$$ this.projection = new ProjectionMatrixBuffer("ListMore schematic preview"); + //#else + this.projection = new PerspectiveProjectionMatrixBuffer("ListMore schematic preview"); + //#endif + } + RenderSystem.setProjectionMatrix(this.projection.getBuffer(projectionMatrix), ProjectionType.PERSPECTIVE); + GpuBuffer previousGlobalUniform = RenderSystem.getGlobalSettingsUniform(); + this.writeGlobalUniform(camera); + try { + //#if MC >= 26.2 + //$$ Minecraft.getInstance().gameRenderer.lighting().setupFor(Lighting.Entry.LEVEL); + //#else + Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.LEVEL); + //#endif + this.renderMeshes(camera); + } finally { + RenderSystem.setGlobalSettingsUniform(previousGlobalUniform); + RenderSystem.restoreProjectionMatrix(); + modelView.popMatrix(); + } } - /** - * 将预览模型绘制到右侧的信息面板中。 - * 渲染后端和 GUI 上下文的 API 差异被限制在这里,公共会话层不需要了解 Minecraft 的图形管线。 - */ - public boolean render( + // 向 GPU 写入全局 Uniform 缓冲区,包含相机位置和窗口尺寸 + // 使用 Std140 布局,与着色器中的 GlobalSettings 结构体对齐 + private void writeGlobalUniform(Vector3f camera) { + if (this.globalUniform == null) { + this.globalUniform = RenderSystem.getDevice().createBuffer(() -> "ListMore preview global settings", + GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_COPY_DST, GlobalSettingsUniform.UBO_SIZE); + } + try (MemoryStack stack = MemoryStack.stackPush()) { //#if MC >= 1.21.11 - //$$ GuiContext context, + //$$ int floorX = Mth.floor(camera.x); + //$$ int floorY = Mth.floor(camera.y); + //$$ int floorZ = Mth.floor(camera.z); + //$$ RenderSystem.getDevice().createCommandEncoder().writeToBuffer(this.globalUniform.slice(), + //$$ Std140Builder.onStack(stack, GlobalSettingsUniform.UBO_SIZE) + //$$ .putIVec3(floorX, floorY, floorZ) + //$$ .putVec3(floorX - camera.x, floorY - camera.y, floorZ - camera.z) + //$$ .putVec2(Minecraft.getInstance().getWindow().getWidth(), Minecraft.getInstance().getWindow().getHeight()) + //$$ .putFloat(1.0F).putFloat(0.0F).putInt(0).putInt(0) + //$$ .get()); //#else - GuiGraphics context, + RenderSystem.getDevice().createCommandEncoder().writeToBuffer(this.globalUniform.slice(), + Std140Builder.onStack(stack, GlobalSettingsUniform.UBO_SIZE) + .putVec2(Minecraft.getInstance().getWindow().getWidth(), Minecraft.getInstance().getWindow().getHeight()) + .putFloat(1.0F).putFloat(0.0F).putInt(0) + .get()); //#endif - SchematicPreviewLayout layout, SchematicPreviewTransform transform) { - return this.backend != null && this.backend.render(context, layout, transform, this.model, this.modelRevision); + } + RenderSystem.setGlobalSettingsUniform(this.globalUniform); } - public SchematicPreviewModel model() { - return this.model; + // 按距离排序所有网格,为每个网格构建 Transform/SectionInfo Uniform,然后分不透明/半透明两层渲染 + private void renderMeshes(Vector3f camera) { + var atlas = Minecraft.getInstance().getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(); + List orderedMeshes = new ArrayList<>(this.meshes); + // 按距离降序排列(远的先画),负号使 Comparator 从大到小 + orderedMeshes.sort(Comparator.comparingDouble(mesh -> -mesh.distanceTo(camera))); + //#if MC >= 1.21.11 + //$$ DynamicUniforms.ChunkSectionInfo[] infos = new DynamicUniforms.ChunkSectionInfo[orderedMeshes.size()]; + //$$ for (int i = 0; i < orderedMeshes.size(); i++) { + //$$ ChunkMesh mesh = orderedMeshes.get(i); + //$$ infos[i] = new DynamicUniforms.ChunkSectionInfo( + //#if MC >= 26.2 + //$$ RenderSystem.getModelViewMatrixCopy(), + //#else + //$$ new Matrix4f(RenderSystem.getModelViewMatrix()), + //#endif + //$$ mesh.chunk().getMinBlockX(), 0, + //$$ mesh.chunk().getMinBlockZ(), 1.0F, atlas.getWidth(0), atlas.getHeight(0)); + //$$ } + //$$ GpuBufferSlice[] sectionUniforms = RenderSystem.getDynamicUniforms().writeChunkSections(infos); + //#else + DynamicUniforms.Transform[] transforms = new DynamicUniforms.Transform[orderedMeshes.size()]; + for (int i = 0; i < orderedMeshes.size(); i++) { + ChunkMesh mesh = orderedMeshes.get(i); + //#if MC < 1.21.11 + transforms[i] = new DynamicUniforms.Transform(new Matrix4f(RenderSystem.getModelViewMatrix()), + new Vector4f(1.0F), new Vector3f(mesh.chunk().getMinBlockX() - camera.x, -camera.y, + mesh.chunk().getMinBlockZ() - camera.z), new Matrix4f(), 1.0F); + //#else + //$$ transforms[i] = new DynamicUniforms.Transform(new Matrix4f(RenderSystem.getModelViewMatrix()), + //$$ new Vector4f(1.0F), new Vector3f(mesh.chunk().getMinBlockX() - camera.x, -camera.y, + //$$ mesh.chunk().getMinBlockZ() - camera.z), new Matrix4f()); + //#endif + } + GpuBufferSlice[] transformUniforms = RenderSystem.getDynamicUniforms().writeTransforms(transforms); + //#endif + // 获取顺序索引缓冲区,供没有独立索引缓冲的网格共享使用 + RenderSystem.AutoStorageIndexBuffer sequential = RenderSystem.getSequentialBuffer( + //#if MC >= 26.2 + //$$ PrimitiveTopology.QUADS + //#else + VertexFormat.Mode.QUADS + //#endif + ); + // 找出所有网格中最大的顺序索引需求,确保共享缓冲区足够大 + int maxSequentialIndices = 0; + for (ChunkMesh chunk : orderedMeshes) { + for (SectionBuffers mesh : chunk.buffers().values()) { + if (mesh.indexBuffer() == null) { + maxSequentialIndices = Math.max(maxSequentialIndices, mesh.indexCount()); + } + } + } + GpuBuffer sharedIndex = maxSequentialIndices == 0 ? null : sequential.getBuffer(maxSequentialIndices); + IndexType sharedIndexType = maxSequentialIndices == 0 ? null : sequential.type(); + // 先画不透明层(reverse=false),再画半透明层(reverse=true,从远到近保证混合正确) + //#if MC >= 1.21.11 + //$$ this.renderLayerGroup(orderedMeshes, sectionUniforms, atlas, sharedIndex, sharedIndexType, + //$$ ChunkSectionLayerGroup.OPAQUE, false); + //$$ this.renderLayerGroup(orderedMeshes, sectionUniforms, atlas, sharedIndex, sharedIndexType, + //$$ ChunkSectionLayerGroup.TRANSLUCENT, true); + //#else + this.renderLayerGroup(orderedMeshes, transformUniforms, atlas, sharedIndex, sharedIndexType, + ChunkSectionLayerGroup.OPAQUE, false); + this.renderLayerGroup(orderedMeshes, transformUniforms, atlas, sharedIndex, sharedIndexType, + ChunkSectionLayerGroup.TRANSLUCENT, true); + //#endif } - /** - * 预览浏览器关闭时释放渲染器持有的快照。 - * GPU 帧缓冲和网格资源会由对应版本的渲染适配器在这里一并释放。 - */ - @Override - public void close() { - if (this.backend != null) { - this.backend.close(); + // 为指定的渲染层组(不透明或半透明)创建 RenderPass 并绘制所有网格 + // 每个 ChunkSectionLayer 对应一种渲染管线(如 solid、cutout、translucent) + // reverse 参数控制绘制顺序:半透明层需要从远到近绘制以保证混合正确 + private void renderLayerGroup(List orderedMeshes, GpuBufferSlice[] meshUniforms, + GpuTextureView atlas, GpuBuffer sharedIndex, IndexType sharedIndexType, + ChunkSectionLayerGroup group, boolean reverse) { + try (RenderPass pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass( + () -> "ListMore schematic preview " + group.label(), this.target.getColorTextureView(), + //#if MC >= 26.2 + //$$ Optional.empty(), + //#else + OptionalInt.empty(), + //#endif + this.target.getDepthTextureView(), OptionalDouble.empty())) { + RenderSystem.bindDefaultUniforms(pass); + // 绑定光照纹理(Sampler2),所有渲染管线都需要 + //#if MC >= 26.1 + //$$ pass.bindTexture("Sampler2", Minecraft.getInstance().gameRenderer.lightmap(), this.sampler); + //#elseif MC >= 1.21.11 + //$$ pass.bindTexture("Sampler2", Minecraft.getInstance().gameRenderer.lightTexture().getTextureView(), this.sampler); + //#else + pass.bindSampler("Sampler2", Minecraft.getInstance().gameRenderer.lightTexture().getTextureView()); + //#endif + for (ChunkSectionLayer layer : group.layers()) { + pass.setPipeline(layer.pipeline()); + // 绑定方块纹理图集(Sampler0) + //#if MC >= 26.1 + //$$ pass.bindTexture("Sampler0", atlas, this.sampler); + //#elseif MC >= 1.21.11 + //$$ pass.bindTexture("Sampler0", atlas, this.sampler); + //#else + pass.bindSampler("Sampler0", atlas); + //#endif + List> draws = new ArrayList<>(); + for (int i = 0; i < orderedMeshes.size(); i++) { + SectionBuffers mesh = orderedMeshes.get(i).buffers().get(layer); + if (mesh == null || mesh.indexCount() <= 0 || mesh.vertexBuffer().isClosed()) continue; + // 捕获 i 到局部变量,lambda 内不能直接引用循环变量 + int uniformIndex = i; + //#if MC >= 26.2 + //$$ draws.add(new RenderPass.Draw<>(0, mesh.vertexBuffer(), mesh.indexBuffer(), + //$$ mesh.indexBuffer() == null ? null : mesh.indexType(), 0, mesh.indexCount(), 0, + //$$ (slices, uploader) -> uploader.upload("ChunkSection", slices[uniformIndex]))); + //#elseif MC >= 26.1 + //$$ draws.add(new RenderPass.Draw<>(0, mesh.vertexBuffer(), mesh.indexBuffer(), + //$$ mesh.indexBuffer() == null ? null : mesh.indexType(), 0, mesh.indexCount(), 0, + //$$ (slices, uploader) -> uploader.upload("ChunkSection", slices[uniformIndex]))); + //#elseif MC >= 1.21.11 + //$$ draws.add(new RenderPass.Draw<>(0, mesh.vertexBuffer(), mesh.indexBuffer(), + //$$ mesh.indexBuffer() == null ? null : mesh.indexType(), 0, mesh.indexCount(), + //$$ (slices, uploader) -> uploader.upload("ChunkSection", slices[uniformIndex]))); + //#else + draws.add(new RenderPass.Draw<>(0, mesh.vertexBuffer(), mesh.indexBuffer(), + mesh.indexBuffer() == null ? null : mesh.indexType(), 0, mesh.indexCount(), + (slices, uploader) -> uploader.upload("DynamicTransforms", slices[uniformIndex]))); + //#endif + } + if (!draws.isEmpty()) { + // 半透明层需要反转绘制顺序(远 -> 近),不透明层保持原序(近 -> 远,利用深度测试) + if (reverse) draws = draws.reversed(); + //#if MC >= 1.21.11 + //$$ pass.drawMultipleIndexed(draws, sharedIndex, sharedIndexType, List.of("ChunkSection"), meshUniforms); + //#else + pass.drawMultipleIndexed(draws, sharedIndex, sharedIndexType, List.of("DynamicTransforms"), meshUniforms); + //#endif + } + } } - this.model = null; - this.schematic = null; - this.modelRevision++; } - private static SchematicPreviewRenderBackend createBackend() { - try { - Class backendClass = Class.forName("com.listmore.schematic.SchematicPreviewRenderer26"); - return backendClass.asSubclass(SchematicPreviewRenderBackend.class).getConstructor().newInstance(); - } catch (ReflectiveOperationException ignored) { - // 当前版本尚未提供专属后端时保留 GUI 占位预览。 - return null; + private void closeMeshes() { + this.meshes.forEach(ChunkMesh::close); + this.meshes.clear(); + } + + // 快照变化时重建 GPU 缓冲区 + private record SectionBuffers(GpuBuffer vertexBuffer, GpuBuffer indexBuffer, int indexCount, + IndexType indexType) implements AutoCloseable { + @Override + public void close() { + this.vertexBuffer.close(); + if (this.indexBuffer != null) { + this.indexBuffer.close(); + } + } + } + + // 每个区块的网格构建器,按 ChunkSectionLayer 分组管理 BufferBuilder + private static final class ChunkMeshBuilder implements AutoCloseable { + private final Map allocators = new EnumMap<>(ChunkSectionLayer.class); + private final Map builders = new EnumMap<>(ChunkSectionLayer.class); + + private BufferBuilder builder(ChunkSectionLayer layer) { + return this.builders.computeIfAbsent(layer, ignored -> new BufferBuilder( + this.allocators.computeIfAbsent(layer, unused -> new ByteBufferBuilder(4096)), + //#if MC >= 26.2 + //$$ layer.pipeline().getPrimitiveTopology(), layer.vertexFormat() + //#else + layer.pipeline().getVertexFormatMode(), layer.pipeline().getVertexFormat() + //#endif + )); + } + + // 将 BufferBuilder 中的网格数据上传到 GPU,创建顶点和索引缓冲区 + // 返回包含所有 GPU 缓冲区和原始 MeshData 的 ChunkMesh + private ChunkMesh build(ChunkPos position) { + Map buffers = new EnumMap<>(ChunkSectionLayer.class); + List meshData = new ArrayList<>(); + this.builders.forEach((layer, builder) -> { + // BufferBuilder.build() 将 CPU 端顶点数据打包为 MeshData + MeshData mesh = builder.build(); + if (mesh == null) { + return; + } + meshData.add(mesh); + // 将 MeshData 上传到 GPU,创建顶点和索引缓冲区 + GpuBuffer vertex = RenderSystem.getDevice().createBuffer(() -> "ListMore preview vertex buffer", + GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, mesh.vertexBuffer()); + // 索引缓冲区可能为空(使用共享顺序索引时) + GpuBuffer index = mesh.indexBuffer() == null ? null : RenderSystem.getDevice().createBuffer( + () -> "ListMore preview index buffer", GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, + mesh.indexBuffer()); + buffers.put(layer, new SectionBuffers(vertex, index, mesh.drawState().indexCount(), mesh.drawState().indexType())); + }); + if (buffers.isEmpty()) { + // 所有层都为空时释放资源并返回 null + meshData.forEach(MeshData::close); + this.close(); + return null; + } + return new ChunkMesh(position, buffers, meshData, new ArrayList<>(this.allocators.values())); + } + + @Override + public void close() { + this.allocators.values().forEach(ByteBufferBuilder::close); + this.allocators.clear(); } } + + // 一个区块位置的完整网格数据,包含各渲染层的 GPU 缓冲区和原始构建数据 + private record ChunkMesh(ChunkPos chunk, Map buffers, + List meshData, List allocators) implements AutoCloseable { + // 计算区块中心到相机的距离平方,用于排序(远到近) + private double distanceTo(Vector3f camera) { + float x = this.chunk.getMinBlockX() + 8.0F; + float z = this.chunk.getMinBlockZ() + 8.0F; + return Mth.square(x - camera.x) + Mth.square(z - camera.z); + } + + @Override + public void close() { + this.allocators.forEach(ByteBufferBuilder::close); + this.meshData.forEach(MeshData::close); + this.buffers.values().forEach(SectionBuffers::close); + } + } + + @Override + public void close() { + this.closeMeshes(); + this.builtRevision = Long.MIN_VALUE; + //#if MC >= 26.1 + //$$ this.schematic = null; + //#endif + this.world = null; + if (this.target != null) { this.target.destroyBuffers(); this.target = null; } + //#if MC >= 1.21.11 + //$$ if (this.sampler != null) { this.sampler.close(); this.sampler = null; } + //#endif + if (this.projection != null) { this.projection.close(); this.projection = null; } + if (this.globalUniform != null) { this.globalUniform.close(); this.globalUniform = null; } + } } diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewSession.java b/src/main/java/com/listmore/schematic/SchematicPreviewSession.java index 0704b51..435c1e3 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewSession.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewSession.java @@ -4,23 +4,19 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicLong; -import com.listmore.ListMore; import fi.dy.masa.litematica.schematic.LitematicaSchematic; import net.minecraft.core.Vec3i; -/** - * 单个原理图浏览器的预览生命周期。 - * 异步加载完成后会检查 generation,避免之前选择的文件覆盖当前预览。 - */ +// 单个原理图浏览器的预览状态 public final class SchematicPreviewSession { private final AtomicLong generation = new AtomicLong(); private final SchematicPreviewTransform transform = new SchematicPreviewTransform(); - private final SchematicPreviewRenderer renderer = new SchematicPreviewRenderer(); + private final SchematicPreviewRenderManager renderer = new SchematicPreviewRenderManager(); private Path file; private CompletableFuture loadingTask; private SchematicPreviewModel model; private Throwable loadFailure; - /** 后台加载线程写入,GUI 绘制线程读取。 */ + // 后台加载线程写入,GUI绘制线程读取 private volatile LoadResult pendingResult; private boolean closed; @@ -28,8 +24,8 @@ public SchematicPreviewTransform transform() { return this.transform; } - /** 返回当前浏览器专属的预览渲染器。 */ - public SchematicPreviewRenderer renderer() { + // 返回当前浏览器专属的预览渲染器 + public SchematicPreviewRenderManager renderer() { return this.renderer; } @@ -49,24 +45,28 @@ public boolean hasFailure() { return this.loadFailure != null; } - /** 仅在用户在原理图列表中选中新的 litematic 文件时启动新的加载任务。 */ + // 仅在列表中选中新的 litematic 文件时启动新的加载任务 + // 流程:路径规范化 -> 去重检查 -> 重置旧状态 -> 启动异步加载 -> 结果写入 pendingResult + // generation 防止旧请求的结果覆盖新请求 public void setFile(Path file) { Path normalizedFile = file.toAbsolutePath().normalize(); if (this.closed || normalizedFile.equals(this.file)) { return; } + // 重置所有状态,准备新文件加载 this.file = normalizedFile; - ListMore.LOGGER.info("Schematic preview: selected {}", normalizedFile); this.model = null; this.renderer.close(); this.loadFailure = null; this.pendingResult = null; this.transform.reset(); + // 递增 generation 使旧请求的回调失效 long requestGeneration = this.generation.incrementAndGet(); CompletableFuture task = SchematicPreviewLoader.load(normalizedFile); this.loadingTask = task; task.whenComplete((loaded, throwable) -> { + // 检查:未关闭 + generation 匹配 + 未被新任务替换 if (this.closed || requestGeneration != this.generation.get() || task != this.loadingTask) { return; } @@ -74,7 +74,9 @@ public void setFile(Path file) { }); } - /** 在 GUI 绘制线程应用后台加载结果,避免后台线程修改渲染状态。 */ + // 在 GUI 绘制线程调用,消费后台加载线程写入的 pendingResult + // 检查 generation 确保只处理最新请求的结果,忽略已过期的加载任务 + // 加载成功后从 LitematicaSchematic 提取 SchematicPreviewModel 并提交给渲染器 public void update() { LoadResult result = this.pendingResult; if (result == null || result.generation() != this.generation.get()) { @@ -83,7 +85,6 @@ public void update() { this.pendingResult = null; if (result.throwable() != null) { - ListMore.LOGGER.error("Schematic preview: load result failed for {}", this.file, result.throwable()); this.loadFailure = result.throwable(); return; } @@ -93,18 +94,15 @@ public void update() { } try { + // 加载成功 -> 提取模型并提交给渲染器 this.model = SchematicPreviewModel.from(result.schematic()); - ListMore.LOGGER.info("Schematic preview: model ready for {} (size={}x{}x{}, nonAirBlocks={})", this.file, - this.model.size().getX(), this.model.size().getY(), this.model.size().getZ(), this.model.blocks().size()); this.renderer.setSchematic(result.schematic()); this.renderer.setModel(this.model); } catch (Throwable throwable) { - ListMore.LOGGER.error("Schematic preview: model creation failed for {}", this.file, throwable); this.loadFailure = throwable; } } - /** 关闭浏览器时取消仍在等待的任务,并使所有回调失效。 */ public void close() { this.closed = true; this.generation.incrementAndGet(); diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewTransform.java b/src/main/java/com/listmore/schematic/SchematicPreviewTransform.java index 902715c..77178b4 100644 --- a/src/main/java/com/listmore/schematic/SchematicPreviewTransform.java +++ b/src/main/java/com/listmore/schematic/SchematicPreviewTransform.java @@ -1,6 +1,6 @@ package com.listmore.schematic; -/** Camera state for the fixed preview directions. */ +// 固定预览方向的相机状态 public final class SchematicPreviewTransform { private static final float DEFAULT_YAW = 45.0F; private static final float DEFAULT_PITCH = -25.0F; @@ -22,6 +22,8 @@ public float pitch() { return this.pitch; } + // 根据模型包围盒尺寸计算相机距离,半径取包围盒对角线的一半,乘以 2.4 倍再除以缩放因子 + // 最小距离 4.0 防止模型过小时相机穿透 public float distance(float sizeX, float sizeY, float sizeZ) { float radius = (float) Math.sqrt(sizeX * sizeX + sizeY * sizeY + sizeZ * sizeZ) * 0.5F; return Math.max(4.0F, radius * 2.4F) / this.zoom; @@ -41,21 +43,25 @@ public void zoomOut() { this.zoom = Math.max(MIN_ZOOM, this.zoom / ZOOM_STEP); } + // 将 UI 方向按钮映射为相机旋转,每次旋转 45° + // UP/DOWN 改变俯仰角,LEFT/RIGHT 改变偏航角,RESET 恢复默认视角 public void applyDirection(SchematicPreviewDirection direction) { - if (direction == SchematicPreviewDirection.CENTER) { + if (direction == SchematicPreviewDirection.RESET) { this.reset(); return; } switch (direction) { - case NORTH -> this.rotate(0.0F, -ROTATION_STEP); - case WEST -> this.rotate(ROTATION_STEP, 0.0F); - case EAST -> this.rotate(-ROTATION_STEP, 0.0F); - case SOUTH -> this.rotate(0.0F, ROTATION_STEP); - case CENTER -> { } + case UP -> this.rotate(0.0F, -ROTATION_STEP); + case LEFT -> this.rotate(ROTATION_STEP, 0.0F); + case RIGHT -> this.rotate(-ROTATION_STEP, 0.0F); + case DOWN -> this.rotate(0.0F, ROTATION_STEP); + case RESET -> { } } } + //TODO:话说如果未来要换成鼠标拖曳的话还是四元数好点 + // 偏航角累加后归一化到 [0, 360),俯仰角限制在 [-89°, 89°] 防止万向节锁 private void rotate(float yawDelta, float pitchDelta) { this.yaw = (this.yaw + yawDelta) % 360.0F; if (this.yaw < 0.0F) { diff --git a/src/main/java/com/listmore/schematic/SchematicPreviewWorld.java b/src/main/java/com/listmore/schematic/SchematicPreviewWorld.java new file mode 100644 index 0000000..a27461d --- /dev/null +++ b/src/main/java/com/listmore/schematic/SchematicPreviewWorld.java @@ -0,0 +1,90 @@ +package com.listmore.schematic; + +import fi.dy.masa.litematica.world.FakeLightingProvider; + +//#if MC >= 26.1 +//$$ import net.minecraft.client.renderer.block.BlockAndTintGetter; +//#else +import net.minecraft.world.level.BlockAndTintGetter; +//#endif +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +//#if MC >= 26.1 +//$$ import net.minecraft.world.level.CardinalLighting; +//#else +import net.minecraft.core.Direction; +//#endif +import net.minecraft.world.level.ColorResolver; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.material.FluidState; +import org.jetbrains.annotations.Nullable; + +// 构建预览方块网格时使用的只读世界视图 +final class SchematicPreviewWorld implements BlockAndTintGetter { + private final Minecraft minecraft; + private final LevelLightEngine lightEngine; + private SchematicPreviewModel model; + + SchematicPreviewWorld(Minecraft minecraft) { + this.minecraft = minecraft; + this.lightEngine = new FakeLightingProvider(minecraft.level.getChunkSource()); + } + + public void setModel(SchematicPreviewModel model) { + this.model = model; + } + + @Override + public BlockState getBlockState(BlockPos pos) { + if (this.model == null) { + return Blocks.AIR.defaultBlockState(); + } + return this.model.blockStateAt(pos.getX(), pos.getY(), pos.getZ()); + } + + @Override + public FluidState getFluidState(BlockPos pos) { + return this.getBlockState(pos).getFluidState(); + } + + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { + return null; + } + + @Override + public int getHeight() { + return this.model != null ? Math.max(1, this.model.size().getY()) : 1; + } + + @Override + public int getMinY() { + return 0; + } + + @Override + public LevelLightEngine getLightEngine() { + return this.lightEngine; + } + + //#if MC >= 26.1 + //$$ @Override + //$$ public CardinalLighting cardinalLighting() { + //$$ return CardinalLighting.DEFAULT; + //$$ } + //#else + @Override + public float getShade(Direction direction, boolean shaded) { + return this.minecraft.level.getShade(direction, shaded); + } + //#endif + + @Override + public int getBlockTint(BlockPos pos, ColorResolver resolver) { + return resolver.getColor(this.minecraft.level.getBiome(pos).value(), pos.getX(), pos.getZ()); + } +} diff --git a/src/main/resources/assets/listmore/lang/en_us.json b/src/main/resources/assets/listmore/lang/en_us.json index 83dbbae..c865974 100644 --- a/src/main/resources/assets/listmore/lang/en_us.json +++ b/src/main/resources/assets/listmore/lang/en_us.json @@ -4,6 +4,7 @@ "listmore.hotkeys.category.generic": "General Hotkeys", "listmore.gui.tab.all": "All", "listmore.gui.tab.generic": "General", + "listmore.gui.tab.litematica": "Litematica", "listmore.config.generic.name.projectileLandingPrediction": "Projectile Landing Prediction", "listmore.config.generic.prettyName.projectileLandingPrediction": "Projectile Landing Prediction", "listmore.config.generic.comment.projectileLandingPrediction": "Show the predicted landing point for supported projectiles", diff --git a/src/main/resources/assets/listmore/lang/zh_cn.json b/src/main/resources/assets/listmore/lang/zh_cn.json index 0ed2ef2..7331499 100644 --- a/src/main/resources/assets/listmore/lang/zh_cn.json +++ b/src/main/resources/assets/listmore/lang/zh_cn.json @@ -4,6 +4,7 @@ "listmore.hotkeys.category.generic": "通用快捷键", "listmore.gui.tab.all": "全部", "listmore.gui.tab.generic": "通用", + "listmore.gui.tab.litematica": "Litematica", "listmore.config.generic.name.projectileLandingPrediction": "投射物落点预测", "listmore.config.generic.prettyName.projectileLandingPrediction": "投射物落点预测", "listmore.config.generic.comment.projectileLandingPrediction": "显示支持投射物的预测落点", diff --git a/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewRenderer26.java b/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewRenderer26.java deleted file mode 100644 index 5f2fe93..0000000 --- a/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewRenderer26.java +++ /dev/null @@ -1,446 +0,0 @@ -package com.listmore.schematic; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalDouble; - -import com.listmore.ListMore; -import com.mojang.blaze3d.IndexType; -import com.mojang.blaze3d.PrimitiveTopology; -import com.mojang.blaze3d.GpuFormat; -import com.mojang.blaze3d.ProjectionType; -import com.mojang.blaze3d.buffers.GpuBuffer; -import com.mojang.blaze3d.buffers.GpuBufferSlice; -import com.mojang.blaze3d.buffers.Std140Builder; -import com.mojang.blaze3d.pipeline.TextureTarget; -import com.mojang.blaze3d.platform.Lighting; -import com.mojang.blaze3d.systems.RenderPass; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.textures.AddressMode; -import com.mojang.blaze3d.textures.FilterMode; -import com.mojang.blaze3d.textures.GpuSampler; -import com.mojang.blaze3d.textures.GpuTextureView; -import com.mojang.blaze3d.vertex.BufferBuilder; -import com.mojang.blaze3d.vertex.ByteBufferBuilder; -import com.mojang.blaze3d.vertex.MeshData; -import fi.dy.masa.litematica.render.schematic.BlockModelRendererSchematic; -import fi.dy.masa.litematica.render.schematic.IBlockOutputSchematic; -import fi.dy.masa.litematica.schematic.LitematicaSchematic; -import fi.dy.masa.malilib.render.GuiContext; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.render.TextureSetup; -import net.minecraft.client.renderer.block.FluidRenderer; -import net.minecraft.client.renderer.DynamicUniforms; -import net.minecraft.client.renderer.GlobalSettingsUniform; -import net.minecraft.client.renderer.Projection; -import net.minecraft.client.renderer.ProjectionMatrixBuffer; -import net.minecraft.client.renderer.RenderPipelines; -import net.minecraft.client.renderer.chunk.ChunkSectionLayer; -import net.minecraft.client.renderer.chunk.ChunkSectionLayerGroup; -import net.minecraft.client.renderer.state.level.CameraRenderState; -import net.minecraft.client.renderer.state.gui.BlitRenderState; -import net.minecraft.client.renderer.texture.TextureAtlas; -import net.minecraft.client.resources.model.ModelManager; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.util.Mth; -import net.minecraft.world.level.block.RenderShape; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.Vec3; -import org.joml.Matrix4f; -import org.joml.Matrix4fc; -import org.joml.Matrix4fStack; -import org.joml.Matrix3x2f; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.lwjgl.system.MemoryStack; - -/** 26.x 鐨勭鏈?framebuffer 鏂瑰潡棰勮鍚庣銆?*/ -public final class SchematicPreviewRenderer26 implements SchematicPreviewRenderBackend { - private final List meshes = new ArrayList<>(); - private long builtRevision = Long.MIN_VALUE; - private LitematicaSchematic schematic; - private SchematicPreviewWorld26 world; - private TextureTarget target; - private GpuSampler sampler; - private ProjectionMatrixBuffer projection; - private final Projection previewProjection = new Projection(); - private GpuBuffer globalUniform; - private long loggedRenderRevision = Long.MIN_VALUE; - private long loggedDrawRevision = Long.MIN_VALUE; - private long loggedFrustumRevision = Long.MIN_VALUE; - private final FluidRenderer fluidRenderer = new FluidRenderer(Minecraft.getInstance().getModelManager().getFluidStateModelSet()); - private final CameraRenderState cameraRenderState = new CameraRenderState(); - - @Override - public void setSchematic(Object schematic) { - this.schematic = schematic instanceof LitematicaSchematic loaded ? loaded : null; - } - - public boolean render(Object rawContext, SchematicPreviewLayout layout, SchematicPreviewTransform transform, - SchematicPreviewModel model, long revision) { - GuiContext context = (GuiContext) rawContext; - if (model == null || model.blocks().isEmpty() || layout.contentWidth() <= 0 || layout.contentHeight() <= 0) { - return false; - } - this.ensureTarget(layout.contentWidth(), layout.contentHeight()); - if (this.builtRevision != revision) { - try { - this.rebuild(model, revision); - } catch (Throwable throwable) { - ListMore.LOGGER.error("Schematic preview: mesh rebuild failed (revision={})", revision, throwable); - return false; - } - } - if (this.meshes.isEmpty()) { - return false; - } - - RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures( - this.target.getColorTexture(), new org.joml.Vector4f(0.105F, 0.125F, 0.157F, 1.0F), - this.target.getDepthTexture(), 0.0D); - try { - this.renderTarget(model, transform); - } catch (Throwable throwable) { - ListMore.LOGGER.error("Schematic preview: framebuffer render failed (revision={})", revision, throwable); - return false; - } - context.addSimpleElement(new BlitRenderState( - RenderPipelines.GUI_TEXTURED, - TextureSetup.singleTexture(this.target.getColorTextureView(), this.sampler), - new Matrix3x2f(context.pose()), - layout.contentX(), layout.contentY(), - layout.contentX() + layout.contentWidth(), layout.contentY() + layout.contentHeight(), - 0.0F, 1.0F, 1.0F, 0.0F, 0xFFFFFFFF, context.scissorStack.peek())); - return true; - } - - private void ensureTarget(int width, int height) { - int scaledWidth = Math.max(1, Math.round(width * Minecraft.getInstance().getWindow().getGuiScale())); - int scaledHeight = Math.max(1, Math.round(height * Minecraft.getInstance().getWindow().getGuiScale())); - if (this.target == null) { - this.target = new TextureTarget("ListMore schematic preview", scaledWidth, scaledHeight, true, GpuFormat.RGBA8_UNORM); - } else if (this.target.width != scaledWidth || this.target.height != scaledHeight) { - this.target.resize(scaledWidth, scaledHeight); - } - if (this.sampler == null) { - this.sampler = RenderSystem.getDevice().createSampler(AddressMode.CLAMP_TO_EDGE, AddressMode.CLAMP_TO_EDGE, - FilterMode.LINEAR, FilterMode.LINEAR, 1, OptionalDouble.empty()); - } - } - - private void rebuild(SchematicPreviewModel model, long revision) { - this.closeMeshes(); - this.builtRevision = revision; - this.loggedRenderRevision = Long.MIN_VALUE; - this.loggedDrawRevision = Long.MIN_VALUE; - this.loggedFrustumRevision = Long.MIN_VALUE; - if (this.schematic == null) { - return; - } - if (this.world == null) { - this.world = new SchematicPreviewWorld26(Minecraft.getInstance()); - } - this.world.setSchematic(this.schematic); - SchematicPreviewWorld26 view = this.world; - ModelManager modelManager = Minecraft.getInstance().getModelManager(); - Map chunks = new HashMap<>(); - int modelBlocks = 0; - int fluidBlocks = 0; - BlockModelRendererSchematic renderer = new BlockModelRendererSchematic(); - renderer.enableCache(); - try { - for (SchematicPreviewModel.Block block : model.blocks()) { - BlockState state = block.state(); - BlockPos position = new BlockPos(block.x(), block.y(), block.z()); - ChunkMeshBuilder chunk = chunks.computeIfAbsent(new ChunkPos(block.x() >> 4, block.z() >> 4), ignored -> new ChunkMeshBuilder()); - if (!state.getFluidState().isEmpty()) { - fluidBlocks++; - this.fluidRenderer.tesselate(view, position, chunk::builder, state, state.getFluidState()); - } - if (state.getRenderShape() != RenderShape.MODEL) { - continue; - } - modelBlocks++; - IBlockOutputSchematic output = (x, y, z, quad, instance) -> chunk.builder(quad.materialInfo().layer()) - .putBlockBakedQuad(x, y, z, quad, instance); - renderer.tessellateBlock(view, state, position, new Vec3(block.x() & 15, block.y(), block.z() & 15), - modelManager.getBlockStateModelSet().get(state), state.getSeed(position), output); - } - } finally { - renderer.disableCache(); - } - chunks.forEach((position, chunk) -> { - ChunkMesh built = chunk.build(position); - if (built != null) { - this.meshes.add(built); - } - }); - int layers = this.meshes.stream().mapToInt(mesh -> mesh.buffers().size()).sum(); - int indices = this.meshes.stream().flatMap(mesh -> mesh.buffers().values().stream()).mapToInt(SectionBuffers::indexCount).sum(); - ListMore.LOGGER.info("Schematic preview: mesh rebuild complete (revision={}, sourceBlocks={}, modelBlocks={}, fluidBlocks={}, chunks={}, layers={}, indices={})", - revision, model.blocks().size(), modelBlocks, fluidBlocks, this.meshes.size(), layers, indices); - } - - private void renderTarget(SchematicPreviewModel model, SchematicPreviewTransform transform) { - float distance = transform.distance(model.size().getX(), model.size().getY(), model.size().getZ()); - float yaw = transform.yaw() * Mth.DEG_TO_RAD; - float pitch = transform.pitch() * Mth.DEG_TO_RAD; - float horizontal = Mth.cos(pitch); - Vector3f camera = new Vector3f( - model.centerX() - Mth.sin(yaw) * horizontal * distance, - model.centerY() - Mth.sin(pitch) * distance, - model.centerZ() + Mth.cos(yaw) * horizontal * distance); - if (this.loggedRenderRevision != this.builtRevision) { - this.loggedRenderRevision = this.builtRevision; - ListMore.LOGGER.info("Schematic preview: rendering revision={} (framebuffer={}x{}, camera={}, yaw={}, pitch={}, distance={})", - this.builtRevision, this.target.width, this.target.height, camera, transform.yaw(), transform.pitch(), distance); - } - Matrix4fStack modelView = RenderSystem.getModelViewStack(); - modelView.pushMatrix(); - // Match SchematicPreview's orbit convention: UI yaw describes the viewing - // direction, while the terrain view matrix uses the inverse camera yaw. - Quaternionf rotation = new Quaternionf().rotationYXZ(-yaw, pitch, 0.0F).conjugate(); - modelView.set(new Matrix4f().rotation(rotation)); - this.previewProjection.setupPerspective(0.05F, 4096.0F, 70.0F, - this.target.width, this.target.height); - Matrix4f projectionMatrix = this.previewProjection.getMatrix(new Matrix4f()); - this.logFrustum(model, camera, modelView, projectionMatrix); - this.cameraRenderState.initialized = true; - this.cameraRenderState.orientation = rotation; - this.cameraRenderState.pos = new Vec3(camera.x, camera.y, camera.z); - this.cameraRenderState.blockPos = BlockPos.containing(this.cameraRenderState.pos); - this.cameraRenderState.xRot = transform.pitch(); - this.cameraRenderState.yRot = -transform.yaw(); - RenderSystem.backupProjectionMatrix(); - if (this.projection == null) { - this.projection = new ProjectionMatrixBuffer("ListMore schematic preview"); - } - RenderSystem.setProjectionMatrix(this.projection.getBuffer(projectionMatrix), ProjectionType.PERSPECTIVE); - GpuBuffer previousGlobalUniform = RenderSystem.getGlobalSettingsUniform(); - this.writeGlobalUniform(camera); - try { - Minecraft.getInstance().gameRenderer.lighting().setupFor(Lighting.Entry.LEVEL); - this.renderMeshes(camera); - } finally { - RenderSystem.setGlobalSettingsUniform(previousGlobalUniform); - RenderSystem.restoreProjectionMatrix(); - modelView.popMatrix(); - } - } - - private void writeGlobalUniform(Vector3f camera) { - if (this.globalUniform == null) { - this.globalUniform = RenderSystem.getDevice().createBuffer(() -> "ListMore preview global settings", - GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_COPY_DST, GlobalSettingsUniform.UBO_SIZE); - } - int floorX = Mth.floor(camera.x); - int floorY = Mth.floor(camera.y); - int floorZ = Mth.floor(camera.z); - try (MemoryStack stack = MemoryStack.stackPush()) { - RenderSystem.getDevice().createCommandEncoder().writeToBuffer(this.globalUniform.slice(), - Std140Builder.onStack(stack, GlobalSettingsUniform.UBO_SIZE) - .putIVec3(floorX, floorY, floorZ) - .putVec3(floorX - camera.x, floorY - camera.y, floorZ - camera.z) - .putVec2(Minecraft.getInstance().getWindow().getWidth(), Minecraft.getInstance().getWindow().getHeight()) - .putFloat(1.0F).putFloat(0.0F).putInt(0).putInt(0).get()); - } - RenderSystem.setGlobalSettingsUniform(this.globalUniform); - } - - /** Logs the terrain shader's world-to-clip transform once for each preview. */ - private void logFrustum(SchematicPreviewModel model, Vector3f camera, Matrix4fc modelView, - Matrix4fc projectionMatrix) { - if (this.loggedFrustumRevision == this.builtRevision) { - return; - } - this.loggedFrustumRevision = this.builtRevision; - Matrix4f transform = new Matrix4f(projectionMatrix).mul(modelView); - int visibleCorners = 0; - float minX = Float.POSITIVE_INFINITY; - float minY = Float.POSITIVE_INFINITY; - float maxX = Float.NEGATIVE_INFINITY; - float maxY = Float.NEGATIVE_INFINITY; - for (int x : new int[] { 0, model.size().getX() }) { - for (int y : new int[] { 0, model.size().getY() }) { - for (int z : new int[] { 0, model.size().getZ() }) { - org.joml.Vector4f clip = transform.transform(new org.joml.Vector4f( - x - camera.x, y - camera.y, z - camera.z, 1.0F)); - if (clip.w > 0.0F) { - float ndcX = clip.x / clip.w; - float ndcY = clip.y / clip.w; - minX = Math.min(minX, ndcX); - minY = Math.min(minY, ndcY); - maxX = Math.max(maxX, ndcX); - maxY = Math.max(maxY, ndcY); - if (Math.abs(ndcX) <= 1.0F && Math.abs(ndcY) <= 1.0F) { - visibleCorners++; - } - } - } - } - } - ListMore.LOGGER.info("Schematic preview: CPU frustum revision={} visibleCorners={}/8 ndcX=[{}, {}] ndcY=[{}, {}]", - this.builtRevision, visibleCorners, minX, maxX, minY, maxY); - } - - private void renderMeshes(Vector3f camera) { - var atlas = Minecraft.getInstance().getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(); - List orderedMeshes = new ArrayList<>(this.meshes); - orderedMeshes.sort(Comparator.comparingDouble(mesh -> -mesh.distanceTo(camera))); - DynamicUniforms.ChunkSectionInfo[] infos = new DynamicUniforms.ChunkSectionInfo[orderedMeshes.size()]; - for (int i = 0; i < orderedMeshes.size(); i++) { - ChunkMesh mesh = orderedMeshes.get(i); - infos[i] = new DynamicUniforms.ChunkSectionInfo(RenderSystem.getModelViewMatrixCopy(), mesh.chunk().getMinBlockX(), 0, - mesh.chunk().getMinBlockZ(), 1.0F, atlas.getWidth(0), atlas.getHeight(0)); - } - GpuBufferSlice[] sectionUniforms = RenderSystem.getDynamicUniforms().writeChunkSections(infos); - RenderSystem.AutoStorageIndexBuffer sequential = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS); - int maxSequentialIndices = 0; - for (ChunkMesh chunk : orderedMeshes) { - for (SectionBuffers mesh : chunk.buffers().values()) { - if (mesh.indexBuffer() == null) { - maxSequentialIndices = Math.max(maxSequentialIndices, mesh.indexCount()); - } - } - } - GpuBuffer sharedIndex = maxSequentialIndices == 0 ? null : sequential.getBuffer(maxSequentialIndices); - IndexType sharedIndexType = maxSequentialIndices == 0 ? null : sequential.type(); - this.renderLayerGroup(orderedMeshes, sectionUniforms, atlas, sharedIndex, sharedIndexType, - ChunkSectionLayerGroup.OPAQUE, false); - this.renderLayerGroup(orderedMeshes, sectionUniforms, atlas, sharedIndex, sharedIndexType, - ChunkSectionLayerGroup.TRANSLUCENT, true); - } - - private void renderLayerGroup(List orderedMeshes, GpuBufferSlice[] sectionUniforms, - GpuTextureView atlas, GpuBuffer sharedIndex, IndexType sharedIndexType, - ChunkSectionLayerGroup group, boolean reverse) { - int drawCount = 0; - try (RenderPass pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass( - () -> "ListMore schematic preview " + group.label(), this.target.getColorTextureView(), Optional.empty(), - this.target.getDepthTextureView(), OptionalDouble.empty())) { - RenderSystem.bindDefaultUniforms(pass); - pass.bindTexture("Sampler2", Minecraft.getInstance().gameRenderer.lightmap(), - RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR)); - for (ChunkSectionLayer layer : group.layers()) { - pass.setPipeline(layer.pipeline()); - pass.bindTexture("Sampler0", atlas, this.sampler); - List> draws = new ArrayList<>(); - for (int i = 0; i < orderedMeshes.size(); i++) { - SectionBuffers mesh = orderedMeshes.get(i).buffers().get(layer); - if (mesh == null || mesh.indexCount() <= 0 || mesh.vertexBuffer().isClosed()) continue; - int uniformIndex = i; - draws.add(new RenderPass.Draw<>(0, mesh.vertexBuffer(), mesh.indexBuffer(), - mesh.indexBuffer() == null ? null : mesh.indexType(), 0, mesh.indexCount(), 0, - (slices, uploader) -> uploader.upload("ChunkSection", slices[uniformIndex]))); - } - if (!draws.isEmpty()) { - if (reverse) draws = draws.reversed(); - drawCount += draws.size(); - if (this.loggedDrawRevision != this.builtRevision) { - int layerIndices = draws.stream().mapToInt(RenderPass.Draw::indexCount).sum(); - ListMore.LOGGER.info("Schematic preview: layer {} has {} draws and {} indices (group={})", - layer, draws.size(), layerIndices, group.label()); - } - pass.drawMultipleIndexed(draws, sharedIndex, sharedIndexType, List.of("ChunkSection"), sectionUniforms); - } - } - } - if (this.loggedDrawRevision != this.builtRevision) { - this.loggedDrawRevision = this.builtRevision; - ListMore.LOGGER.info("Schematic preview: submitted {} draws for {} (sections={}, sharedIndex={})", - drawCount, group.label(), sectionUniforms.length, sharedIndex != null); - } - } - - private void closeMeshes() { - this.meshes.forEach(ChunkMesh::close); - this.meshes.clear(); - } - - /** 姣忔蹇収鍙樺寲鏃跺缓绔嬩竴娆?GPU 缂撳啿锛屽抚闂寸洿鎺ュ鐢ㄣ€?*/ - private record SectionBuffers(GpuBuffer vertexBuffer, GpuBuffer indexBuffer, int indexCount, - IndexType indexType) implements AutoCloseable { - @Override - public void close() { - this.vertexBuffer.close(); - if (this.indexBuffer != null) { - this.indexBuffer.close(); - } - } - } - - private static final class ChunkMeshBuilder implements AutoCloseable { - private final Map allocators = new EnumMap<>(ChunkSectionLayer.class); - private final Map builders = new EnumMap<>(ChunkSectionLayer.class); - - private BufferBuilder builder(ChunkSectionLayer layer) { - return this.builders.computeIfAbsent(layer, ignored -> new BufferBuilder( - this.allocators.computeIfAbsent(layer, unused -> new ByteBufferBuilder(4096)), - layer.pipeline().getPrimitiveTopology(), layer.vertexFormat())); - } - - private ChunkMesh build(ChunkPos position) { - Map buffers = new EnumMap<>(ChunkSectionLayer.class); - List meshData = new ArrayList<>(); - this.builders.forEach((layer, builder) -> { - MeshData mesh = builder.build(); - if (mesh == null) { - return; - } - meshData.add(mesh); - GpuBuffer vertex = RenderSystem.getDevice().createBuffer(() -> "ListMore preview vertex buffer", - GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, mesh.vertexBuffer()); - GpuBuffer index = mesh.indexBuffer() == null ? null : RenderSystem.getDevice().createBuffer( - () -> "ListMore preview index buffer", GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, - mesh.indexBuffer()); - buffers.put(layer, new SectionBuffers(vertex, index, mesh.drawState().indexCount(), mesh.drawState().indexType())); - }); - if (buffers.isEmpty()) { - meshData.forEach(MeshData::close); - this.close(); - return null; - } - return new ChunkMesh(position, buffers, meshData, new ArrayList<>(this.allocators.values())); - } - - @Override - public void close() { - this.allocators.values().forEach(ByteBufferBuilder::close); - this.allocators.clear(); - } - } - - private record ChunkMesh(ChunkPos chunk, Map buffers, - List meshData, List allocators) implements AutoCloseable { - private double distanceTo(Vector3f camera) { - float x = this.chunk.getMinBlockX() + 8.0F; - float z = this.chunk.getMinBlockZ() + 8.0F; - return Mth.square(x - camera.x) + Mth.square(z - camera.z); - } - - @Override - public void close() { - this.allocators.forEach(ByteBufferBuilder::close); - this.meshData.forEach(MeshData::close); - this.buffers.values().forEach(SectionBuffers::close); - } - } - - @Override - public void close() { - this.closeMeshes(); - this.builtRevision = Long.MIN_VALUE; - this.schematic = null; - this.world = null; - if (this.target != null) { this.target.destroyBuffers(); this.target = null; } - if (this.sampler != null) { this.sampler.close(); this.sampler = null; } - if (this.projection != null) { this.projection.close(); this.projection = null; } - if (this.globalUniform != null) { this.globalUniform.close(); this.globalUniform = null; } - } -} diff --git a/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewWorld26.java b/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewWorld26.java deleted file mode 100644 index 06988fd..0000000 --- a/versions/26.2-fabric/src/main/java/com/listmore/schematic/SchematicPreviewWorld26.java +++ /dev/null @@ -1,556 +0,0 @@ -package com.listmore.schematic; - -import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableMap; -import com.listmore.ListMore; -import fi.dy.masa.litematica.schematic.LitematicaSchematic; -import fi.dy.masa.litematica.schematic.container.LitematicaBlockStateContainer; -import net.minecraft.client.ClientClockManager; -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.client.renderer.block.BlockAndTintGetter; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Holder; -import net.minecraft.core.HolderLookup; -import net.minecraft.core.SectionPos; -import net.minecraft.core.Vec3i; -import net.minecraft.core.particles.ExplosionParticleInfo; -import net.minecraft.core.particles.ParticleOptions; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.resources.Identifier; -import net.minecraft.sounds.SoundEvent; -import net.minecraft.sounds.SoundSource; -import net.minecraft.util.ProblemReporter; -import net.minecraft.util.random.WeightedList; -import net.minecraft.world.*; -import net.minecraft.world.attribute.EnvironmentAttributeSystem; -import net.minecraft.world.clock.ClockManager; -import net.minecraft.world.damagesource.DamageSource; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.boss.enderdragon.EnderDragonPart; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.flag.FeatureFlagSet; -import net.minecraft.world.flag.FeatureFlags; -import net.minecraft.world.item.alchemy.PotionBrewing; -import net.minecraft.world.item.crafting.RecipeAccess; -import net.minecraft.world.level.*; -import net.minecraft.world.level.biome.Biome; -import net.minecraft.world.level.biome.Biomes; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.entity.FuelValues; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.border.WorldBorder; -import net.minecraft.world.level.chunk.*; -import net.minecraft.world.level.chunk.status.ChunkStatus; -import net.minecraft.world.level.dimension.BuiltinDimensionTypes; -import net.minecraft.world.level.entity.LevelEntityGetter; -import net.minecraft.world.level.gameevent.GameEvent; -import net.minecraft.world.level.lighting.LayerLightEventListener; -import net.minecraft.world.level.lighting.LevelLightEngine; -import net.minecraft.world.level.material.Fluid; -import net.minecraft.world.level.material.FluidState; -import net.minecraft.world.level.saveddata.maps.MapId; -import net.minecraft.world.level.saveddata.maps.MapItemSavedData; -import net.minecraft.world.level.storage.LevelData; -import net.minecraft.world.level.storage.TagValueInput; -import net.minecraft.world.phys.Vec3; -import net.minecraft.world.scores.Scoreboard; -import net.minecraft.world.ticks.BlackholeTickAccess; -import net.minecraft.world.ticks.LevelTickAccess; -import net.minecraft.world.ticks.TickContainerAccess; -import org.jetbrains.annotations.Nullable; -import org.slf4j.LoggerFactory; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BooleanSupplier; -import java.util.function.Supplier; - -final class SchematicPreviewWorld26 extends Level implements LightChunkGetter, BlockAndTintGetter { - - private final LevelLightEngine lightingProvider = new FakeLightingProvider(this); - private final FakeChunkManager fakeChunkManager = new FakeChunkManager(); - private final WorldBorder worldBorder = new WorldBorder(); - private final PalettedContainerFactory palettesFactory; - private final Map areaPoses = new HashMap<>(); - private final Biome biome; - private LitematicaSchematic schematic; - private Vec3i areasOrigin = new Vec3i(0, 0, 0); - private Vec3i size = new Vec3i(0, 0, 0); - private BlockState[] blocksData; - private Map> blockEntities; - - SchematicPreviewWorld26(Minecraft mc) { - super( - new ClientLevel.ClientLevelData(Difficulty.PEACEFUL, false, true), - Level.OVERWORLD, - mc.level.registryAccess(), - mc.level.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(BuiltinDimensionTypes.OVERWORLD), - true, - false, - 0L, - 0 - ); - palettesFactory = mc.level.palettedContainerFactory(); - biome = registryAccess().lookupOrThrow(Registries.BIOME).getValue(Biomes.PLAINS); - } - - public void setSchematic(LitematicaSchematic schematic) { - this.schematic = schematic; - areaPoses.clear(); - schematic.getAreas().forEach((region, box) -> areaPoses.put(region, BlockPos.min(box.getPos1(), box.getPos2()))); - BlockPos.MutableBlockPos areasOrigin = new BlockPos.MutableBlockPos(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); - BlockPos.MutableBlockPos areasEnd = new BlockPos.MutableBlockPos(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); - areaPoses.forEach((region, areaPos) -> { - areasOrigin.set( - Math.min(areasOrigin.getX(), areaPos.getX()), - Math.min(areasOrigin.getY(), areaPos.getY()), - Math.min(areasOrigin.getZ(), areaPos.getZ()) - ); - Vec3i areaSize = schematic.getSubRegionContainer(region).getSize(); - areasEnd.set( - Math.max(areasEnd.getX(), areaPos.getX() + areaSize.getX()), - Math.max(areasEnd.getY(), areaPos.getY() + areaSize.getY()), - Math.max(areasEnd.getZ(), areaPos.getZ() + areaSize.getZ()) - ); - }); - this.areasOrigin = areasOrigin; - size = areasEnd.subtract(areasOrigin); - ListMore.LOGGER.info("Schematic preview: virtual world initialized (regions={}, origin={}, size={}x{}x{})", - areaPoses.size(), this.areasOrigin, size.getX(), size.getY(), size.getZ()); - blocksData = new BlockState[size.getX() * size.getY() * size.getZ()]; - ImmutableMap.Builder> blockEntitiesBuilder = ImmutableMap.builder(); - areaPoses.forEach((region, areaPos) -> { - BlockPos shift = areaPos.subtract(areasOrigin); - schematic.getBlockEntityMapForRegion(region).forEach((relPos, nbtCompound) -> { - BlockPos pos = relPos.offset(shift); - blockEntitiesBuilder.put(pos, Suppliers.memoize(() -> { - BlockEntity blockEntity = silentCreateTileFromNbt(pos, getBlockState(pos), nbtCompound, registryAccess()); - if (blockEntity != null) { - blockEntity.setLevel(this); - } - return blockEntity; - })); - }); - }); - blockEntities = blockEntitiesBuilder.buildKeepingLast(); - } - - @Nullable - private static BlockEntity silentCreateTileFromNbt(BlockPos pos, BlockState state, CompoundTag nbt, HolderLookup.Provider registries) { - String string = nbt.getStringOr("id", ""); - Identifier identifier = Identifier.tryParse(string); - if (identifier == null) { - return null; - } else { - return BuiltInRegistries.BLOCK_ENTITY_TYPE.getOptional(identifier).map((type) -> { - try { - return type.create(pos, state); - } catch (Exception e) { - return null; - } - }).map((blockEntity) -> { - try (ProblemReporter.ScopedCollector logging = new ProblemReporter.ScopedCollector(blockEntity.problemPath(), LoggerFactory.getLogger("ListMore schematic preview"))) { - blockEntity.loadWithComponents(TagValueInput.create(logging, registries, nbt)); - return blockEntity; - } catch (Exception e) { - return null; - } - }).orElse(null); - } - } - - public Vec3i getSize() { - return size; - } - - @Override - public LevelLightEngine getLightEngine() { - return lightingProvider; - } - - @Override - public CardinalLighting cardinalLighting() { - return CardinalLighting.DEFAULT; - } - - @Override - public int getBlockTint(BlockPos pos, ColorResolver colorResolver) { - return colorResolver.getColor(biome, pos.getX(), pos.getZ()); - } - - @Override - public Holder getUncachedNoiseBiome(int biomeX, int biomeY, int biomeZ) { - return this.registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS); - } - - @Override - public int getSeaLevel() { - return 0; - } - - @Nullable - @Override - public BlockEntity getBlockEntity(BlockPos pos) { - Supplier supplier = blockEntities.get(pos); - return supplier != null ? supplier.get() : null; - } - - @Override - public void setRespawnData(LevelData.RespawnData spawnPoint) { - - } - - @Override - public LevelData.RespawnData getRespawnData() { - return null; - } - - @Nullable - @Override - public Entity getEntity(int id) { - return null; - } - - @Override - public Collection dragonParts() { - return List.of(); - } - - @Override - public TickRateManager tickRateManager() { - return null; - } - - @Nullable - @Override - public MapItemSavedData getMapData(MapId id) { - return null; - } - - @Override - public void destroyBlockProgress(int entityId, BlockPos pos, int progress) { - - } - - @Override - public Scoreboard getScoreboard() { - return null; - } - - @Override - public RecipeAccess recipeAccess() { - return null; - } - - @Override - protected LevelEntityGetter getEntities() { - return null; - } - - @Override - public ClockManager clockManager() { - return new ClientClockManager(); - } - - @Override - public EnvironmentAttributeSystem environmentAttributes() { - return EnvironmentAttributeSystem.builder().addDefaultLayers(this).build(); - } - - @Override - public PotionBrewing potionBrewing() { - return null; - } - - @Override - public FuelValues fuelValues() { - return null; - } - - public Map> getBlockEntities() { - return blockEntities; - } - - @Override - public BlockState getBlockState(BlockPos pos) { - if (blocksData == null || isOutOfSize(pos.getX(), pos.getY(), pos.getZ(), size)) { - return Blocks.AIR.defaultBlockState(); - } - int index = getBlocksDataIndex(pos); - BlockState state = blocksData[index]; - if (state == null) { - for (Map.Entry areaEntry : areaPoses.entrySet()) { - LitematicaBlockStateContainer container = schematic.getSubRegionContainer(areaEntry.getKey()); - int x = pos.getX() - areaEntry.getValue().getX() + areasOrigin.getX(); - int y = pos.getY() - areaEntry.getValue().getY() + areasOrigin.getY(); - int z = pos.getZ() - areaEntry.getValue().getZ() + areasOrigin.getZ(); - if (!isOutOfSize(x, y, z, container.getSize())) { - return blocksData[index] = container.get(x, y, z); - } - } - } - return state != null ? state : Blocks.AIR.defaultBlockState(); - } - - private static boolean isOutOfSize(int x, int y, int z, Vec3i size) { - return x < 0 || x >= size.getX() || y < 0 || y >= size.getY() || z < 0 || z >= size.getZ(); - } - - private int getBlocksDataIndex(BlockPos pos) { - return pos.getY() * size.getX() * size.getZ() + pos.getZ() * size.getX() + pos.getX(); - } - - @Override - public FluidState getFluidState(BlockPos pos) { - return getBlockState(pos).getFluidState(); - } - - @Override - public void playSeededSound(@Nullable Entity source, double x, double y, double z, Holder sound, SoundSource category, float volume, float pitch, long seed) { - - } - - @Override - public void playSeededSound(@Nullable Entity source, Entity entity, Holder sound, SoundSource category, float volume, float pitch, long seed) { - - } - - @Override - public void explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, ExplosionInteraction explosionSourceType, ParticleOptions smallParticle, ParticleOptions largeParticle, WeightedList blockParticles, Holder soundEvent) { - - } - - @Override - public String gatherChunkSourceStats() { - return getClass().getSimpleName(); - } - - @Override - public int getHeight() { - return 384; - } - - @Override - public int getMinY() { - return 0; - } - - @Override - public FeatureFlagSet enabledFeatures() { - return FeatureFlags.DEFAULT_FLAGS; - } - - @Override - public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) {} - - @Override - public @org.jspecify.annotations.Nullable LightChunk getChunkForLighting(int i, int j) { - return null; - } - - @Override - public BlockGetter getLevel() { - return this; - } - - @Override - public LevelTickAccess getBlockTicks() { - return BlackholeTickAccess.emptyLevelList(); - } - - @Override - public LevelTickAccess getFluidTicks() { - return BlackholeTickAccess.emptyLevelList(); - } - - @Override - public ChunkSource getChunkSource() { - return fakeChunkManager; - } - - @Override - public void levelEvent(@Nullable Entity source, int eventId, BlockPos pos, int data) { - - } - - @Override - public void gameEvent(Holder event, Vec3 emitterPos, GameEvent.Context emitter) {} - - @Override - public List players() { - return List.of(); - } - - @Override - public WorldBorder getWorldBorder() { - return worldBorder; - } - - private static class FakeLightingProvider extends LevelLightEngine { - - private final LayerLightEventListener FULL_BRIGHT_VIEW = new LayerLightEventListener() { - @Nullable - @Override - public DataLayer getDataLayerData(SectionPos pos) { - return null; - } - @Override - public int getLightValue(BlockPos pos) { - return 15; - } - @Override - public void checkBlock(BlockPos pos) {} - @Override - public boolean hasLightWork() { - return false; - } - @Override - public int runLightUpdates() { - return 15; - } - @Override - public void updateSectionStatus(SectionPos pos, boolean notReady) {} - @Override - public void setLightEnabled(ChunkPos pos, boolean retainData) {} - @Override - public void propagateLightSources(ChunkPos chunkPos) {} - }; - - public FakeLightingProvider(LightChunkGetter chunkProvider) { - super(chunkProvider, false, false); - } - - @Override - public LayerLightEventListener getLayerListener(LightLayer lightType) { - return FULL_BRIGHT_VIEW; - } - - } - - private class FakeChunkManager extends ChunkSource { - - private final Map fakeChunks = new HashMap<>(); - - @Nullable - @Override - public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) { - ChunkPos chunkPos = new ChunkPos(x, z); - return fakeChunks.computeIfAbsent(chunkPos, FakeChunk::new); - } - - @Override - public void tick(BooleanSupplier shouldKeepTicking, boolean tickChunks) { - - } - - @Override - public String gatherStats() { - return "FakeChunkManager"; - } - - @Override - public int getLoadedChunksCount() { - return 0; - } - - @Override - public LevelLightEngine getLightEngine() { - return lightingProvider; - } - - @Override - public BlockGetter getLevel() { - return SchematicPreviewWorld26.this; - } - - } - - private class FakeChunk extends ChunkAccess { - - private final BlockPos chunkOrigin; - - public FakeChunk(ChunkPos pos) { - super(pos, new UpgradeData(new CompoundTag(), SchematicPreviewWorld26.this), SchematicPreviewWorld26.this, palettesFactory, 0, null, null); - chunkOrigin = pos.getWorldPosition(); - } - - @Nullable - @Override - public BlockState setBlockState(BlockPos pos, BlockState state, int flags) { - return null; - } - - @Override - public void setBlockEntity(BlockEntity blockEntity) { - - } - - @Override - public void addEntity(Entity entity) { - - } - - @Override - public ChunkStatus getPersistedStatus() { - return ChunkStatus.FULL; - } - - @Override - public void removeBlockEntity(BlockPos pos) { - - } - - @Nullable - @Override - public CompoundTag getBlockEntityNbtForSaving(BlockPos pos, HolderLookup.Provider registries) { - BlockEntity blockEntity = getBlockEntity(pos); - if (blockEntity != null) { - CompoundTag nbtCompound = blockEntity.saveWithFullMetadata(registries); - nbtCompound.putBoolean("keepPacked", false); - return nbtCompound; - } - return null; - } - - @Override - public TickContainerAccess getBlockTicks() { - return BlackholeTickAccess.emptyContainer(); - } - - @Override - public TickContainerAccess getFluidTicks() { - return BlackholeTickAccess.emptyContainer(); - } - - @Override - public PackedTicks getTicksForSerialization(long time) { - return new PackedTicks(List.of(), List.of()); - } - - @Nullable - @Override - public BlockEntity getBlockEntity(BlockPos pos) { - return SchematicPreviewWorld26.this.getBlockEntity(chunkOrigin.offset(pos)); - } - - @Override - public BlockState getBlockState(BlockPos pos) { - return SchematicPreviewWorld26.this.getBlockState(chunkOrigin.offset(pos)); - } - - @Override - public FluidState getFluidState(BlockPos pos) { - return SchematicPreviewWorld26.this.getFluidState(chunkOrigin.offset(pos)); - } - - } - -} - From 0fb38848d79a4a3ab99d7f2bcac45e2318a09dcd Mon Sep 17 00:00:00 2001 From: Fenad2 Date: Fri, 7 Aug 2026 15:41:42 +0800 Subject: [PATCH 3/7] Update readme.md --- README.md | 35 ++++++++++++++++------------------- docs/new.jpg | Bin 0 -> 125695 bytes gradle.properties | 2 +- 3 files changed, 17 insertions(+), 20 deletions(-) create mode 100644 docs/new.jpg diff --git a/README.md b/README.md index 224a87f..0f0d2ce 100644 --- a/README.md +++ b/README.md @@ -3,58 +3,55 @@ ## 功能列表 -**复制准星目标 ID** +- **复制准星目标 ID** 复制准星所指的方块或实体 ID -**实体高亮边框** +- **实体高亮边框** 为名单中的实体类型添加原版发光描边效果 -**实体高亮边框-选择列表** +- **实体高亮边框-选择列表** 每行一个实体 ID -**实体高亮边框-颜色** +- **实体高亮边框-颜色** 设置名单中实体的描边颜色 -**实体渲染黑名单开关** +- **实体渲染黑名单开关** 跳过名单中实体的客户端渲染 -**实体渲染黑名单-列表** +- **实体渲染黑名单-列表** 每行一个实体 ID -**实体渲染黑名单-范围** +- **实体渲染黑名单-范围** 黑名单实体在该三维范围内会重新渲染,0 为始终不渲染 -**掏炉灰助手-开关** +- **掏炉灰助手-开关** 高亮输入槽含有无法烧制物品的普通熔炉 -**掏炉灰助手-范围设置** +- **掏炉灰助手-范围设置** 以玩家所在区块为中心,区块为范围的扫描半径 -**TNT爆炸预览** +- **TNT爆炸预览** 以TNT当前实时位置预览方块破坏范围,红色为保守范围,黄色为额外可能范围 -**TNT爆炸预览-显示模式** +- **TNT爆炸预览-显示模式** 选择是否显示可能被炸坏的方块 -**玩家追踪线** +- **投射物落点预测** -连线到附近玩家 - -**玩家追踪线颜色** - -设置追踪线的颜色 +显示支持投射物的预测落点 -**投射物落点预测** +- **投影实时预览** -显示支持投射物的预测落点 +在投影文件选择界面新增原理图预览窗口,实时显示当前选中的原理图 +![](docs/new.jpg) diff --git a/docs/new.jpg b/docs/new.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6eac46618f1bd4e1cc2863fd99626d17e0758a4b GIT binary patch literal 125695 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 z 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 nweeFao;ZdEB#N?&atXMnF5RkRzCj^=6oz3 literal 0 HcmV?d00001 diff --git a/gradle.properties b/gradle.properties index 903e110..4b60c12 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G org.gradle.configuration-cache=false -mod_version=1.0.1 +mod_version=1.0.5 archives_base_name=ListMore-Fabric maven_group=com.listmore loom_version=1.17-SNAPSHOT From 1b439909775ee0844f4460b4c8d6b656cbc4dbc9 Mon Sep 17 00:00:00 2001 From: Fenad2 Date: Fri, 7 Aug 2026 15:53:39 +0800 Subject: [PATCH 4/7] fix "litematica_version" --- versions/26.1-fabric/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/26.1-fabric/gradle.properties b/versions/26.1-fabric/gradle.properties index f01b2bf..14086a8 100644 --- a/versions/26.1-fabric/gradle.properties +++ b/versions/26.1-fabric/gradle.properties @@ -7,4 +7,4 @@ fabric_api_version=0.148.2+26.1.2 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-26.1.2:0.28.4 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=20.0.0-beta.4 -litematica_version=fi.dy.masa.litematica:litematica-fabric-26.1.2:0.27.4 +litematica_version=maven.modrinth:litematica:0.27.4 From c97215e8ab1b14c4e16a98932d3ae3ab11f7947e Mon Sep 17 00:00:00 2001 From: Fenad2 Date: Fri, 7 Aug 2026 16:03:07 +0800 Subject: [PATCH 5/7] fix "litematica_version" --- versions/1.21.8-fabric/gradle.properties | 2 +- versions/26.1-fabric/gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/versions/1.21.8-fabric/gradle.properties b/versions/1.21.8-fabric/gradle.properties index 3d628cb..aba8428 100644 --- a/versions/1.21.8-fabric/gradle.properties +++ b/versions/1.21.8-fabric/gradle.properties @@ -12,4 +12,4 @@ litematica_version=com.github.sakura-ryoko:litematica:1.21.8-0.23.5 # https://github.com/sakura-ryoko/malilib/releases # https://masa.dy.fi/maven/fi/dy/masa/malilib/ -# https://maven.terraformersmc.com/releases/com/terraformersmc/modmenu/ +# https://maven.terraformersmc.com/releases/com/terraformersmc/modmenu/ \ No newline at end of file diff --git a/versions/26.1-fabric/gradle.properties b/versions/26.1-fabric/gradle.properties index 14086a8..5c64672 100644 --- a/versions/26.1-fabric/gradle.properties +++ b/versions/26.1-fabric/gradle.properties @@ -7,4 +7,4 @@ fabric_api_version=0.148.2+26.1.2 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-26.1.2:0.28.4 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=20.0.0-beta.4 -litematica_version=maven.modrinth:litematica:0.27.4 +litematica_version=com.github.sakura-ryoko:litematica:26.1.2-0.27.4 From 2e2abe97231f7ee7cde2714db999715c802f7009 Mon Sep 17 00:00:00 2001 From: Fenad2 Date: Fri, 7 Aug 2026 16:08:52 +0800 Subject: [PATCH 6/7] fix "litematica_version" --- versions/1.21.11-fabric/gradle.properties | 2 +- versions/26.2-fabric/gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/versions/1.21.11-fabric/gradle.properties b/versions/1.21.11-fabric/gradle.properties index 28a2f47..c71e664 100644 --- a/versions/1.21.11-fabric/gradle.properties +++ b/versions/1.21.11-fabric/gradle.properties @@ -7,4 +7,4 @@ fabric_api_version=0.141.4+1.21.11 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-1.21.11:0.27.10 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=17.0.0-beta.2 -litematica_version=fi.dy.masa.litematica:litematica-fabric-1.21.11:0.26.12 +litematica_version=com.github.sakura-ryoko:litematica:1.21.11-0.26.12 diff --git a/versions/26.2-fabric/gradle.properties b/versions/26.2-fabric/gradle.properties index f4a2e8a..c829632 100644 --- a/versions/26.2-fabric/gradle.properties +++ b/versions/26.2-fabric/gradle.properties @@ -7,4 +7,4 @@ fabric_api_version=0.154.2+26.2 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-26.2:0.29.0 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=20.0.0-beta.4 -litematica_version=fi.dy.masa.litematica:litematica-fabric-26.2:0.28.0 +litematica_version=com.github.sakura-ryoko:litematica:26.2-0.28.0 From ba6fcca427935800f75e9c0f89712240b1344451 Mon Sep 17 00:00:00 2001 From: Fenad2 Date: Fri, 7 Aug 2026 16:15:51 +0800 Subject: [PATCH 7/7] fix "litematica_version" --- versions/1.21.10-fabric/gradle.properties | 2 +- versions/1.21.11-fabric/gradle.properties | 2 +- versions/1.21.8-fabric/gradle.properties | 2 +- versions/26.1-fabric/gradle.properties | 2 +- versions/26.2-fabric/gradle.properties | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/versions/1.21.10-fabric/gradle.properties b/versions/1.21.10-fabric/gradle.properties index 7130583..5524ded 100644 --- a/versions/1.21.10-fabric/gradle.properties +++ b/versions/1.21.10-fabric/gradle.properties @@ -7,4 +7,4 @@ fabric_api_version=0.138.4+1.21.10 dependencies.api.malilib_version=com.github.sakura-ryoko:malilib:1.21.10-0.26.6 dependencies.api.malilib_version.use_sakura_fork=true dependencies.api.modmenu_version=17.0.0 -litematica_version=com.github.sakura-ryoko:litematica:1.21.10-0.24.5 +litematica_version=fi.dy.masa.litematica:litematica-fabric-1.21.10:0.24.5 diff --git a/versions/1.21.11-fabric/gradle.properties b/versions/1.21.11-fabric/gradle.properties index c71e664..28a2f47 100644 --- a/versions/1.21.11-fabric/gradle.properties +++ b/versions/1.21.11-fabric/gradle.properties @@ -7,4 +7,4 @@ fabric_api_version=0.141.4+1.21.11 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-1.21.11:0.27.10 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=17.0.0-beta.2 -litematica_version=com.github.sakura-ryoko:litematica:1.21.11-0.26.12 +litematica_version=fi.dy.masa.litematica:litematica-fabric-1.21.11:0.26.12 diff --git a/versions/1.21.8-fabric/gradle.properties b/versions/1.21.8-fabric/gradle.properties index aba8428..cf9a9bc 100644 --- a/versions/1.21.8-fabric/gradle.properties +++ b/versions/1.21.8-fabric/gradle.properties @@ -7,7 +7,7 @@ fabric_api_version=0.136.1+1.21.8 dependencies.api.malilib_version=com.github.sakura-ryoko:malilib:1.21.8-0.25.7 dependencies.api.malilib_version.use_sakura_fork=true dependencies.api.modmenu_version=15.0.0 -litematica_version=com.github.sakura-ryoko:litematica:1.21.8-0.23.5 +litematica_version=fi.dy.masa.litematica:litematica-fabric-1.21.8:0.23.5 # https://github.com/sakura-ryoko/malilib/releases diff --git a/versions/26.1-fabric/gradle.properties b/versions/26.1-fabric/gradle.properties index 5c64672..f01b2bf 100644 --- a/versions/26.1-fabric/gradle.properties +++ b/versions/26.1-fabric/gradle.properties @@ -7,4 +7,4 @@ fabric_api_version=0.148.2+26.1.2 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-26.1.2:0.28.4 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=20.0.0-beta.4 -litematica_version=com.github.sakura-ryoko:litematica:26.1.2-0.27.4 +litematica_version=fi.dy.masa.litematica:litematica-fabric-26.1.2:0.27.4 diff --git a/versions/26.2-fabric/gradle.properties b/versions/26.2-fabric/gradle.properties index c829632..f4a2e8a 100644 --- a/versions/26.2-fabric/gradle.properties +++ b/versions/26.2-fabric/gradle.properties @@ -7,4 +7,4 @@ fabric_api_version=0.154.2+26.2 dependencies.api.malilib_version=fi.dy.masa.malilib:malilib-fabric-26.2:0.29.0 dependencies.api.malilib_version.use_sakura_fork=false dependencies.api.modmenu_version=20.0.0-beta.4 -litematica_version=com.github.sakura-ryoko:litematica:26.2-0.28.0 +litematica_version=fi.dy.masa.litematica:litematica-fabric-26.2:0.28.0