From 2ca45bbd0706924b4924ee723df75544b88a35fd Mon Sep 17 00:00:00 2001 From: Fff Date: Sun, 20 Sep 2026 12:10:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=93=9D=E5=9B=BE?= =?UTF-8?q?=E5=BB=BA=E9=80=A0=E4=B8=8E=E9=A2=84=E8=A7=88=E5=B9=B6=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=B5=81=E4=BD=93=E7=BD=91=E7=BB=9C=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E5=92=8C=E5=B7=A8=E5=9E=8B=E9=93=81=E7=A0=A7=E7=A2=B0=E6=92=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 分离蓝图配置、材料与容器内容,返还材料自带物品,支持缺料确认后放置空容器 - 校验方块实体与实体数据及操作权限,保留机器配置并转换方向和蓝图内部引用 - 修正蓝图自动朝向,优化建筑杖半透明预览及扫描仪预览缩放、居中和边框对齐 - 补充建筑杖放置音效,更新组件不匹配与容器缺料的中英文提示 - 延后流体容器能力检测,避免加载回调中同步查询其他区块 - 略微收缩下落巨型铁砧的碰撞箱 --- .../assets/anvilcraft/lang/en_ud.json | 3 +- .../assets/anvilcraft/lang/en_us.json | 3 +- .../fluid/network/FluidNetworkManager.java | 6 +- .../entity/StructureScannerBlockEntity.java | 6 +- .../building/BlockEntityContentAdapter.java | 67 ++- .../building/BlueprintBlockConfiguration.java | 396 ++++++++++++++++++ .../building/BlueprintPlacement.java | 9 + .../building/BuildingBlockMaterial.java | 139 +++++- .../building/BuildingEntityTransform.java | 30 ++ .../building/BuildingMaterials.java | 89 +++- .../building/BuildingRodService.java | 98 ++++- .../building/SignDecorationAdapter.java | 12 +- .../client/building/BuildingRodClient.java | 6 +- .../building/BuildingRodRenderTypes.java | 18 + .../client/building/BuildingRodRenderer.java | 8 +- .../gui/screen/StructureScannerScreen.java | 175 +++----- .../anvilcraft/client/init/ModShaders.java | 4 + .../client/support/RenderSupport.java | 14 +- .../anvilcraft/data/lang/BuildingRodLang.java | 4 +- .../entity/FallingGiantAnvilEntity.java | 2 +- .../event/ServerBlockEntityEventListener.java | 8 +- .../shaders/core/building_rod_ghost.fsh | 29 ++ .../shaders/core/building_rod_ghost.json | 116 +++++ 23 files changed, 1046 insertions(+), 196 deletions(-) create mode 100644 src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockConfiguration.java create mode 100644 src/main/resources/assets/anvilcraft/shaders/core/building_rod_ghost.fsh create mode 100644 src/main/resources/assets/anvilcraft/shaders/core/building_rod_ghost.json diff --git a/src/generated/resources/assets/anvilcraft/lang/en_ud.json b/src/generated/resources/assets/anvilcraft/lang/en_ud.json index 2c78d05a42..e61e2ed53f 100644 --- a/src/generated/resources/assets/anvilcraft/lang/en_ud.json +++ b/src/generated/resources/assets/anvilcraft/lang/en_ud.json @@ -1326,7 +1326,8 @@ "key.categories.anvilcraft": "ʇɟɐɹƆꞁᴉʌuⱯ", "message.anvilcraft.buffer_boots.charged": "dɯnɾ oʇ %1$s ssǝɹԀ", "message.anvilcraft.building_rod.blocked": "pǝʇɔǝʇoɹd ɹo pǝʇɔnɹʇsqo sᴉ ɐǝɹɐ ʇuǝɯǝɔɐꞁԀ", - "message.anvilcraft.building_rod.component_mismatch": "ʇuǝɯǝɔɐꞁd ɯɹᴉɟuoɔ oʇ spuoɔǝs Ɛ uᴉɥʇᴉʍ uᴉɐᵷɐ ʞɔᴉꞁɔ-ʇɥᵷᴉɹ ⸵sʇuǝuodɯoɔ pǝɥɔʇɐɯsᴉɯ ǝʌɐɥ sʞɔoꞁq ǝɯoS", + "message.anvilcraft.building_rod.component_mismatch": "ɐʇɐp ɹᴉǝɥʇ puɐ sꞁɐᴉɹǝʇɐɯ ǝꞁqɐꞁᴉɐʌɐ ǝɥʇ ǝsn oʇ spuoɔǝs Ɛ uᴉɥʇᴉʍ uᴉɐᵷɐ ʞɔᴉꞁɔ-ʇɥᵷᴉɹ ⸵sʇuǝuodɯoɔ pǝɥɔʇɐɯsᴉɯ ǝʌɐɥ sʞɔoꞁq ǝɯoS", + "message.anvilcraft.building_rod.contents_missing": "ʎʇdɯǝ sɹǝuᴉɐʇuoɔ ǝsoɥʇ ǝɔɐꞁd oʇ spuoɔǝs Ɛ uᴉɥʇᴉʍ uᴉɐᵷɐ ʞɔᴉꞁɔ-ʇɥᵷᴉɹ ⸵sʇuǝʇuoɔ ɹǝuᴉɐʇuoɔ ᵷuᴉɥɔʇɐɯ ᵷuᴉssᴉW", "message.anvilcraft.building_rod.invalid_structure": "9⥝×9⥝×9⥝ uɐɥʇ ɹǝᵷɹɐꞁ ɹo ʇuᴉɹdǝnꞁq pᴉꞁɐʌuI", "message.anvilcraft.building_rod.missing_blocks": "pᴉnꞁɟ ɹo sꞁɐᴉɹǝʇɐɯ ɥᵷnouǝ ʇoN", "message.anvilcraft.building_rod.missing_book": "ʇsᴉꞁ ꞁɐᴉɹǝʇɐɯ ɐ ǝʌᴉǝɔǝɹ oʇ ʞooq ɐ ʎɹɹɐɔ ⸵sꞁɐᴉɹǝʇɐɯ ᵷuᴉssᴉW", diff --git a/src/generated/resources/assets/anvilcraft/lang/en_us.json b/src/generated/resources/assets/anvilcraft/lang/en_us.json index 233146b372..82540587e5 100644 --- a/src/generated/resources/assets/anvilcraft/lang/en_us.json +++ b/src/generated/resources/assets/anvilcraft/lang/en_us.json @@ -1326,7 +1326,8 @@ "key.categories.anvilcraft": "AnvilCraft", "message.anvilcraft.buffer_boots.charged": "Press %1$s to jump", "message.anvilcraft.building_rod.blocked": "Placement area is obstructed or protected", - "message.anvilcraft.building_rod.component_mismatch": "Some blocks have mismatched components; right-click again within 3 seconds to confirm placement", + "message.anvilcraft.building_rod.component_mismatch": "Some blocks have mismatched components; right-click again within 3 seconds to use the available materials and their data", + "message.anvilcraft.building_rod.contents_missing": "Missing matching container contents; right-click again within 3 seconds to place those containers empty", "message.anvilcraft.building_rod.invalid_structure": "Invalid blueprint or larger than 16×16×16", "message.anvilcraft.building_rod.missing_blocks": "Not enough materials or fluid", "message.anvilcraft.building_rod.missing_book": "Missing materials; carry a book to receive a material list", diff --git a/src/main/java/dev/dubhe/anvilcraft/api/fluid/network/FluidNetworkManager.java b/src/main/java/dev/dubhe/anvilcraft/api/fluid/network/FluidNetworkManager.java index d17c55667c..ba49daf85c 100644 --- a/src/main/java/dev/dubhe/anvilcraft/api/fluid/network/FluidNetworkManager.java +++ b/src/main/java/dev/dubhe/anvilcraft/api/fluid/network/FluidNetworkManager.java @@ -71,10 +71,10 @@ public void addContainer(Level level, BlockPos pos) { } /** - * 登记刚加载的方块实体容器,并把网络重建延后一轮。 + * 登记刚加载的方块实体候选位置,并把容器检测与网络重建延后一轮。 * - *

外部模组可能在方块实体首次 tick 时才完成多方块流体处理器初始化; - * 延后一轮可避免缓存到尚未刷新的处理器。

+ *

能力查询可能通过多方块代理同步请求其他区块,因此加载回调中不能检测能力。 + * 重建时会剔除非容器;延后一轮也可等待首次 tick 完成流体处理器初始化。

*/ public void addContainerAfterLoad(Level level, BlockPos pos) { if (level.isClientSide()) { diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/StructureScannerBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/StructureScannerBlockEntity.java index 3750810980..9e9aa374f7 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/StructureScannerBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/StructureScannerBlockEntity.java @@ -168,7 +168,7 @@ public boolean isItemValid(int slot, ItemStack stack) { private String autoSaveStructureName = ""; // 自动保存的结构名称 /** - * 缓存的方块数据;nbt 为方块实体的完整原版结构语义数据(saveWithId),无方块实体时为 null + * 缓存的方块数据;nbt 为方块实体完整数据及扫描时的世界坐标,无方块实体时为 null */ public record CachedBlockData( int x, @@ -363,11 +363,11 @@ private void scanNextLayer() { net.minecraft.world.level.block.state.BlockState blockState = this.level.getBlockState(worldPos); if (!blockState.isAir() && BlueprintMultiblocks.shouldRecord(blockState)) { - // 与原版结构 fillFromWorld 一致,方块实体使用 saveWithId 保留完整数据 + // 保留扫描时的世界坐标,用于粘贴时重定位蓝图内部的引用。 net.minecraft.world.level.block.entity.BlockEntity worldBlockEntity = this.level.getBlockEntity(worldPos); CompoundTag blockEntityNbt = worldBlockEntity != null - ? worldBlockEntity.saveWithId(this.level.registryAccess()) + ? worldBlockEntity.saveWithFullMetadata(this.level.registryAccess()) : null; this.scannedBlocks.add( new CachedBlockData(x, this.currentScanLayer, z, blockState, blockEntityNbt) diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlockEntityContentAdapter.java b/src/main/java/dev/dubhe/anvilcraft/building/BlockEntityContentAdapter.java index af723aaadb..ef730173bf 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BlockEntityContentAdapter.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlockEntityContentAdapter.java @@ -1,17 +1,27 @@ package dev.dubhe.anvilcraft.building; import dev.dubhe.anvilcraft.api.itemhandler.IItemHandlerHolder; +import dev.dubhe.anvilcraft.block.entity.storage.StorageBlockEntity; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.core.NonNullList; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponents; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.world.Container; import net.minecraft.world.ContainerHelper; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.JukeboxBlockEntity; +import net.minecraft.world.level.block.entity.LecternBlockEntity; import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.items.IItemHandlerModifiable; @@ -40,16 +50,49 @@ public static Extracted extract(BlockState state, @Nullable CompoundTag nbt, Hol if (nbt == null || nbt.isEmpty()) { return Extracted.empty(); } + return extract(BlockEntity.loadStatic(BlockPos.ZERO, state, nbt, registries), nbt, registries, null); + } + + static Extracted extract(BlockEntity entity, HolderLookup.Provider registries, @Nullable Level level) { + return extract(entity, entity.saveWithFullMetadata(registries), registries, level); + } + + private static Extracted extract( + @Nullable BlockEntity loaded, CompoundTag nbt, HolderLookup.Provider registries, @Nullable Level level + ) { if (nbt.contains("LootTable", Tag.TAG_STRING) && !nbt.getString("LootTable").isEmpty()) { return new Extracted(stripResources(nbt), List.of(), true); } List contents = new ArrayList<>(); - BlockEntity loaded = BlockEntity.loadStatic(BlockPos.ZERO, state, nbt, registries); + if (loaded instanceof LecternBlockEntity lectern) { + ItemStack book = lectern.getBook(); + if (!book.isEmpty() && !book.is(Items.WRITTEN_BOOK) && !book.is(Items.WRITABLE_BOOK)) { + throw new IllegalArgumentException("Blueprint lectern contains an invalid book"); + } + if (!book.isEmpty()) contents.add(new SlotStack(0, book.copy())); + CompoundTag config = stripResources(nbt); + config.remove("Book"); + config.remove("Page"); + return new Extracted(config, List.copyOf(contents), false); + } + if (loaded instanceof JukeboxBlockEntity jukebox) { + ItemStack record = jukebox.getTheItem(); + if (!record.isEmpty()) contents.add(new SlotStack(0, record.copy())); + CompoundTag config = stripResources(nbt); + config.remove("RecordItem"); + config.remove("ticks_since_song_started"); + return new Extracted(config, List.copyOf(contents), false); + } if (loaded instanceof Container container) { extractContainer(container, contents); + var components = loaded.collectComponents(); + if (components.has(DataComponents.CONTAINER)) { + loaded.applyComponents(DataComponentMap.builder().addAll(components) + .set(DataComponents.CONTAINER, ItemContainerContents.EMPTY).build(), DataComponentPatch.EMPTY); + } return new Extracted(configOf(loaded, registries), List.copyOf(contents), false); } - IItemHandler handler = itemHandlerOf(loaded); + IItemHandler handler = itemHandlerOf(loaded, level); if (handler != null) { extractHandler(handler, contents); CompoundTag config = configOf(loaded, registries); @@ -68,6 +111,10 @@ public static void insert( if (contents.isEmpty()) { return; } + if (blockEntity instanceof LecternBlockEntity lectern) { + lectern.setBook(contents.getFirst().stack().copy()); + return; + } if (blockEntity instanceof Container container) { for (SlotStack content : contents) { if (content.slot() >= 0 && content.slot() < container.getContainerSize()) { @@ -77,7 +124,7 @@ public static void insert( container.setChanged(); return; } - IItemHandler handler = itemHandlerOf(blockEntity); + IItemHandler handler = itemHandlerOf(blockEntity, blockEntity.getLevel()); if (handler instanceof IItemHandlerModifiable modifiable) { for (SlotStack content : contents) { if (content.slot() >= 0 && content.slot() < modifiable.getSlots()) { @@ -125,7 +172,6 @@ private static void extractContainer(Container container, List conten ItemStack stack = container.getItem(slot); if (stack.isEmpty()) continue; contents.add(new SlotStack(slot, stack.copy())); - container.setItem(slot, ItemStack.EMPTY); } } @@ -138,11 +184,16 @@ private static void extractHandler(IItemHandler handler, List content } @Nullable - private static IItemHandler itemHandlerOf(@Nullable BlockEntity blockEntity) { + private static IItemHandler itemHandlerOf(@Nullable BlockEntity blockEntity, @Nullable Level level) { + // 外部仓储通过组件引用全局数据,不能在规划时读取或清空实际仓储。 + if (blockEntity instanceof StorageBlockEntity) return null; if (blockEntity instanceof IItemHandlerHolder holder) { return holder.getItemHandler(); } - return blockEntity instanceof IItemHandler handler ? handler : null; + if (blockEntity instanceof IItemHandler handler) return handler; + if (blockEntity == null || level == null) return null; + return level.getCapability(Capabilities.ItemHandler.BLOCK, blockEntity.getBlockPos(), + blockEntity.getBlockState(), blockEntity, null); } private static CompoundTag configOf(BlockEntity blockEntity, HolderLookup.Provider registries) { @@ -167,6 +218,10 @@ private static void stripSerializedHandlerItems(CompoundTag config) { private static void stripSerializedHandlerItems(Tag tag) { if (tag instanceof CompoundTag compound) { compound.remove("SlotItem"); + compound.remove("Items"); + if (compound.contains("components", Tag.TAG_COMPOUND)) { + compound.getCompound("components").remove("minecraft:container"); + } for (String key : List.copyOf(compound.getAllKeys())) { Tag child = compound.get(key); if (child != null) { diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockConfiguration.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockConfiguration.java new file mode 100644 index 0000000000..fd24006094 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockConfiguration.java @@ -0,0 +1,396 @@ +package dev.dubhe.anvilcraft.building; + +import dev.dubhe.anvilcraft.api.taslatower.TeslaFilter; +import dev.dubhe.anvilcraft.block.entity.ActiveSilencerBlockEntity; +import dev.dubhe.anvilcraft.block.entity.AdvancedComparatorBlockEntity; +import dev.dubhe.anvilcraft.block.entity.AutoEnchantingTableBlockEntity; +import dev.dubhe.anvilcraft.block.entity.BaseChuteBlockEntity; +import dev.dubhe.anvilcraft.block.entity.ChargerBlockEntity; +import dev.dubhe.anvilcraft.block.entity.DischargerBlockEntity; +import dev.dubhe.anvilcraft.block.entity.ExpCollectorBlockEntity; +import dev.dubhe.anvilcraft.block.entity.HeliostatsBlockEntity; +import dev.dubhe.anvilcraft.block.entity.IFilterBlockEntity; +import dev.dubhe.anvilcraft.block.entity.ItemCollectorBlockEntity; +import dev.dubhe.anvilcraft.block.entity.ItemDetectorBlockEntity; +import dev.dubhe.anvilcraft.block.entity.PulseGeneratorBlockEntity; +import dev.dubhe.anvilcraft.block.entity.RedstoneDiceBlockEntity; +import dev.dubhe.anvilcraft.block.entity.SmartBlockPlacerBlockEntity; +import dev.dubhe.anvilcraft.block.entity.StructureScannerBlockEntity; +import dev.dubhe.anvilcraft.block.entity.TeslaTowerBlockEntity; +import dev.dubhe.anvilcraft.block.entity.TradingStationBlockEntity; +import dev.dubhe.anvilcraft.block.entity.batch.BaseBatchCraftingBlockEntity; +import dev.dubhe.anvilcraft.block.entity.batch.BatchCrafterBlockEntity; +import dev.dubhe.anvilcraft.block.entity.fluid.AbstractPipeCheckValveBlockEntity; +import dev.dubhe.anvilcraft.block.entity.fluid.ControlValveBlockEntity; +import dev.dubhe.anvilcraft.init.item.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.CrafterBlockEntity; +import net.minecraft.world.level.block.entity.LecternBlockEntity; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.neoforged.neoforge.fluids.FluidStack; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +/** 蓝图只复制经过字段校验的设置;库存、身份和执行缓存不属于设置。 */ +public final class BlueprintBlockConfiguration { + private BlueprintBlockConfiguration() { + } + + static CompoundTag take(BlockEntity entity, CompoundTag source, HolderLookup.Provider registries) { + CompoundTag result = new CompoundTag(); + if (entity instanceof SignBlockEntity) { + result = SignDecorationAdapter.sanitize(entity.getBlockState(), source, registries); + remove(source, "front_text", "back_text", "is_waxed"); + } + if (entity instanceof IFilterBlockEntity filter && filter.getFilteredItemStackHandler().getSlots() > 0) { + for (String key : List.of("Inventory", "Depository", "Items")) { + if (!source.contains(key, Tag.TAG_COMPOUND)) continue; + result.put(key, filtering(source.getCompound(key), filter.getFilteredItemStackHandler().getSlots(), registries)); + stripFiltering(source.getCompound(key)); + } + } + if ((entity instanceof PulseGeneratorBlockEntity || entity instanceof AdvancedComparatorBlockEntity) + && source.contains("ExtraData", Tag.TAG_COMPOUND)) { + CompoundTag data = source.getCompound("ExtraData"); + CompoundTag settings = new CompoundTag(); + bool(data, settings, "OutputMode"); + if (entity instanceof PulseGeneratorBlockEntity) { + integer(data, settings, "StartMode", 0, 2); + integer(data, settings, "WaitingTime", 0, 24000); + integer(data, settings, "SignalDuration", 0, 24000); + if (settings.getInt("WaitingTime") == 0 && settings.getInt("SignalDuration") == 0) settings.putInt("SignalDuration", 1); + } else { + integer(data, settings, "CompareMode", 0, 1); + integer(data, settings, "HighLimit", 0, 15); + integer(data, settings, "LowLimit", 0, 15); + bool(data, settings, "RedstoneControl"); + } + result.put("ExtraData", settings); + source.remove("ExtraData"); + } + if (entity instanceof BaseChuteBlockEntity) source.remove("Cooldown"); + if (entity instanceof ChargerBlockEntity || entity instanceof DischargerBlockEntity) { + remove(source, "TimeLeft", "TimeTotalCache", "PowerValue", "StartupCoolDown", "FeCharging", "FeDischarging", "FeCooldown"); + } + if (entity instanceof TradingStationBlockEntity) { + result.put("Filters", filterSamples(source.getCompound("Filters"), 3, registries)); + for (String key : List.of("AllowPlayer", "AllowVillager", "AllowInput", "AllowOutput")) bool(source, result, key); + remove(source, "Filters", "Owner"); + } + if (entity instanceof ItemDetectorBlockEntity) { + integer(source, result, "Range", 1, 8); + choice(source, result, "FilterMode", "ANY", "ALL"); + bool(source, result, "OutputInvert"); + result.put("Filter", filterSamples(source.getCompound("Filter"), 9, registries)); + remove(source, "Filter", "OutputSignal"); + } + if (entity instanceof ItemCollectorBlockEntity || entity instanceof ExpCollectorBlockEntity) { + integer(source, result, "Cooldown", 0, 3); + integer(source, result, "RangeRadius", 0, 3); + source.remove("cd"); + } + if (entity instanceof ActiveSilencerBlockEntity) { + var sounds = ActiveSilencerBlockEntity.CODEC.parse(NbtOps.INSTANCE, source.getCompound("MutedSound")) + .result().orElse(List.of()); + result.put("MutedSound", ActiveSilencerBlockEntity.CODEC.encodeStart(NbtOps.INSTANCE, + sounds.stream().distinct().limit(1024).toList()).getOrThrow()); + source.remove("MutedSound"); + } + if (entity instanceof TeslaTowerBlockEntity) tesla(source, result); + if (entity instanceof ControlValveBlockEntity) valve(source, result, registries); + if (entity instanceof AutoEnchantingTableBlockEntity) enchanting(source, result, registries); + if (entity instanceof StructureScannerBlockEntity) { + for (String key : List.of("rangeX", "rangeY", "rangeZ")) integer(source, result, key, 0, 15); + remove(source, "isScanning", "currentScanLayer", "scannedBlocks", "pendingAutoSave", "autoSaveStructureName"); + } + if (entity instanceof BaseBatchCraftingBlockEntity) { + remove(source, "PoweredBefore", "Cooldown", "HasDisplayItemStack", "ResultItemStack"); + } + if (entity instanceof BatchCrafterBlockEntity) integer(source, result, "Selecting", 0, Integer.MAX_VALUE); + if (entity instanceof SmartBlockPlacerBlockEntity) smartPlacer(source, result); + if (entity instanceof RedstoneDiceBlockEntity) { + bool(source, result, "Uniform"); + remove(source, "Rolling", "PreviousFaces", "Faces", "Output", "RollStart"); + } + if (entity instanceof CrafterBlockEntity) { + result.putIntArray("disabled_slots", Arrays.stream(source.getIntArray("disabled_slots")) + .filter(slot -> slot >= 0 && slot < 9).distinct().toArray()); + remove(source, "disabled_slots", "crafting_ticks_remaining", "triggered"); + } + if (entity instanceof LecternBlockEntity) integer(source, result, "Page", 0, Integer.MAX_VALUE); + if (entity instanceof HeliostatsBlockEntity && source.contains("Ix", Tag.TAG_INT)) { + for (String key : List.of("Ix", "Iy", "Iz")) integer(source, result, key, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + if (entity instanceof AbstractPipeCheckValveBlockEntity) { + result.put("Valves", checkValves(source.getList("Valves", Tag.TAG_COMPOUND))); + remove(source, "Valves", "Powered"); + } + return result; + } + + static void strip(BlockEntity entity, CompoundTag tag, HolderLookup.Provider registries) { + take(entity, tag, registries); + if (entity instanceof IFilterBlockEntity filter && filter.getFilteredItemStackHandler().getSlots() > 0) { + remove(tag, "Inventory", "Depository", "Items"); + } + } + + private static CompoundTag filtering(CompoundTag source, int size, HolderLookup.Provider registries) { + CompoundTag result = new CompoundTag(); + result.putBoolean("FilterEnabled", source.getBoolean("FilterEnabled")); + result.putInt("Size", size); + ListTag entries = new ListTag(); + for (int slot = 0; slot < size; slot++) { + CompoundTag entry = new CompoundTag(); + entry.putInt("Slot", slot); + entry.putBoolean("IsEmptySlot", true); + entry.putInt("SlotLimit", 64); + for (Tag value : source.getList("Inventory", Tag.TAG_COMPOUND)) { + CompoundTag saved = (CompoundTag) value; + if (saved.getInt("Slot") != slot) continue; + entry.putBoolean("Disabled", saved.getBoolean("Disabled")); + entry.putInt("SlotLimit", saved.contains("SlotLimit") ? Math.clamp(saved.getInt("SlotLimit"), 1, 64) : 64); + if (saved.getBoolean("SlotFilterEnabled")) { + ItemStack item = ItemStack.parseOptional(registries, saved.getCompound("SlotFilterItem")); + if (!item.isEmpty()) { + entry.putBoolean("SlotFilterEnabled", true); + entry.put("SlotFilterItem", item.copyWithCount(1).save(registries)); + } + } + } + entries.add(entry); + } + result.put("Inventory", entries); + return result; + } + + private static void stripFiltering(CompoundTag source) { + source.remove("FilterEnabled"); + for (Tag value : source.getList("Inventory", Tag.TAG_COMPOUND)) { + CompoundTag entry = (CompoundTag) value; + remove(entry, "SlotFilterEnabled", "SlotFilterItem", "Disabled", "SlotLimit"); + } + } + + private static CompoundTag filterSamples(CompoundTag source, int size, HolderLookup.Provider registries) { + var items = new dev.dubhe.anvilcraft.inventory.container.FilterOnlyContainer(null, size); + for (Tag value : source.getList("Items", Tag.TAG_COMPOUND)) { + CompoundTag tag = (CompoundTag) value; + int slot = tag.getInt("Slot"); + if (slot < 0 || slot >= size) continue; + ItemStack item = ItemStack.parseOptional(registries, tag); + if (!item.isEmpty()) items.setItem(slot, item.copyWithCount(Math.min(item.getCount(), item.getMaxStackSize()))); + } + return items.serializeNBT(registries); + } + + private static void tesla(CompoundTag source, CompoundTag result) { + for (String key : List.copyOf(source.getAllKeys())) { + int delimiter = key.indexOf("_-_"); + if (delimiter < 0) continue; + String id = key.substring(0, delimiter); + if (!TeslaFilter.getFilter(id).getId().isEmpty() && source.contains(key, Tag.TAG_STRING)) { + String argument = source.getString(key); + if (argument.length() > 256) throw new IllegalArgumentException("Tesla filter argument is too long"); + result.putString(key, argument); + } + source.remove(key); + } + remove(source, "LastStrikeTime", "TargetEntityUUID", "TargetLightningRod"); + } + + private static void valve(CompoundTag source, CompoundTag result, HolderLookup.Provider registries) { + integer(source, result, "MaxRate", 0, ControlValveBlockEntity.MAX_RATE); + integer(source, result, "Facing", 0, 5); + ListTag filters = new ListTag(); + for (Tag value : source.getList("Filters", Tag.TAG_COMPOUND)) { + CompoundTag tag = (CompoundTag) value; + int slot = tag.getInt("Slot"); + if (slot < 0 || slot >= ControlValveBlockEntity.FILTER_SLOT_COUNT) continue; + FluidStack fluid = FluidStack.parseOptional(registries, tag.getCompound("Fluid")); + if (fluid.isEmpty()) continue; + CompoundTag entry = new CompoundTag(); + entry.putInt("Slot", slot); + entry.put("Fluid", fluid.copyWithAmount(1).save(registries)); + filters.add(entry); + } + result.put("Filters", filters); + source.remove("Filters"); + } + + private static void enchanting(CompoundTag source, CompoundTag result, HolderLookup.Provider registries) { + choice(source, result, "WorkMode", Arrays.stream(AutoEnchantingTableBlockEntity.WorkMode.values()) + .map(AutoEnchantingTableBlockEntity.WorkMode::getSerializedName).toArray(String[]::new)); + integer(source, result, "LiquidEnchantmentLevel", 0, 255); + ListTag selected = new ListTag(); + Set seen = new HashSet<>(); + for (Tag value : source.getList("SelectedEnchantments", Tag.TAG_STRING)) { + String id = value.getAsString(); + ResourceLocation key = ResourceLocation.tryParse(id); + if (key != null && seen.add(id) && registries.lookupOrThrow(Registries.ENCHANTMENT) + .get(net.minecraft.resources.ResourceKey.create(Registries.ENCHANTMENT, key)).isPresent()) { + selected.add(StringTag.valueOf(id)); + } + } + result.put("SelectedEnchantments", selected); + remove(source, "SelectedEnchantments", "CooldownTicks", "ShelfLevel"); + } + + private static void smartPlacer(CompoundTag source, CompoundTag result) { + choice(source, result, "operation", "pickup", "move"); + choice(source, result, "target", "position", "blueprint"); + choice(source, result, "placement", "skip", "wait"); + integer(source, result, "selectedLayer", 0, SmartBlockPlacerBlockEntity.POSITION_GRID_SIZE - 1); + byte[] stored = source.contains("layerPositions", Tag.TAG_BYTE_ARRAY) + ? source.getByteArray("layerPositions") : source.getByteArray("positionMarks"); + byte[] positions = Arrays.copyOf(stored, SmartBlockPlacerBlockEntity.POSITION_COUNT); + if (source.contains("layerPositions", Tag.TAG_COMPOUND)) { + for (int layer = 0; layer < SmartBlockPlacerBlockEntity.POSITION_GRID_SIZE; layer++) { + for (int position : source.getCompound("layerPositions").getIntArray("layer_" + layer)) { + if (position >= 0 && position < SmartBlockPlacerBlockEntity.POSITIONS_PER_LAYER) { + positions[layer * SmartBlockPlacerBlockEntity.POSITIONS_PER_LAYER + position] = 1; + } + } + } + } + for (int i = 0; i < positions.length; i++) positions[i] = positions[i] == 0 ? (byte) 0 : (byte) 1; + result.putByteArray("layerPositions", positions); + remove(source, "layerPositions", "positionMarks", "currentPlacementIndex", "phase", "progress", "blueprintStates", + "loadedStructureName", "invalidStructure", "missingBlock", "currentHeldBlock"); + } + + private static ListTag checkValves(ListTag source) { + ListTag result = new ListTag(); + Set faces = new HashSet<>(); + for (Tag value : source) { + CompoundTag entry = (CompoundTag) value; + int face = entry.getInt("Face"); + int flow = entry.getInt("Flow"); + if (face < 0 || face > 5 || flow < 0 || flow > 5 || !faces.add(face) + || Direction.from3DDataValue(face).getAxis() != Direction.from3DDataValue(flow).getAxis()) { + throw new IllegalArgumentException("Invalid blueprint check valve direction"); + } + CompoundTag valve = new CompoundTag(); + valve.putInt("Face", face); + valve.putInt("Flow", flow); + result.add(valve); + } + return result; + } + + static List materials(CompoundTag config) { + int count = config.getList("Valves", Tag.TAG_COMPOUND).size(); + return count == 0 ? List.of() : List.of(ModItems.CHECK_VALVE.asStack(count)); + } + + public static void transform( + CompoundTag config, BlueprintPlacement placement, @Nullable BlockPos sourceOrigin, StructureSnapshot snapshot + ) { + if (config.contains("Facing")) { + Direction facing = Direction.from3DDataValue(config.getInt("Facing")); + config.putInt("Facing", placement.rotation().rotate(placement.mirror().mirror(facing)).get3DDataValue()); + } + for (Tag value : config.getList("Valves", Tag.TAG_COMPOUND)) { + CompoundTag tag = (CompoundTag) value; + for (String key : List.of("Face", "Flow")) { + Direction direction = Direction.from3DDataValue(tag.getInt(key)); + tag.putInt(key, placement.rotation().rotate(placement.mirror().mirror(direction)).get3DDataValue()); + } + } + if (config.contains("layerPositions") && placement.mirror() != Mirror.NONE) { + byte[] source = Arrays.copyOf(config.getByteArray("layerPositions"), SmartBlockPlacerBlockEntity.POSITION_COUNT); + byte[] mirrored = source.clone(); + int side = SmartBlockPlacerBlockEntity.POSITION_GRID_SIZE; + for (int i = 0; i < source.length; i++) mirrored[i - i % side + side - 1 - i % side] = source[i]; + config.putByteArray("layerPositions", mirrored); + } + if (!config.contains("Ix")) return; + BlockPos target = new BlockPos(config.getInt("Ix"), config.getInt("Iy"), config.getInt("Iz")); + if (sourceOrigin != null) { + BlockPos local = target.subtract(sourceOrigin); + if (local.getX() >= 0 && local.getY() >= 0 && local.getZ() >= 0 && local.getX() < snapshot.size().getX() + && local.getY() < snapshot.size().getY() && local.getZ() < snapshot.size().getZ()) { + BlockPos world = placement.worldOf(local); + config.putInt("Ix", world.getX()); + config.putInt("Iy", world.getY()); + config.putInt("Iz", world.getZ()); + return; + } + } + remove(config, "Ix", "Iy", "Iz"); + } + + @Nullable + public static BlockPos sourceOrigin(StructureSnapshot snapshot) { + BlockPos origin = null; + for (var entry : snapshot.blocks()) { + CompoundTag tag = entry.nbt().orElse(null); + if (tag == null || !tag.contains("x", Tag.TAG_INT) + || !tag.contains("y", Tag.TAG_INT) || !tag.contains("z", Tag.TAG_INT)) continue; + BlockPos candidate = new BlockPos(tag.getInt("x"), tag.getInt("y"), tag.getInt("z")).subtract(entry.pos()); + if (origin != null && !origin.equals(candidate)) return null; + origin = candidate; + } + return origin; + } + + static void apply(BlockEntity entity, CompoundTag settings, ServerPlayer player) { + if (settings.isEmpty() && !(entity instanceof TradingStationBlockEntity)) return; + CompoundTag tag = entity.saveWithFullMetadata(player.registryAccess()); + tag.merge(settings.copy()); + if (entity instanceof TradingStationBlockEntity) tag.putUUID("Owner", player.getUUID()); + if (entity instanceof PulseGeneratorBlockEntity pulse) pulse.loadBlueprint(tag, player.registryAccess()); + else entity.loadWithComponents(tag, player.registryAccess()); + } + + static void afterContents(BlockEntity entity, CompoundTag settings, ServerPlayer player) { + if (entity instanceof LecternBlockEntity lectern && !lectern.getBook().isEmpty() && settings.contains("Page")) { + apply(entity, settings, player); + } + if (entity instanceof BatchCrafterBlockEntity crafter) crafter.setSelecting(settings.getInt("Selecting")); + if (entity instanceof AutoEnchantingTableBlockEntity enchanting) { + enchanting.setLiquidLevel(settings.getInt("LiquidEnchantmentLevel")); + } + if (entity instanceof SmartBlockPlacerBlockEntity placer) placer.applyDiskData(settings); + } + + private static void integer(CompoundTag source, CompoundTag target, String key, int min, int max) { + if (!source.contains(key, Tag.TAG_ANY_NUMERIC)) return; + target.putInt(key, Math.clamp(source.getInt(key), min, max)); + source.remove(key); + } + + private static void bool(CompoundTag source, CompoundTag target, String key) { + if (!source.contains(key, Tag.TAG_ANY_NUMERIC)) return; + target.putBoolean(key, source.getBoolean(key)); + source.remove(key); + } + + private static void choice(CompoundTag source, CompoundTag target, String key, String... values) { + if (!source.contains(key)) return; + String value = source.getString(key); + target.putString(key, Arrays.asList(values).contains(value) ? value : values[0]); + source.remove(key); + } + + private static void remove(CompoundTag tag, String... keys) { + for (String key : keys) tag.remove(key); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintPlacement.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintPlacement.java index ce49fc508a..d782046f76 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintPlacement.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintPlacement.java @@ -1,6 +1,7 @@ package dev.dubhe.anvilcraft.building; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.Vec3i; import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.Rotation; @@ -14,6 +15,14 @@ * 语义镜像和旋转,再平移到锚点;方块状态使用原版 mirror/rotate,保证与最终提交一致。 */ public record BlueprintPlacement(BlockPos anchor, Rotation rotation, Mirror mirror) { + public static Rotation facingPlayer(Direction source, Direction playerFacing) { + Direction front = source.getAxis().isHorizontal() ? source : Direction.NORTH; + for (Rotation candidate : Rotation.values()) { + if (candidate.rotate(front) == playerFacing.getOpposite()) return candidate; + } + return Rotation.NONE; + } + /** 快照局部方块坐标经镜像与旋转后的局部坐标,不含锚点平移。 */ @SuppressWarnings("deprecation") public BlockPos localOf(BlockPos snapshotLocal) { diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingBlockMaterial.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingBlockMaterial.java index 3ef9bbc09d..3506d213d8 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingBlockMaterial.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingBlockMaterial.java @@ -4,34 +4,157 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.LecternBlockEntity; +import net.minecraft.world.level.block.entity.SignBlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.neoforged.neoforge.fluids.capability.IFluidHandler; +import java.util.List; import javax.annotation.Nullable; -record BuildingBlockMaterial(ItemStack stack, CompoundTag config) { +record BuildingBlockMaterial( + ItemStack stack, CompoundTag config, List contents, boolean requiresOperator +) { @SuppressWarnings("deprecation") - static BuildingBlockMaterial extract(BlockState state, @Nullable CompoundTag nbt, HolderLookup.Provider registries) { + static BuildingBlockMaterial extract(BlockState state, @Nullable CompoundTag nbt, Level level) { + final HolderLookup.Provider registries = level.registryAccess(); ItemStack stack = new ItemStack(state.getBlock().asItem()); - CompoundTag config = nbt == null ? new CompoundTag() : nbt.copy(); - BlockEntity entity = nbt == null ? null : BlockEntity.loadStatic(BlockPos.ZERO, state, nbt, registries); - if (entity == null || stack.isEmpty()) return new BuildingBlockMaterial(stack, config); + BlockEntity entity = createEntity(state); + if (entity == null) return new BuildingBlockMaterial(stack, new CompoundTag(), List.of(), false); + boolean restricted = entity.onlyOpCanSetNbt() + && !(entity instanceof SignBlockEntity) && !(entity instanceof LecternBlockEntity); + if (nbt == null || nbt.isEmpty()) return new BuildingBlockMaterial(stack, new CompoundTag(), List.of(), restricted); + String id = BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(entity.getType()).toString(); + if (nbt.contains("id") && !id.equals(nbt.getString("id"))) { + throw new IllegalArgumentException("Blueprint block entity type does not match its block"); + } + CompoundTag input = nbt.copy(); + if (entity instanceof SignBlockEntity) { + input = SignDecorationAdapter.sanitize(state, input, registries); + } + final CompoundTag settings = BlueprintBlockConfiguration.take(entity, input, registries); + input = entity.saveWithFullMetadata(registries).merge(input); + try { + entity.loadWithComponents(input, registries); + } catch (RuntimeException exception) { + throw new IllegalArgumentException("Invalid blueprint block entity data", exception); + } + BlockEntityContentAdapter.Extracted extracted = BlockEntityContentAdapter.extract(entity, registries, level); + BlockEntity empty = createEntity(state); + if (empty == null) throw new IllegalArgumentException("Missing blueprint block entity"); + empty.loadWithComponents(extracted.config(), registries); + entity = empty; if (entity instanceof IFluidHandlerHolder || entity instanceof IFluidHandler) { - // 流体容器的数据由实际消耗的物品提供,不能再从蓝图恢复或另扣流体。 entity.saveToItem(stack, registries); - return new BuildingBlockMaterial(stack, new CompoundTag()); + var data = stack.get(DataComponents.BLOCK_ENTITY_DATA); + if (data != null) { + CompoundTag clean = data.copyTag(); + BlueprintBlockConfiguration.strip(entity, clean, registries); + BlockEntity defaults = createEntity(state); + if (defaults != null) { + CompoundTag emptyData = defaults.saveWithFullMetadata(registries); + removeDefaults(clean, emptyData); + } + BlockItem.setBlockEntityData(stack, entity.getType(), clean); + } + return new BuildingBlockMaterial(stack, settings, extracted.contents(), restricted); } stack.applyComponents(entity.collectComponents()); if (!stack.getPrototype().has(DataComponents.CONTAINER) && stack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY).equals(ItemContainerContents.EMPTY)) { stack.remove(DataComponents.CONTAINER); } + CompoundTag config = entity.saveWithFullMetadata(registries); entity.removeComponentsFromTag(config); config.remove("components"); - return new BuildingBlockMaterial(stack, config); + BlueprintBlockConfiguration.strip(entity, config, registries); + config.merge(settings); + if (entity instanceof AbstractFurnaceBlockEntity) { + config.remove("RecipesUsed"); + config.remove("BurnTime"); + config.remove("CookTime"); + config.remove("CookTimeTotal"); + } + return new BuildingBlockMaterial(stack, config, extracted.contents(), restricted); + } + + @SuppressWarnings("deprecation") + BuildingBlockMaterial withConfiguration(BlockState state, CompoundTag extracted, HolderLookup.Provider registries) { + BlockEntity entity = createEntity(state); + if (entity == null) return this; + CompoundTag remaining = extracted.copy(); + final CompoundTag safe = BlueprintBlockConfiguration.take(entity, remaining, registries); + BlueprintBlockConfiguration.strip(entity, remaining, registries); + CompoundTag defaults = entity.saveWithFullMetadata(registries); + entity.removeComponentsFromTag(defaults); + for (String key : List.copyOf(remaining.getAllKeys())) { + var value = remaining.get(key); + if (value != null && value.equals(defaults.get(key))) remaining.remove(key); + } + remaining.remove("id"); + remaining.remove("x"); + remaining.remove("y"); + remaining.remove("z"); + ItemStack material = this.stack.copy(); + if (!remaining.isEmpty()) { + // 未列为安全配置的数据必须由实际消耗的物品携带,降级放置不能从蓝图补回。 + BlockItem.setBlockEntityData(material, entity.getType(), remaining); + } + return new BuildingBlockMaterial(material, safe, this.contents, this.requiresOperator); + } + + record Supplied(ItemStack stack, List contents) { + } + + static Supplied separateContents(ItemStack supplied, Level level) { + if (!(supplied.getItem() instanceof BlockItem item)) return new Supplied(supplied.copy(), List.of()); + BlockEntity entity = createEntity(item.getBlock().defaultBlockState()); + if (entity == null) return new Supplied(supplied.copy(), List.of()); + var data = supplied.get(DataComponents.BLOCK_ENTITY_DATA); + if (data != null) entity.loadWithComponents(data.copyTag(), level.registryAccess()); + entity.applyComponentsFromItemStack(supplied); + var extracted = BlockEntityContentAdapter.extract(entity, level.registryAccess(), level); + ItemStack empty = supplied.copy(); + if (empty.getPrototype().has(DataComponents.CONTAINER)) empty.set(DataComponents.CONTAINER, ItemContainerContents.EMPTY); + else empty.remove(DataComponents.CONTAINER); + if (data != null) { + CompoundTag clean = extracted.config().copy(); + BlueprintBlockConfiguration.strip(entity, clean, level.registryAccess()); + BlockEntity defaults = createEntity(item.getBlock().defaultBlockState()); + if (defaults != null) removeDefaults(clean, defaults.saveWithFullMetadata(level.registryAccess())); + CompoundTag original = data.copyTag(); + for (String key : List.copyOf(clean.getAllKeys())) { + if (!original.contains(key)) clean.remove(key); + } + BlockItem.setBlockEntityData(empty, entity.getType(), clean); + } + return new Supplied(empty, extracted.contents()); + } + + private static void removeDefaults(CompoundTag tag, CompoundTag defaults) { + for (String key : List.copyOf(tag.getAllKeys())) { + var value = tag.get(key); + var defaultValue = defaults.get(key); + if (value instanceof CompoundTag compound && defaultValue instanceof CompoundTag defaultCompound) { + removeDefaults(compound, defaultCompound); + if (compound.isEmpty()) tag.remove(key); + } else if (value != null && value.equals(defaultValue)) { + tag.remove(key); + } + } + } + + @Nullable + private static BlockEntity createEntity(BlockState state) { + return state.getBlock() instanceof EntityBlock block ? block.newBlockEntity(BlockPos.ZERO, state) : null; } } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingEntityTransform.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingEntityTransform.java index ef093c8462..04dfbdcd85 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingEntityTransform.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingEntityTransform.java @@ -5,8 +5,19 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.FloatTag; 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.decoration.ArmorStand; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.entity.decoration.Painting; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.Boat; import net.minecraft.world.phys.Vec3; +import java.util.ArrayList; +import java.util.List; + public final class BuildingEntityTransform { private BuildingEntityTransform() { } @@ -39,4 +50,23 @@ public static CompoundTag transform(StructureSnapshot.EntityEntry entry, Bluepri tag.remove("Passengers"); return tag; } + + static CompoundTag sanitize(Entity entity, CompoundTag tag) { + List keys = new ArrayList<>(List.of("id", "Pos", "Rotation", "CustomName", "CustomNameVisible")); + if (entity instanceof Boat) keys.add("Type"); + if (entity instanceof AbstractMinecart) { + keys.addAll(List.of("CustomDisplayTile", "DisplayState", "DisplayOffset")); + if (entity.getType() == EntityType.COMMAND_BLOCK_MINECART) keys.add("Command"); + } + if (entity instanceof ArmorStand) keys.addAll(List.of("Pose", "Small", "ShowArms", "NoBasePlate", "Invisible", "NoGravity")); + if (entity instanceof ItemFrame || entity instanceof Painting) { + keys.addAll(List.of("Facing", "facing", "TileX", "TileY", "TileZ", "ItemRotation", "Invisible", "variant")); + } + CompoundTag safe = new CompoundTag(); + for (String key : keys) { + Tag value = tag.get(key); + if (value != null) safe.put(key, value.copy()); + } + return safe; + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingMaterials.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingMaterials.java index f91ef2c12c..f81f4ab530 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingMaterials.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingMaterials.java @@ -9,6 +9,8 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.fluids.FluidStack; import net.neoforged.neoforge.fluids.capability.IFluidHandler; @@ -45,6 +47,7 @@ private static final class Source { private final ItemStack resource; private final long available; private final IntConsumer extract; + @Nullable private BuildingBlockMaterial.Supplied blockMaterial; private int reserved; private Source(ItemStack resource, long available, IntConsumer extract) { @@ -114,29 +117,71 @@ public boolean reserve(List materials, List fluids) { BuildingRodService.Group reserve(BuildingRodService.Group group, boolean allowMismatch) { final int[] before = this.sources.stream().mapToInt(source -> source.reserved).toArray(); BuildingRodService.Group allocated = new BuildingRodService.Group(); - allocated.cells.addAll(group.cells); - allocated.entities.addAll(group.entities); + if (!group.separateContents) allocated.entities.addAll(group.entities); allocated.fluids.addAll(group.fluids); allocated.materials.addAll(group.materials); for (var entry : group.blockMaterials.entrySet()) { ItemStack expected = entry.getValue(); - List taken = new ArrayList<>(); - int remaining = this.creative ? 0 : this.reserveBlock(expected, true, expected.getCount(), taken); - if (this.creative) taken.add(expected.copy()); - if (remaining > 0 && allowMismatch) remaining = this.reserveBlock(expected, false, remaining, taken); + List taken = new ArrayList<>(); + int remaining = this.creative ? 0 : this.reserveBlock(expected, true, expected.getCount(), taken, group.separateContents); + if (this.creative) taken.add(new BlockSupply(expected.copy(), expected.copy(), List.of())); + if (remaining > 0 && allowMismatch) remaining = this.reserveBlock(expected, false, remaining, taken, group.separateContents); if (remaining > 0) { for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; return null; } - allocated.blockMaterials.put(entry.getKey(), taken.getFirst()); - allocated.materials.addAll(taken); - allocated.componentMismatch |= taken.stream().anyMatch(stack -> !ItemStack.isSameItemSameComponents(stack, expected)); + allocated.blockMaterials.put(entry.getKey(), taken.getFirst().placed()); + taken.forEach(supply -> { + allocated.materials.add(supply.original()); + allocated.returned.addAll(supply.returned()); + }); + allocated.componentMismatch |= taken.stream() + .anyMatch(supply -> !ItemStack.isSameItemSameComponents(supply.placed(), expected)); } - if (!this.reserve(group.materials, group.fluids)) { + if (!this.reserve(group.materials)) { + for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; + return null; + } + for (var cell : group.cells) { + List contents = cell.contents().stream().map(BlockEntityContentAdapter.SlotStack::stack).toList(); + if (this.reserve(contents)) { + allocated.cells.add(cell); + allocated.materials.addAll(contents); + } else if (allowMismatch) { + BlockState state = cell.state(); + if (state.hasProperty(BlockStateProperties.HAS_BOOK)) state = state.setValue(BlockStateProperties.HAS_BOOK, false); + if (state.hasProperty(BlockStateProperties.HAS_RECORD)) state = state.setValue(BlockStateProperties.HAS_RECORD, false); + allocated.cells.add(new BuildingRodService.Cell(cell.pos(), state, cell.config(), List.of())); + allocated.componentMismatch = true; + allocated.missingContents = true; + } else { + for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; + return null; + } + } + if (group.separateContents) { + for (var entity : group.entities) { + List contents = entity.contents().stream().map(EntityBuildAdapter.SlotStack::stack).toList(); + if (this.reserve(contents)) { + allocated.entities.add(entity); + allocated.materials.addAll(contents); + } else if (allowMismatch) { + allocated.entities.add(new EntityBuildAdapter.Planned(entity.material(), entity.returned(), + entity.entityNbt(), List.of(), entity.fluids(), false)); + allocated.componentMismatch = true; + allocated.missingContents = true; + } else { + for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; + return null; + } + } + } + if (!this.reserve(List.of(), group.fluids)) { for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; return null; } if (!this.creative) { + allocated.returned.forEach(stack -> this.returned.add(stack.copy())); for (ItemStack stack : allocated.blockMaterials.values()) { if (stack.is(Items.POWDER_SNOW_BUCKET)) { ItemStack bucket = new ItemStack(Items.BUCKET, stack.getCount()); @@ -148,15 +193,25 @@ BuildingRodService.Group reserve(BuildingRodService.Group group, boolean allowMi return allocated; } - private int reserveBlock(ItemStack expected, boolean exact, int remaining, List taken) { + private record BlockSupply(ItemStack original, ItemStack placed, List returned) { + } + + private int reserveBlock(ItemStack expected, boolean exact, int remaining, List taken, boolean separateContents) { for (Source source : this.sources) { - if (!ItemStack.isSameItem(source.resource, expected) - || ItemStack.isSameItemSameComponents(source.resource, expected) != exact) continue; + if (!ItemStack.isSameItem(source.resource, expected) || source.available <= source.reserved) continue; + if (separateContents && source.blockMaterial == null) { + source.blockMaterial = BuildingBlockMaterial.separateContents(source.resource, this.player.level()); + } + var material = separateContents && source.blockMaterial != null + ? source.blockMaterial : new BuildingBlockMaterial.Supplied(source.resource, List.of()); + ItemStack empty = material.stack(); + if (ItemStack.isSameItemSameComponents(empty, expected) != exact) continue; int take = (int) Math.min(remaining, source.available - source.reserved); - if (take == 0) continue; source.reserved += take; remaining -= take; - taken.add(source.resource.copyWithCount(take)); + List returned = material.contents().stream() + .map(content -> content.stack().copyWithCount(content.stack().getCount() * take)).toList(); + taken.add(new BlockSupply(source.resource.copyWithCount(take), empty.copyWithCount(take), returned)); if (remaining == 0) break; } return remaining; @@ -203,6 +258,10 @@ List missing(List groups) { List fluids = new ArrayList<>(); for (var group : groups) { List required = new ArrayList<>(group.materials); + group.cells.forEach(cell -> cell.contents().forEach(content -> required.add(content.stack()))); + if (group.separateContents) { + group.entities.forEach(entity -> entity.contents().forEach(content -> required.add(content.stack()))); + } required.addAll(group.blockMaterials.values()); for (ItemStack material : required) { ItemStack combined = items.stream().filter(stack -> ItemStack.isSameItemSameComponents(stack, material)) diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodService.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodService.java index 2060076b19..7ab329db0f 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodService.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodService.java @@ -4,7 +4,6 @@ import dev.dubhe.anvilcraft.block.RedstoneWireBlock; import dev.dubhe.anvilcraft.block.RedstoneWireNetworkManager; import dev.dubhe.anvilcraft.block.cfa.CelestialForgingAnvilAmplifierBlock; -import dev.dubhe.anvilcraft.block.entity.PulseGeneratorBlockEntity; import dev.dubhe.anvilcraft.block.item.FlexibleMultiPartBlockItem; import dev.dubhe.anvilcraft.block.item.LargeCakeBlockItem; import dev.dubhe.anvilcraft.block.item.SimpleMultiPartBlockItem; @@ -21,6 +20,9 @@ import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.Container; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.player.Player; @@ -36,6 +38,7 @@ import net.minecraft.world.level.block.DoublePlantBlock; import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.SoundType; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.CommandBlockEntity; import net.minecraft.world.level.block.piston.PistonHeadBlock; @@ -54,9 +57,11 @@ import net.neoforged.neoforge.network.PacketDistributor; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; import javax.annotation.Nullable; @@ -66,12 +71,16 @@ public final class BuildingRodService { private static final Map CONFIRMATIONS = new WeakHashMap<>(); private record Confirmation(ResourceKey dimension, ItemStack disk, ItemStack rod, List cells, - List materials, boolean partial, long expiresAt) { - boolean matches(ServerPlayer player, List planned, List supplied, boolean allowPartial) { + List entities, List materials, boolean partial, long expiresAt) { + boolean matches( + ServerPlayer player, List planned, List plannedEntities, + List supplied, boolean allowPartial + ) { if (this.dimension != player.level().dimension() || this.rod != BuildingRodItem.heldRod(player) || this.partial != allowPartial || player.level().getGameTime() > this.expiresAt || !ItemStack.isSameItemSameComponents(this.disk, BuildingRodItem.material(player)) - || this.cells.size() != planned.size() || this.materials.size() != supplied.size()) return false; + || this.cells.size() != planned.size() || this.entities.size() != plannedEntities.size() + || this.materials.size() != supplied.size()) return false; for (int i = 0; i < planned.size(); i++) { Cell expected = this.cells.get(i); Cell current = planned.get(i); @@ -83,6 +92,9 @@ boolean matches(ServerPlayer player, List planned, List supplie if (first.slot() != second.slot() || !ItemStack.matches(first.stack(), second.stack())) return false; } } + for (int i = 0; i < plannedEntities.size(); i++) { + if (!this.entities.get(i).equals(plannedEntities.get(i).entityNbt())) return false; + } for (int i = 0; i < supplied.size(); i++) { if (!ItemStack.matches(this.materials.get(i), supplied.get(i))) return false; } @@ -123,7 +135,9 @@ static final class Group { final List entities = new ArrayList<>(); final Map blockMaterials = new LinkedHashMap<>(); final List returned = new ArrayList<>(); + boolean separateContents; boolean componentMismatch; + boolean missingContents; } public static long volume(BlockPos first, BlockPos last) { @@ -393,6 +407,7 @@ public static void blueprint(ServerPlayer player, BlockPos anchor, Rotation rota player.registryAccess()).snapshot(), disk.direction(), disk.upsideDown()); BlueprintPlacement placement = new BlueprintPlacement(anchor, rotation, mirror); + BlockPos sourceOrigin = BlueprintBlockConfiguration.sourceOrigin(snapshot); Map groups = new LinkedHashMap<>(); for (BlueprintMultiblocks.PlacedBlock entry : BlueprintMultiblocks.expand(snapshot, placement, -1)) { BlockState state = entry.state(); @@ -409,14 +424,19 @@ public static void blueprint(ServerPlayer player, BlockPos anchor, Rotation rota } BlockPos core = core(pos, state); Group group = groups.computeIfAbsent(core, ignored -> new Group()); + group.separateContents = true; BuildingBlockMaterial blockMaterial = BuildingBlockMaterial.extract(state, - entry.nbt().orElse(null), player.registryAccess()); - BlockEntityContentAdapter.Extracted extracted = BlockEntityContentAdapter.extract( - state, blockMaterial.config(), player.registryAccess()); - group.cells.add(new Cell(pos, state, extracted.config(), extracted.contents())); - extracted.contents().forEach(content -> group.materials.add(content.stack())); + entry.nbt().orElse(null), player.level()); + if (blockMaterial.requiresOperator() && !player.canUseGameMasterBlocks()) { + message(player, "blocked"); + return; + } + blockMaterial = blockMaterial.withConfiguration(state, blockMaterial.config(), player.registryAccess()); + BlueprintBlockConfiguration.transform(blockMaterial.config(), placement, sourceOrigin, snapshot); + group.materials.addAll(BlueprintBlockConfiguration.materials(blockMaterial.config())); + group.cells.add(new Cell(pos, state, blockMaterial.config(), blockMaterial.contents())); if (SignDecorationAdapter.isSign(state)) { - var decoration = SignDecorationAdapter.extract(state, extracted.config(), player.registryAccess()); + var decoration = SignDecorationAdapter.extract(state, blockMaterial.config(), player.registryAccess()); decoration.decorations().stream().map(SignDecorationAdapter.Decoration::material) .filter(material -> !material.isEmpty()).forEach(group.materials::add); } @@ -451,6 +471,16 @@ public static void blueprint(ServerPlayer player, BlockPos anchor, Rotation rota for (var entry : snapshot.entities()) { EntityType type = EntityType.by(entry.nbt()).orElse(null); if (type == null || EntityBuildAdapters.isTransient(type)) continue; + Entity probe = type.create(player.serverLevel()); + if (probe == null) { + message(player, "unsupported"); + return; + } + if ((type == EntityType.COMMAND_BLOCK_MINECART || probe.onlyOpCanSetNbt()) + && !player.canUseGameMasterBlocks()) { + message(player, "blocked"); + return; + } Vec3 world = placement.localOf(entry.pos(), entry.blockPos()) .add(anchor.getX(), anchor.getY(), anchor.getZ()); if (!canModify(player, BlockPos.containing(world))) { @@ -472,9 +502,12 @@ public static void blueprint(ServerPlayer player, BlockPos anchor, Rotation rota message(player, "unsupported"); return; } + plan = new EntityBuildAdapter.Planned(plan.material(), plan.returned(), + BuildingEntityTransform.sanitize(probe, plan.entityNbt()), plan.contents(), plan.fluids(), false); Group group = new Group(); + group.separateContents = probe instanceof Container; group.materials.add(plan.material()); - plan.contents().forEach(content -> group.materials.add(content.stack())); + if (!group.separateContents) plan.contents().forEach(content -> group.materials.add(content.stack())); plan.fluids().forEach(fluid -> group.fluids.add(fluid.fluid())); group.entities.add(plan); allGroups.add(group); @@ -552,10 +585,12 @@ static boolean commit(ServerPlayer player, List groups, boolean partial, List supplied = new ArrayList<>(); groups.forEach(group -> group.blockMaterials.values().forEach(stack -> supplied.add(stack.copy()))); placedGroups.forEach(group -> group.materials.forEach(stack -> supplied.add(stack.copy()))); - if (previous == null || !previous.matches(player, cells, supplied, partial)) { + if (previous == null || !previous.matches(player, cells, entities, supplied, partial)) { CONFIRMATIONS.put(player, new Confirmation(player.level().dimension(), BuildingRodItem.material(player).copy(), - BuildingRodItem.heldRod(player), List.copyOf(cells), supplied, partial, player.level().getGameTime() + 60)); - message(player, "component_mismatch"); + BuildingRodItem.heldRod(player), List.copyOf(cells), entities.stream().map(plan -> plan.entityNbt().copy()).toList(), + supplied, partial, player.level().getGameTime() + 60)); + message(player, placedGroups.stream().anyMatch(group -> group.missingContents) + ? "contents_missing" : "component_mismatch"); return false; } } @@ -580,19 +615,23 @@ static boolean commit(ServerPlayer player, List groups, boolean partial, for (Cell cell : cells) { BlockEntity blockEntity = player.level().getBlockEntity(cell.pos()); if (blockEntity != null) { - if (!cell.config().isEmpty() && !(blockEntity instanceof CommandBlockEntity)) { - if (quiet && blockEntity instanceof PulseGeneratorBlockEntity pulse) { - pulse.loadBlueprint(cell.config(), player.registryAccess()); - } else { - blockEntity.loadWithComponents(cell.config(), player.registryAccess()); - } + if (!quiet && !cell.config().isEmpty() && !(blockEntity instanceof CommandBlockEntity)) { + blockEntity.loadWithComponents(cell.config(), player.registryAccess()); } - BlockEntityContentAdapter.insert(blockEntity, cell.contents(), player.registryAccess()); ItemStack supplied = placedMaterials.get(cell.pos()); if (supplied != null) { BlockItem.updateCustomBlockEntityTag(player.level(), player, cell.pos(), supplied); blockEntity.applyComponentsFromItemStack(supplied); } + if (quiet && !SignDecorationAdapter.isSign(cell.state())) { + BlueprintBlockConfiguration.apply(blockEntity, cell.config(), player); + } + if (quiet && SignDecorationAdapter.isSign(cell.state())) { + blockEntity.loadWithComponents( + SignDecorationAdapter.sanitize(cell.state(), cell.config(), player.registryAccess()), player.registryAccess()); + } + BlockEntityContentAdapter.insert(blockEntity, cell.contents(), player.registryAccess()); + if (quiet) BlueprintBlockConfiguration.afterContents(blockEntity, cell.config(), player); blockEntity.setChanged(); player.level().sendBlockUpdated(cell.pos(), cell.state(), cell.state(), Block.UPDATE_CLIENTS); } @@ -616,9 +655,26 @@ static boolean commit(ServerPlayer player, List groups, boolean partial, undo.finish(player); BuildingRodMaterialBook.give(player, shortages); finishPlacement(player, cells.size()); + if (quiet || !placedMaterials.isEmpty()) playPlacementSounds(player, cells, quiet); return !missing; } + private static void playPlacementSounds(ServerPlayer player, List cells, boolean blueprint) { + Level level = player.level(); + if (blueprint) { + level.playSound(null, player.blockPosition(), SoundEvents.NOTE_BLOCK_PLING.value(), SoundSource.PLAYERS, 1.0f, 1.0f); + return; + } + Set sounded = new HashSet<>(); + for (Cell cell : cells) { + BlockState state = cell.state(); + if (!sounded.add(state.getBlock())) continue; + SoundType sound = state.getSoundType(level, cell.pos(), player); + level.playSound(null, cell.pos(), sound.getPlaceSound(), SoundSource.BLOCKS, + (sound.getVolume() + 1.0f) / 2.0f, sound.getPitch() * 0.8f); + } + } + static void finishPlacement(ServerPlayer player, int blocks) { BuildingRodItem.consume(player, BuildingRodItem.heldRod(player), blocks); player.getInventory().setChanged(); diff --git a/src/main/java/dev/dubhe/anvilcraft/building/SignDecorationAdapter.java b/src/main/java/dev/dubhe/anvilcraft/building/SignDecorationAdapter.java index 915597d2d6..cfde27cacd 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/SignDecorationAdapter.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/SignDecorationAdapter.java @@ -117,8 +117,7 @@ private static void extractSide( .parse(registries.createSerializationContext(NbtOps.INSTANCE), side) .result() .orElse(null); - // 解析不出来的文本拆不成加工项,原样留在配置里,不悄悄丢掉蓝图数据 - if (text == null) return; + if (text == null) throw new IllegalArgumentException("Invalid blueprint sign text"); Tag messages = side.get(MESSAGES); config.remove(key); if (messages != null && hasText(text)) { @@ -146,6 +145,15 @@ private static void extractSide( } } + public static CompoundTag sanitize(BlockState state, CompoundTag nbt, HolderLookup.Provider registries) { + CompoundTag blank = new CompoundTag(); + blank.put(FRONT_TEXT, blankSide(registries)); + blank.put(BACK_TEXT, blankSide(registries)); + blank.putBoolean(IS_WAXED, false); + Extracted extracted = extract(state, nbt, registries); + return compose(blank, extracted.decorations(), registries); + } + /** 提交时把配置与已交付的加工项合成最终 NBT;未交付项保持原版空白默认。 */ public static CompoundTag compose( @Nullable CompoundTag config, diff --git a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodClient.java b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodClient.java index bab6706f50..f929d5566a 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodClient.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodClient.java @@ -242,11 +242,7 @@ private static void updateTarget() { private static Rotation viewRotation() { var player = Minecraft.getInstance().player; if (player == null || disk == null || !disk.autoRotate()) return Rotation.NONE; - Direction source = disk.direction().getAxis().isHorizontal() ? disk.direction() : Direction.NORTH; - for (Rotation candidate : Rotation.values()) { - if (candidate.rotate(source) == player.getDirection()) return candidate; - } - return Rotation.NONE; + return BlueprintPlacement.facingPlayer(disk.direction(), player.getDirection()); } @SubscribeEvent diff --git a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderTypes.java b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderTypes.java index 487121b52f..429c39b214 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderTypes.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderTypes.java @@ -1,18 +1,36 @@ package dev.dubhe.anvilcraft.client.building; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.VertexFormat; +import dev.dubhe.anvilcraft.client.init.ModShaders; import dev.dubhe.anvilcraft.client.support.RenderSupport; +import net.minecraft.client.renderer.RenderStateShard; import net.minecraft.client.renderer.RenderType; import java.util.LinkedHashMap; import java.util.Map; final class BuildingRodRenderTypes { + private static final RenderType BLOCK_GHOST = RenderType.create( + "anvilcraft:building_rod_ghost", DefaultVertexFormat.BLOCK, VertexFormat.Mode.QUADS, 786432, true, true, + RenderType.CompositeState.builder() + .setShaderState(new RenderStateShard.ShaderStateShard(ModShaders::getBuildingRodGhostShader)) + .setTextureState(RenderStateShard.BLOCK_SHEET_MIPPED) + .setLightmapState(RenderStateShard.LIGHTMAP) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setOutputState(RenderStateShard.TRANSLUCENT_TARGET) + .createCompositeState(true) + ); private static final Map TYPES = new LinkedHashMap<>(); private BuildingRodRenderTypes() { } static RenderType ghost(RenderType original) { + if (original == RenderType.solid() || original == RenderType.cutout() + || original == RenderType.cutoutMipped() || original == RenderType.tripwire() || original == RenderType.translucent()) { + return BLOCK_GHOST; + } if (TYPES.size() >= 128) TYPES.clear(); return TYPES.computeIfAbsent(original, RenderSupport::useTranslucentIfPossible); } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderer.java b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderer.java index bfd263960b..6c44946818 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderer.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderer.java @@ -15,6 +15,7 @@ import dev.anvilcraft.lib.v2.cube.client.SelectionPart; import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.block.multipart.AbstractMultiPartBlock; +import dev.dubhe.anvilcraft.building.BlueprintBlockConfiguration; import dev.dubhe.anvilcraft.building.BlueprintMultiblocks; import dev.dubhe.anvilcraft.building.BlueprintPlacement; import dev.dubhe.anvilcraft.building.BuildingEntityTransform; @@ -252,12 +253,17 @@ private static void build(StructureSnapshot snapshot, BlueprintPlacement placeme meshAlpha = alpha; return; } + BlockPos sourceOrigin = BlueprintBlockConfiguration.sourceOrigin(snapshot); for (var entry : blocks) { BlockPos pos = entry.pos(); var state = entry.state(); BlockEntity entity = state.getBlock() instanceof EntityBlock block ? block.newBlockEntity(pos, state) : null; if (entity != null) { - entry.nbt().ifPresent(nbt -> entity.loadWithComponents(nbt, mc.level.registryAccess())); + entry.nbt().ifPresent(nbt -> { + var transformed = nbt.copy(); + BlueprintBlockConfiguration.transform(transformed, local, sourceOrigin, snapshot); + entity.loadWithComponents(transformed, mc.level.registryAccess()); + }); entity.setLevel(mc.level); ENTITIES.add(entity); } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/StructureScannerScreen.java b/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/StructureScannerScreen.java index 83781c47b5..da74263aa0 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/StructureScannerScreen.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/StructureScannerScreen.java @@ -63,9 +63,12 @@ import net.minecraft.world.level.block.HorizontalDirectionalBlock; import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.neoforged.neoforge.network.PacketDistributor; +import org.joml.Matrix3f; +import org.joml.Quaternionf; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL30; @@ -151,6 +154,8 @@ private static ResourceLocation scannerTexture(String name) { private LevelLike cachedPreviewLevelLike; @Nullable private StructureScannerMenu.ImportedStructure cachedImportedStructure; @Nullable private LevelLike cachedImportedPreview; + private AABB cachedPreviewBounds = new AABB(BlockPos.ZERO); + private AABB cachedImportedPreviewBounds = new AABB(BlockPos.ZERO); private Direction cachedPreviewFacing = Direction.NORTH; // 扫描数据版本追踪(用于缓存失效) @@ -936,7 +941,7 @@ private void renderPreview(GuiGraphics guiGraphics) { this.renderPreviewContent( guiGraphics, this.previewWindowX + this.previewWindowWidth / 2, - this.previewWindowY + this.previewWindowHeight / 2 + 5 + this.previewWindowY + this.previewWindowHeight / 2 ); RenderSystem.disableScissor(); @@ -1024,58 +1029,49 @@ private void renderPreview(GuiGraphics guiGraphics) { /** * 渲染3D预览内容 */ - @SuppressWarnings("checkstyle:VariableDeclarationUsageDistance") private void renderPreviewContent(GuiGraphics guiGraphics, int posX, int posY) { - if (this.minecraft == null || this.minecraft.level == null) { - return; - } + if (this.minecraft == null || this.minecraft.level == null) return; var imported = this.menu.getImportedStructure(); + LevelLike preview; + AABB bounds; if (imported != null) { - LevelLike preview = this.buildImportedPreview(imported, this.minecraft.level); - RenderSupport.renderLevelLikeWithFixedSize(preview, guiGraphics, posX, posY, 80.0F, - this.previewRotationX, this.previewRotationY + 270.0F, - Math.max(1, preview.horizontalSize()), Math.max(1, preview.verticalSize()), -0.5F); - return; - } - - // 获取Structure Scanner方块的状态 - if (this.cachedBlockEntity == null) { - return; - } + preview = this.buildImportedPreview(imported, this.minecraft.level); + bounds = this.cachedImportedPreviewBounds; + } else { + if (this.cachedBlockEntity == null) return; + var state = this.minecraft.level.getBlockState(this.cachedBlockEntity.getBlockPos()); + LevelLike scannedPreview = this.buildPreviewLevelLike(state.getValue(HorizontalDirectionalBlock.FACING)); + if (scannedPreview == null) return; + preview = scannedPreview; + bounds = this.cachedPreviewBounds; + } + + PoseStack pose = guiGraphics.pose(); + pose.pushPose(); + this.applyPreviewTransform(pose, bounds, posX, posY); + RenderSupport.renderLevelLikeBlocks(preview, pose, BlockPos.betweenClosed( + BlockPos.containing(bounds.minX, bounds.minY, bounds.minZ), + BlockPos.containing(bounds.maxX - 1, bounds.maxY - 1, bounds.maxZ - 1))); + if (imported == null) this.renderScannerBorder(pose); + pose.popPose(); + } - var level = this.minecraft.level; - var blockState = level.getBlockState(this.cachedBlockEntity.getBlockPos()); - var facing = blockState.getValue(HorizontalDirectionalBlock.FACING); - - // 构建并渲染 LevelLike(使用缓存) - LevelLike previewLevelLike = this.buildPreviewLevelLike(facing); - if (previewLevelLike != null) { - // 计算选区的实际尺寸(忽略 Scanner) - int rangeX = this.cachedRangeX; - int rangeY = this.cachedRangeY; - - // 使用选区范围作为缩放基准,忽略 Scanner 的影响 - int sizeX = Math.max(1, rangeX); - int sizeY = Math.max(1, rangeY); - - // 应用朝向旋转偏移,让预览根据Scanner的实际朝向旋转 - RenderSupport.renderLevelLikeWithFixedSize( - previewLevelLike, - guiGraphics, - posX, - posY, - (float) 80.0, - this.previewRotationX, - this.previewRotationY + getFacingYawOffset(facing), - sizeX, - sizeY, - -0.5f - ); - } - - // 渲染边框 - this.renderScannerBorder(guiGraphics, posX, posY, facing); + private void applyPreviewTransform(PoseStack pose, AABB bounds, int posX, int posY) { + Quaternionf rotation = Axis.XP.rotationDegrees(this.previewRotationX) + .mul(Axis.YP.rotationDegrees(this.previewRotationY + 315.0F)); + Matrix3f matrix = new Matrix3f().rotation(rotation); + float sizeX = (float) bounds.getXsize(); + float sizeY = (float) bounds.getYsize(); + float sizeZ = (float) bounds.getZsize(); + float projectedWidth = Math.abs(matrix.m00()) * sizeX + Math.abs(matrix.m10()) * sizeY + Math.abs(matrix.m20()) * sizeZ; + float projectedHeight = Math.abs(matrix.m01()) * sizeX + Math.abs(matrix.m11()) * sizeY + Math.abs(matrix.m21()) * sizeZ; + float scale = Math.min((this.previewWindowWidth - 8.0F) / projectedWidth, + (this.previewWindowHeight - 8.0F) / projectedHeight); + pose.translate(posX, posY, 100); + pose.scale(-scale, -scale, -scale); + pose.mulPose(rotation); + pose.translate(-(bounds.minX + bounds.maxX) / 2, -(bounds.minY + bounds.maxY) / 2, -(bounds.minZ + bounds.maxZ) / 2); } private LevelLike buildImportedPreview(StructureScannerMenu.ImportedStructure imported, ClientLevel level) { @@ -1087,9 +1083,11 @@ private LevelLike buildImportedPreview(StructureScannerMenu.ImportedStructure im int minY = blocks.stream().mapToInt(block -> block.pos().getY()).min().orElse(0); int minZ = blocks.stream().mapToInt(block -> block.pos().getZ()).min().orElse(0); BlockPos origin = new BlockPos(minX, minY, minZ); + this.cachedImportedPreviewBounds = new AABB(BlockPos.ZERO); for (var block : blocks) { BlockPos pos = block.pos().subtract(origin); preview.setBlockState(pos, block.state()); + this.cachedImportedPreviewBounds = this.cachedImportedPreviewBounds.minmax(new AABB(pos)); var entity = preview.getBlockEntity(pos); if (entity != null) block.nbt().ifPresent(tag -> entity.loadWithComponents(tag, level.registryAccess())); } @@ -1117,6 +1115,8 @@ private LevelLike buildImportedPreview(StructureScannerMenu.ImportedStructure im // 获取扫描范围 int rangeX = this.cachedRangeX; int rangeY = this.cachedRangeY; + this.cachedPreviewBounds = new AABB(0, 0, 0, Math.max(1, rangeX), + Math.max(1, rangeY), Math.max(1, this.cachedRangeZ) + 2); boolean upsideDown = false; if (this.cachedBlockEntity.getBlockState().hasProperty(dev.dubhe.anvilcraft.block.StructureScannerBlock.UPSIDE_DOWN)) { @@ -1144,7 +1144,10 @@ private LevelLike buildImportedPreview(StructureScannerMenu.ImportedStructure im int renderY = upsideDown ? (Math.max(1, rangeY) - 1 - data.y()) : data.y(); BlueprintPlacement placement = new BlueprintPlacement(new BlockPos(data.x(), renderY, data.z() + 1), this.rotationForPreview(facing), Mirror.NONE); - BlueprintMultiblocks.forEachPart(BlockPos.ZERO, data.state(), placement, previewLevelLike::setBlockState); + BlueprintMultiblocks.forEachPart(BlockPos.ZERO, data.state(), placement, (pos, state) -> { + previewLevelLike.setBlockState(pos, state); + this.cachedPreviewBounds = this.cachedPreviewBounds.minmax(new AABB(pos)); + }); } } @@ -1167,77 +1170,13 @@ private Rotation rotationForPreview(Direction scannerFacing) { }; } - @SuppressWarnings("unused") - private float getFacingYawOffset(Direction scannerFacing) { - return 270f; - } - - /** - * 渲染Structure Scanner的边框(与世界渲染一致) - */ - private void renderScannerBorder(GuiGraphics guiGraphics, int posX, int posY, Direction facing) { - if (this.minecraft == null || this.minecraft.level == null) { - return; - } - - if (this.cachedBlockEntity == null) return; - - // 使用缓存的扫描范围 - int rangeX = this.cachedRangeX; - int rangeY = this.cachedRangeY; - int rangeZ = this.cachedRangeZ; - - // 使用选区范围作为缩放基准,忽略 Scanner - int sizeX = Math.max(1, rangeX); - int sizeY = Math.max(1, rangeY); - - // 获取缓冲区 + private void renderScannerBorder(PoseStack pose) { + if (this.minecraft == null || this.cachedBlockEntity == null) return; MultiBufferSource.BufferSource buffers = this.minecraft.renderBuffers().bufferSource(); - final VertexConsumer consumer = buffers.getBuffer(RenderType.lines()); - - // 设置PoseStack - PoseStack poseStack = guiGraphics.pose(); - poseStack.pushPose(); - - // 1. 平移到预览窗口中心 - poseStack.translate(posX, posY, 100); - - // 2. 缩放(与方块渲染保持一致,使用选区范围) - float scaleX = 80.0f / (sizeX * Mth.SQRT_OF_TWO); - float scaleY = 80.0f / (float) sizeY; - float scale = Math.min(scaleY, scaleX); - poseStack.scale(-scale, -scale, -scale); - - // 3. 平移到中心 - poseStack.translate(-(float) sizeX / 2, -(float) sizeY / 2, 0); - - // 4. 应用X轴旋转 - poseStack.mulPose(Axis.XP.rotationDegrees(this.previewRotationX)); - - // 5. Y轴旋转 - float offsetX = (float) -sizeX / 2 + 0.05f; - float offsetZ = (float) -sizeX / 2 + 1; - poseStack.translate(-offsetX, 0, -offsetZ); - // 应用朝向旋转偏移 - float yawOffset = getFacingYawOffset(facing); - poseStack.mulPose(Axis.YP.rotationDegrees(this.previewRotationY + 45 + yawOffset)); - poseStack.translate(offsetX, 0, offsetZ); - - // 6. 平移Z轴 - poseStack.translate(0, 0, -1); - - // 7. 创建边框形状 - 与世界中渲染的边框完全一致 - // 在预览坐标系中: - // - Scanner 在 Z=0 - // - 选区从 Z=2 到 Z=rangeZ+1 - // (内部渲染已规整化为正数纵向区间,无需使用负Y向下延伸边框) - final VoxelShape borderShape = Shapes.create(0.0, 0.0, 2.0, rangeX, rangeY, rangeZ + 2); - - // 8. 渲染边框(青色) - TooltipRenderHelper.renderOutline(poseStack, consumer, 0, 0, 0, BlockPos.ZERO, borderShape, 0xFF00FFCC); - + VertexConsumer consumer = buffers.getBuffer(RenderType.lines()); + VoxelShape border = Shapes.create(0, 0, 2, this.cachedRangeX, this.cachedRangeY, this.cachedRangeZ + 2); + TooltipRenderHelper.renderOutline(pose, consumer, 0, 0, 0, BlockPos.ZERO, border, 0xFF00FFCC); buffers.endBatch(RenderType.lines()); - poseStack.popPose(); } /** diff --git a/src/main/java/dev/dubhe/anvilcraft/client/init/ModShaders.java b/src/main/java/dev/dubhe/anvilcraft/client/init/ModShaders.java index cd39ebd688..d3b5c811c0 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/init/ModShaders.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/init/ModShaders.java @@ -43,9 +43,13 @@ public class ModShaders { static ShaderInstance scanPreviewItemShader; @Getter static Matrix4f orthoMatrix = new Matrix4f(); + @Getter + private static ShaderInstance buildingRodGhostShader; public static void register(RegisterShadersEvent event) { try { + event.registerShader(new ShaderInstance(event.getResourceProvider(), AnvilCraft.of("building_rod_ghost"), + DefaultVertexFormat.BLOCK), shader -> buildingRodGhostShader = shader); event.registerShader(new ShaderInstance(event.getResourceProvider(), AnvilCraft.of("equipment_charge"), DefaultVertexFormat.POSITION_TEX_COLOR), shader -> equipmentChargeShader = shader); event.registerShader( diff --git a/src/main/java/dev/dubhe/anvilcraft/client/support/RenderSupport.java b/src/main/java/dev/dubhe/anvilcraft/client/support/RenderSupport.java index 29d77551bb..4e510ddcde 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/support/RenderSupport.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/support/RenderSupport.java @@ -435,7 +435,6 @@ public static void renderLevelLikeWithFixedSize( float centerOffset ) { RenderSystem.enableBlend(); - final Minecraft minecraft = Minecraft.getInstance(); PoseStack pose = guiGraphics.pose(); pose.pushPose(); @@ -482,9 +481,18 @@ public static void renderLevelLikeWithFixedSize( pose.pushPose(); RenderSystem.setShaderColor(1f, 1f, 1f, 1f); pose.translate(0, 0, -1); + renderLevelLikeBlocks(level, pose, iter); + pose.popPose(); + pose.popPose(); + } + + public static void renderLevelLikeBlocks(LevelLike level, PoseStack pose, Iterable positions) { + RenderSystem.enableBlend(); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + Minecraft minecraft = Minecraft.getInstance(); MultiBufferSource.BufferSource buffers = minecraft.renderBuffers().bufferSource(); BlockRenderDispatcher blockRenderer = minecraft.getBlockRenderer(); - for (BlockPos pos : iter) { + for (BlockPos pos : positions) { BlockState state = level.getBlockState(pos); pose.pushPose(); @@ -509,8 +517,6 @@ public static void renderLevelLikeWithFixedSize( pose.popPose(); } buffers.endBatch(); - pose.popPose(); - pose.popPose(); } private static Optional getCachedBlockEntity(BlockState state) { diff --git a/src/main/java/dev/dubhe/anvilcraft/data/lang/BuildingRodLang.java b/src/main/java/dev/dubhe/anvilcraft/data/lang/BuildingRodLang.java index 87466e105d..a5ae65adeb 100644 --- a/src/main/java/dev/dubhe/anvilcraft/data/lang/BuildingRodLang.java +++ b/src/main/java/dev/dubhe/anvilcraft/data/lang/BuildingRodLang.java @@ -8,7 +8,9 @@ private BuildingRodLang() { public static void init(RegistrumLangProvider provider) { provider.add("message.anvilcraft.building_rod.component_mismatch", - "Some blocks have mismatched components; right-click again within 3 seconds to confirm placement"); + "Some blocks have mismatched components; right-click again within 3 seconds to use the available materials and their data"); + provider.add("message.anvilcraft.building_rod.contents_missing", + "Missing matching container contents; right-click again within 3 seconds to place those containers empty"); provider.add("message.anvilcraft.building_rod.missing_book", "Missing materials; carry a book to receive a material list"); provider.add("message.anvilcraft.building_rod.nothing_to_undo", "No unchanged placement to undo"); provider.add("message.anvilcraft.building_rod.undone", "Last placement undone"); diff --git a/src/main/java/dev/dubhe/anvilcraft/entity/FallingGiantAnvilEntity.java b/src/main/java/dev/dubhe/anvilcraft/entity/FallingGiantAnvilEntity.java index a578c09037..d37f102c8a 100644 --- a/src/main/java/dev/dubhe/anvilcraft/entity/FallingGiantAnvilEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/entity/FallingGiantAnvilEntity.java @@ -275,6 +275,6 @@ public void tick() { @Override protected AABB makeBoundingBox() { - return EntityDimensions.scalable(3, 3).makeBoundingBox(this.position().add(0, -1, 0)); + return EntityDimensions.scalable(3, 3).makeBoundingBox(this.position().add(0, -1, 0)).deflate(0.01); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/event/ServerBlockEntityEventListener.java b/src/main/java/dev/dubhe/anvilcraft/event/ServerBlockEntityEventListener.java index 495ebfd1b8..a69eec83c3 100644 --- a/src/main/java/dev/dubhe/anvilcraft/event/ServerBlockEntityEventListener.java +++ b/src/main/java/dev/dubhe/anvilcraft/event/ServerBlockEntityEventListener.java @@ -5,7 +5,6 @@ import dev.dubhe.anvilcraft.api.event.BlockEntityEvent; import dev.dubhe.anvilcraft.api.fluid.IFluidHandlerHolder; import dev.dubhe.anvilcraft.api.fluid.network.FluidNetworkManager; -import dev.dubhe.anvilcraft.api.fluid.network.FluidNetworkScanner; import dev.dubhe.anvilcraft.api.heat.HeaterManager; import dev.dubhe.anvilcraft.api.power.IPowerComponent; import dev.dubhe.anvilcraft.api.power.PowerGrid; @@ -22,11 +21,8 @@ public class ServerBlockEntityEventListener { @SubscribeEvent public static void onLoad(BlockEntityEvent.ServerLoad event) { - // 外部模组容器不实现 AnvilCraft 接口,加载时也要登记到流体网络。 - if (!(event.getEntity() instanceof IFluidHandlerHolder) - && FluidNetworkScanner.isContainer( - event.getLevel(), event.getEntity().getBlockPos(), event.getEntity() - )) { + // 加载回调中只登记候选位置;能力代理可能跨区块回查,必须等区块加载完成后再检测。 + if (!(event.getEntity() instanceof IFluidHandlerHolder)) { FluidNetworkManager.INSTANCE.addContainerAfterLoad( event.getLevel(), event.getEntity().getBlockPos() ); diff --git a/src/main/resources/assets/anvilcraft/shaders/core/building_rod_ghost.fsh b/src/main/resources/assets/anvilcraft/shaders/core/building_rod_ghost.fsh new file mode 100644 index 0000000000..1739c5cb28 --- /dev/null +++ b/src/main/resources/assets/anvilcraft/shaders/core/building_rod_ghost.fsh @@ -0,0 +1,29 @@ +#version 150 + +#moj_import + +uniform sampler2D Sampler0; + +uniform vec4 ColorModulator; +uniform float FogStart; +uniform float FogEnd; +uniform vec4 FogColor; + +in float vertexDistance; +in vec4 vertexColor; +in vec2 texCoord0; + +out vec4 fragColor; + +void main() { + vec4 color = texture(Sampler0, texCoord0); + // Keep cutout holes out of the depth buffer without clipping low-opacity previews. + if (color.a < 0.1) { + discard; + } + color *= vertexColor * ColorModulator; + if (color.a <= 0.0) { + discard; + } + fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor); +} diff --git a/src/main/resources/assets/anvilcraft/shaders/core/building_rod_ghost.json b/src/main/resources/assets/anvilcraft/shaders/core/building_rod_ghost.json new file mode 100644 index 0000000000..65e6762264 --- /dev/null +++ b/src/main/resources/assets/anvilcraft/shaders/core/building_rod_ghost.json @@ -0,0 +1,116 @@ +{ + "vertex": "minecraft:rendertype_translucent", + "fragment": "anvilcraft:building_rod_ghost", + "samplers": [ + { + "name": "Sampler0" + }, + { + "name": "Sampler2" + } + ], + "uniforms": [ + { + "name": "ModelViewMat", + "type": "matrix4x4", + "count": 16, + "values": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "name": "ProjMat", + "type": "matrix4x4", + "count": 16, + "values": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "name": "ChunkOffset", + "type": "float", + "count": 3, + "values": [ + 0.0, + 0.0, + 0.0 + ] + }, + { + "name": "ColorModulator", + "type": "float", + "count": 4, + "values": [ + 1.0, + 1.0, + 1.0, + 1.0 + ] + }, + { + "name": "FogStart", + "type": "float", + "count": 1, + "values": [ + 0.0 + ] + }, + { + "name": "FogEnd", + "type": "float", + "count": 1, + "values": [ + 1.0 + ] + }, + { + "name": "FogColor", + "type": "float", + "count": 4, + "values": [ + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "name": "FogShape", + "type": "int", + "count": 1, + "values": [ + 0 + ] + } + ] +}