Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -71,6 +72,7 @@ Entity.class, new LevelCallbackCallbacks()
private final ChunkSource chunkSource = new SandboxChunkProvider(this);
private final Holder<Biome> 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;
Expand All @@ -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,
Expand All @@ -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];
Expand Down Expand Up @@ -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();
}
Expand All @@ -220,7 +233,7 @@ public Stream<BlockPos> getFilledBlocks() {
* 计算包围所有已渲染方块与实体的世界坐标轴对齐包围盒。
*/
public Bounds getBounds() {
if (this.filledBlocks.isEmpty()) {
if (!this.hasContent()) {
return new Bounds(BlockPos.ZERO, BlockPos.ZERO);
}

Expand All @@ -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)));
Expand Down Expand Up @@ -403,7 +416,7 @@ public Holder<Biome> getUncachedNoiseBiome(int i, int i1, int i2) {

@Override
public FeatureFlagSet enabledFeatures() {
return FeatureFlags.DEFAULT_FLAGS;
return this.enabledFeatures;
}

private static class LevelCallbackCallbacks implements LevelCallback<Entity> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 预览的居中放置原点。
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading