## [1.1.0] — Bug Fix & Refactor Release
### Bug Fixes
#### 🔴 Critical
- **[BUG-1] DonutShardProvider.take() race condition** — Replaced two-step
`getBalance() → take()` with `shardManager.safeTake()` (atomic CAS via
`ConcurrentHashMap.compute()`). Players can no longer go negative or bypass
balance checks when AFK ticker and shop purchase happen simultaneously.
- **[BUG-2] ShopHook.charge() double-fires spent hook** — `DonutShardAPI.take()`
already calls `fireSpentHooks()` internally. Removed the redundant
`plugin.getApiProvider().fireSpent()` call in `ShopHook.charge()`. External
`ShardEventHook` implementations no longer receive 2 events for 1 purchase.
#### 🟡 Medium
- **[BUG-3] DonutShardProvider.hooks not thread-safe** — Changed
`ArrayList<ShardEventHook>` to `CopyOnWriteArrayList`. Prevents
`ConcurrentModificationException` when hooks are fired from the async SQLite
writer thread while another thread calls `registerHook()` / `unregisterHook()`.
- **[BUG-4] autoDetectAfkPlayers() checks world only, not zone bounds** — Added
`plugin.getAfkConfigManager().isInAnyZone(player.getLocation())` check before
creating an auto-AFK session. Previously any player in the correct world
(not just inside the zone) would start earning rewards.
- **[BUG-5] getRank() O(n log n) on every PAPI render** — Added `rankCache`
(`Map<UUID, Integer>`) with 60s TTL in `ShardManager`, mirroring the existing
leaderboard cache. `invalidateLeaderboardCache()` now resets both caches.
PlaceholderAPI `%donutshard_rank%` no longer sorts all players on every
tab-list refresh.
- **[BUG-6] ShardShopGUI calls saveAll() after every purchase** — Added
`Storage.savePlayer(UUID)` default method. YamlStorage overrides it to write
only `balances.yml` (vs. all 4 YAML files). SQLiteStorage keeps the default
no-op (already async). Reduces synchronous main-thread I/O on busy shops.
#### 🟢 Low
- **[BUG-7] AFK ticker does not invalidate leaderboard cache** — Added
`shardManager.invalidateLeaderboardCache()` at the end of `tickAllSessions()`
whenever shards were distributed. Leaderboard now reflects AFK rewards within
the same 60s cache window.
- **[BUG-8] autoDetect calls startSession() → unnecessary teleport** — Split
auto-detect path into `beginSessionInPlace()` which does NOT teleport.
`startSession()` (called by `/afk`) continues to teleport. Players standing
inside a zone are no longer sent to spawn on auto-detection.
- **[BUG-9] Duplicate `afk_session_restored` key in en.yml** — Removed the
stale copy at line 88 (the parser was silently using the version at line 105).
- **[BUG-10] sqlite-jdbc not relocated in maven-shade-plugin** — Added
`<relocation>` block in `pom.xml` to repackage `org.sqlite` →
`dev.duong2012g.libs.sqlite`. Prevents ClassLoader conflicts with other plugins
that also shade sqlite-jdbc (e.g. LuckPerms, AdvancedBan).
- **[BUG-11] `donutshard.zone.bypass` not declared in plugin.yml** — Added the
permission to `plugin.yml` with `default: op` and included it as a child of
`donutshard.admin`. LuckPerms / PermissionsEx now show it in autocomplete.
- **[BUG-12] 17 dead message getters in ConfigManager** — Removed all
`getMsgXxx()` fields and getters following the completed migration of
`DonutShardCommand` to `LanguageManager`. `ConfigManager` now only holds
numeric/boolean/format config values. The `prefix` field was also removed
(LanguageManager is now the single source-of-truth for all messages).
### Refactors
- **ConfigManager cleaned** — Down from ~230 lines to ~120 lines. Zero message
strings remain; all messages live in `languages/<code>.yml`.
- **AfkManager restructured** — Code reorganised into three clearly labelled
sections: ① Session lifecycle, ② Reward ticker, ③ Auto-detect. No external
API change.
- **ShardManager — dual cache** — `getTopBalances()` and `getRank()` now share
the same 60s TTL and are invalidated together via `invalidateLeaderboardCache()`.
- **AfkSettingCommand migrated** — All inline response messages now use
`lm().getPrefix()` consistently. The `reload` subcommand now reloads AFK
config, shop config, and language files in addition to the main config.
- **plugin.yml** — All permissions now fully declared including
`donutshard.zone.bypass` and `donutshard.language`. `donutshard.admin` grants
all child permissions including zone bypass.
Developer information
Projects on Modrinth are automatically available through a Maven repository for use with JVM build tools such as Gradle.
maven { name = "Modrinth"; url = "https://api.modrinth.com/maven" }