This commit is contained in:
rockyhawk64 2025-07-20 18:57:49 +10:00
parent 54be917af7
commit aecf37695c
34 changed files with 104 additions and 125 deletions

View File

@ -1,4 +1,4 @@
version: 4.0.2
version: 4.0.3
main: me.rockyhawk.commandpanels.CommandPanels
name: CommandPanels
author: RockyHawk

View File

@ -1,11 +1,12 @@
package me.rockyhawk.commandpanels.builder.inventory.items.itemcomponents;
import io.papermc.paper.datacomponent.DataComponentTypes;
import io.papermc.paper.datacomponent.item.PotionContents;
import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.builder.inventory.items.ItemComponent;
import me.rockyhawk.commandpanels.session.inventory.PanelItem;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.potion.PotionType;
public class PotionComponent implements ItemComponent {
@ -16,14 +17,11 @@ public class PotionComponent implements ItemComponent {
//if the item is a potion, give it an effect
String[] effectType = ctx.text.parseTextToString(player,item.potion()).split("\\s");
PotionMeta potionMeta = (PotionMeta) itemStack.getItemMeta();
assert potionMeta != null;
PotionType newData = PotionType.valueOf(effectType[0].toUpperCase());
//set meta
potionMeta.setBasePotionType(newData);
itemStack.setItemMeta(potionMeta);
itemStack.setData(DataComponentTypes.POTION_CONTENTS,
PotionContents.potionContents().addCustomEffect(newData.getPotionEffects().get(0)));
return itemStack;
}

View File

@ -13,73 +13,55 @@ import org.bukkit.profile.PlayerTextures;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.*;
public class CustomHeads {
// Static to remember saved heads across different instances
private static final Map<String, ItemStack> savedCustomHeads = new HashMap<>();
private static final Map<String, PlayerProfile> profileCache = new HashMap<>();
public ItemStack getCustomHead(String base64Texture) {
if (savedCustomHeads.containsKey(base64Texture)) {
return savedCustomHeads.get(base64Texture);
}
trimCacheIfNeeded();
String decodedTexture = extractSkinUrlFromBase64(base64Texture);
if (decodedTexture == null) return new ItemStack(Material.PLAYER_HEAD);
PlayerProfile profile = getOrCreateProfile(base64Texture);
if (profile == null) return new ItemStack(Material.PLAYER_HEAD);
ItemStack skull = new ItemStack(Material.PLAYER_HEAD, 1);
if (!(skull.getItemMeta() instanceof SkullMeta skullMeta)) return skull;
PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID());
PlayerTextures textures = profile.getTextures();
try {
textures.setSkin(new URL(decodedTexture));
} catch (MalformedURLException ignore) {}
profile.setTextures(textures);
skullMeta.setOwnerProfile(profile);
skull.setItemMeta(skullMeta);
savedCustomHeads.put(base64Texture, skull);
return skull;
return skull; // New item each time, only shares profile (skin)
}
public ItemStack getPlayerHead(String playerName) {
if (savedCustomHeads.containsKey(playerName)) {
return savedCustomHeads.get(playerName);
}
trimCacheIfNeeded();
PlayerProfile profile = profileCache.computeIfAbsent(playerName, key -> {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
return Bukkit.createPlayerProfile(offlinePlayer.getUniqueId());
});
ItemStack skull = new ItemStack(Material.PLAYER_HEAD, 1);
if (!(skull.getItemMeta() instanceof SkullMeta skullMeta)) return skull;
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
PlayerProfile profile = Bukkit.createPlayerProfile(offlinePlayer.getUniqueId());
skullMeta.setOwnerProfile(profile);
skull.setItemMeta(skullMeta);
savedCustomHeads.put(playerName, skull);
return skull;
}
private void trimCacheIfNeeded() {
int CACHE_LIMIT = 2000;
if (savedCustomHeads.size() <= CACHE_LIMIT) return;
Iterator<Map.Entry<String, ItemStack>> iterator = savedCustomHeads.entrySet().iterator();
while (iterator.hasNext() && savedCustomHeads.size() > CACHE_LIMIT) {
iterator.next();
iterator.remove();
}
private PlayerProfile getOrCreateProfile(String base64Texture) {
return profileCache.computeIfAbsent(base64Texture, key -> {
String skinUrl = extractSkinUrlFromBase64(base64Texture);
if (skinUrl == null) return null;
try {
PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID());
PlayerTextures textures = profile.getTextures();
textures.setSkin(new URL(skinUrl));
profile.setTextures(textures);
return profile;
} catch (MalformedURLException e) {
return null;
}
});
}
private String extractSkinUrlFromBase64(String base64Texture) {
@ -94,4 +76,4 @@ public class CustomHeads {
return null;
}
}
}
}

View File

@ -46,7 +46,7 @@ public class NameHandler {
String name = panelItem.displayName();
if (!name.isEmpty()) {
item.setData(DataComponentTypes.ITEM_NAME,
item.setData(DataComponentTypes.CUSTOM_NAME,
ctx.text.parseTextToComponent(player, name));
}

View File

@ -1,6 +1,7 @@
package me.rockyhawk.commandpanels.builder.logic;
import me.rockyhawk.commandpanels.Context;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@ -20,8 +21,15 @@ public class ComparisonNode implements ConditionNode {
@Override
public boolean evaluate(Player player, Context ctx) {
String parsedLeft = ctx.text.parseTextToString(player, left); // e.g., %player_balance% "600"
String parsedRight = ctx.text.parseTextToString(player, right); // Just in case right has variables
String parsedLeftRaw = ctx.text.parseTextToString(player, left); // e.g., %player_balance% "600"
String parsedRightRaw = ctx.text.parseTextToString(player, right);
/*
parseTextToString will parse colour and placeholders
After parsing strip colour, parsing and stripping will remove colour formatting
*/
String parsedLeft = LegacyComponentSerializer.legacySection().deserialize(parsedLeftRaw).content();
String parsedRight = LegacyComponentSerializer.legacySection().deserialize(parsedRightRaw).content();
switch (operator) {
case "$EQUALS":

View File

@ -66,7 +66,7 @@ public class TextFormatter {
return LegacyComponentSerializer.legacySection().serialize(component);
}
private String applyPlaceholders(Player player, String input) {
public String applyPlaceholders(Player player, String input) {
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
OfflinePlayer offp = Bukkit.getOfflinePlayer(player.getUniqueId());
return PlaceholderAPI.setPlaceholders(offp, input);
@ -76,31 +76,27 @@ public class TextFormatter {
private Component deserializeAppropriately(String input) {
try {
Component component;
if (containsLegacyCodes(input)) {
component = legacySerializer.deserialize(input.replaceAll("(?i)&([0-9a-fk-or])", "§$1"));
} else {
component = miniMessage.deserialize(input);
Component mmComponent = miniMessage.deserialize(input);
Component plain = Component.text(input);
// If MiniMessage result is just plain text, it did nothing useful. fallback to legacy
if (mmComponent.equals(plain)) {
return legacySerializer.deserialize(input.replaceAll("(?i)&([0-9a-fk-or])", "§$1"))
.decoration(TextDecoration.ITALIC, false);
}
// Apply non-italic to root only if not already specified
// By default Minecraft items are ITALIC
if (component.decoration(TextDecoration.ITALIC) == TextDecoration.State.NOT_SET) {
component = component.decoration(TextDecoration.ITALIC, false);
// Otherwise, MiniMessage worked. ensure non-italic if not explicitly set.
if (mmComponent.decoration(TextDecoration.ITALIC) == TextDecoration.State.NOT_SET) {
mmComponent = mmComponent.decoration(TextDecoration.ITALIC, false);
}
return component;
return mmComponent;
} catch (Exception e) {
// Fallback plain non-italic text
// Totally broken input, fallback to plain non-italic
return Component.text(input).decoration(TextDecoration.ITALIC, false);
}
}
// Check for legacy codes with regex
private boolean containsLegacyCodes(String input) {
return input.matches(".*&[0-9a-fk-or].*");
}
public TextComponent getTag() {
return tag;
}

View File

@ -21,8 +21,7 @@ public class SessionDataPlaceholder implements PlaceholderResolver {
if(session == null) return "no_session_found";
// Get the data from session
String data = session.getData(ctx.text.parseTextToString((Player) player, key));
return ctx.text.parseTextToString((Player) player, data);
return session.getData(key);
}
}

View File

@ -44,16 +44,17 @@ public class CommandRunner {
public void runCommand(Panel panel, Player player, String command) {
for (CommandTagResolver resolver : resolvers) {
command = ctx.text.parseTextToString(player, command.trim());
if (command.isEmpty()) return;
String[] parts = command.split("\\s+", 2); // Split into 2 parts: tag and rest
String tag = parts[0];
String args = (parts.length > 1) ? parts[1].trim() : "";
String argsParsed = ctx.text.parseTextToString(player, args);
if (resolver.isCorrectTag(tag)) {
resolver.handle(ctx, panel, player, args);
resolver.handle(ctx, panel, player, args, argsParsed);
return;
}
}

View File

@ -9,5 +9,5 @@ public interface CommandTagResolver {
* @return true if this tag handled the command and it shouldn't be dispatched further.
*/
boolean isCorrectTag(String tag);
void handle(Context ctx, Panel panel, Player player, String command);
void handle(Context ctx, Panel panel, Player player, String raw, String command);
}

View File

@ -31,24 +31,27 @@ public class RequirementRunner {
public boolean processRequirements(Panel panel, Player player, List<String> requirements) {
List<RequirementTagResolver> toExecute = new ArrayList<>();
List<String> argsList = new ArrayList<>();
List<String> argsParsedList = new ArrayList<>();
for (String requirement : requirements) {
requirement = ctx.text.parseTextToString(player, requirement.trim());
if (requirement.isEmpty()) continue;
String[] parts = requirement.split("\\s+", 2);
String tag = parts[0];
String args = (parts.length > 1) ? parts[1].trim() : "";
String argsParsed = ctx.text.parseTextToString(player, args);
boolean matched = false;
for (RequirementTagResolver resolver : resolvers) {
if (resolver.isCorrectTag(tag)) {
matched = true;
if (!resolver.check(ctx, panel, player, args)) {
if (!resolver.check(ctx, panel, player, args, argsParsed)) {
return false; // Fail early
}
toExecute.add(resolver);
argsList.add(args);
argsParsedList.add(argsParsed);
break;
}
}
@ -61,7 +64,7 @@ public class RequirementRunner {
// All passed, now execute
for (int i = 0; i < toExecute.size(); i++) {
toExecute.get(i).execute(ctx, panel, player, argsList.get(i));
toExecute.get(i).execute(ctx, panel, player, argsList.get(i), argsParsedList.get(i));
}
return true;

View File

@ -6,6 +6,6 @@ import org.bukkit.entity.Player;
public interface RequirementTagResolver {
boolean isCorrectTag(String tag);
boolean check(Context ctx, Panel panel, Player player, String args);
void execute(Context ctx, Panel panel, Player player, String args);
boolean check(Context ctx, Panel panel, Player player, String raw, String args);
void execute(Context ctx, Panel panel, Player player, String raw, String args);
}

View File

@ -13,7 +13,7 @@ public class DataTag implements RequirementTagResolver {
}
@Override
public boolean check(Context ctx, Panel panel, Player player, String args) {
public boolean check(Context ctx, Panel panel, Player player, String raw, String args) {
String[] split = args.trim().split("\\s");
if (split.length != 2) {
ctx.text.sendError(player, "Invalid data requirement. Use: [data] <key> <amount>");
@ -42,7 +42,7 @@ public class DataTag implements RequirementTagResolver {
}
@Override
public void execute(Context ctx, Panel panel, Player player, String args) {
public void execute(Context ctx, Panel panel, Player player, String raw, String args) {
String[] split = args.trim().split("\\s");
if (split.length != 2) return;

View File

@ -24,14 +24,14 @@ public class ItemTag implements RequirementTagResolver {
}
@Override
public boolean check(Context ctx, Panel panel, Player player, String args) {
public boolean check(Context ctx, Panel panel, Player player, String raw, String args) {
ParsedItemRequirement req = parseArgs(ctx, player, args);
if (req == null) return false;
return hasMatchingItems(ctx, player, req);
}
@Override
public void execute(Context ctx, Panel panel, Player player, String args) {
public void execute(Context ctx, Panel panel, Player player, String raw, String args) {
ParsedItemRequirement req = parseArgs(ctx, player, args);
if (req == null) return;
if(req.remove) removeMatchingItems(ctx, player, req);

View File

@ -33,7 +33,7 @@ public class VaultTag implements RequirementTagResolver {
}
@Override
public boolean check(Context ctx, Panel panel, Player player, String args) {
public boolean check(Context ctx, Panel panel, Player player, String raw, String args) {
if (economy == null) return false;
Double amount = parseAmount(ctx, player, args);
@ -41,7 +41,7 @@ public class VaultTag implements RequirementTagResolver {
}
@Override
public void execute(Context ctx, Panel panel, Player player, String args) {
public void execute(Context ctx, Panel panel, Player player, String raw, String args) {
if (economy == null) return;
Double amount = parseAmount(ctx, player, args);

View File

@ -13,7 +13,7 @@ public class XpTag implements RequirementTagResolver {
}
@Override
public boolean check(Context ctx, Panel panel, Player player, String args) {
public boolean check(Context ctx, Panel panel, Player player, String raw, String args) {
String[] split = args.trim().split("\\s");
if (split.length != 2) {
ctx.text.sendError(player, "Invalid XP requirement. Use: [xp] <levels|points> <amount>");
@ -41,7 +41,7 @@ public class XpTag implements RequirementTagResolver {
}
@Override
public void execute(Context ctx, Panel panel, Player player, String args) {
public void execute(Context ctx, Panel panel, Player player, String raw, String args) {
String[] split = args.trim().split("\\s");
if (split.length != 2) return;

View File

@ -14,7 +14,7 @@ public class ChatTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
Bukkit.getScheduler().runTask(ctx.plugin, () -> {
player.chat(command);
});

View File

@ -13,7 +13,7 @@ public class ClosePanelTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
player.closeInventory();
}
}

View File

@ -14,7 +14,7 @@ public class ConsoleCmdTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
Bukkit.getScheduler().runTask(ctx.plugin, () -> {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command);
});

View File

@ -13,9 +13,11 @@ public class DataTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
String playerName = player.getName();
String[] args = command.split("\\s");
// Use raw placeholder parsing
String[] args = ctx.text.applyPlaceholders(player, raw).split("\\s");
if (args.length < 1) return;

View File

@ -18,7 +18,7 @@ public class GiveTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
Bukkit.getScheduler().runTask(ctx.plugin, () -> {
// Example: [give] DIAMOND 3
// Basic give tag that gives normal items and drops them if inventory is full

View File

@ -21,7 +21,7 @@ public class ItemActionTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
try {
String[] args = ctx.text.parseTextToString(player, command).split("\\s+");
if (args.length < 2) {

View File

@ -14,7 +14,7 @@ public class MessageTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
Bukkit.getScheduler().runTask(ctx.plugin, () -> {
player.sendMessage(command);
});

View File

@ -14,7 +14,7 @@ public class OpenPanelTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
if (ctx.plugin.panels.get(command) == null) {
return;
}

View File

@ -15,7 +15,7 @@ public class PreviousPanelTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
// Open the previous panel
PanelSession session = ctx.session.getPlayerSession(player);
if(ctx.session.getPlayerSession(player) != null){

View File

@ -14,7 +14,7 @@ public class RefreshPanelTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
// Reopen the current panel to refresh it
panel.open(ctx, player, SessionManager.PanelOpenType.REFRESH);
}

View File

@ -15,7 +15,7 @@ public class ServerTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
// Remove the tag prefix and parse placeholders
String parsedCmd = ctx.text.parseTextToString(player, command);
String[] args = parsedCmd.split("\\s+");

View File

@ -13,8 +13,9 @@ public class SessionTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
String[] args = command.split("\\s");
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
// Use raw placeholder parsing
String[] args = ctx.text.applyPlaceholders(player, raw).split("\\s");
if (args.length < 1) return;

View File

@ -17,7 +17,7 @@ public class SoundTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
String[] args = ctx.text.parseTextToString(player, command).split("\\s+");
if (args.length == 0) {

View File

@ -16,7 +16,7 @@ public class StopSoundTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
String[] args = ctx.text.parseTextToString(player, command).split("\\s+");
if (args.length == 0) {

View File

@ -16,7 +16,7 @@ public class TeleportTag implements CommandTagResolver {
}
@Override
public void handle(Context ctx, Panel panel, Player player, String command) {
public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
String[] args = ctx.text.parseTextToString(player, command).split("\\s+");
float x, y, z, yaw = 0, pitch = 0;

View File

@ -55,15 +55,13 @@ public class PanelOpenCommand implements Listener {
}
// If there are args add data and open panel
// Clear data and use internal for EXTERNAL equivalent but no data flush after panel open
ctx.session.getPlayerSession(e.getPlayer()).clearData();
for (int i = 0; i < args.length; i++) {
String key = pnlCmdArgs[i];
String value = args[i];
ctx.session.getPlayerSession(e.getPlayer()).setData(key, value);
}
Bukkit.getScheduler().runTask(ctx.plugin, () -> {
panel.open(ctx, e.getPlayer(), SessionManager.PanelOpenType.CUSTOM);
panel.open(ctx, e.getPlayer(), SessionManager.PanelOpenType.EXTERNAL);
});
}

View File

@ -26,6 +26,8 @@ public class PanelSession {
}
public void setPanel(Panel panel) {
// First panel since session start
if(this.panel == null) return;
// Update previous panel if new panel is different
if(!panel.getName().equals(this.panel.getName()))
this.previous = this.panel;

View File

@ -32,16 +32,12 @@ public class SessionManager implements Listener {
panelSessions.compute(uuid, (key, session) -> {
// Start a new session no panel open, or switch panel
if (session == null || openType == PanelOpenType.EXTERNAL) {
if (session == null) {
session = new PanelSession(panel, player);
if(panelSnooper && panel != null) ctx.text.sendInfo(Bukkit.getConsoleSender(), String.format("%s opened %s, new session has been started.", player.getName(), panel.getName()));
} else if(openType == PanelOpenType.CUSTOM) {
} else {
session.setPanel(panel);
if(panelSnooper) ctx.text.sendInfo(Bukkit.getConsoleSender(), String.format("%s opened %s, new session has been started.", player.getName(), panel.getName()));
} else if(openType == PanelOpenType.INTERNAL){
session.setPanel(panel);
if(panelSnooper) ctx.text.sendInfo(Bukkit.getConsoleSender(), String.format("%s opened %s, continuing existing session.", player.getName(), panel.getName()));
}
if(panelSnooper && panel != null) ctx.text.sendInfo(Bukkit.getConsoleSender(), String.format("%s opened %s.", player.getName(), panel.getName()));
// Update data file when sessions are started
ctx.dataLoader.saveDataFileAsync();
@ -72,7 +68,6 @@ public class SessionManager implements Listener {
public enum PanelOpenType {
EXTERNAL, // Opened via external action
CUSTOM, // External but don't clear data
INTERNAL, // Opened via in-panel navigation
REFRESH // Internal and refresh only
}

View File

@ -1,5 +1,6 @@
package me.rockyhawk.commandpanels.session.inventory;
import io.papermc.paper.persistence.PersistentDataContainerView;
import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.builder.inventory.InventoryPanelBuilder;
import me.rockyhawk.commandpanels.builder.inventory.items.ItemBuilder;
@ -8,8 +9,6 @@ import me.rockyhawk.commandpanels.session.PanelSession;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.scheduler.BukkitRunnable;
@ -51,10 +50,7 @@ public class InventoryPanelUpdater implements PanelUpdater {
ItemStack item = inv.getItem(slot);
if (item == null || item.getType().isAir()) continue;
ItemMeta meta = item.getItemMeta();
if (meta == null) continue;
PersistentDataContainer container = meta.getPersistentDataContainer();
PersistentDataContainerView container = item.getPersistentDataContainer();
if (!container.has(itemIdKey, PersistentDataType.STRING) ||
container.has(fillItem, PersistentDataType.STRING)) continue;
String itemId = container.get(itemIdKey, PersistentDataType.STRING);
@ -76,9 +72,7 @@ public class InventoryPanelUpdater implements PanelUpdater {
ItemStack newItem = builder.buildItem(panel, panelItem);
// Update base item to original base item
ItemMeta newMeta = newItem.getItemMeta();
newMeta.getPersistentDataContainer().set(baseIdKey, PersistentDataType.STRING, baseItemId);
newItem.setItemMeta(newMeta);
newItem.editPersistentDataContainer(c -> c.set(baseIdKey, PersistentDataType.STRING, baseItemId));
inv.setItem(slot, newItem);
}