Compare commits

...
Author SHA1 Message Date
NotAlexNoyle 860d86e424 Point submodules at canonical Gitea repositories 2026-11-01 12:52:14 -05:00
NotAlexNoyle 1a87bcd008 Update canonical Gitea links 2026-10-31 14:17:37 -04:00
NotAlexNoyle ac16d028eb Update canonical Gitea links 2026-10-31 14:17:37 -04:00
NotAlexNoyle 184618a0ea Update canonical Gitea links 2026-10-31 11:40:04 -04:00
NotAlexNoyle 9e00df0d5c Update canonical Gitea links 2026-10-31 11:40:04 -04:00
NotAlexNoyle ca04362ea1 Exclude CODEOWNERS from language statistics 2026-10-31 09:54:57 -04:00
NotAlexNoyle 26d5fa9598 Add CODEOWNERS (one staff approval) 2026-10-31 09:54:56 -04:00
NotAlexNoyle 18f055d93c Update Utilities-OG API 2026-09-14 02:04:14 -04:00
NotAlexNoyle 070f63f202 Update DiamondBank-OG API 2026-07-29 11:44:51 -04:00
NotAlexNoyle 9e7ff6d170 Update DiamondBank-OG API 2026-07-27 08:02:12 -04:00
NotAlexNoyle f5aa21d785 Update DiamondBank-OG API 2026-07-22 07:10:23 -04:00
NotAlexNoyle cbb31352cb Update Utilities-OG API 2026-07-20 00:53:41 -04:00
NotAlexNoyle 36fad60bb9 Update GxUI-OG 2026-07-17 12:51:15 -04:00
NotAlexNoyle 625bcf50f8 Remove Template-OG artifact 2026-06-21 21:05:50 -04:00
NotAlexNoyle 031bc8347d Add world whitelist 2026-06-16 07:24:16 -04:00
NotAlexNoyle ead1832422 World whitelist for bounty claim messages 2026-06-15 10:20:24 -04:00
NotAlexNoyle f233cc3d01 Update GxUI-OG 2026-05-21 11:38:58 -04:00
NotAlexNoyle a0c6e351db Update Utilities-OG API 2026-05-20 11:23:50 -04:00
NotAlexNoyle 71739a7e47 Fix spacing 2026-05-18 05:17:22 -04:00
NotAlexNoyle c98d605f44 Update bounty head display 2026-05-11 09:07:04 -04:00
NotAlexNoyle 7f2048f407 Update Utilities-OG 2026-05-11 00:54:16 -04:00
NotAlexNoyle 1c96e9ed01 Divide hologram over player head into two lines 2026-05-07 12:18:32 -04:00
NotAlexNoyle e248f5cfa0 Fix Unions-OG compatibility 2026-04-23 12:17:12 -04:00
NotAlexNoyle d36ff4723c Support formatting heads with offline players 2026-04-22 16:49:43 -04:00
NotAlexNoyle d98aee4d64 v1.2 update 2026-04-22 11:38:58 -04:00
NotAlexNoyle 67d9de29fb Unify head display strings 2026-04-22 06:49:54 -04:00
NotAlexNoyle e687076830 1.1 update 2026-04-21 17:23:04 -04:00
NotAlexNoyle 38493f29a7 Add more bounty head actions 2026-04-21 17:09:42 -04:00
NotAlexNoyle fcd0154503 Update visual style of head labels 2026-04-21 12:51:12 -04:00
NotAlexNoyle 1ca9aaed36 Consistently format heads, add hover display 2026-04-21 12:27:31 -04:00
NotAlexNoyle 2008150b11 Update GxUI-OG and Utilities-OG API 2026-04-20 02:17:11 -04:00
NotAlexNoyle 0c31f3a99d Update DiamondBank-OG 2026-04-07 09:38:06 -04:00
NotAlexNoyle 4f096ca141 Update DiamondBank-OG API 2026-04-06 00:20:45 -04:00
NotAlexNoyle c0c1b0e849 Update for new GxUI-OG API 2026-04-05 14:22:50 -04:00
NotAlexNoyle cae1830b4d Update DiamondBank-OG API 2026-04-05 13:41:06 -04:00
NotAlexNoyle d699c3db87 Update TrueOG Network APIs 2026-04-05 13:14:22 -04:00
NotAlexNoyle d7d0002f87 Update DiamondBank-OG API 2026-04-05 11:13:43 -04:00
NotAlexNoyle 3c75c7e93f Update DiamondBank-OG API 2026-04-04 14:11:07 -04:00
NotAlexNoyle d07f0b9e0c Update DiamondBank-OG API 2026-04-01 15:30:07 -04:00
NotAlexNoyle e917f9bbd5 Update DiamondBank-OG API 2026-04-01 05:57:01 -04:00
NotAlexNoyle 405204148e Update DiamondBank-OG API 2026-04-01 05:03:54 -04:00
NotAlexNoyle 27bdae86bf Update DiamondBank-OG API 2026-03-30 08:38:45 -04:00
NotAlexNoyle c6fc703255 Update Utilities-OG API 2026-03-30 05:09:28 -04:00
NotAlexNoyle b880fa09fe Depend on CoreProtect-OG 2026-03-30 04:07:50 -04:00
NotAlexNoyle 488653651e Use CoreProtect-OG v10 API 2026-03-30 04:04:52 -04:00
NotAlexNoyle 82b7e43af2 Update Utilities-OG 2026-03-25 11:57:27 -04:00
NotAlexNoyle f6b1b382d1 Update DiamondBank-OG 2026-03-11 00:27:43 -04:00
NotAlexNoyle 04ff28acab Update DiamondBank-OG 2026-03-11 00:23:48 -04:00
NotAlexNoyle d197ad1047 Use Utilities-OG to strip formatting, remove deprecated event usage 2026-03-11 00:18:02 -04:00
NotAlexNoyle b157619568 Use live TrueOG Network config file 2026-03-11 00:05:33 -04:00
NotAlexNoyle aebcad3f03 Merge pull request #15 from true-og/codex/update-minecraft-plugin-permissions
Rename permission nodes from playerbountiesplus to playerbountiesog
2026-03-09 11:48:07 -04:00
NotAlexNoyle d6a08aaa90 Rename permission nodes to playerbountiesog 2026-03-09 11:47:52 -04:00
NotAlexNoyle 311c4d5ae6 Formatting 2026-03-09 11:13:51 -04:00
NotAlexNoyle 22d55cd3ec Merge pull request #14 from true-og/codex/update-bounty-head-display-and-functionality
Add bounty increase workflow and refine bounty head messaging
2026-03-09 10:39:40 -04:00
NotAlexNoyle 1f185b7c2b Add bounty increase flow and update head messaging 2026-03-09 10:36:26 -04:00
NotAlexNoyle 62fe86f28b Specify CoreProtect-OG explicitly 2026-03-09 10:20:21 -04:00
NotAlexNoyle c6a88e9bc8 Merge pull request #13 from true-og/codex/fix-coreprotect-api-sql-syntax-error
Fix CoreProtect skull logging to avoid unsupported `skin` inserts
2026-03-09 10:19:22 -04:00
51 changed files with 1589 additions and 578 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
+3 -3
View File
@@ -1,9 +1,9 @@
[submodule "libs/DiamondBank-OG"]
path = libs/DiamondBank-OG
url = https://github.com/true-og/DiamondBank-OG
url = https://git.trueog.net/trueog/DiamondBank-OG
[submodule "libs/Utilities-OG"]
path = libs/Utilities-OG
url = https://github.com/true-og/Utilities-OG
url = https://git.trueog.net/trueog/Utilities-OG
[submodule "libs/GxUI-OG"]
path = libs/GxUI-OG
url = https://github.com/true-og/GxUI-OG/
url = https://git.trueog.net/trueog/GxUI-OG
+18
View File
@@ -0,0 +1,18 @@
# Changelog
## 1.2
- Unified bounty head formatting across item names, armor stand labels, and action bar messaging into a single canonical line showing victim, slayer, and diamond reward.
- Removed redundant bounty head lore now that the item display name carries full claim information.
- `/bounty check` now resolves offline players who still have active bounties, and tab completion suggests their names.
- Bounty check output now formats the amount through the DiamondBank shards-to-diamonds formatter for consistency with head labels and announcements.
- Bounty leaderboard GUI now reads the beheading chance from the `bounty-head-drop-chance` config value instead of hardcoding 50%.
- Added the missing `playerbountiesog.command.admin.bounty.get` permission entry and Italian translation listing; removed dead `death.announce-claimed` lang key.
- Replaced raw stack traces in the bounty data manager with logger output and pruned dead plugin-discovery code from plugin startup.
## 1.1
- Added bounty amounts to bounty head item titles so the selected hotbar item text shows the reward without using the action bar.
- Updated placed bounty head hover labels to render on separate lines for clearer in-world head information.
- Added right-click action bar messaging for bounty heads that shows who claimed the bounty for both hotbar items and placed heads.
- Stored claimant metadata on bounty heads so claim information survives item drops and place/break cycles.
+20 -19
View File
@@ -1,22 +1,23 @@
# PlayerBounties-OG
## Overview
PlayerBounties-OG is a hard fork of [PlayerBountiesPlus](https://github.com/TechnicallyCoded/PlayerBountiesPlus) that lets players set and claim bounties while blocking teammates from claiming rewards. Hooks into [DiamondBank-OG](https://github.com/true-og/DiamondBank-OG) for economy handling and supports [SimpleClans](https://github.com/true-og/SimpleClans). Built for Purpur 1.19.4.
PlayerBounties-OG is a hard fork of [PlayerBountiesPlus](https://github.com/TechnicallyCoded/PlayerBountiesPlus) that lets players set and claim bounties while blocking teammates from claiming rewards. Hooks into [DiamondBank-OG](https://git.trueog.net/trueog/DiamondBank-OG) for economy handling and supports [SimpleClans](https://git.trueog.net/trueog/Unions-OG) and API-compatible forks such as Unions-OG. Built for Purpur 1.19.4.
## Features
- Clan-aware bounty system that prevents teammates from claiming each other's bounties.
- Inventory GUI for viewing bounties.
- Customizable odds for receiving player head from bounty victim.
- Localized messages in multiple languages.
- Integrates [CoreProtect-OG](https://github.com/true-og/CoreProtect-OG) for player head rollback injection.
- Integrates [DiamondBank-OG](https://github.com/true-og/DiamondBank-OG) for currency.
- Integrates [LuckPerms](https://github.com/true-og/LuckPerms) for player name and rank formatting.
- Integrates [SimpleClans](https://github.com/true-og/SimpleClans) for team detection.
- Integrates [CoreProtect-OG](https://git.trueog.net/trueog/CoreProtect-OG) for player head rollback injection.
- Integrates [DiamondBank-OG](https://git.trueog.net/trueog/DiamondBank-OG) for currency.
- Integrates [LuckPerms](https://git.trueog.net/trueog/LuckPerms) for player name and rank formatting.
- Integrates [SimpleClans](https://git.trueog.net/trueog/Unions-OG) and compatible forks such as Unions-OG for team detection.
## Commands
- `/bounty set <player> <amount>` – set a bounty on a player
- `/bounty add <player> <amount>` – add diamonds to an existing bounty
- `/bounty top` – list the top bounties
- `/bounty check <player>` – check a player's bounty
- `/bounty check <player>` – check a player's bounty (online or offline)
- `/pbp reload` – reload configuration and messages
- `/pbp version` – display plugin version
- `/pbp bounty set <player> <amount>` – admin set a bounty
@@ -28,22 +29,22 @@ PlayerBounties-OG is a hard fork of [PlayerBountiesPlus](https://github.com/Tech
## Permissions
| Permission | Default |
| --- | --- |
| `playerbountiesplus.command.bounty` | all |
| `playerbountiesplus.command.bounty.set` | all |
| `playerbountiesplus.event.claim` | all |
| `playerbountiesplus.command.admin` | op |
| `playerbountiesplus.command.admin.version` | op |
| `playerbountiesplus.command.admin.reload` | op |
| `playerbountiesplus.command.admin.bounty.set` | op |
| `playerbountiesplus.command.admin.bounty.add` | op |
| `playerbountiesplus.command.admin.bounty.remove` | op |
| `playerbountiesplus.command.admin.bounty.delete` | op |
| `playerbountiesplus.command.admin.bounty.get` | op |
| `playerbountiesog.command.bounty` | all |
| `playerbountiesog.command.bounty.set` | all |
| `playerbountiesog.command.bounty.add` | all |
| `playerbountiesog.event.claim` | all |
| `playerbountiesog.command.admin` | op |
| `playerbountiesog.command.admin.version` | op |
| `playerbountiesog.command.admin.reload` | op |
| `playerbountiesog.command.admin.bounty.set` | op |
| `playerbountiesog.command.admin.bounty.add` | op |
| `playerbountiesog.command.admin.bounty.remove` | op |
| `playerbountiesog.command.admin.bounty.delete` | op |
| `playerbountiesog.command.admin.bounty.get` | op |
## Developer Notes
- Commands reside in `com.tcoded.playerbountiesplus.command` with subpackages for bounty and admin actions.
- Event listeners like `DeathListener` handle bounty claim logic.
- Hooks under `com.tcoded.playerbountiesplus.hook` integrate with DiamondBank-OG and SimpleClans.
- Hooks under `com.tcoded.playerbountiesplus.hook` integrate with DiamondBank-OG and supported clan/team plugins such as SimpleClans and Unions-OG.
- Inventory GUIs in `com.tcoded.playerbountiesplus.gui` present bounty data.
- `BountyDataManager` manages persistence of bounties.
+1 -3
View File
@@ -1,5 +1,3 @@
/* This is free and unencumbered software released into the public domain */
/* ------------------------------ Plugins ------------------------------ */
plugins {
id("java") // Import Java plugin.
@@ -29,7 +27,7 @@ kotlin { jvmToolchain(17) }
/* ----------------------------- Metadata ------------------------------ */
group = "com.tcoded" // Declare bundle identifier.
version = "1.0" // Declare plugin version (will be in .jar).
version = "1.1" // Declare plugin version (will be in .jar).
val apiVersion = "1.19" // Declare minecraft server target version.
@@ -1,28 +1,28 @@
package com.tcoded.playerbountiesplus;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.ServicesManager;
import org.bukkit.plugin.java.JavaPlugin;
import com.google.common.collect.ImmutableList;
import com.tcoded.playerbountiesplus.command.BountyCommand;
import com.tcoded.playerbountiesplus.command.PlayerBountiesPlusAdminCmd;
import com.tcoded.playerbountiesplus.hook.currency.DiamondBankOGHook;
import com.tcoded.playerbountiesplus.hook.logging.CoreProtectHook;
import com.tcoded.playerbountiesplus.hook.currency.EconomyHook;
import com.tcoded.playerbountiesplus.hook.team.TeamHook;
import com.tcoded.playerbountiesplus.listener.BountyHeadLabelListener;
import com.tcoded.playerbountiesplus.listener.BountyHeadListener;
import com.tcoded.playerbountiesplus.listener.BountyIncreasePromptListener;
import com.tcoded.playerbountiesplus.listener.DeathListener;
import com.tcoded.playerbountiesplus.listener.GuiProtectionListener;
import com.tcoded.playerbountiesplus.manager.BountyDataManager;
import com.tcoded.playerbountiesplus.util.BountyHeadFormatter;
import com.tcoded.playerbountiesplus.util.BountyHeadMetadata;
import com.tcoded.playerbountiesplus.util.LangUtil;
import net.luckperms.api.LuckPerms;
@@ -48,6 +48,8 @@ public final class PlayerBountiesOG extends JavaPlugin {
// Utilities.
private LangUtil langUtil;
private BountyHeadFormatter bountyHeadFormatter;
private BountyHeadMetadata bountyHeadMetadata;
// Managers.
private BountyDataManager bountyDataManager;
@@ -65,6 +67,9 @@ public final class PlayerBountiesOG extends JavaPlugin {
// CoreProtect API hook.
private CoreProtectHook coreProtectHook;
private BountyIncreasePromptListener bountyIncreasePromptListener;
private BountyHeadLabelListener bountyHeadLabelListener;
public PlayerBountiesOG() {
instance = this;
@@ -96,7 +101,7 @@ public final class PlayerBountiesOG extends JavaPlugin {
if (!this.coreProtectHook.isEnabled()) {
getLogger().warning(
"CoreProtect API v11+ not found - custom bounty head metadata may not rollback correctly.");
"CoreProtect API v10+ not found - custom bounty head metadata may not rollback correctly.");
}
@@ -118,6 +123,8 @@ public final class PlayerBountiesOG extends JavaPlugin {
// Utils.
this.reloadLang();
this.bountyHeadFormatter = new BountyHeadFormatter(this.diamondBankAPI, this.luckPerms);
this.bountyHeadMetadata = new BountyHeadMetadata(this);
// Managers.
this.bountyDataManager = new BountyDataManager(this);
@@ -155,21 +162,14 @@ public final class PlayerBountiesOG extends JavaPlugin {
}
// Listeners.
this.getServer().getPluginManager().registerEvents(new DeathListener(this, this.diamondBankAPI, this.luckPerms),
this);
this.getServer().getPluginManager().registerEvents(
new BountyHeadListener(this, this.diamondBankAPI, this.luckPerms, this.coreProtectHook), this);
this.getServer().getPluginManager().registerEvents(new GuiProtectionListener(this), this);
this.getServer().getPluginManager().registerEvents(new DeathListener(this), this);
this.bountyIncreasePromptListener = new BountyIncreasePromptListener(this);
final List<Plugin> plugins = ImmutableList.copyOf(this.getServer().getPluginManager().getPlugins());
findPluginWithQuery(plugins, "team");
findPluginWithQuery(plugins, "teams");
findPluginWithQuery(plugins, "clan");
findPluginWithQuery(plugins, "clans");
findPluginWithQuery(plugins, "party", "voteparty");
findPluginWithQuery(plugins, "parties");
findPluginWithQuery(plugins, "guild");
findPluginWithQuery(plugins, "guilds");
this.getServer().getPluginManager().registerEvents(new BountyHeadListener(this), this);
this.bountyHeadLabelListener = new BountyHeadLabelListener(this);
this.getServer().getPluginManager().registerEvents(this.bountyHeadLabelListener, this);
this.getServer().getPluginManager().registerEvents(this.bountyIncreasePromptListener, this);
this.getServer().getPluginManager().registerEvents(new GuiProtectionListener(this), this);
}
@@ -182,6 +182,12 @@ public final class PlayerBountiesOG extends JavaPlugin {
@Override
public void onDisable() {
if (this.bountyHeadLabelListener != null) {
this.bountyHeadLabelListener.shutdown();
}
HandlerList.unregisterAll(this);
UtilitiesOG.logToConsole(getPrefix(), "PlayerBounties-OG has been shut down successfully!");
@@ -230,45 +236,21 @@ public final class PlayerBountiesOG extends JavaPlugin {
}
// Utilities.
private static void findPluginWithQuery(List<Plugin> plugins, String pluginNameQuery, String... excludeStrings) {
public BountyHeadFormatter getBountyHeadFormatter() {
final String firstPluginFound = plugins.stream().filter(p -> {
return this.bountyHeadFormatter;
final String lowerName = StringUtils.lowerCase(p.getName());
// If the plugin name doesn't contain the query, skip.
if (!StringUtils.contains(lowerName, pluginNameQuery)) {
}
return false;
public BountyHeadMetadata getBountyHeadMetadata() {
}
return this.bountyHeadMetadata;
// If the plugin name contains any of the exclude strings, skip.
for (String excludeString : excludeStrings) {
}
if (StringUtils.contains(lowerName, excludeString)) {
public BountyIncreasePromptListener getBountyIncreasePromptListener() {
return false;
}
}
return true;
}).findFirst().map(p -> {
final List<String> authors = p.getPluginMeta().getAuthors();
return p.getName() + " (" + (authors.isEmpty() ? "N/A" : authors.get(0)) + ")";
}).orElse(null);
if (firstPluginFound == null) {
return;
}
return this.bountyIncreasePromptListener;
}
@@ -318,4 +300,4 @@ public final class PlayerBountiesOG extends JavaPlugin {
}
}
}
@@ -16,6 +16,7 @@ import org.jetbrains.annotations.Nullable;
import com.google.common.collect.Lists;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.command.bounty.BountyCheckCmd;
import com.tcoded.playerbountiesplus.command.bounty.BountyAddCmd;
import com.tcoded.playerbountiesplus.command.bounty.BountySetCmd;
import com.tcoded.playerbountiesplus.command.bounty.BountyTopCmd;
@@ -69,6 +70,12 @@ public class BountyCommand implements CommandExecutor, TabCompleter {
}
case "add" -> {
return BountyAddCmd.handleCmd(plugin, sender, cmd, cmdName, args);
}
case "top" -> {
return BountyTopCmd.handleCmd(plugin, diamondBankAPI, sender, cmd, cmdName, args);
@@ -99,7 +106,7 @@ public class BountyCommand implements CommandExecutor, TabCompleter {
if (args.length == 1) {
return Lists.newArrayList("set", "top", "check").stream()
return Lists.newArrayList("set", "add", "top", "check").stream()
.filter(action -> StringUtils.startsWith(action, StringUtils.lowerCase(args[0])))
.collect(Collectors.toList());
@@ -112,6 +119,10 @@ public class BountyCommand implements CommandExecutor, TabCompleter {
.onTabComplete(sender, args).stream().filter(suggestion -> StringUtils
.startsWith(StringUtils.lowerCase(suggestion), StringUtils.lowerCase(args[1])))
.collect(Collectors.toList());
case "add" -> BountyAddCmd
.onTabComplete(sender, args).stream().filter(suggestion -> StringUtils
.startsWith(StringUtils.lowerCase(suggestion), StringUtils.lowerCase(args[1])))
.collect(Collectors.toList());
case "check" -> BountyCheckCmd
.onTabComplete(sender, args).stream().filter(suggestion -> StringUtils
.startsWith(StringUtils.lowerCase(suggestion), StringUtils.lowerCase(args[1])))
@@ -127,4 +138,4 @@ public class BountyCommand implements CommandExecutor, TabCompleter {
}
}
}
@@ -16,7 +16,7 @@ import net.trueog.utilitiesog.UtilitiesOG;
public class PlayerBountiesPlusReloadCmd implements TabCompleter {
private static final String RELOAD_PERMISSION = "playerbountiesplus.command.playerbountiesplus.reload";
private static final String RELOAD_PERMISSION = "playerbountiesog.command.admin.reload";
public static boolean handleCmd(PlayerBountiesOG plugin, CommandSender sender, Command cmd, String cmdName,
String[] args)
@@ -14,7 +14,7 @@ import net.trueog.utilitiesog.UtilitiesOG;
public class PlayerBountiesPlusVersionCmd {
private static final String VERSION_PERMISSION = "playerbountiesplus.command.playerbountiesplus.version";
private static final String VERSION_PERMISSION = "playerbountiesog.command.admin.version";
public static boolean handleCmd(PlayerBountiesOG plugin, CommandSender sender, Command cmd, String cmdName,
String[] args)
@@ -19,7 +19,7 @@ import net.trueog.utilitiesog.UtilitiesOG;
public class AdminBountyAddCmd {
private static final String PERMISSION = "playerbountiesplus.command.admin.bounty.add";
private static final String PERMISSION = "playerbountiesog.command.admin.bounty.add";
public static boolean handleCmd(PlayerBountiesOG plugin, CommandSender sender, Command cmd, String label,
String[] args)
@@ -19,7 +19,7 @@ import net.trueog.utilitiesog.UtilitiesOG;
public class AdminBountyDeleteCmd {
private static final String PERMISSION = "playerbountiesplus.command.admin.bounty.delete";
private static final String PERMISSION = "playerbountiesog.command.admin.bounty.delete";
public static boolean handleCmd(PlayerBountiesOG plugin, CommandSender sender, Command cmd, String label,
String[] args)
@@ -19,7 +19,7 @@ import net.trueog.utilitiesog.UtilitiesOG;
public class AdminBountyGetCmd {
private static final String PERMISSION = "playerbountiesplus.command.admin.bounty.get";
private static final String PERMISSION = "playerbountiesog.command.admin.bounty.get";
public static boolean handleCmd(PlayerBountiesOG plugin, CommandSender sender, Command cmd, String label,
String[] args)
@@ -19,7 +19,7 @@ import net.trueog.utilitiesog.UtilitiesOG;
public class AdminBountyRemoveCmd {
private static final String PERMISSION = "playerbountiesplus.command.admin.bounty.remove";
private static final String PERMISSION = "playerbountiesog.command.admin.bounty.remove";
public static boolean handleCmd(PlayerBountiesOG plugin, CommandSender sender, Command cmd, String label,
String[] args)
@@ -19,7 +19,7 @@ import net.trueog.utilitiesog.UtilitiesOG;
public class AdminBountySetCmd {
private static final String PERMISSION = "playerbountiesplus.command.admin.bounty.set";
private static final String PERMISSION = "playerbountiesog.command.admin.bounty.set";
public static boolean handleCmd(PlayerBountiesOG plugin, CommandSender sender, Command cmd, String label,
String[] args)
@@ -0,0 +1,365 @@
package com.tcoded.playerbountiesplus.command.bounty;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.manager.BountyDataManager;
import com.tcoded.playerbountiesplus.util.BountyWorldUtil;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import net.trueog.diamondbankog.DiamondBankException.EconomyDisabledException;
import net.trueog.diamondbankog.DiamondBankException.InsufficientFundsException;
import net.trueog.diamondbankog.DiamondBankException.InvalidPlayerException;
import net.trueog.diamondbankog.DiamondBankException.PlayerNotOnlineException;
import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.utilitiesog.UtilitiesOG;
public class BountyAddCmd {
public static boolean handleCmd(PlayerBountiesOG plugin, CommandSender sender, Command cmd, String cmdName,
String[] args)
{
if (!(sender instanceof Player player)) {
UtilitiesOG.logToConsole(PlayerBountiesOG.getPrefix(), "&cERROR: Only players can use this command.");
return true;
}
if (!sender.hasPermission("playerbountiesog.command.bounty.add")) {
final String noPerm = plugin.getLang().getColored("command.no-permission");
final String noPermDetailed = plugin.getLang().getColored("command.no-permission-detailed")
.replace("{no-permission-msg}", noPerm)
.replace("{permission}", "playerbountiesog.command.bounty.add");
UtilitiesOG.trueogMessage(player, noPermDetailed);
return true;
}
if (!BountyWorldUtil.isBountyWorldAllowed(plugin, player)) {
UtilitiesOG.trueogMessage(player, plugin.getLang().getColored("command.bounty.world-not-whitelisted")
.replace("{worlds}", BountyWorldUtil.formatBountyWorldWhitelist(plugin)));
return true;
}
if (args.length < 3) {
UtilitiesOG.trueogMessage(player, plugin.getLang().getColored("command.bounty.add.missing-args"));
return true;
}
final String targetNameArg = args[1];
final double amount;
try {
amount = parseAmount(args[2]);
} catch (NumberFormatException numberFormatException) {
UtilitiesOG.trueogMessage(player, plugin.getLang().getColored("command.bounty.add.amount-nan"));
return true;
}
final OfflinePlayer target = Bukkit.getOfflinePlayer(targetNameArg);
if (target.getUniqueId() != null) {
return addBounty(plugin, player, target.getUniqueId(), target.getName(), amount, true);
}
UtilitiesOG.trueogMessage(player, plugin.getLang().getColored("command.bounty.add.player-not-found"));
return true;
}
public static boolean addBounty(PlayerBountiesOG plugin, Player setter, UUID targetUuid, String targetName,
double parsedAmount, boolean saveData)
{
if (!BountyWorldUtil.isBountyWorldAllowed(plugin, setter)) {
UtilitiesOG.trueogMessage(setter, plugin.getLang().getColored("command.bounty.world-not-whitelisted")
.replace("{worlds}", BountyWorldUtil.formatBountyWorldWhitelist(plugin)));
return true;
}
final String safeTargetName = StringUtils.defaultIfBlank(targetName,
Bukkit.getOfflinePlayer(targetUuid).getName());
if (safeTargetName == null || safeTargetName.isBlank()) {
UtilitiesOG.trueogMessage(setter, plugin.getLang().getColored("command.bounty.add.player-not-found"));
return true;
}
final DiamondBankAPIJava diamondBankAPI = resolveDiamondBankApi(plugin);
final double minimum = plugin.getConfig().getDouble("bounty-minimum", 1.0);
if (parsedAmount < minimum) {
UtilitiesOG.trueogMessage(setter, plugin.getLang().getColored("command.bounty.set.under-minimum")
.replace("{minimum}", String.valueOf(minimum)));
return true;
}
final double maximum = plugin.getConfig().getDouble("bounty-maximum", 1000000.0);
if (parsedAmount > maximum) {
UtilitiesOG.trueogMessage(setter, plugin.getLang().getColored("command.bounty.set.over-maximum")
.replace("{maximum}", String.valueOf(maximum)));
return true;
}
final long addedShards = diamondBankAPI.diamondsToShards((float) parsedAmount);
if (addedShards <= 0L) {
UtilitiesOG.trueogMessage(setter, plugin.getLang().getColored("command.bounty.set.internal-invalid-value"));
return true;
}
if (!chargeSetter(plugin, setter, diamondBankAPI, safeTargetName, addedShards)) {
return true;
}
final BountyDataManager bountyDataManager = plugin.getBountyDataManager();
final long existingShards = diamondBankAPI.diamondsToShards((float) bountyDataManager.getBounty(targetUuid));
final long totalShards = existingShards + addedShards;
final double totalBounty = Double.parseDouble(diamondBankAPI.shardsToDiamonds(totalShards));
bountyDataManager.setBounty(targetUuid, totalBounty);
final LuckPerms luckPerms = resolveLuckPerms(plugin);
final String setterDisplay = formatLuckPermsDisplay(luckPerms, setter);
final String targetDisplay = resolveLuckPermsDisplayName(luckPerms, targetUuid, safeTargetName);
final String addedDisplay = diamondBankAPI.shardsToDiamonds(addedShards);
final String totalDisplay = diamondBankAPI.shardsToDiamonds(totalShards);
UtilitiesOG.trueogMessage(setter, plugin.getLang().getColored("command.bounty.add.success")
.replace("{amount}", addedDisplay).replace("{target}", targetDisplay).replace("{total}", totalDisplay));
if (plugin.getConfig().getBoolean("bounty-placed-announce", true)) {
final String announce = plugin.getLang().getColored("command.bounty.add.announce")
.replace("{amount}", addedDisplay).replace("{target}", targetDisplay)
.replace("{total}", totalDisplay).replace("{player}", setterDisplay);
Bukkit.getOnlinePlayers().forEach(
player -> UtilitiesOG.trueogMessage(player, PlayerBountiesOG.getPrefix() + "&a" + announce));
}
if (saveData) {
bountyDataManager.saveBountiesAsync();
}
return true;
}
private static boolean chargeSetter(PlayerBountiesOG plugin, Player setter, DiamondBankAPIJava diamondBankAPI,
String targetName, long addedShards)
{
try {
diamondBankAPI.consumeFromPlayer(setter.getUniqueId(), addedShards,
"Player " + setter.getName() + " contributed " + diamondBankAPI.shardsToDiamonds(addedShards)
+ " Diamonds toward a bounty on " + targetName + ".",
"Plugin: PlayerBounties-OG");
return true;
} catch (EconomyDisabledException economyDisabledException) {
PlayerBountiesOG.disableSelf(
"[PlayerBounties-OG] DiamondBank-OG economy disabled: " + economyDisabledException.getMessage());
return false;
} catch (InvalidPlayerException invalidPlayerException) {
plugin.getLogger().warning("Invalid player for consumeFromPlayer: " + setter.getName());
UtilitiesOG.trueogMessage(setter,
"&cERROR: your player account could not be found by DiamondBank-OG. Contact an administrator!");
return false;
} catch (InsufficientFundsException insufficientFundsException) {
UtilitiesOG.trueogMessage(setter, plugin.getLang().getColored("command.bounty.set.not-enough-money"));
return false;
}
// TODO: Remove PlayerNotOnlineException catch block after next DiamondBank-OG
// update.
catch (PlayerNotOnlineException temporaryPlayerNotOnlineError) {
plugin.getLogger().warning("Player " + setter.getName()
+ " not online when attempting to consumeFromPlayer. This error is expected to be removed in the next DiamondBank-OG update.");
temporaryPlayerNotOnlineError.printStackTrace();
return false;
}
}
private static double parseAmount(String amountInput) {
if (!amountInput.matches("^\\d+(?:\\.\\d)?$")) {
throw new NumberFormatException("Invalid amount format");
}
return Double.parseDouble(amountInput);
}
private static DiamondBankAPIJava resolveDiamondBankApi(PlayerBountiesOG plugin) {
final RegisteredServiceProvider<DiamondBankAPIJava> provider = plugin.getServer().getServicesManager()
.getRegistration(DiamondBankAPIJava.class);
if (provider == null) {
throw new IllegalStateException("DiamondBank-OG API is null");
}
return provider.getProvider();
}
private static LuckPerms resolveLuckPerms(PlayerBountiesOG plugin) {
final RegisteredServiceProvider<LuckPerms> provider = plugin.getServer().getServicesManager()
.getRegistration(LuckPerms.class);
return provider == null ? null : provider.getProvider();
}
private static String resolveLuckPermsDisplayName(LuckPerms luckPerms, UUID playerId, String playerName) {
final String safeName = StringUtils.defaultIfBlank(playerName, "Unknown Player");
if (luckPerms == null) {
return "&f" + safeName;
}
final User user = luckPerms.getUserManager().getUser(playerId);
if (user == null) {
return "&f" + safeName;
}
final CachedMetaData meta = user.getCachedData().getMetaData();
final String prefix = StringUtils.trim(StringUtils.defaultString(meta.getPrefix()).replace('§', '&'));
final String suffix = StringUtils.trim(StringUtils.defaultString(meta.getSuffix()).replace('§', '&'));
final boolean metaContainsName = containsVisibleName(prefix, safeName) || containsVisibleName(suffix, safeName);
final StringBuilder out = new StringBuilder();
if (!prefix.isBlank()) {
out.append(prefix).append(' ');
}
if (!metaContainsName) {
out.append(safeName);
}
if (!suffix.isBlank()) {
out.append(' ').append(suffix);
}
return sanitizeDisplay(out.toString(), safeName);
}
private static String formatLuckPermsDisplay(LuckPerms luckPerms, Player player) {
return resolveLuckPermsDisplayName(luckPerms, player.getUniqueId(), player.getName());
}
private static String sanitizeDisplay(String formatted, String fallbackName) {
final String cleaned = StringUtils.trimToEmpty(formatted).replaceAll("(?i)(?:\\s*(?:<reset>|[&§]r))+$", "")
.replaceAll("\\s*>$", "");
return cleaned.isBlank() ? fallbackName : cleaned;
}
private static boolean containsVisibleName(String text, String playerName) {
if (text == null || playerName == null || playerName.isBlank()) {
return false;
}
final String normalizedText = StringUtils.lowerCase(UtilitiesOG.stripFormatting(text), Locale.ROOT);
final String normalizedName = StringUtils.lowerCase(playerName, Locale.ROOT);
return StringUtils.contains(normalizedText, normalizedName);
}
@Nullable
public static List<String> onTabComplete(@NotNull CommandSender sender, @NotNull String[] args) {
if (args.length == 2) {
return sender.getServer().getOnlinePlayers().stream().map(Player::getName)
.filter(name -> StringUtils.startsWith(StringUtils.lowerCase(name), StringUtils.lowerCase(args[1])))
.collect(Collectors.toList());
} else if (args.length == 3) {
return Collections.singletonList("<amount>");
}
return Collections.emptyList();
}
}
@@ -4,8 +4,11 @@ import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -43,36 +46,50 @@ public class BountyCheckCmd {
final String playerNameArg = args[1];
final Player target = plugin.getServer().getPlayerExact(playerNameArg);
final UUID targetUuid;
final String targetName;
if (target == null) {
final Player onlineTarget = plugin.getServer().getPlayerExact(playerNameArg);
if (onlineTarget != null) {
final String playerNotFoundMessage = plugin.getLang().getColored("command.bounty.check.player-not-found");
if (!(sender instanceof Player)) {
targetUuid = onlineTarget.getUniqueId();
targetName = onlineTarget.getName();
UtilitiesOG.logToConsole(PlayerBountiesOG.getPrefix(), playerNotFoundMessage);
} else {
} else {
final OfflinePlayer offlineTarget = resolveOfflineTarget(playerNameArg);
if (offlineTarget == null) {
UtilitiesOG.trueogMessage((Player) sender, playerNotFoundMessage);
final String playerNotFoundMessage = plugin.getLang()
.getColored("command.bounty.check.player-not-found");
if (!(sender instanceof Player)) {
UtilitiesOG.logToConsole(PlayerBountiesOG.getPrefix(), playerNotFoundMessage);
} else {
UtilitiesOG.trueogMessage((Player) sender, playerNotFoundMessage);
}
return true;
}
return true;
targetUuid = offlineTarget.getUniqueId();
targetName = StringUtils.defaultIfBlank(offlineTarget.getName(), playerNameArg);
}
final UUID playerUUID = target.getUniqueId();
final BountyDataManager bountyDataManager = plugin.getBountyDataManager();
final boolean hasBounty = bountyDataManager.hasBounty(playerUUID);
final boolean hasBounty = bountyDataManager.hasBounty(targetUuid);
if (hasBounty) {
// Confirmation.
final double bounty = bountyDataManager.getBounty(playerUUID);
final double bounty = bountyDataManager.getBounty(targetUuid);
final String formattedBounty = plugin.getBountyHeadFormatter().formatDiamonds(bounty) + " Diamonds";
final String bountyFoundMessage = plugin.getLang().getColored("command.bounty.check.bounty-found")
.replace("{target}", target.getName()).replace("{bounty}", Double.toString(bounty));
.replace("{target}", targetName).replace("{bounty}", formattedBounty);
if (!(sender instanceof Player)) {
UtilitiesOG.logToConsole(PlayerBountiesOG.getPrefix(), bountyFoundMessage);
@@ -86,7 +103,7 @@ public class BountyCheckCmd {
} else {
final String noBountyFoundMessage = plugin.getLang().getColored("command.bounty.check.no-bounty")
.replace("{target}", target.getName());
.replace("{target}", targetName);
if (!(sender instanceof Player)) {
UtilitiesOG.logToConsole(PlayerBountiesOG.getPrefix(), noBountyFoundMessage);
@@ -103,19 +120,43 @@ public class BountyCheckCmd {
}
@Nullable
public static List<String> onTabComplete(@NotNull CommandSender sender, @NotNull String[] args) {
private static OfflinePlayer resolveOfflineTarget(String name) {
if (args.length == 2) {
for (OfflinePlayer candidate : Bukkit.getOfflinePlayers()) {
// Suggest online player names for the username.
return sender.getServer().getOnlinePlayers().stream().map(Player::getName)
.filter(name -> StringUtils.startsWith(StringUtils.lowerCase(name), StringUtils.lowerCase(args[1])))
.collect(Collectors.toList());
if (StringUtils.equalsIgnoreCase(candidate.getName(), name)) {
return candidate;
}
}
return Collections.emptyList();
return Bukkit.getOfflinePlayerIfCached(name);
}
@Nullable
public static List<String> onTabComplete(@NotNull CommandSender sender, @NotNull String[] args) {
if (args.length != 2) {
return Collections.emptyList();
}
final String prefix = StringUtils.lowerCase(args[1]);
final PlayerBountiesOG plugin = PlayerBountiesOG.getInstance();
final BountyDataManager bountyDataManager = plugin == null ? null : plugin.getBountyDataManager();
final Stream<String> onlineNames = sender.getServer().getOnlinePlayers().stream().map(Player::getName);
final Stream<String> offlineBountyNames = bountyDataManager == null ? Stream.empty()
: bountyDataManager.getBounties().keySet().stream().map(Bukkit::getOfflinePlayer)
.map(OfflinePlayer::getName).filter(name -> name != null && !name.isBlank());
return Stream.concat(onlineNames, offlineBountyNames).distinct()
.filter(name -> StringUtils.startsWith(StringUtils.lowerCase(name), prefix))
.collect(Collectors.toList());
}
@@ -17,6 +17,7 @@ import org.jetbrains.annotations.Nullable;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.event.BountySetEvent;
import com.tcoded.playerbountiesplus.manager.BountyDataManager;
import com.tcoded.playerbountiesplus.util.BountyWorldUtil;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.cacheddata.CachedMetaData;
@@ -47,12 +48,12 @@ public class BountySetCmd {
}
if (sender instanceof Player && !sender.hasPermission("playerbountiesplus.command.bounty.set")) {
if (sender instanceof Player && !sender.hasPermission("playerbountiesog.command.bounty.set")) {
final String noPerm = plugin.getLang().getColored("command.no-permission");
final String noPermDetailed = plugin.getLang().getColored("command.no-permission-detailed")
.replace("{no-permission-msg}", noPerm)
.replace("{permission}", "playerbountiesplus.command.bounty.set");
.replace("{permission}", "playerbountiesog.command.bounty.set");
if (!(sender instanceof Player)) {
UtilitiesOG.logToConsole(PlayerBountiesOG.getPrefix(), noPermDetailed);
@@ -67,6 +68,14 @@ public class BountySetCmd {
}
if (sender instanceof Player player && !BountyWorldUtil.isBountyWorldAllowed(plugin, player)) {
UtilitiesOG.trueogMessage(player, plugin.getLang().getColored("command.bounty.world-not-whitelisted")
.replace("{worlds}", BountyWorldUtil.formatBountyWorldWhitelist(plugin)));
return true;
}
final String playerNameArg = args[1];
final double parsedAmount;
try {
@@ -297,13 +306,7 @@ public class BountySetCmd {
final RegisteredServiceProvider<LuckPerms> provider = plugin.getServer().getServicesManager()
.getRegistration(LuckPerms.class);
if (provider == null) {
return null;
}
return provider.getProvider();
return provider == null ? null : provider.getProvider();
}
@@ -314,17 +317,17 @@ public class BountySetCmd {
if (prefix.isBlank()) {
return player.getName();
return player.getName().replaceAll("\\s*>$", "");
}
if (leadingColorCodes.isBlank()) {
return prefix + " " + player.getName();
return (prefix + " " + player.getName()).replaceAll("\\s*>$", "");
}
return prefix + " " + leadingColorCodes + player.getName();
return (prefix + " " + leadingColorCodes + player.getName()).replaceAll("\\s*>$", "");
}
@@ -428,4 +431,4 @@ public class BountySetCmd {
}
}
}
@@ -3,7 +3,6 @@ package com.tcoded.playerbountiesplus.command.bounty;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
@@ -15,10 +14,8 @@ import org.bukkit.entity.Player;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.gui.MainBountyGui;
import com.tcoded.playerbountiesplus.util.BountyHeadFormatter;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.utilitiesog.UtilitiesOG;
@@ -45,6 +42,8 @@ public class BountyTopCmd {
private static List<MainBountyGui.BountyGuiEntry> buildGuiEntries(PlayerBountiesOG plugin) {
final BountyHeadFormatter formatter = plugin.getBountyHeadFormatter();
final Set<Map.Entry<UUID, Double>> bountiesSet = plugin.getBountyDataManager().getBounties().entrySet();
final List<Map.Entry<UUID, Double>> bounties = new ArrayList<>(bountiesSet);
@@ -68,7 +67,7 @@ public class BountyTopCmd {
}
final String displayName = resolveLuckPermsDisplayName(plugin.getLuckPerms(), entry.getKey(), targetName);
final String displayName = formatter.resolvePlayerDisplay(entry.getKey(), targetName);
entries.add(new MainBountyGui.BountyGuiEntry(entry.getKey(), targetName, displayName, entry.getValue()));
@@ -78,96 +77,12 @@ public class BountyTopCmd {
}
private static String resolveLuckPermsDisplayName(LuckPerms luckPerms, UUID playerId, String playerName) {
final String safeName = StringUtils.defaultIfBlank(playerName, "Unknown Player");
if (luckPerms == null) {
return "&f" + safeName;
}
final User user = luckPerms.getUserManager().getUser(playerId);
if (user == null) {
return "&f" + safeName;
}
final CachedMetaData meta = user.getCachedData().getMetaData();
final String prefix = StringUtils.trim(StringUtils.defaultString(meta.getPrefix()).replace('§', '&'));
final String suffix = StringUtils.trim(StringUtils.defaultString(meta.getSuffix()).replace('§', '&'));
final boolean metaContainsName = containsVisibleName(prefix, safeName) || containsVisibleName(suffix, safeName);
final StringBuilder out = new StringBuilder();
if (!prefix.isBlank()) {
out.append(prefix).append(' ');
}
if (!metaContainsName) {
out.append(safeName);
}
if (!suffix.isBlank()) {
out.append(' ').append(suffix);
}
return sanitizeDisplay(out.toString(), safeName);
}
private static String sanitizeDisplay(String formatted, String fallbackName) {
final String cleaned = StringUtils.trimToEmpty(formatted).replaceAll("(?i)(?:\\s*(?:<reset>|[&§]r))+$", "")
.replaceAll("\\s+>", " >").replaceAll("\\s*>$", "");
if (cleaned.isBlank()) {
return fallbackName;
}
return cleaned;
}
private static boolean containsVisibleName(String text, String playerName) {
if (text == null || playerName == null || playerName.isBlank()) {
return false;
}
final String normalizedText = stripFormatting(text).toLowerCase(Locale.ROOT);
final String normalizedName = playerName.toLowerCase(Locale.ROOT);
return normalizedText.contains(normalizedName);
}
private static String stripFormatting(String input) {
if (input == null || input.isBlank()) {
return "";
}
return input.replaceAll("(?i)[&§][0-9A-FK-ORX]", "").replaceAll("<[^>]+>", "").trim();
}
private static void sendTextTopList(PlayerBountiesOG plugin, DiamondBankAPIJava diamondBankAPI,
CommandSender sender)
{
final BountyHeadFormatter formatter = plugin.getBountyHeadFormatter();
final Set<Map.Entry<UUID, Double>> bountiesSet = plugin.getBountyDataManager().getBounties().entrySet();
final List<Map.Entry<UUID, Double>> bounties = new ArrayList<>(bountiesSet);
@@ -196,8 +111,7 @@ public class BountyTopCmd {
.shardsToDiamonds(diamondBankAPI.diamondsToShards(entry.getValue()));
strb.append("&7 - ");
final String displayName = resolveLuckPermsDisplayName(plugin.getLuckPerms(), entry.getKey(),
targetName);
final String displayName = formatter.resolvePlayerDisplay(entry.getKey(), targetName);
strb.append(displayName);
strb.append(": &b");
@@ -228,4 +142,4 @@ public class BountyTopCmd {
}
}
}
@@ -7,6 +7,7 @@ import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Material;
import org.bukkit.entity.Player;
@@ -16,6 +17,8 @@ import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.gxui.GUIBase;
import net.trueog.gxui.GUIButton;
import net.trueog.gxui.GUIItem;
import net.trueog.utilitiesog.UtilitiesOG;
import net.kyori.adventure.text.TextComponent;
public class MainBountyGui extends GUIBase {
@@ -74,8 +77,9 @@ public class MainBountyGui extends GUIBase {
addItem(22,
createStaticItem(Material.WRITABLE_BOOK, "&6&lNo Active Bounties",
List.of("&7Nobody currently has a bounty.", "&7Place one with your bounty command to get",
"&7this leaderboard populated again.")));
List.of(UtilitiesOG.trueogColorize("&7Nobody currently has a bounty."),
UtilitiesOG.trueogColorize("&7Place one with your bounty command to get"),
UtilitiesOG.trueogColorize("&7this leaderboard populated again."))));
return;
}
@@ -102,7 +106,7 @@ public class MainBountyGui extends GUIBase {
if (currentPage > 0) {
addItem(PREVIOUS_PAGE_SLOT, createButtonItem(Material.ARROW, "&a&lPrevious Page",
List.of("&7Go to page &f" + currentPage), new GUIButton()
List.of(UtilitiesOG.trueogColorize("&7Go to page &f" + currentPage)), new GUIButton()
{
@Override
@@ -137,18 +141,18 @@ public class MainBountyGui extends GUIBase {
} else {
addItem(PREVIOUS_PAGE_SLOT,
createStaticItem(Material.GRAY_DYE, "&7Previous Page", List.of("&8Already on the first page.")));
addItem(PREVIOUS_PAGE_SLOT, createStaticItem(Material.GRAY_DYE, "&7Previous Page",
List.of(UtilitiesOG.trueogColorize("&8Already on the first page."))));
}
addItem(PAGE_INFO_SLOT, createStaticItem(Material.BOOK, "&3&lPage " + (currentPage + 1) + "/" + (maxPage + 1),
List.of("&6Active bounties: &c" + totalBounties)));
List.of(UtilitiesOG.trueogColorize("&6Active bounties: &c" + totalBounties))));
if (currentPage < maxPage) {
addItem(NEXT_PAGE_SLOT, createButtonItem(Material.ARROW, "&a&lNext Page",
List.of("&7Go to page &f" + (currentPage + 2)), new GUIButton()
List.of(UtilitiesOG.trueogColorize("&7Go to page &f" + (currentPage + 2))), new GUIButton()
{
@Override
@@ -183,13 +187,13 @@ public class MainBountyGui extends GUIBase {
} else {
addItem(NEXT_PAGE_SLOT,
createStaticItem(Material.GRAY_DYE, "&7Next Page", List.of("&8Already on the last page.")));
addItem(NEXT_PAGE_SLOT, createStaticItem(Material.GRAY_DYE, "&7Next Page",
List.of(UtilitiesOG.trueogColorize("&8Already on the last page."))));
}
addItem(CLOSE_SLOT,
createButtonItem(Material.BARRIER, "&c&lClose", List.of("&7Close the bounty menu."), new GUIButton()
addItem(CLOSE_SLOT, createButtonItem(Material.BARRIER, "&c&lClose",
List.of(UtilitiesOG.trueogColorize("&7Close the bounty menu.")), new GUIButton()
{
@Override
@@ -243,11 +247,16 @@ public class MainBountyGui extends GUIBase {
private GUIItem createBountyItem(BountyGuiEntry entry, int rank) {
final String displayName = safeDisplayName(entry.displayName());
final ArrayList<String> lore = new ArrayList<>();
final ArrayList<TextComponent> lore = new ArrayList<>();
lore.add("&eBounty: &b" + formatBounty(entry.bountyDiamonds()));
lore.add("");
lore.add("&6Claiming the bounty will give you a 50% chance of beheading your victim!");
lore.add(UtilitiesOG.trueogColorize("&eBounty: &b" + formatBounty(entry.bountyDiamonds())));
lore.add(UtilitiesOG.trueogColorize("&7Click to increase this player's bounty."));
lore.add(UtilitiesOG.trueogColorize("&8You will be prompted in chat for the amount."));
lore.add(UtilitiesOG.trueogColorize(""));
final double dropChance = Math.max(0D,
Math.min(100D, plugin.getConfig().getDouble("bounty-head-drop-chance", 50D)));
lore.add(UtilitiesOG.trueogColorize("&6Claiming the bounty will give you a " + formatDropChance(dropChance)
+ "% chance of beheading your victim!"));
final GUIItem item;
if (entry.targetName() != null && !entry.targetName().isBlank()) {
@@ -261,12 +270,65 @@ public class MainBountyGui extends GUIBase {
}
item.lore(lore);
item.setButton(new GUIButton() {
@Override
public boolean leftClick() {
return promptIncrease(entry);
}
@Override
public boolean leftClickShift() {
return promptIncrease(entry);
}
@Override
public boolean rightClick() {
return promptIncrease(entry);
}
@Override
public boolean rightClickShift() {
return promptIncrease(entry);
}
});
item.setPlayErrorSound(false);
return item;
}
private boolean promptIncrease(BountyGuiEntry entry) {
if (entry.targetUuid() == null) {
UtilitiesOG.trueogMessage(viewer, plugin.getLang().getColored("command.bounty.add.player-not-found"));
return true;
}
final String targetName = StringUtils.defaultIfBlank(entry.targetName(), "Unknown Player");
plugin.getBountyIncreasePromptListener().prompt(viewer.getUniqueId(), entry.targetUuid(), targetName);
final String promptMessage = plugin.getLang().getColored("command.bounty.add.prompt").replace("{target}",
safeDisplayName(entry.displayName()));
UtilitiesOG.trueogMessage(viewer, promptMessage);
viewer.closeInventory();
return true;
}
private String formatBounty(double bountyDiamonds) {
final long shards = diamondBankAPI.diamondsToShards(bountyDiamonds);
@@ -275,7 +337,19 @@ public class MainBountyGui extends GUIBase {
}
private GUIItem createStaticItem(Material material, String displayName, List<String> lore) {
private String formatDropChance(double chance) {
if (chance == Math.floor(chance)) {
return Long.toString((long) chance);
}
return StringUtils.stripEnd(StringUtils.stripEnd(String.format("%.2f", chance), "0"), ".");
}
private GUIItem createStaticItem(Material material, String displayName, List<TextComponent> lore) {
final GUIItem item = new GUIItem(material, 1, displayName);
item.lore(new ArrayList<>(lore));
@@ -285,7 +359,9 @@ public class MainBountyGui extends GUIBase {
}
private GUIItem createButtonItem(Material material, String displayName, List<String> lore, GUIButton button) {
private GUIItem createButtonItem(Material material, String displayName, List<TextComponent> lore,
GUIButton button)
{
final GUIItem item = new GUIItem(material, 1, displayName);
item.lore(new ArrayList<>(lore));
@@ -328,13 +404,7 @@ public class MainBountyGui extends GUIBase {
private String safeName(String name) {
if (name == null || name.isBlank()) {
return "Unknown Player";
}
return name;
return name == null || name.isBlank() ? "Unknown Player" : name;
}
@@ -353,4 +423,4 @@ public class MainBountyGui extends GUIBase {
public static record BountyGuiEntry(UUID targetUuid, String targetName, String displayName, double bountyDiamonds) {
}
}
}
@@ -10,6 +10,7 @@ import net.coreprotect.CoreProtectAPI;
public class CoreProtectHook {
private static final int MINIMUM_API_VERSION = 10;
private final CoreProtectAPI coreProtectAPI;
public CoreProtectHook(Plugin plugin) {
@@ -94,7 +95,7 @@ public class CoreProtectHook {
}
final Plugin coreProtectPlugin = plugin.getServer().getPluginManager().getPlugin("CoreProtect");
final Plugin coreProtectPlugin = plugin.getServer().getPluginManager().getPlugin("CoreProtect-OG");
if (!(coreProtectPlugin instanceof CoreProtect coreProtect)) {
return null;
@@ -102,7 +103,7 @@ public class CoreProtectHook {
}
final CoreProtectAPI api = coreProtect.getAPI();
if (api == null || !api.isEnabled() || api.APIVersion() < 11) {
if (api == null || !api.isEnabled() || api.APIVersion() < MINIMUM_API_VERSION) {
return null;
@@ -112,4 +113,4 @@ public class CoreProtectHook {
}
}
}
@@ -30,13 +30,8 @@ public abstract class AbstractTeamHook implements TeamHook {
public String getAuthor() {
final List<String> authors = this.teamPlugin.getPluginMeta().getAuthors();
if (authors.isEmpty()) {
return "N/A";
}
return authors.get(0);
return authors.isEmpty() ? "N/A" : authors.get(0);
}
@@ -35,13 +35,7 @@ public class SimpleClansHook extends AbstractTeamHook {
}
if (clan2 == null) {
return false;
}
return clan1 == clan2;
return clan2 == null ? false : clan1 == clan2;
}
@@ -13,9 +13,9 @@ public interface TeamHook {
final PluginManager pluginManager = plugin.getServer().getPluginManager();
// RoinujNosde's SimpleClans: TrueOG Network branch.
// https://github.com/true-og/SimpleClans
// https://git.trueog.net/trueog/Unions-OG
// License: GPLv3
// (https://raw.githubusercontent.com/true-og/SimpleClans/refs/heads/master/LICENSE)
// (https://git.trueog.net/trueog/Unions-OG/raw/branch/master/LICENSE)
final Plugin simpleClansPlugin = pluginManager.getPlugin("SimpleClans");
if (simpleClansPlugin != null && simpleClansPlugin.isEnabled()) {
@@ -23,6 +23,15 @@ public interface TeamHook {
}
// Unions-OG is a soft fork of SimpleClans that preserves the same API
// and main class but exposes a different plugin name to Bukkit.
final Plugin unionsPlugin = pluginManager.getPlugin("Unions-OG");
if (unionsPlugin != null && unionsPlugin.isEnabled()) {
return new SimpleClansHook(plugin, unionsPlugin);
}
return null;
}
@@ -35,4 +44,4 @@ public interface TeamHook {
boolean isFriendly(Player player1, Player player2);
}
}
@@ -0,0 +1,239 @@
package com.tcoded.playerbountiesplus.listener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.Skull;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.RayTraceResult;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.util.BountyHeadData;
public final class BountyHeadLabelListener implements Listener {
private static final long UPDATE_PERIOD_TICKS = 2L;
// Two stacked armor stands per head: top = name + slayer, bottom = amount.
// Spacing tuned for small marker stands so the hologram stays compact.
private static final double LABEL_BOTTOM_Y_OFFSET = 0.85D;
private static final double LABEL_TOP_Y_OFFSET = 1.10D;
private final PlayerBountiesOG plugin;
private final Map<HeadLabelKey, LabelPair> activeLabels = new HashMap<>();
private final BukkitTask tickTask;
public BountyHeadLabelListener(PlayerBountiesOG plugin) {
this.plugin = plugin;
this.tickTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::tick, UPDATE_PERIOD_TICKS,
UPDATE_PERIOD_TICKS);
}
public void shutdown() {
if (tickTask != null) {
tickTask.cancel();
}
clearLabels();
}
private void tick() {
if (!isHoverModeEnabled()) {
clearLabels();
return;
}
final Set<HeadLabelKey> visibleTargets = new HashSet<>();
for (Player player : plugin.getServer().getOnlinePlayers()) {
final HeadTarget target = findTargetedHead(player);
if (target == null) {
continue;
}
visibleTargets.add(target.key());
upsertLabel(target);
}
final Set<HeadLabelKey> staleLabels = new HashSet<>(activeLabels.keySet());
staleLabels.removeAll(visibleTargets);
staleLabels.forEach(this::removeLabel);
}
private HeadTarget findTargetedHead(Player player) {
final double maxRange = Math.max(1D, plugin.getConfig().getDouble("bounty-head-label-range", 8D));
final RayTraceResult rayTrace = player.getWorld().rayTraceBlocks(player.getEyeLocation(),
player.getEyeLocation().getDirection(), maxRange, FluidCollisionMode.NEVER, true);
if (rayTrace == null) {
return null;
}
final Block hitBlock = rayTrace.getHitBlock();
if (hitBlock == null || !isHeadMaterial(hitBlock.getType())) {
return null;
}
final BlockState state = hitBlock.getState();
if (!(state instanceof Skull skull)) {
return null;
}
final BountyHeadData headData = plugin.getBountyHeadMetadata().read(skull.getPersistentDataContainer());
if (headData == null) {
return null;
}
return new HeadTarget(HeadLabelKey.fromBlock(hitBlock), hitBlock.getLocation(), headData);
}
private void upsertLabel(HeadTarget target) {
final LabelPair existing = activeLabels.get(target.key());
if (existing != null && existing.top().isValid() && existing.bottom().isValid()) {
existing.top().customName(plugin.getBountyHeadFormatter().buildCanonicalLineTop(target.data()));
existing.bottom().customName(plugin.getBountyHeadFormatter().buildCanonicalLineBottom(target.data()));
return;
}
if (existing != null) {
removeLabel(target.key());
}
final World world = target.location().getWorld();
final Location bottomLocation = target.location().clone().add(0.5D, LABEL_BOTTOM_Y_OFFSET, 0.5D);
final Location topLocation = target.location().clone().add(0.5D, LABEL_TOP_Y_OFFSET, 0.5D);
final ArmorStand top = world.spawn(topLocation, ArmorStand.class, stand -> {
configureLabelStand(stand);
stand.customName(plugin.getBountyHeadFormatter().buildCanonicalLineTop(target.data()));
});
final ArmorStand bottom = world.spawn(bottomLocation, ArmorStand.class, stand -> {
configureLabelStand(stand);
stand.customName(plugin.getBountyHeadFormatter().buildCanonicalLineBottom(target.data()));
});
activeLabels.put(target.key(), new LabelPair(top, bottom));
}
private static void configureLabelStand(ArmorStand stand) {
stand.setInvisible(true);
stand.setGravity(false);
stand.setMarker(true);
stand.setSmall(true);
stand.setInvulnerable(true);
stand.setSilent(true);
stand.setPersistent(false);
stand.setBasePlate(false);
stand.setArms(false);
stand.setCustomNameVisible(true);
}
private void removeLabel(HeadLabelKey key) {
final LabelPair pair = activeLabels.remove(key);
if (pair == null) {
return;
}
if (pair.top() != null && pair.top().isValid()) {
pair.top().remove();
}
if (pair.bottom() != null && pair.bottom().isValid()) {
pair.bottom().remove();
}
}
private void clearLabels() {
final Set<HeadLabelKey> keys = new HashSet<>(activeLabels.keySet());
keys.forEach(this::removeLabel);
}
private boolean isHoverModeEnabled() {
return StringUtils.equalsIgnoreCase(plugin.getConfig().getString("bounty-head-label-mode", "hover"), "hover");
}
private static boolean isHeadMaterial(Material material) {
return material == Material.PLAYER_HEAD || material == Material.PLAYER_WALL_HEAD;
}
private record HeadTarget(HeadLabelKey key, Location location, BountyHeadData data) {
}
private record LabelPair(ArmorStand top, ArmorStand bottom) {
}
private record HeadLabelKey(UUID worldId, int x, int y, int z) {
private static HeadLabelKey fromBlock(Block block) {
final World world = block.getWorld();
return new HeadLabelKey(world.getUID(), block.getX(), block.getY(), block.getZ());
}
}
}
@@ -1,13 +1,6 @@
package com.tcoded.playerbountiesplus.listener;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.block.BlockState;
import org.bukkit.block.Skull;
import org.bukkit.entity.Player;
@@ -22,37 +15,20 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.hook.logging.CoreProtectHook;
import net.kyori.adventure.text.TextComponent;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.utilitiesog.UtilitiesOG;
import com.tcoded.playerbountiesplus.util.BountyHeadData;
public class BountyHeadListener implements Listener {
private final DiamondBankAPIJava diamondBankAPI;
private final NamespacedKey targetKey;
private final NamespacedKey targetUuidKey;
private final NamespacedKey amountKey;
private final LuckPerms luckPerms;
private final PlayerBountiesOG plugin;
private final CoreProtectHook coreProtectHook;
public BountyHeadListener(PlayerBountiesOG plugin, DiamondBankAPIJava diamondBankAPI, LuckPerms luckPerms,
CoreProtectHook coreProtectHook)
{
public BountyHeadListener(PlayerBountiesOG plugin) {
this.diamondBankAPI = diamondBankAPI;
this.targetKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_NAME_KEY);
this.targetUuidKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_UUID_KEY);
this.amountKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_AMOUNT_KEY);
this.luckPerms = luckPerms;
this.coreProtectHook = coreProtectHook;
this.plugin = plugin;
this.coreProtectHook = plugin.getCoreProtectHook();
}
@@ -72,11 +48,8 @@ public class BountyHeadListener implements Listener {
}
final String targetName = itemMeta.getPersistentDataContainer().get(targetKey, PersistentDataType.STRING);
final String targetUuid = itemMeta.getPersistentDataContainer().get(targetUuidKey, PersistentDataType.STRING);
final Double bountyAmount = itemMeta.getPersistentDataContainer().get(amountKey, PersistentDataType.DOUBLE);
if (targetName == null || targetUuid == null || bountyAmount == null) {
final BountyHeadData headData = plugin.getBountyHeadMetadata().read(itemMeta.getPersistentDataContainer());
if (headData == null) {
return;
@@ -89,10 +62,7 @@ public class BountyHeadListener implements Listener {
}
final PersistentDataContainer blockData = skull.getPersistentDataContainer();
blockData.set(targetKey, PersistentDataType.STRING, targetName);
blockData.set(targetUuidKey, PersistentDataType.STRING, targetUuid);
blockData.set(amountKey, PersistentDataType.DOUBLE, bountyAmount);
plugin.getBountyHeadMetadata().write(skull.getPersistentDataContainer(), headData);
skull.update(true, false);
if (this.coreProtectHook != null) {
@@ -120,11 +90,8 @@ public class BountyHeadListener implements Listener {
}
final String targetName = skull.getPersistentDataContainer().get(targetKey, PersistentDataType.STRING);
final String targetUuid = skull.getPersistentDataContainer().get(targetUuidKey, PersistentDataType.STRING);
final Double bountyAmount = skull.getPersistentDataContainer().get(amountKey, PersistentDataType.DOUBLE);
if (targetName == null || targetUuid == null || bountyAmount == null) {
final BountyHeadData headData = plugin.getBountyHeadMetadata().read(skull.getPersistentDataContainer());
if (headData == null) {
return;
@@ -133,10 +100,8 @@ public class BountyHeadListener implements Listener {
final ItemStack drop = new ItemStack(Material.PLAYER_HEAD, 1);
final SkullMeta meta = (SkullMeta) drop.getItemMeta();
meta.setPlayerProfile(skull.getPlayerProfile());
applyBountyData(meta, targetName, targetUuid, bountyAmount);
plugin.getBountyHeadFormatter().applyHeadMeta(meta, headData, skull.getPlayerProfile(),
plugin.getBountyHeadMetadata());
drop.setItemMeta(meta);
if (this.coreProtectHook != null) {
@@ -160,31 +125,30 @@ public class BountyHeadListener implements Listener {
}
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && event.getClickedBlock() != null
&& isHeadMaterial(event.getClickedBlock().getType()))
{
final BlockState state = event.getClickedBlock().getState();
if (state instanceof Skull skull) {
sendClaimantActionbar(event.getPlayer(), skull.getPersistentDataContainer());
return;
}
}
final ItemStack item = event.getItem();
if (item != null && isHeadMaterial(item.getType())) {
sendBountyHeadActionbar(event.getPlayer(), item.getItemMeta());
sendClaimantActionbar(event.getPlayer(), item.getItemMeta());
}
if (event.getClickedBlock() == null || !isHeadMaterial(event.getClickedBlock().getType())) {
return;
}
final BlockState state = event.getClickedBlock().getState();
if (!(state instanceof Skull skull)) {
return;
}
sendBountyHeadActionbar(event.getPlayer(), skull.getPersistentDataContainer());
}
private void sendBountyHeadActionbar(Player player, ItemMeta itemMeta) {
private void sendClaimantActionbar(Player player, ItemMeta itemMeta) {
if (itemMeta == null) {
@@ -192,166 +156,37 @@ public class BountyHeadListener implements Listener {
}
sendBountyHeadActionbar(player, itemMeta.getPersistentDataContainer());
sendClaimantActionbar(player, itemMeta.getPersistentDataContainer());
}
private void sendBountyHeadActionbar(Player player, PersistentDataContainer dataContainer) {
private void sendClaimantActionbar(Player player, PersistentDataContainer dataContainer) {
final String targetName = dataContainer.get(targetKey, PersistentDataType.STRING);
final String targetUuid = dataContainer.get(targetUuidKey, PersistentDataType.STRING);
final Double bountyAmount = dataContainer.get(amountKey, PersistentDataType.DOUBLE);
if (targetName == null || targetUuid == null || bountyAmount == null) {
final BountyHeadData headData = plugin.getBountyHeadMetadata().read(dataContainer);
if (headData == null) {
return;
}
final UUID uuid = parseUuid(targetUuid);
final String playerDisplay = formatLuckPermsDisplay(uuid, targetName);
final String diamonds = formatDiamonds(bountyAmount);
player.sendActionBar(
UtilitiesOG.trueogColorize("&cBounty Head: " + playerDisplay + " &7- &b" + diamonds + " &bDiamonds"));
final net.kyori.adventure.text.Component line = plugin.getBountyHeadFormatter().buildCanonicalLine(headData);
}
// Send action bar immediately
player.sendActionBar(line);
private void applyBountyData(SkullMeta meta, String targetName, String targetUuid, double bountyAmount) {
// Also schedule a second send to effectively double the visible duration.
// 40 ticks ~= 2 seconds; sending again after that will keep the action bar
// visible
// for roughly twice the default single-send duration.
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
meta.displayName(UtilitiesOG.trueogColorize("&c" + targetName + "'s Bounty Head"));
if (player.isOnline()) {
final UUID uuid = parseUuid(targetUuid);
final String playerDisplay = formatLuckPermsDisplay(uuid, targetName);
player.sendActionBar(line);
final List<TextComponent> lore = new ArrayList<>();
lore.add(UtilitiesOG.trueogColorize("&6Player: " + playerDisplay));
lore.add(UtilitiesOG.trueogColorize("&cBeheaded for: &b" + formatDiamonds(bountyAmount) + " &bDiamonds"));
meta.lore(lore);
}
meta.getPersistentDataContainer().set(targetKey, PersistentDataType.STRING, targetName);
meta.getPersistentDataContainer().set(targetUuidKey, PersistentDataType.STRING, targetUuid);
meta.getPersistentDataContainer().set(amountKey, PersistentDataType.DOUBLE, bountyAmount);
}
private UUID parseUuid(String uuidString) {
if (uuidString == null || uuidString.isBlank()) {
return null;
}
try {
return UUID.fromString(uuidString);
} catch (IllegalArgumentException ignored) {
return null;
}
}
private String formatLuckPermsDisplay(UUID targetUuid, String fallbackName) {
final String playerName = StringUtils.defaultIfBlank(fallbackName, "Unknown Player");
if (targetUuid == null || luckPerms == null) {
return "&f" + playerName;
}
final User user = luckPerms.getUserManager().getUser(targetUuid);
if (user == null) {
return "&f" + playerName;
}
final CachedMetaData meta = user.getCachedData().getMetaData();
final String prefix = StringUtils.trim(StringUtils.defaultString(meta.getPrefix()).replace('§', '&'));
final String suffix = StringUtils.trim(StringUtils.defaultString(meta.getSuffix()).replace('§', '&'));
final boolean metaContainsName = containsVisibleName(prefix, playerName)
|| containsVisibleName(suffix, playerName);
final StringBuilder out = new StringBuilder();
if (!prefix.isBlank()) {
out.append(prefix).append(' ');
}
if (!metaContainsName) {
out.append(playerName);
}
if (!suffix.isBlank()) {
out.append(' ').append(suffix);
}
return sanitizeDisplay(out.toString(), playerName);
}
private static String sanitizeDisplay(String formatted, String fallbackName) {
final String cleaned = StringUtils.trimToEmpty(formatted).replaceAll("(?i)(?:\\s*(?:<reset>|[&§]r))+$", "")
.replaceAll("\\s*>$", "");
if (cleaned.isBlank()) {
return fallbackName;
}
return cleaned;
}
private static boolean containsVisibleName(String text, String playerName) {
if (text == null || playerName == null || playerName.isBlank()) {
return false;
}
final String normalizedText = stripFormatting(text).toLowerCase(Locale.ROOT);
final String normalizedName = playerName.toLowerCase(Locale.ROOT);
return normalizedText.contains(normalizedName);
}
private static String stripFormatting(String input) {
if (input == null || input.isBlank()) {
return "";
}
return input.replaceAll("(?i)[&§][0-9A-FK-ORX]", "").replaceAll("<[^>]+>", "").trim();
}
private String formatDiamonds(double diamonds) {
try {
return diamondBankAPI.shardsToDiamonds(diamondBankAPI.diamondsToShards(diamonds));
} catch (RuntimeException runtimeException) {
return String.valueOf(diamonds);
}
}, 40L);
}
@@ -361,4 +196,4 @@ public class BountyHeadListener implements Listener {
}
}
}
@@ -0,0 +1,100 @@
package com.tcoded.playerbountiesplus.listener;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.command.bounty.BountyAddCmd;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.trueog.utilitiesog.UtilitiesOG;
public class BountyIncreasePromptListener implements Listener {
private final PlayerBountiesOG plugin;
private final Map<UUID, PendingIncreasePrompt> pendingPrompts = new ConcurrentHashMap<>();
public BountyIncreasePromptListener(PlayerBountiesOG plugin) {
this.plugin = plugin;
}
public void prompt(UUID setterUuid, UUID targetUuid, String targetName) {
pendingPrompts.put(setterUuid, new PendingIncreasePrompt(targetUuid, targetName));
}
public boolean hasPrompt(UUID setterUuid) {
return pendingPrompts.containsKey(setterUuid);
}
@EventHandler(ignoreCancelled = true)
public void onChat(AsyncChatEvent event) {
final PendingIncreasePrompt prompt = pendingPrompts.get(event.getPlayer().getUniqueId());
if (prompt == null) {
return;
}
event.setCancelled(true);
final String input = StringUtils.trimToEmpty(event.message().examinableName());
if (StringUtils.equalsIgnoreCase(input, "cancel")) {
pendingPrompts.remove(event.getPlayer().getUniqueId());
UtilitiesOG.trueogMessage(event.getPlayer(), plugin.getLang().getColored("command.bounty.add.cancelled"));
return;
}
final double amount;
try {
if (!input.matches("^\\d+(?:\\.\\d)?$")) {
throw new NumberFormatException("Invalid amount format");
}
amount = Double.parseDouble(input);
} catch (NumberFormatException numberFormatException) {
UtilitiesOG.trueogMessage(event.getPlayer(), plugin.getLang().getColored("command.bounty.add.amount-nan"));
return;
}
pendingPrompts.remove(event.getPlayer().getUniqueId());
plugin.getServer().getScheduler().runTask(plugin, () -> BountyAddCmd.addBounty(plugin, event.getPlayer(),
prompt.targetUuid(), prompt.targetName(), amount, true));
}
@EventHandler
public void onQuit(PlayerQuitEvent playerQuitEvent) {
pendingPrompts.remove(playerQuitEvent.getPlayer().getUniqueId());
}
private static record PendingIncreasePrompt(UUID targetUuid, String targetName) {
}
}
@@ -1,12 +1,12 @@
package com.tcoded.playerbountiesplus.listener;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
@@ -16,20 +16,17 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.persistence.PersistentDataType;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.event.BountyClaimEvent;
import com.tcoded.playerbountiesplus.hook.team.TeamHook;
import com.tcoded.playerbountiesplus.manager.BountyDataManager;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import com.tcoded.playerbountiesplus.util.BountyHeadData;
import com.tcoded.playerbountiesplus.util.BountyWorldUtil;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.utilitiesog.UtilitiesOG;
public class DeathListener implements Listener {
@@ -37,16 +34,16 @@ public class DeathListener implements Listener {
public static final String BOUNTY_HEAD_NAME_KEY = "bounty_head_target";
public static final String BOUNTY_HEAD_UUID_KEY = "bounty_head_target_uuid";
public static final String BOUNTY_HEAD_AMOUNT_KEY = "bounty_head_diamonds";
public static final String BOUNTY_HEAD_CLAIMANT_KEY = "bounty_head_claimant";
public static final String BOUNTY_HEAD_CLAIMANT_UUID_KEY = "bounty_head_claimant_uuid";
private final PlayerBountiesOG plugin;
private final DiamondBankAPIJava diamondBankAPI;
private final LuckPerms luckPerms;
public DeathListener(PlayerBountiesOG plugin, DiamondBankAPIJava diamondBankAPI, LuckPerms luckPerms) {
public DeathListener(PlayerBountiesOG plugin) {
this.plugin = plugin;
this.diamondBankAPI = diamondBankAPI;
this.luckPerms = luckPerms;
this.luckPerms = plugin.getLuckPerms();
}
@@ -62,7 +59,7 @@ public class DeathListener implements Listener {
}
if (!killer.hasPermission("playerbountiesplus.event.claim")) {
if (!killer.hasPermission("playerbountiesog.event.claim")) {
UtilitiesOG.trueogMessage(killer, plugin.getLang().getColored("death.no-permission"));
return;
@@ -78,6 +75,14 @@ public class DeathListener implements Listener {
}
if (!BountyWorldUtil.isBountyWorldAllowed(plugin, victim.getWorld())) {
UtilitiesOG.trueogMessage(killer, plugin.getLang().getColored("death.world-not-whitelisted")
.replace("{worlds}", BountyWorldUtil.formatBountyWorldWhitelist(plugin)));
return;
}
final TeamHook teamHook = this.plugin.getTeamHook();
if (teamHook != null && teamHook.isFriendly(killer, victim)) {
@@ -135,46 +140,45 @@ public class DeathListener implements Listener {
final String killerDisplay = formatLuckPermsDisplay(killer);
final String victimDisplay = formatLuckPermsDisplay(victim);
final boolean beheaded = maybeDropPlayerHead(event, victim, bounty);
final boolean beheaded = maybeDropPlayerHead(event, killer, victim, bounty);
if (beheaded) {
if (plugin.getConfig().getBoolean("bounty-claimed-announce", true)) {
final String bountyDisplay = formatDiamonds(bounty);
final String beheadedMessage = killerDisplay + "&r &abeheaded " + victimDisplay + "&r &afor &b"
+ bountyDisplay + " &bDiamonds&a!";
Bukkit.getOnlinePlayers().forEach(player -> UtilitiesOG.trueogMessage(player, beheadedMessage));
playGlobalCelebrationEffects(killer, victim);
} else if (plugin.getConfig().getBoolean("bounty-claimed-announce", true)) {
final String bountyDisplay = formatDiamonds(bounty);
final String awardedDisplay = formatDiamonds(awardedAmount);
final String message;
if (bountyClaimable && awardedAmount > 0D) {
if (Double.compare(awardedAmount, bounty) == 0) {
if (beheaded) {
message = killerDisplay + "&r &akilled " + victimDisplay + "&r &aand earned &b" + awardedDisplay
+ " &bDiamonds &afor the bounty!";
message = killerDisplay + "&r &abeheaded " + victimDisplay + "&r &afor &b" + bountyDisplay
+ " &bDiamonds&a!";
} else {
final String awardedDisplay = formatDiamonds(awardedAmount);
if (bountyClaimable && awardedAmount > 0D) {
if (Double.compare(awardedAmount, bounty) == 0) {
message = killerDisplay + "&r &akilled " + victimDisplay + "&r &aand earned &b" + awardedDisplay
+ " &bDiamonds &afor the bounty!";
} else {
message = killerDisplay + "&r &akilled " + victimDisplay + "&r &aand claimed &b" + bountyDisplay
+ " &bDiamonds &afor &b" + awardedDisplay + " &bDiamonds&a!";
}
} else {
message = killerDisplay + "&r &akilled " + victimDisplay + "&r &aand claimed &b" + bountyDisplay
+ " &bDiamonds &afor &b" + awardedDisplay + " &bDiamonds&a!";
+ " &bDiamonds &abounty!";
}
} else {
message = killerDisplay + "&r &akilled " + victimDisplay + "&r &aand claimed &b" + bountyDisplay
+ " &bDiamonds &abounty!";
}
Bukkit.getOnlinePlayers().forEach(player -> UtilitiesOG.trueogMessage(player, message));
playGlobalCelebrationEffects(killer, victim);
announceBountyClaim(message, killer, victim);
}
@@ -183,10 +187,11 @@ public class DeathListener implements Listener {
}
private void playGlobalCelebrationEffects(Player killer, Player victim) {
private void announceBountyClaim(String message, Player killer, Player victim) {
Bukkit.getOnlinePlayers().forEach(player -> {
Bukkit.getOnlinePlayers().stream().filter(this::isClaimAnnouncementRecipient).forEach(player -> {
UtilitiesOG.trueogMessage(player, message);
player.playSound(player.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0F, 1.0F);
final Particle fireworkParticle = resolveFireworkParticle();
@@ -198,6 +203,16 @@ public class DeathListener implements Listener {
}
private boolean isClaimAnnouncementRecipient(Player player) {
final List<String> worldWhitelist = plugin.getConfig().getStringList("bounty-claimed-announce-world-whitelist");
final boolean hasConfiguredWorld = worldWhitelist.stream().anyMatch(worldName -> !worldName.isBlank());
return !hasConfiguredWorld || worldWhitelist.stream().anyMatch(
worldName -> StringUtils.equalsIgnoreCase(StringUtils.trim(worldName), player.getWorld().getName()));
}
private Particle resolveFireworkParticle() {
try {
@@ -214,15 +229,7 @@ public class DeathListener implements Listener {
private String formatDiamonds(double diamonds) {
try {
return diamondBankAPI.shardsToDiamonds(diamondBankAPI.diamondsToShards(diamonds));
} catch (RuntimeException runtimeException) {
return String.valueOf(diamonds);
}
return plugin.getBountyHeadFormatter().formatDiamonds(diamonds);
}
@@ -256,11 +263,11 @@ public class DeathListener implements Listener {
}
return out.toString();
return StringUtils.trim(out.toString()).replaceAll("\\s*>$", "");
}
private boolean maybeDropPlayerHead(PlayerDeathEvent event, Player victim, double bounty) {
private boolean maybeDropPlayerHead(PlayerDeathEvent event, Player killer, Player victim, double bounty) {
final double dropChance = Math.max(0D,
Math.min(100D, plugin.getConfig().getDouble("bounty-head-drop-chance", 50D)));
@@ -272,22 +279,11 @@ public class DeathListener implements Listener {
final ItemStack head = new ItemStack(Material.PLAYER_HEAD, 1);
final SkullMeta headMeta = (SkullMeta) head.getItemMeta();
final BountyHeadData headData = new BountyHeadData(victim.getUniqueId(), victim.getName(), bounty,
killer.getUniqueId(), killer.getName());
final PlayerProfile profile = Bukkit.createProfile(victim.getUniqueId());
headMeta.setPlayerProfile(profile);
headMeta.displayName(UtilitiesOG.trueogColorize("&c" + victim.getName() + "'s Bounty Head"));
final String bountyDisplay = formatDiamonds(bounty);
headMeta.lore(java.util.List.of(
Component.text("Target: ", NamedTextColor.GRAY)
.append(Component.text(victim.getName(), NamedTextColor.WHITE)),
Component.text("Beheaded for: ", NamedTextColor.GRAY)
.append(Component.text(bountyDisplay + " Diamonds", NamedTextColor.AQUA))));
headMeta.getPersistentDataContainer().set(new NamespacedKey(plugin, BOUNTY_HEAD_NAME_KEY),
PersistentDataType.STRING, victim.getName());
headMeta.getPersistentDataContainer().set(new NamespacedKey(plugin, BOUNTY_HEAD_UUID_KEY),
PersistentDataType.STRING, victim.getUniqueId().toString());
headMeta.getPersistentDataContainer().set(new NamespacedKey(plugin, BOUNTY_HEAD_AMOUNT_KEY),
PersistentDataType.DOUBLE, bounty);
plugin.getBountyHeadFormatter().applyHeadMeta(headMeta, headData, profile, plugin.getBountyHeadMetadata());
head.setItemMeta(headMeta);
event.getDrops().add(head);
@@ -400,8 +396,8 @@ public class DeathListener implements Listener {
}
return out.toString();
return StringUtils.trim(out.toString()).replaceAll("\\s*>$", "");
}
}
}
@@ -6,6 +6,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration;
@@ -95,7 +96,8 @@ public class BountyDataManager {
} catch (IOException ioException) {
ioException.printStackTrace();
this.plugin.getLogger().log(Level.SEVERE,
"Failed to save bounties file: " + this.bountiesFile.getPath(), ioException);
}
@@ -0,0 +1,8 @@
package com.tcoded.playerbountiesplus.util;
import java.util.UUID;
public record BountyHeadData(UUID targetUuid, String targetName, double bountyAmount, UUID claimantUuid,
String claimantName)
{
}
@@ -0,0 +1,249 @@
package com.tcoded.playerbountiesplus.util;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.inventory.meta.SkullMeta;
import com.destroystokyo.paper.profile.PlayerProfile;
import net.kyori.adventure.text.TextComponent;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import net.luckperms.api.model.user.UserManager;
import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.utilitiesog.UtilitiesOG;
public final class BountyHeadFormatter {
// LuckPerms data for offline players is not in memory, so we load it async
// and cache the resolved display string. Entries expire so rank changes get
// picked up without requiring a server restart.
private static final long DISPLAY_CACHE_TTL_MILLIS = 5L * 60L * 1000L;
private final DiamondBankAPIJava diamondBankAPI;
private final LuckPerms luckPerms;
private final Map<UUID, CachedDisplay> offlineDisplayCache = new ConcurrentHashMap<>();
private final Map<UUID, Boolean> offlineLoadsInFlight = new ConcurrentHashMap<>();
public BountyHeadFormatter(DiamondBankAPIJava diamondBankAPI, LuckPerms luckPerms) {
this.diamondBankAPI = diamondBankAPI;
this.luckPerms = luckPerms;
}
public void applyHeadMeta(SkullMeta meta, BountyHeadData data, PlayerProfile profile, BountyHeadMetadata metadata) {
if (profile != null) {
meta.setPlayerProfile(profile);
}
meta.displayName(buildCanonicalLine(data));
metadata.write(meta.getPersistentDataContainer(), data);
}
public TextComponent buildCanonicalLine(BountyHeadData data) {
return UtilitiesOG.trueogColorize(resolvePlayerDisplay(data.targetUuid(), data.targetName()) + " &cslain by "
+ resolveClaimantDisplay(data) + " &cfor a bounty of &b" + formatDiamonds(data.bountyAmount())
+ " &bDiamonds");
}
public TextComponent buildCanonicalLineTop(BountyHeadData data) {
return UtilitiesOG.trueogColorize(resolvePlayerDisplay(data.targetUuid(), data.targetName()) + " &cslain by "
+ resolveClaimantDisplay(data));
}
public TextComponent buildCanonicalLineBottom(BountyHeadData data) {
return UtilitiesOG.trueogColorize("&cfor a bounty of &b" + formatDiamonds(data.bountyAmount()) + " &bDiamonds");
}
public String resolveClaimantDisplay(BountyHeadData data) {
if (data == null) {
return "&fUnknown";
}
return resolvePlayerDisplay(data.claimantUuid(), data.claimantName());
}
public String resolvePlayerDisplay(UUID targetUuid, String fallbackName) {
final String playerName = StringUtils.defaultIfBlank(fallbackName, "Unknown Player");
if (targetUuid == null || luckPerms == null) {
return "&f" + playerName;
}
final User loadedUser = luckPerms.getUserManager().getUser(targetUuid);
if (loadedUser != null) {
return formatFromUser(loadedUser, playerName);
}
final CachedDisplay cached = offlineDisplayCache.get(targetUuid);
if (cached != null && !cached.isExpired()) {
return cached.display();
}
scheduleOfflineUserLoad(targetUuid, playerName);
return cached != null ? cached.display() : "&f" + playerName;
}
private void scheduleOfflineUserLoad(UUID targetUuid, String playerName) {
if (luckPerms == null) {
return;
}
if (offlineLoadsInFlight.putIfAbsent(targetUuid, Boolean.TRUE) != null) {
return;
}
final UserManager userManager = luckPerms.getUserManager();
userManager.loadUser(targetUuid).whenComplete((user, throwable) -> {
try {
if (user == null) {
return;
}
final String display = formatFromUser(user, playerName);
offlineDisplayCache.put(targetUuid,
new CachedDisplay(display, System.currentTimeMillis() + DISPLAY_CACHE_TTL_MILLIS));
// LuckPerms keeps loaded users resident until explicitly released.
// Release only when the UUID is not currently online so we do not
// evict live data another system may depend on.
if (Bukkit.getPlayer(targetUuid) == null) {
userManager.cleanupUser(user);
}
} finally {
offlineLoadsInFlight.remove(targetUuid);
}
});
}
private String formatFromUser(User user, String playerName) {
final CachedMetaData meta = user.getCachedData().getMetaData();
final String prefix = StringUtils.trim(StringUtils.defaultString(meta.getPrefix()).replace('§', '&'));
final String suffix = StringUtils.trim(StringUtils.defaultString(meta.getSuffix()).replace('§', '&'));
final boolean metaContainsName = containsVisibleName(prefix, playerName)
|| containsVisibleName(suffix, playerName);
final StringBuilder out = new StringBuilder();
if (!prefix.isBlank()) {
out.append(prefix).append(' ');
}
if (!metaContainsName) {
out.append(playerName);
}
if (!suffix.isBlank()) {
out.append(' ').append(suffix);
}
return sanitizeDisplay(out.toString(), playerName);
}
public String formatDiamonds(double diamonds) {
if (diamondBankAPI == null) {
return String.valueOf(diamonds);
}
try {
return diamondBankAPI.shardsToDiamonds(diamondBankAPI.diamondsToShards(diamonds));
} catch (RuntimeException runtimeException) {
return String.valueOf(diamonds);
}
}
private static String sanitizeDisplay(String formatted, String fallbackName) {
final String cleaned = StringUtils.trimToEmpty(formatted).replaceAll("(?i)(?:\\s*(?:<reset>|[&§]r))+$", "")
.replaceAll("\\s*>$", "");
return cleaned.isBlank() ? fallbackName : cleaned;
}
private static boolean containsVisibleName(String text, String playerName) {
if (text == null || playerName == null || playerName.isBlank()) {
return false;
}
final String normalizedText = StringUtils.lowerCase(UtilitiesOG.stripFormatting(text), Locale.ROOT);
final String normalizedName = StringUtils.lowerCase(playerName, Locale.ROOT);
return StringUtils.contains(normalizedText, normalizedName);
}
private record CachedDisplay(String display, long expiresAtMillis) {
boolean isExpired() {
return System.currentTimeMillis() >= expiresAtMillis;
}
}
}
@@ -0,0 +1,96 @@
package com.tcoded.playerbountiesplus.util;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.listener.DeathListener;
public final class BountyHeadMetadata {
private final NamespacedKey targetKey;
private final NamespacedKey targetUuidKey;
private final NamespacedKey amountKey;
private final NamespacedKey claimantKey;
private final NamespacedKey claimantUuidKey;
public BountyHeadMetadata(PlayerBountiesOG plugin) {
this.targetKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_NAME_KEY);
this.targetUuidKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_UUID_KEY);
this.amountKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_AMOUNT_KEY);
this.claimantKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_CLAIMANT_KEY);
this.claimantUuidKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_CLAIMANT_UUID_KEY);
}
public BountyHeadData read(PersistentDataContainer dataContainer) {
final String targetName = dataContainer.get(targetKey, PersistentDataType.STRING);
final String targetUuid = dataContainer.get(targetUuidKey, PersistentDataType.STRING);
final Double bountyAmount = dataContainer.get(amountKey, PersistentDataType.DOUBLE);
final String claimantName = dataContainer.get(claimantKey, PersistentDataType.STRING);
final String claimantUuid = dataContainer.get(claimantUuidKey, PersistentDataType.STRING);
if (StringUtils.isBlank(targetName) || StringUtils.isBlank(targetUuid) || bountyAmount == null) {
return null;
}
try {
return new BountyHeadData(UUID.fromString(targetUuid), targetName, bountyAmount,
parseOptionalUuid(claimantUuid), claimantName);
} catch (IllegalArgumentException ignored) {
return null;
}
}
public void write(PersistentDataContainer dataContainer, BountyHeadData data) {
dataContainer.set(targetKey, PersistentDataType.STRING, data.targetName());
dataContainer.set(targetUuidKey, PersistentDataType.STRING, data.targetUuid().toString());
dataContainer.set(amountKey, PersistentDataType.DOUBLE, data.bountyAmount());
if (StringUtils.isBlank(data.claimantName()) || data.claimantUuid() == null) {
dataContainer.remove(claimantKey);
dataContainer.remove(claimantUuidKey);
return;
}
dataContainer.set(claimantKey, PersistentDataType.STRING, data.claimantName());
dataContainer.set(claimantUuidKey, PersistentDataType.STRING, data.claimantUuid().toString());
}
private static UUID parseOptionalUuid(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
try {
return UUID.fromString(value);
} catch (IllegalArgumentException ignored) {
return null;
}
}
}
@@ -0,0 +1,65 @@
package com.tcoded.playerbountiesplus.util;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.World;
import org.bukkit.entity.Player;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
public final class BountyWorldUtil {
private static final String BOUNTY_WORLD_WHITELIST_KEY = "bounty-world-whitelist";
private static final List<String> DEFAULT_BOUNTY_WORLD_WHITELIST = List.of("world", "world_nether",
"world_the_end");
private BountyWorldUtil() {
}
public static boolean isBountyWorldAllowed(PlayerBountiesOG plugin, Player player) {
return player != null && isBountyWorldAllowed(plugin, player.getWorld());
}
public static boolean isBountyWorldAllowed(PlayerBountiesOG plugin, World world) {
return world != null && isBountyWorldAllowed(plugin, world.getName());
}
public static boolean isBountyWorldAllowed(PlayerBountiesOG plugin, String worldName) {
final List<String> worldWhitelist = getBountyWorldWhitelist(plugin);
final boolean hasConfiguredWorld = worldWhitelist.stream().anyMatch(world -> !StringUtils.isBlank(world));
return !hasConfiguredWorld || worldWhitelist.stream()
.anyMatch(world -> StringUtils.equalsIgnoreCase(StringUtils.trim(world), worldName));
}
public static String formatBountyWorldWhitelist(PlayerBountiesOG plugin) {
final List<String> worldWhitelist = getBountyWorldWhitelist(plugin).stream().map(StringUtils::trimToEmpty)
.filter(StringUtils::isNotBlank).collect(Collectors.toList());
return worldWhitelist.isEmpty() ? "every world" : StringUtils.join(worldWhitelist, ", ");
}
private static List<String> getBountyWorldWhitelist(PlayerBountiesOG plugin) {
if (plugin.getConfig().contains(BOUNTY_WORLD_WHITELIST_KEY)) {
return plugin.getConfig().getStringList(BOUNTY_WORLD_WHITELIST_KEY);
}
return DEFAULT_BOUNTY_WORLD_WHITELIST;
}
}
+24 -10
View File
@@ -1,7 +1,4 @@
# We currently only support a few languages, but please contribute your own translations!
# To contribute new languages, please join our Discord server or open a pull request on GitHub!
# You can find both of these links on the plugin download page: https://www.spigotmc.org/resources/player-bounties-plus.108637/
#
# PlayerBounties-OG Config File.
# Manually verified translations:
# de_de - German (Germany)
# en_us - English (US)
@@ -13,35 +10,52 @@
# es_es - Spanish (Spain)
# hi_in - Hindi (India)
# hu_hu - Hungarian (Hungary)
# it_it - Italian (Italy)
# ru_ru - Russian (Russia)
# se_se - Swedish (Sweden)
# zh_cn - Chinese (China)
# en_uwu - English (Don't ask)
lang: "en_us"
# Add more money on the victim player than the command sender spends
# For example: /bounty set TechnicallyCoded 100 -> turns into a bounty of 200 while only taking 100 from the sender
# Add more money on the victim player than the command sender spends.
# For example: /bounty set TechnicallyCoded 100 -> turns into a bounty of 200 while only taking 100 from the sender.
bounty-multiplier: 1.0
# Minimum and maximum bounty amount - These values are checked BEFORE the multiplier above is applied.
bounty-minimum: 1.0
bounty-maximum: 1000000.0
# If we should give the bounty money to the killer
# If we should give the bounty money to the killer.
bounty-claimable: true
# Worlds where players may set, add to, and claim bounties.
# Leave empty to allow bounty placement and claims in every world. World names are case-insensitive.
bounty-world-whitelist: ["world", "world_nether", "world_the_end"]
# Give more or less money to the killer. This value can be negative if you want to.
bounty-claim-multiplier: 1.0
# If we should announce that the bounty was claimed
# If we should announce that the bounty was claimed.
bounty-claimed-announce: true
# Worlds whose players should receive bounty claim and beheading announcements.
# Leave empty to announce to players in every world. World names are case-insensitive.
bounty-claimed-announce-world-whitelist: ["world", "world_nether", "world_the_end"]
# Chance (0-100) that claiming a bounty drops the victim's player head.
bounty-head-drop-chance: 50.0
# If we should announce that the bounty was placed
# How placed bounty heads should display extra labels.
# none = disabled
# hover = show a temporary floating label while a player aims at the head
bounty-head-label-mode: hover
# Maximum range in blocks for detecting a looked-at bounty head.
bounty-head-label-range: 8.0
# If we should announce that the bounty was placed.
bounty-placed-announce: true
# Should the bounty money be taken from the player who dies?
# This can reduce same-team killing (or the use of alts) to claim the bounty from themselves
# This can reduce same-team killing (or the use of alts) to claim the bounty from themselves.
bounty-take-from-victim: false
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aDer Spieler, den du getötet hast, hat eine aktive Belohnung, aber du bist im selben Team wie er!"
announce-claimed: "&4&l{killer} hat die Belohnung von {victim} im Wert von ${bounty} beansprucht"
no-permission: "&cSie dürfen keine Kopfgelder beanspruchen!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aThe player you killed has an active bounty but you are in the same team as they are!"
announce-claimed: "&4&l{killer} claimed {victim}'s bounty worth ${bounty}"
no-permission: "&cYou are not allowed to claim bounties!"
command:
+12 -3
View File
@@ -1,7 +1,7 @@
death:
same-team: "&aThe player you killed has an active bounty but you are in the same team as they are!"
announce-claimed: "&4&l{killer} claimed {victim}'s bounty worth ${bounty}"
no-permission: "&cYou are not allowed to claim bounties!"
world-not-whitelisted: "&cYou can only claim bounties in these worlds: {worlds}."
command:
no-permission: "&cYou don't have the permission to use this command!"
@@ -36,8 +36,9 @@ command:
player-not-found: "&cThat player is not online or doesn't exist!"
success: "&a{target} currently has a bounty of {bounty}."
bounty:
no-action: "&cYou need to specify an action! /bounty <set/top/check>"
no-action: "&cYou need to specify an action! /bounty <set/add/top/check>"
invalid-action: "&cThat's not a valid action!"
world-not-whitelisted: "&cYou can only set or increase bounties in these worlds: {worlds}."
set:
missing-args: "&cYou need to specify a player and an amount! /bounty set <player> <amount>"
amount-nan: "&cThat's not a valid number!"
@@ -49,6 +50,15 @@ command:
success: "&cYou placed a bounty of {bounty} on {target}'s head!"
announce: "\nA bounty of {bounty} was placed on {target}'s &ahead by&r {player}!{extra}\n"
announce-extra: " (Total: {total})"
cancelled: "&cBounty placement was cancelled."
add:
missing-args: "&cYou need to specify a player and an amount! /bounty add <player> <amount>"
amount-nan: "&cThat's not a valid number!"
player-not-found: "&cThat player could not be found!"
success: "&aYou increased {target}'s bounty by {amount}. Total: {total}."
announce: "\n{player} increased {target}'s bounty by {amount}&a! &7(Total: &b{total}&7)\n"
prompt: "&eType how many diamonds to add to {target}'s bounty in chat. Type &ccancel &eto abort."
cancelled: "&cBounty increase cancelled."
check:
no-player-specified: "&cYou need to specify a player! /bounty check <player>"
player-not-found: "&cThat player is not online or doesn't exist!"
@@ -61,4 +71,3 @@ command:
gui:
main:
title: "&eBounties"
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aDa pwayer you kiwwed has an active bounty but you awe in da same team as they awe!"
announce-claimed: "&4&w{killer} cwaimed {victim}'s bounty wowth ${bounty}"
no-permission: "&cYou awe nyot awwowed to cwaim bounties!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aEl jugador que mataste tiene una recompensa activa pero estás en el mismo equipo que él."
announce-claimed: "&4&l{killer} reclamó la recompensa de {victim} valorada en ${bounty}"
no-permission: "&c¡No se le permite reclamar recompensas!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aLe joueur que vous avez tué a une prime sur sa tête mais vous êtes dans la même équipe!"
announce-claimed: "&4&l{killer} a gagné la prime de {victim} au prix de ${bounty}"
no-permission: "&cVous n'êtes pas autorisé à réclamer des primes!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aवह खिलाड़ी जिसे आपने मार डाला है, उसके पास एक सक्रिय इनाम है, लेकिन आप उनकी टीम में हैं!"
announce-claimed: "&4&l{killer} ने {victim} के इनाम {bounty} को मांग लिया"
no-permission: "&cआपको इनामों का दावा करने की अनुमति नहीं है!"
command:
-1
View File
@@ -4,7 +4,6 @@
death:
same-team: "&aA játékos, akit megöltél, aktív fejpénzt volt a fején, de ugyanabba a csapatba tartozol, mint ő!"
announce-claimed: "&4&l{killer} behajtotta {victim} fejpénzét és megkapta jutalmát aminek értéke: ${bounty}"
no-permission: "&cNem igényelheted a fejpénzeket!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aIl giocatore che hai ucciso ha una taglia attiva, ma sei nella stessa squadra!"
announce-claimed: "&4&l{killer} ha reclamato la taglia di {victim} del valore di ${bounty}"
no-permission: "&cNon ti è permesso riscuotere le taglie!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aDe speler die je hebt gedood heeft een actieve beloning, maar je zit in hetzelfde team als zij!"
announce-claimed: "&4&l{killer} heeft de beloning van {victim} ter waarde van ${bounty} geclaimd"
no-permission: "&cU mag geen premies claimen!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aИгрок, которого вы убили, имеет активную награду, но вы находитесь в одной команде с ними!"
announce-claimed: "&4&l{killer} получил награду {victim} на сумму ${bounty}"
no-permission: "&cВам не разрешено требовать наград!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&aDen spelare du dödade har en aktiv prissumma, men du tillhör samma lag som de är!"
announce-claimed: "&4&l{killer} har tagit {victim}s prissumma värd {bounty} dollar"
no-permission: "&cDu får inte hämta in prissummor!"
command:
-1
View File
@@ -1,6 +1,5 @@
death:
same-team: "&a你击杀的玩家有悬赏,但你与他们在同一队伍中!"
announce-claimed: "&4&l{killer} 已领取 {victim} 的悬赏,价值 ${bounty}"
no-permission: "&c你没有权限领取悬赏!"
command:
+18 -14
View File
@@ -4,37 +4,41 @@ main: com.tcoded.playerbountiesplus.PlayerBountiesOG
api-version: '${apiVersion}'
authors: [ TechnicallyCoded, NotAlexNoyle ]
description: A player bounty plugin for TrueOG Network using our custom Diamond Economy.
website: https://github.com/true-og/PlayerBounties-OG/
website: https://git.trueog.net/trueog/PlayerBounties-OG/
folia-supported: false
depend:
- DiamondBank-OG
softdepend:
- CoreProtect-OG
softdepend:
- SimpleClans
- Unions-OG
- LuckPerms
commands:
bounty:
permission: playerbountiesplus.command.bounty
permission: playerbountiesog.command.bounty
aliases: [bounties, prime, primes]
playerbountiesplusadmin:
permission: playerbountiesplus.command.admin
permission: playerbountiesog.command.admin
aliases: [playerbountiesplus, pbpa, pbp]
permissions:
playerbountiesplus.command.bounty:
playerbountiesog.command.bounty:
default: true
playerbountiesplus.command.admin:
playerbountiesog.command.admin:
default: op
children:
playerbountiesplus.command.admin.version: {}
playerbountiesplus.command.admin.reload: {}
playerbountiesplus.command.admin.bounty.set: {}
playerbountiesplus.command.admin.bounty.add: {}
playerbountiesplus.command.admin.bounty.remove: {}
playerbountiesplus.command.admin.bounty.delete: {}
playerbountiesplus.command.bounty.set:
playerbountiesog.command.admin.version: {}
playerbountiesog.command.admin.reload: {}
playerbountiesog.command.admin.bounty.set: {}
playerbountiesog.command.admin.bounty.add: {}
playerbountiesog.command.admin.bounty.remove: {}
playerbountiesog.command.admin.bounty.delete: {}
playerbountiesog.command.admin.bounty.get: {}
playerbountiesog.command.bounty.set:
default: true
playerbountiesplus.event.claim:
playerbountiesog.command.bounty.add:
default: true
playerbountiesog.event.claim:
default: true