diff --git a/src/generated/resources/assets/anvilcraft/lang/en_ud.json b/src/generated/resources/assets/anvilcraft/lang/en_ud.json index b02d34c946..b6d932fad4 100644 --- a/src/generated/resources/assets/anvilcraft/lang/en_ud.json +++ b/src/generated/resources/assets/anvilcraft/lang/en_ud.json @@ -1084,7 +1084,7 @@ "item.anvilcraft.brown_cement_bucket": "ʇǝʞɔnᗺ ʇuǝɯǝƆ uʍoɹᗺ", "item.anvilcraft.buffer_boots": "sʇooᗺ ɹǝɟɟnᗺ", "item.anvilcraft.building_rod": "poᴚ ᵷuᴉpꞁᴉnᗺ", - "item.anvilcraft.building_rod.controls": "ʇuᴉɹdǝnꞁq ɐ ɟo ʇɹɐd ǝꞁqɐꞁᴉɐʌɐ ǝɥʇ pꞁᴉnq oʇ ʇɟᴉɥS pꞁoɥ ⸵(sʞɔoꞁq 000'߈ oʇ dn) xoq ɐ ꞁꞁᴉɟ oʇ ᵷɐɹp puɐ ǝsn pꞁoH", + "item.anvilcraft.building_rod.controls": "ʇuᴉɹdǝnꞁq ɐ ɟo ʇɹɐd ǝꞁqɐꞁᴉɐʌɐ ǝɥʇ pꞁᴉnq oʇ ʇɟᴉɥS pꞁoɥ ⸵ǝʇsɐd oʇ uᴉɐᵷɐ ʞɔᴉꞁɔ-ʇɥᵷᴉɹ uǝɥʇ 'ɯǝɥʇ ʞɔoꞁ oʇ ǝsɐǝꞁǝɹ 'suoᴉʇɔǝɾoɹd ʇuᴉɹdǝnꞁq ʇɐǝdǝɹ oʇ ᵷɐɹp puɐ ǝsn pꞁoɥ ⸵ꞁǝɔuɐɔ oʇ ꞁɹʇƆ ǝsɐǝꞁǝɹ ɹo 'ǝɔɐꞁd oʇ ʞɔᴉꞁɔ-ʇɥᵷᴉɹ 'ʇsnɾpɐ oʇ sʎǝʞ ʍoɹɹɐ ǝsn 'ǝɔuɐʇsᴉp ǝɥʇ dǝǝʞ oʇ ɹǝuɹoɔ ʇsɹᴉɟ ǝɥʇ ᵷuᴉʇɔǝꞁǝs ɹǝʇɟɐ ꞁɹʇƆ pꞁoɥ ⸵(sʞɔoꞁq 000'߈ oʇ dn) xoq ɐ ꞁꞁᴉɟ oʇ ʞɔoꞁq ǝꞁqɐpᴉꞁꞁoɔ ɐ ɯoɹɟ ᵷɐɹp puɐ ǝsn pꞁoH", "item.anvilcraft.building_rod.desc": "ɥɔɐǝɹ ʞɔoꞁq ϛ⥝+ ɥʇᴉʍ pꞁᴉnq oʇ puɐɥ ɹǝɥʇᴉǝ uᴉ pꞁoɥ ⸵sʇǝʞɔod uᴉ ᵷuᴉpnꞁɔuᴉ 'pǝᴉɹɹɐɔ ǝꞁᴉɥʍ ɥɔɐǝɹ ʍɐꞁɔ qɐɹɔ sʇuɐɹ⅁", "item.anvilcraft.building_rod.energy": "ʞɔoꞁq/ƎℲ 00⥝ · ƎℲ 000'000'8 / %s", "item.anvilcraft.building_rod.fluids": "ǝᵷɐɹoʇs pǝʞuᴉꞁ ɹo ʎɹoʇuǝʌuᴉ ʎq pǝᴉꞁddns 'ꞁꞁᴉɟ ɹǝd ᗺ ᘔ ʇsoɔ spᴉnꞁɟ ǝꞁqɐʍǝuǝɹ ⸵sʞɔoꞁq ᵷoꞁɹǝʇɐʍ puɐ sɐǝɹɐ ꞁꞁᴉɟ uɐɔ sʇǝʞɔnᗺ", @@ -1332,11 +1332,13 @@ "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", "message.anvilcraft.building_rod.no_energy": "ɹoʇᴉɔɐdɐɔ pǝᵷɹɐɥɔ ɐ ʎɹɹɐɔ ⸵ɹǝʍod ɥᵷnouǝ ʇoN", - "message.anvilcraft.building_rod.nothing_to_undo": "opun oʇ ʇuǝɯǝɔɐꞁd pǝᵷuɐɥɔun oN", + "message.anvilcraft.building_rod.nothing_to_undo": "opun oʇ ʇuǝɯǝɔɐꞁd oN", "message.anvilcraft.building_rod.placed": "pǝɔɐꞁd ʇuᴉɹdǝnꞁᗺ", "message.anvilcraft.building_rod.too_many": "ǝɔuo ʇɐ sʞɔoꞁq 000'߈ uɐɥʇ ǝɹoɯ ǝɔɐꞁd ʇouuɐƆ", + "message.anvilcraft.building_rod.undo_partial": "uᴉɐᵷɐ opun puɐ sɯǝʇᴉ pǝuɹnʇǝɹ ǝɥʇ ɹǝʌoɔǝᴚ ˙ᵷuᴉpuǝd ǝɹɐ spunɟǝɹ ɹo sǝᴉʇᴉʇuǝ ǝɯos ⸵pǝɹoʇsǝɹ ɐǝɹⱯ", "message.anvilcraft.building_rod.undone": "ǝuopun ʇuǝɯǝɔɐꞁd ʇsɐꞀ", "message.anvilcraft.building_rod.unsupported": "pǝᴉꞁddns ǝq ʇouuɐɔ ʇɐɥʇ sꞁɐᴉɹǝʇɐɯ suᴉɐʇuoɔ ʇuᴉɹdǝnꞁq sᴉɥ⟘", + "message.anvilcraft.building_rod.unsupported_type": "%s :ʇɔǝɾqo ʇuᴉɹdǝnꞁq ɹoɟ ꞁɐᴉɹǝʇɐɯ ᵷuᴉpꞁᴉnq pǝʇɹoddns oN", "message.anvilcraft.chute.cannot_place": "uoᴉʇɔǝɹᴉp sᴉɥʇ ǝɔɐɟ ʇouuɐɔ ǝʇnɥƆ", "message.anvilcraft.code_gen_check": "˙sɯǝʇᴉ sɐɥ ʇoꞁs ʇndʇno ǝɥʇ ɟᴉ puɐ ǝqnɔ ɐ sᴉ ɐǝɹɐ pǝʇɔǝꞁǝs ǝɥʇ ɟᴉ ʞɔǝɥɔ ǝsɐǝꞁԀ", "message.anvilcraft.code_gen_filed": "pǝꞁᴉɐɟ uoᴉʇɐɹǝuǝᵷ ǝpoƆ", @@ -1390,8 +1392,9 @@ "screen.anvilcraft.balance_mode.restock": "ʎꞁuo ʞɔoʇsǝᴚ", "screen.anvilcraft.balance_mode.smart": "ʇɹɐɯS", "screen.anvilcraft.batch_crafter.switch_recipe": "ǝdᴉɔǝɹ ᵷuᴉʇɟɐɹɔ ɥɔʇᴉʍS", - "screen.anvilcraft.building_rod.distance": "ʇuᴉɹdǝnꞁq ʞɔoꞀ %s | ʇsnɾpɐ oʇ %s/%s\nǝɔuɐʇsᴉp uoᴉʇɔǝɾoɹd ǝɥʇ dǝǝʞ oʇ %s pꞁoH", - "screen.anvilcraft.building_rod.optimized": "ɹoɹɹᴉW %s | ǝʇɐʇoᴚ %s %s\nǝʌoW %s %s %s %s %s %s\nꞁǝɔuɐƆ %s | ǝɔɐꞁԀ %s", + "screen.anvilcraft.building_rod.distance": "ǝʇsɐd oʇ uᴉɐᵷɐ ʞɔᴉꞁɔ uǝɥʇ 'suoᴉʇɔǝɾoɹd ʇɐǝdǝɹ oʇ ᵷɐɹp puɐ pꞁoH / uoᴉʇɔǝɾoɹd ʞɔoꞀ %s | ʇsnɾpɐ oʇ %s/%s\nǝɔuɐʇsᴉp uoᴉʇɔǝɾoɹd ǝɥʇ dǝǝʞ oʇ %s pꞁoH", + "screen.anvilcraft.building_rod.optimized": "ɹoɹɹᴉW %s | ǝʇɐʇoᴚ %s %s\nǝʌoW %s %s %s %s %s %s\nꞁǝɔuɐƆ %s | suoᴉʇɔǝɾoɹd ǝʇsɐԀ %s", + "screen.anvilcraft.building_rod.selection_size": "%s × %s × %s", "screen.anvilcraft.building_rod.suspended": "ʞɔoꞁun oʇ %s puɐ poɹ ᵷuᴉpꞁᴉnq ǝɥʇ pꞁoH | pǝʞɔoꞁ uoᴉʇɔǝɾoɹԀ", "screen.anvilcraft.building_rod.tool.cancel": "ʇuǝɯǝɔɐꞁd ꞁǝɔuɐƆ", "screen.anvilcraft.building_rod.tool.confirm": "ʇuǝɯǝɔɐꞁd ɯɹᴉɟuoƆ", @@ -1403,7 +1406,7 @@ "screen.anvilcraft.building_rod.traditional": "ꞁǝɔuɐƆ %s | %s · ǝʇnɔǝxǝ :ǝs∩ | sɹǝʎɐꞁ :ꞁꞁoɹɔS + %s | ʇsnɾpɐ :ꞁꞁoɹɔS + %s | ꞁooʇ ʇɔǝꞁǝs :ꞁꞁoɹɔS + %s", "screen.anvilcraft.building_rod.traditional.anchor_following": "ɹᴉɐɥssoɹɔ ᵷuᴉʍoꞁꞁoℲ", "screen.anvilcraft.building_rod.traditional.anchor_locked": "pǝʞɔoꞁ ɹoɥɔuⱯ", - "screen.anvilcraft.building_rod.traditional.hint": "ɹɐqʇoɥ sǝᵷuɐɥɔ %s sǝʇnɔǝxǝ %s\nsʇsnɾpɐ %s+%s sꞁooʇ sǝɥɔʇᴉʍs %s+%s", + "screen.anvilcraft.building_rod.traditional.hint": "ɹɐqʇoɥ sǝᵷuɐɥɔ %s (ǝʇsɐd oʇ uᴉɐᵷɐ ʞɔᴉꞁɔ uǝɥʇ 'suoᴉʇɔǝɾoɹd ʇɐǝdǝɹ oʇ ǝʌoW ɥʇᴉʍ ᵷɐɹp puɐ pꞁoɥ) sǝʇnɔǝxǝ %s\nsʇsnɾpɐ %s+%s sꞁooʇ sǝɥɔʇᴉʍs %s+%s", "screen.anvilcraft.building_rod.traditional.layer_all": "ꞁꞁⱯ", "screen.anvilcraft.building_rod.traditional.scroll": "ꞁꞁoɹɔS", "screen.anvilcraft.building_rod.traditional.status": "%6$s | %5$s ɹǝʎɐꞀ | %4$s ɹoɹɹᴉW | %3$s uoᴉʇɐʇoᴚ\n%2$s\n%1$s", diff --git a/src/generated/resources/assets/anvilcraft/lang/en_us.json b/src/generated/resources/assets/anvilcraft/lang/en_us.json index f5272f205a..2d710979c6 100644 --- a/src/generated/resources/assets/anvilcraft/lang/en_us.json +++ b/src/generated/resources/assets/anvilcraft/lang/en_us.json @@ -1084,7 +1084,7 @@ "item.anvilcraft.brown_cement_bucket": "Brown Cement Bucket", "item.anvilcraft.buffer_boots": "Buffer Boots", "item.anvilcraft.building_rod": "Building Rod", - "item.anvilcraft.building_rod.controls": "Hold use and drag to fill a box (up to 4,000 blocks); hold Shift to build the available part of a blueprint", + "item.anvilcraft.building_rod.controls": "Hold use and drag from a collidable block to fill a box (up to 4,000 blocks); hold Ctrl after selecting the first corner to keep the distance, use arrow keys to adjust, right-click to place, or release Ctrl to cancel; hold use and drag to repeat blueprint projections, release to lock them, then right-click again to paste; hold Shift to build the available part of a blueprint", "item.anvilcraft.building_rod.desc": "Grants crab claw reach while carried, including in pockets; hold in either hand to build with +15 block reach", "item.anvilcraft.building_rod.energy": "%s / 8,000,000 FE · 100 FE/block", "item.anvilcraft.building_rod.fluids": "Buckets can fill areas and waterlog blocks; renewable fluids cost 2 B per fill, supplied by inventory or linked storage", @@ -1332,11 +1332,13 @@ "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", "message.anvilcraft.building_rod.no_energy": "Not enough power; carry a charged capacitor", - "message.anvilcraft.building_rod.nothing_to_undo": "No unchanged placement to undo", + "message.anvilcraft.building_rod.nothing_to_undo": "No placement to undo", "message.anvilcraft.building_rod.placed": "Blueprint placed", "message.anvilcraft.building_rod.too_many": "Cannot place more than 4,000 blocks at once", + "message.anvilcraft.building_rod.undo_partial": "Area restored; some entities or refunds are pending. Recover the returned items and undo again", "message.anvilcraft.building_rod.undone": "Last placement undone", "message.anvilcraft.building_rod.unsupported": "This blueprint contains materials that cannot be supplied", + "message.anvilcraft.building_rod.unsupported_type": "No supported building material for blueprint object: %s", "message.anvilcraft.chute.cannot_place": "Chute cannot face this direction", "message.anvilcraft.code_gen_check": "Please check if the selected area is a cube and if the output slot has items.", "message.anvilcraft.code_gen_filed": "Code generation failed", @@ -1390,8 +1392,9 @@ "screen.anvilcraft.balance_mode.restock": "Restock only", "screen.anvilcraft.balance_mode.smart": "Smart", "screen.anvilcraft.batch_crafter.switch_recipe": "Switch crafting recipe", - "screen.anvilcraft.building_rod.distance": "Hold %s to keep the projection distance\n%s/%s to adjust | %s Lock blueprint", - "screen.anvilcraft.building_rod.optimized": "%s Place | %s Cancel\n%s %s %s %s %s %s Move\n%s %s Rotate | %s Mirror", + "screen.anvilcraft.building_rod.distance": "Hold %s to keep the projection distance\n%s/%s to adjust | %s Lock projection / Hold and drag to repeat projections, then click again to paste", + "screen.anvilcraft.building_rod.optimized": "%s Paste projections | %s Cancel\n%s %s %s %s %s %s Move\n%s %s Rotate | %s Mirror", + "screen.anvilcraft.building_rod.selection_size": "%s × %s × %s", "screen.anvilcraft.building_rod.suspended": "Projection locked | Hold the building rod and %s to unlock", "screen.anvilcraft.building_rod.tool.cancel": "Cancel placement", "screen.anvilcraft.building_rod.tool.confirm": "Confirm placement", @@ -1403,7 +1406,7 @@ "screen.anvilcraft.building_rod.traditional": "%s + Scroll: select tool | %s + Scroll: adjust | %s + Scroll: layers | Use: execute · %s | %s Cancel", "screen.anvilcraft.building_rod.traditional.anchor_following": "Following crosshair", "screen.anvilcraft.building_rod.traditional.anchor_locked": "Anchor locked", - "screen.anvilcraft.building_rod.traditional.hint": "%s+%s switches tools %s+%s adjusts\n%s executes %s changes hotbar", + "screen.anvilcraft.building_rod.traditional.hint": "%s+%s switches tools %s+%s adjusts\n%s executes (hold and drag with Move to repeat projections, then click again to paste) %s changes hotbar", "screen.anvilcraft.building_rod.traditional.layer_all": "All", "screen.anvilcraft.building_rod.traditional.scroll": "Scroll", "screen.anvilcraft.building_rod.traditional.status": "%1$s\n%2$s\nRotation %3$s | Mirror %4$s | Layer %5$s | %6$s", diff --git a/src/main/java/dev/dubhe/anvilcraft/api/power/PowerGrid.java b/src/main/java/dev/dubhe/anvilcraft/api/power/PowerGrid.java index 3aa981d00a..d266aa0ef8 100644 --- a/src/main/java/dev/dubhe/anvilcraft/api/power/PowerGrid.java +++ b/src/main/java/dev/dubhe/anvilcraft/api/power/PowerGrid.java @@ -1,6 +1,7 @@ package dev.dubhe.anvilcraft.api.power; import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.item.WeatherproofChestplateItem; import dev.dubhe.anvilcraft.network.PowerGridRemovePacket; import dev.dubhe.anvilcraft.network.PowerGridSyncChunkPacket; import lombok.Getter; @@ -182,12 +183,17 @@ public boolean flush() { this.consume += consumer.getInputPower(); } + int players = 0; for (DynamicPowerComponent dynamicComponent : new ArrayList<>(this.dynamicComponents)) { Entity owner = dynamicComponent.getOwner(); if (owner.level() != this.level || !this.collideFast(dynamicComponent.boundingBox())) { dynamicComponent.switchTo(null); continue; } + if (owner instanceof ServerPlayer) { + WeatherproofChestplateItem.clearGridDemand(dynamicComponent); + players++; + } int power = dynamicComponent.getPowerConsumption(); if (power > 0) { this.consume += power; @@ -196,6 +202,13 @@ public boolean flush() { } } + long chargingPowerPerPlayer = this.getRemaining() / Math.max(1, players); + for (DynamicPowerComponent dynamicComponent : this.dynamicComponents) { + if (dynamicComponent.getOwner() instanceof ServerPlayer player) { + this.consume += WeatherproofChestplateItem.refreshGridDemand(player, chargingPowerPerPlayer); + } + } + if (this.consume != oldConsume || this.generate != oldGenerate || this.hasInfinitePower != oldInfinitePower) { this.changed = true; } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/PulseGeneratorBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/PulseGeneratorBlockEntity.java index 72f70dc7ba..2f396e2294 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/PulseGeneratorBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/PulseGeneratorBlockEntity.java @@ -91,7 +91,7 @@ protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) tag.put("ExtraData", data); } - /** 恢复蓝图保存的停机状态,不把加载当作输入边沿或循环启动指令。 */ + /** 暂缓结构恢复过程中的输入判定,完成后由蓝图统一激活。 */ public void loadBlueprint(CompoundTag tag, HolderLookup.Provider registries) { this.loadingBlueprint = true; this.skipBlueprintLoadUpdate = true; 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 cc7ea95f51..78ace99c82 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/StructureScannerBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/StructureScannerBlockEntity.java @@ -1,6 +1,7 @@ package dev.dubhe.anvilcraft.block.entity; import dev.dubhe.anvilcraft.block.StructureScannerBlock; +import dev.dubhe.anvilcraft.building.BlueprintCapture; import dev.dubhe.anvilcraft.building.BlueprintMultiblocks; import dev.dubhe.anvilcraft.init.ModMenuTypes; import dev.dubhe.anvilcraft.init.item.ModItems; @@ -429,8 +430,8 @@ public List captureEntities() { entity -> !(entity instanceof Player) ); for (Entity entity : entities) { - CompoundTag entityNbt = new CompoundTag(); - entity.save(entityNbt); + CompoundTag entityNbt = BlueprintCapture.captureEntity(entity); + if (entityNbt == null) continue; Vec3 previewPos = worldToPreview(entity.position(), halfRangeX); BlockPos previewBlockPos = entity instanceof Painting painting ? worldBlockToPreview(painting.getPos(), halfRangeX) diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockConfiguration.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockConfiguration.java index fd24006094..7072474fbc 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockConfiguration.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockConfiguration.java @@ -23,15 +23,19 @@ import dev.dubhe.anvilcraft.block.entity.fluid.AbstractPipeCheckValveBlockEntity; import dev.dubhe.anvilcraft.block.entity.fluid.ControlValveBlockEntity; import dev.dubhe.anvilcraft.init.item.ModItems; +import dev.dubhe.anvilcraft.inventory.container.FilterOnlyContainer; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; 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.NbtUtils; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.ItemStack; @@ -48,13 +52,14 @@ 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(); + final CompoundTag runtime = BlueprintRuntimeData.take(entity, source); if (entity instanceof SignBlockEntity) { result = SignDecorationAdapter.sanitize(entity.getBlockState(), source, registries); remove(source, "front_text", "back_text", "is_waxed"); @@ -142,6 +147,7 @@ static CompoundTag take(BlockEntity entity, CompoundTag source, HolderLookup.Pro result.put("Valves", checkValves(source.getList("Valves", Tag.TAG_COMPOUND))); remove(source, "Valves", "Powered"); } + result.merge(runtime); return result; } @@ -190,7 +196,7 @@ private static void stripFiltering(CompoundTag source) { } private static CompoundTag filterSamples(CompoundTag source, int size, HolderLookup.Provider registries) { - var items = new dev.dubhe.anvilcraft.inventory.container.FilterOnlyContainer(null, size); + var items = new FilterOnlyContainer(null, size); for (Tag value : source.getList("Items", Tag.TAG_COMPOUND)) { CompoundTag tag = (CompoundTag) value; int slot = tag.getInt("Slot"); @@ -245,7 +251,7 @@ private static void enchanting(CompoundTag source, CompoundTag result, HolderLoo 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()) { + .get(ResourceKey.create(Registries.ENCHANTMENT, key)).isPresent()) { selected.add(StringTag.valueOf(id)); } } @@ -303,6 +309,12 @@ static List materials(CompoundTag config) { public static void transform( CompoundTag config, BlueprintPlacement placement, @Nullable BlockPos sourceOrigin, StructureSnapshot snapshot ) { + if ("minecraft:piston".equals(config.getString("id"))) { + var moved = NbtUtils.readBlockState(BuiltInRegistries.BLOCK.asLookup(), config.getCompound("blockState")); + config.put("blockState", NbtUtils.writeBlockState(placement.stateOf(moved))); + Direction facing = Direction.from3DDataValue(config.getInt("facing")); + config.putInt("facing", placement.rotation().rotate(placement.mirror().mirror(facing)).get3DDataValue()); + } if (config.contains("Facing")) { Direction facing = Direction.from3DDataValue(config.getInt("Facing")); config.putInt("Facing", placement.rotation().rotate(placement.mirror().mirror(facing)).get3DDataValue()); @@ -361,6 +373,12 @@ static void apply(BlockEntity entity, CompoundTag settings, ServerPlayer player) } static void afterContents(BlockEntity entity, CompoundTag settings, ServerPlayer player) { + CompoundTag runtime = BlueprintRuntimeData.take(entity, settings.copy()); + if (!runtime.isEmpty()) { + CompoundTag restored = entity.saveWithFullMetadata(player.registryAccess()).merge(runtime); + if (entity instanceof PulseGeneratorBlockEntity pulse) pulse.loadBlueprint(restored, player.registryAccess()); + else entity.loadWithComponents(restored, player.registryAccess()); + } if (entity instanceof LecternBlockEntity lectern && !lectern.getBook().isEmpty() && settings.contains("Page")) { apply(entity, settings, player); } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockEntities.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockEntities.java new file mode 100644 index 0000000000..49596ee30a --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintBlockEntities.java @@ -0,0 +1,27 @@ +package dev.dubhe.anvilcraft.building; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.piston.PistonMovingBlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; + +public final class BlueprintBlockEntities { + private BlueprintBlockEntities() { + } + + @Nullable + public static BlockEntity create(Level level, BlockPos pos, BlockState state, @Nullable CompoundTag data) { + BlockEntity entity = state.is(Blocks.MOVING_PISTON) ? new PistonMovingBlockEntity(pos, state) + : state.getBlock() instanceof EntityBlock block ? block.newBlockEntity(pos, state) : null; + if (entity == null) return null; + if (data != null) entity.loadWithComponents(data, level.registryAccess()); + entity.setLevel(level); + return entity; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintCapture.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintCapture.java new file mode 100644 index 0000000000..c7f859af4c --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintCapture.java @@ -0,0 +1,73 @@ +package dev.dubhe.anvilcraft.building; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.decoration.Painting; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; + +public final class BlueprintCapture { + private BlueprintCapture() { + } + + @Nullable + public static CompoundTag captureEntity(Entity entity) { + CompoundTag tag = new CompoundTag(); + if (!BlueprintLeashes.save(entity, tag)) return null; + tag.remove("Passengers"); + if (entity.getVehicle() != null) tag.putUUID("anvilcraft:vehicle", entity.getVehicle().getUUID()); + return tag; + } + + public static BlueprintNormalizer.Result capture(ServerLevel level, AABB area) { + BlockPos origin = BlockPos.containing(area.minX, area.minY, area.minZ); + BlockPos end = BlockPos.containing(area.maxX - 1, area.maxY - 1, area.maxZ - 1); + Map blocks = new LinkedHashMap<>(); + List palette = new ArrayList<>(); + for (BlockPos cursor : BlockPos.betweenClosed(origin, end)) { + BlockPos pos = cursor.immutable(); + BlockState state = level.getBlockState(pos); + if (state.isAir() || !BlueprintMultiblocks.shouldRecord(state)) continue; + BlueprintMultiblocks.forEachPart(pos, state, (part, generated) -> { + BlockState actual = level.getBlockState(part); + if (actual.isAir()) actual = generated; + int index = palette.indexOf(actual); + if (index < 0) { + index = palette.size(); + palette.add(actual); + } + var entity = level.getBlockEntity(part); + blocks.put(part, new StructureSnapshot.BlockEntry(part.subtract(origin), index, + Optional.ofNullable(entity).map(value -> value.saveWithFullMetadata(level.registryAccess())))); + }); + } + List entities = new ArrayList<>(); + for (Entity entity : level.getEntities((Entity) null, area, entity -> !(entity instanceof Player))) { + CompoundTag tag = captureEntity(entity); + if (tag == null) continue; + Vec3 pos = entity.position().subtract(Vec3.atLowerCornerOf(origin)); + BlockPos block = entity instanceof Painting painting ? painting.getPos().subtract(origin) : BlockPos.containing(pos); + entities.add(new StructureSnapshot.EntityEntry(pos, block, tag)); + } + List ticks = BlueprintTicks.capture(level, BoundingBox.fromCorners(origin, end)).stream() + .map(tick -> tick.at(tick.pos().subtract(origin))).toList(); + StructureSnapshot raw = new StructureSnapshot(new Vec3i(end.getX() - origin.getX() + 1, + end.getY() - origin.getY() + 1, end.getZ() - origin.getZ() + 1), palette, new ArrayList<>(blocks.values()), + entities, ticks, level.getGameTime()); + return BlueprintNormalizer.normalize(ScannerDiskNormalizer.normalize(raw, Direction.NORTH, false)); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintEntities.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintEntities.java new file mode 100644 index 0000000000..e7e4f54741 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintEntities.java @@ -0,0 +1,227 @@ +package dev.dubhe.anvilcraft.building; + +import dev.dubhe.anvilcraft.block.item.HasMobBlockItem; +import dev.dubhe.anvilcraft.block.item.ResinBlockItem; +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.phys.Vec3; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import javax.annotation.Nullable; + +final class BlueprintEntities { + private static final double MAX_SURVIVAL_SPEED = 15.9; + static final String SOURCE = "anvilcraft:source_entity"; + static final String VEHICLE = "anvilcraft:vehicle"; + + private BlueprintEntities() { + } + + @Nullable + static EntityBuildAdapter.Planned fromMaterial(ServerPlayer player, ItemStack material, EntityBuildAdapter.Planned plan) { + Entity entity; + if (material.getItem() instanceof ResinBlockItem) { + entity = HasMobBlockItem.getMobFromItem(player.level(), material); + if (entity != null && material.has(DataComponents.CUSTOM_NAME)) { + entity.setCustomName(material.get(DataComponents.CUSTOM_NAME)); + if (entity instanceof Mob mob) mob.setPersistenceRequired(); + } + } else if (material.getItem() instanceof SpawnEggItem egg) { + entity = egg.getType(material).create(player.level()); + if (entity instanceof Mob mob) { + var pos = plan.entityNbt().getList("Pos", Tag.TAG_DOUBLE); + mob.setPos(pos.getDouble(0), pos.getDouble(1), pos.getDouble(2)); + mob.finalizeSpawn(player.serverLevel(), player.level().getCurrentDifficultyAt(mob.blockPosition()), + MobSpawnType.SPAWN_EGG, null); + if (mob.isSpawnCancelled()) return null; + } + if (entity != null) EntityType.createDefaultStackConfig(player.serverLevel(), material, player).accept(entity); + } else { + return null; + } + if (!(entity instanceof Mob) || entity.getType() != EntityType.by(plan.entityNbt()).orElse(null)) return null; + CompoundTag data = new CompoundTag(); + if (!entity.saveAsPassenger(data)) return null; + data.remove("UUID"); + data.remove("Passengers"); + data.remove("Leash"); + // 只从蓝图继承空间状态和已付费的连接,生物属性与物品完全来自实际材料。 + for (String key : List.of("Pos", "Rotation", "Motion", SOURCE, VEHICLE, "leash")) { + data.remove(key); + Tag value = plan.entityNbt().get(key); + if (value != null) data.put(key, value.copy()); + } + return new EntityBuildAdapter.Planned(plan.material(), plan.returned(), data, List.of(), List.of(), false); + } + + private static Vec3 motion(CompoundTag data) { + ListTag motion = data.getList("Motion", Tag.TAG_DOUBLE); + if (motion.size() != 3) return Vec3.ZERO; + double x = motion.getDouble(0); + double y = motion.getDouble(1); + double z = motion.getDouble(2); + return Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z) ? new Vec3(x, y, z) : Vec3.ZERO; + } + + static void limitMotion(CompoundTag data) { + Vec3 velocity = motion(data); + double largest = Math.max(Math.abs(velocity.x), Math.max(Math.abs(velocity.y), Math.abs(velocity.z))); + if (largest > 0) { + Vec3 scaled = new Vec3(velocity.x / largest, velocity.y / largest, velocity.z / largest); + double length = scaled.length(); + if (largest > MAX_SURVIVAL_SPEED / length) velocity = scaled.scale(MAX_SURVIVAL_SPEED / length); + } + ListTag motion = new ListTag(); + motion.add(DoubleTag.valueOf(velocity.x)); + motion.add(DoubleTag.valueOf(velocity.y)); + motion.add(DoubleTag.valueOf(velocity.z)); + data.put("Motion", motion); + } + + static void restoreMotion(Entity entity, CompoundTag data) { + // Entity.load 会清零绝对值大于 10 的速度分量;蓝图已在材料分配阶段统一限速。 + entity.setDeltaMovement(motion(data)); + } + + static void relocateMemories(CompoundTag data, StructureSnapshot.EntityEntry entry, + StructureSnapshot snapshot, BlueprintPlacement placement, String dimension) { + BlockPos origin = BlueprintBlockConfiguration.sourceOrigin(snapshot); + if (origin == null) { + ListTag saved = entry.nbt().getList("Pos", Tag.TAG_DOUBLE); + origin = saved.size() == 3 ? new BlockPos( + (int) Math.round(saved.getDouble(0) - entry.pos().x), + (int) Math.round(saved.getDouble(1) - entry.pos().y), + (int) Math.round(saved.getDouble(2) - entry.pos().z)) : BlockPos.ZERO; + } + BlueprintLeashes.transform(data, origin, placement); + CompoundTag memories = data.getCompound("Brain").getCompound("memories"); + for (String key : List.copyOf(memories.getAllKeys())) { + CompoundTag value = memories.getCompound(key).getCompound("value"); + var pos = NbtUtils.readBlockPos(value, "pos"); + if (pos.isEmpty()) continue; + BlockPos local = pos.get().subtract(origin); + 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()) { + memories.remove(key); + } else { + value.put("pos", NbtUtils.writeBlockPos(placement.worldOf(local))); + value.putString("dimension", dimension); + } + } + } + + static List flatten(List entries) { + List result = new ArrayList<>(); + Map seen = new HashMap<>(); + for (int i = 0; i < entries.size(); i++) flatten(entries.get(i), result, seen, "entity:" + i, 0); + return result; + } + + private static void flatten(StructureSnapshot.EntityEntry entry, List result, + Map seen, String path, int depth) { + if (depth > 32 || result.size() >= StructureSnapshotCodec.MAX_ENTITY_ENTRIES) { + throw new IllegalArgumentException("Too many blueprint passengers"); + } + CompoundTag tag = entry.nbt().copy(); + if (!tag.hasUUID("UUID")) { + tag.putUUID("UUID", UUID.nameUUIDFromBytes((path + entry.pos() + tag).getBytes(StandardCharsets.UTF_8))); + } + CompoundTag existing = seen.putIfAbsent(tag.getUUID("UUID"), tag); + if (existing != null) { + if (tag.hasUUID(VEHICLE)) existing.putUUID(VEHICLE, tag.getUUID(VEHICLE)); + return; + } + ListTag passengers = tag.getList("Passengers", Tag.TAG_COMPOUND); + tag.remove("Passengers"); + result.add(new StructureSnapshot.EntityEntry(entry.pos(), entry.blockPos(), tag)); + ListTag savedPos = entry.nbt().getList("Pos", Tag.TAG_DOUBLE); + Vec3 offset = savedPos.size() == 3 + ? entry.pos().subtract(new Vec3(savedPos.getDouble(0), savedPos.getDouble(1), savedPos.getDouble(2))) : Vec3.ZERO; + for (int i = 0; i < passengers.size(); i++) { + CompoundTag passenger = passengers.getCompound(i).copy(); + ListTag pos = passenger.getList("Pos", Tag.TAG_DOUBLE); + Vec3 local = pos.size() == 3 + ? new Vec3(pos.getDouble(0), pos.getDouble(1), pos.getDouble(2)).add(offset) : entry.pos(); + passenger.putUUID(VEHICLE, tag.getUUID("UUID")); + flatten(new StructureSnapshot.EntityEntry(local, BlockPos.containing(local), passenger), + result, seen, path + "/" + i, depth + 1); + } + } + + static Map identities(StructureSnapshot snapshot, BlueprintPlacement placement) { + Map ids = new HashMap<>(); + for (var entry : snapshot.entities()) { + if (!entry.nbt().hasUUID("UUID")) continue; + UUID old = entry.nbt().getUUID("UUID"); + ids.put(old, UUID.nameUUIDFromBytes((old + ":" + placement.anchor().toShortString()).getBytes(StandardCharsets.UTF_8))); + } + return ids; + } + + static void scope(CompoundTag target, CompoundTag original, Map ids) { + remap(target, ids); + if (original.hasUUID("UUID")) target.putUUID(SOURCE, ids.get(original.getUUID("UUID"))); + if (original.hasUUID(VEHICLE)) { + UUID vehicle = ids.get(original.getUUID(VEHICLE)); + if (vehicle != null) target.putUUID(VEHICLE, vehicle); + } + } + + static List link(List> spawned) { + Map ids = new HashMap<>(); + Map entities = new HashMap<>(); + for (var pair : spawned) { + if (!pair.getValue().hasUUID(SOURCE)) continue; + UUID source = pair.getValue().getUUID(SOURCE); + ids.put(source, pair.getKey().getUUID()); + entities.put(source, pair.getKey()); + } + for (var pair : spawned) { + Entity entity = pair.getKey(); + CompoundTag saved = new CompoundTag(); + BlueprintLeashes.save(entity, saved); + remap(saved, ids); + entity.load(saved); + restoreMotion(entity, saved); + CompoundTag original = pair.getValue(); + if (!original.hasUUID(VEHICLE)) continue; + Entity vehicle = entities.get(original.getUUID(VEHICLE)); + if (vehicle != null && vehicle != entity) entity.startRiding(vehicle, true); + } + return BlueprintLeashes.link(spawned.stream().map(Map.Entry::getKey).toList()); + } + + private static void remap(CompoundTag tag, Map ids) { + for (String key : List.copyOf(tag.getAllKeys())) { + Tag value = tag.get(key); + if (value instanceof IntArrayTag array && array.getAsIntArray().length == 4) { + UUID target = ids.get(NbtUtils.loadUUID(array)); + if (target != null) tag.putUUID(key, target); + } else if (value instanceof CompoundTag compound) { + remap(compound, ids); + } else if (value instanceof ListTag list) { + for (Tag element : list) { + if (element instanceof CompoundTag compound) remap(compound, ids); + } + } + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintIgnition.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintIgnition.java new file mode 100644 index 0000000000..f2b06857fd --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintIgnition.java @@ -0,0 +1,46 @@ +package dev.dubhe.anvilcraft.building; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.BaseFireBlock; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.NetherPortalBlock; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.ArrayDeque; +import java.util.LinkedHashMap; +import java.util.Map; + +final class BlueprintIgnition { + private BlueprintIgnition() { + } + + static boolean isIgnition(BlockState state) { + return state.getBlock() instanceof BaseFireBlock || state.is(Blocks.NETHER_PORTAL); + } + + static Map portalCores(Map blocks) { + Map cores = new LinkedHashMap<>(); + for (var entry : blocks.entrySet()) { + if (!entry.getValue().is(Blocks.NETHER_PORTAL) || cores.containsKey(entry.getKey())) continue; + BlockPos core = entry.getKey(); + Direction.Axis axis = entry.getValue().getValue(NetherPortalBlock.AXIS); + ArrayDeque pending = new ArrayDeque<>(); + cores.put(core, core); + pending.add(core); + while (!pending.isEmpty()) { + BlockPos pos = pending.removeFirst(); + for (Direction direction : Direction.values()) { + if (direction.getAxis() != Direction.Axis.Y && direction.getAxis() != axis) continue; + BlockPos next = pos.relative(direction); + BlockState state = blocks.get(next); + if (cores.containsKey(next) || state == null || !state.is(Blocks.NETHER_PORTAL) + || state.getValue(NetherPortalBlock.AXIS) != axis) continue; + cores.put(next, core); + pending.addLast(next); + } + } + } + return cores; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintLeashes.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintLeashes.java new file mode 100644 index 0000000000..5b2ea595f9 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintLeashes.java @@ -0,0 +1,94 @@ +package dev.dubhe.anvilcraft.building; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Leashable; +import net.minecraft.world.entity.decoration.LeashFenceKnotEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +final class BlueprintLeashes { + private BlueprintLeashes() { + } + + static Optional fence(CompoundTag tag) { + Optional current = NbtUtils.readBlockPos(tag, Leashable.LEASH_TAG); + if (current.isPresent()) return current; + CompoundTag legacy = tag.getCompound("Leash"); + return legacy.contains("X", Tag.TAG_ANY_NUMERIC) + ? Optional.of(new BlockPos(legacy.getInt("X"), legacy.getInt("Y"), legacy.getInt("Z"))) : Optional.empty(); + } + + static boolean hasLeash(CompoundTag tag) { + return fence(tag).isPresent() || tag.getCompound(Leashable.LEASH_TAG).hasUUID("UUID") + || tag.getCompound("Leash").hasUUID("UUID"); + } + + static Optional attachment(CompoundTag tag) { + if (!"minecraft:leash_knot".equals(tag.getString("id"))) return fence(tag); + var pos = tag.getList("Pos", Tag.TAG_DOUBLE); + return pos.size() == 3 ? Optional.of(BlockPos.containing(pos.getDouble(0), pos.getDouble(1), pos.getDouble(2))) + : Optional.empty(); + } + + static void transform(CompoundTag tag, BlockPos origin, BlueprintPlacement placement) { + Optional fence = fence(tag); + if (fence.isPresent()) { + tag.put(Leashable.LEASH_TAG, NbtUtils.writeBlockPos(placement.worldOf(fence.get().subtract(origin)))); + tag.remove("Leash"); + } else if (tag.getCompound("Leash").hasUUID("UUID")) { + tag.put(Leashable.LEASH_TAG, tag.getCompound("Leash").copy()); + tag.remove("Leash"); + } + } + + static boolean save(Entity entity, CompoundTag tag) { + if (entity.saveAsPassenger(tag)) return true; + if (!(entity instanceof LeashFenceKnotEntity)) return false; + entity.saveWithoutId(tag); + tag.putString("id", EntityType.getKey(entity.getType()).toString()); + return true; + } + + @Nullable + static Entity create(CompoundTag tag, Level level) { + if (!"minecraft:leash_knot".equals(tag.getString("id"))) return EntityType.create(tag, level).orElse(null); + var position = tag.getList("Pos", Tag.TAG_DOUBLE); + if (position.size() != 3) return null; + BlockPos pos = BlockPos.containing(position.getDouble(0), position.getDouble(1), position.getDouble(2)); + LeashFenceKnotEntity knot = new LeashFenceKnotEntity(level, pos); + knot.load(tag); + knot.setPos(pos.getX(), pos.getY(), pos.getZ()); + return knot; + } + + static List link(List entities) { + List created = new ArrayList<>(); + for (Entity entity : entities) { + if (!(entity instanceof Leashable leashable) || !(entity.level() instanceof ServerLevel level)) continue; + var data = leashable.getLeashData(); + if (data == null || data.delayedLeashInfo == null) continue; + Entity holder = data.delayedLeashInfo.left().map(level::getEntity).orElse(null); + Optional pos = data.delayedLeashInfo.right(); + if (holder == null && pos.isPresent() && level.getBlockState(pos.get()).is(BlockTags.FENCES)) { + boolean existing = !level.getEntities(EntityType.LEASH_KNOT, new AABB(pos.get()).inflate(1), + knot -> pos.get().equals(knot.getPos())).isEmpty(); + holder = LeashFenceKnotEntity.getOrCreateKnot(level, pos.get()); + if (!existing) created.add(holder); + } + if (holder != null) leashable.setLeashedTo(holder, true); + } + return created; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintNormalizer.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintNormalizer.java index 002fd0b7ab..5a32b382af 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintNormalizer.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintNormalizer.java @@ -51,9 +51,6 @@ public static Result normalize(StructureSnapshot snapshot) { Map original = new LinkedHashMap<>(); for (var entry : snapshot.blocks()) { BlockState state = snapshot.stateOf(entry); - if (state.getBlock() instanceof MovingPistonBlock) { - throw new IllegalArgumentException("Moving piston at " + entry.pos().toShortString()); - } if (OrdinaryBlockAdapter.mapping(state) != OrdinaryBlockAdapter.Mapping.AIR) { original.put(entry.pos(), new BlueprintMultiblocks.PlacedBlock(entry.pos(), state, entry.nbt())); } @@ -63,6 +60,8 @@ public static Result normalize(StructureSnapshot snapshot) { if (!BlueprintMultiblocks.shouldRecord(entry.state())) continue; BlueprintMultiblocks.forEachPart(entry.pos(), entry.state(), (pos, generated) -> { var previous = original.get(pos); + if (previous != null && previous.state().getBlock() instanceof MovingPistonBlock + && !pos.equals(entry.pos())) return; if (complete.containsKey(pos) || previous != null && !pos.equals(entry.pos()) && (BlueprintMultiblocks.shouldRecord(previous.state()) || !sameFamily(previous.state(), generated))) { throw new IllegalArgumentException("Overlapping blueprint parts at " + pos.toShortString()); @@ -113,7 +112,8 @@ public static Result normalize(StructureSnapshot snapshot) { entry.nbt().copy())).toList(); int added = (int) complete.keySet().stream().filter(pos -> !original.containsKey(pos)).count(); int removed = (int) original.keySet().stream().filter(pos -> !complete.containsKey(pos)).count(); - return new Result(StructureSnapshotCodec.canonicalize(new StructureSnapshot(size, palette, blocks, entities)), + return new Result(StructureSnapshotCodec.canonicalize(new StructureSnapshot(size, palette, blocks, entities, + snapshot.ticks().stream().map(tick -> tick.at(tick.pos().offset(offset))).toList(), snapshot.capturedAt())), offset, added, removed); } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintPlacement.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintPlacement.java index d782046f76..3d42e76b14 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintPlacement.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintPlacement.java @@ -10,6 +10,9 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; import net.minecraft.world.phys.Vec3; +import java.util.ArrayList; +import java.util.List; + /** * 蓝图放置参数与坐标变换。快照局部坐标以零点为枢轴按原版 {@link StructureTemplate} * 语义镜像和旋转,再平移到锚点;方块状态使用原版 mirror/rotate,保证与最终提交一致。 @@ -112,4 +115,32 @@ public BoundingBox bounds(Vec3i size) { BlockPos second = this.worldOf(new BlockPos(size.getX() - 1, size.getY() - 1, size.getZ() - 1)); return BoundingBox.fromCorners(first, second); } + + public List tile(StructureSnapshot snapshot, BlockPos last) { + BoundingBox bounds = this.bounds(snapshot.size()); + int width = bounds.getXSpan(); + int height = bounds.getYSpan(); + int depth = bounds.getZSpan(); + long countX = Math.abs((long) last.getX() - this.anchor.getX()) / width + 1; + long countY = Math.abs((long) last.getY() - this.anchor.getY()) / height + 1; + long countZ = Math.abs((long) last.getZ() - this.anchor.getZ()) / depth + 1; + if (countX > BuildingRodService.MAX_BLOCKS || countY > BuildingRodService.MAX_BLOCKS + || countZ > BuildingRodService.MAX_BLOCKS) return List.of(); + long count = countX * countY * countZ; + long entries = Math.max(1L, (long) snapshot.nonAirBlockCount() + snapshot.entities().size()); + if (count > 1 && count * entries > BuildingRodService.MAX_BLOCKS) return List.of(); + int signX = Integer.compare(last.getX(), this.anchor.getX()); + int signY = Integer.compare(last.getY(), this.anchor.getY()); + int signZ = Integer.compare(last.getZ(), this.anchor.getZ()); + List placements = new ArrayList<>((int) count); + for (int x = 0; x < countX; x++) { + for (int y = 0; y < countY; y++) { + for (int z = 0; z < countZ; z++) { + placements.add(new BlueprintPlacement(this.anchor.offset( + x * width * signX, y * height * signY, z * depth * signZ), this.rotation, this.mirror)); + } + } + } + return List.copyOf(placements); + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintRuntimeData.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintRuntimeData.java new file mode 100644 index 0000000000..2df7b5ad7a --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintRuntimeData.java @@ -0,0 +1,75 @@ +package dev.dubhe.anvilcraft.building; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.piston.PistonMovingBlockEntity; + +import java.util.List; + +final class BlueprintRuntimeData { + private static final List FIELDS = List.of( + "Cooldown", "TransferCooldown", "TimeLeft", "TimeTotalCache", "StartupCoolDown", "cd", "CooldownTicks", + "PoweredBefore", "OutputSignal", "Powered", "crafting_ticks_remaining", "triggered", "ticks_since_song_started", + "Rolling", "PreviousFaces", "Faces", "Output", "RollStart", "phase", "progress", "currentPlacementIndex" + ); + + private BlueprintRuntimeData() { + } + + static CompoundTag take(BlockEntity entity, CompoundTag source) { + CompoundTag runtime = new CompoundTag(); + for (String key : FIELDS) move(source, runtime, key); + if (entity instanceof AbstractFurnaceBlockEntity) { + for (String key : List.of("BurnTime", "CookTime", "CookTimeTotal")) move(source, runtime, key); + } + if (source.contains("ExtraData", Tag.TAG_COMPOUND)) { + CompoundTag extra = source.getCompound("ExtraData"); + CompoundTag kept = new CompoundTag(); + for (String key : List.of("State", "Inputting", "PhaseStartGameTime", "PhaseDuration", + "RemainingWaitingTime", "RemainingSignalDuration")) { + move(extra, kept, key); + } + if (!kept.isEmpty()) runtime.put("ExtraData", kept); + } + if (entity instanceof PistonMovingBlockEntity) { + for (String key : List.of("blockState", "facing", "progress", "extending", "source")) move(source, runtime, key); + } + return runtime; + } + + private static void move(CompoundTag source, CompoundTag target, String key) { + Tag value = source.get(key); + if (value != null) target.put(key, value.copy()); + source.remove(key); + } + + static void rebase(CompoundTag config, long capturedAt, long now) { + for (String key : List.of("PhaseStartGameTime", "RollStart")) { + if (config.contains(key, Tag.TAG_ANY_NUMERIC) && config.getLong(key) >= 0) { + config.putLong(key, capturedAt > 0 ? now + config.getLong(key) - capturedAt : now); + } + } + if (config.contains("ExtraData", Tag.TAG_COMPOUND)) rebase(config.getCompound("ExtraData"), capturedAt, now); + } + + static void restoreClock(CompoundTag config, StructureSnapshot snapshot, BlockPos local, long now) { + rebase(config, snapshot.capturedAt(), now); + CompoundTag extra = config.getCompound("ExtraData"); + if (extra.isEmpty()) return; + int remaining = Math.max(extra.getInt("RemainingWaitingTime"), extra.getInt("RemainingSignalDuration")); + if (snapshot.capturedAt() == 0) { + for (var tick : snapshot.ticks()) { + if (!tick.fluid() && tick.pos().equals(local)) remaining = Math.max(1, tick.delay()); + } + } + if (remaining > 0 && (snapshot.capturedAt() == 0 || !extra.contains("PhaseStartGameTime"))) { + int duration = Math.max(remaining, extra.getInt("PhaseDuration")); + long start = now - (duration - remaining); + extra.putLong("PhaseStartGameTime", Math.max(0, start)); + extra.putInt("PhaseDuration", start >= 0 ? duration : remaining); + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintSpecialBlocks.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintSpecialBlocks.java new file mode 100644 index 0000000000..fbe9297529 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintSpecialBlocks.java @@ -0,0 +1,51 @@ +package dev.dubhe.anvilcraft.building; + +import dev.dubhe.anvilcraft.init.block.ModBlocks; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.CandleCakeBlock; +import net.minecraft.world.level.block.EndPortalFrameBlock; +import net.minecraft.world.level.block.FlowerPotBlock; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.List; + +final class BlueprintSpecialBlocks { + private BlueprintSpecialBlocks() { + } + + static ItemStack material(BlockState state) { + if (state.is(ModBlocks.SIMPLE_MAGNETIC_CHUTE.get())) return ModBlocks.MAGNETIC_CHUTE.asStack(); + if (state.getBlock() instanceof FlowerPotBlock) return new ItemStack(Items.FLOWER_POT); + if (state.getBlock() instanceof CandleCakeBlock) return new ItemStack(Items.CAKE); + if (state.is(Blocks.ATTACHED_PUMPKIN_STEM)) return new ItemStack(Items.PUMPKIN_SEEDS); + if (state.is(Blocks.ATTACHED_MELON_STEM)) return new ItemStack(Items.MELON_SEEDS); + if (state.is(Blocks.TALL_SEAGRASS)) return new ItemStack(Items.SEAGRASS); + if (state.is(Blocks.KELP_PLANT)) return new ItemStack(Items.KELP); + if (state.is(Blocks.BAMBOO_SAPLING)) return new ItemStack(Items.BAMBOO); + if (state.is(Blocks.WEEPING_VINES_PLANT)) return new ItemStack(Items.WEEPING_VINES); + if (state.is(Blocks.TWISTING_VINES_PLANT)) return new ItemStack(Items.TWISTING_VINES); + if (state.is(Blocks.CAVE_VINES_PLANT)) return new ItemStack(Items.GLOW_BERRIES); + if (state.is(Blocks.FROSTED_ICE)) return new ItemStack(Items.ICE); + return ItemStack.EMPTY; + } + + static List extra(BlockState state) { + if (state.getBlock() instanceof FlowerPotBlock pot && pot.getPotted() != Blocks.AIR) { + return List.of(new ItemStack(pot.getPotted())); + } + if (state.getBlock() instanceof CandleCakeBlock) { + ResourceLocation id = BuiltInRegistries.BLOCK.getKey(state.getBlock()); + ResourceLocation candle = id.withPath(id.getPath().replace("_cake", "")); + return List.of(new ItemStack(BuiltInRegistries.ITEM.get(candle))); + } + if (state.is(Blocks.TALL_SEAGRASS)) return List.of(new ItemStack(Items.BONE_MEAL)); + if (state.is(Blocks.END_PORTAL_FRAME) && state.getValue(EndPortalFrameBlock.HAS_EYE)) { + return List.of(new ItemStack(Items.ENDER_EYE)); + } + return List.of(); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BlueprintTicks.java b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintTicks.java new file mode 100644 index 0000000000..00231a3e02 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BlueprintTicks.java @@ -0,0 +1,106 @@ +package dev.dubhe.anvilcraft.building; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.ticks.LevelChunkTicks; +import net.minecraft.world.ticks.TickPriority; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +public final class BlueprintTicks { + public static final String KEY = "anvilcraft:scheduled_ticks"; + + private BlueprintTicks() { + } + + public record Entry(BlockPos pos, ResourceLocation type, int delay, int priority, boolean fluid, long order) { + public Entry(BlockPos pos, ResourceLocation type, int delay, int priority, boolean fluid) { + this(pos, type, delay, priority, fluid, 0); + } + + public Entry at(BlockPos target) { + return new Entry(target, this.type, this.delay, this.priority, this.fluid, this.order); + } + } + + public static List read(CompoundTag tag) { + ListTag saved = tag.getList(KEY, Tag.TAG_COMPOUND); + if (saved.size() > 32768) throw new IllegalArgumentException("Too many scheduled blueprint ticks"); + List ticks = new ArrayList<>(); + for (Tag value : saved) { + CompoundTag entry = (CompoundTag) value; + BlockPos pos = NbtUtils.readBlockPos(entry, "pos").orElseThrow(); + ResourceLocation type = ResourceLocation.parse(entry.getString("type")); + boolean fluid = entry.getBoolean("fluid"); + if (!(fluid ? BuiltInRegistries.FLUID.containsKey(type) : BuiltInRegistries.BLOCK.containsKey(type))) { + throw new IllegalArgumentException("Unknown scheduled tick type " + type); + } + ticks.add(new Entry(pos, type, Math.max(0, entry.getInt("delay")), entry.getInt("priority"), fluid, entry.getLong("order"))); + } + return ticks; + } + + public static void write(CompoundTag tag, List ticks) { + ListTag entries = new ListTag(); + for (Entry tick : ticks) { + CompoundTag entry = new CompoundTag(); + entry.put("pos", NbtUtils.writeBlockPos(tick.pos())); + entry.putString("type", tick.type().toString()); + entry.putInt("delay", tick.delay()); + entry.putInt("priority", tick.priority()); + entry.putBoolean("fluid", tick.fluid()); + entry.putLong("order", tick.order()); + entries.add(entry); + } + tag.put(KEY, entries); + } + + public static List capture(ServerLevel level, BoundingBox bounds) { + List ticks = new ArrayList<>(); + for (int x = bounds.minX() >> 4; x <= bounds.maxX() >> 4; x++) { + for (int z = bounds.minZ() >> 4; z <= bounds.maxZ() >> 4; z++) { + var chunk = level.getChunkAt(new BlockPos(x << 4, 0, z << 4)); + ((LevelChunkTicks) chunk.getBlockTicks()).getAll() + .filter(tick -> bounds.isInside(tick.pos())).forEach(tick -> ticks.add(new Entry( + tick.pos(), BuiltInRegistries.BLOCK.getKey(tick.type()), + (int) Math.clamp(tick.triggerTick() - level.getGameTime(), 0, Integer.MAX_VALUE), + tick.priority().getValue(), false, tick.subTickOrder()))); + ((LevelChunkTicks) chunk.getFluidTicks()).getAll() + .filter(tick -> bounds.isInside(tick.pos())).forEach(tick -> ticks.add(new Entry( + tick.pos(), BuiltInRegistries.FLUID.getKey(tick.type()), + (int) Math.clamp(tick.triggerTick() - level.getGameTime(), 0, Integer.MAX_VALUE), + tick.priority().getValue(), true, tick.subTickOrder()))); + } + } + return ticks; + } + + public static void restore(ServerLevel level, List ticks, Set placed) { + for (Entry tick : ticks.stream().sorted(Comparator.comparingLong(Entry::order)).toList()) { + if (!placed.contains(tick.pos())) continue; + if (tick.fluid()) { + var fluid = BuiltInRegistries.FLUID.get(tick.type()); + if (level.getFluidState(tick.pos()).getType() == fluid) { + level.scheduleTick(tick.pos(), fluid, tick.delay(), TickPriority.byValue(tick.priority())); + } + } else { + var block = BuiltInRegistries.BLOCK.get(tick.type()); + if (level.getBlockState(tick.pos()).is(block)) { + level.scheduleTick(tick.pos(), block, tick.delay(), TickPriority.byValue(tick.priority())); + } + } + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingBlockMaterial.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingBlockMaterial.java index adc44df799..e7c4a2f848 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingBlockMaterial.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingBlockMaterial.java @@ -29,6 +29,8 @@ record BuildingBlockMaterial( static BuildingBlockMaterial extract(BlockState state, @Nullable CompoundTag nbt, Level level) { final HolderLookup.Provider registries = level.registryAccess(); ItemStack stack = ProcessingTablePlacement.baseMaterial(state); + ItemStack special = BlueprintSpecialBlocks.material(state); + if (!special.isEmpty()) stack = special; BlockEntity entity = createEntity(state); if (entity == null) return new BuildingBlockMaterial(stack, new CompoundTag(), List.of(), false); boolean restricted = entity.onlyOpCanSetNbt() @@ -81,9 +83,6 @@ static BuildingBlockMaterial extract(BlockState state, @Nullable CompoundTag nbt 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); } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingCommit.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingCommit.java index 4775a7e6ed..e8d280b23c 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingCommit.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingCommit.java @@ -2,22 +2,35 @@ import dev.dubhe.anvilcraft.block.RedstoneWireBlock; import dev.dubhe.anvilcraft.block.RedstoneWireNetworkManager; +import dev.dubhe.anvilcraft.block.entity.PulseGeneratorBlockEntity; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.ObserverBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** 移植悦灵建造的静默提交,全部状态就位后再恢复方块实体,避免红石和多方块中途更新。 */ public final class BuildingCommit { private static final ThreadLocal QUIET_LEVEL = new ThreadLocal<>(); + private static final ThreadLocal ACTIVATION = new ThreadLocal<>(); + + private record Activation(Level level, Map states) { + } private BuildingCommit() { } @@ -43,6 +56,64 @@ public static Level quietLevel() { return QUIET_LEVEL.get(); } + public static void activate(ServerLevel level, List cells, List ticks) { + Map states = cells.stream() + .collect(Collectors.toMap(BuildingRodService.Cell::pos, BuildingRodService.Cell::state)); + Activation previous = ACTIVATION.get(); + ACTIVATION.set(new Activation(level, states)); + try { + activatePlaced(level, cells, ticks, states.keySet()); + } finally { + if (previous == null) ACTIVATION.remove(); + else ACTIVATION.set(previous); + } + } + + public static boolean isRestoredObserverUpdate(Level level, Direction direction, BlockState source, + BlockPos pos, BlockPos sourcePos) { + Activation activation = ACTIVATION.get(); + if (activation == null || activation.level() != level) return false; + BlockState observer = activation.states().get(pos); + return observer != null && observer.getBlock() instanceof ObserverBlock + && observer.getValue(ObserverBlock.FACING) == direction && level.getBlockState(pos) == observer + && activation.states().get(sourcePos) == source && level.getBlockState(sourcePos) == source; + } + + private static void activatePlaced(ServerLevel level, List cells, + List ticks, Set placed) { + for (BlockPos pos : placed) { + BoundingBox bounds = new BoundingBox(pos); + level.getBlockTicks().clearArea(bounds); + level.getFluidTicks().clearArea(bounds); + } + BlueprintTicks.restore(level, ticks, placed); + for (var cell : cells) { + BlockPos pos = cell.pos(); + BlockState state = level.getBlockState(pos); + state.onPlace(level, pos, Blocks.AIR.defaultBlockState(), false); + if (level.getBlockEntity(pos) instanceof PulseGeneratorBlockEntity pulse + && pulse.isProcessing() && !level.getBlockTicks().hasScheduledTick(pos, state.getBlock())) { + level.scheduleTick(pos, state.getBlock(), Math.max(1, pulse.getPhaseRemainingTicks())); + } + if (!state.getFluidState().isEmpty()) { + var fluid = state.getFluidState().getType(); + level.scheduleTick(pos, fluid, fluid.getTickDelay(level)); + } + } + for (var cell : cells) { + BlockPos pos = cell.pos(); + BlockState state = level.getBlockState(pos); + state.updateNeighbourShapes(level, pos, Block.UPDATE_ALL); + state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_ALL); + level.updateNeighborsAt(pos, state.getBlock()); + level.updateNeighbourForOutputSignal(pos, state.getBlock()); + for (var direction : Direction.values()) { + BlockPos neighbor = pos.relative(direction); + level.neighborChanged(pos, level.getBlockState(neighbor).getBlock(), neighbor); + } + } + } + public static void set(Level level, BlockPos pos, BlockState state) { if (!level.isInWorldBounds(pos)) return; BlockState previous = level.getBlockState(pos); @@ -80,6 +151,8 @@ public static void set(Level level, BlockPos pos, BlockState state) { } chunk.setUnsaved(true); if (level instanceof ServerLevel serverLevel) { + serverLevel.onBlockStateChange(pos, previous, state); + state.onBlockStateChange(level, pos, previous); serverLevel.getChunkSource().getLightEngine().checkBlock(pos); serverLevel.sendBlockUpdated(pos, previous, state, Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_SUPPRESS_DROPS); diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingEntityTransform.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingEntityTransform.java index 04dfbdcd85..b91745a95f 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingEntityTransform.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingEntityTransform.java @@ -2,20 +2,17 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; import net.minecraft.nbt.FloatTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; 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.level.EmptyBlockGetter; import net.minecraft.world.phys.Vec3; -import java.util.ArrayList; import java.util.List; public final class BuildingEntityTransform { @@ -27,6 +24,14 @@ public static CompoundTag transform(StructureSnapshot.EntityEntry entry, Bluepri placement.anchor().getX(), placement.anchor().getY(), placement.anchor().getZ()); CompoundTag tag = EntityBuildAdapters.withWorldPos(entry.nbt(), world); BlockPos block = placement.worldOf(entry.blockPos()); + if (MagnetizedNodeBuildAdapter.isNode(tag)) { + tag.put("BlockPos", NbtUtils.writeBlockPos(block)); + var state = NbtUtils.readBlockState(BuiltInRegistries.BLOCK.asLookup(), tag.getCompound("BlockState")); + state = placement.stateOf(state); + tag.put("BlockState", NbtUtils.writeBlockState(state)); + double height = state.getCollisionShape(EmptyBlockGetter.INSTANCE, block).max(Direction.Axis.Y, 0.5, 0.5); + tag = EntityBuildAdapters.withWorldPos(tag, Vec3.atBottomCenterOf(block).add(0, height, 0)); + } if (tag.contains("TileX")) { tag.putInt("TileX", block.getX()); tag.putInt("TileY", block.getY()); @@ -36,6 +41,71 @@ public static CompoundTag transform(StructureSnapshot.EntityEntry entry, Bluepri Direction facing = Direction.from3DDataValue(tag.getByte("Facing")); tag.putByte("Facing", (byte) placement.rotation().rotate(placement.mirror().mirror(facing)).get3DDataValue()); } + ListTag motion = tag.getList("Motion", Tag.TAG_DOUBLE); + if (motion.size() == 3) { + Vec3 vector = new Vec3(motion.getDouble(0), motion.getDouble(1), motion.getDouble(2)); + Vec3 rotatedMotion = placement.localOf(vector).subtract(placement.localOf(Vec3.ZERO)); + ListTag transformedMotion = new ListTag(); + transformedMotion.add(DoubleTag.valueOf(rotatedMotion.x)); + transformedMotion.add(DoubleTag.valueOf(rotatedMotion.y)); + transformedMotion.add(DoubleTag.valueOf(rotatedMotion.z)); + tag.put("Motion", transformedMotion); + } + if (DynamicBuildingEntities.Outlet.isOutlet(tag)) { + var old = NbtUtils.readBlockPos(tag, "CauldronPos"); + var target = NbtUtils.readBlockPos(tag, "TargetPos"); + if (old.isPresent() && target.isPresent()) { + tag.put("TargetPos", NbtUtils.writeBlockPos(placement.worldOf(entry.blockPos().offset(target.get().subtract(old.get()))))); + } + tag.put("CauldronPos", NbtUtils.writeBlockPos(block)); + Direction side = Direction.from3DDataValue(tag.getInt("AttachedDirection")); + side = placement.rotation().rotate(placement.mirror().mirror(side)); + tag.putInt("AttachedDirection", side.get3DDataValue()); + var state = NbtUtils.readBlockState(BuiltInRegistries.BLOCK.asLookup(), tag.getCompound("CauldronState")); + tag.put("CauldronState", NbtUtils.writeBlockState(placement.stateOf(state))); + } + if (tag.contains("BlockState") && !MagnetizedNodeBuildAdapter.isNode(tag)) { + var state = NbtUtils.readBlockState(BuiltInRegistries.BLOCK.asLookup(), tag.getCompound("BlockState")); + tag.put("BlockState", NbtUtils.writeBlockState(placement.stateOf(state))); + } + if ("anvilcraft:animate_ascending_block".equals(tag.getString("id"))) { + for (String key : List.of("Start", "End")) { + var relative = NbtUtils.readBlockPos(tag, "Relative" + key); + if (relative.isPresent()) { + tag.put(key + "Pos", NbtUtils.writeBlockPos(placement.worldOf(entry.blockPos().offset(relative.get())))); + } + } + } + if (tag.contains("SlidingBlocks", Tag.TAG_COMPOUND)) { + var start = NbtUtils.readBlockPos(entry.nbt(), "StartPos"); + ListTag oldPos = entry.nbt().getList("Pos", Tag.TAG_DOUBLE); + BlockPos sourcePos = oldPos.size() == 3 + ? BlockPos.containing(oldPos.getDouble(0), oldPos.getDouble(1), oldPos.getDouble(2)) : entry.blockPos(); + BlockPos delta = NbtUtils.readBlockPos(tag, "RelativeStart").orElse(start.orElse(sourcePos).subtract(sourcePos)); + if (Math.abs(delta.getX()) > 64 || Math.abs(delta.getY()) > 64 || Math.abs(delta.getZ()) > 64) delta = BlockPos.ZERO; + BlockPos localStart = entry.blockPos().offset(delta); + tag.put("RelativeStart", NbtUtils.writeBlockPos(placement.localOf(delta))); + tag.put("StartPos", NbtUtils.writeBlockPos(placement.worldOf(localStart))); + Direction direction = Direction.byName(tag.getString("MovingDirection")); + if (direction != null) { + tag.putString("MovingDirection", placement.rotation().rotate(placement.mirror().mirror(direction)).getSerializedName()); + } + for (Tag value : tag.getCompound("SlidingBlocks").getList("blocks", Tag.TAG_COMPOUND)) { + CompoundTag part = (CompoundTag) value; + BlockPos offset = NbtUtils.readBlockPos(part, "offset").orElseThrow(); + BlockPos rotated = placement.localOf(offset); + part.put("offset", NbtUtils.writeBlockPos(rotated)); + var state = NbtUtils.readBlockState(BuiltInRegistries.BLOCK.asLookup(), part.getCompound("state")); + part.put("state", NbtUtils.writeBlockState(placement.stateOf(state))); + CompoundTag data = part.getCompound("entityData"); + BlockPos worldPart = BlockPos.containing(world).offset(rotated); + if (!data.isEmpty()) { + data.putInt("x", worldPart.getX()); + data.putInt("y", worldPart.getY()); + data.putInt("z", worldPart.getZ()); + } + } + } ListTag angles = tag.getList("Rotation", 5); float yaw = angles.isEmpty() ? 0 : angles.getFloat(0); yaw = switch (placement.mirror()) { @@ -52,21 +122,9 @@ public static CompoundTag transform(StructureSnapshot.EntityEntry entry, Bluepri } 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()); - } + CompoundTag safe = tag.copy(); + safe.remove("UUID"); + safe.remove("Passengers"); 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 f81f4ab530..27579c23ae 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingMaterials.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingMaterials.java @@ -2,13 +2,21 @@ import dev.dubhe.anvilcraft.api.StoragePortManager; import dev.dubhe.anvilcraft.api.itemhandler.unlimited.UnlimitedItemStacksResourceHandler; +import dev.dubhe.anvilcraft.block.item.ResinBlockItem; +import dev.dubhe.anvilcraft.init.block.ModBlocks; +import dev.dubhe.anvilcraft.init.item.ModComponents; +import dev.dubhe.anvilcraft.init.item.ModItems; import dev.dubhe.anvilcraft.inventory.PocketInventory; +import dev.dubhe.anvilcraft.item.AnvilHammerItem; import dev.dubhe.anvilcraft.item.BuildingRodItem; import dev.dubhe.anvilcraft.rpc.StorageServerStub; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.item.SpawnEggItem; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.neoforged.neoforge.capabilities.Capabilities; @@ -49,6 +57,7 @@ private static final class Source { private final IntConsumer extract; @Nullable private BuildingBlockMaterial.Supplied blockMaterial; private int reserved; + private boolean retainedTool; private Source(ItemStack resource, long available, IntConsumer extract) { this.resource = resource.copy(); @@ -58,7 +67,11 @@ private Source(ItemStack resource, long available, IntConsumer extract) { } public BuildingMaterials(ServerPlayer player) { - this.creative = player.isCreative(); + this(player, player.isCreative()); + } + + BuildingMaterials(ServerPlayer player, boolean creative) { + this.creative = creative; this.player = player; ItemStack held = BuildingRodItem.material(player); this.offhand = new Source(held, held.getCount(), held::shrink); @@ -116,8 +129,45 @@ public boolean reserve(List materials, List fluids) { @Nullable BuildingRodService.Group reserve(BuildingRodService.Group group, boolean allowMismatch) { final int[] before = this.sources.stream().mapToInt(source -> source.reserved).toArray(); + final int returnedBefore = this.returned.size(); + final int[] fluidBefore = this.storedFluids.stream().mapToInt(source -> source.reserved).toArray(); + boolean[] toolsBefore = new boolean[this.sources.size()]; + for (int i = 0; i < toolsBefore.length; i++) toolsBefore[i] = this.sources.get(i).retainedTool; BuildingRodService.Group allocated = new BuildingRodService.Group(); - if (!group.separateContents) allocated.entities.addAll(group.entities); + if (group.hammer && !this.reserveHammer()) { + this.restoreSources(before, toolsBefore); + return null; + } + ItemStack creature = group.creature == null ? ItemStack.EMPTY : this.reserveCreature(group.creature, allocated); + if (creature == null) { + this.restoreSources(before, toolsBefore); + return null; + } + List plannedEntities = new ArrayList<>(); + for (var plan : group.entities) { + var supplied = creature.isEmpty() ? plan : BlueprintEntities.fromMaterial(this.player, creature, plan); + if (supplied == null) { + this.restoreSources(before, toolsBefore); + return null; + } + plannedEntities.add(supplied); + } + for (Item tool : group.tools) { + if (this.reserveTool(tool)) continue; + this.restoreSources(before, toolsBefore); + return null; + } + if (group.ignitions > 0 && !this.reserveTool(Items.FLINT_AND_STEEL)) { + if (this.reserveConsumable(Items.FIRE_CHARGE, group.ignitions, allocated.materials) > 0) { + this.restoreSources(before, toolsBefore); + return null; + } + } + if (group.leads > 0 && this.reserveConsumable(Items.LEAD, group.leads, allocated.materials) > 0) { + this.restoreSources(before, toolsBefore); + return null; + } + if (!group.separateContents) allocated.entities.addAll(plannedEntities); allocated.fluids.addAll(group.fluids); allocated.materials.addAll(group.materials); for (var entry : group.blockMaterials.entrySet()) { @@ -127,7 +177,7 @@ BuildingRodService.Group reserve(BuildingRodService.Group group, boolean allowMi 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]; + this.restoreSources(before, toolsBefore); return null; } allocated.blockMaterials.put(entry.getKey(), taken.getFirst().placed()); @@ -139,7 +189,7 @@ BuildingRodService.Group reserve(BuildingRodService.Group group, boolean allowMi .anyMatch(supply -> !ItemStack.isSameItemSameComponents(supply.placed(), expected)); } if (!this.reserve(group.materials)) { - for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; + this.restoreSources(before, toolsBefore); return null; } for (var cell : group.cells) { @@ -155,12 +205,12 @@ BuildingRodService.Group reserve(BuildingRodService.Group group, boolean allowMi allocated.componentMismatch = true; allocated.missingContents = true; } else { - for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; + this.restoreSources(before, toolsBefore); return null; } } if (group.separateContents) { - for (var entity : group.entities) { + for (var entity : plannedEntities) { List contents = entity.contents().stream().map(EntityBuildAdapter.SlotStack::stack).toList(); if (this.reserve(contents)) { allocated.entities.add(entity); @@ -171,13 +221,13 @@ BuildingRodService.Group reserve(BuildingRodService.Group group, boolean allowMi allocated.componentMismatch = true; allocated.missingContents = true; } else { - for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; + this.restoreSources(before, toolsBefore); return null; } } } if (!this.reserve(List.of(), group.fluids)) { - for (int i = 0; i < before.length; i++) this.sources.get(i).reserved = before[i]; + this.restoreSources(before, toolsBefore); return null; } if (!this.creative) { @@ -190,15 +240,98 @@ BuildingRodService.Group reserve(BuildingRodService.Group group, boolean allowMi } } } + if (!this.creative) { + allocated.entities.forEach(entity -> BlueprintEntities.limitMotion(entity.entityNbt())); + allocated.materials.clear(); + for (int i = 0; i < this.sources.size(); i++) { + Source source = this.sources.get(i); + int count = source.reserved - before[i]; + if (count > 0) allocated.materials.add(source.resource.copyWithCount(count)); + } + allocated.returned.clear(); + this.returned.subList(returnedBefore, this.returned.size()).forEach(stack -> allocated.returned.add(stack.copy())); + for (int i = 0; i < this.storedFluids.size(); i++) { + StoredFluid source = this.storedFluids.get(i); + int count = source.reserved - (i < fluidBefore.length ? fluidBefore[i] : 0); + if (count > 0) allocated.fluidPayments.add(new FluidPayment(source.storage, source.fluid.copyWithAmount(count))); + } + } return allocated; } + public record FluidPayment(UUID storage, FluidStack fluid) { + } + + private boolean reserveTool(Item tool) { + if (this.creative) return true; + for (Source source : this.sources) { + if (!source.resource.is(tool) || source.available <= source.reserved) continue; + source.retainedTool = true; + return true; + } + return false; + } + + private boolean reserveHammer() { + if (this.creative) return true; + for (Source source : this.sources) { + if (source.resource.getItem() instanceof AnvilHammerItem && source.available > source.reserved) { + source.retainedTool = true; + return true; + } + } + return false; + } + + @Nullable + private ItemStack reserveCreature(EntityType type, BuildingRodService.Group allocated) { + if (this.creative) return ItemStack.EMPTY; + for (int pass = 0; pass < 2; pass++) { + for (Source source : this.sources) { + if (source.available <= source.reserved + (source.retainedTool ? 1 : 0)) continue; + var saved = source.resource.get(ModComponents.SAVED_ENTITY); + boolean resin = source.resource.getItem() instanceof ResinBlockItem && saved != null + && EntityType.by(saved.tag()).orElse(null) == type; + boolean egg = source.resource.getItem() instanceof SpawnEggItem spawnEgg && spawnEgg.getType(source.resource) == type; + if (pass == 0 ? !resin : !egg) continue; + source.reserved++; + allocated.materials.add(source.resource.copyWithCount(1)); + if (resin) allocated.returned.add(ModItems.RESIN.asStack(this.player.getRandom().nextInt(1, 4))); + return source.resource.copyWithCount(1); + } + } + return null; + } + + private int reserveConsumable(Item item, int remaining, List materials) { + if (this.creative) return 0; + for (Source source : this.sources) { + if (!source.resource.is(item)) continue; + int take = (int) Math.min(remaining, source.available - source.reserved); + if (take == 0) continue; + source.reserved += take; + remaining -= take; + materials.add(source.resource.copyWithCount(take)); + if (remaining == 0) break; + } + return remaining; + } + + private void restoreSources(int[] reserved, boolean[] tools) { + for (int i = 0; i < reserved.length; i++) { + Source source = this.sources.get(i); + source.reserved = reserved[i]; + source.retainedTool = tools[i]; + } + } + 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) || source.available <= source.reserved) continue; + if (!ItemStack.isSameItem(source.resource, expected) + || source.available <= source.reserved + (source.retainedTool ? 1 : 0)) continue; if (separateContents && source.blockMaterial == null) { source.blockMaterial = BuildingBlockMaterial.separateContents(source.resource, this.player.level()); } @@ -206,7 +339,7 @@ private int reserveBlock(ItemStack expected, boolean exact, int remaining, List< ? 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); + int take = (int) Math.min(remaining, source.available - source.reserved - (source.retainedTool ? 1 : 0)); source.reserved += take; remaining -= take; List returned = material.contents().stream() @@ -221,7 +354,7 @@ private int reserveItem(ItemStack material) { int remaining = material.getCount(); for (Source source : this.sources) { if (!ItemStack.isSameItemSameComponents(source.resource, material)) continue; - int take = (int) Math.min(remaining, source.available - source.reserved); + int take = (int) Math.min(remaining, source.available - source.reserved - (source.retainedTool ? 1 : 0)); source.reserved += take; remaining -= take; if (remaining == 0) break; @@ -256,7 +389,22 @@ private int reserveFluid(FluidStack fluid) { List missing(List groups) { List items = new ArrayList<>(); List fluids = new ArrayList<>(); + List tools = new ArrayList<>(); + int ignitions = 0; + int leads = 0; for (var group : groups) { + for (Item tool : group.tools) { + if (!tools.contains(tool)) tools.add(tool); + } + if (group.hammer && !this.reserveHammer() && !tools.contains(ModItems.ANVIL_HAMMER.get())) { + tools.add(ModItems.ANVIL_HAMMER.get()); + } + if (group.creature != null && this.reserveCreature(group.creature, new BuildingRodService.Group()) == null) { + items.add(SpawnEggItem.byId(group.creature) == null ? ModBlocks.RESIN_BLOCK.asStack() + : new ItemStack(SpawnEggItem.byId(group.creature))); + } + ignitions += group.ignitions; + leads += group.leads; List required = new ArrayList<>(group.materials); group.cells.forEach(cell -> cell.contents().forEach(content -> required.add(content.stack()))); if (group.separateContents) { @@ -277,6 +425,18 @@ List missing(List groups) { } } List lines = new ArrayList<>(); + int missingLeads = this.reserveConsumable(Items.LEAD, leads, new ArrayList<>()); + if (missingLeads > 0) lines.add(new ItemStack(Items.LEAD).getHoverName().copy().append(" ×" + missingLeads)); + for (Item tool : tools) { + if (!this.reserveTool(tool)) lines.add(new ItemStack(tool).getHoverName().copy().append(" ×1")); + } + if (ignitions > 0 && !this.reserveTool(Items.FLINT_AND_STEEL)) { + int missing = this.reserveConsumable(Items.FIRE_CHARGE, ignitions, new ArrayList<>()); + if (missing > 0) { + lines.add(new ItemStack(Items.FLINT_AND_STEEL).getHoverName().copy().append(" ×1 / ") + .append(new ItemStack(Items.FIRE_CHARGE).getHoverName()).append(" ×" + missing)); + } + } for (ItemStack material : items) { int missing = this.reserveItem(material); if (missing > 0) lines.add(material.getHoverName().copy().append(" ×" + missing)); @@ -289,7 +449,7 @@ List missing(List groups) { } private int reserveContainer(Source source, FluidStack fluid, int remaining) { - while (remaining > 0 && source.available > source.reserved) { + while (remaining > 0 && source.available > source.reserved + (source.retainedTool ? 1 : 0)) { ItemStack container = source.resource.copyWithCount(1); IFluidHandlerItem handler = container.getCapability(Capabilities.FluidHandler.ITEM); if (handler == null) break; diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingRegionSnapshot.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingRegionSnapshot.java new file mode 100644 index 0000000000..c57016eedb --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingRegionSnapshot.java @@ -0,0 +1,189 @@ +package dev.dubhe.anvilcraft.building; + +import dev.dubhe.anvilcraft.block.RedstoneWireBlock; +import dev.dubhe.anvilcraft.block.RedstoneWireNetworkManager; +import dev.dubhe.anvilcraft.mixin.accessor.BlueprintBlockEventsAccessor; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockEventData; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.phys.AABB; +import net.neoforged.neoforge.common.CommonHooks; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +final class BuildingRegionSnapshot { + private final ServerLevel level; + private final BoundingBox bounds; + private final long capturedAt; + private final List blocks = new ArrayList<>(); + private final List entities = new ArrayList<>(); + private final List ticks; + private final List events; + + private record SavedBlock(BlockPos pos, BlockState state, @Nullable CompoundTag data) { + } + + private record SavedEntity(UUID uuid, CompoundTag data, Entity original) { + } + + BuildingRegionSnapshot(ServerPlayer player, BoundingBox bounds) { + this.level = player.serverLevel(); + this.bounds = BoundingBox.fromCorners(new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()), + new BlockPos(bounds.maxX(), bounds.maxY(), bounds.maxZ())); + this.capturedAt = this.level.getGameTime(); + for (BlockPos cursor : BlockPos.betweenClosed(bounds.minX(), bounds.minY(), bounds.minZ(), + bounds.maxX(), bounds.maxY(), bounds.maxZ())) { + BlockPos pos = cursor.immutable(); + if (!BuildingRodService.canModify(player, pos)) throw new IllegalArgumentException("Undo area is unavailable"); + BlockEntity entity = this.level.getBlockEntity(pos); + this.blocks.add(new SavedBlock(pos, this.level.getBlockState(pos), + entity == null ? null : entity.saveWithFullMetadata(this.level.registryAccess()))); + } + for (Entity entity : this.level.getEntities((Entity) null, AABB.of(bounds), entity -> !(entity instanceof Player))) { + CompoundTag tag = new CompoundTag(); + if (!BlueprintLeashes.save(entity, tag)) continue; + tag.remove("Passengers"); + if (entity.getVehicle() != null) tag.putUUID(BlueprintEntities.VEHICLE, entity.getVehicle().getUUID()); + this.entities.add(new SavedEntity(entity.getUUID(), tag, entity)); + } + this.ticks = BlueprintTicks.capture(this.level, bounds); + this.events = ((BlueprintBlockEventsAccessor) this.level).anvilcraft$getBlockEvents().stream() + .filter(event -> bounds.isInside(event.pos())).toList(); + } + + boolean contains(BlockPos pos) { + return this.bounds.isInside(pos); + } + + boolean containsEntity(UUID uuid) { + return this.entities.stream().anyMatch(entity -> entity.uuid().equals(uuid)); + } + + boolean canRestore(ServerPlayer player) { + for (SavedBlock block : this.blocks) { + if (!BuildingRodService.canModify(player, block.pos())) return false; + BlockState current = this.level.getBlockState(block.pos()); + if (current != block.state() && !current.isAir() && CommonHooks.fireBlockBreak(this.level, + player.gameMode.getGameModeForPlayer(), player, block.pos(), current).isCanceled()) { + return false; + } + } + for (SavedEntity saved : this.entities) { + Entity entity = find(this.level, saved.uuid()); + if (entity != null) { + if (!entity.level().mayInteract(player, entity.blockPosition())) return false; + } else { + Entity.RemovalReason reason = saved.original().getRemovalReason(); + if (reason == null || !reason.shouldDestroy()) return false; + } + } + return true; + } + + List restoredMaterials() { + List result = new ArrayList<>(); + for (SavedBlock block : this.blocks) { + if (block.data() == null) continue; + List before = BlockEntityContentAdapter.extract(block.state(), block.data(), this.level.registryAccess()) + .contents().stream().map(content -> content.stack().copy()).collect(Collectors.toCollection(ArrayList::new)); + BlockEntity current = this.level.getBlockEntity(block.pos()); + if (current != null) { + var contents = BlockEntityContentAdapter.extract(current, this.level.registryAccess(), this.level); + for (var content : contents.contents()) subtract(before, content.stack()); + } + before.stream().filter(stack -> !stack.isEmpty()).forEach(result::add); + } + return result; + } + + static int subtract(List stacks, ItemStack required) { + int remaining = required.getCount(); + for (ItemStack stack : stacks) { + if (!ItemStack.isSameItemSameComponents(stack, required)) continue; + int take = Math.min(stack.getCount(), remaining); + stack.shrink(take); + remaining -= take; + if (remaining == 0) break; + } + return required.getCount() - remaining; + } + + void restore() { + this.level.clearBlockEvents(this.bounds); + this.level.getBlockTicks().clearArea(this.bounds); + this.level.getFluidTicks().clearArea(this.bounds); + BuildingCommit.quietly(this.level, () -> { + for (SavedBlock block : this.blocks) { + this.level.removeBlockEntity(block.pos()); + BuildingCommit.set(this.level, block.pos(), block.state()); + } + for (SavedBlock block : this.blocks) { + if (block.data() == null) continue; + CompoundTag data = block.data().copy(); + BlueprintRuntimeData.rebase(data, this.capturedAt, this.level.getGameTime()); + BlockEntity entity = BlueprintBlockEntities.create(this.level, block.pos(), block.state(), data); + if (entity != null) { + this.level.setBlockEntity(entity); + entity.setChanged(); + } + this.level.sendBlockUpdated(block.pos(), block.state(), block.state(), Block.UPDATE_CLIENTS); + } + }); + List> restored = new ArrayList<>(); + for (SavedEntity saved : this.entities) { + Entity entity = find(this.level, saved.uuid()); + if (entity instanceof Player) continue; + if (entity != null && entity.level() != this.level) { + entity.discard(); + entity = null; + } + if (entity == null) { + entity = EntityBuildAdapters.create(saved.data().copy(), this.level).orElse(null); + if (entity == null || !this.level.addFreshEntity(entity)) continue; + } else { + entity.stopRiding(); + entity.load(saved.data().copy()); + } + CompoundTag link = saved.data().copy(); + link.putUUID(BlueprintEntities.SOURCE, saved.uuid()); + restored.add(Map.entry(entity, link)); + } + BlueprintEntities.link(restored); + List cells = this.blocks.stream().map(block -> new BuildingRodService.Cell( + block.pos(), block.state(), block.data() == null ? new CompoundTag() : block.data(), List.of())).toList(); + Map wires = new LinkedHashMap<>(); + cells.stream().filter(cell -> cell.state().getBlock() instanceof RedstoneWireBlock) + .forEach(cell -> wires.put(cell.pos(), cell.state())); + RedstoneWireNetworkManager.restoreBlueprint(this.level, wires); + BuildingCommit.activate(this.level, cells, this.ticks); + for (BlockEventData event : this.events) { + if (this.level.getBlockState(event.pos()).is(event.block())) { + this.level.blockEvent(event.pos(), event.block(), event.paramA(), event.paramB()); + } + } + } + + @Nullable + static Entity find(ServerLevel level, UUID uuid) { + for (ServerLevel world : level.getServer().getAllLevels()) { + Entity entity = world.getEntity(uuid); + if (entity != null) return entity; + } + return null; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodService.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodService.java index ee7aca8e0e..feac0dfa41 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodService.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodService.java @@ -10,6 +10,10 @@ import dev.dubhe.anvilcraft.block.item.SimpleMultiPartBlockItem; import dev.dubhe.anvilcraft.block.multipart.AbstractMultiPartBlock; import dev.dubhe.anvilcraft.block.multipart.SimpleMultiPartBlock; +import dev.dubhe.anvilcraft.entity.AnimateAscendingBlockEntity; +import dev.dubhe.anvilcraft.entity.CauldronOutletEntity; +import dev.dubhe.anvilcraft.entity.MagnetizedNodeEntity; +import dev.dubhe.anvilcraft.entity.SlidingBlockEntity; import dev.dubhe.anvilcraft.init.item.ModComponents; import dev.dubhe.anvilcraft.init.item.ModItems; import dev.dubhe.anvilcraft.item.BuildingRodItem; @@ -17,17 +21,25 @@ import dev.dubhe.anvilcraft.util.StructureLoadUtil; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; 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.tags.BlockTags; import net.minecraft.world.Container; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Leashable; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.item.FallingBlockEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.context.BlockPlaceContext; @@ -48,6 +60,7 @@ import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; import net.minecraft.world.level.block.state.properties.SlabType; +import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.material.Fluids; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; @@ -62,6 +75,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.WeakHashMap; import javax.annotation.Nullable; @@ -130,6 +144,12 @@ public record Cell(BlockPos pos, BlockState state, CompoundTag config, List tools = new ArrayList<>(); + final List fluidPayments = new ArrayList<>(); + int ignitions; + int leads; + boolean hammer; + @Nullable EntityType creature; final List cells = new ArrayList<>(); final List materials = new ArrayList<>(); final List fluids = new ArrayList<>(); @@ -398,132 +418,238 @@ private static

> void addMultipart(Group group, BlockPos pos, } public static void blueprint(ServerPlayer player, BlockPos anchor, Rotation rotation, Mirror mirror, boolean partial) { + blueprints(player, anchor, anchor, rotation, mirror, partial); + } + + public static void blueprints( + ServerPlayer player, BlockPos anchor, BlockPos last, Rotation rotation, Mirror mirror, boolean partial + ) { if (!BuildingRodItem.isHeld(player) || !BuildingRodItem.material(player).is(ModItems.STRUCTURE_DISK)) return; var disk = BuildingRodItem.material(player).get(ModComponents.STRUCTURE_DISK_DATA); - if (disk == null || !withinReach(player, anchor, 26)) return; + if (disk == null || !withinReach(player, anchor, 26) || !withinReach(player, last, 26)) return; CompoundTag tag = StructureLoadUtil.readStructureFileOnServer(player.serverLevel(), disk.file()); if (tag == null) return; try { StructureSnapshot snapshot = BlueprintNormalizer.load(tag, player.registryAccess(), disk.direction(), disk.upsideDown()); BlueprintPlacement placement = new BlueprintPlacement(anchor, rotation, mirror); - BlockPos sourceOrigin = BlueprintBlockConfiguration.sourceOrigin(snapshot); - Map groups = new LinkedHashMap<>(); - List blueprint = BlueprintMultiblocks.expand(snapshot, placement, -1); + List placements = placement.tile(snapshot, last); + if (placements.isEmpty()) { + message(player, "too_many"); + return; + } + List allGroups = new ArrayList<>(); Map declared = new LinkedHashMap<>(); - blueprint.forEach(entry -> declared.put(entry.pos(), entry.state())); - for (BlueprintMultiblocks.PlacedBlock entry : blueprint) { - BlockState state = entry.state(); - if (OrdinaryBlockAdapter.mapping(state) == OrdinaryBlockAdapter.Mapping.AIR) continue; - BlockPos pos = entry.pos(); - if (!canModify(player, pos)) { - message(player, "blocked"); - return; - } - if (player.level().getBlockState(pos).equals(state)) continue; - if (!player.level().getBlockState(pos).canBeReplaced()) { - message(player, "blocked"); - return; + List ticks = new ArrayList<>(); + for (BlueprintPlacement copy : placements) { + snapshot.ticks().forEach(tick -> ticks.add(tick.at(copy.worldOf(tick.pos())))); + if (!planBlueprint(player, snapshot, copy, allGroups, declared)) return; + } + var undoBounds = placements.getFirst().bounds(snapshot.size()); + undoBounds.encapsulate(placements.getLast().bounds(snapshot.size())); + boolean complete = commit(player, allGroups, partial, true, declared, ticks, undoBounds); + if (complete) PacketDistributor.sendToPlayer(player, new BuildingRodResultPacket(true)); + } catch (ConstructionBlueprintException | IllegalArgumentException exception) { + message(player, "invalid_structure"); + } + } + + private static boolean planBlueprint( + ServerPlayer player, StructureSnapshot snapshot, BlueprintPlacement placement, + List allGroups, Map declared + ) { + BlockPos sourceOrigin = BlueprintBlockConfiguration.sourceOrigin(snapshot); + Map groups = new LinkedHashMap<>(); + List blueprint = BlueprintMultiblocks.expand(snapshot, placement, -1); + blueprint.forEach(entry -> declared.put(entry.pos(), entry.state())); + Map portalCores = BlueprintIgnition.portalCores(declared); + Map localPositions = new LinkedHashMap<>(); + snapshot.blocks().forEach(block -> localPositions.put(placement.worldOf(block.pos()), block.pos())); + for (BlueprintMultiblocks.PlacedBlock entry : blueprint) { + BlockState state = entry.state(); + if (OrdinaryBlockAdapter.mapping(state) == OrdinaryBlockAdapter.Mapping.AIR) continue; + BlockPos pos = entry.pos(); + if (!canModify(player, pos)) { + message(player, "blocked"); + return false; + } + if (player.level().getBlockState(pos).equals(state)) continue; + if (!player.level().getBlockState(pos).canBeReplaced()) { + message(player, "blocked"); + return false; + } + BlockPos core = portalCores.getOrDefault(pos, BlueprintMultiblocks.core(pos, state)); + Group group = groups.computeIfAbsent(core, ignored -> new Group()); + group.separateContents = true; + if (state.is(Blocks.MOVING_PISTON)) { + CompoundTag source = entry.nbt().orElseThrow(() -> new IllegalArgumentException("Moving piston has no runtime data")); + if (!"minecraft:piston".equals(source.getString("id")) || !Float.isFinite(source.getFloat("progress"))) { + throw new IllegalArgumentException("Invalid moving piston runtime data"); } - BlockPos core = BlueprintMultiblocks.core(pos, state); - Group group = groups.computeIfAbsent(core, ignored -> new Group()); - group.separateContents = true; - BuildingBlockMaterial blockMaterial = BuildingBlockMaterial.extract(state, - entry.nbt().orElse(null), player.level()); - if (blockMaterial.requiresOperator() && !player.canUseGameMasterBlocks()) { + BlockState moved = NbtUtils.readBlockState(player.registryAccess().lookupOrThrow(Registries.BLOCK), + source.getCompound("blockState")); + if (BuildingBlockMaterial.extract(moved, null, player.level()).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, blockMaterial.config(), player.registryAccess()); - decoration.decorations().stream().map(SignDecorationAdapter.Decoration::material) - .filter(material -> !material.isEmpty()).forEach(group.materials::add); - } - if (state.hasProperty(BlockStateProperties.WATERLOGGED) && state.getValue(BlockStateProperties.WATERLOGGED) - && !player.level().getFluidState(pos).isSource()) group.fluids.add(new FluidStack(Fluids.WATER, 1000)); - if (FluidBuildAdapter.isLiquidBlock(state)) { - if (!state.getFluidState().isSource()) continue; - group.fluids.add(FluidBuildAdapter.liquidOf(state)); - continue; - } - if (FluidBuildAdapter.isFilledCauldron(state)) { - group.materials.add(new ItemStack(Items.CAULDRON)); - group.fluids.add(FluidBuildAdapter.cauldronFluidOf(state)); - continue; + return false; } - if (!core.equals(pos)) continue; - ItemStack upgrade = UseItemOnBlock.materialFor(state); - if (!upgrade.isEmpty()) group.materials.add(upgrade); - ItemStack material = blockMaterial.stack().copyWithCount(materialCount(state)); - if (material.isEmpty() && !player.isCreative()) { - message(player, "unsupported"); - return; + CompoundTag config = source.copy(); + BlueprintBlockConfiguration.transform(config, placement, sourceOrigin, snapshot); + group.cells.add(new Cell(pos, state, config, List.of())); + if (!moved.is(Blocks.PISTON_HEAD)) { + ItemStack material = OrdinaryBlockAdapter.material(moved); + if (material.isEmpty() && !player.isCreative()) { + unsupported(player, state.getBlock().getName()); + return false; + } + if (!material.isEmpty()) group.materials.add(material); } - if (!material.isEmpty()) group.blockMaterials.put(pos, material); + continue; + } + if (BlueprintIgnition.isIgnition(state)) { + group.cells.add(new Cell(pos, state, new CompoundTag(), List.of())); + group.ignitions = 1; + continue; + } + BuildingBlockMaterial blockMaterial = BuildingBlockMaterial.extract(state, + entry.nbt().orElse(null), player.level()); + if (blockMaterial.requiresOperator() && !player.canUseGameMasterBlocks()) { + message(player, "blocked"); + return false; + } + blockMaterial = blockMaterial.withConfiguration(state, blockMaterial.config(), player.registryAccess()); + BlueprintBlockConfiguration.transform(blockMaterial.config(), placement, sourceOrigin, snapshot); + BlueprintRuntimeData.restoreClock(blockMaterial.config(), snapshot, + localPositions.getOrDefault(pos, BlockPos.ZERO), player.level().getGameTime()); + 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, blockMaterial.config(), player.registryAccess()); + decoration.decorations().stream().map(SignDecorationAdapter.Decoration::material) + .filter(material -> !material.isEmpty()).forEach(group.materials::add); + } + if (state.hasProperty(BlockStateProperties.WATERLOGGED) && state.getValue(BlockStateProperties.WATERLOGGED) + && !player.level().getFluidState(pos).isSource()) group.fluids.add(new FluidStack(Fluids.WATER, 1000)); + if (FluidBuildAdapter.isLiquidBlock(state)) { + if (!state.getFluidState().isSource()) continue; + group.fluids.add(FluidBuildAdapter.liquidOf(state)); + continue; + } + if (state.is(Blocks.BUBBLE_COLUMN)) { + group.fluids.add(new FluidStack(Fluids.WATER, 1000)); + continue; + } + if (FluidBuildAdapter.isFilledCauldron(state)) { + group.materials.add(new ItemStack(Items.CAULDRON)); + group.fluids.add(FluidBuildAdapter.cauldronFluidOf(state)); + continue; + } + if (!core.equals(pos)) continue; + group.materials.addAll(BlueprintSpecialBlocks.extra(state)); + ItemStack upgrade = UseItemOnBlock.materialFor(state); + if (!upgrade.isEmpty()) group.materials.add(upgrade); + ItemStack material = blockMaterial.stack().copyWithCount(materialCount(state)); + if (material.isEmpty() && !player.isCreative()) { + unsupported(player, state.getBlock().getName()); + return false; } - for (var entry : groups.entrySet()) { - if (entry.getValue().cells.stream().noneMatch(cell -> cell.pos().equals(entry.getKey())) - && player.level().getBlockState(entry.getKey()).isAir()) { + if (!material.isEmpty()) group.blockMaterials.put(pos, material); + } + for (var entry : groups.entrySet()) { + if (entry.getValue().cells.stream().noneMatch(cell -> cell.pos().equals(entry.getKey())) + && player.level().getBlockState(entry.getKey()).isAir()) { + message(player, "invalid_structure"); + return false; + } + } + allGroups.addAll(groups.values()); + var identities = BlueprintEntities.identities(snapshot, placement); + 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) { + unsupported(player, type.getDescription()); + return false; + } + if (EntityBuildAdapters.isTransient(probe)) continue; + if (probe instanceof AnimateAscendingBlockEntity && !entry.nbt().contains("BlockState")) continue; + if ((type == EntityType.COMMAND_BLOCK_MINECART || probe.onlyOpCanSetNbt() + && !(probe instanceof FallingBlockEntity) && !(probe instanceof SlidingBlockEntity)) + && !player.canUseGameMasterBlocks()) { + message(player, "blocked"); + return false; + } + CompoundTag transformed = BuildingEntityTransform.transform(entry, placement); + BlueprintEntities.relocateMemories(transformed, entry, snapshot, placement, player.level().dimension().location().toString()); + var leashFence = BlueprintLeashes.attachment(transformed); + if (leashFence.isPresent() && (!canModify(player, leashFence.get()) + || !declared.getOrDefault(leashFence.get(), player.level().getBlockState(leashFence.get())).is(BlockTags.FENCES))) { + message(player, "blocked"); + return false; + } + if (!player.canUseGameMasterBlocks() && DynamicBuildingEntities.requiresOperator(transformed, player.serverLevel())) { + message(player, "blocked"); + return false; + } + var position = transformed.getList("Pos", Tag.TAG_DOUBLE); + Vec3 world = new Vec3(position.getDouble(0), position.getDouble(1), position.getDouble(2)); + if (!canModify(player, BlockPos.containing(world))) { + message(player, "blocked"); + return false; + } + if (!player.level().getEntitiesOfClass(Entity.class, AABB.ofSize(world, 0.25, 0.25, 0.25), + entity -> entity.getType() == type && entity.position().distanceToSqr(world) < 0.02).isEmpty()) { + continue; + } + EntityBuildAdapter adapter = EntityBuildAdapters.find(probe, transformed).orElse(null); + if (adapter == null) { + unsupported(player, type.getDescription()); + return false; + } + var plan = probe instanceof Mob && !player.isCreative() + ? new EntityBuildAdapter.Planned(ItemStack.EMPTY, ItemStack.EMPTY, transformed.copy(), List.of(), List.of(), false) + : adapter.plan(player.serverLevel(), entry, transformed); + if (plan.unsupported()) { + unsupported(player, type.getDescription()); + return false; + } + plan = new EntityBuildAdapter.Planned(plan.material(), plan.returned(), + BuildingEntityTransform.sanitize(probe, plan.entityNbt()), plan.contents(), plan.fluids(), false); + if (transformed.contains("Motion", Tag.TAG_LIST)) { + plan.entityNbt().put("Motion", transformed.getList("Motion", Tag.TAG_DOUBLE).copy()); + } + BlueprintEntities.scope(plan.entityNbt(), entry.nbt(), identities); + Group group = null; + if (probe instanceof MagnetizedNodeEntity || probe instanceof CauldronOutletEntity) { + BlockPos support = NbtUtils.readBlockPos(plan.entityNbt(), + probe instanceof MagnetizedNodeEntity ? "BlockPos" : "CauldronPos").orElseThrow(); + BlockState state = declared.get(support); + if (state == null) { message(player, "invalid_structure"); - return; + return false; } + group = groups.get(BlueprintMultiblocks.core(support, state)); } - List allGroups = new ArrayList<>(groups.values()); - 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))) { - message(player, "blocked"); - return; - } - if (!player.level().getEntitiesOfClass(Entity.class, AABB.ofSize(world, 0.25, 0.25, 0.25), - entity -> entity.getType() == type && entity.position().distanceToSqr(world) < 0.02).isEmpty()) { - continue; - } - CompoundTag transformed = BuildingEntityTransform.transform(entry, placement); - EntityBuildAdapter adapter = EntityBuildAdapters.find(type, transformed).orElse(null); - if (adapter == null) { - message(player, "unsupported"); - return; - } - var plan = adapter.plan(player.serverLevel(), entry, transformed); - if (plan.unsupported()) { - 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()); - 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); + if (group == null) { + group = new Group(); allGroups.add(group); } - boolean complete = commit(player, allGroups, partial, true, declared); - if (complete) PacketDistributor.sendToPlayer(player, new BuildingRodResultPacket(true)); - } catch (ConstructionBlueprintException | IllegalArgumentException exception) { - message(player, "invalid_structure"); + group.separateContents |= probe instanceof Container; + ItemStack tool = adapter.requiredTool(); + if (!tool.isEmpty()) group.tools.add(tool.getItem()); + group.hammer |= adapter.requiresHammer(); + if (probe instanceof Leashable && BlueprintLeashes.hasLeash(plan.entityNbt())) group.leads = 1; + if (probe instanceof Mob) group.creature = type; + else group.materials.add(plan.material()); + if (!group.separateContents) { + for (var content : plan.contents()) group.materials.add(content.stack()); + } + for (var fluid : plan.fluids()) group.fluids.add(fluid.fluid()); + group.entities.add(plan); } + return true; } - private static int materialCount(BlockState state) { + static int materialCount(BlockState state) { if (state.hasProperty(BlockStateProperties.SLAB_TYPE) && state.getValue(BlockStateProperties.SLAB_TYPE) == SlabType.DOUBLE) { return 2; } @@ -534,11 +660,13 @@ private static int materialCount(BlockState state) { } static boolean commit(ServerPlayer player, List groups, boolean partial, boolean quiet) { - return commit(player, groups, partial, quiet, Map.of()); + return commit(player, groups, partial, quiet, Map.of(), List.of(), null); } private static boolean commit( - ServerPlayer player, List groups, boolean partial, boolean quiet, Map declared + ServerPlayer player, List groups, boolean partial, boolean quiet, + Map declared, List ticks, + @Nullable BoundingBox undoBounds ) { if (!quiet && groups.stream().mapToInt(group -> group.cells.size()).sum() > MAX_BLOCKS) { message(player, "too_many"); @@ -553,7 +681,10 @@ private static boolean commit( boolean missing = false; List placedGroups = new ArrayList<>(); for (Group group : groups) { - Group allocated = materials.reserve(group, allowMismatch); + boolean missingFence = group.entities.stream().map(entity -> BlueprintLeashes.attachment(entity.entityNbt())) + .flatMap(Optional::stream).anyMatch(pos -> !player.level().getBlockState(pos).is(BlockTags.FENCES) + && cells.stream().noneMatch(cell -> cell.pos().equals(pos) && cell.state().is(BlockTags.FENCES))); + Group allocated = missingFence ? null : materials.reserve(group, allowMismatch); if (allocated == null) { missing = true; if (!partial) { @@ -610,18 +741,24 @@ private static boolean commit( } ItemStack rod = BuildingRodItem.heldRod(player); if (!BuildingRodItem.ready(player, rod)) return false; + final BuildingRodUndo undo = new BuildingRodUndo(player, placedGroups, undoBounds); if (!materials.consume()) { message(player, "missing_blocks"); if (quiet) BuildingRodMaterialBook.give(player, new BuildingMaterials(player).missing(groups)); return false; } - final BuildingRodUndo undo = new BuildingRodUndo(player, placedGroups); + undo.consumed(); Map placedMaterials = new LinkedHashMap<>(); placedGroups.forEach(group -> placedMaterials.putAll(group.blockMaterials)); + List> spawned = new ArrayList<>(); Runnable place = () -> { for (Cell cell : cells) BuildingCommit.set(player.level(), cell.pos(), cell.state()); for (Cell cell : cells) { BlockEntity blockEntity = player.level().getBlockEntity(cell.pos()); + if (blockEntity == null && cell.state().is(Blocks.MOVING_PISTON)) { + blockEntity = BlueprintBlockEntities.create(player.level(), cell.pos(), cell.state(), cell.config()); + if (blockEntity != null) player.level().setBlockEntity(blockEntity); + } if (blockEntity != null) { if (!quiet && !cell.config().isEmpty() && !(blockEntity instanceof CommandBlockEntity)) { blockEntity.loadWithComponents(cell.config(), player.registryAccess()); @@ -657,7 +794,11 @@ private static boolean commit( if (quiet) RedstoneWireNetworkManager.restoreBlueprint(player.serverLevel(), wires); for (var plan : entities) { Entity entity = EntityBuildAdapters.spawn(player.serverLevel(), new BuildingEntityOp(plan.entityNbt(), plan.returned())); - if (entity != null) EntityBuildAdapters.insertContents(entity, plan.contents(), player.registryAccess()); + if (entity != null) { + EntityBuildAdapters.insertContents(entity, plan.contents(), player.registryAccess()); + spawned.add(Map.entry(entity, plan.entityNbt())); + undo.recordEntity(plan, entity); + } if (!player.isCreative() && !plan.returned().isEmpty()) { player.getInventory().placeItemBackInInventory(plan.returned().copy()); } @@ -665,7 +806,9 @@ private static boolean commit( }; if (quiet) BuildingCommit.quietly(player.level(), place); else place.run(); + BlueprintEntities.link(spawned).forEach(undo::recordAuxiliary); undo.finish(player); + if (quiet) BuildingCommit.activate(player.serverLevel(), cells, ticks); BuildingRodMaterialBook.give(player, shortages); finishPlacement(player, cells.size()); if (quiet || !placedMaterials.isEmpty()) playPlacementSounds(player, cells, quiet); @@ -704,6 +847,10 @@ private static boolean withinReach(ServerPlayer player, BlockPos pos, int allowa return player.getEyePosition().distanceToSqr(Vec3.atCenterOf(pos)) <= Math.pow(player.blockInteractionRange() + allowance + 1, 2); } + private static void unsupported(ServerPlayer player, Component type) { + player.displayClientMessage(Component.translatable("message.anvilcraft.building_rod.unsupported_type", type), true); + } + public static void message(ServerPlayer player, String key) { player.displayClientMessage(Component.translatable("message.anvilcraft.building_rod." + key), true); } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodUndo.java b/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodUndo.java index adc162c87c..90ba4dd57d 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodUndo.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/BuildingRodUndo.java @@ -1,83 +1,190 @@ package dev.dubhe.anvilcraft.building; +import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.api.StoragePortManager; import dev.dubhe.anvilcraft.item.BuildingRodItem; import net.minecraft.core.BlockPos; -import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Leashable; +import net.minecraft.world.entity.decoration.LeashFenceKnotEntity; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; -import net.neoforged.neoforge.common.CommonHooks; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.phys.AABB; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent; +import net.neoforged.neoforge.event.level.BlockDropsEvent; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; import java.util.ArrayList; +import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.WeakHashMap; +import javax.annotation.Nullable; -/** 只保留最近一次放置;多方块作为一组核对和恢复,避免拆除后来放置的替代品。 */ +/** 最近一次放置的区域快照与逐组材料账单。区域恢复不依赖当前方块或实体仍与蓝图相同。 */ +@EventBusSubscriber(modid = AnvilCraft.MOD_ID) public final class BuildingRodUndo { private static final Map HISTORY = new WeakHashMap<>(); - private final Level level; - private final List groups = new ArrayList<>(); - private final Map positions = new LinkedHashMap<>(); - - private record Saved(BlockPos pos, BlockState state, CompoundTag nbt) { - static Saved capture(Level level, BlockPos pos) { - BlockEntity entity = level.getBlockEntity(pos); - return new Saved(pos.immutable(), level.getBlockState(pos), entity == null - ? new CompoundTag() : entity.saveWithFullMetadata(level.registryAccess())); - } - - boolean matches(Level level) { - return level.hasChunkAt(this.pos) && this.equals(capture(level, this.pos)); - } - } + private final ServerLevel level; + private final BuildingRegionSnapshot region; + private final List receipts = new ArrayList<>(); + private final Map entityReceipts = new IdentityHashMap<>(); + private final Map auxiliary = new LinkedHashMap<>(); + private final Map derived = new LinkedHashMap<>(); + private final Set changedPositions = new HashSet<>(); + private boolean restored; + private boolean restoring; - private static final class PlacedGroup { - private final List before = new ArrayList<>(); - private final List expected = new ArrayList<>(); - private final List after = new ArrayList<>(); + private static final class Receipt { private final List materials = new ArrayList<>(); private final List returned = new ArrayList<>(); - private boolean replaced; + private final List fluids = new ArrayList<>(); + private final Map entities = new LinkedHashMap<>(); + private final Map drops = new LinkedHashMap<>(); + private boolean accepted; } BuildingRodUndo(ServerPlayer player, List planned) { - this.level = player.level(); + this(player, planned, null); + } + + BuildingRodUndo(ServerPlayer player, List planned, @Nullable BoundingBox bounds) { + this.level = player.serverLevel(); + this.region = new BuildingRegionSnapshot(player, bounds == null ? bounds(planned) : bounds); for (var group : planned) { - if (group.cells.isEmpty()) continue; - PlacedGroup saved = new PlacedGroup(); - for (var cell : group.cells) { - saved.before.add(Saved.capture(this.level, cell.pos())); - saved.expected.add(cell.state()); - this.positions.put(cell.pos(), saved); + if (group.cells.isEmpty() && group.entities.isEmpty()) continue; + Receipt receipt = new Receipt(); + if (!player.isCreative()) { + group.materials.forEach(stack -> receipt.materials.add(stack.copy())); + group.returned.forEach(stack -> receipt.returned.add(stack.copy())); + group.entities.forEach(entity -> { + if (!entity.returned().isEmpty()) receipt.returned.add(entity.returned().copy()); + }); + group.fluidPayments.forEach(payment -> receipt.fluids.add( + new BuildingMaterials.FluidPayment(payment.storage(), payment.fluid().copy()))); } - if (!player.isCreative()) group.materials.forEach(stack -> saved.materials.add(stack.copy())); - group.returned.forEach(stack -> saved.returned.add(stack.copy())); - this.groups.add(saved); + group.entities.forEach(entity -> this.entityReceipts.put(entity, receipt)); + this.receipts.add(receipt); } } - void finish(ServerPlayer player) { - for (PlacedGroup group : this.groups) { - for (int index = 0; index < group.before.size(); index++) { - Saved after = Saved.capture(this.level, group.before.get(index).pos()); - group.after.add(after); - if (after.state().getBlock() != group.expected.get(index).getBlock()) group.replaced = true; - } + private static BoundingBox bounds(List planned) { + List positions = new ArrayList<>(); + planned.forEach(group -> { + group.cells.forEach(cell -> positions.add(cell.pos())); + group.entities.forEach(entity -> { + var pos = entity.entityNbt().getList("Pos", Tag.TAG_DOUBLE); + if (pos.size() == 3) positions.add(BlockPos.containing(pos.getDouble(0), pos.getDouble(1), pos.getDouble(2))); + }); + }); + return BoundingBox.encapsulatingPositions(positions).orElseThrow(); + } + + void consumed() { + List restoredItems = this.region.restoredMaterials(); + for (Receipt receipt : this.receipts) { + for (ItemStack material : receipt.materials) material.shrink(BuildingRegionSnapshot.subtract(restoredItems, material)); + receipt.materials.removeIf(ItemStack::isEmpty); } + } + + void finish(ServerPlayer player) { HISTORY.put(player, this); } + void recordEntity(EntityBuildAdapter.Planned plan, Entity entity) { + if (entity instanceof LeashFenceKnotEntity) { + this.recordAuxiliary(entity); + return; + } + Receipt receipt = this.entityReceipts.get(plan); + if (receipt != null) receipt.entities.put(entity.getUUID(), entity); + } + + void recordAuxiliary(Entity entity) { + this.auxiliary.put(entity.getUUID(), entity); + } + public static void replaced(Level level, BlockPos pos, BlockState before, BlockState after) { if (level.isClientSide || before.getBlock() == after.getBlock()) return; for (BuildingRodUndo undo : HISTORY.values()) { - if (undo.level != level) continue; - PlacedGroup group = undo.positions.get(pos); - if (group != null) group.replaced = true; + if (undo.level == level && !undo.restoring && !undo.restored && undo.region.contains(pos)) { + undo.changedPositions.add(pos.immutable()); + } + } + } + + public static void spawnedBy(Entity source, Entity spawned) { + for (BuildingRodUndo undo : HISTORY.values()) { + if (undo.restoring) continue; + if (!undo.restored && (undo.derived.containsKey(source.getUUID()) || undo.region.containsEntity(source.getUUID()))) { + undo.derived.put(spawned.getUUID(), spawned); + } + for (Receipt receipt : undo.receipts) { + if (!receipt.accepted && (receipt.entities.containsKey(source.getUUID()) || receipt.drops.containsKey(source.getUUID()))) { + receipt.drops.put(spawned.getUUID(), spawned); + break; + } + } + } + } + + @SubscribeEvent + public static void blockDrops(BlockDropsEvent event) { + for (BuildingRodUndo undo : HISTORY.values()) { + if (undo.level != event.getLevel() || undo.restoring || undo.restored || !undo.region.contains(event.getPos())) continue; + event.getDrops().forEach(drop -> undo.derived.put(drop.getUUID(), drop)); + } + } + + @SubscribeEvent + public static void fallingBlock(EntityJoinLevelEvent event) { + if (event.loadedFromDisk()) return; + Entity entity = event.getEntity(); + BlockPos source = entity instanceof FallingBlockEntity falling ? falling.getStartPos() + : entity instanceof PrimedTnt ? entity.blockPosition() : null; + if (source == null) return; + for (BuildingRodUndo undo : HISTORY.values()) { + if (undo.level == event.getLevel() && !undo.restoring && !undo.restored && undo.changedPositions.contains(source)) { + undo.derived.put(entity.getUUID(), entity); + } + } + } + + private boolean canRemove(ServerPlayer player, Map owned) { + for (var entry : owned.entrySet()) { + Entity entity = BuildingRegionSnapshot.find(this.level, entry.getKey()); + if (entity instanceof Player) return false; + if (entity != null) { + if (!entity.level().mayInteract(player, entity.blockPosition())) return false; + } else { + Entity.RemovalReason reason = entry.getValue().getRemovalReason(); + if (reason == null || !reason.shouldDestroy()) return false; + } + } + return true; + } + + private void remove(Map owned) { + for (UUID uuid : owned.keySet()) { + Entity entity = BuildingRegionSnapshot.find(this.level, uuid); + if (entity == null || entity instanceof Player) continue; + if (entity instanceof Leashable leashable) leashable.setLeashData(null); + entity.discard(); } } @@ -88,42 +195,58 @@ public static void undo(ServerPlayer player) { BuildingRodService.message(player, "nothing_to_undo"); return; } - List restore = new ArrayList<>(); - BuildingMaterials materials = new BuildingMaterials(player); - for (PlacedGroup group : undo.groups) { - if (group.replaced || group.after.stream().anyMatch(saved -> !saved.matches(undo.level) - || !BuildingRodService.canModify(player, saved.pos()))) { - continue; + if (!undo.restored && !undo.region.canRestore(player)) { + BuildingRodService.message(player, "blocked"); + return; + } + BuildingMaterials recovery = new BuildingMaterials(player, false); + List accepted = new ArrayList<>(); + for (Receipt receipt : undo.receipts) { + if (receipt.accepted || !undo.canRemove(player, receipt.entities)) continue; + if (recovery.reserve(receipt.returned)) accepted.add(receipt); + } + if (!recovery.consume()) return; + undo.restoring = true; + try { + for (Receipt receipt : accepted) { + undo.remove(receipt.entities); + undo.remove(receipt.drops); + receipt.accepted = true; } - if (group.after.stream().anyMatch(saved -> CommonHooks.fireBlockBreak(undo.level, - player.gameMode.getGameModeForPlayer(), player, saved.pos(), saved.state()).isCanceled())) { - continue; + if (!undo.restored) { + undo.remove(undo.derived); + undo.region.restore(); + undo.restored = true; } - if (materials.reserve(group.returned)) restore.add(group); - } - if (!materials.consume()) return; - HISTORY.remove(player); - for (PlacedGroup group : restore) { - for (Saved saved : group.before) BuildingCommit.set(undo.level, saved.pos(), saved.state()); - for (Saved saved : group.before) { - BlockEntity entity = undo.level.getBlockEntity(saved.pos()); - if (entity != null && !saved.nbt().isEmpty()) { - entity.loadWithComponents(saved.nbt(), player.registryAccess()); - entity.setChanged(); - undo.level.sendBlockUpdated(saved.pos(), saved.state(), saved.state(), Block.UPDATE_CLIENTS); + for (Entity original : undo.auxiliary.values()) { + Entity knot = BuildingRegionSnapshot.find(undo.level, original.getUUID()); + if (knot == null || !knot.level().getEntities(knot, new AABB(knot.blockPosition()).inflate(16), + entity -> entity instanceof Leashable leashable && leashable.getLeashHolder() == knot).isEmpty()) { + continue; } + knot.discard(); } - for (ItemStack material : group.materials) player.getInventory().placeItemBackInInventory(material); - } - for (PlacedGroup group : restore) { - for (int index = 0; index < group.before.size(); index++) { - Saved saved = group.before.get(index); - undo.level.markAndNotifyBlock(saved.pos(), undo.level.getChunkAt(saved.pos()), - group.after.get(index).state(), saved.state(), Block.UPDATE_ALL, Block.UPDATE_LIMIT); + for (Receipt receipt : undo.receipts) { + if (!receipt.accepted) continue; + receipt.materials.forEach(stack -> player.getInventory().placeItemBackInInventory(stack.copy())); + receipt.materials.clear(); + for (var payment : receipt.fluids) { + while (!payment.fluid().isEmpty()) { + IFluidHandler target = StoragePortManager.findRefillTarget(payment.storage(), payment.fluid()); + if (target == null) break; + int filled = target.fill(payment.fluid().copy(), IFluidHandler.FluidAction.EXECUTE); + if (filled <= 0) break; + payment.fluid().shrink(filled); + } + } } + undo.receipts.removeIf(receipt -> receipt.accepted && receipt.fluids.stream().allMatch(payment -> payment.fluid().isEmpty())); + if (undo.receipts.isEmpty()) HISTORY.remove(player); + } finally { + undo.restoring = false; } player.getInventory().setChanged(); player.containerMenu.broadcastChanges(); - BuildingRodService.message(player, restore.isEmpty() ? "nothing_to_undo" : "undone"); + BuildingRodService.message(player, undo.receipts.isEmpty() ? "undone" : "undo_partial"); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/DynamicBuildingEntities.java b/src/main/java/dev/dubhe/anvilcraft/building/DynamicBuildingEntities.java new file mode 100644 index 0000000000..529f0741e3 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/DynamicBuildingEntities.java @@ -0,0 +1,188 @@ +package dev.dubhe.anvilcraft.building; + +import dev.dubhe.anvilcraft.api.sliding.SlidingBlockSection; +import dev.dubhe.anvilcraft.block.UseItemOnBlock; +import dev.dubhe.anvilcraft.entity.AnimateAscendingBlockEntity; +import dev.dubhe.anvilcraft.entity.IonocraftEntity; +import dev.dubhe.anvilcraft.entity.SlidingBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Container; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.npc.InventoryCarrier; +import net.minecraft.world.entity.projectile.AbstractArrow; +import net.minecraft.world.entity.projectile.ThrowableItemProjectile; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +final class DynamicBuildingEntities implements EntityBuildAdapter { + static boolean requiresOperator(CompoundTag tag, ServerLevel level) { + if (tag.contains("SlidingBlocks")) { + var section = SlidingBlockSection.CODEC.parse(NbtOps.INSTANCE, tag.getCompound("SlidingBlocks")).getOrThrow(); + return section.blocks().stream().anyMatch(block -> + BuildingBlockMaterial.extract(block.state(), block.beTag(), level).requiresOperator()); + } + if (tag.contains("BlockState") && !MagnetizedNodeBuildAdapter.isNode(tag)) { + var state = NbtUtils.readBlockState(level.holderLookup(Registries.BLOCK), + tag.getCompound("BlockState")); + return BuildingBlockMaterial.extract(state, tag.getCompound("TileEntityData"), level).requiresOperator(); + } + return false; + } + + @Override + public boolean matches(Entity entity, CompoundTag nbt) { + return entity instanceof Mob || entity instanceof FallingBlockEntity || this.matches(entity.getType(), nbt); + } + + @Override + public boolean matches(EntityType type, CompoundTag nbt) { + return nbt.contains("BlockState") || nbt.contains("Health") || SpawnEggItem.byId(type) != null + || nbt.contains("item") || type == EntityType.SNOWBALL || type == EntityType.EGG || type == EntityType.ENDER_PEARL + || type == EntityType.POTION || type == EntityType.FIREBALL || type == EntityType.SMALL_FIREBALL + || type == EntityType.FALLING_BLOCK || type == EntityType.TNT || type == EntityType.ITEM + || type == EntityType.END_CRYSTAL || "anvilcraft:sliding_block".equals(nbt.getString("id")) + || "anvilcraft:ionocraft".equals(nbt.getString("id")); + } + + @Override + public Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, CompoundTag nbt) { + Entity entity = EntityType.create(nbt, level).orElse(null); + if (entity instanceof AbstractArrow arrow) { + ItemStack material = arrow.getPickupItemStackOrigin().copyWithCount(1); + return material.isEmpty() ? Planned.skip() + : new Planned(material, ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + if (entity instanceof ThrowableItemProjectile projectile) { + return new Planned(projectile.getItem().copyWithCount(1), ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + if (entity != null && (entity.getType() == EntityType.FIREBALL || entity.getType() == EntityType.SMALL_FIREBALL)) { + return new Planned(new ItemStack(Items.FIRE_CHARGE), ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + if (entity instanceof AnimateAscendingBlockEntity) { + return new Planned(ItemStack.EMPTY, ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + if (entity instanceof ItemEntity item) { + return new Planned(item.getItem().copy(), ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + if (entity instanceof IonocraftEntity vehicle) { + ItemStack material = vehicle.getPickResult(); + if (material == null || material.isEmpty()) return Planned.skip(); + return new Planned(material, ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + if (entity != null && entity.getType() == EntityType.END_CRYSTAL) { + return new Planned(new ItemStack(Items.END_CRYSTAL), ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + if (entity instanceof SlidingBlockEntity) { + var section = SlidingBlockSection.CODEC + .parse(NbtOps.INSTANCE, nbt.getCompound("SlidingBlocks")).getOrThrow(); + List materials = new ArrayList<>(); + for (var block : section.blocks()) { + BuildingBlockMaterial material = BuildingBlockMaterial.extract(block.state(), block.beTag(), level); + if (BlueprintMultiblocks.shouldRecord(block.state())) { + if (material.stack().isEmpty() && !BlueprintIgnition.isIgnition(block.state())) return Planned.skip(); + if (!material.stack().isEmpty()) { + materials.add(new SlotStack(-1, material.stack().copyWithCount(BuildingRodService.materialCount(block.state())))); + } + for (ItemStack extra : BlueprintSpecialBlocks.extra(block.state())) materials.add(new SlotStack(-1, extra)); + ItemStack upgrade = UseItemOnBlock.materialFor(block.state()); + if (!upgrade.isEmpty()) materials.add(new SlotStack(-1, upgrade)); + } + for (var content : material.contents()) materials.add(new SlotStack(-1, content.stack())); + } + return new Planned(ItemStack.EMPTY, ItemStack.EMPTY, nbt.copy(), materials, List.of(), false); + } + if (entity instanceof FallingBlockEntity falling) { + BuildingBlockMaterial block = BuildingBlockMaterial.extract(falling.getBlockState(), nbt.getCompound("TileEntityData"), level); + ItemStack material = block.stack(); + if (material.isEmpty()) return Planned.skip(); + CompoundTag saved = nbt.copy(); + // time=0 的原版下落实体会先删除起点方块;蓝图已将实体与方块分别恢复。 + saved.putInt("Time", Math.max(1, saved.getInt("Time"))); + List costs = new ArrayList<>(); + for (var content : block.contents()) costs.add(new SlotStack(-1, content.stack())); + ItemStack upgrade = UseItemOnBlock.materialFor(falling.getBlockState()); + if (!upgrade.isEmpty()) costs.add(new SlotStack(-1, upgrade)); + return new Planned(material, ItemStack.EMPTY, saved, costs, List.of(), false); + } + if (entity instanceof Mob mob) { + List contents = new ArrayList<>(); + for (EquipmentSlot slot : EquipmentSlot.values()) { + ItemStack stack = mob.getItemBySlot(slot); + if (!stack.isEmpty()) contents.add(new SlotStack(100 + slot.ordinal(), stack.copy())); + mob.setItemSlot(slot, ItemStack.EMPTY); + } + if (mob instanceof InventoryCarrier carrier) { + Container inventory = carrier.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty()) contents.add(new SlotStack(i, stack.copy())); + inventory.setItem(i, ItemStack.EMPTY); + } + } + CompoundTag saved = new CompoundTag(); + mob.save(saved); + saved.remove("UUID"); + SpawnEggItem egg = SpawnEggItem.byId(mob.getType()); + return new Planned(egg == null ? ItemStack.EMPTY : new ItemStack(egg), ItemStack.EMPTY, + saved, contents, List.of(), false); + } + if (entity != null && entity.getType() == EntityType.TNT) { + return new Planned(new ItemStack(Items.TNT), ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + return Planned.skip(); + } + + @Override + public @Nullable Entity spawn(ServerLevel level, BuildingEntityOp op) { + Entity entity = EntityBuildAdapter.super.spawn(level, op); + if (entity instanceof FallingBlockEntity falling) falling.setStartPos(falling.blockPosition()); + if (entity instanceof SlidingBlockEntity sliding && sliding.getMoveDirection() != null) { + sliding.setMoveDirection(sliding.getMoveDirection()); + } + return entity; + } + + static final class Outlet implements EntityBuildAdapter { + static boolean isOutlet(CompoundTag tag) { + return "anvilcraft:cauldron_outlet".equals(tag.getString("id")); + } + + static BlockPos support(StructureSnapshot.EntityEntry entry) { + Direction direction = Direction.from3DDataValue(entry.nbt().getInt("AttachedDirection")); + Vec3 inward = entry.pos().subtract(Vec3.atLowerCornerOf(direction.getNormal()).scale(0.51)); + return BlockPos.containing(inward); + } + + @Override + public boolean matches(EntityType type, CompoundTag nbt) { + return isOutlet(nbt); + } + + @Override + public boolean requiresHammer() { + return true; + } + + @Override + public Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, CompoundTag nbt) { + return new Planned(ItemStack.EMPTY, ItemStack.EMPTY, nbt.copy(), List.of(), List.of(), false); + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/EntityBuildAdapter.java b/src/main/java/dev/dubhe/anvilcraft/building/EntityBuildAdapter.java index eedac990c2..a6bce60116 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/EntityBuildAdapter.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/EntityBuildAdapter.java @@ -34,8 +34,20 @@ public static Planned skip() { boolean matches(EntityType type, CompoundTag nbt); + default boolean matches(Entity entity, CompoundTag nbt) { + return this.matches(entity.getType(), nbt); + } + Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, CompoundTag transformedNbt); + default ItemStack requiredTool() { + return ItemStack.EMPTY; + } + + default boolean requiresHammer() { + return false; + } + default ItemStack returnAfterDeliver(ServerLevel level, BuildingEntityOp op) { return op.returnStack().copy(); } @@ -47,7 +59,7 @@ default ItemStack returnAfterDeliver(ServerLevel level, BuildingEntityOp op) { } CompoundTag copy = nbt.copy(); copy.remove("UUID"); - return EntityType.create(copy, level).map(entity -> { + return EntityBuildAdapters.create(copy, level).map(entity -> { if (!level.addFreshEntity(entity)) { return null; } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/EntityBuildAdapters.java b/src/main/java/dev/dubhe/anvilcraft/building/EntityBuildAdapters.java index 2cd6ec1a5b..e0cba6ae53 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/EntityBuildAdapters.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/EntityBuildAdapters.java @@ -1,5 +1,6 @@ package dev.dubhe.anvilcraft.building; +import dev.dubhe.anvilcraft.entity.WeaponBeamEntity; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.DoubleTag; @@ -9,14 +10,16 @@ import net.minecraft.world.entity.AreaEffectCloud; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.LightningBolt; import net.minecraft.world.entity.Marker; -import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.npc.InventoryCarrier; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.FishingHook; -import net.minecraft.world.entity.projectile.Projectile; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import java.util.ArrayList; @@ -30,6 +33,9 @@ public final class EntityBuildAdapters { static { VanillaBuildingEntities.register(); + register(new MagnetizedNodeBuildAdapter()); + register(new DynamicBuildingEntities.Outlet()); + register(new DynamicBuildingEntities()); } private EntityBuildAdapters() { @@ -48,13 +54,24 @@ public static Optional find(EntityType type, CompoundTag return Optional.empty(); } + public static Optional find(Entity entity, CompoundTag nbt) { + return REGISTRY.stream().filter(adapter -> adapter.matches(entity, nbt)).findFirst(); + } + + public static Optional create(CompoundTag tag, Level level) { + Entity entity = BlueprintLeashes.create(tag, level); + if (entity != null) BlueprintEntities.restoreMotion(entity, tag); + return Optional.ofNullable(entity); + } + public static boolean isTransient(EntityType type) { Class base = type.getBaseClass(); - return Player.class.isAssignableFrom(base) - || ItemEntity.class.isAssignableFrom(base) + return type == EntityType.PLAYER || type == EntityType.EXPERIENCE_ORB + || type == EntityType.LIGHTNING_BOLT + || type == EntityType.AREA_EFFECT_CLOUD || type == EntityType.FISHING_BOBBER || type == EntityType.MARKER + || Player.class.isAssignableFrom(base) || ExperienceOrb.class.isAssignableFrom(base) || LightningBolt.class.isAssignableFrom(base) - || Projectile.class.isAssignableFrom(base) || AreaEffectCloud.class.isAssignableFrom(base) || FishingHook.class.isAssignableFrom(base) || Marker.class.isAssignableFrom(base) @@ -62,6 +79,12 @@ public static boolean isTransient(EntityType type) { || type == EntityType.INTERACTION; } + public static boolean isTransient(Entity entity) { + return isTransient(entity.getType()) || entity instanceof Player + || entity instanceof ExperienceOrb || entity instanceof LightningBolt || entity instanceof WeaponBeamEntity + || entity instanceof AreaEffectCloud || entity instanceof FishingHook || entity instanceof Marker; + } + @Nullable public static Entity spawn(ServerLevel level, BuildingEntityOp op) { return findType(op) @@ -73,7 +96,7 @@ public static Entity spawn(ServerLevel level, BuildingEntityOp op) { } CompoundTag copy = nbt.copy(); copy.remove("UUID"); - return EntityType.create(copy, level).map(entity -> { + return create(copy, level).map(entity -> { if (!level.addFreshEntity(entity)) { return null; } @@ -93,6 +116,17 @@ public static void insertContents( List contents, HolderLookup.Provider registries ) { + if (entity instanceof Mob mob) { + for (var content : contents) { + if (content.slot() >= 100 && content.slot() < 100 + EquipmentSlot.values().length) { + mob.setItemSlot(EquipmentSlot.values()[content.slot() - 100], content.stack().copy()); + } else if (mob instanceof InventoryCarrier carrier + && content.slot() >= 0 && content.slot() < carrier.getInventory().getContainerSize()) { + carrier.getInventory().setItem(content.slot(), content.stack().copy()); + } + } + return; + } Optional adapter = find(entity.getType(), new CompoundTag()); if (adapter.isPresent()) { adapter.get().insertContents(entity, contents, registries); diff --git a/src/main/java/dev/dubhe/anvilcraft/building/LitematicaImporter.java b/src/main/java/dev/dubhe/anvilcraft/building/LitematicaImporter.java index 941d9b0ec7..8bbc113356 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/LitematicaImporter.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/LitematicaImporter.java @@ -8,6 +8,7 @@ import net.minecraft.nbt.ListTag; import net.minecraft.nbt.LongArrayTag; import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; import java.util.ArrayList; import java.util.List; @@ -108,6 +109,7 @@ public static ConvertedStructure convert(CompoundTag litematic) throws Construct int overlapCount = 0; ListTag entitiesOut = new ListTag(); + List ticks = new ArrayList<>(); for (ParsedRegion region : parsedRegions) { int[] paletteMap = new int[region.palette().size()]; for (int index = 0; index < region.palette().size(); index++) { @@ -115,6 +117,19 @@ public static ConvertedStructure convert(CompoundTag litematic) throws Construct } overlapCount += region.blit(min, size, paletteMap, stateByPosition, nbtByPosition, covered); region.appendEntities(min, entitiesOut); + for (boolean fluid : List.of(false, true)) { + ListTag saved = regions.getCompound(region.name()).getList( + fluid ? "PendingFluidTicks" : "PendingBlockTicks", Tag.TAG_COMPOUND); + for (Tag value : saved) { + CompoundTag tick = (CompoundTag) value; + BlockPos pos = new BlockPos(tick.getInt("x"), tick.getInt("y"), tick.getInt("z")) + .offset(region.min()).subtract(min); + ticks.add(new BlueprintTicks.Entry(pos, + ResourceLocation.parse(tick.getString(fluid ? "Fluid" : "Block")), + (int) Math.clamp(tick.getLong("Time"), 0, Integer.MAX_VALUE), + tick.getInt("Priority"), fluid, tick.getLong("SubTick"))); + } + } } if (overlapCount > 0) { warnings.add(new StructureSnapshotCodec.BlueprintWarning( @@ -155,6 +170,7 @@ public static ConvertedStructure convert(CompoundTag litematic) throws Construct } out.put("blocks", blocksTag); out.put("entities", entitiesOut); + BlueprintTicks.write(out, ticks); return new ConvertedStructure(out, warnings); } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/MagnetizedNodeBuildAdapter.java b/src/main/java/dev/dubhe/anvilcraft/building/MagnetizedNodeBuildAdapter.java new file mode 100644 index 0000000000..f8585914e1 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/building/MagnetizedNodeBuildAdapter.java @@ -0,0 +1,52 @@ +package dev.dubhe.anvilcraft.building; + +import dev.dubhe.anvilcraft.init.item.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.EmptyBlockGetter; + +import java.util.List; + +final class MagnetizedNodeBuildAdapter implements EntityBuildAdapter { + private static final String ID = "anvilcraft:magnetized_node"; + + static boolean isNode(CompoundTag tag) { + return ID.equals(tag.getString("id")); + } + + static BlockPos support(StructureSnapshot snapshot, StructureSnapshot.EntityEntry entry) { + String block = entry.nbt().getCompound("BlockState").getString("Name"); + BlockPos column = BlockPos.containing(entry.pos()); + for (var candidate : snapshot.blocks()) { + BlockPos pos = candidate.pos(); + if (pos.getX() != column.getX() || pos.getZ() != column.getZ()) continue; + var state = snapshot.stateOf(candidate); + if (!BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString().equals(block)) continue; + double height = state.getCollisionShape(EmptyBlockGetter.INSTANCE, pos).max(Direction.Axis.Y, 0.5, 0.5); + if (Math.abs(pos.getY() + height - entry.pos().y) < 1.0E-5) return pos; + } + throw new IllegalArgumentException("Missing magnetized node support at " + entry.pos()); + } + + @Override + public boolean matches(EntityType type, CompoundTag nbt) { + return isNode(nbt); + } + + @Override + public ItemStack requiredTool() { + return ModItems.MAGNET.asStack(); + } + + @Override + public Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, CompoundTag transformedNbt) { + if (NbtUtils.readBlockPos(transformedNbt, "BlockPos").isEmpty()) return Planned.skip(); + return new Planned(ItemStack.EMPTY, ItemStack.EMPTY, transformedNbt.copy(), List.of(), List.of(), false); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/building/ScannerDiskNormalizer.java b/src/main/java/dev/dubhe/anvilcraft/building/ScannerDiskNormalizer.java index 9bab3fb07a..c5ac9619e1 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/ScannerDiskNormalizer.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/ScannerDiskNormalizer.java @@ -35,6 +35,7 @@ public static StructureSnapshot normalize(StructureSnapshot snapshot, Direction blocks.add(new StructureSnapshot.BlockEntry(normalized, entry.stateIndex(), entry.nbt())); } + StructureSnapshot normalizedBlocks = new StructureSnapshot(normalizedSize, snapshot.palette(), blocks, List.of()); List entities = new ArrayList<>(snapshot.entities().size()); for (StructureSnapshot.EntityEntry entry : snapshot.entities()) { Vec3 pos = entry.pos(); @@ -45,7 +46,8 @@ public static StructureSnapshot normalize(StructureSnapshot snapshot, Direction case EAST -> new Vec3(sz - pos.z, y, pos.x); default -> new Vec3(pos.x, y, pos.z); }; - BlockPos blockPos = entry.blockPos(); + BlockPos blockPos = DynamicBuildingEntities.Outlet.isOutlet(entry.nbt()) + ? DynamicBuildingEntities.Outlet.support(entry) : entry.blockPos(); int blockY = upsideDown ? sy - 1 - blockPos.getY() : blockPos.getY(); BlockPos normalizedBlockPos = switch (facing) { case SOUTH -> new BlockPos(sx - 1 - blockPos.getX(), blockY, sz - 1 - blockPos.getZ()); @@ -53,9 +55,24 @@ public static StructureSnapshot normalize(StructureSnapshot snapshot, Direction case EAST -> new BlockPos(sz - 1 - blockPos.getZ(), blockY, blockPos.getX()); default -> new BlockPos(blockPos.getX(), blockY, blockPos.getZ()); }; + if (MagnetizedNodeBuildAdapter.isNode(entry.nbt())) { + normalizedBlockPos = MagnetizedNodeBuildAdapter.support(normalizedBlocks, + new StructureSnapshot.EntityEntry(normalized, normalizedBlockPos, entry.nbt())); + } entities.add(new StructureSnapshot.EntityEntry(normalized, normalizedBlockPos, entry.nbt())); } - return new StructureSnapshot(normalizedSize, snapshot.palette(), blocks, entities); + List ticks = snapshot.ticks().stream().map(tick -> { + BlockPos pos = tick.pos(); + int y = upsideDown ? sy - 1 - pos.getY() : pos.getY(); + BlockPos normalized = switch (facing) { + case SOUTH -> new BlockPos(sx - 1 - pos.getX(), y, sz - 1 - pos.getZ()); + case WEST -> new BlockPos(pos.getZ(), y, sx - 1 - pos.getX()); + case EAST -> new BlockPos(sz - 1 - pos.getZ(), y, pos.getX()); + default -> new BlockPos(pos.getX(), y, pos.getZ()); + }; + return tick.at(normalized); + }).toList(); + return new StructureSnapshot(normalizedSize, snapshot.palette(), blocks, entities, ticks, snapshot.capturedAt()); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/StructureSnapshot.java b/src/main/java/dev/dubhe/anvilcraft/building/StructureSnapshot.java index ca95f31cd9..2ac110f4e2 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/StructureSnapshot.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/StructureSnapshot.java @@ -18,12 +18,19 @@ public record StructureSnapshot( Vec3i size, List palette, List blocks, - List entities + List entities, + List ticks, + long capturedAt ) { + public StructureSnapshot(Vec3i size, List palette, List blocks, List entities) { + this(size, palette, blocks, entities, List.of(), 0); + } + public StructureSnapshot { palette = List.copyOf(palette); blocks = List.copyOf(blocks); entities = List.copyOf(entities); + ticks = List.copyOf(ticks); } /** 一个方块条目;nbt 为方块实体的完整数据(saveWithId 语义),无方块实体时为空。 */ diff --git a/src/main/java/dev/dubhe/anvilcraft/building/StructureSnapshotCodec.java b/src/main/java/dev/dubhe/anvilcraft/building/StructureSnapshotCodec.java index 1488b121ca..04a00b6682 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/StructureSnapshotCodec.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/StructureSnapshotCodec.java @@ -83,7 +83,8 @@ public static ParsedSnapshot parse(CompoundTag tag, HolderLookup.Provider regist List blocks = parseBlocks(tag, size, palette.size(), warnings); List entities = parseEntities(tag, registries, warnings); - StructureSnapshot snapshot = canonicalize(new StructureSnapshot(size, palette, blocks, entities)); + StructureSnapshot snapshot = canonicalize(new StructureSnapshot(size, palette, blocks, entities, + BlueprintTicks.read(tag), tag.getLong("anvilcraft:captured_at"))); return new ParsedSnapshot(snapshot, List.copyOf(warnings)); } @@ -287,7 +288,7 @@ public static StructureSnapshot canonicalize(StructureSnapshot snapshot) { blocks.add(new StructureSnapshot.BlockEntry(entry.pos(), index, entry.nbt())); } - List entities = new ArrayList<>(snapshot.entities()); + List entities = new ArrayList<>(BlueprintEntities.flatten(snapshot.entities())); entities.sort(Comparator .comparingInt((StructureSnapshot.EntityEntry entry) -> entry.blockPos().getY()) .thenComparingInt(entry -> entry.blockPos().getZ()) @@ -297,7 +298,7 @@ public static StructureSnapshot canonicalize(StructureSnapshot snapshot) { .thenComparingDouble(entry -> entry.pos().x) .thenComparing(entry -> entry.nbt().getString("id"))); - return new StructureSnapshot(snapshot.size(), palette, blocks, entities); + return new StructureSnapshot(snapshot.size(), palette, blocks, entities, snapshot.ticks(), snapshot.capturedAt()); } /** 写出规范结构 NBT;输入必须已经过 {@link #canonicalize}。 */ @@ -348,6 +349,8 @@ public static CompoundTag write(StructureSnapshot snapshot) { entitiesTag.add(entityTag); } tag.put("entities", entitiesTag); + BlueprintTicks.write(tag, snapshot.ticks()); + tag.putLong("anvilcraft:captured_at", snapshot.capturedAt()); return tag; } diff --git a/src/main/java/dev/dubhe/anvilcraft/building/VanillaBuildingEntities.java b/src/main/java/dev/dubhe/anvilcraft/building/VanillaBuildingEntities.java index bdbf6c649c..5cc834c0ea 100644 --- a/src/main/java/dev/dubhe/anvilcraft/building/VanillaBuildingEntities.java +++ b/src/main/java/dev/dubhe/anvilcraft/building/VanillaBuildingEntities.java @@ -1,9 +1,10 @@ package dev.dubhe.anvilcraft.building; import dev.dubhe.anvilcraft.api.fluid.IFluidHandlerHolder; +import dev.dubhe.anvilcraft.entity.FluidTankMinecartEntity; +import dev.dubhe.anvilcraft.init.item.ModItems; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.Tag; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Container; import net.minecraft.world.entity.Entity; @@ -11,13 +12,13 @@ import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.entity.decoration.LeashFenceKnotEntity; 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.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; -import net.minecraft.world.item.SpawnEggItem; import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.fluids.capability.IFluidHandler; import net.neoforged.neoforge.items.IItemHandler; @@ -36,12 +37,12 @@ public static void register() { EntityBuildAdapters.register(new VehicleAdapter()); EntityBuildAdapters.register(new ArmorStandAdapter()); EntityBuildAdapters.register(new HangingAdapter()); - EntityBuildAdapters.register(new SpawnEggAdapter()); + } private static CompoundTag saveSanitized(Entity entity) { CompoundTag tag = new CompoundTag(); - entity.save(tag); + BlueprintLeashes.save(entity, tag); tag.remove("UUID"); return tag; } @@ -73,6 +74,11 @@ private static void extractHandler(IItemHandler handler, List type, CompoundTag nbt) { if (type == EntityType.BOAT @@ -92,7 +98,7 @@ public boolean matches(EntityType type, CompoundTag nbt) { @Override public Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, CompoundTag transformedNbt) { EntityType type = EntityType.by(transformedNbt).orElse(null); - Entity entity = EntityType.create(transformedNbt, level).orElse(null); + Entity entity = EntityBuildAdapters.create(transformedNbt, level).orElse(null); List contents = new ArrayList<>(); ItemStack material = ItemStack.EMPTY; if (entity instanceof Container container) { @@ -112,7 +118,21 @@ public Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, Comp } CompoundTag sanitized = entity == null ? transformedNbt.copy() : saveSanitized(entity); sanitized.putString("id", transformedNbt.getString("id")); - return new Planned(material, ItemStack.EMPTY, sanitized, List.copyOf(contents), List.of(), false); + List fluids = new ArrayList<>(); + if (entity != null) { + IFluidHandler handler = fluidHandlerOf(entity); + if (handler != null) { + for (int tank = 0; tank < handler.getTanks(); tank++) { + if (!handler.getFluidInTank(tank).isEmpty()) { + fluids.add(new FluidBuildAdapter.TankFluid(tank, handler.getFluidInTank(tank).copy())); + } + } + } + if (entity instanceof FluidTankMinecartEntity) { + material = ModItems.FLUID_TANK_MINECART.asStack(); + } + } + return new Planned(material, ItemStack.EMPTY, sanitized, List.copyOf(contents), fluids, false); } } @@ -124,7 +144,7 @@ public boolean matches(EntityType type, CompoundTag nbt) { @Override public Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, CompoundTag transformedNbt) { - Entity entity = EntityType.create(transformedNbt, level).orElse(null); + Entity entity = EntityBuildAdapters.create(transformedNbt, level).orElse(null); if (!(entity instanceof ArmorStand stand)) { return Planned.skip(); } @@ -166,30 +186,33 @@ private static final class HangingAdapter implements EntityBuildAdapter { public boolean matches(EntityType type, CompoundTag nbt) { return type == EntityType.ITEM_FRAME || type == EntityType.GLOW_ITEM_FRAME - || type == EntityType.PAINTING; + || type == EntityType.PAINTING || type == EntityType.LEASH_KNOT; } @Override public Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, CompoundTag transformedNbt) { - Entity entity = EntityType.create(transformedNbt, level).orElse(null); + Entity entity = EntityBuildAdapters.create(transformedNbt, level).orElse(null); if (entity == null) { return Planned.skip(); } List contents = new ArrayList<>(); ItemStack material; - if (entity instanceof ItemFrame frame) { - material = new ItemStack(frame.getType() == EntityType.GLOW_ITEM_FRAME - ? Items.GLOW_ITEM_FRAME - : Items.ITEM_FRAME); - ItemStack displayed = frame.getItem(); - if (!displayed.isEmpty()) { - contents.add(new SlotStack(0, displayed.copy())); - frame.setItem(ItemStack.EMPTY); + switch (entity) { + case ItemFrame frame -> { + material = new ItemStack(frame.getType() == EntityType.GLOW_ITEM_FRAME + ? Items.GLOW_ITEM_FRAME + : Items.ITEM_FRAME); + ItemStack displayed = frame.getItem(); + if (!displayed.isEmpty()) { + contents.add(new SlotStack(0, displayed.copy())); + frame.setItem(ItemStack.EMPTY); + } + } + case Painting painting -> material = new ItemStack(Items.PAINTING); + case LeashFenceKnotEntity leashFenceKnotEntity -> material = ItemStack.EMPTY; + default -> { + return Planned.skip(); } - } else if (entity instanceof Painting) { - material = new ItemStack(Items.PAINTING); - } else { - return Planned.skip(); } return new Planned(material, ItemStack.EMPTY, saveSanitized(entity), List.copyOf(contents), List.of(), false); } @@ -203,38 +226,6 @@ public void insertContents(Entity entity, List contents, HolderLookup } } - private static final class SpawnEggAdapter implements EntityBuildAdapter { - @Override - public boolean matches(EntityType type, CompoundTag nbt) { - return SpawnEggItem.byId(type) != null; - } - - @Override - public Planned plan(ServerLevel level, StructureSnapshot.EntityEntry entry, CompoundTag transformedNbt) { - EntityType type = EntityType.by(transformedNbt).orElse(null); - SpawnEggItem egg = type == null ? null : SpawnEggItem.byId(type); - if (egg == null) { - return Planned.skip(); - } - CompoundTag sanitized = new CompoundTag(); - sanitized.putString("id", transformedNbt.getString("id")); - if (transformedNbt.contains("Pos", Tag.TAG_LIST)) { - sanitized.put("Pos", transformedNbt.getList("Pos", Tag.TAG_DOUBLE).copy()); - } - if (transformedNbt.contains("Rotation", Tag.TAG_LIST)) { - sanitized.put("Rotation", transformedNbt.getList("Rotation", Tag.TAG_FLOAT).copy()); - } - return new Planned( - new ItemStack(egg), - ItemStack.EMPTY, - sanitized, - List.of(), - List.of(), - false - ); - } - } - private static ItemStack vehicleItem(@Nullable EntityType type) { if (type == EntityType.HOPPER_MINECART) { return new ItemStack(Items.HOPPER_MINECART); diff --git a/src/main/java/dev/dubhe/anvilcraft/client/building/BlueprintClientFiles.java b/src/main/java/dev/dubhe/anvilcraft/client/building/BlueprintClientFiles.java index ad865700ba..6f4b73d929 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/building/BlueprintClientFiles.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/building/BlueprintClientFiles.java @@ -18,6 +18,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.UUID; import javax.annotation.Nullable; @@ -122,10 +123,12 @@ public static void receive(StructureScannerFileResultPacket packet) { try { if (!packet.error().isEmpty()) throw new IOException(packet.error()); if (exporting) { - if (packet.total() != 0 || packet.offset() != 0 || packet.bytes().length != 0) { + String exported = new String(packet.bytes(), StandardCharsets.UTF_8); + if (packet.total() != packet.bytes().length || packet.offset() != 0 + || !StructureFileTransfer.isSafeName(exported) || !exported.toLowerCase(Locale.ROOT).endsWith(".nbt")) { throw new IOException("Invalid export result"); } - message("exported", fileName); + message("exported", exported); if (Minecraft.getInstance().screen instanceof StructureScannerScreen screen && screen.getMenu().containerId == pendingContainerId) requestFiles(pendingContainerId); clearTransfer(); 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 fe12c07b4c..58a772fbb8 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodClient.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodClient.java @@ -27,9 +27,11 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; import net.neoforged.api.distmarker.Dist; @@ -65,6 +67,12 @@ public final class BuildingRodClient { private static Rotation rotationOffset = Rotation.NONE; private static double previewDistance = 5; private static boolean distanceHeld; + private static BlockPos selectionOffset = BlockPos.ZERO; + private static BlockPos blueprintOffset = BlockPos.ZERO; + @Nullable private static StructureSnapshot tiledSnapshot; + @Nullable private static BlueprintPlacement tiledPlacement; + private static BlockPos tiledOffset = BlockPos.ZERO; + private static List tiledBlueprints = List.of(); private static boolean useHeld; private static boolean invalidDisk; private static Direction face = Direction.UP; @@ -83,8 +91,10 @@ private BuildingRodClient() { public static void cancel() { BuildingRodTraditionalControls.clear(); - first = null; - patternSeed = PATTERN_RANDOM.nextLong(); + clearSelection(); + blueprintOffset = BlockPos.ZERO; + tiledSnapshot = null; + tiledBlueprints = List.of(); locked = false; layer = -1; yOffset = 0; @@ -137,9 +147,7 @@ public static void tick(ClientTickEvent.Post event) { } if (!active() || mc.screen != null || mc.level == null || mc.player == null) { if (!locked && !(traditional() && BuildingRodTraditionalControls.isActive())) cancel(); - first = null; - target = null; - distanceHeld = false; + clearSelection(); useHeld = mc.options.keyUse.isDown(); KEY_REPEAT.clear(); discardControlClicks(); @@ -174,25 +182,39 @@ public static void tick(ClientTickEvent.Post event) { if (mc.options.keyUse.isDown() && !useHeld) press(); if (!mc.options.keyUse.isDown() && useHeld) release(); controls(); + if (first != null) { + BoundingBox bounds = selectionBounds(); + mc.gui.setOverlayMessage(bounds == null ? Component.empty() + : Component.translatable("screen.anvilcraft.building_rod.selection_size", + bounds.getXSpan(), bounds.getYSpan(), bounds.getZSpan()), false); + } } private static void updateTarget() { placementCells = null; Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.level == null) return; - if (traditional() && snapshot != null) { + if (traditional() && snapshot != null && first == null) { BuildingRodTraditionalControls.updateTarget(); return; } - HitResult result = mc.player.pick(mc.player.blockInteractionRange(), 1, false); - boolean floating = snapshot != null && !locked && !traditional() && mc.isWindowActive() - && (physicalModifiers(mc.getWindow().getWindow()) & GLFW.GLFW_MOD_CONTROL) != 0; + boolean floating = canAdjustDistance(); + if (snapshot == null && first != null && distanceHeld && !floating) { + clearSelection(); + return; + } + HitResult result = snapshot == null && first == null + ? mc.level.clip(new ClipContext(mc.player.getEyePosition(), + mc.player.getEyePosition().add(mc.player.getLookAngle().scale(mc.player.blockInteractionRange())), + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, mc.player)) + : mc.player.pick(mc.player.blockInteractionRange(), 1, false); if (floating) { if (!distanceHeld && result.getType() == HitResult.Type.BLOCK) { previewDistance = Math.clamp(mc.player.getEyePosition().distanceTo(result.getLocation()), 1, mc.player.blockInteractionRange()); } target = BlockPos.containing(mc.player.getEyePosition().add(mc.player.getLookAngle().scale(previewDistance))); + if (first != null) target = target.offset(selectionOffset); face = Direction.UP; } else if (result instanceof BlockHitResult hit && result.getType() == HitResult.Type.BLOCK) { currentHit = hit; @@ -202,8 +224,10 @@ private static void updateTarget() { && BuildingRodFluids.isWater(BuildingRodItem.material(mc.player)); if (snapshot == null && BuildingRodItem.isPlacementMaterial(BuildingRodItem.material(mc.player)) && !BuildingRodItem.material(mc.player).is(ModItems.FILTER) && !waterlogging) { - UseOnContext use = BlockPlacementPicking.forPlacement( - new UseOnContext(mc.player, BuildingRodItem.materialHand(mc.player), hit)); + UseOnContext use = first == null + ? new UseOnContext(mc.level, mc.player, BuildingRodItem.materialHand(mc.player), + BuildingRodItem.material(mc.player), hit) + : BlockPlacementPicking.forPlacement(new UseOnContext(mc.player, BuildingRodItem.materialHand(mc.player), hit)); if (use instanceof BlockPlacementPicking.PlayerClick click && !click.anvilcraft$hasBlockHit()) { target = null; currentHit = null; @@ -223,6 +247,7 @@ private static void updateTarget() { return; } distanceHeld = floating; + if (snapshot != null && first != null) blueprintOffset = target.subtract(first); if (snapshot == null || locked || disk == null) return; Rotation rotation = viewRotation().getRotated(rotationOffset); BlueprintPlacement local = new BlueprintPlacement(BlockPos.ZERO, rotation, placement.mirror()); @@ -268,16 +293,20 @@ public static void interaction(InputEvent.InteractionKeyMappingTriggered event) private static void press() { useHeld = true; updateTarget(); + if (first != null && distanceHeld) { + confirmSelection(); + return; + } if (traditional() && disk != null) { BuildingRodTraditionalControls.press(); return; } if (snapshot != null) { if (target == null && !locked) return; - if (!locked) { - locked = true; - } else { + if (locked) { confirm(); + } else { + beginBlueprintSelection(); } } else if (target != null && BuildingRodItem.isPlacementMaterial(selected)) { first = target; @@ -289,16 +318,66 @@ private static void press() { private static void release() { useHeld = false; + if (distanceHeld) return; + confirmSelection(); + } + + static void beginBlueprintSelection() { + if (snapshot == null) return; + first = target == null ? placement.anchor() : target; + target = first; + blueprintOffset = BlockPos.ZERO; + locked = true; + } + + private static void confirmSelection() { if (first == null) return; - if (target != null) { + if (snapshot != null) { + clearSelection(); + if (traditional()) BuildingRodTraditionalControls.finishSelection(); + return; + } else if (target != null) { BuildingRodItemRenderer.preparePlacement(); PacketDistributor.sendToServer(new BuildingRodPacket(first, target, firstHit == null ? face : firstHit.getDirection(), false, Rotation.NONE, Mirror.NONE, false, firstHit, BuildingRodPacket.Action.PLACE, patternSeed)); } + clearSelection(); + } + + private static void clearSelection() { + if (first != null) Minecraft.getInstance().gui.setOverlayMessage(Component.empty(), false); first = null; firstHit = null; + target = null; + currentHit = null; patternSeed = PATTERN_RANDOM.nextLong(); placementCells = null; + distanceHeld = false; + selectionOffset = BlockPos.ZERO; + KEY_REPEAT.clear(); + } + + static List blueprintPlacements() { + if (snapshot == null) return List.of(); + if (tiledSnapshot != snapshot || !placement.equals(tiledPlacement) || !blueprintOffset.equals(tiledOffset)) { + tiledSnapshot = snapshot; + tiledPlacement = placement; + tiledOffset = blueprintOffset; + tiledBlueprints = placement.tile(snapshot, placement.anchor().offset(blueprintOffset)); + } + return tiledBlueprints; + } + + @Nullable + static BoundingBox selectionBounds() { + if (snapshot != null && (first != null || !blueprintOffset.equals(BlockPos.ZERO))) { + List copies = blueprintPlacements(); + if (!copies.isEmpty()) { + BoundingBox bounds = copies.getFirst().bounds(snapshot.size()); + return bounds.encapsulate(copies.getLast().bounds(snapshot.size())); + } + } + return first == null || target == null ? null : BoundingBox.fromCorners(first, target); } public static List placementPreview() { @@ -321,13 +400,13 @@ private static KeyMapping[] controlKeys() { private static boolean canControl() { Minecraft mc = Minecraft.getInstance(); - return (locked || canAdjustDistance()) && active() && !traditional() + return (first == null && locked && !traditional() || canAdjustDistance()) && active() && mc.screen == null && mc.getOverlay() == null && mc.isWindowActive(); } private static boolean canAdjustDistance() { Minecraft mc = Minecraft.getInstance(); - return !locked && snapshot != null && active() && !traditional() && mc.screen == null + return (first != null || !locked && snapshot != null && !traditional()) && active() && mc.screen == null && mc.getOverlay() == null && mc.isWindowActive() && (physicalModifiers(mc.getWindow().getWindow()) & GLFW.GLFW_MOD_CONTROL) != 0; } @@ -335,6 +414,9 @@ private static boolean canAdjustDistance() { /** 在 KeyboardHandler 入口处理,避免依赖可能被其他模组取消的 NeoForge 末尾事件。 */ public static boolean handleKeyboardInput(int key, int scanCode, int action, int modifiers) { Minecraft mc = Minecraft.getInstance(); + if (first != null && (key == GLFW.GLFW_KEY_LEFT_CONTROL || key == GLFW.GLFW_KEY_RIGHT_CONTROL)) { + updateTarget(); + } if (key == GLFW.GLFW_KEY_Z && (modifiers & GLFW.GLFW_MOD_CONTROL) != 0 && active() && mc.screen == null && mc.getOverlay() == null && mc.isWindowActive()) { if (action == GLFW.GLFW_PRESS) { @@ -362,7 +444,7 @@ private static boolean handleControl(InputConstants.Key pressed, int scanCode, i if (bound.equals(InputConstants.UNKNOWN)) continue; if (!bound.equals(pressed) && !(bound.getType() == InputConstants.Type.SCANCODE && bound.getValue() == scanCode)) continue; if (action == GLFW.GLFW_RELEASE) KEY_REPEAT.release(i); - if (!enabled || (canAdjustDistance() && i >= 2) || !modifiersMatch(keys[i], modifiers)) continue; + if (!enabled || !controlAvailable(i) || !modifiersMatch(keys[i], modifiers)) continue; if (action == GLFW.GLFW_PRESS && KEY_REPEAT.press(i, System.nanoTime())) applyControl(i, 1); // 系统重复事件只拦截,不执行位移;连移统一使用物理状态与自己的计时。 handled = true; @@ -429,7 +511,7 @@ private static void controls() { KeyMapping[] keys = controlKeys(); long now = System.nanoTime(); for (int i = 0; i < keys.length; i++) { - boolean down = (!canAdjustDistance() || i < 2) + boolean down = controlAvailable(i) && physicalKeyDown(keys[i].getKey(), i, window) && modifiersMatch(keys[i], modifiers); int steps = KEY_REPEAT.update(i, down, now); if (steps > 0) applyControl(i, steps); @@ -440,9 +522,15 @@ private static void applyControl(int index, int steps) { var player = Minecraft.getInstance().player; if (player == null) return; if (canAdjustDistance()) { - if (index >= 2) return; + if (!controlAvailable(index)) return; updateTarget(); - previewDistance = Math.clamp(previewDistance + (index == 0 ? steps : -steps), 1, player.blockInteractionRange()); + if (index < 2) { + previewDistance = Math.clamp(previewDistance + (index == 0 ? steps : -steps), 1, player.blockInteractionRange()); + } else { + Direction forward = player.getDirection(); + Direction[] directions = {forward.getCounterClockWise(), forward.getClockWise(), Direction.UP, Direction.DOWN}; + selectionOffset = selectionOffset.relative(directions[index - 2], steps); + } updateTarget(); return; } @@ -458,12 +546,17 @@ private static void applyControl(int index, int steps) { } } + private static boolean controlAvailable(int index) { + return !canAdjustDistance() || index < (first == null ? 2 : 6); + } + static void move(Direction direction, int steps) { placement = new BlueprintPlacement(placement.anchor().relative(direction, steps), placement.rotation(), placement.mirror()); } static void transform(Rotation rotation, Mirror mirror) { if (snapshot == null) return; + if (first != null) return; if (!locked && !traditional()) { for (Rotation candidate : Rotation.values()) { if (viewRotation().getRotated(candidate) == rotation) rotationOffset = candidate; @@ -528,8 +621,8 @@ static void confirm() { var player = Minecraft.getInstance().player; if (player == null || !hasMatchingDisk()) return; BuildingRodItemRenderer.preparePlacement(); - PacketDistributor.sendToServer(new BuildingRodPacket(placement.anchor(), BlockPos.ZERO, face, true, - placement.rotation(), placement.mirror(), player.isShiftKeyDown())); + PacketDistributor.sendToServer(new BuildingRodPacket(placement.anchor(), placement.anchor().offset(blueprintOffset), face, true, + placement.rotation(), placement.mirror(), player.isShiftKeyDown(), null, BuildingRodPacket.Action.PLACE_BLUEPRINTS)); } @SubscribeEvent 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 6c44946818..56ec087439 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderer.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodRenderer.java @@ -16,10 +16,12 @@ import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.block.multipart.AbstractMultiPartBlock; import dev.dubhe.anvilcraft.building.BlueprintBlockConfiguration; +import dev.dubhe.anvilcraft.building.BlueprintBlockEntities; import dev.dubhe.anvilcraft.building.BlueprintMultiblocks; import dev.dubhe.anvilcraft.building.BlueprintPlacement; import dev.dubhe.anvilcraft.building.BuildingEntityTransform; import dev.dubhe.anvilcraft.building.BuildingRodService; +import dev.dubhe.anvilcraft.building.EntityBuildAdapters; import dev.dubhe.anvilcraft.building.StructureSnapshot; import dev.dubhe.anvilcraft.client.event.LargeBlockPlacePreviewEventListener; import dev.dubhe.anvilcraft.client.selection.ModelBlockSelection; @@ -36,9 +38,7 @@ import net.minecraft.core.Vec3i; import net.minecraft.util.RandomSource; import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EntityType; import net.minecraft.world.item.BlockItem; -import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.entity.BlockEntity; @@ -103,11 +103,12 @@ public static void render(RenderLevelStageEvent event) { if (mc.level == null || mc.player == null) return; Vec3 camera = event.getCamera().getPosition(); PoseStack pose = event.getPoseStack(); - BlockPos first = BuildingRodClient.first; - BlockPos target = BuildingRodClient.target; - if (first != null && target != null) { - AABB box = AABB.encapsulatingFullBlocks(first, target).move(-camera.x, -camera.y, -camera.z); - boolean valid = BuildingRodService.volume(first, target) <= BuildingRodService.MAX_BLOCKS; + var bounds = BuildingRodClient.selectionBounds(); + if (bounds != null) { + AABB box = AABB.of(bounds).move(-camera.x, -camera.y, -camera.z); + boolean valid = BuildingRodClient.snapshot == null + ? (long) bounds.getXSpan() * bounds.getYSpan() * bounds.getZSpan() <= BuildingRodService.MAX_BLOCKS + : !BuildingRodClient.blueprintPlacements().isEmpty(); var buffers = mc.renderBuffers().bufferSource(); LevelRenderer.renderLineBox(pose, buffers.getBuffer(RenderType.lines()), box, valid ? 0.2f : 1, valid ? 1 : 0.2f, 0.8f, 1); buffers.endBatch(RenderType.lines()); @@ -132,14 +133,27 @@ && renderPlacementOutline(event, cells)) { snapshot = placementSnapshot; placement = new BlueprintPlacement(placementAnchor, Rotation.NONE, Mirror.NONE); } - if (snapshot == null || (target == null && !BuildingRodClient.locked)) return; + if (snapshot == null || (BuildingRodClient.target == null && !BuildingRodClient.locked)) return; int alpha = materialPreview ? (int) Math.round(255 * AnvilCraft.CLIENT_CONFIG.multiPartPreviewGhostOpacity) : 110; if (built != snapshot || rotation != placement.rotation() || mirror != placement.mirror() || layer != BuildingRodClient.layer || meshAlpha != alpha) { build(snapshot, placement, alpha); } + List copies = materialPreview ? List.of(placement) : BuildingRodClient.blueprintPlacements(); + for (BlueprintPlacement copy : copies) { + renderCopy(event, snapshot, copy, alpha, materialPreview, !materialPreview && copies.size() == 1); + } + } + + private static void renderCopy( + RenderLevelStageEvent event, StructureSnapshot snapshot, BlueprintPlacement placement, + int alpha, boolean materialPreview, boolean animate + ) { + Vec3 camera = event.getCamera().getPosition(); + PoseStack pose = event.getPoseStack(); Vec3 center = AABB.of(placement.bounds(snapshot.size())).getCenter(); - BuildingRodAnimation.Pose visual = materialPreview ? new BuildingRodAnimation.Pose(center.x, center.y, center.z, 0, 1, 1) + BuildingRodAnimation.Pose visual = !animate ? new BuildingRodAnimation.Pose(center.x, center.y, center.z, placement.yawDegrees(), + placement.mirrorX(), placement.mirrorZ()) : ANIMATION.sample(center.x, center.y, center.z, placement.yawDegrees(), placement.mirrorX(), placement.mirrorZ(), System.nanoTime()); pose.pushPose(); @@ -149,6 +163,7 @@ && renderPlacementOutline(event, cells)) { pose.scale(visual.mirrorX() * placement.mirrorX(), 1, visual.mirrorZ() * placement.mirrorZ()); pose.mulPose(Axis.YP.rotationDegrees(-placement.yawDegrees())); pose.translate(placement.anchor().getX() - center.x, placement.anchor().getY() - center.y, placement.anchor().getZ() - center.z); + Minecraft mc = Minecraft.getInstance(); if (!materialPreview && BuildingRodClient.traditional()) { var local = new BlueprintPlacement(BlockPos.ZERO, placement.rotation(), placement.mirror()); var outlineBuffers = mc.renderBuffers().bufferSource(); @@ -257,20 +272,20 @@ private static void build(StructureSnapshot snapshot, BlueprintPlacement placeme for (var entry : blocks) { BlockPos pos = entry.pos(); var state = entry.state(); - BlockEntity entity = state.getBlock() instanceof EntityBlock block ? block.newBlockEntity(pos, state) : null; + var data = entry.nbt().map(nbt -> { + var transformed = nbt.copy(); + BlueprintBlockConfiguration.transform(transformed, local, sourceOrigin, snapshot); + return transformed; + }).orElse(null); + BlockEntity entity = BlueprintBlockEntities.create(mc.level, pos, state, data); if (entity != null) { - 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); } view.put(pos, state, entity); } for (var entry : snapshot.entities()) { - EntityType.create(BuildingEntityTransform.transform(entry, local), mc.level).ifPresent(PREVIEW_ENTITIES::add); + EntityBuildAdapters.create(BuildingEntityTransform.transform(entry, local), mc.level) + .filter(entity -> !EntityBuildAdapters.isTransient(entity)).ifPresent(PREVIEW_ENTITIES::add); } try (ByteBufferBuilder memory = new ByteBufferBuilder(2_097_152)) { BufferBuilder buffer = new BufferBuilder(memory, VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK); diff --git a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodTraditionalControls.java b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodTraditionalControls.java index 3769aaac96..1fbf5c4017 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodTraditionalControls.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/building/BuildingRodTraditionalControls.java @@ -60,10 +60,14 @@ static void press() { BuildingRodClient.placement = new BlueprintPlacement(BlockPos.ZERO, Rotation.NONE, Mirror.NONE); BuildingRodRenderer.clear(); updateTarget(); + BuildingRodClient.beginBlueprintSelection(); return; } switch (tool) { - case MOVE -> BuildingRodClient.locked = !BuildingRodClient.locked; + case MOVE -> { + if (BuildingRodClient.locked) BuildingRodClient.locked = false; + else BuildingRodClient.beginBlueprintSelection(); + } case ROTATE -> BuildingRodClient.rotate(1); case FLIP -> mirror(1); case LAYER_DOWN -> BuildingRodClient.stepLayer(-1); @@ -75,6 +79,10 @@ static void press() { } } + static void finishSelection() { + tool = Tool.CONFIRM; + } + static void updateTarget() { var snapshot = BuildingRodClient.snapshot; var player = Minecraft.getInstance().player; 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 4a0bc9d15d..8f0299936c 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 @@ -16,8 +16,8 @@ import com.mojang.math.Axis; import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.api.tooltip.TooltipRenderHelper; +import dev.dubhe.anvilcraft.block.StructureScannerBlock; import dev.dubhe.anvilcraft.block.entity.StructureScannerBlockEntity; -import dev.dubhe.anvilcraft.building.BlueprintMultiblocks; import dev.dubhe.anvilcraft.building.BlueprintPlacement; import dev.dubhe.anvilcraft.building.StructureSnapshot; import dev.dubhe.anvilcraft.client.building.BlueprintClientFiles; @@ -161,6 +161,10 @@ private static ResourceLocation scannerTexture(String name) { private AABB cachedPreviewBounds = new AABB(BlockPos.ZERO); private AABB cachedImportedPreviewBounds = new AABB(BlockPos.ZERO); private Direction cachedPreviewFacing = Direction.NORTH; + private List cachedPreviewBlocks = List.of(); + private List cachedPreviewEntities = List.of(); + private long lastPreviewBlockCheck = -1; + private long lastPreviewEntityCheck = -10; // 扫描数据版本追踪(用于缓存失效) private int cachedScannedBlocksSize = -1; @@ -1128,20 +1132,8 @@ private void applyPreviewTransform(PoseStack pose, AABB bounds, int posX, int po private LevelLike buildImportedPreview(StructureScannerMenu.ImportedStructure imported, ClientLevel level) { if (imported == this.cachedImportedStructure && this.cachedImportedPreview != null) return this.cachedImportedPreview; LevelLike preview = new LevelLike(level); - var blocks = BlueprintMultiblocks.expand(imported.snapshot(), - new BlueprintPlacement(BlockPos.ZERO, Rotation.NONE, Mirror.NONE), -1); - int minX = blocks.stream().mapToInt(block -> block.pos().getX()).min().orElse(0); - 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())); - } + preview.addBlueprint(imported.snapshot(), new BlueprintPlacement(BlockPos.ZERO, Rotation.NONE, Mirror.NONE)); + this.cachedImportedPreviewBounds = preview.getRenderBounds(); this.cachedImportedStructure = imported; this.cachedImportedPreview = preview; return preview; @@ -1155,10 +1147,23 @@ private LevelLike buildImportedPreview(StructureScannerMenu.ImportedStructure im return null; } - // 如果缓存有效,直接返回 - if (this.cachedPreviewLevelLike != null && this.cachedPreviewFacing == facing) { - return this.cachedPreviewLevelLike; + var scanned = this.cachedBlockEntity.getScannedBlocks(); + long now = this.minecraft.level.getGameTime(); + if (this.lastPreviewBlockCheck != now || this.cachedPreviewLevelLike == null) { + this.lastPreviewBlockCheck = now; + if (!scanned.equals(this.cachedPreviewBlocks)) { + this.cachedPreviewBlocks = scanned.stream().map(block -> new StructureScannerBlockEntity.CachedBlockData( + block.x(), block.y(), block.z(), block.state(), block.nbt() == null ? null : block.nbt().copy())).toList(); + this.cachedPreviewLevelLike = null; + } } + if (this.cachedPreviewLevelLike == null || now < this.lastPreviewEntityCheck || now - this.lastPreviewEntityCheck >= 10) { + var captured = this.cachedBlockEntity.captureEntities(); + if (!captured.equals(this.cachedPreviewEntities)) this.cachedPreviewLevelLike = null; + this.cachedPreviewEntities = captured; + this.lastPreviewEntityCheck = now; + } + if (this.cachedPreviewLevelLike != null && this.cachedPreviewFacing == facing) return this.cachedPreviewLevelLike; ClientLevel level = this.minecraft.level; LevelLike previewLevelLike = new LevelLike(level); @@ -1170,8 +1175,8 @@ private LevelLike buildImportedPreview(StructureScannerMenu.ImportedStructure im 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)) { - upsideDown = this.cachedBlockEntity.getBlockState().getValue(dev.dubhe.anvilcraft.block.StructureScannerBlock.UPSIDE_DOWN); + if (this.cachedBlockEntity.getBlockState().hasProperty(StructureScannerBlock.UPSIDE_DOWN)) { + upsideDown = this.cachedBlockEntity.getBlockState().getValue(StructureScannerBlock.UPSIDE_DOWN); } // Scanner在预览中的位置:X居中,Y=0,Z=0(选区前面) @@ -1184,32 +1189,27 @@ private LevelLike buildImportedPreview(StructureScannerMenu.ImportedStructure im new BlockPos(scannerX, scannerY, scannerZ), ModBlocks.STRUCTURE_SCANNER.get().defaultBlockState() .setValue(HorizontalDirectionalBlock.FACING, Direction.NORTH) - .setValue(dev.dubhe.anvilcraft.block.StructureScannerBlock.UPSIDE_DOWN, upsideDown) + .setValue(StructureScannerBlock.UPSIDE_DOWN, upsideDown) ); // 使用缓存的扫描结果渲染方块 List scannedBlocks = this.cachedBlockEntity.getScannedBlocks(); this.blueprintError = null; - if (!scannedBlocks.isEmpty()) { + if (!scannedBlocks.isEmpty() || this.cachedBlockEntity.getCurrentScanLayer() > 0) { try { var result = StructureSaveUtil.buildSnapshot(this.cachedBlockEntity, scannedBlocks); var snapshot = result.snapshot(); - for (var entry : snapshot.blocks()) { - BlockPos local = entry.pos().subtract(result.offset()); - int x = local.getX(); - int z = local.getZ(); - BlockPos pos = switch (facing) { - case SOUTH -> new BlockPos(rangeX - 1 - x, local.getY(), this.cachedRangeZ + 1 - z); - case WEST -> new BlockPos(rangeX - 1 - z, local.getY(), x + 2); - case EAST -> new BlockPos(z, local.getY(), this.cachedRangeZ + 1 - x); - default -> new BlockPos(x, local.getY(), z + 2); - }; - previewLevelLike.setBlockState(pos, snapshot.stateOf(entry).rotate(this.rotationForPreview(facing))); - this.cachedPreviewBounds = this.cachedPreviewBounds.minmax(new AABB(pos)); - var entity = previewLevelLike.getBlockEntity(pos); - if (entity != null) entry.nbt().ifPresent(tag -> entity.loadWithComponents(tag, level.registryAccess())); - } + BlockPos anchor = switch (facing) { + case SOUTH -> new BlockPos(rangeX - 1, 0, this.cachedRangeZ + 1); + case WEST -> new BlockPos(rangeX - 1, 0, 2); + case EAST -> new BlockPos(0, 0, this.cachedRangeZ + 1); + default -> new BlockPos(0, 0, 2); + }; + BlueprintPlacement placement = new BlueprintPlacement(anchor, this.rotationForPreview(facing), Mirror.NONE); + placement = new BlueprintPlacement(anchor.subtract(placement.localOf(result.offset())), placement.rotation(), Mirror.NONE); + previewLevelLike.addBlueprint(snapshot, placement); + this.cachedPreviewBounds = this.cachedPreviewBounds.minmax(previewLevelLike.getRenderBounds()); } catch (IllegalArgumentException exception) { this.blueprintError = exception.getMessage(); } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java index 36a738153c..891f8d790c 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java @@ -220,6 +220,11 @@ public void renderItemBody( MultiBufferSource buffer, int packedOverlay ) { + if (CelestialBodyRenderer.defer(poseStack, (pose, deferredBuffers) -> this.renderItemBody( + blockEntity, pose, deferredBuffers, packedOverlay + ))) { + return; + } CelestialBodyData body = blockEntity.getCelestialBodyData(); if (body == null) return; @Nullable BakedModel model = null; @@ -1169,6 +1174,12 @@ private void renderCelestialBody( @Nullable StellarEventProfile eventProfile, float eventProgress ) { + if (CelestialBodyRenderer.defer(poseStack, (pose, deferredBuffers) -> this.renderCelestialBody( + bodyData, centerY, bodyRotation, pose, deferredBuffers, packedOverlay, seed, + animProgress, bodyScale, stellarVisual, eventProfile, eventProgress + ))) { + return; + } poseStack.pushPose(); poseStack.translate(0.5, centerY, 0.5); float baseScale = bodyScale; @@ -1446,6 +1457,11 @@ private void renderCelestialRing( float bodyScaleMultiplier ) { if (bodyData.ringType() == RingType.NONE) return; + if (CelestialBodyRenderer.defer(poseStack, (pose, deferredBuffers) -> this.renderCelestialRing( + bodyData, centerY, bodyRotation, pose, deferredBuffers, packedOverlay, animProgress, bodyScaleMultiplier + ))) { + return; + } ResourceLocation ringTexture = CelestialBodyTextureBakery.getOrBakeRing(bodyData); if (ringTexture == null) return; @@ -1553,6 +1569,11 @@ private void renderSupernovaFlash( PoseStack poseStack, MultiBufferSource bufferSource ) { + if (CelestialBodyRenderer.defer(poseStack, (pose, deferredBuffers) -> this.renderSupernovaFlash( + blockEntity, partialTick, pose, deferredBuffers + ))) { + return; + } int ticks = blockEntity.getSupernovaFlashTicks(); int total = CelestialForgingAnvilBlockEntity.SUPERNOVA_FLASH_TICKS; /// 已经过的进度 0→1(含 partialTick 平滑) diff --git a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/CelestialBodyRenderer.java b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/CelestialBodyRenderer.java index b37838ff32..ecb424a8e8 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/CelestialBodyRenderer.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/CelestialBodyRenderer.java @@ -5,6 +5,8 @@ import dev.dubhe.anvilcraft.block.entity.celestial.StarData; import dev.dubhe.anvilcraft.block.entity.celestial.Temperature; import dev.dubhe.anvilcraft.client.init.ModRenderTypes; +import dev.dubhe.anvilcraft.integration.iris.CelestialIrisRenderer; +import dev.dubhe.anvilcraft.integration.iris.IrisState; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; @@ -19,6 +21,7 @@ import org.joml.Matrix4f; import org.joml.Vector3f; +import java.util.function.BiConsumer; import javax.annotation.Nullable; /// 天体渲染器 —— 提供行星体、大气、星晕和行星环的渲染方法。 @@ -29,6 +32,10 @@ public class CelestialBodyRenderer { private static final Vector3f LIGHT_DIR = new Vector3f(0.7f, 0.5f, 0.5f).normalize(); + public static boolean defer(PoseStack pose, BiConsumer draw) { + return IrisState.isShaderEnabled() && CelestialIrisRenderer.defer(pose, draw); + } + static Vector3f localLightDirection(Matrix4f inversePose) { return inversePose.transformDirection(LIGHT_DIR, new Vector3f()).normalize(); } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/PlanetAtmosphereRenderer.java b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/PlanetAtmosphereRenderer.java index 587c10f112..9dcd957027 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/PlanetAtmosphereRenderer.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/PlanetAtmosphereRenderer.java @@ -6,6 +6,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.blaze3d.vertex.VertexFormat; import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.integration.iris.CelestialIrisRenderer; import dev.dubhe.anvilcraft.integration.iris.IrisState; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.LightTexture; @@ -78,6 +79,13 @@ public static void registerShaders(RegisterShadersEvent event) { } public static void render(PoseStack poseStack, MultiBufferSource buffers, float[] color, int overlay) { + if (IrisState.isShaderEnabled()) { + float[] tint = color.clone(); + if (CelestialIrisRenderer.deferAtmosphere(poseStack, + (pose, deferredBuffers) -> render(pose, deferredBuffers, tint, overlay))) { + return; + } + } if (!STATE.standard(AnvilCraft.CLIENT_CONFIG.planetAtmosphereRenderingMode, IrisState.isShaderEnabled())) { VanillaCelestialRenderer.atmosphere(poseStack, buffers, color, overlay); return; @@ -99,7 +107,8 @@ private static void renderStandard(PoseStack poseStack, MultiBufferSource buffer throw new IllegalStateException("Invalid planet atmosphere transform"); } ShaderInstance shader = atmosphereShader; - if (shader == null || IrisState.isShaderEnabled() || !(buffers instanceof MultiBufferSource.BufferSource)) { + boolean shaderPackPass = IrisState.isShaderEnabled() && !CelestialIrisRenderer.isRendering(); + if (shader == null || shaderPackPass || !(buffers instanceof MultiBufferSource.BufferSource)) { renderPortable(poseStack, buffers, camera, light, color, overlay); return; } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/StellarEmissionRenderer.java b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/StellarEmissionRenderer.java index 0b23443bf1..cdaef57dfd 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/StellarEmissionRenderer.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/celestial/StellarEmissionRenderer.java @@ -11,6 +11,9 @@ import dev.dubhe.anvilcraft.block.entity.celestial.StarData; import dev.dubhe.anvilcraft.block.entity.celestial.StellarEventProfile; import dev.dubhe.anvilcraft.block.entity.celestial.StellarVisualState; +import dev.dubhe.anvilcraft.client.init.ModRenderTypes; +import dev.dubhe.anvilcraft.client.renderer.RenderState; +import dev.dubhe.anvilcraft.integration.iris.CelestialIrisRenderer; import dev.dubhe.anvilcraft.integration.iris.IrisState; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.GameRenderer; @@ -111,6 +114,7 @@ private StellarEmissionRenderer() { @SubscribeEvent public static void registerShaders(RegisterShadersEvent event) { + if (RenderState.isIrisPresent()) CelestialIrisRenderer.reset(); STATE.beginReload(); surfaceShader = null; try { @@ -152,6 +156,10 @@ public static void render( } } + public static void renderAfterShaders() { + CelestialIrisRenderer.render(SURFACE, CORONA, ModRenderTypes.CELESTIAL_ATMOSPHERE); + } + private static void renderStandard( StarData star, BakedModel model, PoseStack poseStack, MultiBufferSource buffers, int overlay, @Nullable StellarVisualState visual, @Nullable StellarEventProfile event, float eventProgress @@ -169,7 +177,7 @@ private static void renderStandard( } StellarRadiance.normalizeColor(color); float[] coreColor = StellarRadiance.coreColor(color, temperature, exposure); - boolean custom = surfaceShader != null && !IrisState.isShaderEnabled(); + boolean custom = surfaceShader != null && (!IrisState.isShaderEnabled() || CelestialIrisRenderer.isRendering()); float brightness = custom ? 1.0f : StellarRadiance.toneMap(1.0f, exposure); brightness *= StellarRadiance.surfaceGain(temperature); float red = coreColor[0] * brightness; 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 4e510ddcde..eb5af9aa96 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/support/RenderSupport.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/support/RenderSupport.java @@ -57,6 +57,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; import net.minecraft.world.level.material.FluidState; +import net.neoforged.neoforge.client.ClientHooks; import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions; import net.neoforged.neoforge.client.model.data.ModelData; import org.joml.Quaternionf; @@ -506,9 +507,10 @@ public static void renderLevelLikeBlocks(LevelLike level, PoseStack pose, Iterab } if (state.getRenderShape() != RenderShape.INVISIBLE) { BakedModel bakedModel = blockRenderer.getBlockModel(state); - for (RenderType type : bakedModel.getRenderTypes(state, RANDOM, ModelData.EMPTY)) { + ModelData modelData = level.getModelData(pos); + for (RenderType type : bakedModel.getRenderTypes(state, RANDOM, modelData)) { VertexConsumer vertex = buffers.getBuffer(type); - blockRenderer.renderBatched(state, pos, level, pose, vertex, false, RANDOM, ModelData.EMPTY, type); + blockRenderer.renderBatched(state, pos, level, pose, vertex, false, RANDOM, modelData, type); } } @@ -516,6 +518,14 @@ public static void renderLevelLikeBlocks(LevelLike level, PoseStack pose, Iterab .ifPresent(blockEntity -> renderBlockEntity(blockEntity, pose, buffers)); pose.popPose(); } + for (var entity : level.getEntities()) { + var renderer = minecraft.getEntityRenderDispatcher().getRenderer(entity); + var offset = renderer.getRenderOffset(entity, 0); + pose.pushPose(); + pose.translate(entity.getX() + offset.x, entity.getY() + offset.y, entity.getZ() + offset.z); + renderer.render(entity, entity.getYRot(), 0, pose, buffers, 0xF000F0); + pose.popPose(); + } buffers.endBatch(); } @@ -625,7 +635,7 @@ public static void renderItemStackWithTransparency( } } - bakedModel = net.neoforged.neoforge.client.ClientHooks.handleCameraTransforms(poseStack, bakedModel, displayContext, leftHand); + bakedModel = ClientHooks.handleCameraTransforms(poseStack, bakedModel, displayContext, leftHand); poseStack.translate(-0.5F, -0.5F, -0.5F); if (!bakedModel.isCustomRenderer() && (!itemStack.is(Items.TRIDENT) || flag)) { boolean flag1; 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 a5ae65adeb..134e3be6d3 100644 --- a/src/main/java/dev/dubhe/anvilcraft/data/lang/BuildingRodLang.java +++ b/src/main/java/dev/dubhe/anvilcraft/data/lang/BuildingRodLang.java @@ -7,19 +7,23 @@ private BuildingRodLang() { } public static void init(RegistrumLangProvider provider) { + provider.add("message.anvilcraft.building_rod.undo_partial", + "Area restored; some entities or refunds are pending. Recover the returned items and undo again"); + provider.add("message.anvilcraft.building_rod.unsupported_type", "No supported building material for blueprint object: %s"); provider.add("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"); 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.nothing_to_undo", "No placement to undo"); provider.add("message.anvilcraft.building_rod.undone", "Last placement undone"); + provider.add("screen.anvilcraft.building_rod.selection_size", "%s × %s × %s"); provider.add("item.anvilcraft.building_rod.fluids", "Buckets can fill areas and waterlog blocks; renewable fluids cost 2 B per fill, supplied by inventory or linked storage"); provider.add("screen.anvilcraft.building_rod.traditional.hint", """ %s+%s switches tools %s+%s adjusts - %s executes %s changes hotbar"""); + %s executes (hold and drag with Move to repeat projections, then click again to paste) %s changes hotbar"""); provider.add("screen.anvilcraft.building_rod.traditional.scroll", "Scroll"); provider.add("screen.anvilcraft.building_rod.traditional.status", """ @@ -33,7 +37,7 @@ public static void init(RegistrumLangProvider provider) { provider.add("screen.anvilcraft.building_rod.distance", """ Hold %s to keep the projection distance - %s/%s to adjust | %s Lock blueprint"""); + %s/%s to adjust | %s Lock projection / Hold and drag to repeat projections, then click again to paste"""); provider.add("item.anvilcraft.building_rod.summary", "Place blocks and blueprints in bulk."); provider.add("message.anvilcraft.building_rod.placed", "Blueprint placed"); provider.add( @@ -50,7 +54,11 @@ public static void init(RegistrumLangProvider provider) { ); provider.add( "item.anvilcraft.building_rod.controls", - "Hold use and drag to fill a box (up to 4,000 blocks); hold Shift to build the available part of a blueprint" + "Hold use and drag from a collidable block to fill a box (up to 4,000 blocks); " + + "hold Ctrl after selecting the first corner to keep the distance, use arrow keys to adjust, " + + "right-click to place, or release Ctrl to cancel; hold use and drag to repeat blueprint projections, " + + "release to lock them, then right-click again to paste; " + + "hold Shift to build the available part of a blueprint" ); provider.add( "item.anvilcraft.building_rod.import", @@ -83,7 +91,7 @@ public static void init(RegistrumLangProvider provider) { provider.add( "screen.anvilcraft.building_rod.optimized", """ - %s Place | %s Cancel + %s Paste projections | %s Cancel %s %s %s %s %s %s Move %s %s Rotate | %s Mirror""" ); diff --git a/src/main/java/dev/dubhe/anvilcraft/entity/AnimateAscendingBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/entity/AnimateAscendingBlockEntity.java index a8753cfdb6..f16b193c47 100644 --- a/src/main/java/dev/dubhe/anvilcraft/entity/AnimateAscendingBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/entity/AnimateAscendingBlockEntity.java @@ -4,7 +4,9 @@ import dev.dubhe.anvilcraft.init.entity.ModEntities; import lombok.Getter; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; @@ -107,10 +109,19 @@ public void tick() { @Override protected void readAdditionalSaveData(CompoundTag compound) { + this.blockState = NbtUtils.readBlockState(this.level().holderLookup(Registries.BLOCK), + compound.getCompound("BlockState")); + this.setStartPos(NbtUtils.readBlockPos(compound, "StartPos").orElse(this.blockPosition())); + this.setEndPos(NbtUtils.readBlockPos(compound, "EndPos").orElse(this.blockPosition())); } @Override protected void addAdditionalSaveData(CompoundTag compound) { + compound.put("BlockState", NbtUtils.writeBlockState(this.blockState)); + compound.put("StartPos", NbtUtils.writeBlockPos(this.getStartPos())); + compound.put("EndPos", NbtUtils.writeBlockPos(this.getEndPos())); + compound.put("RelativeStart", NbtUtils.writeBlockPos(this.getStartPos().subtract(this.blockPosition()))); + compound.put("RelativeEnd", NbtUtils.writeBlockPos(this.getEndPos().subtract(this.blockPosition()))); } /** diff --git a/src/main/java/dev/dubhe/anvilcraft/entity/CauldronOutletEntity.java b/src/main/java/dev/dubhe/anvilcraft/entity/CauldronOutletEntity.java index 5edf90ea5b..bad73086d5 100644 --- a/src/main/java/dev/dubhe/anvilcraft/entity/CauldronOutletEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/entity/CauldronOutletEntity.java @@ -29,6 +29,7 @@ import net.neoforged.neoforge.items.IItemHandler; import java.util.List; +import javax.annotation.Nullable; public class CauldronOutletEntity extends Entity { private static final EntityDataAccessor DATA_CAULDRON_POS = SynchedEntityData.defineId( @@ -47,7 +48,7 @@ public class CauldronOutletEntity extends Entity { // 标记是否处于活塞推动状态 private boolean wasMoving = false; // 目标位置 - private BlockPos targetPos = null; + @Nullable private BlockPos targetPos; public CauldronOutletEntity(EntityType entityType, Level level) { super(entityType, level); @@ -326,6 +327,8 @@ public void setCauldronState(BlockState state) { @Override protected void readAdditionalSaveData(CompoundTag compoundTag) { this.setCauldronPos(NbtUtils.readBlockPos(compoundTag, "CauldronPos").orElse(BlockPos.ZERO)); + this.wasMoving = compoundTag.getBoolean("WasMoving"); + this.targetPos = NbtUtils.readBlockPos(compoundTag, "TargetPos").orElse(null); this.setAttachedDirection(Direction.from3DDataValue(compoundTag.getInt("AttachedDirection"))); this.setCauldronState(NbtUtils.readBlockState(this.level().holderLookup(Registries.BLOCK), compoundTag.getCompound("CauldronState"))); @@ -333,6 +336,8 @@ protected void readAdditionalSaveData(CompoundTag compoundTag) { @Override protected void addAdditionalSaveData(CompoundTag compoundTag) { + compoundTag.putBoolean("WasMoving", this.wasMoving); + if (this.targetPos != null) compoundTag.put("TargetPos", NbtUtils.writeBlockPos(this.targetPos)); compoundTag.put("CauldronState", NbtUtils.writeBlockState(this.getCauldronState())); compoundTag.put("CauldronPos", NbtUtils.writeBlockPos(this.getCauldronPos())); compoundTag.putInt("AttachedDirection", this.getAttachedDirection().get3DDataValue()); diff --git a/src/main/java/dev/dubhe/anvilcraft/entity/MagnetizedNodeEntity.java b/src/main/java/dev/dubhe/anvilcraft/entity/MagnetizedNodeEntity.java index c27dbda2a0..bc2925ceb2 100644 --- a/src/main/java/dev/dubhe/anvilcraft/entity/MagnetizedNodeEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/entity/MagnetizedNodeEntity.java @@ -46,8 +46,7 @@ public MagnetizedNodeEntity(Level level, Vec3 pos, BlockPos blockPos) { this.zo = pos.z; this.noPhysics = true; this.setInvulnerable(true); - this.blockPos = blockPos; - this.blockState = level.getBlockState(blockPos); + this.setSupport(blockPos, level.getBlockState(blockPos)); } @Override @@ -93,10 +92,22 @@ protected void defineSynchedData(SynchedEntityData.Builder builder) { builder.define(DATA_BLOCK_POS, BlockPos.ZERO).define(DATA_BLOCK_STATE, Blocks.AIR.defaultBlockState()); } + private void setSupport(BlockPos pos, BlockState state) { + this.entityData.set(DATA_BLOCK_POS, pos); + this.entityData.set(DATA_BLOCK_STATE, state); + } + + @Override + public void onSyncedDataUpdated(EntityDataAccessor key) { + super.onSyncedDataUpdated(key); + if (DATA_BLOCK_POS.equals(key)) this.blockPos = this.entityData.get(DATA_BLOCK_POS); + if (DATA_BLOCK_STATE.equals(key)) this.blockState = this.entityData.get(DATA_BLOCK_STATE); + } + @Override protected void readAdditionalSaveData(CompoundTag compoundTag) { - this.blockPos = NbtUtils.readBlockPos(compoundTag, "BlockPos").orElse(BlockPos.ZERO); - this.blockState = NbtUtils.readBlockState(this.level().holderLookup(Registries.BLOCK), compoundTag.getCompound("BlockState")); + this.setSupport(NbtUtils.readBlockPos(compoundTag, "BlockPos").orElse(BlockPos.ZERO), + NbtUtils.readBlockState(this.level().holderLookup(Registries.BLOCK), compoundTag.getCompound("BlockState"))); } @Override diff --git a/src/main/java/dev/dubhe/anvilcraft/entity/SlidingBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/entity/SlidingBlockEntity.java index 0c392d4bfc..455c39795c 100644 --- a/src/main/java/dev/dubhe/anvilcraft/entity/SlidingBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/entity/SlidingBlockEntity.java @@ -16,6 +16,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.EndTag; import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.NbtUtils; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; @@ -194,6 +195,9 @@ public boolean causeFallDamage(float fallDistance, float multiplier, DamageSourc @Override protected void addAdditionalSaveData(CompoundTag compound) { + compound.put("StartPos", NbtUtils.writeBlockPos(this.getStartPos())); + compound.putInt("Time", this.time); + compound.put("RelativeStart", NbtUtils.writeBlockPos(this.getStartPos().subtract(this.blockPosition()))); SlidingBlockSection.CODEC.encode(this.section, NbtOps.INSTANCE, new CompoundTag()) .ifSuccess(tag -> compound.put("SlidingBlocks", tag)); Direction.CODEC.encode(Objects.requireNonNull(this.moveDirection), NbtOps.INSTANCE, EndTag.INSTANCE) @@ -202,6 +206,8 @@ protected void addAdditionalSaveData(CompoundTag compound) { @Override protected void readAdditionalSaveData(CompoundTag compound) { + this.setStartPos(NbtUtils.readBlockPos(compound, "StartPos").orElse(this.blockPosition())); + this.time = compound.getInt("Time"); SlidingBlockSection.CODEC.decode(NbtOps.INSTANCE, compound.getCompound("SlidingBlocks")) .ifSuccess(data -> this.section = data.getFirst()); Direction.CODEC.decode(NbtOps.INSTANCE, Objects.requireNonNull(compound.get("MovingDirection"))) diff --git a/src/main/java/dev/dubhe/anvilcraft/integration/iris/CelestialIrisRenderer.java b/src/main/java/dev/dubhe/anvilcraft/integration/iris/CelestialIrisRenderer.java new file mode 100644 index 0000000000..e7b92c07e3 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/integration/iris/CelestialIrisRenderer.java @@ -0,0 +1,283 @@ +package dev.dubhe.anvilcraft.integration.iris; + +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.shaders.FogShape; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexSorting; +import dev.dubhe.anvilcraft.AnvilCraft; +import net.irisshaders.iris.pathways.HandRenderer; +import net.irisshaders.iris.shadows.ShadowRenderingState; +import net.irisshaders.iris.vertices.ImmediateState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.ShaderInstance; +import net.neoforged.neoforge.client.GlStateBackup; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL13C; +import org.lwjgl.opengl.GL14C; +import org.lwjgl.opengl.GL20C; +import org.lwjgl.opengl.GL30C; +import org.lwjgl.system.MemoryStack; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SequencedMap; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; + +/** Replays celestial emission and atmosphere against scene depth after the shader pack's final color pass. */ +public final class CelestialIrisRenderer { + private static final List DRAWS = new ArrayList<>(); + private static final List ATMOSPHERES = new ArrayList<>(); + private static final SequencedMap STORAGE = new LinkedHashMap<>(); + private static @Nullable ByteBufferBuilder shared; + private static @Nullable MultiBufferSource.BufferSource buffers; + private static @Nullable FrameState frameState; + private static boolean collecting; + private static boolean rendering; + private static boolean renderingAtmospheres; + private static boolean failed; + + private CelestialIrisRenderer() { + } + + public static void beginFrame() { + DRAWS.clear(); + ATMOSPHERES.clear(); + frameState = null; + collecting = !failed; + } + + public static boolean isRendering() { + return rendering; + } + + public static boolean defer(PoseStack pose, BiConsumer draw) { + return defer(pose, draw, DRAWS); + } + + private static boolean defer(PoseStack pose, BiConsumer draw, List queue) { + boolean lateAtmosphere = rendering && !renderingAtmospheres && queue == ATMOSPHERES; + if ((!collecting && !lateAtmosphere) || failed) return false; + // First-person items have their own projection and compressed depth range. + if (HandRenderer.INSTANCE.isActive()) return false; + // Shadow passes use light-space poses; these must never enter the camera's draw queue. + if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) return true; + if (frameState == null) frameState = FrameState.capture(); + PoseStack copy = new PoseStack(); + copy.mulPose(pose.last().pose()); + copy.last().normal().set(pose.last().normal()); + queue.add(() -> { + MultiBufferSource.BufferSource target = buffers; + if (target != null) draw.accept(copy, target); + }); + return true; + } + + public static boolean deferAtmosphere(PoseStack pose, BiConsumer draw) { + return defer(pose, draw, ATMOSPHERES); + } + + public static void render(RenderType surface, RenderType corona, RenderType atmosphere) { + collecting = false; + FrameState captured = frameState; + if ((DRAWS.isEmpty() && ATMOSPHERES.isEmpty()) || captured == null) return; + try (RenderScope ignored = new RenderScope()) { + RenderTarget main = Minecraft.getInstance().getMainRenderTarget(); + main.bindWrite(true); + RenderSystem.disableScissor(); + RenderSystem.colorMask(true, true, true, true); + captured.apply(); + rendering = true; + if (buffers == null) { + shared = new ByteBufferBuilder(1536); + buffers = new CelestialBufferSource(shared, surface, corona, atmosphere); + } + for (Runnable draw : DRAWS) draw.run(); + buffers.endLastBatch(); + // All stellar surfaces must populate depth before any star's transparent halo is drawn. + buffers.endBatch(surface); + buffers.endBatch(); + // Atmospheres own per-planet uniforms and must not flush before stellar surfaces populate depth. + renderingAtmospheres = true; + for (Runnable atmosphereDraw : ATMOSPHERES) atmosphereDraw.run(); + buffers.endBatch(); + if (buffers instanceof CelestialBufferSource celestialBuffers) celestialBuffers.finishFrame(); + } catch (RuntimeException exception) { + failed = true; + releaseBuffers(); + AnvilCraft.LOGGER.warn("Celestial shader composition failed; using compatibility rendering until reload.", exception); + } finally { + rendering = false; + renderingAtmospheres = false; + DRAWS.clear(); + ATMOSPHERES.clear(); + frameState = null; + } + } + + public static void reset() { + collecting = false; + rendering = false; + renderingAtmospheres = false; + failed = false; + DRAWS.clear(); + ATMOSPHERES.clear(); + frameState = null; + releaseBuffers(); + } + + private static void releaseBuffers() { + buffers = null; + STORAGE.values().forEach(ByteBufferBuilder::close); + STORAGE.clear(); + if (shared != null) shared.close(); + shared = null; + } + + private static final class CelestialBufferSource extends MultiBufferSource.BufferSource { + private final Map types = new IdentityHashMap<>(); + + private CelestialBufferSource(ByteBufferBuilder shared, RenderType surface, RenderType corona, RenderType atmosphere) { + super(shared, STORAGE); + this.fixedBuffers.put(this.mainTarget(surface), new ByteBufferBuilder(1536)); + this.fixedBuffers.put(this.mainTarget(corona), new ByteBufferBuilder(8192)); + this.fixedBuffers.put(this.mainTarget(atmosphere), new ByteBufferBuilder(8192)); + } + + private RenderType mainTarget(RenderType type) { + return this.types.computeIfAbsent(type, original -> new MainTargetRenderType(original, + () -> Minecraft.getInstance().getMainRenderTarget().bindWrite(true))); + } + + @Override + public VertexConsumer getBuffer(RenderType type) { + return super.getBuffer(this.mainTarget(type)); + } + + @Override + public void endBatch(RenderType type) { + super.endBatch(this.types.getOrDefault(type, type)); + } + + private void finishFrame() { + // Skins, baked planet textures and per-planet shader states must not accumulate between frames. + this.types.entrySet().removeIf(entry -> !this.fixedBuffers.containsKey(entry.getValue())); + } + } + + private static final class MainTargetRenderType extends RenderType { + private MainTargetRenderType(RenderType original, Runnable bindTarget) { + super("anvilcraft:celestial_composition", original.format(), original.mode(), original.bufferSize(), + original.affectsCrumbling(), original.sortOnUpload(), () -> { + // COLOR_DEPTH_WRITE assumes this default; Iris's final pass leaves depth writes disabled. + RenderSystem.depthMask(true); + original.setupRenderState(); + // Some vanilla translucent types select a Fabulous target whose composition has already finished. + bindTarget.run(); + }, original::clearRenderState); + } + } + + private record FrameState( + Matrix4f modelView, Matrix4f projection, VertexSorting sorting, + float fogStart, float fogEnd, FogShape fogShape, float[] fogColor, float[] color + ) { + private static FrameState capture() { + return new FrameState(new Matrix4f(RenderSystem.getModelViewMatrix()), new Matrix4f(RenderSystem.getProjectionMatrix()), + RenderSystem.getVertexSorting(), RenderSystem.getShaderFogStart(), RenderSystem.getShaderFogEnd(), + RenderSystem.getShaderFogShape(), RenderSystem.getShaderFogColor().clone(), RenderSystem.getShaderColor().clone()); + } + + private void apply() { + RenderSystem.getModelViewStack().set(this.modelView); + RenderSystem.applyModelViewMatrix(); + RenderSystem.setProjectionMatrix(this.projection, this.sorting); + RenderSystem.setShaderFogStart(this.fogStart); + RenderSystem.setShaderFogEnd(this.fogEnd); + RenderSystem.setShaderFogShape(this.fogShape); + RenderSystem.setShaderFogColor(this.fogColor[0], this.fogColor[1], this.fogColor[2], this.fogColor[3]); + RenderSystem.setShaderColor(this.color[0], this.color[1], this.color[2], this.color[3]); + } + } + + private static final class RenderScope implements AutoCloseable { + private final GlStateBackup state = new GlStateBackup(); + private final FrameState previous = FrameState.capture(); + private final @Nullable ShaderInstance shader = RenderSystem.getShader(); + private final int program = GL11C.glGetInteger(GL20C.GL_CURRENT_PROGRAM); + private final int drawFramebuffer = GL11C.glGetInteger(GL30C.GL_DRAW_FRAMEBUFFER_BINDING); + private final int readFramebuffer = GL11C.glGetInteger(GL30C.GL_READ_FRAMEBUFFER_BINDING); + private final int activeTexture = GL11C.glGetInteger(GL13C.GL_ACTIVE_TEXTURE); + private final int[] textures = new int[3]; + private final int[] bindings = new int[3]; + private final int[] viewport = new int[4]; + private final boolean renderingLevel = ImmediateState.isRenderingLevel; + private final boolean extendedFormat = ImmediateState.renderWithExtendedVertexFormat; + private final boolean skipExtension = ImmediateState.skipExtension.get(); + + private RenderScope() { + RenderSystem.backupGlState(this.state); + // Iris also changes GL directly, so query the real state at this boundary. + this.state.blendEnabled = GL11C.glIsEnabled(GL11C.GL_BLEND); + this.state.blendSrcRgb = GL11C.glGetInteger(GL14C.GL_BLEND_SRC_RGB); + this.state.blendDestRgb = GL11C.glGetInteger(GL14C.GL_BLEND_DST_RGB); + this.state.blendSrcAlpha = GL11C.glGetInteger(GL14C.GL_BLEND_SRC_ALPHA); + this.state.blendDestAlpha = GL11C.glGetInteger(GL14C.GL_BLEND_DST_ALPHA); + this.state.depthEnabled = GL11C.glIsEnabled(GL11C.GL_DEPTH_TEST); + this.state.depthMask = GL11C.glGetBoolean(GL11C.GL_DEPTH_WRITEMASK); + this.state.depthFunc = GL11C.glGetInteger(GL11C.GL_DEPTH_FUNC); + this.state.cullEnabled = GL11C.glIsEnabled(GL11C.GL_CULL_FACE); + this.state.scissorEnabled = GL11C.glIsEnabled(GL11C.GL_SCISSOR_TEST); + try (MemoryStack stack = MemoryStack.stackPush()) { + ByteBuffer mask = stack.malloc(4); + GL11C.glGetBooleanv(GL11C.GL_COLOR_WRITEMASK, mask); + this.state.colorMaskRed = mask.get(0) != 0; + this.state.colorMaskGreen = mask.get(1) != 0; + this.state.colorMaskBlue = mask.get(2) != 0; + this.state.colorMaskAlpha = mask.get(3) != 0; + } + RenderSystem.restoreGlState(this.state); + GL11C.glGetIntegerv(GL11C.GL_VIEWPORT, this.viewport); + for (int unit = 0; unit < this.textures.length; unit++) { + this.textures[unit] = RenderSystem.getShaderTexture(unit); + RenderSystem.activeTexture(GL13C.GL_TEXTURE0 + unit); + this.bindings[unit] = GL11C.glGetInteger(GL11C.GL_TEXTURE_BINDING_2D); + } + RenderSystem.activeTexture(this.activeTexture); + ImmediateState.isRenderingLevel = false; + ImmediateState.renderWithExtendedVertexFormat = false; + ImmediateState.skipExtension.set(true); + } + + @Override + public void close() { + ImmediateState.isRenderingLevel = this.renderingLevel; + ImmediateState.renderWithExtendedVertexFormat = this.extendedFormat; + ImmediateState.skipExtension.set(this.skipExtension); + this.previous.apply(); + RenderSystem.setShader(() -> this.shader); + for (int unit = 0; unit < this.textures.length; unit++) { + RenderSystem.setShaderTexture(unit, this.textures[unit]); + RenderSystem.activeTexture(GL13C.GL_TEXTURE0 + unit); + GlStateManager._bindTexture(this.bindings[unit]); + } + RenderSystem.activeTexture(this.activeTexture); + GlStateManager._glUseProgram(this.program); + GlStateManager._glBindFramebuffer(GL30C.GL_DRAW_FRAMEBUFFER, this.drawFramebuffer); + GlStateManager._glBindFramebuffer(GL30C.GL_READ_FRAMEBUFFER, this.readFramebuffer); + RenderSystem.viewport(this.viewport[0], this.viewport[1], this.viewport[2], this.viewport[3]); + RenderSystem.restoreGlState(this.state); + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/item/IonocraftBackpackItem.java b/src/main/java/dev/dubhe/anvilcraft/item/IonocraftBackpackItem.java index 3096ccb9cd..7bc39de2d8 100644 --- a/src/main/java/dev/dubhe/anvilcraft/item/IonocraftBackpackItem.java +++ b/src/main/java/dev/dubhe/anvilcraft/item/IonocraftBackpackItem.java @@ -106,7 +106,6 @@ public static ItemStack getByPlayer(Player player) { } public static void refreshPower(ServerPlayer player) { - WeatherproofChestplateItem.refreshGridDemand(player); DynamicPowerComponent component = IDynamicPowerComponentHolder.of(player).anvilcraft$getPowerComponent(); if (!getByPlayer(player).isEmpty() && !(getByPlayer(player).getItem() instanceof WeatherproofChestplateItem) && player.isAlive() && !player.isCreative() && !player.isSpectator() diff --git a/src/main/java/dev/dubhe/anvilcraft/item/WeatherproofChestplateItem.java b/src/main/java/dev/dubhe/anvilcraft/item/WeatherproofChestplateItem.java index db11435418..fe4cb96867 100644 --- a/src/main/java/dev/dubhe/anvilcraft/item/WeatherproofChestplateItem.java +++ b/src/main/java/dev/dubhe/anvilcraft/item/WeatherproofChestplateItem.java @@ -47,23 +47,24 @@ public boolean charge(ItemStack stack, IFullCapacitor capacitor, ItemStack capac return this.canAccept(stack, capacitor, capacitorStack, false) && this.chargeForce(stack, capacitor, capacitorStack); } - public static void refreshGridDemand(ServerPlayer player) { - DynamicPowerComponent component = IDynamicPowerComponentHolder.of(player).anvilcraft$getPowerComponent(); - int previous = 0; + public static void clearGridDemand(DynamicPowerComponent component) { for (DynamicPowerComponent.PowerConsumption demand : CHARGING_POWER) { - if (component.getPowerConsumptions().remove(demand)) previous = demand.amount(); + component.getPowerConsumptions().remove(demand); } + } + + public static int refreshGridDemand(ServerPlayer player, long available) { + DynamicPowerComponent component = IDynamicPowerComponentHolder.of(player).anvilcraft$getPowerComponent(); ItemStack stack = getByPlayer(player); PowerGrid grid = component.getPowerGrid(); if (!(stack.getItem() instanceof WeatherproofChestplateItem) || grid == null || !player.isAlive() - || player.isCreative() || player.isSpectator() || getEnergyStored(stack) >= MAX_ENERGY) return; - long players = Math.max(1, grid.getDynamicComponents().stream().filter(entry -> entry.getOwner() instanceof ServerPlayer).count()); - long available = grid.getRemaining() / players + previous; + || player.isCreative() || player.isSpectator() || getEnergyStored(stack) >= MAX_ENERGY) return 0; for (int index = CHARGING_POWER.length - 1; index >= 0; index--) { if (available < CHARGING_POWER[index].amount()) continue; component.getPowerConsumptions().add(CHARGING_POWER[index]); - break; + return CHARGING_POWER[index].amount(); } + return 0; } private static void chargeFromGrid(ServerPlayer player, ItemStack stack) { diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/BlueprintEntityDropsMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/BlueprintEntityDropsMixin.java new file mode 100644 index 0000000000..1b3aa477ab --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/BlueprintEntityDropsMixin.java @@ -0,0 +1,20 @@ +package dev.dubhe.anvilcraft.mixin; + +import dev.dubhe.anvilcraft.building.BuildingRodUndo; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +abstract class BlueprintEntityDropsMixin { + @Inject(method = "spawnAtLocation(Lnet/minecraft/world/item/ItemStack;F)Lnet/minecraft/world/entity/item/ItemEntity;", + at = @At("RETURN")) + private void anvilcraft$trackBlueprintDrops(ItemStack stack, float offset, CallbackInfoReturnable cir) { + ItemEntity item = cir.getReturnValue(); + if (item != null && !item.level().isClientSide()) BuildingRodUndo.spawnedBy((Entity) (Object) this, item); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/BlueprintLevelMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/BlueprintLevelMixin.java index df4d4bd175..577eae4de6 100644 --- a/src/main/java/dev/dubhe/anvilcraft/mixin/BlueprintLevelMixin.java +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/BlueprintLevelMixin.java @@ -2,6 +2,7 @@ import dev.dubhe.anvilcraft.building.BuildingCommit; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; import org.spongepowered.asm.mixin.Mixin; @@ -17,6 +18,12 @@ public class BlueprintLevelMixin { if (BuildingCommit.isQuiet((Level) (Object) this)) ci.cancel(); } + @Inject(method = "neighborShapeChanged", at = @At("HEAD"), cancellable = true) + private void anvilcraft$keepBlueprintObserverPhase(Direction direction, BlockState source, BlockPos pos, + BlockPos sourcePos, int flags, int recursion, CallbackInfo ci) { + if (BuildingCommit.isRestoredObserverUpdate((Level) (Object) this, direction, source, pos, sourcePos)) ci.cancel(); + } + @Inject(method = "setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;II)Z", at = @At("HEAD"), cancellable = true) private void anvilcraft$quietBlueprintState(BlockPos pos, BlockState state, int flags, int recursion, diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/BlueprintBlockEventsAccessor.java b/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/BlueprintBlockEventsAccessor.java new file mode 100644 index 0000000000..859daba3a3 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/accessor/BlueprintBlockEventsAccessor.java @@ -0,0 +1,13 @@ +package dev.dubhe.anvilcraft.mixin.accessor; + +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.BlockEventData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ServerLevel.class) +public interface BlueprintBlockEventsAccessor { + @Accessor("blockEvents") + ObjectLinkedOpenHashSet anvilcraft$getBlockEvents(); +} diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/compat/IrisStellarMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/compat/IrisStellarMixin.java new file mode 100644 index 0000000000..348131f8f6 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/compat/IrisStellarMixin.java @@ -0,0 +1,27 @@ +package dev.dubhe.anvilcraft.mixin.compat; + +import dev.dubhe.anvilcraft.client.renderer.blockentity.celestial.StellarEmissionRenderer; +import dev.dubhe.anvilcraft.integration.iris.CelestialIrisRenderer; +import net.irisshaders.iris.pipeline.IrisRenderingPipeline; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = IrisRenderingPipeline.class, remap = false) +abstract class IrisStellarMixin { + @Inject(method = "beginLevelRendering", at = @At("HEAD")) + private void anvilcraft$beginStellarFrame(CallbackInfo ci) { + CelestialIrisRenderer.beginFrame(); + } + + @Inject(method = "finalizeLevelRendering", at = @At("RETURN")) + private void anvilcraft$composeStars(CallbackInfo ci) { + StellarEmissionRenderer.renderAfterShaders(); + } + + @Inject(method = "destroy", at = @At("HEAD")) + private void anvilcraft$releaseStellarBuffers(CallbackInfo ci) { + CelestialIrisRenderer.reset(); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/network/BuildingRodPacket.java b/src/main/java/dev/dubhe/anvilcraft/network/BuildingRodPacket.java index 692554c9fb..a4be1c2cd3 100644 --- a/src/main/java/dev/dubhe/anvilcraft/network/BuildingRodPacket.java +++ b/src/main/java/dev/dubhe/anvilcraft/network/BuildingRodPacket.java @@ -20,7 +20,7 @@ public record BuildingRodPacket(BlockPos first, BlockPos last, Direction face, boolean blueprint, Rotation rotation, Mirror mirror, boolean partial, @Nullable BlockHitResult hit, Action action, long seed) implements IServerboundPacket { - public enum Action { PLACE, START, UNDO } + public enum Action { PLACE, START, UNDO, PLACE_BLUEPRINTS } public BuildingRodPacket(BlockPos first, BlockPos last, Direction face, boolean blueprint, Rotation rotation, Mirror mirror, boolean partial, @Nullable BlockHitResult hit, Action action) { @@ -73,7 +73,12 @@ public void handleOnServer(Player player) { BuildingRodService.start(serverPlayer, this.first, this.face, this.hit, this.seed); return; } - if (this.blueprint) BuildingRodService.blueprint(serverPlayer, this.first, this.rotation, this.mirror, this.partial); - else BuildingRodService.box(serverPlayer, this.first, this.last, this.face, this.hit, this.seed); + if (this.action == Action.PLACE_BLUEPRINTS && this.blueprint) { + BuildingRodService.blueprints(serverPlayer, this.first, this.last, this.rotation, this.mirror, this.partial); + } else if (this.blueprint) { + BuildingRodService.blueprint(serverPlayer, this.first, this.rotation, this.mirror, this.partial); + } else { + BuildingRodService.box(serverPlayer, this.first, this.last, this.face, this.hit, this.seed); + } } } diff --git a/src/main/java/dev/dubhe/anvilcraft/util/BlockPlacementUtil.java b/src/main/java/dev/dubhe/anvilcraft/util/BlockPlacementUtil.java index 295b4583c1..39b2c71877 100644 --- a/src/main/java/dev/dubhe/anvilcraft/util/BlockPlacementUtil.java +++ b/src/main/java/dev/dubhe/anvilcraft/util/BlockPlacementUtil.java @@ -109,13 +109,25 @@ public static ItemStack placeBlock( // 垂直朝向同样生效(抬头/低头放置)。 orientPlayerForDirection(player, defaultFacing); } - blockItem.place(level, pos, player, InteractionHand.MAIN_HAND); + BlockPos placementPos = pos; + if (requiredState != null && requiredState.getBlock() instanceof AbstractMultiPartBlock multiPartBlock) { + placementPos = getMultiblockPlacementPos(pos, requiredState, multiPartBlock); + } + blockItem.place(level, placementPos, player, InteractionHand.MAIN_HAND); return player.getMainHandItem(); } finally { AnvilCraftFakePlayers.getBlockPlacer().disable(player); } } + private static

> BlockPos getMultiblockPlacementPos( + BlockPos pos, BlockState requiredState, AbstractMultiPartBlock

block + ) { + // 蓝图定位到核心部件,物品放置则从默认部件开始生成整个结构。 + P placementPart = block.defaultBlockState().getValue(block.getPart()); + return pos.offset(block.offsetFrom(requiredState, placementPart)); + } + private static void orientPlayerForState(ServerPlayer player, BlockState state) { Direction facing = null; if (state.hasProperty(BlockStateProperties.HORIZONTAL_FACING)) { diff --git a/src/main/java/dev/dubhe/anvilcraft/util/LevelLike.java b/src/main/java/dev/dubhe/anvilcraft/util/LevelLike.java index 072fe217ef..29466ea2bd 100644 --- a/src/main/java/dev/dubhe/anvilcraft/util/LevelLike.java +++ b/src/main/java/dev/dubhe/anvilcraft/util/LevelLike.java @@ -3,6 +3,13 @@ import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; import dev.anvilcraft.lib.v2.util.predicate.BlockStatePredicate; +import dev.dubhe.anvilcraft.building.BlueprintBlockConfiguration; +import dev.dubhe.anvilcraft.building.BlueprintBlockEntities; +import dev.dubhe.anvilcraft.building.BlueprintMultiblocks; +import dev.dubhe.anvilcraft.building.BlueprintPlacement; +import dev.dubhe.anvilcraft.building.BuildingEntityTransform; +import dev.dubhe.anvilcraft.building.EntityBuildAdapters; +import dev.dubhe.anvilcraft.building.StructureSnapshot; import lombok.Getter; import lombok.Setter; import net.minecraft.client.Minecraft; @@ -10,6 +17,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.Entity; import net.minecraft.world.level.BlockAndTintGetter; import net.minecraft.world.level.ColorResolver; import net.minecraft.world.level.LightLayer; @@ -21,6 +29,8 @@ import net.minecraft.world.level.lighting.LevelLightEngine; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.AABB; +import net.neoforged.neoforge.client.model.data.ModelData; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -33,6 +43,7 @@ public class LevelLike implements BlockAndTintGetter { private final ListMultimap blocks = MultimapBuilder.hashKeys().arrayListValues().build(); private final Set alwaysRenderBlocks = new HashSet<>(); // 始终渲染的方块 private final ClientLevel parent; + private final List entities = new ArrayList<>(); @Setter @Getter @@ -46,6 +57,41 @@ public LevelLike(ClientLevel parent) { this.parent = parent; } + public void addBlueprint(StructureSnapshot snapshot, BlueprintPlacement placement) { + BlockPos sourceOrigin = BlueprintBlockConfiguration.sourceOrigin(snapshot); + for (var block : BlueprintMultiblocks.expand(snapshot, placement, -1)) { + var data = block.nbt().map(tag -> { + var transformed = tag.copy(); + BlueprintBlockConfiguration.transform(transformed, placement, sourceOrigin, snapshot); + return transformed; + }).orElse(null); + BlockEntity entity = BlueprintBlockEntities.create(this.parent, block.pos(), block.state(), data); + this.blocks.removeAll(block.pos()); + this.blocks.put(block.pos(), new BlockStateAndEntity(block.state(), entity)); + } + for (var entry : snapshot.entities()) { + EntityBuildAdapters.create(BuildingEntityTransform.transform(entry, placement), this.parent) + .filter(entity -> !EntityBuildAdapters.isTransient(entity)).ifPresent(this.entities::add); + } + } + + public List getEntities() { + return Collections.unmodifiableList(this.entities); + } + + public AABB getRenderBounds() { + AABB bounds = new AABB(BlockPos.ZERO); + for (BlockPos pos : this.blocks.keySet()) bounds = bounds.minmax(new AABB(pos)); + for (Entity entity : this.entities) bounds = bounds.minmax(entity.getBoundingBox()); + return bounds; + } + + @Override + public ModelData getModelData(BlockPos pos) { + BlockEntity entity = this.getBlockEntity(pos); + return entity == null ? ModelData.EMPTY : entity.getModelData(); + } + public int horizontalSize() { Set keys = blocks.keySet(); return Math.max( diff --git a/src/main/java/dev/dubhe/anvilcraft/util/StructureBlueprintFiles.java b/src/main/java/dev/dubhe/anvilcraft/util/StructureBlueprintFiles.java index 81a414fb31..c8c56c6bdb 100644 --- a/src/main/java/dev/dubhe/anvilcraft/util/StructureBlueprintFiles.java +++ b/src/main/java/dev/dubhe/anvilcraft/util/StructureBlueprintFiles.java @@ -5,10 +5,10 @@ import java.io.IOException; import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -56,15 +56,25 @@ public static byte[] read(MinecraftServer server, String name) throws IOExceptio return bytes; } - public static void write(MinecraftServer server, String name, byte[] bytes) throws IOException { + public static String write(MinecraftServer server, String name, byte[] bytes) throws IOException { if (!name.toLowerCase(Locale.ROOT).endsWith(".nbt")) throw new IOException("Invalid export file name"); if (bytes.length == 0 || bytes.length > StructureFileTransfer.MAX_BYTES) throw new IOException("Invalid file size"); Path target = resolve(server, name); Path temporary = Files.createTempFile(target.getParent(), ".scanner-", ".tmp"); try { Files.write(temporary, bytes); - if (!resolve(server, name).equals(target)) throw new IOException("Export directory changed"); - Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + String candidate = name; + for (int index = 1; ; index++) { + if (!resolve(server, candidate).equals(target)) throw new IOException("Export directory changed"); + try { + Files.move(temporary, target); + return candidate; + } catch (FileAlreadyExistsException exception) { + String suffix = "_" + index + name.substring(name.length() - 4); + candidate = name.substring(0, Math.min(name.length() - 4, 128 - suffix.length())) + suffix; + target = resolve(server, candidate); + } + } } finally { Files.deleteIfExists(temporary); } diff --git a/src/main/java/dev/dubhe/anvilcraft/util/StructureSaveUtil.java b/src/main/java/dev/dubhe/anvilcraft/util/StructureSaveUtil.java index ae5e3eb850..2e46f066b3 100644 --- a/src/main/java/dev/dubhe/anvilcraft/util/StructureSaveUtil.java +++ b/src/main/java/dev/dubhe/anvilcraft/util/StructureSaveUtil.java @@ -1,6 +1,7 @@ package dev.dubhe.anvilcraft.util; import dev.dubhe.anvilcraft.block.entity.StructureScannerBlockEntity; +import dev.dubhe.anvilcraft.building.BlueprintCapture; import dev.dubhe.anvilcraft.building.BlueprintNormalizer; import dev.dubhe.anvilcraft.building.ScannerDiskNormalizer; import dev.dubhe.anvilcraft.building.StructureSnapshot; @@ -14,6 +15,7 @@ import net.minecraft.core.Vec3i; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtIo; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; @@ -152,6 +154,9 @@ public static CompoundTag buildStructureNBT( public static BlueprintNormalizer.Result buildSnapshot( StructureScannerBlockEntity blockEntity, List scannedBlocks ) { + if (blockEntity.getLevel() instanceof ServerLevel serverLevel) { + return BlueprintCapture.capture(serverLevel, blockEntity.getScanBounds()); + } List palette = new ArrayList<>(); List blocks = new ArrayList<>(); for (var data : scannedBlocks) { diff --git a/src/main/java/dev/dubhe/anvilcraft/util/StructureScannerFiles.java b/src/main/java/dev/dubhe/anvilcraft/util/StructureScannerFiles.java index 4e490d4589..dd5f812329 100644 --- a/src/main/java/dev/dubhe/anvilcraft/util/StructureScannerFiles.java +++ b/src/main/java/dev/dubhe/anvilcraft/util/StructureScannerFiles.java @@ -30,6 +30,7 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -56,8 +57,8 @@ public static void handle(ServerPlayer player, StructureScannerMenu menu, Struct sendFile(player, packet.id(), preview); } case EXPORT -> { - exportBlueprint(player, menu, packet.name()); - PacketDistributor.sendToPlayer(player, new StructureScannerFileResultPacket(packet.id(), "", 0, 0, new byte[0])); + String exported = exportBlueprint(player, menu, packet.name()); + sendFile(player, packet.id(), exported.getBytes(StandardCharsets.UTF_8)); } default -> throw new IOException("Unsupported blueprint action"); } @@ -153,9 +154,9 @@ private static void sendFile(ServerPlayer player, UUID id, byte[] bytes) { } } - public static void exportBlueprint(ServerPlayer player, StructureScannerMenu menu, String name) + public static String exportBlueprint(ServerPlayer player, StructureScannerMenu menu, String name) throws IOException, ConstructionBlueprintException { - StructureBlueprintFiles.write(player.server, name, compress(exportStructure(player, menu))); + return StructureBlueprintFiles.write(player.server, name, compress(exportStructure(player, menu))); } public static CompoundTag exportStructure(ServerPlayer player, StructureScannerMenu menu) diff --git a/src/main/resources/anvilcraft.mixins.json b/src/main/resources/anvilcraft.mixins.json index da2edf7bf3..e0ad5f72b5 100644 --- a/src/main/resources/anvilcraft.mixins.json +++ b/src/main/resources/anvilcraft.mixins.json @@ -15,6 +15,7 @@ "BlockPlaceContextMixin", "BlockStateMixin", "BlueprintLevelMixin", + "BlueprintEntityDropsMixin", "BlueprintServerLevelMixin", "BlueprintTicksMixin", "BuddingAmethystBlockMixin", @@ -87,6 +88,7 @@ "ZombieVillagerMixin", "accessor.AbstractContainerMenuSlotsAccessor", "accessor.BaseSpawnerAccessor", + "accessor.BlueprintBlockEventsAccessor", "accessor.CropBlockAccessor", "accessor.DelegateBorderChangeListenerAccessor", "accessor.EntityAccessor", @@ -158,6 +160,7 @@ "accessor.ScreenAccessor", "compat.EmbBlockOcclusionCacheMixin", "compat.EmbChunkBuilderMeshingTaskMixin", + "compat.IrisStellarMixin", "compat.SodiumBlockOcclusionCacheMixin", "compat.SodiumChunkBuilderMeshingTaskMixin", "mun.MunEntityShadowMixin",