diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9682569 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +All notable changes to Sudden Death are documented in this file. + +## [2.3.3] - 2026-08-31 + +### Fixed + +- Restricted the extended target search used by Zombie Break Block to standard zombies holding a supported axe, pickaxe, or shovel. +- Prevented husks, drowned, zombie villagers, zombified piglins, and unequipped zombies from retaining player targets at the feature's configured maximum distance. +- Prevented Zombie Break Block target persistence from running in worlds where the feature is disabled. + +### Changed + +- Moved Zombie Break Block entity inspection back to the server thread instead of reading Bukkit entity state asynchronously. + +### Known Issues + +- Automated compilation validates API compatibility, but target acquisition behavior still requires an in-server Paper playtest with each zombie variant. \ No newline at end of file diff --git a/README_STATE_SNAPSHOT.md b/README_STATE_SNAPSHOT.md new file mode 100644 index 0000000..0f0c0c8 --- /dev/null +++ b/README_STATE_SNAPSHOT.md @@ -0,0 +1,62 @@ +# Sudden Death State Snapshot + +## Current Status + +- Current version: `2.3.3`. +- Current development branch: `fix/zombie-target-range`. +- Platform: Paper/Spigot API 1.21 with Java 21. +- Build system: Gradle Wrapper 9.5.1. +- Build status: `gradlew.bat clean build --no-daemon` passed on 2026-08-31 using the local JDK 25 host and Java 21 release target. +- The project is a survival difficulty plugin with configurable player, world, event, monster ability, and custom mob systems. +- The current correction limits Zombie Break Block's custom target acquisition to eligible standard zombies. + +## Implemented Features + +- Survival mechanics including bleeding, infection, fall stun, hunger nausea, realistic pickup, dangerous coal, electricity shock, snow slow, stone stiffness, and advanced player drops. +- World events including Blood Moon, Meteor Rain, and enhanced thunderstorms. +- Monster abilities for guardians, spiders, skeletons, breezes, creepers, End mobs, blazes, evokers, phantoms, pillagers, slimes, drowned, zombies, witches, wither skeletons, and other hostile mobs. +- Configurable monster attack, movement speed, health, armor-piercing, and damage-resistance modifiers. +- Custom mob creation and editing with equipment, attributes, potion effects, names, and spawn probabilities. +- Zombie Break Block for standard zombies holding axes, pickaxes, or shovels, including configurable block lists, drops, and maximum player search distance. +- Command and GUI flows for status, administration, recipes, feature management, events, and custom mobs. +- Optional integrations for WorldGuard and supported claim plugins. + +## Pending Features + +- No new gameplay feature is currently scheduled in this repository snapshot. +- Complete an in-server regression test for the Zombie Break Block target-range correction. + +## Technical Debt + +- The project does not currently include source-level automated tests for Bukkit event and entity behavior. +- Several feature loops scan all entities of a class and may need profiling on high-population servers. +- Some older classes use broad exception handling and should be reviewed incrementally rather than refactored during unrelated fixes. +- The build declares Java 21 but may be invoked from newer local JDKs through Gradle toolchain behavior. + +## Known Issues + +- Runtime behavior cannot be fully validated without launching a Paper 1.21 test server. +- Existing servers upgrading from 2.3.2 should be restarted so entities do not retain transient targets assigned by the old plugin instance. + +## Architecture Notes + +- `FeatureManager` creates and registers feature classes derived from `AbstractFeature`. +- `Feature` defines each feature's default modifiers and owns its generated configuration file access. +- `ZombieBreakBlockFeature` owns block selection, block-breaking tasks, custom target search, and target persistence. +- Bukkit entity state must be read and modified on the primary server thread. The target search correction follows this rule. +- Paper's `Zombie` interface also covers `Drowned`, `Husk`, `PigZombie`, and `ZombieVillager`; code intended only for a standard zombie must explicitly check `EntityType.ZOMBIE`. +- `max-target-distance` remains a Zombie Break Block setting and is applied only to standard zombies holding a supported tool. +- ProtocolLib is a required dependency and is used for block break animation packets. + +## Recent Changes + +- Version 2.3.3 prevents Zombie Break Block from extending target acquisition for zombie variants and unequipped zombies. +- Target persistence now stops when the feature is disabled or the zombie becomes ineligible. +- Zombie entity inspection for this feature now runs on the server thread. + +## Next Recommended Tasks + +1. Start a private Paper 1.21 test server with ProtocolLib and Sudden Death 2.3.3. +2. Spawn a zombie, husk, drowned, zombie villager, and zombified piglin more than the vanilla targeting distance from a Survival player. +3. Verify that only a standard zombie holding a supported tool uses the configured Zombie Break Block distance. +4. Verify feature disable/reload, entity death, world changes, and block-breaking cleanup before release. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8f7b33b..cb5135e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'org.NguyenDevs' -version = '2.3.2' +version = '2.3.3' repositories { mavenCentral() diff --git a/src/main/java/org/nguyendevs/suddendeath/Features/mob/hostile/ZombieBreakBlockFeature.java b/src/main/java/org/nguyendevs/suddendeath/Features/mob/hostile/ZombieBreakBlockFeature.java index b0f155f..639731f 100644 --- a/src/main/java/org/nguyendevs/suddendeath/Features/mob/hostile/ZombieBreakBlockFeature.java +++ b/src/main/java/org/nguyendevs/suddendeath/Features/mob/hostile/ZombieBreakBlockFeature.java @@ -61,8 +61,10 @@ public void run() { continue; List zombies = new ArrayList<>(world.getEntitiesByClass(Zombie.class)); for (Zombie zombie : zombies) { + if (!isEligibleBlockBreakingZombie(zombie)) + continue; long randomDelay = ThreadLocalRandom.current().nextLong(0, 5); - Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> { + Bukkit.getScheduler().runTaskLater(plugin, () -> { try { if (zombie.isValid() && (zombie.getTarget() instanceof Player @@ -103,14 +105,12 @@ public void run() { public void run() { try { for (World world : Bukkit.getWorlds()) { + if (!Feature.ZOMBIE_BREAK_BLOCK.isEnabled(world)) + continue; for (Zombie zombie : new ArrayList<>(world.getEntitiesByClass(Zombie.class))) { - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - try { - searchForNearbyPlayers(zombie); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error searching for players", e); - } - }); + if (!isEligibleBlockBreakingZombie(zombie)) + continue; + searchForNearbyPlayers(zombie); } } } catch (Exception e) { @@ -146,6 +146,12 @@ public void onEntityTarget(EntityTargetEvent event) { return; UUID zombieUUID = zombie.getUniqueId(); + if (!Feature.ZOMBIE_BREAK_BLOCK.isEnabled(zombie) || !isEligibleBlockBreakingZombie(zombie)) { + persistentTargets.remove(zombieUUID); + lastTargetSearchTime.remove(zombieUUID); + return; + } + if (event.getTarget() instanceof Player player) { if (!isValidGameMode(player.getGameMode())) { event.setCancelled(true); @@ -177,7 +183,8 @@ public void onEntityTarget(EntityTargetEvent event) { } private void searchForNearbyPlayers(Zombie zombie) { - if (!zombie.isValid()) + if (!zombie.isValid() || !Feature.ZOMBIE_BREAK_BLOCK.isEnabled(zombie) + || !isEligibleBlockBreakingZombie(zombie)) return; UUID zombieUUID = zombie.getUniqueId(); @@ -228,6 +235,11 @@ private void maintainPersistentTargets() { if (!(zombieEntity instanceof Zombie zombie) || !zombie.isValid()) return true; + if (!Feature.ZOMBIE_BREAK_BLOCK.isEnabled(zombie) || !isEligibleBlockBreakingZombie(zombie)) { + zombie.setTarget(null); + return true; + } + Player target = Bukkit.getPlayer(entry.getValue()); if (target == null || !target.isOnline()) return true; @@ -257,6 +269,8 @@ private void processZombieBreakBlock(Zombie zombie) { private void applyZombieBreakBlock(Zombie zombie) { if (zombie == null || zombie.getHealth() <= 0) return; + if (!Feature.ZOMBIE_BREAK_BLOCK.isEnabled(zombie) || !isEligibleBlockBreakingZombie(zombie)) + return; if (activeBreakingTasks.containsKey(zombie.getUniqueId())) return; @@ -521,6 +535,12 @@ private boolean isValidTool(Material material) { return isPickaxe(material) || isShovel(material) || isAxe(material); } + private boolean isEligibleBlockBreakingZombie(Zombie zombie) { + return zombie.getType() == EntityType.ZOMBIE + && zombie.getEquipment() != null + && isValidTool(zombie.getEquipment().getItemInMainHand().getType()); + } + private boolean isPickaxe(Material material) { return material.name().contains("PICKAXE"); } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f79a4da..2d5a22b 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: SuddenDeath -version: 2.3.2 +version: 2.3.3 main: org.nguyendevs.suddendeath.SuddenDeath depend: [ProtocolLib] softdepend: [WorldGuard, SkinsRestorer, Lands, SuperiorSkyblock2, GriefPrevention, SimpleClaimSystem, xclaim]