Compare commits

..
Author SHA1 Message Date
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
22 changed files with 1128 additions and 107 deletions

No files matched your search

+5 -2
View File
@@ -5,10 +5,13 @@ PlayerBounties-OG is a hard fork of [PlayerBountiesPlus](https://github.com/Tech
## 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://github.com/true-og/CoreProtect-OG) for player head rollback injection.
- Integrates [DiamondBank-OG](https://github.com/true-og/DiamondBank-OG) for currency.
- Integrates [LuckPerms](https://github.com/true-og/LuckPerms) for player name and rank formatting.
- Integrates [SimpleClans](https://github.com/true-og/SimpleClans) for team detection.
## Commands
- `/bounty set <player> <amount>` – set a bounty on a player
+2
View File
@@ -66,6 +66,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 +74,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")) {
@@ -16,9 +16,12 @@ 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.BountyHeadListener;
import com.tcoded.playerbountiesplus.listener.DeathListener;
import com.tcoded.playerbountiesplus.listener.GuiProtectionListener;
import com.tcoded.playerbountiesplus.manager.BountyDataManager;
import com.tcoded.playerbountiesplus.util.LangUtil;
@@ -59,6 +62,9 @@ public final class PlayerBountiesOG extends JavaPlugin {
// LuckPerms API hook.
private LuckPerms luckPerms;
// CoreProtect API hook.
private CoreProtectHook coreProtectHook;
public PlayerBountiesOG() {
instance = this;
@@ -86,6 +92,14 @@ public final class PlayerBountiesOG extends JavaPlugin {
}
this.coreProtectHook = new CoreProtectHook(this);
if (!this.coreProtectHook.isEnabled()) {
getLogger().warning(
"CoreProtect API v11+ not found - custom bounty head metadata may not rollback correctly.");
}
// Initialize the DiamondBank-OG API.
final RegisteredServiceProvider<DiamondBankAPIJava> provider = getServer().getServicesManager()
.getRegistration(DiamondBankAPIJava.class);
@@ -143,6 +157,9 @@ public final class PlayerBountiesOG extends JavaPlugin {
// Listeners.
this.getServer().getPluginManager().registerEvents(new DeathListener(this, this.diamondBankAPI, this.luckPerms),
this);
this.getServer().getPluginManager().registerEvents(
new BountyHeadListener(this, this.diamondBankAPI, this.luckPerms, this.coreProtectHook), this);
this.getServer().getPluginManager().registerEvents(new GuiProtectionListener(this), this);
final List<Plugin> plugins = ImmutableList.copyOf(this.getServer().getPluginManager().getPlugins());
findPluginWithQuery(plugins, "team");
@@ -201,6 +218,18 @@ public final class PlayerBountiesOG extends JavaPlugin {
}
public LuckPerms getLuckPerms() {
return this.luckPerms;
}
public CoreProtectHook getCoreProtectHook() {
return this.coreProtectHook;
}
// Utilities.
private static void findPluginWithQuery(List<Plugin> plugins, String pluginNameQuery, String... excludeStrings) {
@@ -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;
@@ -36,27 +37,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" -> {
@@ -76,7 +80,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".
@@ -97,18 +100,21 @@ 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());
.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 "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();
@@ -121,4 +127,4 @@ public class BountyCommand implements CommandExecutor, TabCompleter {
}
}
}
@@ -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);
@@ -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());
}
@@ -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)
@@ -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)
@@ -5,6 +5,7 @@ import java.util.List;
import java.util.UUID;
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;
@@ -109,7 +110,8 @@ public class BountyCheckCmd {
// 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());
}
@@ -5,10 +5,12 @@ 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;
@@ -16,6 +18,10 @@ import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.event.BountySetEvent;
import com.tcoded.playerbountiesplus.manager.BountyDataManager;
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 {
@@ -65,7 +71,7 @@ public class BountySetCmd {
final double parsedAmount;
try {
parsedAmount = Integer.parseInt(args[2]);
parsedAmount = parseAmount(args[2]);
} catch (NumberFormatException numberFormatException) {
@@ -215,47 +221,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 +265,148 @@ 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);
if (provider == null) {
return null;
}
return 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();
}
if (leadingColorCodes.isBlank()) {
return prefix + " " + player.getName();
}
return prefix + " " + leadingColorCodes + player.getName();
}
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 +414,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) {
@@ -3,9 +3,11 @@ package com.tcoded.playerbountiesplus.command.bounty;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -14,6 +16,9 @@ import org.bukkit.entity.Player;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.gui.MainBountyGui;
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;
@@ -25,7 +30,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;
@@ -62,7 +68,9 @@ public class BountyTopCmd {
}
entries.add(new MainBountyGui.BountyGuiEntry(entry.getKey(), targetName, entry.getValue()));
final String displayName = resolveLuckPermsDisplayName(plugin.getLuckPerms(), entry.getKey(), targetName);
entries.add(new MainBountyGui.BountyGuiEntry(entry.getKey(), targetName, displayName, entry.getValue()));
}
@@ -70,6 +78,92 @@ public class BountyTopCmd {
}
private static String resolveLuckPermsDisplayName(LuckPerms luckPerms, UUID playerId, String playerName) {
final String safeName = StringUtils.defaultIfBlank(playerName, "Unknown Player");
if (luckPerms == null) {
return "&f" + safeName;
}
final User user = luckPerms.getUserManager().getUser(playerId);
if (user == null) {
return "&f" + safeName;
}
final CachedMetaData meta = user.getCachedData().getMetaData();
final String prefix = StringUtils.trim(StringUtils.defaultString(meta.getPrefix()).replace('§', '&'));
final String suffix = StringUtils.trim(StringUtils.defaultString(meta.getSuffix()).replace('§', '&'));
final boolean metaContainsName = containsVisibleName(prefix, safeName) || containsVisibleName(suffix, safeName);
final StringBuilder out = new StringBuilder();
if (!prefix.isBlank()) {
out.append(prefix).append(' ');
}
if (!metaContainsName) {
out.append(safeName);
}
if (!suffix.isBlank()) {
out.append(' ').append(suffix);
}
return sanitizeDisplay(out.toString(), safeName);
}
private static String sanitizeDisplay(String formatted, String fallbackName) {
final String cleaned = StringUtils.trimToEmpty(formatted).replaceAll("(?i)(?:\\s*(?:<reset>|[&§]r))+$", "")
.replaceAll("\\s+>", " >").replaceAll("\\s*>$", "");
if (cleaned.isBlank()) {
return fallbackName;
}
return cleaned;
}
private static boolean containsVisibleName(String text, String playerName) {
if (text == null || playerName == null || playerName.isBlank()) {
return false;
}
final String normalizedText = stripFormatting(text).toLowerCase(Locale.ROOT);
final String normalizedName = playerName.toLowerCase(Locale.ROOT);
return normalizedText.contains(normalizedName);
}
private static String stripFormatting(String input) {
if (input == null || input.isBlank()) {
return "";
}
return input.replaceAll("(?i)[&§][0-9A-FK-ORX]", "").replaceAll("<[^>]+>", "").trim();
}
private static void sendTextTopList(PlayerBountiesOG plugin, DiamondBankAPIJava diamondBankAPI,
CommandSender sender)
{
@@ -102,9 +196,13 @@ public class BountyTopCmd {
.shardsToDiamonds(diamondBankAPI.diamondsToShards(entry.getValue()));
strb.append("&7 - ");
strb.append(targetName);
strb.append(": ");
final String displayName = resolveLuckPermsDisplayName(plugin.getLuckPerms(), entry.getKey(),
targetName);
strb.append(displayName);
strb.append(": &b");
strb.append(formattedBounty);
strb.append(" &bDiamonds");
strb.append('\n');
}
@@ -116,7 +214,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)) {
@@ -130,4 +228,4 @@ public class BountyTopCmd {
}
}
}
@@ -6,7 +6,6 @@ import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import org.bukkit.Material;
import org.bukkit.entity.Player;
@@ -28,8 +27,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 +73,9 @@ public class MainBountyGui extends GUIBase {
if (sortedBounties.isEmpty()) {
addItem(22,
createStaticItem(Material.WRITABLE_BOOK, "&e&lNo Active Bounties",
createStaticItem(Material.WRITABLE_BOOK, "&6&lNo Active Bounties",
List.of("&7Nobody currently has a bounty.", "&7Place one with your bounty command to get",
"&7this leaderboard moving again.")));
"&7this leaderboard populated again.")));
return;
}
@@ -145,10 +142,8 @@ public class MainBountyGui extends GUIBase {
}
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("&6Active bounties: &c" + totalBounties)));
if (currentPage < maxPage) {
@@ -247,23 +242,21 @@ public class MainBountyGui extends GUIBase {
private GUIItem createBountyItem(BountyGuiEntry entry, int rank) {
final String targetName = safeName(entry.targetName());
final String displayName = safeDisplayName(entry.displayName());
final ArrayList<String> lore = new ArrayList<>();
lore.add("&7Rank: &f#" + rank);
lore.add("&7Target: &f" + targetName);
lore.add("&7Bounty: &6" + formatBounty(entry.bountyDiamonds()));
lore.add("&eBounty: &b" + formatBounty(entry.bountyDiamonds()));
lore.add("");
lore.add("&8Highest bounty sits at the top.");
lore.add("&6Claiming the bounty will give you a 50% 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);
}
@@ -278,7 +271,7 @@ public class MainBountyGui extends GUIBase {
final long shards = diamondBankAPI.diamondsToShards(bountyDiamonds);
return diamondBankAPI.shardsToDiamonds(shards);
return diamondBankAPI.shardsToDiamonds(shards) + " Diamonds";
}
@@ -345,14 +338,19 @@ public class MainBountyGui extends GUIBase {
}
private boolean isUsableHeadName(String name) {
private String safeDisplayName(String displayName) {
return name != null && !name.isBlank() && !"Unknown Player".equals(name)
&& MINECRAFT_USERNAME_PATTERN.matcher(name).matches();
if (displayName == null || displayName.isBlank()) {
return "&fUnknown Player";
}
return displayName;
}
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,115 @@
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 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");
if (!(coreProtectPlugin instanceof CoreProtect coreProtect)) {
return null;
}
final CoreProtectAPI api = coreProtect.getAPI();
if (api == null || !api.isEnabled() || api.APIVersion() < 11) {
return null;
}
return api;
}
}
@@ -0,0 +1,364 @@
package com.tcoded.playerbountiesplus.listener;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.block.BlockState;
import org.bukkit.block.Skull;
import org.bukkit.entity.Player;
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 org.bukkit.persistence.PersistentDataType;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.hook.logging.CoreProtectHook;
import net.kyori.adventure.text.TextComponent;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import net.trueog.diamondbankog.api.DiamondBankAPIJava;
import net.trueog.utilitiesog.UtilitiesOG;
public class BountyHeadListener implements Listener {
private final DiamondBankAPIJava diamondBankAPI;
private final NamespacedKey targetKey;
private final NamespacedKey targetUuidKey;
private final NamespacedKey amountKey;
private final LuckPerms luckPerms;
private final CoreProtectHook coreProtectHook;
public BountyHeadListener(PlayerBountiesOG plugin, DiamondBankAPIJava diamondBankAPI, LuckPerms luckPerms,
CoreProtectHook coreProtectHook)
{
this.diamondBankAPI = diamondBankAPI;
this.targetKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_NAME_KEY);
this.targetUuidKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_UUID_KEY);
this.amountKey = new NamespacedKey(plugin, DeathListener.BOUNTY_HEAD_AMOUNT_KEY);
this.luckPerms = luckPerms;
this.coreProtectHook = coreProtectHook;
}
@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 String targetName = itemMeta.getPersistentDataContainer().get(targetKey, PersistentDataType.STRING);
final String targetUuid = itemMeta.getPersistentDataContainer().get(targetUuidKey, PersistentDataType.STRING);
final Double bountyAmount = itemMeta.getPersistentDataContainer().get(amountKey, PersistentDataType.DOUBLE);
if (targetName == null || targetUuid == null || bountyAmount == null) {
return;
}
final BlockState state = event.getBlockPlaced().getState();
if (!(state instanceof Skull skull)) {
return;
}
final PersistentDataContainer blockData = skull.getPersistentDataContainer();
blockData.set(targetKey, PersistentDataType.STRING, targetName);
blockData.set(targetUuidKey, PersistentDataType.STRING, targetUuid);
blockData.set(amountKey, PersistentDataType.DOUBLE, bountyAmount);
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 String targetName = skull.getPersistentDataContainer().get(targetKey, PersistentDataType.STRING);
final String targetUuid = skull.getPersistentDataContainer().get(targetUuidKey, PersistentDataType.STRING);
final Double bountyAmount = skull.getPersistentDataContainer().get(amountKey, PersistentDataType.DOUBLE);
if (targetName == null || targetUuid == null || bountyAmount == null) {
return;
}
final ItemStack drop = new ItemStack(Material.PLAYER_HEAD, 1);
final SkullMeta meta = (SkullMeta) drop.getItemMeta();
meta.setPlayerProfile(skull.getPlayerProfile());
applyBountyData(meta, targetName, targetUuid, bountyAmount);
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;
}
final ItemStack item = event.getItem();
if (item != null && isHeadMaterial(item.getType())) {
sendBountyHeadActionbar(event.getPlayer(), item.getItemMeta());
}
if (event.getClickedBlock() == null || !isHeadMaterial(event.getClickedBlock().getType())) {
return;
}
final BlockState state = event.getClickedBlock().getState();
if (!(state instanceof Skull skull)) {
return;
}
sendBountyHeadActionbar(event.getPlayer(), skull.getPersistentDataContainer());
}
private void sendBountyHeadActionbar(Player player, ItemMeta itemMeta) {
if (itemMeta == null) {
return;
}
sendBountyHeadActionbar(player, itemMeta.getPersistentDataContainer());
}
private void sendBountyHeadActionbar(Player player, PersistentDataContainer dataContainer) {
final String targetName = dataContainer.get(targetKey, PersistentDataType.STRING);
final String targetUuid = dataContainer.get(targetUuidKey, PersistentDataType.STRING);
final Double bountyAmount = dataContainer.get(amountKey, PersistentDataType.DOUBLE);
if (targetName == null || targetUuid == null || bountyAmount == null) {
return;
}
final UUID uuid = parseUuid(targetUuid);
final String playerDisplay = formatLuckPermsDisplay(uuid, targetName);
final String diamonds = formatDiamonds(bountyAmount);
player.sendActionBar(
UtilitiesOG.trueogColorize("&cBounty Head: " + playerDisplay + " &7- &b" + diamonds + " &bDiamonds"));
}
private void applyBountyData(SkullMeta meta, String targetName, String targetUuid, double bountyAmount) {
meta.displayName(UtilitiesOG.trueogColorize("&c" + targetName + "'s Bounty Head"));
final UUID uuid = parseUuid(targetUuid);
final String playerDisplay = formatLuckPermsDisplay(uuid, targetName);
final List<TextComponent> lore = new ArrayList<>();
lore.add(UtilitiesOG.trueogColorize("&6Player: " + playerDisplay));
lore.add(UtilitiesOG.trueogColorize("&cBeheaded for: &b" + formatDiamonds(bountyAmount) + " &bDiamonds"));
meta.lore(lore);
meta.getPersistentDataContainer().set(targetKey, PersistentDataType.STRING, targetName);
meta.getPersistentDataContainer().set(targetUuidKey, PersistentDataType.STRING, targetUuid);
meta.getPersistentDataContainer().set(amountKey, PersistentDataType.DOUBLE, bountyAmount);
}
private UUID parseUuid(String uuidString) {
if (uuidString == null || uuidString.isBlank()) {
return null;
}
try {
return UUID.fromString(uuidString);
} catch (IllegalArgumentException ignored) {
return null;
}
}
private String formatLuckPermsDisplay(UUID targetUuid, String fallbackName) {
final String playerName = StringUtils.defaultIfBlank(fallbackName, "Unknown Player");
if (targetUuid == null || luckPerms == null) {
return "&f" + playerName;
}
final User user = luckPerms.getUserManager().getUser(targetUuid);
if (user == null) {
return "&f" + playerName;
}
final CachedMetaData meta = user.getCachedData().getMetaData();
final String prefix = StringUtils.trim(StringUtils.defaultString(meta.getPrefix()).replace('§', '&'));
final String suffix = StringUtils.trim(StringUtils.defaultString(meta.getSuffix()).replace('§', '&'));
final boolean metaContainsName = containsVisibleName(prefix, playerName)
|| containsVisibleName(suffix, playerName);
final StringBuilder out = new StringBuilder();
if (!prefix.isBlank()) {
out.append(prefix).append(' ');
}
if (!metaContainsName) {
out.append(playerName);
}
if (!suffix.isBlank()) {
out.append(' ').append(suffix);
}
return sanitizeDisplay(out.toString(), playerName);
}
private static String sanitizeDisplay(String formatted, String fallbackName) {
final String cleaned = StringUtils.trimToEmpty(formatted).replaceAll("(?i)(?:\\s*(?:<reset>|[&§]r))+$", "")
.replaceAll("\\s*>$", "");
if (cleaned.isBlank()) {
return fallbackName;
}
return cleaned;
}
private static boolean containsVisibleName(String text, String playerName) {
if (text == null || playerName == null || playerName.isBlank()) {
return false;
}
final String normalizedText = stripFormatting(text).toLowerCase(Locale.ROOT);
final String normalizedName = playerName.toLowerCase(Locale.ROOT);
return normalizedText.contains(normalizedName);
}
private static String stripFormatting(String input) {
if (input == null || input.isBlank()) {
return "";
}
return input.replaceAll("(?i)[&§][0-9A-FK-ORX]", "").replaceAll("<[^>]+>", "").trim();
}
private String formatDiamonds(double diamonds) {
try {
return diamondBankAPI.shardsToDiamonds(diamondBankAPI.diamondsToShards(diamonds));
} catch (RuntimeException runtimeException) {
return String.valueOf(diamonds);
}
}
private static boolean isHeadMaterial(Material material) {
return material == Material.PLAYER_HEAD || material == Material.PLAYER_WALL_HEAD;
}
}
@@ -1,20 +1,31 @@
package com.tcoded.playerbountiesplus.listener;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
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 org.bukkit.persistence.PersistentDataType;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.tcoded.playerbountiesplus.PlayerBountiesOG;
import com.tcoded.playerbountiesplus.event.BountyClaimEvent;
import com.tcoded.playerbountiesplus.hook.team.TeamHook;
import com.tcoded.playerbountiesplus.manager.BountyDataManager;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
@@ -23,6 +34,10 @@ 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";
private final PlayerBountiesOG plugin;
private final DiamondBankAPIJava diamondBankAPI;
private final LuckPerms luckPerms;
@@ -113,10 +128,26 @@ public class DeathListener implements Listener {
}
if (plugin.getConfig().getBoolean("bounty-claimed-announce", true)) {
// 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, victim, bounty);
if (beheaded) {
final String bountyDisplay = formatDiamonds(bounty);
final String beheadedMessage = killerDisplay + "&r &abeheaded " + victimDisplay + "&r &afor &b"
+ bountyDisplay + " &bDiamonds&a!";
Bukkit.getOnlinePlayers().forEach(player -> UtilitiesOG.trueogMessage(player, beheadedMessage));
playGlobalCelebrationEffects(killer, victim);
} else if (plugin.getConfig().getBoolean("bounty-claimed-announce", true)) {
final String killerDisplay = formatLuckPermsDisplay(killer);
final String victimDisplay = formatLuckPermsDisplay(victim);
final String bountyDisplay = formatDiamonds(bounty);
final String awardedDisplay = formatDiamonds(awardedAmount);
@@ -125,24 +156,25 @@ public class DeathListener implements Listener {
if (Double.compare(awardedAmount, bounty) == 0) {
message = killerDisplay + " &akilled " + victimDisplay + " &aand earned the &b" + awardedDisplay
+ " &abounty!";
message = killerDisplay + "&r &akilled " + victimDisplay + "&r &aand earned &b" + awardedDisplay
+ " &bDiamonds &afor the bounty!";
} else {
message = killerDisplay + " &akilled " + victimDisplay + " &aand claimed the &b" + bountyDisplay
+ " &abounty for &b" + awardedDisplay + "&a!";
message = killerDisplay + "&r &akilled " + victimDisplay + "&r &aand claimed &b" + bountyDisplay
+ " &bDiamonds &afor &b" + awardedDisplay + " &bDiamonds&a!";
}
} else {
message = killerDisplay + " &akilled " + victimDisplay + " &aand claimed the &b" + bountyDisplay
+ " &abounty!";
message = killerDisplay + "&r &akilled " + victimDisplay + "&r &aand claimed &b" + bountyDisplay
+ " &bDiamonds &abounty!";
}
Bukkit.getOnlinePlayers().forEach(player -> UtilitiesOG.trueogMessage(player, message));
playGlobalCelebrationEffects(killer, victim);
}
@@ -151,6 +183,35 @@ public class DeathListener implements Listener {
}
private void playGlobalCelebrationEffects(Player killer, Player victim) {
Bukkit.getOnlinePlayers().forEach(player -> {
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 Particle resolveFireworkParticle() {
try {
return Particle.valueOf("FIREWORK");
} catch (IllegalArgumentException exception) {
return Particle.FIREWORKS_SPARK;
}
}
private String formatDiamonds(double diamonds) {
try {
@@ -167,22 +228,99 @@ public class DeathListener implements Listener {
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 out.toString();
}
private boolean maybeDropPlayerHead(PlayerDeathEvent event, 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 PlayerProfile profile = Bukkit.createProfile(victim.getUniqueId());
headMeta.setPlayerProfile(profile);
headMeta.displayName(UtilitiesOG.trueogColorize("&c" + victim.getName() + "'s Bounty Head"));
final String bountyDisplay = formatDiamonds(bounty);
headMeta.lore(java.util.List.of(
Component.text("Target: ", NamedTextColor.GRAY)
.append(Component.text(victim.getName(), NamedTextColor.WHITE)),
Component.text("Beheaded for: ", NamedTextColor.GRAY)
.append(Component.text(bountyDisplay + " Diamonds", NamedTextColor.AQUA))));
headMeta.getPersistentDataContainer().set(new NamespacedKey(plugin, BOUNTY_HEAD_NAME_KEY),
PersistentDataType.STRING, victim.getName());
headMeta.getPersistentDataContainer().set(new NamespacedKey(plugin, BOUNTY_HEAD_UUID_KEY),
PersistentDataType.STRING, victim.getUniqueId().toString());
headMeta.getPersistentDataContainer().set(new NamespacedKey(plugin, BOUNTY_HEAD_AMOUNT_KEY),
PersistentDataType.DOUBLE, bounty);
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('§', '&');
}
@@ -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")));
}
}
@@ -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 {
}
}
}
+4 -1
View File
@@ -36,9 +36,12 @@ bounty-claim-multiplier: 1.0
# If we should announce that the bounty was claimed
bounty-claimed-announce: true
# Chance (0-100) that claiming a bounty drops the victim's player head.
bounty-head-drop-chance: 50.0
# If we should announce that the bounty was placed
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
bounty-take-from-victim: false
+1 -1
View File
@@ -47,7 +47,7 @@ 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})"
check:
no-player-specified: "&cYou need to specify a player! /bounty check <player>"
+1
View File
@@ -10,6 +10,7 @@ folia-supported: false
depend:
- DiamondBank-OG
softdepend:
- CoreProtect-OG
- SimpleClans
- LuckPerms