Compare commits

...
5 Commits
14 changed files with 2741 additions and 122 deletions
+32
View File
@@ -0,0 +1,32 @@
---
name: Bug 反馈
about: 报告一个 Bug 以帮助改进插件
title: "[Bug] "
labels: bug
assignees: ''
---
## Bug 描述
请简明扼要地描述 Bug 是什么。
## 复现步骤
1. 进入 ...
2. 点击 ...
3. 出现错误 ...
## 期望行为
描述你期望发生的事情。
## 实际行为
描述实际发生了什么。
## 服务端信息
- 服务器类型和版本(如 Paper 1.21.4):
- 插件版本:
## 截图 / 日志
如有相关截图或报错日志,请粘贴在此处。
## 其他信息
补充任何你认为有助于解决问题的信息。
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: 问题排查指南
url: https://github.com/ypsdf1/sdf1_login
about: 在提交 Bug 前,请先查阅文档确认是否已有相关说明。
@@ -0,0 +1,20 @@
---
name: 功能新增
about: 提出一个新功能的需求建议
title: "[Feature] "
labels: enhancement
assignees: ''
---
## 功能描述
请描述你希望添加的新功能。
## 动机
为什么需要这个功能?它能解决什么问题?
## 建议的实现方式
如果对实现方式有想法,请在此描述。
## 额外信息
补充任何你认为有助于理解需求的上下文。
+17
View File
@@ -0,0 +1,17 @@
---
name: 建议提交
about: 对插件提出改进建议或反馈
title: "[Suggestion] "
labels: suggestion
assignees: ''
---
## 建议内容
请描述你的建议或想法。
## 背景
什么场景下产生的这个建议?
## 预期效果
你认为采用这个建议后能达到什么效果?
+12 -3
View File
@@ -13,7 +13,7 @@
| 新人礼包 | 新玩家7日礼包 | 已完成
| 挂机检测 | 挂机检测踢出 | 已完成
| 自定义菜单 | 可以自己diy指令 | 已完成
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 待施工
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工
## 2.安装说明:
@@ -254,15 +254,16 @@ http-port=443
### 2.2.7 菜单.txt
- 自定义菜单配置文件
```json
# 服务器菜单
# 使用类json格式
// 支持使用://Java单行注释、#yml单行注释,/* */和<!--- --->块注释
// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持<!--- --->的块注释
{
标题:主城传送
指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录
权限类型:玩家
权限类型:玩家
图标:COMPASS
}
@@ -343,6 +344,14 @@ http-port=443
- sdf1_login del 删除玩家账号<br>
- sdf1_login radio 停止播放广播<br>
- sdf1_login radio reload 重载广播并重新打包资源包
- menu 开/关 是否开启自定义菜单
## 4. 自定义菜单物品
- 打开主界面,选择"我的"进入<br>
![](img/menu1.png)
- 将你的自定义物品与雪球交换<br>
![](img/menu2.png)
- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了
## 运行截图
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

@@ -160,6 +160,40 @@ public class DatabaseManager {
+ "alert_date TEXT NOT NULL,"
+ "notified INTEGER DEFAULT 0,"
+ "notified_time INTEGER DEFAULT 0)");
// PVP区域表
st.execute("CREATE TABLE IF NOT EXISTS "
+ "pvp_regions ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "name TEXT NOT NULL UNIQUE,"
+ "world TEXT NOT NULL,"
+ "x1 INTEGER NOT NULL,"
+ "y1 INTEGER NOT NULL,"
+ "z1 INTEGER NOT NULL,"
+ "x2 INTEGER NOT NULL,"
+ "y2 INTEGER NOT NULL,"
+ "z2 INTEGER NOT NULL,"
+ "create_time INTEGER DEFAULT 0"
+ ")");
// PVP击杀总榜表
st.execute("CREATE TABLE IF NOT EXISTS "
+ "pvp_kills ("
+ "player_name TEXT NOT NULL,"
+ "region_name TEXT NOT NULL,"
+ "kills INTEGER DEFAULT 0,"
+ "deaths INTEGER DEFAULT 0,"
+ "PRIMARY KEY(player_name, region_name)"
+ ")");
// 菜单图标表
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 +273,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;
}
}
// ==================== 用户操作 ====================
@@ -747,6 +851,252 @@ public class DatabaseManager {
return false;
}
}
// ==================== PVP区域 ====================
public void savePVPRegion(String name,
String world,
int x1, int y1, int z1,
int x2, int y2, int z2) {
try {
PreparedStatement ps =
db.prepareStatement(
"INSERT OR REPLACE "
+ "INTO pvp_regions "
+ "(name, world, "
+ "x1, y1, z1, "
+ "x2, y2, z2, "
+ "create_time) "
+ "VALUES "
+ "(?,?,?,?,?,?,?,?,?)");
ps.setString(1, name);
ps.setString(2, world);
ps.setInt(3, x1);
ps.setInt(4, y1);
ps.setInt(5, z1);
ps.setInt(6, x2);
ps.setInt(7, y2);
ps.setInt(8, z2);
ps.setLong(9,
System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public Map<String, Object>
getPVPRegion(String name) {
try {
PreparedStatement ps =
db.prepareStatement(
"SELECT * FROM "
+ "pvp_regions "
+ "WHERE name=?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
for (int i = 1; i <= rs
.getMetaData()
.getColumnCount(); i++) {
row.put(rs.getMetaData()
.getColumnName(i),
rs.getObject(i));
}
rs.close();
ps.close();
return row;
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public List<Map<String, Object>>
getAllPVPRegions() {
List<Map<String, Object>> list =
new ArrayList<>();
try {
Statement st =
db.createStatement();
ResultSet rs =
st.executeQuery(
"SELECT * FROM "
+ "pvp_regions");
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
for (int i = 1; i <= rs
.getMetaData()
.getColumnCount(); i++) {
row.put(rs.getMetaData()
.getColumnName(i),
rs.getObject(i));
}
list.add(row);
}
rs.close();
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
public void deletePVPRegion(String name) {
try {
PreparedStatement ps =
db.prepareStatement(
"DELETE FROM "
+ "pvp_regions "
+ "WHERE name=?");
ps.setString(1, name);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// ==================== PVP击杀 ====================
public int getPVPKills(
String player, String region) {
try {
PreparedStatement ps =
db.prepareStatement(
"SELECT kills FROM "
+ "pvp_kills "
+ "WHERE "
+ "player_name=? "
+ "AND region_name=?");
ps.setString(1, player);
ps.setString(2, region);
ResultSet rs = ps.executeQuery();
int kills = 0;
if (rs.next())
kills = rs.getInt("kills");
rs.close();
ps.close();
return kills;
} catch (SQLException e) {
return 0;
}
}
public int getPVPDeaths(
String player, String region) {
try {
PreparedStatement ps =
db.prepareStatement(
"SELECT deaths FROM "
+ "pvp_kills "
+ "WHERE "
+ "player_name=? "
+ "AND region_name=?");
ps.setString(1, player);
ps.setString(2, region);
ResultSet rs = ps.executeQuery();
int deaths = 0;
if (rs.next())
deaths = rs.getInt("deaths");
rs.close();
ps.close();
return deaths;
} catch (SQLException e) {
return 0;
}
}
public void addPVPKill(
String player, String region) {
try {
PreparedStatement ps =
db.prepareStatement(
"INSERT INTO pvp_kills "
+ "(player_name, "
+ "region_name, "
+ "kills, deaths) "
+ "VALUES (?,?,1,0) "
+ "ON CONFLICT"
+ "(player_name, "
+ "region_name) "
+ "DO UPDATE SET "
+ "kills=kills+1");
ps.setString(1, player);
ps.setString(2, region);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void addPVPDeath(
String player, String region) {
try {
PreparedStatement ps =
db.prepareStatement(
"INSERT INTO pvp_kills "
+ "(player_name, "
+ "region_name, "
+ "kills, deaths) "
+ "VALUES (?,?,0,1) "
+ "ON CONFLICT"
+ "(player_name, "
+ "region_name) "
+ "DO UPDATE SET "
+ "deaths=deaths+1");
ps.setString(1, player);
ps.setString(2, region);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public List<Map<String, Object>>
getPVPKillTop(String region, int limit) {
List<Map<String, Object>> list =
new ArrayList<>();
try {
PreparedStatement ps =
db.prepareStatement(
"SELECT player_name, "
+ "kills, deaths "
+ "FROM pvp_kills "
+ "WHERE "
+ "region_name=? "
+ "AND kills>0 "
+ "ORDER BY kills "
+ "DESC LIMIT ?");
ps.setString(1, region);
ps.setInt(2, limit);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("player_name",
rs.getString("player_name"));
row.put("kills",
rs.getInt("kills"));
row.put("deaths",
rs.getInt("deaths"));
list.add(row);
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
// ==================== 安全报警 ====================
@@ -773,6 +1123,7 @@ public class DatabaseManager {
} catch (SQLException e) {
e.printStackTrace();
}
}
public List<Map<String, Object>>
+470 -3
View File
@@ -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) {
+227 -115
View File
@@ -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;
@@ -127,6 +131,8 @@ public class Main extends JavaPlugin
public CommissionManager getCommission() {
return commission;
}
//pvp
private PVPManager pvpManager;
private static class PwdRollback {
final String hash;
@@ -165,6 +171,7 @@ public class Main extends JavaPlugin
return db;
}
public ConfigManager getConfig2() {
return config;
}
@@ -258,6 +265,9 @@ public class Main extends JavaPlugin
// ===== 2. 数据库 =====
db = new DatabaseManager(getDataFolder());
db.init();
menuIconMgr = new MenuIconManager(this);
// ===== 3. 菜单 =====
menu = new MenuManager(this);
@@ -347,7 +357,6 @@ public class Main extends JavaPlugin
startPasswordReminder();
startTicketAutoProcess();
radio.startMainRadio();
new BukkitRunnable() {
public void run() {
if (questTracker == null) return;
@@ -360,9 +369,73 @@ public class Main extends JavaPlugin
}
}.runTaskTimer(this, 600L, 600L);
// 12 ===PVP===
pvpManager = new PVPManager(this);
getServer().getPluginManager()
.registerEvents(pvpManager, this);
// PVP区域检测定时器
getServer().getScheduler()
.runTaskTimer(this,
() -> pvpManager.tickRegions(),
20L, 20L);
setupPVPTab();
getLogger().info("Sdf1_login v1.0 | 就绪");
}
private void setupPVPTab() {
if (getCommand("pvp") != null) {
getCommand("pvp")
.setTabCompleter(
(sender, cmd,
alias, args) -> {
List<String> list =
new ArrayList<>();
if (args.length == 1) {
String[] sub = {
"create", "stats",
"list", "delete",
"tool", "on", "off",
"tempban"};
for (String s : sub) {
if (s.startsWith(
args[0]
.toLowerCase())) {
list.add(s);
}
}
} else if (args.length == 2) {
String sub =
args[0].toLowerCase();
if (sub.equals("stats")) {
for (Player pp : Bukkit
.getOnlinePlayers()) {
if (pp.getName()
.toLowerCase()
.startsWith(args[1]
.toLowerCase())) {
list.add(pp.getName());
}
}
} else if (sub.equals("delete")
|| sub.equals("on")
|| sub.equals("off")
|| sub.equals("tempban")) {
for (String rn :
pvpManager
.getRegionNames()) {
if (rn.toLowerCase()
.startsWith(args[1]
.toLowerCase())) {
list.add(rn);
}
}
}
}
return list;
});
}
}
@Override
public void onDisable() {
@@ -1243,10 +1316,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 +1523,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 +1660,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 +1725,6 @@ public class Main extends JavaPlugin
return;
}
// 同时检查主手和副手
ItemStack hand = p.getInventory()
.getItemInMainHand();
ItemStack offhand = p.getInventory()
@@ -1593,6 +1734,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);
}
}
}
@@ -2023,6 +2173,9 @@ public class Main extends JavaPlugin
public MenuManager getMenu() {
return menu;
}
public PVPManager getPVPManager() {
return pvpManager;
}
// ===== Inventory Click(旧逻辑 + 新主菜单布局 + 新功能) =====
@@ -2411,7 +2564,6 @@ public class Main extends JavaPlugin
break;
}}
// ==================== 主菜单(新布局) ====================
// ===== 主菜单 =====
if (title.equals(GUIManager.T_MAIN)) {
e.setCancelled(true);
@@ -2479,10 +2631,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 +2723,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 +2770,7 @@ public class Main extends JavaPlugin
return;
}
// ==================== 管理面板 ====================
if (title.equals(GUIManager.T_ADMIN)) {
e.setCancelled(true);
@@ -2838,95 +2998,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 +3162,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垃圾回收站")) {
@@ -3153,6 +3254,17 @@ public class Main extends JavaPlugin
String[] args) {
String cmdName = cmd.getName().toLowerCase();
if (label.equalsIgnoreCase("pvp")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可用");
return true;
}
Player p = (Player) sender;
return pvpManager.onCommand(p, args);
}
// ===== /chat 独立命令 =====
if (cmdName.equals("chat")
|| cmdName.equals("聊天")) {
@@ -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);
}
}
File diff suppressed because it is too large Load Diff
+108
View File
@@ -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 {
+4 -1
View File
@@ -86,4 +86,7 @@ commands:
usage: /垃圾清理
menu:
description: 雪球菜单开关控制
usage: /menu <开/关>
usage: /menu <开/关>
pvp :
description: PVP榜单
usage: /pvp