wavedefense-0.5.3.jar
Curse Maven Snippet
What's new
Changelog
[0.5.3] - 2026-08-07 — portals become persistent gates
A portal used to be a doorway that closed moments after the first player stepped through, and it lived and died with the run. That made the one situation it exists for — a party going in and failing — self-defeating: the gate they had not managed to shut vanished with them, and the breach simply reopened somewhere else from the beginning.
A portal is now the arena's foothold in the world. It stands until the arena is actually cleared, keeps leaking waves while nobody is dealing with it, and accepts attempt after attempt. Players step through carrying their own gear, and come back out on the same doorstep with it plus whatever they found — the portal route takes nothing and hands nothing back.
Around that sits a run of correctness work on the same code: a portal that could only be used once, particle traffic an order of magnitude past the boundary ring, a backup system that had never completed a single backup, and a test suite that had never run.
Compile targets are unchanged: Forge 47.2.0, Minecraft 1.20.1, Java 17.
Upgrading: the network protocol moved to version 10, so client and server must be updated together. Arenas reached through a portal no longer take the player's inventory, so their keepInventory and starting-kit settings do not apply on that route. Entry through the admin menu is unaffected. Live sessions saved by an older build load fine; three portal bookkeeping fields were dropped and are ignored on read.
Changed — a portal now stands until its arena is cleared
- A portal used to close on a timer moments after the first player stepped through, and it lived and died with the run. A party that wiped therefore erased the very thing they had failed to shut: the location went quiet, and a fresh portal opened somewhere else with its penalty sequence back at wave one. There was no way to regroup and try again.
- A portal is now the arena's foothold in the world rather than a property of one attempt. It stands, keeps leaking waves while unattended, and accepts attempt after attempt until one succeeds. Clearing the arena is the only thing that takes it down, after which
portalDisappearsOnCompletedecides whether another is scheduled. - A failed attempt winds the run back — wave counter, timers, triggers, spawned mobs — but leaves the portal and its penalty progress untouched, so a repeatedly-failed portal keeps escalating instead of restarting.
- There is no joining window any more. A gate that is standing is open, full stop: walk in while a run is under way and you drop into it at whatever wave it has reached. The timed window only ever produced a locked gate in front of players trying to help, and a breach nobody can get to is the failure state, not a rule worth keeping.
maxPlayersPerLocationstill caps the party and the re-entry cooldown still applies. - Removed setting:
portalOpenAfterStartSec. It had nothing left to govern; a knob in the editor that changes nothing is worse than one less knob. Existing locations load fine — the key is simply ignored. Five lang keys that went with it are gone from all eight locales. portalDisappearsOnCompleteis honoured literally again: off leaves the gate standing after a clear, ready to be run again, with the penalty sequence reset to its first wave. It previously took the gate down either way and only changed how soon the next one opened.
Fixed — three ways the gate misread who had cleared it
- The grace period sealed the gate.
endSessionForLocationhard-codes "victory", and it is also what fires when the 30-second grace period runs out on an arena everyone walked away from. The portal read its fate from there, so abandoning a run counted as clearing it — the precise failure a persistent gate exists to prevent. Closing now hangs offtriggerVictory, the one place that genuinely means the arena was cleared. - Entry was announced before it happened. A player bounced by a re-entry cooldown, a full party or a broken location config was still told they had entered, and the reinforcement window opened for a party that was not there. Entry now reports whether it succeeded, and nothing is announced until it has.
- One player could block the whole party. The entry scan stopped at the first candidate standing on the gate, so if that player could not enter, nobody behind them could either for as long as they stood there. Everyone in range now gets their turn.
Fixed — surrender did nothing but put you back at the arena spawn
- Leaving through a portal puts the player back on it. The portal then saw somebody standing in the gate who was not in a location and pulled them straight in again — so pressing surrender teleported you to the arena's spawn point and nothing else, and the only way out of a location was to die. Leaving any arena now buys six seconds of being invisible to gates, which is enough to step off. The same trap could catch a menu entrant whose backup happened to return them near a gate.
Fixed — the gate was nearly invisible and had a one-block doorway
- Particles went out through the broadcast overload, which the client discards past 32 blocks. A gate may open up to 128 blocks away, so it simply did not exist until you almost walked into it. They now go per viewer with the long-distance flag — the same mechanism that makes a beacon beam visible across a valley — out to 160 blocks, with a column of light 24 blocks up so the gate can be spotted over terrain. Emission rate and outline resolution went up too; the first cut of the optimisation was too aggressive.
- Entry was a 2-block sphere centred one block above the ground, so standing at ground level already spent part of that budget on height and left barely a block of floor that worked — players reported minutes of walking back and forth hunting the spot. Horizontal and vertical reach are now separate: 3 blocks wide, 3 tall.
Added — fixed mob count, for bosses
- A wave entry can be marked fixed count: exactly that many spawn, whatever the party size, the wave number or the difficulty. Their stats still scale — a fixed boss is still tougher on Nightmare, there is simply one of it.
- This exists because the number in the editor is a base, and four multipliers stack on it invisibly: per-wave growth, the player count, the auto-scaler's current difficulty and the difficulty preset. A player who configured two liches for wave four met ten of them. The per-entry summary line now says which of the two modes an entry is in, and the growth field is hidden while the count is fixed.
- Editing a mob also silently reset its scatter radius to 0 — the edit screen's working copy never carried the field across.
Added — Mine and Slash mob levels can follow the party
- Level to match the player: each mob is scaled to the level of the nearest player in the arena, using Mine and Slash's own spawn scaling rather than a reimplementation of its curve. An arena that a level-5 and a level-60 player both walk into cannot be one number.
- A level range: a second field sets an upper bound, and each mob rolls uniformly between the two, so a wave has spread instead of clones.
- Precedence is match-player → range → fixed level. With nobody in the arena — a portal penalty wave, say — match-player falls back to the configured numbers instead of leaving the mob at whatever level it happened to spawn with.
Fixed — the auto-activate zone leaked a session per location
- It created a
LocationSessionunconditionally, every tick, for every auto-activate location — so an arena nobody had gone near still held a live session that nothing ever ended, and it was written to the runtime-state file on every save. The same shape as the ghost sessions the portal used to leave behind. A session is now created only once there is something to record.
Added — a button to open a portal on demand
- The Portal section of the location editor gets Open a gate now: places this location's portal immediately, near a random player who is not already in an arena. It saves the location first, so a radius typed a second earlier is the radius used.
- The reply is always specific — the coordinates where the gate opened, or the reason it could not: portals disabled, nobody in the Overworld, everyone already inside an arena, a gate already standing (with its position), or no loaded ground within the radius.
- The same reasons now reach the server log on the automatic path too. Every refusal in the spawn routine used to be a bare
return, so an admin whose portal never appeared had nothing at all to go on. A successful placement logs its coordinates and distance.
Fixed — mobs stayed in the arena after everyone surrendered
- Only the main wave's tracking set was swept at teardown, so trigger-wave mobs were never removed — they are spawned
persistenceRequiredand their set was cleared along with the session, leaving them in the arena with nothing tracking them. - The tracking sets are not a complete record either: the periodic dead-mob sweep drops any entity it cannot find, and it cannot find one in an unloaded chunk — routine on a large arena. Such a mob was counted as killed, forgotten, and then survived teardown. Teardown now makes a second pass over loaded entities and removes anything still stamped with the location, whatever the tracking sets believe.
- Note that surrendering mid-wave starts the 30-second grace period first, in case somebody rejoins; the arena is cleared when that expires, not immediately.
Fixed — penalty mobs skipped Mine and Slash and had no reason to hunt
- Mobs spilling out of an uncleared portal were built by a separate spawn path that gave them their equipment and nothing else: no Mine and Slash level or resistances even on a location configured for them, and no targeting goal or follow range, so they milled around the gate. They are the breach — coming for the players is the entire point. Both are applied now, exactly as the arena applies them to its own mobs.
- The rest of the Mine and Slash bridge was verified against the installed mod (6.1.15) by resolving every reflected target rather than by inspection:
EntityData.getis static and present,setLevel,getCustomExactStatsandrecalcStats_DONT_CALLall match, and the six stat ids it writes —bonus_expand the five*_resist— are the real GUIDs. Nothing there needed changing.
Fixed — a wiped arena was not fully reset for the next party
- Three cleanups lived in
endSession, and the wipe path does not go through it: when the last defender died, the run was ended directly. So a party that got killed left behind its trigger-wave mobs (only the main wave was swept, and the tracking set was then dropped, so nothing could ever remove them), its info panels, and every one-time trigger wave still flagged as fired — the next party walked into a half-used arena and never saw those waves. All three now happen in the one place every ending goes through, so no exit can skip them. The PvP teardown uses the same funnel. - The run itself was already wound back to wave one on a wipe; what was missing was the world around it.
Fixed — a portal entrant was teleported twice on surrender
- Surrendering put them back on their portal and then immediately moved them again to the arena's surrender exit, which for a portal entrant is the wrong place by design. The arena's exit points apply only to players who arrived some other way.
Fixed — turning a portal off left the gate behind
- Disabling
portalEnabledon a location with a gate standing skipped that location entirely, so the portal was never taken down: invisible, since nothing ran for it, but still holding its session open indefinitely. It is now closed properly.
Changed — portal entry takes nothing and gives nothing back
- Players step through a portal exactly as they are: no inventory is taken, no starting kit handed out, no backup made. What they carried in comes back out with them, along with whatever they found. On the way out they are put down on the portal they used, in its dimension, rather than at the arena's configured exit point — that point is for players who arrived some other way.
- Game mode is the one thing restored, since entering an arena can change it and there is no snapshot to read it back from.
- Entering through the menu is unchanged: inventory is still taken, backed up and returned.
Changed — an uncleared portal escalates instead of stalling
- Penalty waves are driven by the clock alone: one of the location's waves leaves the portal, then the configured clearing period runs again before the next, whether or not the previous one is still standing. Sequential mode used to wait for a clear field, which meant a portal nobody dealt with stalled on wave one indefinitely — the opposite of a penalty.
- Exactly one penalty wave stands at a time — whatever the last one left alive is removed as the next arrives. Otherwise every interval stacked another wave on top of the last, and since the sequence gets harder as it goes, a portal left alone overnight would bury the ground around it. It also keeps the threat honest: what is standing outside a portal is always the wave the sequence has actually reached.
Changed — portals open near a player, on a radius you choose
- New per-location portal spawn radius (5–128 blocks, default 30). Placement is a ring around the chosen player rather than the old fixed ±30-block square, whose corners reached 42 blocks while its edges reached 30.
- The candidate is now drawn from players in the Overworld who are not already inside an arena. Previously any online player could be picked and the portal was placed in the Overworld at whatever coordinates they had — so a player in the Nether, where the horizontal scale is 1:8, got a gate thousands of blocks away, and a player already fighting in an arena got one opened on top of the match.
- The gate's dimension is now recorded and followed by everything that reads it — particles, the entry check, penalty spawns, the nearby broadcast — instead of each one assuming the Overworld. It stays the Overworld in practice: a location is a set of coordinates with no dimension of its own, and entering one teleports the player within the dimension they are standing in, so a gate opened elsewhere would drop the party at the arena's coordinates in the wrong world.
- "A portal has appeared" goes to players within 50 blocks instead of the entire server.
Fixed — a portal could only ever be used once
- Closing had two paths and only one of them cleaned up. The stay open for latecomers timer — the one an admin enables precisely so a party can follow the first player in — cleared the portal's position and nothing else, leaving the entered-count above zero and the entry deadline in the past. It also skipped setting the respawn timer, so a new portal opened on the next tick. The first player to touch it was read as a latecomer to a party whose window had already shut and was told the portal was closed. So was everyone after them: with a positive open-window configured — which is what the editor's own reset button sets — the location became permanently unenterable by portal after its first run. Both paths now share one teardown.
Fixed — an unentered portal ran the arena by itself, forever
PortalManagercreated aLocationSessionthe moment it placed a portal, purely to have somewhere to store the portal's position. The session tick loop had no check for whether anyone was playing, and a fresh session arrives in exactly the state it reads as "lobby over, start wave 1". So an empty arena marched through every wave — each spawn attempt refused for want of players, each refusal logging a warning — and finished attriggerVictory, which incremented the location's persisted completed-sessions counter and tore the session down. That took the portal with it, so the next tick placed a new one and announced it to the whole server again. A portal nobody walked into looped like that indefinitely.- Sessions now advance only when someone is actually in the location. The two countdowns that legitimately run on an empty arena — the grace period after the last player leaves mid-wave, and the post-victory linger — are explicitly allowed through, since both end the session themselves.
- The session tick, the portal's "is this arena in use" test and the mob spawner now ask one shared question, so they can no longer disagree about whether a game is running.
Fixed — portals stacked penalty mobs without limit
- Each penalty wave replaced the tracked mob set instead of adding to it, orphaning everything the previous wave spawned: those mobs stayed in the world untracked, so the portal's cleanup could not discard them and its "is the field clear?" test could not see them. With a fixed penalty wave configured, an ignored portal accumulated mobs with no ceiling. Spawns are tracked cumulatively now, dead ones pruned each tick, and a fixed penalty wave stops firing while 60 of its mobs are still alive.
portalPenaltyTimerSecaccepted 0, at which point the countdown fell past zero on the tick it was read — a fresh penalty wave every tick. Floored at 5 s, both in the setter and where it is used, since locations saved earlier keep their stored value.
Fixed — portal particles cost more than everything else combined
- The gate was drawn every tick at a resolution that grew with its size: an outline of
radius × 12columns each sending two particle packets, plus a solid grid fill of the interior. On a large arena that is close to 500sendParticlescalls per tick — roughly ten thousand packets a second to every client within 32 blocks, an order of magnitude past the boundary ring players already reported as an FPS drop. It ran whether or not anyone was near enough to see it. - Now: emission every 4 ticks, a fixed outline column count, the second particle type on every fourth emission, and a handful of random interior points instead of a grid — with an early exit when no player is in range. Roughly a twentyfold reduction, and the gate looks the same in motion.
Fixed — entering an arena duplicated the gear you were wearing
PlayerBackupcaptured the inventory by walkinggetContainerSize(), which spans main slots, armour and offhand, and captured armour and offhand again into their own lists. Restore wrote both, so a player who walked into an arena in full diamond walked out wearing it and carrying a second set. Every entry and exit minted another copy.- The same loop skipped empty slots, compacting the list, while restore wrote it back by index — so what did survive came back shuffled to the front of the pack, hotbar and all.
- Capture is now per compartment, in slot order, empties included; restore writes each compartment from its own list. A test guards the shape, since the old version reads like a perfectly ordinary loop.
Fixed — "keep what you found" cost you what you brought
- On exit from an arena with keep loot on exit, the code snapshotted the arena inventory slot by slot, restored the backup over it, then wrote the snapshot back by slot index. On an arena that clears your inventory on entry — the normal case — the loot sits in the low slots, exactly where the restore had just put the player's own gear. Every kept item overwrote something the player had walked in with.
- Exit now picks one of three routes depending on what the arena did on the way in: leave the pack alone when the inventory was never taken, add the loot to free slots (dropping the overflow at the player's feet) when it was, and plain restore when loot is not kept.
Fixed — the arena boundary was a sphere but looked like a wall
- The containment test measured three-dimensional distance, so height counted towards the radius, while the particle wall players see is a vertical ring at the spawn's altitude. On any arena with a tower, a roof or a drop the two disagreed: climb higher than the radius above the centre and you were "outside" — surrendered or damaged — in plain view of a boundary tens of blocks away. The check is horizontal now, matching what is drawn.
- The boundary is centred on the player spawn point, and with no spawn set it did nothing at all — silently, with the toggle on and a radius filled in, which reads to an admin as working. It now says so in the log, once per location.
- The out-of-bounds countdown was never cleared when a player left the arena. The map grew with every exit, and someone who left while outside resumed their surrender countdown where it stopped if they came back later. Leaving a location now goes through one method that clears both.
Fixed — the party size cap only applied to one way in
maxPlayersPerLocationwas enforced in the teleport packet, so the menu respected it while the portal, the auto-activate zone and triggers all walked past it. Moved to the single funnel every route goes through.
Fixed — the backup system had never produced a single backup
performBackupopens by stamping a folder name, and the formatter it used was built without a time zone while being handed anInstant. AnInstantis a point on the timeline with no calendar attached, so asking it for a year threwUnsupportedTemporalTypeExceptionon the first line of every backup run — scheduled, manual and pre-restore alike. Nothing was ever written towavedefense_backups/.- The formatter now carries the system zone, matching the one a few methods below it that was already correct. That asymmetry is why it went unnoticed: the code looked right at a glance and the failure was swallowed into a log line.
- This is why the automated suite matters: the very first run after the JUnit launcher was fixed caught it. See below.
Fixed — the test suite had never actually executed
junit-jupiterdoes not bringjunit-platform-launcherwith it and ForgeGradle does not inject one, sodied with
gradlew testNoClassDefFoundError: LauncherFactorybefore running anything — while still reporting a green-looking task on some invocations. Every test in the repository was decorative.- With the launcher added as
testRuntimeOnly, the suite ran for the first time and immediately failed seven times, in three unrelated places: the backup timestamp above, a guard test that had gone stale, and five session tests that had been broken since the code they cover was written. LocationSectionTesthad a hand-maintained copy of every persisted NBT key, which drifted the moment the endless/modifier fields landed — a guard test failing for its own staleness rather than for a real defect. It now scrapes the key list out ofLocationSerializerat run time, so it cannot fall behind again.LocationSessionreached intoWaveManager.triggerEvaland.autoScaleras public final fields. A test double cannot fill a final field, so every test that ticked a session hit a null it had no way to resolve. Both now go through accessors.- 89 tests, all passing — up from 82, with new guards on the value ranges that used to produce a runaway and on the shape of the inventory capture.
Notes
- Translation keys: 1504 → 1509 across all 8 locales (parity checked). Ten added across the portal spawn radius, the open-a-gate button, the Mine and Slash level range and the fixed-count toggle; five removed with the joining window.
- The network protocol moved to version 10.
SpawnPortalPacketwas appended to the table, and packet ids come from registration order, so client and server must be updated together — the version gate refuses a mismatched connection rather than letting it misread the stream. Existing ids are unchanged; only the new one is added at the end. - Five
LocationSessionportal fields were dropped —portalEnteredCount,portalGraceEndTime,portalFirstPlayerEntered,portalEnteredPlayersandportalOpenUntilMs. Older runtime-state files still load; those keys are simply ignored.
This mod has no additional files

