feat: 完善自定义菜单物品功能,新增 MenuIconManager 支持自定义图标
This commit is contained in:
@@ -160,6 +160,14 @@ public class DatabaseManager {
|
||||
+ "alert_date TEXT NOT NULL,"
|
||||
+ "notified INTEGER DEFAULT 0,"
|
||||
+ "notified_time INTEGER DEFAULT 0)");
|
||||
// 菜单图标表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "menu_icons ("
|
||||
+ "player_name TEXT PRIMARY KEY,"
|
||||
+ "icon_b64 TEXT DEFAULT '',"
|
||||
+ "item_name TEXT DEFAULT '',"
|
||||
+ "save_time INTEGER DEFAULT 0"
|
||||
+ ")");
|
||||
|
||||
// 工单表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
@@ -239,6 +247,76 @@ public class DatabaseManager {
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
// ==================== 菜单图标 ====================
|
||||
|
||||
public void saveMenuIcon(String name,
|
||||
String iconB64, String itemName) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR REPLACE INTO menu_icons "
|
||||
+ "(player_name, icon_b64, "
|
||||
+ "item_name, save_time) "
|
||||
+ "VALUES (?,?,?,?)");
|
||||
ps.setString(1, name);
|
||||
ps.setString(2, iconB64);
|
||||
ps.setString(3, itemName);
|
||||
ps.setLong(4,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public String getMenuIcon(String name) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT icon_b64 FROM menu_icons "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, name);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
String b64 = null;
|
||||
if (rs.next()) {
|
||||
b64 = rs.getString("icon_b64");
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return b64;
|
||||
} catch (SQLException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteMenuIcon(String name) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"DELETE FROM menu_icons "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, name);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasMenuIcon(String name) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT 1 FROM menu_icons "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND icon_b64 IS NOT NULL "
|
||||
+ "AND icon_b64 != '' "
|
||||
+ "LIMIT 1");
|
||||
ps.setString(1, name);
|
||||
boolean r = ps.executeQuery().next();
|
||||
ps.close();
|
||||
return r;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 用户操作 ====================
|
||||
|
||||
@@ -773,6 +851,7 @@ public class DatabaseManager {
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public List<Map<String, Object>>
|
||||
|
||||
@@ -11,6 +11,14 @@ import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Base64;
|
||||
|
||||
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -792,6 +800,7 @@ public class GUIManager implements Listener {
|
||||
public void openMyInfo(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54, T_MY_INFO);
|
||||
|
||||
fillBg(g);
|
||||
Map<String, Object> user = plugin.getDb()
|
||||
.getUser(p.getName());
|
||||
@@ -833,10 +842,412 @@ public class GUIManager implements Listener {
|
||||
g.setItem(15, mkItem(Material.EMERALD_BLOCK,
|
||||
"§a§l积分商城",
|
||||
"§7当前: " + points + "积分"));
|
||||
|
||||
|
||||
// 图标说明
|
||||
g.setItem(30, mkItem(Material.PAPER,
|
||||
"§e§l自定义菜单图标",
|
||||
"§7将物品放入右侧槽位",
|
||||
"§7即可保存为你的专属图标",
|
||||
"§7下次加入自动发放"));
|
||||
// ===== 菜单图标区 =====
|
||||
g.setItem(30, mkItem(Material.PAPER,
|
||||
"§e§l自定义菜单图标",
|
||||
"§7Shift+点击背包物品放入右侧槽位",
|
||||
"§7即可保存为你的专属图标",
|
||||
"§7下次加入自动发放"));
|
||||
|
||||
String iconB64 = plugin.getDb()
|
||||
.getMenuIcon(p.getName());
|
||||
if (iconB64 != null && !iconB64.isEmpty()) {
|
||||
ItemStack iconItem =
|
||||
deserializeItem(iconB64);
|
||||
if (iconItem != null) {
|
||||
g.setItem(31, iconItem);
|
||||
} else {
|
||||
putIconPlaceholder(g);
|
||||
}
|
||||
} else {
|
||||
putIconPlaceholder(g);
|
||||
}
|
||||
|
||||
g.setItem(32, mkItem(Material.WOODEN_SHOVEL,
|
||||
"§c清除图标",
|
||||
"§7点击可清除已保存的图标"));
|
||||
|
||||
g.setItem(26, mkItem(Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图标槽的空占位物品
|
||||
*/
|
||||
private static final String ICON_SLOT_DEFAULT =
|
||||
"§7默认菜单(雪球)";
|
||||
|
||||
private void putIconPlaceholder(Inventory g) {
|
||||
ItemStack empty = new ItemStack(
|
||||
Material.SNOWBALL);
|
||||
ItemMeta m = empty.getItemMeta();
|
||||
m.setDisplayName(ICON_SLOT_DEFAULT);
|
||||
m.setLore(Arrays.asList(
|
||||
"§7Shift+点击背包物品放入",
|
||||
"§7保存后下次登录自动发放"));
|
||||
empty.setItemMeta(m);
|
||||
g.setItem(31, empty);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理"我的信息"GUI中菜单图标槽的点击
|
||||
* 在已有的 onInventoryClick 方法中,
|
||||
* 匹配到 T_MY_INFO 标题后调用此方法
|
||||
*
|
||||
* 返回 true 表示已处理(事件应取消)
|
||||
* 返回 false 表示未处理(继续原有逻辑)
|
||||
*/
|
||||
public boolean handleMyInfoIconClick(
|
||||
Player p,
|
||||
InventoryClickEvent event) {
|
||||
|
||||
event.setCancelled(true);
|
||||
Inventory topInv = event.getView()
|
||||
.getTopInventory();
|
||||
int raw = event.getRawSlot();
|
||||
|
||||
if (raw == 32) {
|
||||
doClearIcon(p, topInv);
|
||||
return true;
|
||||
}
|
||||
if (raw != 31) return true;
|
||||
|
||||
ItemStack cursor = event.getCursor();
|
||||
boolean hasCursor = cursor != null
|
||||
&& cursor.getType() != Material.AIR;
|
||||
|
||||
// Shift-click:从背包放入
|
||||
if (event.isShiftClick()
|
||||
&& event.getClickedInventory()
|
||||
== p.getInventory()) {
|
||||
int srcSlot = event.getSlot();
|
||||
ItemStack src =
|
||||
p.getInventory().getItem(srcSlot);
|
||||
if (src != null
|
||||
&& src.getType() != Material.AIR
|
||||
&& !isCustomMenuTrigger(src)) {
|
||||
doPlaceIcon(p, topInv, src, srcSlot);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 普通点击 + 光标有物品 → 放入
|
||||
if (hasCursor
|
||||
&& !isCustomMenuTrigger(cursor)) {
|
||||
doPlaceIcon(p, topInv, cursor, -1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 普通点击 + 光标空 + 非占位 → 清除
|
||||
if (!hasCursor) {
|
||||
ItemStack cur = topInv.getItem(31);
|
||||
if (!isPlaceholder(cur)) {
|
||||
doClearIcon(p, topInv);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== 放入自定义图标 =====
|
||||
private void doPlaceIcon(Player p,
|
||||
Inventory topInv,
|
||||
ItemStack item, int removeSlot) {
|
||||
|
||||
int amount = item.getAmount();
|
||||
removeSnowballFromInventory(p); //没收雪球菜单
|
||||
// 1. 从来源移除
|
||||
if (removeSlot >= 0) {
|
||||
// Shift-click:从背包格子移除
|
||||
p.getInventory()
|
||||
.setItem(removeSlot, null);
|
||||
} else {
|
||||
// 普通点击:从光标移除
|
||||
p.getOpenInventory().setCursor(null);
|
||||
}
|
||||
|
||||
// 2. 退回多余(amount-1)到背包
|
||||
if (amount > 1) {
|
||||
ItemStack excess = item.clone();
|
||||
excess.setAmount(amount - 1);
|
||||
java.util.HashMap<Integer,
|
||||
ItemStack> leftover =
|
||||
p.getInventory()
|
||||
.addItem(excess);
|
||||
for (ItemStack drop :
|
||||
leftover.values()) {
|
||||
p.getWorld().dropItemNaturally(
|
||||
p.getLocation(), drop);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 存原件(数量=1)到DB
|
||||
ItemStack original = item.clone();
|
||||
original.setAmount(1);
|
||||
String b64 = serializeItem(original);
|
||||
if (b64 == null) {
|
||||
p.sendMessage("§c保存失败");
|
||||
return;
|
||||
}
|
||||
plugin.getDb().saveMenuIcon(
|
||||
p.getName(), b64,
|
||||
original.getType().name());
|
||||
|
||||
// 4. 创建带标记的菜单物品
|
||||
ItemStack menu = original.clone();
|
||||
ItemMeta mm = menu.getItemMeta();
|
||||
if (mm != null) {
|
||||
String oldName =
|
||||
mm.getDisplayName();
|
||||
if (oldName == null
|
||||
|| oldName.isEmpty()
|
||||
|| oldName.equals(
|
||||
menu.getType().name())) {
|
||||
oldName = "自定义物品";
|
||||
}
|
||||
mm.setDisplayName(
|
||||
"\u00a7e\u00a7l[菜单] \u00a7f"
|
||||
+ oldName);
|
||||
List<String> lore = mm.hasLore()
|
||||
? new ArrayList<>(mm.getLore())
|
||||
: new ArrayList<>();
|
||||
lore.add(0,
|
||||
"\u00a77右键打开主菜单");
|
||||
mm.setLore(lore);
|
||||
mm.getPersistentDataContainer().set(
|
||||
new org.bukkit.NamespacedKey(
|
||||
plugin, "menu_trigger"),
|
||||
org.bukkit.persistence
|
||||
.PersistentDataType
|
||||
.STRING,
|
||||
"true");
|
||||
menu.setItemMeta(mm);
|
||||
}
|
||||
|
||||
// 5. 发放菜单物品
|
||||
int slot = p.getInventory()
|
||||
.firstEmpty();
|
||||
if (slot >= 0) {
|
||||
p.getInventory().setItem(slot, menu);
|
||||
} else {
|
||||
p.getWorld().dropItemNaturally(
|
||||
p.getLocation(), menu);
|
||||
}
|
||||
|
||||
// 6. 更新GUI
|
||||
topInv.setItem(31, original);
|
||||
p.sendMessage("§a已保存菜单图标: "
|
||||
+ original.getType().name());
|
||||
p.updateInventory();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===== 清除自定义图标 =====
|
||||
private void doClearIcon(Player p,
|
||||
Inventory topInv) {
|
||||
|
||||
String dbIcon = plugin.getDb()
|
||||
.getMenuIcon(p.getName());
|
||||
|
||||
// 1. 没收自定义菜单物品
|
||||
removeCustomMenuFromInventory(p);
|
||||
|
||||
// 2. 删DB
|
||||
plugin.getDb().deleteMenuIcon(
|
||||
p.getName());
|
||||
|
||||
// 3. 强制清空slot 31
|
||||
topInv.setItem(31, null);
|
||||
|
||||
// 4. 发雪球
|
||||
plugin.giveMenuSnowball(p);
|
||||
|
||||
// 5. 还原件
|
||||
if (dbIcon != null && !dbIcon.isEmpty()) {
|
||||
ItemStack original =
|
||||
deserializeItem(dbIcon);
|
||||
if (original != null) {
|
||||
p.getInventory()
|
||||
.addItem(original);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 重新设置占位符
|
||||
putIconPlaceholder(topInv);
|
||||
p.sendMessage("§c已清除菜单图标");
|
||||
p.updateInventory();
|
||||
}
|
||||
|
||||
|
||||
// ===== 辅助方法 =====
|
||||
|
||||
// 只匹配带[菜单]的非雪球物品
|
||||
private boolean isCustomMenuTrigger(
|
||||
ItemStack item) {
|
||||
if (item == null) return false;
|
||||
if (item.getType() == Material.SNOWBALL)
|
||||
return false;
|
||||
ItemMeta im = item.getItemMeta();
|
||||
if (im == null) return false;
|
||||
if (im.hasDisplayName()
|
||||
&& im.getDisplayName()
|
||||
.contains("[菜单]")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只移除自定义菜单物品(不动雪球)
|
||||
private void removeCustomMenuFromInventory(
|
||||
Player p) {
|
||||
for (int i = 0;
|
||||
i < p.getInventory().getSize(); i++) {
|
||||
ItemStack it = p.getInventory()
|
||||
.getItem(i);
|
||||
if (isCustomMenuTrigger(it)) {
|
||||
p.getInventory().setItem(i, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只移除雪球菜单
|
||||
private void removeSnowballFromInventory(
|
||||
Player p) {
|
||||
for (int i = 0;
|
||||
i < p.getInventory().getSize(); i++) {
|
||||
ItemStack it = p.getInventory()
|
||||
.getItem(i);
|
||||
if (it != null
|
||||
&& it.getType()
|
||||
== Material.SNOWBALL
|
||||
&& it.hasItemMeta()) {
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im.hasLore()
|
||||
&& im.getLore() != null) {
|
||||
for (String ln : im.getLore()) {
|
||||
if (ln.contains(
|
||||
"\u00a78")) {
|
||||
p.getInventory()
|
||||
.setItem(i,
|
||||
null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 已有方法,不需要改
|
||||
private boolean isPlaceholder(
|
||||
ItemStack item) {
|
||||
if (item == null) return true;
|
||||
if (item.getType() == Material.AIR)
|
||||
return true;
|
||||
// fillBg 可能覆盖了slot 31
|
||||
if (item.getType()
|
||||
== Material.GRAY_STAINED_GLASS_PANE)
|
||||
return true;
|
||||
ItemMeta im = item.getItemMeta();
|
||||
if (im == null) return true;
|
||||
if ("\u00a77默认菜单(雪球)"
|
||||
.equals(im.getDisplayName()))
|
||||
return true;
|
||||
if (im.hasLore() && im.getLore() != null
|
||||
&& im.getLore().size() >= 2
|
||||
&& im.getLore().get(0)
|
||||
.contains("Shift+点击"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// ===== 辅助方法 =====
|
||||
|
||||
private boolean isMenuTriggerItem(
|
||||
ItemStack item) {
|
||||
if (item == null) return false;
|
||||
ItemMeta im = item.getItemMeta();
|
||||
if (im == null) return false;
|
||||
// 自定义图标:显示名含[菜单]
|
||||
if (im.hasDisplayName()
|
||||
&& im.getDisplayName()
|
||||
.contains("[菜单]")) {
|
||||
return true;
|
||||
}
|
||||
// 默认雪球
|
||||
if (item.getType() == Material.SNOWBALL
|
||||
&& im.hasLore()
|
||||
&& im.getLore() != null) {
|
||||
for (String ln : im.getLore()) {
|
||||
if (ln.contains("\u00a77右键打开主菜单"))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*private boolean isPlaceholder(ItemStack item) {
|
||||
if (item == null) return true;
|
||||
if (item.getType() == Material.AIR)
|
||||
return true;
|
||||
ItemMeta im = item.getItemMeta();
|
||||
if (im == null) return true;
|
||||
if ("\u00a77默认菜单(雪球)"
|
||||
.equals(im.getDisplayName())) {
|
||||
return true;
|
||||
}
|
||||
if (im.hasLore() && im.getLore() != null
|
||||
&& im.getLore().size() >= 2
|
||||
&& im.getLore().get(0)
|
||||
.contains("Shift+点击")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}*/
|
||||
|
||||
private void removeMenuTriggerFromInventory(
|
||||
Player p) {
|
||||
for (int i = 0;
|
||||
i < p.getInventory().getSize(); i++) {
|
||||
ItemStack it = p.getInventory()
|
||||
.getItem(i);
|
||||
if (isMenuTriggerItem(it)) {
|
||||
p.getInventory().setItem(i, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeCustomTriggerFromInventory(
|
||||
Player p) {
|
||||
for (int i = 0;
|
||||
i < p.getInventory().getSize(); i++) {
|
||||
ItemStack it = p.getInventory()
|
||||
.getItem(i);
|
||||
if (it == null) continue;
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im == null) continue;
|
||||
if (im.hasDisplayName()
|
||||
&& im.getDisplayName()
|
||||
.contains("[菜单]")) {
|
||||
p.getInventory().setItem(i, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 在 openGiftStages 方法之前添加:
|
||||
public void openGiftStages(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
@@ -1073,11 +1484,30 @@ public class GUIManager implements Listener {
|
||||
cancel.setItemMeta(cml);
|
||||
}
|
||||
g.setItem(15, cancel);
|
||||
// ===== 图标槽31 =====
|
||||
g.setItem(30, mkItem(Material.PAPER,
|
||||
"§e§l自定义菜单图标",
|
||||
"§7Shift+点击背包物品放入右侧槽位",
|
||||
"§7即可保存为你的专属图标",
|
||||
"§7下次加入自动发放"));
|
||||
|
||||
p.openInventory(g);
|
||||
putIconPlaceholder(g);
|
||||
|
||||
String iconB64 = plugin.getDb()
|
||||
.getMenuIcon(p.getName());
|
||||
if (iconB64 != null && !iconB64.isEmpty()) {
|
||||
ItemStack iconItem =
|
||||
deserializeItem(iconB64);
|
||||
if (iconItem != null) {
|
||||
g.setItem(31, iconItem);
|
||||
}
|
||||
}
|
||||
|
||||
g.setItem(32, mkItem(Material.BARRIER,
|
||||
"§c清除图标",
|
||||
"§7点击可清除已保存的图标"));
|
||||
}
|
||||
|
||||
public void openShopAdmin(Player p) {
|
||||
public void openShopAdmin(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54, "§c§l商城管理");
|
||||
ItemStack glass = new ItemStack(
|
||||
@@ -1439,6 +1869,43 @@ public class GUIManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 本地序列化,不依赖MenuIconManager =====
|
||||
|
||||
public String serializeItem(ItemStack item) {
|
||||
try {
|
||||
ByteArrayOutputStream baos =
|
||||
new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream os =
|
||||
new BukkitObjectOutputStream(baos);
|
||||
os.writeObject(item);
|
||||
os.close();
|
||||
return Base64.getEncoder()
|
||||
.encodeToString(baos.toByteArray());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack deserializeItem(String b64) {
|
||||
if (b64 == null || b64.isEmpty()) return null;
|
||||
try {
|
||||
byte[] data =
|
||||
Base64.getDecoder().decode(b64);
|
||||
ByteArrayInputStream bais =
|
||||
new ByteArrayInputStream(data);
|
||||
BukkitObjectInputStream is =
|
||||
new BukkitObjectInputStream(bais);
|
||||
Object obj = is.readObject();
|
||||
is.close();
|
||||
if (obj instanceof ItemStack)
|
||||
return (ItemStack) obj;
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final String ICON_SLOT_EMPTY =
|
||||
"§7图标槽(空)";
|
||||
|
||||
private ItemStack mkItem(Material mat, String name,
|
||||
String... lore) {
|
||||
|
||||
+147
-115
@@ -14,6 +14,9 @@ import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
|
||||
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
@@ -59,6 +62,7 @@ public class Main extends JavaPlugin
|
||||
TabCompleter {
|
||||
|
||||
private DatabaseManager db;
|
||||
private MenuIconManager menuIconMgr;
|
||||
private ConfigManager config;
|
||||
private EmailManager email;
|
||||
private LoginManager loginMgr;
|
||||
@@ -165,6 +169,7 @@ public class Main extends JavaPlugin
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
public ConfigManager getConfig2() {
|
||||
return config;
|
||||
}
|
||||
@@ -258,6 +263,9 @@ public class Main extends JavaPlugin
|
||||
// ===== 2. 数据库 =====
|
||||
db = new DatabaseManager(getDataFolder());
|
||||
db.init();
|
||||
menuIconMgr = new MenuIconManager(this);
|
||||
|
||||
|
||||
|
||||
// ===== 3. 菜单 =====
|
||||
menu = new MenuManager(this);
|
||||
@@ -359,7 +367,6 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(this, 600L, 600L);
|
||||
|
||||
getLogger().info("Sdf1_login v1.0 | 就绪");
|
||||
}
|
||||
|
||||
@@ -1243,10 +1250,10 @@ public class Main extends JavaPlugin
|
||||
// ===== Events =====
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent e) {
|
||||
|
||||
Player p = e.getPlayer();
|
||||
String name = p.getName();
|
||||
String ip = getPlayerIP(p);
|
||||
|
||||
// 强制重发资源包(清掉客户端拒绝记录)
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(this, () -> {
|
||||
@@ -1450,51 +1457,89 @@ public class Main extends JavaPlugin
|
||||
private static final String MENU_SNOWBALL_TAG =
|
||||
"sdf1_menu";
|
||||
|
||||
// 雪球菜单
|
||||
public void giveMenuSnowball(Player p) {
|
||||
// 检查开关
|
||||
Object val = db.getField(
|
||||
p.getName(), "menu_snowball");
|
||||
if (val != null) {
|
||||
int on = val instanceof Number
|
||||
? ((Number) val).intValue()
|
||||
: 1;
|
||||
if (on == 0) {
|
||||
return; // 开关关了,不发放
|
||||
}
|
||||
? ((Number) val).intValue() : 1;
|
||||
if (on == 0) return;
|
||||
}
|
||||
// 检查是否已有
|
||||
|
||||
for (ItemStack it : p.getInventory()
|
||||
.getContents()) {
|
||||
if (isMenuSnowball(it)) {
|
||||
return;
|
||||
}
|
||||
if (isMenuSnowball(it)) return;
|
||||
if (isCustomMenuTrigger(it)) return;
|
||||
}
|
||||
// 发放
|
||||
|
||||
ItemStack customIcon =
|
||||
menuIconMgr.getIcon(p);
|
||||
if (customIcon != null) {
|
||||
tagAsMenuTrigger(customIcon);
|
||||
// 加外观标识
|
||||
ItemMeta cm = customIcon.getItemMeta();
|
||||
if (cm != null) {
|
||||
String originalName =
|
||||
cm.getDisplayName();
|
||||
if (originalName == null
|
||||
|| originalName.isEmpty()
|
||||
|| originalName.equals(
|
||||
customIcon.getType()
|
||||
.name())) {
|
||||
originalName = "自定义物品";
|
||||
}
|
||||
cm.setDisplayName(
|
||||
"\u00a7e\u00a7l[菜单] \u00a7f"
|
||||
+ originalName);
|
||||
List<String> cmLore =
|
||||
cm.hasLore()
|
||||
? new ArrayList<>(cm.getLore())
|
||||
: new ArrayList<>();
|
||||
// 避免重复添加
|
||||
if (cmLore.isEmpty()
|
||||
|| !cmLore.get(0).contains(
|
||||
"\u00a77右键打开主菜单")) {
|
||||
cmLore.add(0,
|
||||
"\u00a77右键打开主菜单");
|
||||
}
|
||||
cm.setLore(cmLore);
|
||||
customIcon.setItemMeta(cm);
|
||||
}
|
||||
int slot = p.getInventory()
|
||||
.firstEmpty();
|
||||
if (slot >= 0) {
|
||||
p.getInventory().setItem(
|
||||
slot, customIcon);
|
||||
} else {
|
||||
p.getWorld().dropItemNaturally(
|
||||
p.getLocation(), customIcon);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack snow = new ItemStack(
|
||||
Material.SNOWBALL);
|
||||
ItemMeta im = snow.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(
|
||||
"§e§l[菜单] §f右键打开主菜单");
|
||||
List<String> lore =
|
||||
new ArrayList<>();
|
||||
lore.add("§7右键点击打开功能菜单");
|
||||
lore.add("§8" + MENU_SNOWBALL_TAG);
|
||||
"\u00a7e\u00a7l[菜单] \u00a7f右键打开主菜单");
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("\u00a77右键点击打开功能菜单");
|
||||
lore.add("\u00a78" + MENU_SNOWBALL_TAG);
|
||||
im.setLore(lore);
|
||||
snow.setItemMeta(im);
|
||||
}
|
||||
int slot = p.getInventory()
|
||||
.firstEmpty();
|
||||
if (slot >= 0) {
|
||||
p.getInventory().setItem(
|
||||
slot, snow);
|
||||
p.getInventory().setItem(slot, snow);
|
||||
} else {
|
||||
p.getWorld().dropItemNaturally(
|
||||
p.getLocation(), snow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 手动发放雪球菜单(绕过开关检查)
|
||||
*/
|
||||
@@ -1549,6 +1594,37 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// 运行时初始化版本(避免静态加载问题)
|
||||
private NamespacedKey getTriggerKey() {
|
||||
return new NamespacedKey(this, "menu_trigger");
|
||||
}
|
||||
|
||||
/**
|
||||
* 给物品打上菜单触发标记
|
||||
*/
|
||||
private void tagAsMenuTrigger(ItemStack item) {
|
||||
if (item == null) return;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return;
|
||||
meta.getPersistentDataContainer().set(
|
||||
getTriggerKey(),
|
||||
PersistentDataType.STRING,
|
||||
"true");
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查物品是否带菜单触发标记
|
||||
*/
|
||||
private boolean isCustomMenuTrigger(
|
||||
ItemStack item) {
|
||||
if (item == null) return false;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return false;
|
||||
return meta.getPersistentDataContainer()
|
||||
.has(getTriggerKey(),
|
||||
PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
|
||||
@EventHandler
|
||||
@@ -1583,7 +1659,6 @@ public class Main extends JavaPlugin
|
||||
return;
|
||||
}
|
||||
|
||||
// 同时检查主手和副手
|
||||
ItemStack hand = p.getInventory()
|
||||
.getItemInMainHand();
|
||||
ItemStack offhand = p.getInventory()
|
||||
@@ -1593,6 +1668,15 @@ public class Main extends JavaPlugin
|
||||
|| isMenuSnowball(offhand)) {
|
||||
e.setCancelled(true);
|
||||
gui.openMain(p);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCustomMenuTrigger(hand)
|
||||
|| isCustomMenuTrigger(offhand)) {
|
||||
if (db.hasMenuIcon(p.getName())) {
|
||||
e.setCancelled(true);
|
||||
gui.openMain(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2411,7 +2495,6 @@ public class Main extends JavaPlugin
|
||||
break;
|
||||
}}
|
||||
// ==================== 主菜单(新布局) ====================
|
||||
// ===== 主菜单 =====
|
||||
if (title.equals(GUIManager.T_MAIN)) {
|
||||
e.setCancelled(true);
|
||||
|
||||
@@ -2479,10 +2562,14 @@ public class Main extends JavaPlugin
|
||||
"请输入管理密码:");
|
||||
}
|
||||
break;
|
||||
case 31: gui.openMenuManager(p); break;
|
||||
case 32: gui.openMain(p); break;
|
||||
case 31:
|
||||
if (p.isOp()) {
|
||||
gui.openMenuManager(p);
|
||||
}
|
||||
break;
|
||||
|
||||
case 34:
|
||||
if (isAdmin(p)) {
|
||||
if (p.isOp() || isAdmin(p)) {
|
||||
gui.openBalanceOps(p);
|
||||
}
|
||||
break;
|
||||
@@ -2567,6 +2654,9 @@ public class Main extends JavaPlugin
|
||||
// ==================== 我的信息 ====================
|
||||
if (title.equals(GUIManager.T_MY_INFO)) {
|
||||
e.setCancelled(true);
|
||||
// 先处理图标槽
|
||||
if (gui.handleMyInfoIconClick(p, e))
|
||||
return;
|
||||
if (slot == 26) gui.openMain(p);
|
||||
else if (slot == 10) {
|
||||
if (checkIn.isCheckedInToday(
|
||||
@@ -2611,6 +2701,7 @@ public class Main extends JavaPlugin
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// ==================== 管理面板 ====================
|
||||
if (title.equals(GUIManager.T_ADMIN)) {
|
||||
e.setCancelled(true);
|
||||
@@ -2838,95 +2929,6 @@ public class Main extends JavaPlugin
|
||||
p.closeInventory();
|
||||
loginMgr.handleReset(p);
|
||||
}
|
||||
/* .txt子菜单
|
||||
if (title.startsWith("§6§l")
|
||||
&& !title.equals(GUIManager.T_MAIN)
|
||||
&& !title.equals(GUIManager.T_ADMIN)
|
||||
&& !title.equals(GUIManager.T_MY_INFO)
|
||||
&& !title.equals(GUIManager.T_INVITE)
|
||||
&& !title.equals(GUIManager.T_TASK_CENTER)
|
||||
&& !title.equals(GUIManager.T_GIFT_STAGES)
|
||||
&& !title.equals("§6§l任务面板")) {
|
||||
e.setCancelled(true);
|
||||
if (slot == 49) {
|
||||
gui.openMain(p);
|
||||
return;
|
||||
}
|
||||
// ===== 二级菜单(.txt) =====
|
||||
if (title.startsWith("§6§l")
|
||||
&& !title.equals(GUIManager.T_MAIN)
|
||||
&& !title.equals(GUIManager.T_ADMIN)
|
||||
&& !title.equals(GUIManager.T_MY_INFO)
|
||||
&& !title.equals(GUIManager.T_INVITE)
|
||||
&& !title.equals(GUIManager.T_TASK_CENTER)
|
||||
&& !title.equals(GUIManager.T_GIFT_STAGES)
|
||||
&& !title.equals("§6§l任务面板")) {
|
||||
e.setCancelled(true);
|
||||
if (slot == 49) {
|
||||
gui.openMain(p);
|
||||
return;
|
||||
}
|
||||
if (slot < 45 && e.getCurrentItem() != null) {
|
||||
ItemMeta subLm =
|
||||
e.getCurrentItem().getItemMeta();
|
||||
if (subLm != null
|
||||
&& subLm.getLore() != null) {
|
||||
for (String subLn
|
||||
: subLm.getLore()) {
|
||||
if (subLn.contains(
|
||||
"§7指令: §f")) {
|
||||
String cmd =
|
||||
subLn.replace(
|
||||
"§7指令: §f",
|
||||
"").trim();
|
||||
if (cmd.isEmpty()
|
||||
|| cmd.equals("/")
|
||||
|| cmd.equals("null")) {
|
||||
p.sendMessage(
|
||||
"§c该菜单项指令为空");
|
||||
return;
|
||||
}
|
||||
p.closeInventory();
|
||||
if (cmd.startsWith("/"))
|
||||
cmd = cmd.substring(1);
|
||||
Bukkit.dispatchCommand(p, cmd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (slot < 45 && e.getCurrentItem() != null) {
|
||||
ItemMeta lm =
|
||||
e.getCurrentItem().getItemMeta();
|
||||
if (lm != null && lm.getLore() != null) {
|
||||
for (String ln : lm.getLore()) {
|
||||
if (ln.contains("§7指令: §f")) {
|
||||
String cmd = ln.replace(
|
||||
"§7指令: §f", "")
|
||||
.trim();
|
||||
// ★ 空指令保护 ★
|
||||
if (cmd.isEmpty()
|
||||
|| cmd.equals("/")
|
||||
|| cmd.equals("null")) {
|
||||
p.sendMessage(
|
||||
"§c该菜单项指令为空");
|
||||
return;
|
||||
}
|
||||
p.closeInventory();
|
||||
if (cmd.startsWith("/"))
|
||||
cmd = cmd.substring(1);
|
||||
Bukkit.dispatchCommand(p, cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3091,6 +3093,36 @@ public class Main extends JavaPlugin
|
||||
return;
|
||||
}
|
||||
String t = e.getView().getTitle();
|
||||
// 禁止T_MY_INFO拖拽,只允许点击操作
|
||||
// 我的信息:允许拖入图标槽(31)
|
||||
if (GUIManager.T_MY_INFO.equals(
|
||||
e.getView().getTitle())) {
|
||||
boolean toIconSlot = false;
|
||||
for (int raw : e.getRawSlots()) {
|
||||
if (raw == 31) {
|
||||
toIconSlot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (toIconSlot) {
|
||||
ItemStack dragged =
|
||||
e.getOldCursor().clone();
|
||||
String b64 = gui.serializeItem(dragged);
|
||||
if (b64 != null) {
|
||||
db.saveMenuIcon(p.getName(), b64,
|
||||
dragged.getType().name());
|
||||
}
|
||||
p.sendMessage("§a已保存菜单图标: "
|
||||
+ dragged.getType().name());
|
||||
// 刷新GUI显示
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(this, () ->
|
||||
gui.openMyInfo(p), 2L);
|
||||
}
|
||||
e.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 回收站拖拽
|
||||
if (t.equals("§6§l垃圾回收站")) {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Base64;
|
||||
|
||||
public class MenuIconManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final NamespacedKey iconKey;
|
||||
|
||||
public MenuIconManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.iconKey = new NamespacedKey(
|
||||
plugin, "custom_menu_icon");
|
||||
}
|
||||
|
||||
// ========== GUIManager 调用的方法 ==========
|
||||
|
||||
/**
|
||||
* Base64 → ItemStack
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().deserializeItem(...)
|
||||
*/
|
||||
public ItemStack deserializeItem(String b64) {
|
||||
if (b64 == null || b64.isEmpty())
|
||||
return null;
|
||||
try {
|
||||
byte[] data =
|
||||
Base64.getDecoder().decode(b64);
|
||||
ByteArrayInputStream bais =
|
||||
new ByteArrayInputStream(data);
|
||||
BukkitObjectInputStream is =
|
||||
new BukkitObjectInputStream(bais);
|
||||
Object obj = is.readObject();
|
||||
is.close();
|
||||
if (obj instanceof ItemStack)
|
||||
return (ItemStack) obj;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ItemStack → Base64
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().serializeItem(...)
|
||||
*/
|
||||
public String serializeItem(ItemStack item) {
|
||||
if (item == null) return null;
|
||||
try {
|
||||
ByteArrayOutputStream baos =
|
||||
new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream os =
|
||||
new BukkitObjectOutputStream(baos);
|
||||
os.writeObject(item);
|
||||
os.close();
|
||||
return Base64.getEncoder()
|
||||
.encodeToString(
|
||||
baos.toByteArray());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给物品打上菜单图标标记
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().ensureMenuIconTag(...)
|
||||
*/
|
||||
public void ensureMenuIconTag(ItemStack item) {
|
||||
if (item == null) return;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return;
|
||||
meta.getPersistentDataContainer().set(
|
||||
iconKey,
|
||||
PersistentDataType.STRING,
|
||||
"menu");
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标Key
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().getIconKey()
|
||||
*/
|
||||
public NamespacedKey getIconKey() {
|
||||
return iconKey;
|
||||
}
|
||||
|
||||
// ========== LoginManager / Main 调用的方法 ==========
|
||||
|
||||
/**
|
||||
* 获取玩家自定义图标物品
|
||||
* 有返回物品,无返回null
|
||||
*/
|
||||
public ItemStack getIcon(Player player) {
|
||||
String b64 = plugin.getDb()
|
||||
.getMenuIcon(player.getName());
|
||||
if (b64 == null || b64.isEmpty())
|
||||
return null;
|
||||
ItemStack item = deserializeItem(b64);
|
||||
if (item == null) return null;
|
||||
ensureMenuIconTag(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图标到DB
|
||||
*/
|
||||
public void saveIcon(Player player,
|
||||
ItemStack item) {
|
||||
String b64 = serializeItem(item);
|
||||
if (b64 != null) {
|
||||
plugin.getDb().saveMenuIcon(
|
||||
player.getName(), b64,
|
||||
item.getType().name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图标
|
||||
*/
|
||||
public void removeIcon(Player player) {
|
||||
plugin.getDb().deleteMenuIcon(
|
||||
player.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查物品是否是自定义菜单图标
|
||||
*/
|
||||
public boolean isCustomMenuIcon(
|
||||
ItemStack item) {
|
||||
if (item == null) return false;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return false;
|
||||
return meta.getPersistentDataContainer()
|
||||
.has(iconKey,
|
||||
PersistentDataType.STRING);
|
||||
}
|
||||
}
|
||||
@@ -250,6 +250,95 @@ public class RadioManager {
|
||||
Arrays.sort(files);
|
||||
for (File f : files) list.add(f);
|
||||
}
|
||||
/**
|
||||
* 解析 pack.png:优先读取插件目录,
|
||||
* 没有则搜索图片并用ffmpeg转码
|
||||
*/
|
||||
private File resolvePackIcon() {
|
||||
File dataDir = plugin.getDataFolder();
|
||||
File packPng = new File(dataDir, "pack.png");
|
||||
|
||||
if (packPng.exists()
|
||||
&& packPng.length() > 0) {
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 找到 pack.png ("
|
||||
+ packPng.length()
|
||||
+ " bytes)");
|
||||
return packPng;
|
||||
}
|
||||
|
||||
// 搜索可转换的图片文件
|
||||
String[] exts = {"png", "jpg", "jpeg",
|
||||
"bmp", "webp", "tiff", "gif"};
|
||||
File[] found = dataDir.listFiles(
|
||||
(dir, name) -> {
|
||||
String low = name.toLowerCase();
|
||||
for (String ext : exts) {
|
||||
if (low.endsWith("." + ext)
|
||||
&& !name.equals(
|
||||
"pack.png")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (found != null && found.length > 0) {
|
||||
File src = found[0];
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 找到图片: "
|
||||
+ src.getName()
|
||||
+ " → 转码为 pack.png");
|
||||
if (convertToPackPng(src, packPng)) {
|
||||
return packPng;
|
||||
}
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 转码失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 未找到 pack.png 或可转换图片");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ffmpeg 将任意图片转为 256x256 PNG
|
||||
*/
|
||||
private boolean convertToPackPng(
|
||||
File input, File output) {
|
||||
try {
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"ffmpeg", "-y",
|
||||
"-i", input.getAbsolutePath(),
|
||||
"-vf",
|
||||
"scale='min(256,iw)'"
|
||||
+ ":'min(256,ih)':"
|
||||
+ "force_original_aspect_ratio"
|
||||
+ "=decrease,"
|
||||
+ "pad=256:256:"
|
||||
+ "(ow-iw)/2:"
|
||||
+ "(oh-ih)/2:"
|
||||
+ "color=0x00000000",
|
||||
"-c:v", "png",
|
||||
output.getAbsolutePath());
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
boolean ok = p.waitFor(
|
||||
30, TimeUnit.SECONDS);
|
||||
if (!ok) {
|
||||
p.destroyForcibly();
|
||||
return false;
|
||||
}
|
||||
return output.exists()
|
||||
&& output.length() > 0;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] ffmpeg转码失败: "
|
||||
+ e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 资源包构建 ==========
|
||||
private void buildResourcePack() {
|
||||
@@ -320,6 +409,25 @@ public class RadioManager {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] sounds.json写入失败");
|
||||
}
|
||||
// sounds.json 写入后...
|
||||
|
||||
// ===== 新增:打包 pack.png 作为资源包图标 =====
|
||||
File iconPng = resolvePackIcon();
|
||||
if (iconPng != null && iconPng.exists()) {
|
||||
try {
|
||||
Files.copy(iconPng.toPath(),
|
||||
new File(packDir, "pack.png")
|
||||
.toPath(),
|
||||
StandardCopyOption
|
||||
.REPLACE_EXISTING);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] pack.png 已加入资源包");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] pack.png复制失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// pack.mcmeta(pack_format=54)
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user