Compare commits

...

3 Commits

Author SHA1 Message Date
rockyhawk64
0c107e9c00 custom heads fixes 2025-10-15 00:13:28 +11:00
rockyhawk64
9d3c400acb queue implemented for custom heads 2025-10-14 23:39:50 +11:00
rockyhawk64
4408bacc7e fix commands running before panel opens 2025-10-14 17:46:17 +11:00
8 changed files with 124 additions and 32 deletions

View File

@ -1,5 +1,6 @@
package me.rockyhawk.commandpanels; package me.rockyhawk.commandpanels;
import me.rockyhawk.commandpanels.builder.inventory.items.utils.CustomHeads;
import me.rockyhawk.commandpanels.commands.MainCommand; import me.rockyhawk.commandpanels.commands.MainCommand;
import me.rockyhawk.commandpanels.formatter.Placeholders; import me.rockyhawk.commandpanels.formatter.Placeholders;
import me.rockyhawk.commandpanels.formatter.language.TextFormatter; import me.rockyhawk.commandpanels.formatter.language.TextFormatter;
@ -18,6 +19,7 @@ public class Context {
public PanelOpenCommand panelCommand; public PanelOpenCommand panelCommand;
public DataLoader dataLoader; public DataLoader dataLoader;
public GenerateManager generator; public GenerateManager generator;
public CustomHeads customHeads;
public Context(CommandPanels pl) { public Context(CommandPanels pl) {
plugin = pl; plugin = pl;
@ -30,6 +32,7 @@ public class Context {
panelCommand = new PanelOpenCommand(this); panelCommand = new PanelOpenCommand(this);
dataLoader = new DataLoader(this); dataLoader = new DataLoader(this);
generator = new GenerateManager(this); generator = new GenerateManager(this);
customHeads = new CustomHeads(this);
// Register plugin command // Register plugin command
plugin.registerCommand("panels", new MainCommand(this)); plugin.registerCommand("panels", new MainCommand(this));

View File

@ -18,13 +18,13 @@ public class HeadComponent implements MaterialComponent {
public ItemStack createItem(Context ctx, String head, Player player, PanelItem item) { public ItemStack createItem(Context ctx, String head, Player player, PanelItem item) {
try { try {
ItemStack s; ItemStack s;
CustomHeads customHeads = new CustomHeads(); String headName = ctx.text.parseTextToString(player, head);
if (ctx.text.parseTextToString(player,head).length() <= 16) { if (ctx.text.parseTextToString(player,head).length() <= 16) {
//if [head] username //if [head] username
s = customHeads.getPlayerHead(ctx.text.parseTextToString(player, head)); s = ctx.customHeads.getPlayerHeadSync(headName);
} else { } else {
//custom data [head] base64 //custom data [head] base64
s = customHeads.getCustomHead(ctx.text.parseTextToString(player, head)); s = ctx.customHeads.getCustomHead(headName);
} }
return s; return s;
} catch (Exception var32) { } catch (Exception var32) {

View File

@ -3,6 +3,7 @@ package me.rockyhawk.commandpanels.builder.inventory.items.utils;
import com.destroystokyo.paper.profile.PlayerProfile; import com.destroystokyo.paper.profile.PlayerProfile;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import me.rockyhawk.commandpanels.Context;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
@ -14,10 +15,34 @@ import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
public class CustomHeads { public class CustomHeads {
private static final Map<String, PlayerProfile> profileCache = new HashMap<>(); private final Context ctx;
private static final int MAX_CACHE_SIZE = 5000;
private static final Map<String, PlayerProfile> profileCache =
// Used to define a set limit for how long profileCache can get
Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, PlayerProfile> eldest) {
return size() > MAX_CACHE_SIZE;
}
});
private static final Queue<String> lookupQueue = new ConcurrentLinkedQueue<>();
private static boolean queueTaskRunning = false;
/**
* This class must have a single instance across the plugin
* so that heads are cached and the queue is utilised properly.s
*/
public CustomHeads(Context ctx) {
this.ctx = ctx;
}
// ===========================
// BASE64 (SYNC ONLY)
// ===========================
public ItemStack getCustomHead(String base64Texture) { public ItemStack getCustomHead(String base64Texture) {
PlayerProfile profile = getOrCreateProfile(base64Texture); PlayerProfile profile = getOrCreateProfile(base64Texture);
@ -32,22 +57,6 @@ public class CustomHeads {
return skull; // New item each time, only shares profile (skin) return skull; // New item each time, only shares profile (skin)
} }
public ItemStack getPlayerHead(String playerName) {
PlayerProfile profile = profileCache.computeIfAbsent(playerName, key -> {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
return offlinePlayer.getPlayerProfile();
});
ItemStack skull = new ItemStack(Material.PLAYER_HEAD, 1);
if (!(skull.getItemMeta() instanceof SkullMeta skullMeta)) return skull;
skullMeta.setPlayerProfile(profile);
skull.setItemMeta(skullMeta);
return skull;
}
private PlayerProfile getOrCreateProfile(String base64Texture) { private PlayerProfile getOrCreateProfile(String base64Texture) {
return profileCache.computeIfAbsent(base64Texture, key -> { return profileCache.computeIfAbsent(base64Texture, key -> {
String skinUrl = extractSkinUrlFromBase64(base64Texture); String skinUrl = extractSkinUrlFromBase64(base64Texture);
@ -77,4 +86,88 @@ public class CustomHeads {
return null; return null;
} }
} }
}
// ===========================
// PLAYER HEADS (SYNC + ASYNC)
// ===========================
/**
* If head is not cached run the async warmer to cache the head async.
*/
public ItemStack getPlayerHeadSync(String playerName) {
String key = playerName.toLowerCase();
// Start with a PLAYER_HEAD item and a skull meta
ItemStack skull = new ItemStack(Material.PLAYER_HEAD, 1);
SkullMeta skullMeta = (SkullMeta) skull.getItemMeta();
// Try to get a profile from cache first
PlayerProfile profile = profileCache.get(key);
if (profile == null) {
// Fallback to offline player profile to show textures when players are already online
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
profile = offlinePlayer.getPlayerProfile();
// Enqueue async task to fill cache for next time
enqueuePlayerHead(key);
}
// Put profile on the head
skullMeta.setPlayerProfile(profile);
skull.setItemMeta(skullMeta);
return skull;
}
/**
* Asynchronous cache warmer.
* Will resolve and store the PlayerProfile in the cache in the background.
* Does not return an ItemStack.
*/
private void cachePlayerHeadAsync(String playerName) {
String key = playerName.toLowerCase();
if (profileCache.containsKey(key)) return; // already cached
Bukkit.getAsyncScheduler().runNow(ctx.plugin, (t) -> {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
PlayerProfile p = offlinePlayer.getPlayerProfile();
if (p == null) {
UUID offlineUuid = UUID.nameUUIDFromBytes(playerName.getBytes(StandardCharsets.UTF_8));
p = Bukkit.createProfile(offlineUuid, playerName);
}
p.complete(true); // network call
profileCache.put(key, p);
});
}
/**
* Player head async queue code below
* queue is used to help avoid timeouts from Mojang
*/
private void enqueuePlayerHead(String key) {
if (profileCache.containsKey(key) || lookupQueue.contains(key)) return;
lookupQueue.add(key);
startQueueProcessor();
}
private void startQueueProcessor() {
if (queueTaskRunning) return;
queueTaskRunning = true;
Bukkit.getGlobalRegionScheduler().runAtFixedRate(ctx.plugin, task -> {
int maxPerTick = 3;
for (int i = 0; i < maxPerTick; i++) {
String next = lookupQueue.poll();
if (next == null) {
// no more tasks, stop processor
task.cancel();
queueTaskRunning = false;
return;
}
// Run the async fetch for this key
Bukkit.getAsyncScheduler().runNow(ctx.plugin, t -> cachePlayerHeadAsync(next));
}
}, 1, 15); // tick interval per head api lookup
}
}

View File

@ -47,7 +47,6 @@ public class CommandRunner {
String command = commands.get(index).trim(); String command = commands.get(index).trim();
// Handle the delay tag at flow level
if (command.startsWith("[delay]")) { if (command.startsWith("[delay]")) {
String delayStr = ctx.text.applyPlaceholders( String delayStr = ctx.text.applyPlaceholders(
player, player,
@ -70,10 +69,7 @@ public class CommandRunner {
} }
// Run the command // Run the command
Bukkit.getGlobalRegionScheduler().run( runCommand(panel, player, command);
ctx.plugin,
task -> runCommand(panel, player, command)
);
// Move to the next command // Move to the next command
runCommands(panel, player, commands, index + 1); runCommands(panel, player, commands, index + 1);

View File

@ -3,7 +3,6 @@ package me.rockyhawk.commandpanels.interaction.commands.tags;
import me.rockyhawk.commandpanels.Context; import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.interaction.commands.CommandTagResolver; import me.rockyhawk.commandpanels.interaction.commands.CommandTagResolver;
import me.rockyhawk.commandpanels.session.Panel; import me.rockyhawk.commandpanels.session.Panel;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
public class ChatTag implements CommandTagResolver { public class ChatTag implements CommandTagResolver {

View File

@ -15,6 +15,8 @@ public class ConsoleCmdTag implements CommandTagResolver {
@Override @Override
public void handle(Context ctx, Panel panel, Player player, String raw, String command) { public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); Bukkit.getGlobalRegionScheduler().run(ctx.plugin,
task -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command)
);
} }
} }

View File

@ -19,8 +19,8 @@ public class RefreshPanelTag implements CommandTagResolver {
*/ */
@Override @Override
public void handle(Context ctx, Panel panel, Player player, String raw, String command) { public void handle(Context ctx, Panel panel, Player player, String raw, String command) {
Bukkit.getGlobalRegionScheduler().run(ctx.plugin, task -> { Bukkit.getGlobalRegionScheduler().run(ctx.plugin, task ->
panel.open(ctx, player, false); panel.open(ctx, player, false)
}); );
} }
} }

View File

@ -6,7 +6,6 @@ import me.rockyhawk.commandpanels.interaction.commands.RequirementRunner;
import me.rockyhawk.commandpanels.session.ClickActions; import me.rockyhawk.commandpanels.session.ClickActions;
import me.rockyhawk.commandpanels.session.inventory.InventoryPanel; import me.rockyhawk.commandpanels.session.inventory.InventoryPanel;
import me.rockyhawk.commandpanels.session.inventory.PanelItem; import me.rockyhawk.commandpanels.session.inventory.PanelItem;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Event; import org.bukkit.event.Event;