diff --git a/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/structure/StructureProjectionManager.java b/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/structure/StructureProjectionManager.java index 43e002a..744f29a 100644 --- a/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/structure/StructureProjectionManager.java +++ b/src/main/java/dev/anvilcraft/resource/ageratum/client/feat/structure/StructureProjectionManager.java @@ -57,7 +57,7 @@ public static boolean showProjection(StructureTemplate template, BlockPos origin } SandboxRenderLevel previewLevel = StructureSandboxFactory.create(clientLevel, template, BlockPos.ZERO); - if (previewLevel == null || !previewLevel.hasFilledBlocks()) { + if (previewLevel == null || !previewLevel.hasContent()) { return false; } @@ -78,7 +78,7 @@ public static boolean showFloatingProjection(StructureTemplate template, BlockPo } SandboxRenderLevel previewLevel = StructureSandboxFactory.create(clientLevel, template, BlockPos.ZERO); - if (previewLevel == null || !previewLevel.hasFilledBlocks()) { + if (previewLevel == null || !previewLevel.hasContent()) { return false; } diff --git a/src/main/java/dev/anvilcraft/resource/ageratum/client/util/level/SandboxRenderLevel.java b/src/main/java/dev/anvilcraft/resource/ageratum/client/util/level/SandboxRenderLevel.java index de6dd53..8c8a168 100644 --- a/src/main/java/dev/anvilcraft/resource/ageratum/client/util/level/SandboxRenderLevel.java +++ b/src/main/java/dev/anvilcraft/resource/ageratum/client/util/level/SandboxRenderLevel.java @@ -17,6 +17,7 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; import net.minecraft.util.profiling.InactiveProfiler; import net.minecraft.world.Difficulty; import net.minecraft.world.TickRateManager; @@ -71,6 +72,7 @@ Entity.class, new LevelCallbackCallbacks() private final ChunkSource chunkSource = new SandboxChunkProvider(this); private final Holder biome; private final RegistryAccess registryAccess; + private final FeatureFlagSet enabledFeatures; private final LongSet filledBlocks = new LongOpenHashSet(); private final LongSet litSections = new LongOpenHashSet(); private final DataLayer defaultDataLayer; @@ -86,10 +88,15 @@ public SandboxRenderLevel() { } public SandboxRenderLevel(RegistryAccess registryAccess) { - this(createLevelData(), registryAccess); + this(registryAccess, FeatureFlags.DEFAULT_FLAGS); } - private SandboxRenderLevel(ClientLevel.ClientLevelData levelData, RegistryAccess registryAccess) { + /** 使用当前世界启用的特性创建沙盒,保证实体类型与预览一致。 */ + public SandboxRenderLevel(RegistryAccess registryAccess, FeatureFlagSet enabledFeatures) { + this(createLevelData(), registryAccess, enabledFeatures); + } + + private SandboxRenderLevel(ClientLevel.ClientLevelData levelData, RegistryAccess registryAccess, FeatureFlagSet enabledFeatures) { super( levelData, LEVEL_ID, @@ -104,6 +111,7 @@ private SandboxRenderLevel(ClientLevel.ClientLevelData levelData, RegistryAccess ); this.clientLevelData = levelData; this.registryAccess = registryAccess; + this.enabledFeatures = enabledFeatures; this.biome = registryAccess.registryOrThrow(Registries.BIOME).getHolderOrThrow(Biomes.PLAINS); var nibbles = new byte[DataLayer.SIZE]; @@ -199,6 +207,11 @@ public void tickFrameClock() { partialTick = tracker.getGameTimeDeltaPartialTick(false); } + /** 判断预览是否包含方块或实体,供仅有实体的结构选择投影和边界。 */ + public boolean hasContent() { + return this.hasFilledBlocks() || this.getEntitiesForRendering().iterator().hasNext(); + } + public boolean hasFilledBlocks() { return !this.filledBlocks.isEmpty(); } @@ -220,7 +233,7 @@ public Stream getFilledBlocks() { * 计算包围所有已渲染方块与实体的世界坐标轴对齐包围盒。 */ public Bounds getBounds() { - if (this.filledBlocks.isEmpty()) { + if (!this.hasContent()) { return new Bounds(BlockPos.ZERO, BlockPos.ZERO); } @@ -242,9 +255,9 @@ public Bounds getBounds() { for (var entity : getEntitiesForRendering()) { var bounds = entity.getBoundingBox(); - min.setX(Math.min(min.getX(), (int) bounds.minX)); - min.setY(Math.min(min.getY(), (int) bounds.minY)); - min.setZ(Math.min(min.getZ(), (int) bounds.minZ)); + min.setX(Math.min(min.getX(), Mth.floor(bounds.minX))); + min.setY(Math.min(min.getY(), Mth.floor(bounds.minY))); + min.setZ(Math.min(min.getZ(), Mth.floor(bounds.minZ))); max.setX(Math.max(max.getX(), (int) Math.ceil(bounds.maxX))); max.setY(Math.max(max.getY(), (int) Math.ceil(bounds.maxY))); @@ -403,7 +416,7 @@ public Holder getUncachedNoiseBiome(int i, int i1, int i2) { @Override public FeatureFlagSet enabledFeatures() { - return FeatureFlags.DEFAULT_FLAGS; + return this.enabledFeatures; } private static class LevelCallbackCallbacks implements LevelCallback { diff --git a/src/main/java/dev/anvilcraft/resource/ageratum/client/util/level/StructureSandboxFactory.java b/src/main/java/dev/anvilcraft/resource/ageratum/client/util/level/StructureSandboxFactory.java index 211fd78..d91977a 100644 --- a/src/main/java/dev/anvilcraft/resource/ageratum/client/util/level/StructureSandboxFactory.java +++ b/src/main/java/dev/anvilcraft/resource/ageratum/client/util/level/StructureSandboxFactory.java @@ -3,10 +3,19 @@ import lombok.extern.slf4j.Slf4j; import net.minecraft.core.BlockPos; import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.FallingBlockEntity; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.levelgen.SingleThreadedRandomSource; import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.phys.Vec3; import javax.annotation.Nullable; @@ -29,15 +38,64 @@ private StructureSandboxFactory() { SingleThreadedRandomSource random = new SingleThreadedRandomSource(0L); StructurePlaceSettings settings = new StructurePlaceSettings(); settings.setIgnoreEntities(true); + // 保留蓝图中的连接状态,避免邻接更新改变预览结构。 + settings.setKnownShape(true); - SandboxRenderLevel level = new SandboxRenderLevel(clientLevel.registryAccess()); + SandboxRenderLevel level = new SandboxRenderLevel(clientLevel.registryAccess(), clientLevel.enabledFeatures()); DelegatingServerLevelAccessor fakeServerLevel = new DelegatingServerLevelAccessor(level); - if (!template.placeInWorld(fakeServerLevel, placementPos, placementPos, settings, random, 0)) { + boolean placed = template.placeInWorld(fakeServerLevel, placementPos, placementPos, settings, random, Block.UPDATE_KNOWN_SHAPE); + // 原版结构实体加载依赖服务端关卡,预览在客户端沙盒中重建实体。 + for (Tag entityTag : template.save(new CompoundTag()).getList("entities", Tag.TAG_COMPOUND)) { + CompoundTag entry = (CompoundTag) entityTag; + CompoundTag entityData = entry.getCompound("nbt").copy(); + ListTag relativePos = entry.getList("pos", Tag.TAG_DOUBLE); + ListTag originalPos = entityData.getList("Pos", Tag.TAG_DOUBLE); + Vec3 offset = new Vec3( + relativePos.getDouble(0) + placementPos.getX() - originalPos.getDouble(0), + relativePos.getDouble(1) + placementPos.getY() - originalPos.getDouble(1), + relativePos.getDouble(2) + placementPos.getZ() - originalPos.getDouble(2) + ); + relocateEntityData(entityData, offset); + Entity entity = EntityType.loadEntityRecursive(entityData, level, loaded -> { + if (loaded instanceof FallingBlockEntity falling) { + falling.setStartPos(falling.blockPosition()); + } + return loaded; + }); + if (entity != null) { + entity.getSelfAndPassengers().forEach(level::addEntity); + } + } + if (!placed && !level.hasContent()) { log.debug("Failed to place structure preview into sandbox at {}", placementPos); } + if (level.getEntitiesForRendering().iterator().hasNext()) { + level.getLightEngine().runLightUpdates(); + } return level; } + static void relocateEntityData(CompoundTag data, Vec3 offset) { + ListTag original = data.getList("Pos", Tag.TAG_DOUBLE); + ListTag position = new ListTag(); + position.add(DoubleTag.valueOf(original.getDouble(0) + offset.x)); + position.add(DoubleTag.valueOf(original.getDouble(1) + offset.y)); + position.add(DoubleTag.valueOf(original.getDouble(2) + offset.z)); + data.put("Pos", position); + data.remove("UUID"); + // 1.21 的悬挂实体用整数坐标保存附着位置。 + if (data.contains("TileX", Tag.TAG_ANY_NUMERIC) + && data.contains("TileY", Tag.TAG_ANY_NUMERIC) + && data.contains("TileZ", Tag.TAG_ANY_NUMERIC)) { + data.putInt("TileX", data.getInt("TileX") + (int) Math.round(offset.x)); + data.putInt("TileY", data.getInt("TileY") + (int) Math.round(offset.y)); + data.putInt("TileZ", data.getInt("TileZ") + (int) Math.round(offset.z)); + } + for (Tag passenger : data.getList("Passengers", Tag.TAG_COMPOUND)) { + relocateEntityData((CompoundTag) passenger, offset); + } + } + /** * 返回适合 GUI 预览的居中放置原点。 */ diff --git a/src/test/java/dev/anvilcraft/resource/ageratum/client/util/level/StructureSandboxFactoryTest.java b/src/test/java/dev/anvilcraft/resource/ageratum/client/util/level/StructureSandboxFactoryTest.java new file mode 100644 index 0000000..69e4161 --- /dev/null +++ b/src/test/java/dev/anvilcraft/resource/ageratum/client/util/level/StructureSandboxFactoryTest.java @@ -0,0 +1,78 @@ +package dev.anvilcraft.resource.ageratum.client.util.level; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.TagParser; +import net.minecraft.world.phys.Vec3; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StructureSandboxFactoryTest { + @Test + void relocatesHangingEntityAndPreservesDisplayedItem() throws Exception { + CompoundTag data = TagParser.parseTag(""" + {id:"minecraft:item_frame",Pos:[83.5d,-47.5d,13.96875d], + TileX:83,TileY:-48,TileZ:13,Facing:2b,UUID:[I;1,2,3,4], + Item:{id:"minecraft:netherite_scrap",count:1}} + """); + CompoundTag item = data.getCompound("Item").copy(); + + StructureSandboxFactory.relocateEntityData(data, new Vec3(-85, 49, -15)); + + assertPosition(data, -1.5, 1.5, -1.03125); + assertEquals(-2, data.getInt("TileX")); + assertEquals(1, data.getInt("TileY")); + assertEquals(-2, data.getInt("TileZ")); + assertEquals(item, data.getCompound("Item")); + assertEquals(2, data.getByte("Facing")); + assertFalse(data.contains("UUID")); + } + + @Test + void relocatesNestedPassengersWithoutReusingEntityIds() throws Exception { + CompoundTag data = TagParser.parseTag(""" + {id:"minecraft:boat",Pos:[100d,60d,200d],UUID:[I;1,2,3,4], + Passengers:[{id:"minecraft:pig",Pos:[100d,60.5d,200d],UUID:[I;5,6,7,8], + Passengers:[{id:"minecraft:chicken",Pos:[100d,61d,200d],UUID:[I;9,10,11,12]}]}]} + """); + + StructureSandboxFactory.relocateEntityData(data, new Vec3(-101, -60, -201)); + + CompoundTag passenger = data.getList("Passengers", Tag.TAG_COMPOUND).getCompound(0); + CompoundTag nested = passenger.getList("Passengers", Tag.TAG_COMPOUND).getCompound(0); + assertPosition(data, -1, 0, -1); + assertPosition(passenger, -1, 0.5, -1); + assertPosition(nested, -1, 1, -1); + assertFalse(data.contains("UUID")); + assertFalse(passenger.contains("UUID")); + assertFalse(nested.contains("UUID")); + } + + @Test + void preservesFallingBlockStateAndDoesNotInventAttachments() throws Exception { + CompoundTag data = TagParser.parseTag(""" + {id:"minecraft:falling_block",Pos:[50.5d,-46.04d,55.5d], + BlockState:{Name:"minecraft:anvil",Properties:{facing:"west"}},Time:1} + """); + CompoundTag state = data.getCompound("BlockState").copy(); + + StructureSandboxFactory.relocateEntityData(data, new Vec3(-49, 50, -55)); + + assertPosition(data, 1.5, 3.96, 0.5); + assertEquals(state, data.getCompound("BlockState")); + assertTrue(data.contains("Time")); + assertFalse(data.contains("TileX")); + assertFalse(data.contains("TileY")); + assertFalse(data.contains("TileZ")); + } + + private static void assertPosition(CompoundTag data, double x, double y, double z) { + var pos = data.getList("Pos", Tag.TAG_DOUBLE); + assertEquals(x, pos.getDouble(0), 1e-7); + assertEquals(y, pos.getDouble(1), 1e-7); + assertEquals(z, pos.getDouble(2), 1e-7); + } +}