
🍩 DonutShard
A feature-rich custom currency plugin for PaperMC servers — earn Shards by killing, AFK farming, and spending them in a configurable shop.
✨ Features
| Feature | Description |
|---|---|
| 💰 Shard Currency | Custom economy currency with leaderboard, balance commands |
| ⚔️ Kill Rewards | Earn Shards for PvP kills with cooldown and bonus multiplier |
| 🌙 AFK System | Earn Shards passively while AFK in designated warp zones |
| 🛡️ Zone Protection | WorldGuard-like protection for AFK zones (no WorldGuard required) |
| 🏪 Shard Shop | Multi-category GUI shop — buy items with Shards |
| 🌐 Multi-language | 5 built-in languages: vi en zh es fr |
| 📊 Leaderboard | /shard top with PlaceholderAPI support |
| 🔌 Developer API | Hook into Shard events from other plugins |
| 💾 Dual Storage | YAML or SQLite, with WAL-mode async writes |
📦 Requirements
| Requirement | Version |
|---|---|
| PaperMC | 1.21.x |
| Java | 21+ |
| PlaceholderAPI | 2.11+ (optional) |
⚙️ Installation
- Download
DonutShard-1.0.0.jarand drop it into your/pluginsfolder. - Start the server — config files generate automatically.
- Edit
plugins/DonutShard/config.ymlto your liking. - Run
/shard reloadin-game to apply changes without restarting.
🗂️ File Structure
plugins/DonutShard/
├── config.yml # Main config (kill reward, storage, leaderboard)
├── afk.yml # AFK zones, protection rules, earn rates
├── shop.yml # Shop categories & GUI layout
├── shops/
│ ├── Spawners.yml # Spawner items
│ ├── Ores.yml # Ore blocks
│ └── Farm.yml # Farming items
├── languages/
│ ├── en.yml
│ ├── vi.yml
│ ├── zh.yml
│ ├── es.yml
│ └── fr.yml
└── data/ # YAML storage files (if YAML mode)
├── balances.yml
├── afk_sessions.yml
├── afk_warps.yml
└── daily_earnings.yml
🔧 Configuration
config.yml — Quick Reference
language: en # Active language (en/vi/zh/es/fr)
kill-reward:
enabled: true
shards-per-kill: 10
kill-cooldown: 30 # seconds before same kill gives reward again
ignore-afk-victims: false
bonus-multiplier:
enabled: false
threshold: 1000 # if victim has >= this many Shards
multiplier: 1.5
storage:
type: SQLITE # YAML or SQLITE
auto-save-interval: 5 # minutes
leaderboard:
size: 10
afk.yml — AFK System
enabled: true
shards-per-minute: 1
daily-limit: 100 # 0 = unlimited
teleport-cooldown: 5 # seconds between /afk uses
auto-detect-afk: true # auto-detect players already in AFK world
protection:
default:
no-damage: true
no-hunger: true
no-pvp: true
no-mob-spawn: true
no-item-drop: true
restore-health-on-enter: true
clear-effects-on-enter: true
Adding a Shop Category
- Create
plugins/DonutShard/shops/MyCategory.yml:
gui-title: "<gold>⭐ My Category"
items:
- id: DIAMOND
name: "<aqua>💎 Diamond"
price: 100
slot: 10
quantity: 1
lore:
- "<gray>Shiny and expensive"
- id: SPAWNER
name: "<red>🔥 Blaze Spawner"
spawnertype: BLAZE
price: 5000
slot: 11
quantity: 1
- Add the category to
shop.ymlundercategories:
categories:
MyCategory:
icon: DIAMOND
name: "<aqua>My Category"
slot: 4
lore:
- "<gray>Buy diamonds and spawners"
- Run
/shard reload. No restart needed!
💬 Commands
| Command | Description | Permission |
|---|---|---|
/shard balance [player] |
View Shard balance | donutshard.balance |
/shard give <player> <amount> |
Give Shards | donutshard.give |
/shard take <player> <amount> |
Take Shards | donutshard.take |
/shard set <player> <amount> |
Set Shards | donutshard.set |
/shard top [page] |
View leaderboard | donutshard.top |
/shard reload |
Reload config | donutshard.reload |
/shard set-afk-warp <name> |
Set AFK warp to a world | donutshard.admin |
/shard remove-afk-warp <name> |
Remove AFK warp | donutshard.admin |
/shard list-afk-warp |
List all AFK warps | donutshard.admin |
/shard language <code> |
Change language | donutshard.language |
/afk [warpname] |
Enter an AFK zone | donutshard.afk |
/afk-setting |
Manage AFK zone settings | donutshard.afk.admin |
/shardshop |
Open the Shard Shop | donutshard.shop |
Aliases: /shard → /donutshard, /ds
🛡️ Permissions
| Permission | Description | Default |
|---|---|---|
donutshard.balance |
Check own balance | true |
donutshard.balance.others |
Check other's balance | op |
donutshard.give |
Give Shards to others | op |
donutshard.take |
Take Shards from others | op |
donutshard.set |
Set Shard balance | op |
donutshard.top |
View leaderboard | true |
donutshard.reload |
Reload plugin | op |
donutshard.admin |
All admin commands | op |
donutshard.afk |
Use /afk command | true |
donutshard.afk.admin |
Manage AFK settings | op |
donutshard.shop |
Open Shard shop | true |
donutshard.kill.bypass |
This player's deaths give no Shards | false |
donutshard.zone.bypass |
Bypass zone protection | op |
donutshard.language |
Change language | true |
📊 PlaceholderAPI
Register these placeholders in any compatible plugin:
| Placeholder | Returns |
|---|---|
%donutshard_balance% |
Player's Shard balance |
%donutshard_rank% |
Player's leaderboard rank |
%donutshard_top_1% to %donutshard_top_10% |
Top player name at rank N |
%donutshard_top_balance_1% |
Top player balance at rank N |
🔌 Developer API
Add DonutShard as a dependency in your plugin:
// Check availability
if (DonutShardAPI.isAvailable()) {
// Get balance
long balance = DonutShardAPI.getBalance(player);
// Give Shards with a reason
DonutShardAPI.give(player, 100, "Quest reward");
// Take Shards (returns false if insufficient)
boolean success = DonutShardAPI.take(player, 50);
// Listen to Shard events
DonutShardAPI.registerHook(new ShardEventHook() {
@Override
public void onShardEarned(UUID uuid, long amount, String reason) {
// ...
}
@Override
public void onShardSpent(UUID uuid, long amount, String reason, long newBalance) {
// ...
}
});
}
🔨 Building from Source
git clone https://github.com/duong2012g/DonutShard.git
cd DonutShard
mvn clean package
# Output: target/DonutShard-1.0.0.jar
Requirements: Java 21, Maven 3.8+
📝 License
MIT — free to use, modify, and redistribute with attribution.
Made with ❤️ by duong2012g
Ченджлог
1.1.0Релиз1.21.9, 1.21.10, 1.21.11 · 9 июня 2026 г.
[1.1.0] — Bug Fix & Refactor Release
Bug Fixes
🔴 Critical
[BUG-1] DonutShardProvider.take() race condition — Replaced two-step
getBalance() → take()withshardManager.safeTake()(atomic CAS viaConcurrentHashMap.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 callsfireSpentHooks()internally. Removed the redundantplugin.getApiProvider().fireSpent()call inShopHook.charge(). ExternalShardEventHookimplementations no longer receive 2 events for 1 purchase.
🟡 Medium
[BUG-3] DonutShardProvider.hooks not thread-safe — Changed
ArrayList<ShardEventHook>toCopyOnWriteArrayList. PreventsConcurrentModificationExceptionwhen hooks are fired from the async SQLite writer thread while another thread callsregisterHook()/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 inShardManager, 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 onlybalances.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 oftickAllSessions()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_restoredkey 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 inpom.xmlto repackageorg.sqlite→dev.duong2012g.libs.sqlite. Prevents ClassLoader conflicts with other plugins that also shade sqlite-jdbc (e.g. LuckPerms, AdvancedBan).[BUG-11]
donutshard.zone.bypassnot declared in plugin.yml — Added the permission toplugin.ymlwithdefault: opand included it as a child ofdonutshard.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 ofDonutShardCommandtoLanguageManager.ConfigManagernow only holds numeric/boolean/format config values. Theprefixfield 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()andgetRank()now share the same 60s TTL and are invalidated together viainvalidateLeaderboardCache().AfkSettingCommand migrated — All inline response messages now use
lm().getPrefix()consistently. Thereloadsubcommand 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.bypassanddonutshard.language.donutshard.admingrants all child permissions including zone bypass.
Комментарии
Загружаем…