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
+ ]
+ }
+ ]
+}
| | | | |