ZombieSpawnService.java — spawnBossZombie()setArmorContents() was called without setting drop chances to 0.
Bukkit's default drop chance is ~8.5%, so players could farm Diamond Helmet /
Chestplate by killing the Boss.setHelmetDropChance(0f), setChestplateDropChance(0f),
setLeggingsDropChance(0f), and setBootsDropChance(0f) after every armour
assignment, including the custom-model-data head path.evolveZombie() multiplier overriddenZombieSpawnService.java — spawnBossZombie()spawnCustomZombie() internally called evolveZombie() which
applied a tier health multiplier (up to ×2.5 for ABERRANT), but spawnBossZombie()
then hard-reset the attribute to boss.health config value, discarding the tier
scaling entirely.spawnBossZombie() now sets bossHealth first, then calls
evolveZombie() so the tier multiplier compounds on top of the intended base.
Boss HP in late-game now scales correctly with the threat level.ZombieEvolutionService.java — determineTier()else branch covered threat >= 30, but the class-level
comment stated "Threat > 50: Aberrant possible". Players hit ABERRANT-tier
enemies during Phase 3 (Day ~15) instead of Phase 5 (Day ~25+).threat < 50 bracket (no ABERRANT, Mutated max)
and moved ABERRANT to threat >= 50 only, matching the original design intent./zapoc infect shows hardcoded "10m" regardless of configZombieApoc.java — onCommand() case "infect"incubationTicks was read from config but immediately discarded;
the player message used the literal string "10m".timeDisplay is now derived from incubationTicks / 20 seconds so the
displayed time always reflects the actual configured value.hashCode() % 2 (uneven distribution)ZombieBehaviorTask.java — run()UUID.hashCode() is a 32-bit hash with no uniformity guarantee.
In practice this can result in 70 % of zombies processed on one tick and 30 % on
the other, defeating the purpose of two-bucket batching.UUID.getLeastSignificantBits() which is
a raw 64-bit random value from the UUID v4 spec, giving near-perfect 50/50 split.HallucinationListener double-registration on /zapoc reloadZombieApoc.java — startTasks()startTasks() checked if (hallucinationListener == null) before
creating a new instance, but cancelAllTasks() already set it to null.
In specific edge cases (e.g. psychological-horror.enabled toggled via config
between reloads) the old instance could survive unregistered yet the new instance
would be registered on top.startTasks() now unconditionally calls HandlerList.unregisterAll() and
nulls the field before constructing a fresh HallucinationListener, making the
lifecycle explicit and immune to ordering issues.WorldEventTask blood-moon maps leak UUID entries for unloaded worldsWorldEventTask.javabloodMoonStates and bloodMoonRolledDay used world UUIDs as keys
but never removed entries when a world was unloaded (e.g. on map rotation or
/mv unload). Over time this created an unbounded set of stale entries.WorldEventTask now implements Listener and handles WorldUnloadEvent
to remove both maps' entries for the unloaded world. The task is registered and
unregistered as a listener in startTasks() / cancelAllTasks().ZombieBehaviorTask.javamoanEnabled, moanChance, moanVol, moanPitch,
mutationEnabled, mutationInterval, and breakingEnabled were fetched from
FileConfiguration on every call to run().reloadSunConfigCache().handleExplosive() reads explosive.power from YAML at explosion timeZombieBehaviorTask.javacachedExplosivePower field, populated in reloadSunConfigCache().CampfireManager.isInsideSafeZone() reads check-radius from YAML per callCampfireManager.javaplugin.getMechanicsConfig().getInt("campfires.check-radius", 100)
was called on every invocation. isInsideSafeZone() is called once per second
per online player from PlayerStatusTask, making this a hot path.cachedCheckRadius field, initialised in the constructor and
refreshed in reload().TurretListener used legacy ChatColor string APITurretListener.javanet.kyori.adventure.text.Component
using NamedTextColor and TextDecoration, the standard approach on Paper 1.16+.ZombieApoc.sendActionBar() used BungeeCord spigot bridgeZombieApoc.javaplayer.spigot().sendMessage(ChatMessageType.ACTION_BAR, ...)
with the Paper-native player.sendActionBar(Component) call.
Removed unused net.md_5.bungee.api.* imports.Turret system completely non-functional — Adventure vs. Legacy API mismatch (BF-16) —
The crafting recipe assigned the "Sentry Turret" display name via the Adventure API
(meta.displayName(Component.text(...))). TurretListener.isTurretItem() read the name
with the deprecated Legacy API (meta.getDisplayName()), which always returns an empty
string when the name was set via Adventure — so the check never matched and
TurretManager.createTurret() was never called. Placing a crafted turret had no effect.
isTurretItem() now reads the Adventure displayName() component first, serialises it to
plain text, and falls back to the legacy accessor for pre-migration items.
onDisable() never called turretManager.cleanup() — turret data loss on restart (BF-17) —
CampfireManager.shutdown() was called in onDisable() but the analogous
TurretManager.cleanup() call was absent. cleanup() is responsible for cancelling the
internal firing task and flushing turrets_data.yml. Turrets placed during a session
without a prior auto-save were silently lost on server restart.
turretManager.cleanup() is now called from onDisable().
/zapoc reload wiped all zombie custom AI — missing scanExistingZombies() call (BF-19) —
loadConfig() (called by loadAllConfigs() at the start of every reload) called
trackedZombies.clear(). ZombieBehaviorTask iterates that collection each tick; after
a reload it was empty, so every living custom zombie lost its AI (no moans, no screaming,
no sun effect, no mutation) until the zombie died or new ones spawned.
scanExistingZombies() is now called at the end of the reload handler to repopulate
the map immediately.
/zapoc reload did not rebuild TurretManager — config changes had no effect (BF-18) —
TurretManager reads range, damage, and fire-rate-ticks only in its constructor.
After a reload the old instance kept running with stale values. The reload handler now
calls turretManager.cleanup() and recreates a fresh TurretManager, matching the
pattern already applied to StructureManager.
Feign-Death zombie permanently invulnerable and AI-less after server restart (BF-20) —
When a Feign-Death zombie "died", the code set invulnerable=true and AI=false, then
scheduled a BukkitRunnable to revive it after 3–5 s. If the server restarted during
that window, the task was discarded but the entity flags persisted in NBT — leaving an
immortal, motionless zombie that could never be killed. A new PDC key feign_active is
set at the start of feign-death and cleared on revive. ZombieCleanupListener.onChunkLoad
now detects this key and immediately invokes the shared SpecialZombieListener.reviveZombie()
method, which also handles the scheduled revive path.
CampfireManager.damageCampfire() triggered a full YAML write on every zombie hit (BF-21) —
saveCampfires() (atomic file I/O on the main thread) was called each time a zombie
attacked a campfire. With multiple zombies attacking multiple campfires simultaneously
during a Blood Moon, this caused significant main-thread stalls. Replaced with a dirty
flag: damage marks the campfire data as dirty, and the periodic task flushes to disk at
most once every ~5 seconds. Immediate saves are still performed when a campfire is
destroyed.
ZombieBehaviorTask.handleScreamer() read 6 YAML values per zombie per 0.5 s (BF-23) —
Every invocation of handleScreamer() called getZombiesConfig().getDouble/getString/getInt()
six times. With 20 Screamer zombies that is 120 YAML map lookups per 500 ms. All six
values (scream-chance, particle, particle-count, scream-range, darkness-duration,
message) are now cached in the same 5-second refresh cycle used by sun-effect config.
ZombieBehaviorTask.handleBreaker() read success-chance from YAML per zombie per 2 s (BF-24) —
zombie-breaking.success-chance was fetched from FileConfiguration inside the per-zombie
loop every 40 ticks. Now cached alongside the other task-level values.
PsychologicalHorrorTask.activeHallucinations used entity ID (int) as map key (BF-25) —
Entity IDs are 32-bit integers reused by the server after an entity despawns. Using
entity.getEntityId() as the map key could cause stale HallucinationData entries to
collide with or shadow new entries sharing the same recycled ID.
The key type is now UUID (entity.getUniqueId()), which is guaranteed unique per entity.
Campfire heal task used 3×3 chunk scan instead of 5×5 (BF-22) —
isInsideSafeZone() and onSpawn() were already corrected to 5×5 in a prior release,
but the heal loop inside startTask() still searched only 3×3 chunks (~32 block reach).
Tier-3 campfires have a 40-block radius, so players near the edge of the safe zone
were not receiving the Regeneration effect. The heal loop now uses 5×5, consistent with
all other campfire range checks.
data.yml, campfires.yml, and
turrets_data.yml files are fully compatible with 4.5.1.turrets_data.yml will
load and function normally. Turrets placed during a session that ended with a 4.5.0
server stop without an intermediate save may have been lost — this is the bug being fixed.Нет описания изменений
SurvivalGuideListener.PAGE_KEY NPE on startup — static final NamespacedKey PAGE_KEY
was initialised at class-load time before the plugin instance existed, causing a
NullPointerException on every server start. PAGE_KEY is now an instance field
initialised in the constructor. (BF-01)
InfectionListener ignored infection.enabled config — infection spread on every
zombie hit regardless of infection.enabled: false in config.yml. ZombieBehaviorService
already checked the flag but InfectionListener did not, making the config option
partially ineffective. (BF-02a)
InfectionListener ignored zapoc.immune permission — players with zapoc.immune
could still receive infection points. The permission is now checked in both
InfectionListener and ZombieBehaviorService. (BF-02b)
Day-based spawn-rate (shouldSpawn()) was completely bypassed — ZombieBehaviorService
runs at EventPriority.HIGH and cancelled natural spawns before SpawnListener ran,
so the phase-based rates (40% / 70% / 90% / 100%) never took effect — zombies always
spawned at 100% regardless of server age. shouldSpawn() is now called directly inside
ZombieBehaviorService.onCreatureSpawn. (BF-03)
TurretManager could permanently delete turrets_data.yml — the save code called
dataFile.delete() before renameTo(). If the rename failed the data file was gone
permanently. Replaced with Files.move(ATOMIC_MOVE) and a non-atomic fallback. (BF-04)
Sanity config multipliers loaded but never applied — sanityDrainBase,
nightMultiplier, infectedMultiplier, lowHealthMultiplier, and
nearbyZombiesMultiplier were read from mechanics.yml but updateSanity() used
hardcoded constants. Editing these values in config had no visible effect. All five
multipliers are now applied correctly. (BF-05)
whisperPhrases config key loaded but never used — handleWhispers() used an
internal hardcoded array instead of the phrases from mechanics.yml. Player-configured
phrases including phrases-vi / phrases-en are now displayed correctly. (BF-06)
hg.no-netherite-drop bypass via corpse zombie — the death handler stripped netherite
from event.getDrops() but then re-equipped the corpse zombie with raw armour including
netherite. A player could kill the corpse to recover the items. Netherite is now stripped
from the corpse's equipment when the option is enabled. (BF-07)
Tank zombie ENTITY_RAVAGER_STEP sound spam — handleTank() fired a full-volume step
sound every 0.5 s with no throttle, flooding clients when multiple Tank zombies were
active. A 10% per-call chance now limits the frequency. (BF-08)
panicCooldowns HashMap memory leak — UUID entries were added on /zapoc panic but
never removed on disconnect. Entries are now removed in onPlayerQuit. (BF-09)
Stalker spawn used zombie's world for player's Y lookup — getHighestBlockYAt() was
called on the zombie's world; if the target player had recently changed worlds the Y
could be wrong. Player's own world is now used for the lookup. (BF-10)
handleSunEffect() YAML reads inside per-zombie loop eliminated — seven
FileConfiguration.get*() calls per zombie per tick removed. All sun-effect config
values are now cached at task level and refreshed every 5 s. Eliminates ~1,400
redundant map lookups per second at 200 tracked zombies. (BF-11)
Math.random() replaced with ThreadLocalRandom throughout — 20+ usages replaced
across CampfireManager, ZombieApoc, ZombieBehaviorService, ZombieBehaviorTask,
PlayerStatusTask, WorldEventTask, ZombieEvolutionService, and
SpecialZombieListener. (BF-12)
data.yml, campfires.yml, and
turrets_data.yml are fully compatible.| # | Severity | File | Description |
|---|---|---|---|
| 33 | 🔴 | ZombieSpawnService | Zombie names encoded as §c§lSCREAMER → fixed to §c§lSCREAMER. Affected: SCREAMER, Stalker, Crawler, Feign Death, Boss. Names were rendering as garbage text on all servers. |
| 34 | 🔴 | ZombieApoc (onEnable) | registerRecipes() was never called — comment RECIPES REMOVED was accidentally left after feature re-enable. All 6 vanilla crafting recipes (bandage, antivirus, adrenaline, zombie camo, radio, turret) were silently unavailable. |
| 35 | 🔴 | BandageItem | create() read custom-model-data from getPluginConfig() (config.yml) instead of getItemsConfig() (items.yml). Bandages would always spawn without CustomModelData, breaking resource packs. |
| 36 | 🔴 | SurvivalGuideListener | SurvivalGuideHolder.getInventory() returned null — violates InventoryHolder contract. Fixed: holder stores and returns its own Inventory reference. |
| 37 | 🔴 | MilitaryCheckpoint | spawnTowerSniper() re-rolled random offset/height instead of using the cached towerBase/towerHeight computed in cacheTowerPosition(). Tower snipers could spawn anywhere except the actual tower. |
| 38 | 🟠 | AdrenalineItem | onInteract() lacked RIGHT_CLICK_AIR / RIGHT_CLICK_BLOCK guard. Item could trigger on LEFT_CLICK_BLOCK in edge cases. |
| 39 | 🟠 | ZombieApoc (TabComplete) | /zapoc give camo was listed in tab-complete but the switch-case expected "zombie-camo". Fixed to "zombie-camo" in both places. |
| 40 | 🟠 | CrashedSupplyPlane | setMaxHealth() deprecated since Bukkit 1.16. All mob health now uses Attribute.GENERIC_MAX_HEALTH.setBaseValue(). Affected: pilot, supply guard, medic, soldier zombies. |
| 41 | 🟠 | MilitaryCheckpoint | Same setMaxHealth() deprecation fix for heavy soldiers, commanders, and tower snipers. |
| 42 | 🟠 | AbandonedHospital | WHITE_BED placement ignored required BlockData facing direction — bed failed silently. Replaced with WHITE_WOOL as visual equivalent. |
| 43 | 🟠 | RadioItem | Lore cooldown update only scanned main hand + off hand. If player moved radio to any other slot the cooldown label stayed stale forever. Fixed to scan full inventory. |
| 44 | 🟡 | Structure types (×5) | isReplaceable() and isValidMobSpawn() copy-pasted in 5 classes. Extracted to new StructureHelper utility class. |
| 45 | 🟡 | AbandonedHospital, AbandonedOutpostPro, SurvivorCamp | Structure zombies used vanilla world.spawnEntity() — bypassing custom AI, tier evolution, sun effects, and tracking. Now use ZombieSpawnService.spawnCustomZombie(). |
StructureHelper (structure/StructureHelper.java)
isReplaceable(Material) — shared replaceable-block checkisValidMobSpawn(World, Location) — 3-block column spawn validationsetBlockSafe(World/Location, Material) — convenience block-place wrapper§c§lSCREAMER) on all servers due to source-file encoding corruption. All names now render correctly.registerRecipes() is now correctly called in onEnable.BandageItem.create() read CMD from the wrong config file (config.yml instead of items.yml). Bandages now respect resource-pack model overrides.SurvivalGuideHolder.getInventory() returned null, violating the InventoryHolder contract. The holder now stores and returns a proper Inventory reference.MilitaryCheckpoint snipers re-randomised their spawn offset on every call instead of using the cached tower position from cacheTowerPosition(). Snipers now always spawn on the tower platform they are supposed to guard.RIGHT_CLICK_AIR / RIGHT_CLICK_BLOCK guard; item could fire on left-click in edge cases./zapoc give zombie-camo — tab-completion listed "camo" but execution expected "zombie-camo". Both are now consistent.setMaxHealth() deprecation — replaced all deprecated LivingEntity.setMaxHealth() calls in CrashedSupplyPlane and MilitaryCheckpoint with Attribute.GENERIC_MAX_HEALTH.setBaseValue().WHITE_BED placement requires directional BlockData not set during procedural generation, causing silent failures. Replaced with WHITE_WOOL as a visual equivalent.AbandonedHospital, AbandonedOutpostPro, and SurvivorCamp spawned plain vanilla zombies that bypassed custom AI, tier evolution, sun effects, and the tracking map. They now call ZombieSpawnService.spawnCustomZombie(), so all structure zombies scale with server difficulty like any other custom zombie. Thematic equipment (hospital gown, outpost guard gear, survivor leather) is applied on top of the custom zombie.StructureHelper utility class — isReplaceable(Material), isValidMobSpawn(World, Location), and setBlockSafe() were copy-pasted identically across 5 structure classes. This shared utility eliminates the duplication. All structure implementations now delegate to StructureHelper.data.yml, campfires.yml, and turrets_data.yml are fully compatible.
ZombieApocalypseSSS is a comprehensive Minecraft plugin that transforms your server into a thrilling zombie survival apocalypse. Featuring advanced zombie AI, evolving variants, player infection mechanics, psychological horror elements, and dynamic events