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
NotAlexNoyle 5d85028e81 Fix CoreProtect skull logging to use legacy-safe block API 2026-03-09 10:19:09 -04:00
NotAlexNoyle f40afbd4a4 Update GUI 2026-03-09 10:05:43 -04:00
NotAlexNoyle 5ae4b03d30 Formatting 2026-03-09 09:55:36 -04:00
NotAlexNoyle 543912b8a1 Merge pull request #12 from true-og/codex/fix-multiple-issues-in-bounty-plugin
Fix bounty GUI regressions and /bounty no-args behavior
2026-03-09 09:54:18 -04:00
NotAlexNoyle b1ca462de9 Fix bounty GUI regressions and command fallback behavior 2026-03-09 09:53:48 -04:00
NotAlexNoyle 1a47411f3b CoreProtect-OG rollback integration 2026-03-09 09:31:00 -04:00
NotAlexNoyle 7e193edd13 Merge pull request #11 from true-og/codex/use-coreprotect-api-for-head-metadata
Integrate CoreProtect logging for bounty head metadata
2026-03-09 09:13:46 -04:00
NotAlexNoyle c214807fdd Integrate CoreProtect logging for bounty head metadata 2026-03-09 09:13:34 -04:00
NotAlexNoyle c3e63a7475 Formatting 2026-03-09 09:07:15 -04:00
NotAlexNoyle bac11ea028 Merge pull request #10 from true-og/codex/implement-advanced-player-display-features
Use UUID-based heads, LuckPerms-formatted names, right-click actionbar, and explicit "Diamonds" labels
2026-03-09 09:04:55 -04:00
NotAlexNoyle c37165e2c5 Update bounty displays, LuckPerms formatting, and UUID head metadata 2026-03-09 09:04:05 -04:00
NotAlexNoyle ddc4f21868 Update formatting 2026-03-09 08:37:33 -04:00
NotAlexNoyle 7070e2cf38 Merge pull request #9 from true-og/codex/fix-chat-message-boldness-and-add-lore
Fix chat formatting bleed, 1.19.4 firework particle, and persistent bounty-head metadata
2026-03-09 08:14:12 -04:00
NotAlexNoyle 0560fdfd5d Fix chat formatting, fireworks particle fallback, and bounty head metadata 2026-03-09 08:13:49 -04:00
NotAlexNoyle 5726b86bd5 Fix string formatting 2026-03-09 08:03:29 -04:00
NotAlexNoyle aca097a8b1 Update formatting 2026-03-09 07:48:19 -04:00
NotAlexNoyle 193c614cc6 Merge pull request #8 from true-og/codex/implement-bounty-gui-enhancements
Use LuckPerms ranks in bounty GUI and add global celebration on claim
2026-03-09 07:43:03 -04:00
NotAlexNoyle 7dd394e5be Enhance bounty GUI ranks and claim celebrations 2026-03-09 07:42:48 -04:00
NotAlexNoyle 5525065d62 Merge pull request #7 from true-og/codex/update-bounty-handling-and-messaging
Support decimal bounties, use DiamondBank-OG shard conversions, and add behead broadcast
2026-03-09 07:35:58 -04:00
NotAlexNoyle 86b595a196 Support decimal bounty inputs and DiamondBank shard-based bounty messaging 2026-03-09 07:35:28 -04:00
NotAlexNoyle 8030220ffb Update GxUI-OG 2026-03-09 05:20:10 -04:00
NotAlexNoyle 2a7645effc Protect inventory items from player exploitation 2026-03-09 05:09:17 -04:00
NotAlexNoyle ff7e8156ff Update README.md 2026-03-09 04:59:32 -04:00
NotAlexNoyle 37e88c913c Merge pull request #6 from true-og/codex/add-configurable-player-head-drop-chance
Add configurable player head drop chance for bounty claims
2026-03-09 04:58:44 -04:00
NotAlexNoyle babfe91ef1 Add configurable bounty player head drop chance 2026-03-09 04:58:32 -04:00
NotAlexNoyle 2d10ab8eb6 Update formatting 2026-03-09 04:54:50 -04:00
NotAlexNoyle efa90b8101 Merge pull request #5 from true-og/codex/fix-item-extraction-bug-in-gui
Prevent taking placeholder items from bounty GUI
2026-03-09 04:52:52 -04:00
NotAlexNoyle eb41781b3c Suppress default death message during bounty claim 2026-03-09 04:52:33 -04:00
NotAlexNoyle 7cfda08d39 Prevent item extraction from bounty GUI 2026-03-09 04:52:10 -04:00
NotAlexNoyle a84b72071a Update GUI formatting 2026-03-09 04:47:59 -04:00
NotAlexNoyle 7cb97e7c1d Merge pull request #4 from true-og/codex/fix-stack-overflow-errors-in-plugin
Avoid recursive language defaults in LangUtil to prevent StackOverflow
2026-03-09 04:25:18 -04:00
55 changed files with 2330 additions and 298 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.
+21 -17
View File
@@ -1,19 +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.
- Integrates with [DiamondBank-OG](https://github.com/true-og/DiamondBank-OG) and supports [SimpleClans](https://github.com/true-og/SimpleClans) for team detection.
- Inventory GUI for viewing bounties.
- Customizable odds for receiving player head from bounty victim.
- Localized messages in multiple languages.
- Uses [LuckPerms](https://github.com/true-og/LuckPerms) for player name formatting.
- 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
@@ -25,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.
+3 -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.
@@ -66,6 +64,7 @@ repositories {
maven { url = uri("https://repo.glaremasters.me/repository/towny/") }
maven { url = uri("https://repo.extendedclip.com/releases/") }
maven { url = uri("https://repo.tcoded.com/public/") }
maven { url = uri("https://maven.playpro.com") }
}
/* ---------------------- Java project deps ---------------------------- */
@@ -73,6 +72,7 @@ dependencies {
compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Declare Purpur API version to be packaged.
compileOnly("net.sacredlabyrinth.phaed.simpleclans:SimpleClans:2.15.2")
compileOnly("net.luckperms:api:5.5") // Import LuckPerms API.
compileOnly("net.coreprotect:coreprotect:23.1")
compileOnlyApi(project(":libs:Utilities-OG")) // Import TrueOG Network Utilities-OG Java API (from source).
compileOnlyApi(project(":libs:GxUI-OG")) // Import TrueOG Network GxUI-OG Java API (from source).
compileOnlyApi(project(":libs:DiamondBank-OG")) {
@@ -1,25 +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;
@@ -45,6 +48,8 @@ public final class PlayerBountiesOG extends JavaPlugin {
// Utilities.
private LangUtil langUtil;
private BountyHeadFormatter bountyHeadFormatter;
private BountyHeadMetadata bountyHeadMetadata;
// Managers.
private BountyDataManager bountyDataManager;
@@ -59,6 +64,12 @@ public final class PlayerBountiesOG extends JavaPlugin {
// LuckPerms API hook.
private LuckPerms luckPerms;
// CoreProtect API hook.
private CoreProtectHook coreProtectHook;
private BountyIncreasePromptListener bountyIncreasePromptListener;
private BountyHeadLabelListener bountyHeadLabelListener;
public PlayerBountiesOG() {
instance = this;
@@ -86,6 +97,14 @@ public final class PlayerBountiesOG extends JavaPlugin {
}
this.coreProtectHook = new CoreProtectHook(this);
if (!this.coreProtectHook.isEnabled()) {
getLogger().warning(
"CoreProtect API v10+ not found - custom bounty head metadata may not rollback correctly.");
}
// Initialize the DiamondBank-OG API.
final RegisteredServiceProvider<DiamondBankAPIJava> provider = getServer().getServicesManager()
.getRegistration(DiamondBankAPIJava.class);
@@ -104,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);
@@ -141,18 +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 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);
}
@@ -165,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!");
@@ -201,45 +224,33 @@ public final class PlayerBountiesOG extends JavaPlugin {
}
// Utilities.
private static void findPluginWithQuery(List<Plugin> plugins, String pluginNameQuery, String... excludeStrings) {
public LuckPerms getLuckPerms() {
final String firstPluginFound = plugins.stream().filter(p -> {
return this.luckPerms;
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 CoreProtectHook getCoreProtectHook() {
}
return this.coreProtectHook;
// If the plugin name contains any of the exclude strings, skip.
for (String excludeString : excludeStrings) {
}
if (StringUtils.contains(lowerName, excludeString)) {
public BountyHeadFormatter getBountyHeadFormatter() {
return false;
return this.bountyHeadFormatter;
}
}
}
public BountyHeadMetadata getBountyHeadMetadata() {
return true;
return this.bountyHeadMetadata;
}).findFirst().map(p -> {
}
final List<String> authors = p.getPluginMeta().getAuthors();
public BountyIncreasePromptListener getBountyIncreasePromptListener() {
return p.getName() + " (" + (authors.isEmpty() ? "N/A" : authors.get(0)) + ")";
}).orElse(null);
if (firstPluginFound == null) {
return;
}
return this.bountyIncreasePromptListener;
}
@@ -289,4 +300,4 @@ public final class PlayerBountiesOG extends JavaPlugin {
}
}
}
@@ -4,6 +4,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@@ -15,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;
@@ -36,27 +38,30 @@ public class BountyCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(CommandSender sender, Command cmd, String cmdName, String[] args) {
final Player player;
if (!(sender instanceof Player)) {
UtilitiesOG.logToConsole(PlayerBountiesOG.getPrefix(), "&cERROR: Only players can use this command.");
return true;
} else {
player = (Player) sender;
}
if (args.length < 1) {
UtilitiesOG.trueogMessage(player, plugin.getLang().getColored("command.bounty.no-action"));
if (StringUtils.equalsIgnoreCase(cmdName, "bounties")) {
return BountyTopCmd.handleCmd(plugin, diamondBankAPI, sender, cmd, cmdName, args);
}
final String noAction = plugin.getLang().getColored("command.bounty.no-action");
UtilitiesOG.trueogMessage((Player) sender, noAction);
return true;
}
final String action = args[0].toLowerCase();
final String action = StringUtils.lowerCase(args[0]);
switch (action) {
case "set" -> {
@@ -65,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);
@@ -76,7 +87,6 @@ public class BountyCommand implements CommandExecutor, TabCompleter {
return BountyCheckCmd.handleCmd(plugin, sender, cmd, cmdName, args);
}
default -> {
// If "/bounty" is run without a valid subcommand, treat it as "/bounty top".
@@ -96,19 +106,26 @@ public class BountyCommand implements CommandExecutor, TabCompleter {
if (args.length == 1) {
return Lists.newArrayList("set", "top", "check").stream()
.filter(action -> action.startsWith(args[0].toLowerCase())).collect(Collectors.toList());
return Lists.newArrayList("set", "add", "top", "check").stream()
.filter(action -> StringUtils.startsWith(action, StringUtils.lowerCase(args[0])))
.collect(Collectors.toList());
} else if (args.length > 1) {
final String subCommand = args[0].toLowerCase();
final String subCommand = StringUtils.lowerCase(args[0]);
return switch (subCommand) {
case "set" -> BountySetCmd.onTabComplete(sender, args).stream()
.filter(suggestion -> suggestion.toLowerCase().startsWith(args[1].toLowerCase()))
case "set" -> BountySetCmd
.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 -> suggestion.toLowerCase().startsWith(args[1].toLowerCase()))
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])))
.collect(Collectors.toList());
case "top" -> Collections.emptyList();
default -> Collections.emptyList();
@@ -5,6 +5,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@@ -45,7 +46,7 @@ public class PlayerBountiesPlusAdminCmd implements CommandExecutor, TabCompleter
}
final String arg0Lower = args[0].toLowerCase();
final String arg0Lower = StringUtils.lowerCase(args[0]);
return switch (arg0Lower) {
case "reload" -> PlayerBountiesPlusReloadCmd.handleCmd(plugin, sender, command, label, args);
@@ -96,12 +97,13 @@ public class PlayerBountiesPlusAdminCmd implements CommandExecutor, TabCompleter
if (args.length == 1) {
return completions.stream().filter(s -> s.toLowerCase().startsWith(args[0].toLowerCase()))
return completions.stream()
.filter(s -> StringUtils.startsWith(StringUtils.lowerCase(s), StringUtils.lowerCase(args[0])))
.collect(Collectors.toList());
} else if (args.length > 1) {
final String subCommand = args[0].toLowerCase();
final String subCommand = StringUtils.lowerCase(args[0]);
return switch (subCommand) {
case "bounty" -> AdminBountyCmd.onTabComplete(sender, args);
@@ -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)
@@ -64,10 +64,10 @@ public class AdminBountyAddCmd {
}
final String playerName = args[2];
final int amount;
final double amount;
try {
amount = Integer.parseInt(args[3]);
amount = parseAmount(args[3]);
} catch (NumberFormatException numberFormatException) {
@@ -131,6 +131,18 @@ public class AdminBountyAddCmd {
}
private static double parseAmount(String amountInput) {
if (!amountInput.matches("^\\d+(?:\\.\\d)?$")) {
throw new NumberFormatException("Invalid amount format");
}
return Double.parseDouble(amountInput);
}
@Nullable
public static List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args)
@@ -5,6 +5,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -36,7 +37,7 @@ public class AdminBountyCmd {
}
final String action = args[1].toLowerCase();
final String action = StringUtils.lowerCase(args[1]);
return switch (action) {
case "set" -> AdminBountySetCmd.handleCmd(plugin, sender, cmd, label, args);
@@ -75,7 +76,7 @@ public class AdminBountyCmd {
} else if (args.length > 2) {
final String sub = args[1].toLowerCase();
final String sub = StringUtils.lowerCase(args[1]);
switch (sub) {
case "add" -> suggestions = AdminBountyAddCmd.onTabComplete(sender, null, null, args);
@@ -88,8 +89,9 @@ public class AdminBountyCmd {
}
final String input = args[args.length - 1].toLowerCase();
return suggestions.stream().filter(opt -> opt.toLowerCase().startsWith(input)).collect(Collectors.toList());
final String input = StringUtils.lowerCase(args[args.length - 1]);
return suggestions.stream().filter(opt -> StringUtils.startsWith(StringUtils.lowerCase(opt), input))
.collect(Collectors.toList());
}
@@ -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)
@@ -63,10 +63,10 @@ public class AdminBountyRemoveCmd {
}
final String playerName = args[2];
final int amount;
final double amount;
try {
amount = Integer.parseInt(args[3]);
amount = parseAmount(args[3]);
} catch (NumberFormatException numberFormatException) {
@@ -139,6 +139,18 @@ public class AdminBountyRemoveCmd {
}
private static double parseAmount(String amountInput) {
if (!amountInput.matches("^\\d+(?:\\.\\d)?$")) {
throw new NumberFormatException("Invalid amount format");
}
return Double.parseDouble(amountInput);
}
@Nullable
public static List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull 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)
@@ -62,10 +62,10 @@ public class AdminBountySetCmd {
}
final String playerName = args[2];
final int amount;
final double amount;
try {
amount = Integer.parseInt(args[3]);
amount = parseAmount(args[3]);
} catch (NumberFormatException numberFormatException) {
@@ -125,6 +125,18 @@ public class AdminBountySetCmd {
}
private static double parseAmount(String amountInput) {
if (!amountInput.matches("^\\d+(?:\\.\\d)?$")) {
throw new NumberFormatException("Invalid amount format");
}
return Double.parseDouble(amountInput);
}
@Nullable
public static List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull 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,7 +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;
@@ -42,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);
@@ -85,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);
@@ -102,18 +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 -> name.toLowerCase().startsWith(args[1].toLowerCase())).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());
}
@@ -5,17 +5,24 @@ import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
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.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;
import net.luckperms.api.model.user.User;
import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.utilitiesog.UtilitiesOG;
public class BountySetCmd {
@@ -41,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);
@@ -61,11 +68,19 @@ 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 {
parsedAmount = Integer.parseInt(args[2]);
parsedAmount = parseAmount(args[2]);
} catch (NumberFormatException numberFormatException) {
@@ -215,47 +230,41 @@ public class BountySetCmd {
// Calculate total bounty including previous bounties.
final BountyDataManager bountyDataManager = plugin.getBountyDataManager();
final double bountyAlreadyPresent = bountyDataManager.getBounty(playerUUID);
final double chargeAndBountyAmount = (int) amount;
final double totalBounty = chargeAndBountyAmount + bountyAlreadyPresent;
final DiamondBankAPIJava diamondBankAPI = resolveDiamondBankApi(plugin);
final long newBountyShards = diamondBankAPI.diamondsToShards(amount);
final long existingBountyShards = diamondBankAPI
.diamondsToShards((float) bountyDataManager.getBounty(playerUUID));
final long totalBountyShards = newBountyShards + existingBountyShards;
final double totalBounty = Double.parseDouble(diamondBankAPI.shardsToDiamonds(totalBountyShards));
bountyDataManager.setBounty(playerUUID, totalBounty);
// Confirmation.
final String bountySetSuccessMessage = plugin.getLang().getColored("command.bounty.set.success")
.replace("{bounty}", String.valueOf(chargeAndBountyAmount))
.replace("{total}", String.valueOf(totalBounty)).replace("{target}", target.getName())
.replace("{player}", sender.getName());
if (!(sender instanceof Player)) {
UtilitiesOG.logToConsole(PlayerBountiesOG.getPrefix(), bountySetSuccessMessage);
} else {
UtilitiesOG.trueogMessage((Player) sender, bountySetSuccessMessage);
}
// Announcement.
final String extra;
if (bountyAlreadyPresent == 0) {
if (existingBountyShards == 0L) {
extra = "";
} else {
extra = plugin.getLang().getColored("command.bounty.set.announce-extra").replace("{total}",
String.valueOf(totalBounty));
diamondBankAPI.shardsToDiamonds(totalBountyShards));
}
if (plugin.getConfig().getBoolean("bounty-placed-announce", true)) {
final LuckPerms luckPerms = resolveLuckPerms(plugin);
final String setterDisplay = sender instanceof Player player ? formatLuckPermsDisplay(luckPerms, player)
: sender.getName();
final String targetDisplay = formatLuckPermsDisplay(luckPerms, target);
final String bountyDisplay = diamondBankAPI.shardsToDiamonds(newBountyShards);
Bukkit.getOnlinePlayers()
.forEach((Player player) -> UtilitiesOG.trueogMessage(player,
plugin.getLang().getColored("command.bounty.set.announce")
.replace("{bounty}", String.valueOf(chargeAndBountyAmount))
.replace("{target}", target.getName()).replace("{player}", sender.getName())
.replace("{extra}", extra)));
PlayerBountiesOG.getPrefix() + "&a"
+ plugin.getLang().getColored("command.bounty.set.announce")
.replace("{bounty}", bountyDisplay).replace("{target}", targetDisplay)
.replace("{player}", setterDisplay).replace("{extra}", extra)));
}
@@ -265,6 +274,142 @@ public class BountySetCmd {
}
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 formatLuckPermsDisplay(LuckPerms luckPerms, Player player) {
final String prefix = stripLeadingReset(stripTrailingReset(getLuckPermsPrefixLegacy(luckPerms, player)));
final String leadingColorCodes = extractLeadingColorCodes(prefix);
if (prefix.isBlank()) {
return player.getName().replaceAll("\\s*>$", "");
}
if (leadingColorCodes.isBlank()) {
return (prefix + " " + player.getName()).replaceAll("\\s*>$", "");
}
return (prefix + " " + leadingColorCodes + player.getName()).replaceAll("\\s*>$", "");
}
private static String getLuckPermsPrefixLegacy(LuckPerms luckPerms, Player player) {
if (luckPerms == null) {
return "";
}
final User user = luckPerms.getUserManager().getUser(player.getUniqueId());
if (user == null) {
return "";
}
final CachedMetaData meta = user.getCachedData().getMetaData();
final String prefix = meta.getPrefix();
if (prefix == null || prefix.isBlank()) {
return "";
}
return StringUtils.trim(prefix).replace('§', '&');
}
private static String stripTrailingReset(String input) {
if (input == null || input.isBlank()) {
return "";
}
return StringUtils.trim(input.replaceAll("(?i)(?:\\s*(?:<reset>|[&§]r))+$", ""));
}
private static String stripLeadingReset(String input) {
if (input == null || input.isBlank()) {
return "";
}
return StringUtils.trim(input.replaceFirst("(?i)^(?:\\s*(?:<reset>|[&§]r))+", ""));
}
private static String extractLeadingColorCodes(String input) {
if (input == null || input.length() < 2) {
return "";
}
final StringBuilder out = new StringBuilder();
for (int i = 0; i < input.length() - 1; i++) {
final char current = input.charAt(i);
if (current != '&' && current != '§') {
break;
}
out.append(current).append(input.charAt(i + 1));
i++;
}
return out.toString();
}
@Nullable
public static List<String> onTabComplete(@NotNull CommandSender sender, @NotNull String[] args) {
@@ -272,7 +417,8 @@ public class BountySetCmd {
// Suggest online player names for the username.
return sender.getServer().getOnlinePlayers().stream().map(Player::getName)
.filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())).collect(Collectors.toList());
.filter(name -> StringUtils.startsWith(StringUtils.lowerCase(name), StringUtils.lowerCase(args[1])))
.collect(Collectors.toList());
} else if (args.length == 3) {
@@ -285,4 +431,4 @@ public class BountySetCmd {
}
}
}
@@ -6,6 +6,7 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -13,6 +14,7 @@ import org.bukkit.entity.Player;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.gui.MainBountyGui;
import com.tcoded.playerbountiesplus.util.BountyHeadFormatter;
import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.utilitiesog.UtilitiesOG;
@@ -25,7 +27,8 @@ public class BountyTopCmd {
if (sender instanceof Player player) {
new MainBountyGui(plugin, player, () -> buildGuiEntries(plugin), diamondBankAPI).open();
final List<MainBountyGui.BountyGuiEntry> entries = buildGuiEntries(plugin);
new MainBountyGui(plugin, player, () -> entries, diamondBankAPI).open();
return true;
@@ -39,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);
@@ -62,7 +67,9 @@ public class BountyTopCmd {
}
entries.add(new MainBountyGui.BountyGuiEntry(entry.getKey(), targetName, entry.getValue()));
final String displayName = formatter.resolvePlayerDisplay(entry.getKey(), targetName);
entries.add(new MainBountyGui.BountyGuiEntry(entry.getKey(), targetName, displayName, entry.getValue()));
}
@@ -74,6 +81,8 @@ public class BountyTopCmd {
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);
@@ -102,9 +111,12 @@ public class BountyTopCmd {
.shardsToDiamonds(diamondBankAPI.diamondsToShards(entry.getValue()));
strb.append("&7 - ");
strb.append(targetName);
strb.append(": ");
final String displayName = formatter.resolvePlayerDisplay(entry.getKey(), targetName);
strb.append(displayName);
strb.append(": &b");
strb.append(formattedBounty);
strb.append(" &bDiamonds");
strb.append('\n');
}
@@ -116,7 +128,7 @@ public class BountyTopCmd {
}
String message = strb.toString();
message = message.substring(0, message.length() - 1);
message = StringUtils.substring(message, 0, message.length() - 1);
if (!(sender instanceof Player)) {
@@ -6,8 +6,8 @@ import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Material;
import org.bukkit.entity.Player;
@@ -17,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 {
@@ -28,8 +30,6 @@ public class MainBountyGui extends GUIBase {
private static final int NEXT_PAGE_SLOT = 51;
private static final int CLOSE_SLOT = 53;
private static final Pattern MINECRAFT_USERNAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]{1,16}$");
private final PlayerBountiesOG plugin;
private final Player viewer;
private final Supplier<List<BountyGuiEntry>> bountySupplier;
@@ -76,9 +76,10 @@ public class MainBountyGui extends GUIBase {
if (sortedBounties.isEmpty()) {
addItem(22,
createStaticItem(Material.WRITABLE_BOOK, "&e&lNo Active Bounties",
List.of("&7Nobody currently has a bounty.", "&7Place one with your bounty command to get",
"&7this leaderboard moving again.")));
createStaticItem(Material.WRITABLE_BOOK, "&6&lNo Active Bounties",
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;
}
@@ -105,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
@@ -140,20 +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, "&e&lPage " + (currentPage + 1) + "/" + (maxPage + 1),
List.of("&7Active bounties: &f" + totalBounties, "&7Shown per page: &f" + PYRAMID_SLOTS.size(),
"&7Sorted from highest to lowest.")));
addItem(PAGE_INFO_SLOT, createStaticItem(Material.BOOK, "&3&lPage " + (currentPage + 1) + "/" + (maxPage + 1),
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
@@ -188,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
@@ -247,42 +246,110 @@ public class MainBountyGui extends GUIBase {
private GUIItem createBountyItem(BountyGuiEntry entry, int rank) {
final String targetName = safeName(entry.targetName());
final ArrayList<String> lore = new ArrayList<>();
final String displayName = safeDisplayName(entry.displayName());
final ArrayList<TextComponent> lore = new ArrayList<>();
lore.add("&7Rank: &f#" + rank);
lore.add("&7Target: &f" + targetName);
lore.add("&7Bounty: &6" + formatBounty(entry.bountyDiamonds()));
lore.add("");
lore.add("&8Highest bounty sits at the top.");
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 (isUsableHeadName(targetName)) {
if (entry.targetName() != null && !entry.targetName().isBlank()) {
item = new GUIItem(Material.PLAYER_HEAD, 1, "&6&l#" + rank + " &f" + targetName, targetName);
item = new GUIItem(Material.PLAYER_HEAD, 1, " &f#" + rank + " " + displayName, entry.targetName());
} else {
item = new GUIItem(Material.PAPER, 1, "&6&l#" + rank + " &f" + targetName);
item = new GUIItem(Material.PAPER, 1, "&6&l#" + rank + " " + displayName);
}
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);
return diamondBankAPI.shardsToDiamonds(shards);
return diamondBankAPI.shardsToDiamonds(shards) + " Diamonds";
}
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));
@@ -292,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));
@@ -335,24 +404,23 @@ public class MainBountyGui extends GUIBase {
private String safeName(String name) {
if (name == null || name.isBlank()) {
return name == null || name.isBlank() ? "Unknown Player" : name;
return "Unknown Player";
}
private String safeDisplayName(String displayName) {
if (displayName == null || displayName.isBlank()) {
return "&fUnknown Player";
}
return name;
return displayName;
}
private boolean isUsableHeadName(String name) {
return name != null && !name.isBlank() && !"Unknown Player".equals(name)
&& MINECRAFT_USERNAME_PATTERN.matcher(name).matches();
}
public static record BountyGuiEntry(UUID targetUuid, String targetName, double bountyDiamonds) {
public static record BountyGuiEntry(UUID targetUuid, String targetName, String displayName, double bountyDiamonds) {
}
}
@@ -0,0 +1,116 @@
package com.tcoded.playerbountiesplus.hook.logging;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.plugin.Plugin;
import net.coreprotect.CoreProtect;
import net.coreprotect.CoreProtectAPI;
public class CoreProtectHook {
private static final int MINIMUM_API_VERSION = 10;
private final CoreProtectAPI coreProtectAPI;
public CoreProtectHook(Plugin plugin) {
this.coreProtectAPI = findCoreProtect(plugin);
}
public boolean isEnabled() {
return this.coreProtectAPI != null;
}
public boolean logPlacement(String user, BlockState blockState) {
if (this.coreProtectAPI == null || user == null || blockState == null) {
return false;
}
return this.coreProtectAPI.logPlacement(user, blockState.getLocation(), blockState.getType(),
blockState.getBlockData());
}
public boolean logRemoval(String user, BlockState blockState) {
if (this.coreProtectAPI == null || user == null || blockState == null) {
return false;
}
return this.coreProtectAPI.logRemoval(user, blockState.getLocation(), blockState.getType(),
blockState.getBlockData());
}
public boolean logContainerTransaction(String user, Location location) {
if (this.coreProtectAPI == null || user == null || location == null) {
return false;
}
return this.coreProtectAPI.logContainerTransaction(user, location);
}
public boolean logInteraction(String user, Location location) {
if (this.coreProtectAPI == null || user == null || location == null) {
return false;
}
return this.coreProtectAPI.logInteraction(user, location);
}
public boolean hasPlaced(String user, Block block, int time, int offset) {
if (this.coreProtectAPI == null || user == null || block == null) {
return false;
}
return this.coreProtectAPI.hasPlaced(user, block, time, offset);
}
private static CoreProtectAPI findCoreProtect(Plugin plugin) {
if (plugin == null) {
return null;
}
final Plugin coreProtectPlugin = plugin.getServer().getPluginManager().getPlugin("CoreProtect-OG");
if (!(coreProtectPlugin instanceof CoreProtect coreProtect)) {
return null;
}
final CoreProtectAPI api = coreProtect.getAPI();
if (api == null || !api.isEnabled() || api.APIVersion() < MINIMUM_API_VERSION) {
return null;
}
return api;
}
}
@@ -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());
}
}
}
@@ -0,0 +1,199 @@
package com.tcoded.playerbountiesplus.listener;
import org.bukkit.Material;
import org.bukkit.block.BlockState;
import org.bukkit.block.Skull;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.persistence.PersistentDataContainer;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.hook.logging.CoreProtectHook;
import com.tcoded.playerbountiesplus.util.BountyHeadData;
public class BountyHeadListener implements Listener {
private final PlayerBountiesOG plugin;
private final CoreProtectHook coreProtectHook;
public BountyHeadListener(PlayerBountiesOG plugin) {
this.plugin = plugin;
this.coreProtectHook = plugin.getCoreProtectHook();
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlace(BlockPlaceEvent event) {
if (!isHeadMaterial(event.getBlockPlaced().getType())) {
return;
}
final ItemMeta itemMeta = event.getItemInHand().getItemMeta();
if (itemMeta == null) {
return;
}
final BountyHeadData headData = plugin.getBountyHeadMetadata().read(itemMeta.getPersistentDataContainer());
if (headData == null) {
return;
}
final BlockState state = event.getBlockPlaced().getState();
if (!(state instanceof Skull skull)) {
return;
}
plugin.getBountyHeadMetadata().write(skull.getPersistentDataContainer(), headData);
skull.update(true, false);
if (this.coreProtectHook != null) {
this.coreProtectHook.logPlacement(event.getPlayer().getName(), skull);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBreak(BlockBreakEvent event) {
if (!isHeadMaterial(event.getBlock().getType())) {
return;
}
final BlockState state = event.getBlock().getState();
final BlockState snapshot = state;
if (!(state instanceof Skull skull)) {
return;
}
final BountyHeadData headData = plugin.getBountyHeadMetadata().read(skull.getPersistentDataContainer());
if (headData == null) {
return;
}
final ItemStack drop = new ItemStack(Material.PLAYER_HEAD, 1);
final SkullMeta meta = (SkullMeta) drop.getItemMeta();
plugin.getBountyHeadFormatter().applyHeadMeta(meta, headData, skull.getPlayerProfile(),
plugin.getBountyHeadMetadata());
drop.setItemMeta(meta);
if (this.coreProtectHook != null) {
this.coreProtectHook.logRemoval(event.getPlayer().getName(), snapshot);
}
event.setDropItems(false);
event.getBlock().setType(Material.AIR, false);
event.getBlock().getWorld().dropItemNaturally(event.getBlock().getLocation(), drop);
}
@EventHandler(ignoreCancelled = true)
public void onRightClick(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
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())) {
sendClaimantActionbar(event.getPlayer(), item.getItemMeta());
}
}
private void sendClaimantActionbar(Player player, ItemMeta itemMeta) {
if (itemMeta == null) {
return;
}
sendClaimantActionbar(player, itemMeta.getPersistentDataContainer());
}
private void sendClaimantActionbar(Player player, PersistentDataContainer dataContainer) {
final BountyHeadData headData = plugin.getBountyHeadMetadata().read(dataContainer);
if (headData == null) {
return;
}
final net.kyori.adventure.text.Component line = plugin.getBountyHeadFormatter().buildCanonicalLine(headData);
// Send action bar immediately
player.sendActionBar(line);
// 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, () -> {
if (player.isOnline()) {
player.sendActionBar(line);
}
}, 40L);
}
private static boolean isHeadMaterial(Material material) {
return material == Material.PLAYER_HEAD || material == Material.PLAYER_WALL_HEAD;
}
}
@@ -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,37 +1,49 @@
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.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
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 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 {
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();
}
@@ -47,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;
@@ -63,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)) {
@@ -113,36 +133,52 @@ public class DeathListener implements Listener {
}
// Suppress the normal Minecraft death message because this death is being
// handled as a bounty claim.
event.deathMessage(null);
final String killerDisplay = formatLuckPermsDisplay(killer);
final String victimDisplay = formatLuckPermsDisplay(victim);
final boolean beheaded = maybeDropPlayerHead(event, killer, victim, bounty);
if (plugin.getConfig().getBoolean("bounty-claimed-announce", true)) {
final String killerDisplay = formatLuckPermsDisplay(killer);
final String victimDisplay = formatLuckPermsDisplay(victim);
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 + " &akilled " + victimDisplay + " &aand earned the &b" + awardedDisplay
+ " &abounty!";
} else {
message = killerDisplay + " &akilled " + victimDisplay + " &aand claimed the &b" + bountyDisplay
+ " &abounty for &b" + awardedDisplay + "&a!";
}
message = killerDisplay + "&r &abeheaded " + victimDisplay + "&r &afor &b" + bountyDisplay
+ " &bDiamonds&a!";
} else {
message = killerDisplay + " &akilled " + victimDisplay + " &aand claimed the &b" + bountyDisplay
+ " &abounty!";
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 &abounty!";
}
}
Bukkit.getOnlinePlayers().forEach(player -> UtilitiesOG.trueogMessage(player, message));
announceBountyClaim(message, killer, victim);
}
@@ -151,38 +187,136 @@ public class DeathListener implements Listener {
}
private String formatDiamonds(double diamonds) {
private void announceBountyClaim(String message, Player killer, Player victim) {
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();
player.spawnParticle(fireworkParticle, killer.getLocation().add(0D, 1D, 0D), 40, 0.6D, 0.8D, 0.6D, 0.01D);
player.spawnParticle(fireworkParticle, victim.getLocation().add(0D, 1D, 0D), 40, 0.6D, 0.8D, 0.6D, 0.01D);
player.spawnParticle(Particle.TOTEM, killer.getLocation().add(0D, 1D, 0D), 20, 0.5D, 0.7D, 0.5D, 0.01D);
});
}
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 {
return diamondBankAPI.shardsToDiamonds(diamondBankAPI.diamondsToShards(diamonds));
return Particle.valueOf("FIREWORK");
} catch (RuntimeException runtimeException) {
} catch (IllegalArgumentException exception) {
return String.valueOf(diamonds);
return Particle.FIREWORKS_SPARK;
}
}
private String formatDiamonds(double diamonds) {
return plugin.getBountyHeadFormatter().formatDiamonds(diamonds);
}
private String formatLuckPermsDisplay(Player player) {
final String playerName = player.getName();
final String prefix = stripLeadingReset(stripTrailingReset(getLuckPermsPrefixLegacy(player)));
final String suffix = stripLeadingReset(stripTrailingReset(getLuckPermsSuffixLegacy(player)));
final String leadingColorCodes = extractLeadingColorCodes(prefix);
if (prefix.isBlank()) {
final StringBuilder out = new StringBuilder();
if (!prefix.isBlank()) {
return player.getName();
out.append(prefix).append(" ");
}
if (leadingColorCodes.isBlank()) {
return prefix + " " + player.getName();
out.append(playerName);
} else {
out.append(leadingColorCodes).append(playerName);
}
return prefix + " " + leadingColorCodes + player.getName();
if (!suffix.isBlank()) {
out.append(" ").append(suffix);
}
return StringUtils.trim(out.toString()).replaceAll("\\s*>$", "");
}
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)));
if (ThreadLocalRandom.current().nextDouble(100D) >= dropChance) {
return false;
}
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());
plugin.getBountyHeadFormatter().applyHeadMeta(headMeta, headData, profile, plugin.getBountyHeadMetadata());
head.setItemMeta(headMeta);
event.getDrops().add(head);
return true;
}
private String getLuckPermsSuffixLegacy(Player player) {
if (luckPerms == null) {
return "";
}
final User user = luckPerms.getUserManager().getUser(player.getUniqueId());
if (user == null) {
return "";
}
final CachedMetaData meta = user.getCachedData().getMetaData();
final String suffix = meta.getSuffix();
if (suffix == null || suffix.isBlank()) {
return "";
}
return StringUtils.trim(suffix).replace('§', '&');
}
@@ -262,8 +396,8 @@ public class DeathListener implements Listener {
}
return out.toString();
return StringUtils.trim(out.toString()).replaceAll("\\s*>$", "");
}
}
}
@@ -0,0 +1,78 @@
package com.tcoded.playerbountiesplus.listener;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.inventory.InventoryView;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import net.trueog.utilitiesog.UtilitiesOG;
public class GuiProtectionListener implements Listener {
private final PlayerBountiesOG plugin;
public GuiProtectionListener(PlayerBountiesOG plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true)
public void onInventoryClick(InventoryClickEvent event) {
if (!isMainBountyGui(event.getView())) {
return;
}
if (event.getRawSlot() < event.getView().getTopInventory().getSize()) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onInventoryDrag(InventoryDragEvent event) {
if (!isMainBountyGui(event.getView())) {
return;
}
final int topInventorySize = event.getView().getTopInventory().getSize();
for (int rawSlot : event.getRawSlots()) {
if (rawSlot < topInventorySize) {
event.setCancelled(true);
return;
}
}
}
private boolean isMainBountyGui(InventoryView view) {
final HumanEntity player = view.getPlayer();
if (player == null) {
return false;
}
return UtilitiesOG.stripFormatting(view.title().examinableName())
.equals(UtilitiesOG.stripFormatting(plugin.getLang().getColored("gui.main.title")));
}
}
@@ -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;
}
}
@@ -10,6 +10,7 @@ import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
@@ -40,7 +41,7 @@ public class LangUtil {
for (SupportedLang lang : SupportedLang.values()) {
if (lang.name().equalsIgnoreCase(name)) {
if (StringUtils.equalsIgnoreCase(lang.name(), name)) {
return lang;
@@ -54,7 +55,7 @@ public class LangUtil {
public String fileName() {
return this.name().toLowerCase(Locale.ROOT) + ".yml";
return StringUtils.lowerCase(this.name(), Locale.ROOT) + ".yml";
}
@@ -66,7 +67,7 @@ public class LangUtil {
this.logger = plugin.getLogger();
final SupportedLang resolvedLang = resolveSupportedLang(configuredLang);
this.lang = resolvedLang.name().toLowerCase(Locale.ROOT);
this.lang = StringUtils.lowerCase(resolvedLang.name(), Locale.ROOT);
final String englishFilePath = "lang/" + SupportedLang.EN_US.fileName();
final String langFilePath = "lang/" + resolvedLang.fileName();
@@ -102,7 +103,7 @@ public class LangUtil {
private SupportedLang resolveSupportedLang(String configuredLang) {
final String normalized = configuredLang == null ? DEFAULT_LANG_CODE
: configuredLang.trim().toUpperCase(Locale.ROOT);
: StringUtils.upperCase(StringUtils.trim(configuredLang), Locale.ROOT);
final SupportedLang supportedLang = SupportedLang.find(normalized);
@@ -217,4 +218,4 @@ public class LangUtil {
}
}
}
+28 -11
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,32 +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
# If we should announce that the bounty was placed
# 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
# 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
bounty-take-from-victim: false
# 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:
+13 -4
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!"
@@ -47,8 +48,17 @@ command:
not-enough-money: "&cYou don't have enough money for that!"
internal-invalid-value: "&cAn invalid value was reached during internal processing. Please report this to an administrator!"
success: "&cYou placed a bounty of {bounty} on {target}'s head!"
announce: "\nA bounty of {bounty} was placed on {target}'s head by {player}!{extra}\n"
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 -13
View File
@@ -4,36 +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
- 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