Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/main/java/com/yourname/sellplugin/SellPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.yourname.sellplugin.command.SellCommand;
import com.yourname.sellplugin.command.SellMultiCommand;
import com.yourname.sellplugin.command.FastSellAllCommand;
import com.yourname.sellplugin.command.ShowWorthCommand;
import com.yourname.sellplugin.command.TopSellCommand;
import com.yourname.sellplugin.command.WorthCommand;
import com.yourname.sellplugin.economy.EconomyManager;
Expand All @@ -15,6 +16,7 @@
import com.yourname.sellplugin.manager.MultiplierManager;
import com.yourname.sellplugin.manager.PriceManager;
import com.yourname.sellplugin.manager.SellManager;
import com.yourname.sellplugin.manager.WorthVisibilityManager;
import org.bukkit.plugin.java.JavaPlugin;

public class SellPlugin extends JavaPlugin {
Expand All @@ -25,6 +27,7 @@ public class SellPlugin extends JavaPlugin {
private MultiplierManager multiplierManager;
private DailyBonusManager dailyBonusManager;
private SellManager sellManager;
private WorthVisibilityManager worthVisibilityManager;
private WorthPacketListener worthPacketListener;

@Override
Expand All @@ -38,6 +41,7 @@ public void onEnable() {
multiplierManager = new MultiplierManager(this);
dailyBonusManager = new DailyBonusManager(this);
sellManager = new SellManager(this);
worthVisibilityManager = new WorthVisibilityManager(this);

economyManager = new EconomyManager(this);
if (!economyManager.setupEconomy()) {
Expand All @@ -52,6 +56,9 @@ public void onEnable() {
getCommand("topsell").setExecutor(new TopSellCommand(this));
getCommand("sellmulti").setExecutor(new SellMultiCommand(this));
getCommand("sellworth").setExecutor(new WorthCommand(this));
ShowWorthCommand showWorthCommand = new ShowWorthCommand(this);
getCommand("showworth").setExecutor(showWorthCommand);
getCommand("showworth").setTabCompleter(showWorthCommand);
getServer().getPluginManager().registerEvents(new GUIListener(this), this);
getServer().getPluginManager().registerEvents(new WorthRefreshListener(this), this);

Expand All @@ -66,6 +73,9 @@ public void onDisable() {
if (multiplierManager != null) {
multiplierManager.saveAll();
}
if (worthVisibilityManager != null) {
worthVisibilityManager.saveNow();
}
if (worthPacketListener != null) {
worthPacketListener.unregister();
}
Expand All @@ -78,4 +88,5 @@ public void onDisable() {
public MultiplierManager getMultiplierManager() { return multiplierManager; }
public DailyBonusManager getDailyBonusManager() { return dailyBonusManager; }
public SellManager getSellManager() { return sellManager; }
public WorthVisibilityManager getWorthVisibilityManager() { return worthVisibilityManager; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.yourname.sellplugin.command;

import com.yourname.sellplugin.SellPlugin;
import com.yourname.sellplugin.util.Scheduler;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* {@code /showworth [true|false]} — lets a player show or hide the worth
* tooltip for themselves. With no argument it toggles the current state.
*/
public class ShowWorthCommand implements CommandExecutor, TabCompleter {

private final SellPlugin plugin;

public ShowWorthCommand(SellPlugin plugin) {
this.plugin = plugin;
}

@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(plugin.getConfigManager().getText(
"player-only-command", "&cOnly players can use this command."));
return true;
}

if (!player.hasPermission("sellplugin.use")) {
player.sendMessage(plugin.getConfigManager().getMessage("no-permission"));
return true;
}

boolean nowVisible;
if (args.length == 0) {
nowVisible = plugin.getWorthVisibilityManager().toggle(player.getUniqueId());
} else {
Boolean parsed = parseBoolean(args[0]);
if (parsed == null) {
player.sendMessage(plugin.getConfigManager().getText(
"showworth-usage", "&cUsage: /showworth [true|false]"));
return true;
}
nowVisible = parsed;
plugin.getWorthVisibilityManager().setVisible(player.getUniqueId(), nowVisible);
}

String key = nowVisible ? "showworth-enabled" : "showworth-disabled";
String def = nowVisible
? "&aItem worth is now &lshown&r&a in your inventory."
: "&eItem worth is now &lhidden&r&e in your inventory.";
player.sendMessage(plugin.getConfigManager().getText(key, def));

// Resend the inventory so the change is reflected immediately. Runs on
// the player's own region thread for Folia compatibility.
Scheduler.runEntityLater(plugin, player, () -> {
if (player.isOnline()) {
player.updateInventory();
}
}, 1L);
return true;
}

private Boolean parseBoolean(String arg) {
String a = arg.toLowerCase();
return switch (a) {
case "true", "on", "show", "yes", "enable", "enabled" -> Boolean.TRUE;
case "false", "off", "hide", "no", "disable", "disabled" -> Boolean.FALSE;
default -> null;
};
}

@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) {
return Stream.of("true", "false")
.filter(s -> s.startsWith(args[0].toLowerCase()))
.collect(Collectors.toList());
}
return List.of();
}
}
6 changes: 4 additions & 2 deletions src/main/java/com/yourname/sellplugin/gui/GUIListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.yourname.sellplugin.SellPlugin;
import com.yourname.sellplugin.manager.SellManager;
import com.yourname.sellplugin.util.Scheduler;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
Expand Down Expand Up @@ -89,8 +90,9 @@ public void onClick(InventoryClickEvent e) {
}

// Slots 0-44: allow item placement / removal
// After any click, schedule a sell button refresh
plugin.getServer().getScheduler().runTaskLater(plugin, shopGUI::refreshSellButton, 1L);
// After any click, schedule a sell button refresh (on the
// player's own region thread for Folia compatibility).
Scheduler.runEntityLater(plugin, player, shopGUI::refreshSellButton, 1L);
return;
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/yourname/sellplugin/gui/TopSellGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.yourname.sellplugin.manager.ConfigManager;
import com.yourname.sellplugin.manager.MultiplierManager.LeaderboardEntry;
import com.yourname.sellplugin.util.NumberFormatter;
import com.yourname.sellplugin.util.Scheduler;
import com.yourname.sellplugin.util.SmallCaps;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
Expand Down Expand Up @@ -89,7 +90,8 @@ private void populate() {
int rank = i + 1;
// Schedule each skull with a small staggered delay to avoid any
// potential server-side profile look-up spikes (2 ticks apart).
Bukkit.getScheduler().runTaskLater(plugin, () -> {
// Runs on the viewer's region thread so it is Folia-safe.
Scheduler.runEntityLater(plugin, viewer, () -> {
if (viewer.isOnline()) {
inv.setItem(slot, buildEntryHead(entry, rank));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ public void register() {

@Override
public void onPacketSending(PacketEvent event) {
if (!WorthPacketListener.this.plugin.getConfigManager().isWorthEnabled()) return;
if (!shouldDecorate(event.getPlayer())) return;
Player viewer = event.getPlayer();
if (viewer == null) return;
if (!WorthPacketListener.this.plugin.getWorthVisibilityManager()
.isVisible(viewer.getUniqueId())) return;
if (!shouldDecorate(viewer)) return;

if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) {
if (event.getPacket().getItemModifier().size() <= 0) return;
Expand Down Expand Up @@ -143,7 +146,7 @@ private ItemStack addWorthLore(Player player, ItemStack original) {
removeWorthLines(lore);

if (worth > 0) {
if (!lore.isEmpty()) {
if (!lore.isEmpty() && !isBlank(lore.get(lore.size() - 1))) {
lore.add("");
}
lore.add(WORTH_MARKER + plugin.getConfigManager().getWorthFormat()
Expand Down Expand Up @@ -177,7 +180,7 @@ private ItemStack stripWorthLore(ItemStack item) {

private boolean loreHasWorthLine(List<String> lore) {
for (String line : lore) {
if (line != null && line.startsWith(WORTH_MARKER)) return true;
if (line != null && line.contains(WORTH_MARKER)) return true;
}
return false;
}
Expand All @@ -190,17 +193,38 @@ private boolean removeWorthLines(List<String> lore) {
boolean changed = false;
for (int i = lore.size() - 1; i >= 0; i--) {
String line = lore.get(i);
if (line == null || !line.startsWith(WORTH_MARKER)) continue;
// Use contains() rather than startsWith(): when a creative client
// echoes our lore back it can arrive with an extra leading colour
// code (e.g. "§f") prepended, which would defeat a prefix match and
// let the line bake in / duplicate.
if (line == null || !line.contains(WORTH_MARKER)) continue;
lore.remove(i);
changed = true;
// Drop the blank separator we added directly before the worth line.
if (i - 1 >= 0 && lore.get(i - 1).isEmpty()) {
if (i - 1 >= 0 && isBlank(lore.get(i - 1))) {
lore.remove(i - 1);
}
}
return changed;
}

/**
* Treats a line as blank if, after stripping any formatting codes, nothing
* printable remains. Round-tripped separators can come back as "§f" etc.
*/
private boolean isBlank(String line) {
if (line == null || line.isEmpty()) return true;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == SECTION) {
i++; // skip the code character that follows the section sign
continue;
}
if (!Character.isWhitespace(c)) return false;
}
return true;
}

private boolean shouldDecorate(Player player) {
Inventory topInventory = player.getOpenInventory().getTopInventory();
InventoryHolder holder = topInventory.getHolder();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.yourname.sellplugin.listener;

import com.yourname.sellplugin.SellPlugin;
import com.yourname.sellplugin.util.Scheduler;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
Expand Down Expand Up @@ -47,8 +48,9 @@ public void onJoin(PlayerJoinEvent event) {

private void refresh(Player player) {
if (!plugin.getConfigManager().isWorthEnabled()) return;
// Run next tick so the inventory reflects the change that triggered us.
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
// Run next tick, on the player's own region thread (Folia-safe), so the
// inventory reflects the change that triggered us.
Scheduler.runEntityLater(plugin, player, () -> {
if (player.isOnline()) {
player.updateInventory();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.yourname.sellplugin.manager;

import com.yourname.sellplugin.SellPlugin;
import com.yourname.sellplugin.util.Scheduler;
import org.bukkit.configuration.file.YamlConfiguration;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
* Tracks which players have chosen to hide the cosmetic worth tooltip
* ({@code /showworth}). Worth is shown by default (subject to the global
* {@code worth.enabled} config), so we only need to persist the set of players
* who have opted <em>out</em>.
*
* <p>Reads happen from the ProtocolLib packet thread, so the backing set is
* concurrent; writes to disk are pushed off-thread via the async scheduler so
* this is safe on Folia as well as Paper.
*/
public class WorthVisibilityManager {

private final SellPlugin plugin;
private final File file;

/** UUIDs of players who have hidden the worth tooltip. */
private final Set<UUID> hidden = ConcurrentHashMap.newKeySet();

public WorthVisibilityManager(SellPlugin plugin) {
this.plugin = plugin;
this.file = new File(plugin.getDataFolder(), "worth-visibility.yml");
load();
}

private void load() {
if (!file.exists()) return;
YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
for (String raw : config.getStringList("hidden")) {
try {
hidden.add(UUID.fromString(raw));
} catch (IllegalArgumentException ignored) {
// skip malformed UUID entries
}
}
}

/** Persists the current set to disk on the calling thread. */
public void saveNow() {
YamlConfiguration config = new YamlConfiguration();
List<String> list = new ArrayList<>(hidden.size());
for (UUID uuid : hidden) {
list.add(uuid.toString());
}
config.set("hidden", list);
try {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
config.save(file);
} catch (IOException e) {
plugin.getLogger().severe("Failed to save worth-visibility.yml: " + e.getMessage());
}
}

private void saveAsync() {
Scheduler.runAsync(plugin, this::saveNow);
}

/** @return {@code true} if the player has hidden the worth tooltip. */
public boolean isHidden(UUID uuid) {
return hidden.contains(uuid);
}

/** @return {@code true} if the worth tooltip should be shown to this player. */
public boolean isVisible(UUID uuid) {
return plugin.getConfigManager().isWorthEnabled() && !hidden.contains(uuid);
}

/**
* Sets whether the worth tooltip is shown for {@code uuid}.
*
* @return {@code true} if this changed the stored state.
*/
public boolean setVisible(UUID uuid, boolean visible) {
boolean changed = visible ? hidden.remove(uuid) : hidden.add(uuid);
if (changed) {
saveAsync();
}
return changed;
}

/**
* Flips the current preference for {@code uuid}.
*
* @return the new visibility state ({@code true} = now shown).
*/
public boolean toggle(UUID uuid) {
boolean nowVisible = hidden.contains(uuid); // was hidden -> becomes visible
setVisible(uuid, nowVisible);
return nowVisible;
}
}
Loading
Loading