
Список изменений
[4.6.2] — Gameplay Bug Fix Patch (2026-05-23)
Fixed — 9 bugs across 4 files
These bugs were identified through source-code analysis of v4.6.1. None require a database migration or config change.
🔴 Critical — Broken every match
-
BUG-1 ·
GameTask— PLAYING→DEATHMATCH transition bypasses FSM (GameTask.java)GameTask.handlePhaseTransitions()calledarena.setState(MatchState.DEATHMATCH)directly instead ofplugin.getMatchService().transition(arena, MatchState.DEATHMATCH). Bypassingtransition()meant:ArenaDeathmatchEventwas never fired — external plugins received no notification.scheduleDeathmatchTask()was never called — players were not teleported to the Cornucopia centre at Deathmatch start.- The rapid border collapse to 10 × 10 over 60 s never happened.
- Scoreboard was not refreshed via
updateScoreboards().
Fix: replaced
arena.setState(MatchState.DEATHMATCH)withplugin.getMatchService().transition(arena, MatchState.DEATHMATCH). -
BUG-2 ·
GameTask— three different config keys used for the same border size (GameTask.java)Three separate code paths read the "initial border size" value using three different config keys:
config.ymldeclares:game.border-initial-size: 2000(the canonical key)MatchService.javareads:"game.border-initial-size"✅ correctGameTask.javaline 90 read:"game.initial-border-size"❌ key does not exist → fallback1000.0GameTask.javaline 100 read:"game.border.initial-size"❌ key does not exist → fallback1000.0
Effect: the game loop always set the initial border to 1 000 blocks regardless of the admin's configured value. Any server with
border-initial-sizeset above or below 1 000 saw unexpected border behaviour from the very first second of a match.Fix: unified both
GameTaskreads to use"game.border-initial-size". -
BUG-3 ·
MatchService.startMatch(Arena, int)— countdownBukkitRunnablenot stored (MatchService.java)The overload
startMatch(Arena arena, int countdownSeconds)created a countdownBukkitRunnablebut discarded the returnedBukkitTask. The task was never added tocountdownTasks, socancelAllTasksFor()could not cancel it. CallingstartMatch()twice on the same arena (e.g. two rapid/hg startcommands, or a player join triggering auto-start while a manual start was in progress) launched two concurrent countdowns. Both reached zero and each calledstartArena(), resulting in the match starting twice — players teleported twice, inventory cleared twice, cage system entered an inconsistent state.Fix: stored the returned
BukkitTaskincountdownTasks.put(arena.getName(), task).
🟡 Medium — Visible gameplay defects
-
BUG-4 ·
MatchService+GameTask— WorldBorder controlled by two systems simultaneously (MatchService.java)MatchService.transition()(called when enteringPLAYING) scheduledscheduleBorderTask(), a periodic timer that shrank the border every N minutes. At the same time,GameTask.handleWorldBorder()ran every server tick duringPLAYINGand recalculated the border size via smooth linear interpolation, then calledWorldBorder.setSize(). The per-tick recalculation fired every second and silently overrode the periodic task'ssetSize(size, duration)smooth animation, causing the border to visibly flicker or jump rather than shrink smoothly.Fix: removed the
scheduleBorderTask(arena)call fromMatchService.transition(PLAYING).GameTask.handleWorldBorder()is now the sole authority for border size — it provides smoother and more precise interpolation than the coarse periodic approach. -
BUG-5 ·
Arena.getActiveLegendaryLocations()— hologram names always show material ID (Arena.java)The method read
recipe.getItemMeta().getDisplayName()(legacy Bukkit API). All legendary items in this plugin have their display names set via the Adventure Component API (meta.displayName(Component)). The legacygetDisplayName()method returns""for any such item, sohasDisplayName()was alwaysfalse, and every legendary hologram fell back to showingrecipe.getType().name()— raw material names likeWOODEN_SWORDinstead of the intended weapon name (e.g.Excalibur).Fix: replaced
getDisplayName()/hasDisplayName()with Adventure'sPlainTextComponentSerializerto extract the plain-text string from the Component display name. -
BUG-6 ·
MatchService.checkWin()— game doesn't end when a full team is the last survivor (MatchService.java)checkWin()ended the game whenaliveCount == 1. In team mode, if the final two (or more) surviving players were all on the same team,aliveCountwas ≥ 2, so no win was declared and the game ran indefinitely or until the Deathmatch timer expired — forcing teammates to kill each other.Fix: after counting alive players, if
aliveCount > 1the method now checks whether all surviving players share a single team identifier. If every alive player belongs to the same team (or all are solo/no-team and there is only one unique identity),endGame()is called immediately.
🟢 Minor — Low-severity improvements
-
BUG-7 ·
TeamManager— deprecatedChatColorusage produces compile warnings (TeamManager.java)TeamManagerandTeamDatauseorg.bukkit.ChatColorfor team color storage, which is deprecated on Paper 1.21 in favour ofnet.kyori.adventure.text.format.NamedTextColor. Migrating the underlying storage type would require a database schema change andDatabaseManagermigration; that is deferred to a future version. Added@SuppressWarnings("deprecation")onloadColors()with aTODOcomment documenting the planned migration path, silencing the compiler warnings without any behaviour change. -
BUG-8 ·
MatchService.startCountdown()— countdown task not stored, leaks on arena reset (MatchService.java)startCountdown(Arena, List<Player>)(called from the mainstartMatch(Arena, List<Player>)flow) created a countdownBukkitRunnablebut did not add the task tocountdownTasks. If the arena was reset before the countdown finished (e.g. all players left), the orphaned task continued ticking until it reached zero and calledreleasePlayers()on a now-empty arena — potentially transitioning a WAITING arena to PLAYING with no players.Fix: stored the returned task in
countdownTasks.put(arena.getName(), taskRef[0]). -
BUG-9 ·
MatchService— cage cleanup storesBlockobjects instead ofLocation(MatchService.java)arenaCageswas declaredMap<String, List<Block>>.Blockobjects hold a direct reference to the chunk at the time they were obtained. If chunks unload and reload beforebreakGlassCages()runs, the staleBlockreference may report an incorrectgetType()— the GLASS check would fail and the cage blocks would not be cleared, leaving orphaned glass geometry in the arena world.Fix: changed
arenaCagestoMap<String, List<Location>>.breakGlassCages()now callsl.getBlock()at removal time, always querying the current (guaranteed-loaded) chunk state. BothcreateJoinCage()andteleportPlayersAndCreateCages()updated to storel.clone().
Changed
pom.xml— version4.6.1→4.6.2plugin.yml— version4.6.1→4.6.2; description updatedpaper-plugin.yml— version4.6.1→4.6.2
