diff --git a/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/MineTowardsTargetGoal.java b/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/MineTowardsTargetGoal.java index 64c72fad..a89fe0aa 100644 --- a/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/MineTowardsTargetGoal.java +++ b/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/MineTowardsTargetGoal.java @@ -2,6 +2,7 @@ import insane96mcp.enhancedai.modules.mobs.MeleeAttacking; import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundSource; import net.minecraft.util.Mth; @@ -29,14 +30,17 @@ import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.ForgeMod; import net.minecraftforge.event.ForgeEventFactory; +import insane96mcp.enhancedai.EnhancedAI; +import insane96mcp.enhancedai.modules.mobs.miner.persistence.BlockRespawnData; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; -public class MineTowardsTargetGoal extends Goal { +import static insane96mcp.enhancedai.modules.mobs.miner.MinerMobs.blacklistTileEntities; +public class MineTowardsTargetGoal extends Goal { private final Mob miner; private LivingEntity target; private final double reachDistance; @@ -51,12 +55,14 @@ public class MineTowardsTargetGoal extends Goal { private Path path = null; - public MineTowardsTargetGoal(Mob miner){ + public MineTowardsTargetGoal(Mob miner) { this.miner = miner; this.reachDistance = miner.getAttribute(ForgeMod.BLOCK_REACH.get()) == null ? 4.5 : miner.getAttributeValue(ForgeMod.BLOCK_REACH.get()); this.setFlags(EnumSet.of(Flag.LOOK, Flag.MOVE)); } + + public boolean canUse() { if (!MinerMobs.isValidDimension(this.miner) || !this.miner.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) @@ -70,12 +76,12 @@ public boolean canUse() { && (this.miner.distanceToSqr(miner.getTarget()) < maxTargetDistance || maxTargetDistance == 0); } + public boolean canContinueToUse() { if (this.targetBlocks.isEmpty()) return false; if (this.blockState != null && !this.canBreakBlock()) return false; - if (this.target == null || !this.target.isAlive()) return false; @@ -85,6 +91,7 @@ public boolean canContinueToUse() { && this.path != null && (this.path.getDistToTarget() > 1.5d || !this.miner.hasLineOfSight(this.target)); } + public void start() { this.target = this.miner.getTarget(); if (this.target == null) @@ -95,6 +102,7 @@ public void start() { } } + public void stop() { this.target = null; if (!this.targetBlocks.isEmpty()) { @@ -110,6 +118,7 @@ public void stop() { this.miner.setAggressive(false); } + public void tick() { if (this.targetBlocks.isEmpty()) return; @@ -119,24 +128,74 @@ public void tick() { BlockPos pos = this.targetBlocks.get(0); this.breakingTick++; this.miner.getLookControl().setLookAt(pos.getX() + 0.5d, pos.getY() + 0.5d, pos.getZ() + 0.5d); - if (this.prevBreakProgress != (int) ((this.breakingTick / (float) this.tickToBreak) * 10)) { - this.prevBreakProgress = (int) ((this.breakingTick / (float) this.tickToBreak) * 10); - this.miner.level().destroyBlockProgress(this.miner.getId(), pos, this.prevBreakProgress); + + // progress visuals + int progress = (int) ((this.breakingTick / (float) this.tickToBreak) * 10); + if (this.prevBreakProgress != progress) { + this.prevBreakProgress = progress; + this.miner.level().destroyBlockProgress(this.miner.getId(), pos, progress); } - if (this.breakingTick % 6 == 0) { + + if (this.breakingTick % 6 == 0) this.miner.swing(InteractionHand.MAIN_HAND); - } + if (this.breakingTick % 4 == 0) { SoundType soundType = this.blockState.getSoundType(this.miner.level(), pos, this.miner); - this.miner.level().playSound(null, pos, soundType.getHitSound(), SoundSource.BLOCKS, (soundType.getVolume() + 1.0F) / 8.0F, soundType.getPitch() * 0.5F); + this.miner.level().playSound(null, pos, soundType.getHitSound(), SoundSource.BLOCKS, + (soundType.getVolume() + 1.0F) / 8.0F, soundType.getPitch() * 0.5F); } + + // --- Respawn-aware block breaking --- if (this.breakingTick >= this.tickToBreak && this.miner.level() instanceof ServerLevel level) { - if (ForgeEventFactory.onEntityDestroyBlock(this.miner, this.targetBlocks.get(0), this.blockState) && this.miner.level().destroyBlock(pos, false, this.miner) && (!this.blockState.requiresCorrectToolForDrops() || this.miner.getItemBySlot(EquipmentSlot.OFFHAND).isCorrectToolForDrops(this.blockState))) { - BlockEntity blockentity = this.blockState.hasBlockEntity() ? this.miner.level().getBlockEntity(pos) : null; - LootParams.Builder lootparams$builder = (new LootParams.Builder(level)).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)).withParameter(LootContextParams.TOOL, this.miner.getOffhandItem()).withOptionalParameter(LootContextParams.BLOCK_ENTITY, blockentity).withOptionalParameter(LootContextParams.THIS_ENTITY, this.miner); - this.blockState.spawnAfterBreak(level, pos, this.miner.getOffhandItem(), false); - this.blockState.getDrops(lootparams$builder).forEach((itemStack) -> level.addFreshEntity(new ItemEntity(level, pos.getX() + 0.5f, pos.getY() + 0.5f, pos.getZ() + 0.5f, itemStack))); + if (!ForgeEventFactory.onEntityDestroyBlock(this.miner, pos, this.blockState)) return; + + int respawnTime = MinerMobs.BLOCK_RESPAWN_TIME.get(this.miner); + boolean scaleByHardness = MinerMobs.SCALE_RESPAWN_BY_HARDNESS.get(this.miner); + + if (scaleByHardness) { + double hardness = Math.max(0, this.blockState.getDestroySpeed(level, pos)); + int baseTime = MinerMobs.BASE_RESPAWN_TIME.get(this.miner); + double multiplier = MinerMobs.HARDNESS_RESPAWN_MULTIPLIER.get(this.miner); + respawnTime = (int) Math.ceil(baseTime + hardness * multiplier); } + + boolean willRespawn = respawnTime > 0; + + CompoundTag blockNbt = null; + if (willRespawn && MinerMobs.shouldSaveBlockNBT(this.blockState)) { + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity != null) + blockNbt = blockEntity.saveWithFullMetadata(); + } + + if (willRespawn) { + // Remove block without drops, record for respawn + level.removeBlockEntity(pos); + level.setBlock(pos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 3); + + long respawnAt = level.getGameTime() + respawnTime; + BlockRespawnData data = BlockRespawnData.get(level); + data.set(pos, respawnAt, this.blockState, blockNbt); + + EnhancedAI.LOGGER.debug("Scheduled respawn for block {} at {} after {} ticks", + this.blockState.getBlock().getName().getString(), pos, respawnTime); + } + else { + // Normal destruction with drops + if (this.miner.getItemBySlot(EquipmentSlot.OFFHAND).isCorrectToolForDrops(this.blockState)) { + BlockEntity blockEntity = this.blockState.hasBlockEntity() ? level.getBlockEntity(pos) : null; + LootParams.Builder lootparams = (new LootParams.Builder(level)) + .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)) + .withParameter(LootContextParams.TOOL, this.miner.getOffhandItem()) + .withOptionalParameter(LootContextParams.BLOCK_ENTITY, blockEntity) + .withOptionalParameter(LootContextParams.THIS_ENTITY, this.miner); + this.blockState.spawnAfterBreak(level, pos, this.miner.getOffhandItem(), false); + this.blockState.getDrops(lootparams).forEach(stack -> + level.addFreshEntity(new ItemEntity(level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, stack))); + level.removeBlock(pos, false); + } + } + this.miner.level().destroyBlockProgress(this.miner.getId(), pos, -1); this.targetBlocks.remove(0); if (!this.targetBlocks.isEmpty()) @@ -156,52 +215,31 @@ private void initBlockBreak() { private void fillTargetBlocks() { int mobHeight = Mth.ceil(this.miner.getBbHeight()); for (int i = 0; i < mobHeight; i++) { - BlockHitResult rayTraceResult = this.miner.level().clip(new ClipContext(this.miner.position().add(0, i + 0.5d, 0), this.target.getEyePosition(1f).add(0, i, 0), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this.miner)); - if (rayTraceResult.getType() == HitResult.Type.MISS - || this.targetBlocks.contains(rayTraceResult.getBlockPos()) - || rayTraceResult.getBlockPos().getY() > MinerMobs.MAX_Y.get(this.miner)) - continue; + BlockHitResult rayTrace = this.miner.level().clip(new ClipContext(this.miner.position().add(0, i + 0.5d, 0), this.target.getEyePosition(1f).add(0, i, 0), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this.miner)); + if (rayTrace.getType() == HitResult.Type.MISS + || this.targetBlocks.contains(rayTrace.getBlockPos()) + || rayTrace.getBlockPos().getY() > MinerMobs.MAX_Y.get(this.miner)) + continue; - double distance = this.miner.distanceToSqr(rayTraceResult.getLocation()); + double distance = this.miner.distanceToSqr(rayTrace.getLocation()); if (distance > this.reachDistance * this.reachDistance) continue; - BlockState state = this.miner.level().getBlockState(rayTraceResult.getBlockPos()); + BlockState state = this.miner.level().getBlockState(rayTrace.getBlockPos()); - if (state.getDestroySpeed(this.miner.level(), rayTraceResult.getBlockPos()) == -1 - || (state.hasBlockEntity() && MinerMobs.blacklistTileEntities)) + if (state.getDestroySpeed(this.miner.level(), rayTrace.getBlockPos()) == -1 + || (state.hasBlockEntity() && blacklistTileEntities)) continue; boolean listed = state.is(MinerMobs.BLOCK_BLACKLIST); if (listed != MinerMobs.blockBlacklistAsWhitelist) continue; - this.targetBlocks.add(rayTraceResult.getBlockPos()); + this.targetBlocks.add(rayTrace.getBlockPos()); } Collections.reverse(this.targetBlocks); } - public boolean requiresUpdateEveryTick() { - return true; - } - - /** - * Returns true if the miner has been stuck in the same spot (radius 1.5 blocks) for more than 3 seconds - */ - public boolean isStuck() { - if (this.miner.getTarget() == null) - return false; - - if (MeleeAttacking.isWithinMeleeAttackRange(this.miner, this.miner.getTarget()) && this.miner.getSensing().hasLineOfSight(this.miner.getTarget())) - return false; - if (this.lastPosition == null || this.miner.distanceToSqr(this.lastPosition) > 2.25d) { - this.lastPosition = this.miner.position(); - this.lastPositionTickstamp = this.miner.tickCount; - } - return this.miner.getNavigation().isDone() || this.miner.tickCount - this.lastPositionTickstamp >= 60; - } - - // Copy-paste of vanilla code private int computeTickToBreak() { int canHarvestBlock = this.canHarvestBlock() ? 30 : 100; double diggingSpeed = this.getDigSpeed() / this.blockState.getDestroySpeed(this.miner.level(), this.targetBlocks.get(0)) / canHarvestBlock; @@ -211,57 +249,69 @@ private int computeTickToBreak() { private float getDigSpeed() { float digSpeed = this.miner.getOffhandItem().getDestroySpeed(this.blockState); if (digSpeed > 1.0F) { - int efficiencyLevel = EnchantmentHelper.getBlockEfficiency(this.miner); - ItemStack itemstack = this.miner.getOffhandItem(); - if (efficiencyLevel > 0 && !itemstack.isEmpty()) { - digSpeed += (float)(efficiencyLevel * efficiencyLevel + 1); - } + int efficiency = EnchantmentHelper.getBlockEfficiency(this.miner); + if (efficiency > 0) digSpeed += (float) (efficiency * efficiency + 1); } - - if (MobEffectUtil.hasDigSpeed(this.miner)) { - digSpeed *= 1.0F + (float)(MobEffectUtil.getDigSpeedAmplification(this.miner) + 1) * 0.2F; - } - + if (MobEffectUtil.hasDigSpeed(this.miner)) + digSpeed *= 1.0F + (MobEffectUtil.getDigSpeedAmplification(this.miner) + 1) * 0.2F; if (this.miner.hasEffect(MobEffects.DIG_SLOWDOWN)) { - //noinspection ConstantConditions - float miningFatigueAmplifier = switch (this.miner.getEffect(MobEffects.DIG_SLOWDOWN).getAmplifier()) { + float f = switch (this.miner.getEffect(MobEffects.DIG_SLOWDOWN).getAmplifier()) { case 0 -> 0.3F; case 1 -> 0.09F; case 2 -> 0.0027F; default -> 8.1E-4F; }; - - digSpeed *= miningFatigueAmplifier; + digSpeed *= f; } - if (this.miner.isEyeInFluidType(ForgeMod.WATER_TYPE.get()) && !EnchantmentHelper.hasAquaAffinity(this.miner)) digSpeed /= 5.0F; - return digSpeed; } private boolean canBreakBlock() { - MinerMobs.ToolRequirement toolRequirement = MinerMobs.TOOL_REQUIREMENT.get(this.miner); - if (toolRequirement == MinerMobs.ToolRequirement.NONE || toolRequirement == MinerMobs.ToolRequirement.ANY_TOOL) + MinerMobs.ToolRequirement toolReq = MinerMobs.TOOL_REQUIREMENT.get(this.miner); + if (toolReq == MinerMobs.ToolRequirement.NONE || toolReq == MinerMobs.ToolRequirement.ANY_TOOL) return true; - if ((toolRequirement == MinerMobs.ToolRequirement.CORRECT_TOOL_FOR_REQUIRED) && !this.blockState.requiresCorrectToolForDrops()) + if (toolReq == MinerMobs.ToolRequirement.CORRECT_TOOL_FOR_REQUIRED && !this.blockState.requiresCorrectToolForDrops()) return true; - ItemStack stack = this.miner.getOffhandItem(); - if (stack.isEmpty()) - return false; - - return stack.isCorrectToolForDrops(this.blockState); + return !stack.isEmpty() && stack.isCorrectToolForDrops(this.blockState); } private boolean canHarvestBlock() { if (!this.blockState.requiresCorrectToolForDrops()) return true; - ItemStack stack = this.miner.getOffhandItem(); - if (stack.isEmpty()) + return !stack.isEmpty() && stack.isCorrectToolForDrops(this.blockState); + } + + public boolean requiresUpdateEveryTick() { return true; } + + private boolean isStuck() { + if (this.miner.getTarget() == null) return false; + if (MeleeAttacking.isWithinMeleeAttackRange(this.miner, this.miner.getTarget()) + && this.miner.getSensing().hasLineOfSight(this.miner.getTarget())) + return false; + if (this.lastPosition == null || this.miner.distanceToSqr(this.lastPosition) > 2.25d) { + this.lastPosition = this.miner.position(); + this.lastPositionTickstamp = this.miner.tickCount; + } + return this.miner.getNavigation().isDone() || this.miner.tickCount - this.lastPositionTickstamp >= 60; + } + + private void freeSpaceForRespawn(ServerLevel level, BlockPos pos) { + BlockState existing = level.getBlockState(pos); + + // If block is already air, nothing to do + if (existing.isAir()) return; + + // Drop the block as an item + existing.spawnAfterBreak(level, pos, ItemStack.EMPTY, true); + level.removeBlock(pos, false); - return stack.isCorrectToolForDrops(this.blockState); + // Move any entities standing on the block slightly upward + level.getEntities(null, existing.getShape(level, pos).bounds().move(pos.getX(), pos.getY(), pos.getZ())) + .forEach(e -> e.setPos(e.getX(), e.getY() + 1.0, e.getZ())); } } diff --git a/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/MinerMobs.java b/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/MinerMobs.java index bff52d0e..0a3752a2 100644 --- a/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/MinerMobs.java +++ b/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/MinerMobs.java @@ -5,27 +5,41 @@ import insane96mcp.enhancedai.data.EAIDataEnum; import insane96mcp.enhancedai.data.EAIDataList; import insane96mcp.enhancedai.modules.Modules; +import insane96mcp.enhancedai.modules.mobs.miner.persistence.BlockRespawnData; import insane96mcp.enhancedai.utils.GoalHelper; import insane96mcp.insanelib.base.Feature; import insane96mcp.insanelib.base.LoadFeature; import insane96mcp.insanelib.base.Module; import insane96mcp.insanelib.base.config.Config; +import net.minecraft.core.BlockPos; import net.minecraft.core.registries.Registries; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; -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.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.AABB; import net.minecraftforge.common.ForgeMod; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.entity.EntityAttributeModificationEvent; import net.minecraftforge.event.entity.EntityJoinLevelEvent; import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraft.world.level.block.Block; +import java.util.Iterator; import java.util.List; +import java.util.Map; @LoadFeature(module = Modules.Ids.MOBS, description = "Mobs can mine blocks to reach the target. Uses offhand item to mine. Only mobs in the entity type tag enhancedai:mobs/can_mine can spawn with the ability to mine and blocks in the tag enhancedai:miner_blacklist cannot be mined. This feature also adds the block reach attribute to all entities.") public class MinerMobs extends Feature { @@ -50,6 +64,14 @@ public class MinerMobs extends Feature { public static Boolean blacklistTileEntities = true; @Config(description = "Mobs with Miner AI will spawn with a Stone Pickaxe that never drops.") public static Boolean equipStonePick = true; + @Config(min = 0, max = 1200, description = "Time in ticks for a mined block to respawn. Set to 0 for no respawn. (20 ticks = 1 second)") + public static Integer blockRespawnTime = 0; + @Config(description = "If true, block respawn time will scale based on the block's hardness.") + public static Boolean scaleRespawnByHardness = false; + @Config(min = 0, max = 1200, description = "Base respawn time in ticks for a block (used if scaling by hardness is enabled).") + public static Integer baseRespawnTime = 200; + @Config(min = 0d, max = 100d, description = "Multiplier applied to the block's hardness when calculating respawn time (used if scaling by hardness is enabled).") + public static Double hardnessRespawnMultiplier = 100d; public static EAIData MINER; public static EAIDataEnum TOOL_REQUIREMENT; @@ -57,6 +79,10 @@ public class MinerMobs extends Feature { public static EAIData MAX_TARGET_DISTANCE; public static EAIData TIME_TO_BREAK_MULTIPLIER; public static EAIDataList DIMENSION_WHITELIST; + public static EAIData BLOCK_RESPAWN_TIME; + public static EAIData SCALE_RESPAWN_BY_HARDNESS; + public static EAIData BASE_RESPAWN_TIME; + public static EAIData HARDNESS_RESPAWN_MULTIPLIER; public void init(Module module, boolean enabledByDefault, boolean canBeDisabled) { super.init(module, enabledByDefault, canBeDisabled); @@ -70,6 +96,11 @@ public void init(Module module, boolean enabledByDefault, boolean canBeDisabled) MAX_TARGET_DISTANCE = EAIData.ofInt(this.createDataKey("max_target_distance")); TIME_TO_BREAK_MULTIPLIER = EAIData.ofDouble(this.createDataKey("time_to_break_multiplier")); DIMENSION_WHITELIST = EAIDataList.of(this.createDataKey("dimension_whitelist"), String.class); + BLOCK_RESPAWN_TIME = EAIData.ofInt(this.createDataKey("block_respawn_time")); + SCALE_RESPAWN_BY_HARDNESS = EAIData.ofBool(this.createDataKey("scale_respawn_by_hardness")); + BASE_RESPAWN_TIME = EAIData.ofInt(this.createDataKey("base_respawn_time")); + HARDNESS_RESPAWN_MULTIPLIER = EAIData.ofDouble(this.createDataKey("hardness_respawn_multiplier")); + net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(MineTowardsTargetGoal.class); } public static void addAttribute(EntityAttributeModificationEvent event) { @@ -85,9 +116,9 @@ public static void addAttribute(EntityAttributeModificationEvent event) { @SubscribeEvent(priority = EventPriority.LOWEST) public void onSpawn(EntityJoinLevelEvent event) { if (!this.isEnabled() - || event.getLevel().isClientSide + || event.getLevel().isClientSide || !(event.getEntity() instanceof Mob mob) - || !mob.getType().is(CAN_BE_MINER)) + || !mob.getType().is(CAN_BE_MINER)) return; boolean isMiner = mob.getRandom().nextDouble() < minerChance; @@ -102,6 +133,11 @@ public void onSpawn(EntityJoinLevelEvent event) { MAX_TARGET_DISTANCE.applyIfAbsent(mob, maxTargetDistance); TIME_TO_BREAK_MULTIPLIER.applyIfAbsent(mob, timeToBreakMultiplier); DIMENSION_WHITELIST.applyIfAbsent(mob, dimensionWhitelist); + BLOCK_RESPAWN_TIME.applyIfAbsent(mob, blockRespawnTime); + SCALE_RESPAWN_BY_HARDNESS.applyIfAbsent(mob, scaleRespawnByHardness); + BASE_RESPAWN_TIME.applyIfAbsent(mob, baseRespawnTime); + HARDNESS_RESPAWN_MULTIPLIER.applyIfAbsent(mob, hardnessRespawnMultiplier); + } public static boolean isValidDimension(Mob mob) { @@ -119,4 +155,87 @@ public enum ToolRequirement { CORRECT_TOOL_FOR_REQUIRED, CORRECT_TOOL_FOR_ANY_BLOCK } + public static boolean shouldSaveBlockNBT(BlockState state) { + if (state.hasBlockEntity()) return true; + // Could add a tag whitelist or any other dynamic condition + return false; + } + // --- Merged BlockRespawnHandler logic --- + @SubscribeEvent + public static void onServerTick(TickEvent.ServerTickEvent event) { + if (event.phase != TickEvent.Phase.END) + return; + + MinecraftServer server = event.getServer(); + if (server == null) + return; + + for (ServerLevel level : server.getAllLevels()) { + BlockRespawnData data = BlockRespawnData.get(level); + long time = level.getGameTime(); + boolean changed = false; + + Iterator> it = data.getEntries().entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + BlockPos pos = entry.getKey(); + BlockRespawnData.RespawnEntry info = entry.getValue(); + + if (time >= info.time) { + try { + BlockState existing = level.getBlockState(pos); + if (!existing.isAir()) { + BlockEntity existingBe = level.getBlockEntity(pos); + + LootParams.Builder lootBuilder = new LootParams.Builder(level) + .withParameter(LootContextParams.ORIGIN, pos.getCenter()) + .withOptionalParameter(LootContextParams.BLOCK_ENTITY, existingBe) + .withOptionalParameter(LootContextParams.TOOL, ItemStack.EMPTY); + + for (ItemStack drop : existing.getDrops(lootBuilder)) { + level.addFreshEntity(new ItemEntity(level, + pos.getX() + 0.5d, + pos.getY() + 0.5d, + pos.getZ() + 0.5d, + drop)); + } + + existing.spawnAfterBreak(level, pos, ItemStack.EMPTY, false); + level.removeBlock(pos, false); + } + + AABB box = new AABB(pos); + for (Entity e : level.getEntities(null, box)) { + e.setPos(e.getX(), e.getY() + 1.0, e.getZ()); + } + + level.setBlock(pos, info.state, 3); + + if (info.nbt != null) { + BlockEntity be = level.getBlockEntity(pos); + if (be != null) { + be.load(info.nbt); + be.setChanged(); + } + else { + EnhancedAI.LOGGER.warn("BlockEntity missing at {} when respawning; NBT skipped.", pos); + } + } + + EnhancedAI.LOGGER.debug("Respawned block {} at {}", info.state.getBlock().getName().getString(), pos); + + } + catch (Exception e) { + EnhancedAI.LOGGER.warn("Failed to respawn block at {}: {}", pos, e.getMessage()); + } + + it.remove(); + changed = true; + } + } + + if (changed) + data.setDirty(); + } + } } diff --git a/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/persistence/BlockRespawnData.java b/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/persistence/BlockRespawnData.java new file mode 100644 index 00000000..466bf882 --- /dev/null +++ b/src/main/java/insane96mcp/enhancedai/modules/mobs/miner/persistence/BlockRespawnData.java @@ -0,0 +1,99 @@ +package insane96mcp.enhancedai.modules.mobs.miner.persistence; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.core.HolderGetter; +import net.minecraft.nbt.NbtUtils; + +import java.util.HashMap; +import java.util.Map; + +public class BlockRespawnData extends SavedData { + + private final Map respawnEntries = new HashMap<>(); + + public static class RespawnEntry { + public final long time; + public final BlockState state; + public final CompoundTag nbt; + + public RespawnEntry(long time, BlockState state, CompoundTag nbt) { + this.time = time; + this.state = state; + this.nbt = nbt != null ? nbt.copy() : null; + } + } + + public BlockRespawnData() {} + + // Get or create the data instance for a level + public static BlockRespawnData get(ServerLevel level) { + return level.getDataStorage().computeIfAbsent( + nbt -> load(nbt, level), + BlockRespawnData::new, + "enhancedai_block_respawns" + ); + } + + // Load from NBT (requires ServerLevel for registry access) + public static BlockRespawnData load(CompoundTag nbt, ServerLevel level) { + BlockRespawnData data = new BlockRespawnData(); + ListTag list = nbt.getList("Respawns", Tag.TAG_COMPOUND); + + HolderGetter blockRegistry = level.holderLookup(Registries.BLOCK); + + for (Tag t : list) { + if (!(t instanceof CompoundTag tag)) continue; + + BlockPos pos = NbtUtils.readBlockPos(tag.getCompound("Pos")); + long time = tag.getLong("Time"); + BlockState state = NbtUtils.readBlockState(blockRegistry, tag.getCompound("State")); + CompoundTag beNbt = tag.contains("BlockEntity") ? tag.getCompound("BlockEntity").copy() : null; + + data.respawnEntries.put(pos, new RespawnEntry(time, state, beNbt)); + } + + return data; + } + + + + @Override + public CompoundTag save(CompoundTag nbt) { + ListTag list = new ListTag(); + + for (Map.Entry entry : respawnEntries.entrySet()) { + CompoundTag tag = new CompoundTag(); + tag.put("Pos", NbtUtils.writeBlockPos(entry.getKey())); + tag.putLong("Time", entry.getValue().time); + tag.put("State", NbtUtils.writeBlockState(entry.getValue().state)); + if (entry.getValue().nbt != null) tag.put("BlockEntity", entry.getValue().nbt.copy()); + list.add(tag); + } + + nbt.put("Respawns", list); + return nbt; + } + + // Add or update a respawn entry + public void set(BlockPos pos, long time, BlockState state, CompoundTag beNbt) { + respawnEntries.put(pos, new RespawnEntry(time, state, beNbt)); + this.setDirty(); + } + + // Remove a respawn entry + public void remove(BlockPos pos) { + respawnEntries.remove(pos); + this.setDirty(); + } + + // Access all entries + public Map getEntries() { + return respawnEntries; + } +}