GUIFriends.java v1.0
互联网风格全GUI的数据库好友系统,支持备注、申请、红点提醒、分页等社交互动
命令列表
- friend打开好友主界面,所有操作均用可视交互
package org.sircustom.guifriends;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
/**
* @pluginName GUIFriends
* @author ScriptIrc Engine
* @version 1.0
* @description 互联网风格全GUI的数据库好友系统,支持备注、申请、红点提醒、分页等社交互动
* [command]friend|打开好友主界面,所有操作均用可视交互[/command]
* [Permission]guifriends.admin|GUI好友系统管理权限[/Permission]
*/
public class GUIFriends extends JavaPlugin implements Listener, TabCompleter {
private static final String DB_FILE = "guifriends.sqlite";
private Connection dataSource;
// 申请/留言/备注输入 等临时状态维护
private final Map<UUID, InputState> inputStates = new ConcurrentHashMap<>();
// GUI 页码等状态
private final Map<UUID, Integer> guiPage = new ConcurrentHashMap<>();
// GUI上下文 记录玩家在做什么
private final Map<UUID, String> guiContext = new ConcurrentHashMap<>();
// 分页默认参数
private static final int FRIENDS_PER_PAGE = 21; // 一行7个,三行好友格
private static final int GUI_SIZE = 27; // 箱子样式:3行
private static final String GUI_TITLE = ChatColor.AQUA + "§l好友广场";
// -- 自定义输入等待类型 --
private enum InputType { ADD, REMARK, MSG, LEAVE }
private static class InputState {
InputType type;
String toWho; // 目标玩家
InputState(InputType t, String n) {
type = t; toWho = n;
}
}
@Override
public void onEnable() {
Bukkit.getPluginManager().registerEvents(this, this);
Objects.requireNonNull(getCommand("friend")).setTabCompleter(this);
initDatabase();
getLogger().info("GUIFriends 数据库/事件/GUIs 已加载");
}
@Override
public void onDisable() {
try { if (dataSource != null) dataSource.close(); } catch (Exception ignored) {}
getLogger().info("GUIFriends 关闭");
}
// ======================= 数据库初始化 =======================
private void initDatabase() {
try {
File dbFile = new File(getDataFolder(), DB_FILE);
if (!getDataFolder().exists()) getDataFolder().mkdirs();
if (!dbFile.exists()) dbFile.createNewFile();
Class.forName("org.sqlite.JDBC");
dataSource = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getPath());
Statement st = dataSource.createStatement();
st.executeUpdate("CREATE TABLE IF NOT EXISTS friends (id INTEGER PRIMARY KEY AUTOINCREMENT, player1 VARCHAR(16), player2 VARCHAR(16), since VARCHAR(20), remark1 TEXT, remark2 TEXT, apply_msg TEXT, apply_pending INTEGER, unread INTEGER, last_online1 VARCHAR(20), last_online2 VARCHAR(20), last_msg_time VARCHAR(20))");
st.executeUpdate("CREATE TABLE IF NOT EXISTS friend_messages (id INTEGER PRIMARY KEY AUTOINCREMENT, sender VARCHAR(16), receiver VARCHAR(16), content TEXT, time VARCHAR(20), is_read INTEGER DEFAULT 0)");
st.executeUpdate("CREATE TABLE IF NOT EXISTS friend_leaves (id INTEGER PRIMARY KEY AUTOINCREMENT, from_p VARCHAR(16), to_p VARCHAR(16), content TEXT, time VARCHAR(20))");
st.close();
} catch (Exception e) {
getLogger().log(Level.SEVERE, "数据库初始化异常", e);
}
}
// ======================= 指令入口:打开主界面 =======================
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player)) { sender.sendMessage("玩家专用!"); return true; }
Player p = (Player)sender;
openMainGUI(p, 1);
return true;
}
// ======================= 主箱子GUI渲染 =======================
public void openMainGUI(Player player, int page) {
guiContext.put(player.getUniqueId(), "main");
guiPage.put(player.getUniqueId(), page);
// 查自己好友
List<FriendListEntry> friends = getFriendEntries(player.getName());
int totalPage = (int)Math.ceil(friends.size() * 1.0 / FRIENDS_PER_PAGE);
page = Math.max(1, Math.min(totalPage == 0 ? 1 : totalPage, page));
List<FriendListEntry> show = new ArrayList<>();
int start = (page-1)*FRIENDS_PER_PAGE;
for (int i=start; i<start+FRIENDS_PER_PAGE && i<friends.size(); i++)
show.add(friends.get(i));
Inventory inv = Bukkit.createInventory(null, GUI_SIZE, GUI_TITLE + ChatColor.DARK_GRAY + " §7[" + page + "/" + (totalPage==0?1:totalPage) + "]");
int[] friendSlots = {10,11,12,13,14,15,16, 19,20,21,22,23,24,25};
int idx=0;
for (FriendListEntry ent : show) {
if (idx>=friendSlots.length) break;
ItemStack head = buildFriendHead(ent);
inv.setItem(friendSlots[idx], head); idx++;
}
// 右下角添加好友按钮
ItemStack addFriend = new ItemStack(Material.EMERALD_BLOCK);
ItemMeta im = addFriend.getItemMeta();
im.setDisplayName(ChatColor.GOLD + "➕ 添加好友");
addFriend.setItemMeta(im);
inv.setItem(8, addFriend);
// 导航按钮
if (page > 1) inv.setItem(18, navItem("上一页", Material.ARROW));
if (page < totalPage) inv.setItem(26, navItem("下一页", Material.ARROW));
// 搜索好友按钮
inv.setItem(0, navItem("🔍 搜索/申请", Material.PAPER));
player.openInventory(inv);
}
// 构造好友头像条目
private ItemStack buildFriendHead(FriendListEntry ent) {
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
ItemMeta meta = skull.getItemMeta();
String disp = (ent.online?ChatColor.GREEN:"")
+ "§l" + ent.friendName
+ (ent.unread>0?ChatColor.RED+" ●":"");
if (ent.remark!=null && !ent.remark.isEmpty()) {
disp += ChatColor.DARK_AQUA + " (" + ent.remark + ")";
}
meta.setDisplayName(disp);
List<String> lore = new ArrayList<>();
if (ent.online) lore.add(ChatColor.GREEN+"[在线] 互动于: "+ent.lastMsgTime);
else lore.add(ChatColor.GRAY+"[离线] 最后在线: "+ent.lastOnline);
if (ent.applyPending) lore.add(ChatColor.YELLOW+"[等待验证对方同意]");
if (ent.remark!=null && !ent.remark.isEmpty()) lore.add(ChatColor.AQUA+"备注: "+ent.remark);
lore.add("");
lore.add(ChatColor.GOLD+"左键:进入聊天对话");
lore.add(ChatColor.YELLOW+"右键:更多操作(删除/备注/留言)");
meta.setLore(lore);
skull.setItemMeta(meta);
return skull;
}
// 分页/导航物品
private ItemStack navItem(String name, Material mat) {
ItemStack it = new ItemStack(mat); ItemMeta im = it.getItemMeta();
im.setDisplayName(ChatColor.AQUA + name);
it.setItemMeta(im); return it;
}
// ======================= 交互事件:箱子点击 =======================
@EventHandler
public void onInventoryClick(InventoryClickEvent e) {
if (!(e.getWhoClicked() instanceof Player)) return;
Player p = (Player)e.getWhoClicked();
Inventory inv = e.getInventory();
if (e.getView().getTitle().startsWith(GUI_TITLE)) {
e.setCancelled(true);
int slot = e.getRawSlot();
if (slot<0 || slot>=GUI_SIZE) return;
ItemStack clicked = e.getCurrentItem();
if (clicked==null || !clicked.hasItemMeta() || !clicked.getItemMeta().hasDisplayName()) return;
String dn = ChatColor.stripColor(clicked.getItemMeta().getDisplayName());
if (slot == 8) { // 添加好友
inputStates.put(p.getUniqueId(), new InputState(InputType.ADD, null));
p.closeInventory(); p.sendMessage(ChatColor.GOLD+"请输入要添加的玩家名:");
return;
}
if (slot == 0) {// 搜索/申请入口
inputStates.put(p.getUniqueId(), new InputState(InputType.ADD, null));
p.closeInventory(); p.sendMessage(ChatColor.YELLOW+"请输入部分ID回车,可模糊查找;如输入完整ID将直接发起申请。");
return;
}
if (slot == 18) { // 上一页
openMainGUI(p, guiPage.getOrDefault(p.getUniqueId(), 1)-1); return;
}
if (slot == 26) { //下一页
openMainGUI(p, guiPage.getOrDefault(p.getUniqueId(), 1)+1); return;
}
// 判断是否好友条目:通过Material.Player_Head
if (clicked.getType()==Material.PLAYER_HEAD) {
String friendName = dn.replaceAll("●.*", "").trim();
if (e.isLeftClick()) {
// 打开聊天历史/输入界面
openFriendChatGUI(p, friendName, 1);
} else if (e.isRightClick()) {
// 弹出操作面板:备注、留言、删除
openFriendOpsGUI(p, friendName);
}
}
}
// 其他界面略,可拓展更多界面类型如openFriendChatGUI操作
}
// 右键好友弹窗(菜单)
public void openFriendOpsGUI(Player p, String friendName) {
Inventory inv = Bukkit.createInventory(null, 9, ChatColor.AQUA+"好友管理:"+friendName);
inv.setItem(2, navItem("📝 备注", Material.PAPER));
inv.setItem(4, navItem("📨 留言", Material.BOOK));
inv.setItem(6, navItem("❌ 删除", Material.BARRIER));
p.openInventory(inv);
guiContext.put(p.getUniqueId(), "ops:"+friendName);
}
@EventHandler
public void onSubInvClick(InventoryClickEvent e) {
if (!(e.getWhoClicked() instanceof Player)) return;
Player p = (Player)e.getWhoClicked();
String ctx = guiContext.getOrDefault(p.getUniqueId(),"null");
if (!e.getView().getTitle().startsWith("好友管理:")) return;
e.setCancelled(true);
ItemStack it = e.getCurrentItem();
if (it==null || !it.hasItemMeta()) return;
String name = ChatColor.stripColor(it.getItemMeta().getDisplayName());
String friendName = e.getView().getTitle().replace("好友管理:","").trim();
if (name.contains("备注")) {
inputStates.put(p.getUniqueId(), new InputState(InputType.REMARK, friendName));
p.closeInventory(); p.sendMessage(ChatColor.YELLOW+"请输入新的备注内容,或输入“清空”删除备注。");
} else if (name.contains("留言")) {
inputStates.put(p.getUniqueId(), new InputState(InputType.LEAVE, friendName));
p.closeInventory(); p.sendMessage(ChatColor.YELLOW+"请输入留言内容:");
} else if (name.contains("删除")) {
delFriend(p, friendName); p.closeInventory();
p.sendMessage(ChatColor.RED+"已删除好友 "+friendName);
Bukkit.getScheduler().runTaskLater(this,()->openMainGUI(p,1),10L);
}
}
// 好友聊天GUI[简要,用书本组件实现]
public void openFriendChatGUI(Player p, String friendName, int page) {
List<MsgEntry> msgs = getFriendChatMsgs(p.getName(), friendName, 10, (page-1)*10);
// 这里只简单用聊天栏互动,请自行参考进一步用书本/箱子多页完善
p.closeInventory();
p.sendMessage(ChatColor.AQUA+"与 "+friendName+" 聊天(共"+msgs.size()+"条):");
for (MsgEntry m:msgs) {
p.sendMessage((m.from.equals(p.getName()) ? ChatColor.YELLOW + "我: " : ChatColor.GRAY + m.from + ": ") + m.content + ChatColor.GRAY + " ["+m.time+"]");
}
inputStates.put(p.getUniqueId(), new InputState(InputType.MSG, friendName));
p.sendMessage(ChatColor.GOLD+"请输入聊天消息内容,或输入“退出”返回主界面。");
}
// ======================= 聊天/备注/申请/留言输入交互 =======================
@EventHandler
public void onChatInput(AsyncPlayerChatEvent e) {
Player p = e.getPlayer();
UUID uid = p.getUniqueId();
if (!inputStates.containsKey(uid)) return;
e.setCancelled(true);
String text = e.getMessage();
InputState state = inputStates.remove(uid);
switch (state.type) {
case ADD:
if (text.length()<=0) { p.sendMessage(ChatColor.RED+"不可为空名!"); return;}
addFriendApply(p, text, ""); // 本例演示默认备注与申请留言留空
break;
case MSG:
if ("退出".equalsIgnoreCase(text)) {
Bukkit.getScheduler().runTask(this,()->openMainGUI(p,1));
return;
}
sendChatMsg(p, state.toWho, text);
Bukkit.getScheduler().runTask(this,()->openFriendChatGUI(p, state.toWho, 1));
break;
case LEAVE:
leaveMsg(p, state.toWho, text);
p.sendMessage(ChatColor.GREEN+"已留言给 "+state.toWho);
Bukkit.getScheduler().runTask(this,()->openMainGUI(p,1));
break;
case REMARK:
setRemark(p, state.toWho, "清空".equalsIgnoreCase(text) ? "" : text);
p.sendMessage(ChatColor.GREEN+"备注已更新!");
Bukkit.getScheduler().runTask(this,()->openMainGUI(p,1));
break;
}
}
// ========== 数据库相关 - 好友条目 ==========
private static class FriendListEntry {
String friendName, remark, lastOnline, lastMsgTime;
boolean online = false, applyPending = false;
int unread = 0;
FriendListEntry(String n, boolean o, String r, boolean ap, int ur, String lo, String lm) {
friendName=n; online=o; remark=r; applyPending=ap; unread=ur; lastOnline=lo; lastMsgTime=lm;
}
}
private List<FriendListEntry> getFriendEntries(String myName) {
List<FriendListEntry> res = new ArrayList<>();
try {
PreparedStatement ps = dataSource.prepareStatement(
"SELECT * FROM friends WHERE player1=? OR player2=?"); ps.setString(1, myName); ps.setString(2,myName);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
String p1=rs.getString("player1"), p2=rs.getString("player2");
boolean meP1=myName.equalsIgnoreCase(p1);
String other = meP1?p2:p1;
String remark = rs.getString(meP1?"remark1":"remark2");
boolean pending = rs.getInt("apply_pending")==1;
int unread = rs.getInt("unread");
String lo = rs.getString(meP1?"last_online2":"last_online1");
String lm = rs.getString("last_msg_time");
boolean online = Bukkit.getPlayerExact(other)!=null;
res.add(new FriendListEntry(other, online, remark, pending, unread, lo, lm));
}
rs.close(); ps.close();
} catch (Exception e) {}
res.sort((a,b)->Boolean.compare(b.online,a.online)); // 在线优先
return res;
}
// 聊天记录获取(简版,分页,未读管理略)
private static class MsgEntry { String from, content, time;
MsgEntry(String f, String c, String t){from=f;content=c;time=t;}
}
private List<MsgEntry> getFriendChatMsgs(String my, String fr, int count, int skip) {
List<MsgEntry> res = new ArrayList<>();
try {
PreparedStatement ps = dataSource.prepareStatement(
"SELECT * FROM friend_messages WHERE (sender=? AND receiver=?) OR (sender=? AND receiver=?) ORDER BY id DESC LIMIT ? OFFSET ?");
ps.setString(1,my);ps.setString(2,fr);ps.setString(3,fr);ps.setString(4,my);ps.setInt(5,count);ps.setInt(6,skip);
ResultSet rs=ps.executeQuery();
while (rs.next()) res.add(new MsgEntry(rs.getString("sender"),rs.getString("content"), rs.getString("time")));
rs.close(); ps.close();
} catch(Exception x){}
Collections.reverse(res);
return res;
}
// ========== 数据库相关 - 添加/删除/备注/聊天/留言 ==========
private void addFriendApply(Player p, String to, String msg) {
if (p.getName().equalsIgnoreCase(to)) { p.sendMessage("不可以添加自己为好友"); return; }
if (isFriend(p.getName(), to)) { p.sendMessage("已是好友"); return; }
// 暂简化为直接添加双向好友记录且等待同意,实际可做申请流程
try (PreparedStatement st = dataSource.prepareStatement(
"INSERT INTO friends(player1, player2, since, apply_msg, apply_pending, unread, last_online1, last_online2, last_msg_time) VALUES (?,?,?,?,1,0,?,?,?)")) {
String now = nowStr();
st.setString(1, p.getName()); st.setString(2, to);
st.setString(3, now); st.setString(4, msg);
st.setString(5, now); st.setString(6, now); st.setString(7, now);
st.executeUpdate();
p.sendMessage(ChatColor.GREEN+"已发出添加好友请求给 "+to);
Player t = Bukkit.getPlayerExact(to);
if (t!=null) t.sendMessage(ChatColor.YELLOW+p.getName()+" 请求添加你为好友, 请同意!");
} catch (Exception e) { p.sendMessage("申请失败:"+e.getMessage()); }
}
private void delFriend(Player p, String fr) {
try (PreparedStatement st=dataSource.prepareStatement(
"DELETE FROM friends WHERE (player1=? AND player2=?) OR (player1=? AND player2=?)")) {
st.setString(1,p.getName()); st.setString(2,fr); st.setString(3,fr); st.setString(4,p.getName());
st.executeUpdate();
} catch (Exception ignored){}
}
private void setRemark(Player p, String fr, String remark) {
String sql = "UPDATE friends SET remark1=? WHERE player1=? AND player2=?";
String alt = "UPDATE friends SET remark2=? WHERE player2=? AND player1=?";
try {
int n = 0;
try (PreparedStatement ps = dataSource.prepareStatement(sql)) {
ps.setString(1, remark); ps.setString(2,p.getName()); ps.setString(3,fr);
n = ps.executeUpdate();
}
if (n==0) try (PreparedStatement ps = dataSource.prepareStatement(alt)) {
ps.setString(1, remark); ps.setString(2,p.getName()); ps.setString(3,fr);
ps.executeUpdate();
}
} catch (Exception ignored) {}
}
private boolean isFriend(String a, String b) {
try (PreparedStatement ps = dataSource.prepareStatement("SELECT 1 FROM friends WHERE (player1=? AND player2=?) OR (player1=? AND player2=?)")) {
ps.setString(1,a);ps.setString(2,b);ps.setString(3,b);ps.setString(4,a);
try(ResultSet rs=ps.executeQuery()){ return rs.next(); }
}catch(Exception e){return false;}
}
private void sendChatMsg(Player p, String fr, String msg) {
String now=nowStr();
try (PreparedStatement st=dataSource.prepareStatement(
"INSERT INTO friend_messages(sender,receiver,content,time,is_read) VALUES(?,?,?,?,'0')")) {
st.setString(1,p.getName());st.setString(2,fr);st.setString(3,msg);st.setString(4,now);st.executeUpdate();
p.sendMessage("§e[我→"+fr+"]:"+msg);
Player t=Bukkit.getPlayerExact(fr);
if (t!=null) t.sendMessage("§a["+p.getName()+"(好友) →你]:"+msg);
} catch (Exception e){}
}
private void leaveMsg(Player p, String fr, String msg) {
try (PreparedStatement st=dataSource.prepareStatement(
"INSERT INTO friend_leaves(from_p, to_p, content, time) VALUES (?,?,?,?)")) {
st.setString(1,p.getName());st.setString(2,fr);st.setString(3,msg);st.setString(4,nowStr());
st.executeUpdate();
} catch (Exception ignored) {}
}
private String nowStr(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date());
}
// 自动Tab补全
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) {
return Collections.emptyList(); // 全部GUI操作无需补全
}
// 离线更新好友表最后一次在线
@EventHandler
public void onQuit(PlayerQuitEvent event) {
String name=event.getPlayer().getName(), now=nowStr();
try (PreparedStatement ps=dataSource.prepareStatement(
"UPDATE friends SET last_online1=CASE WHEN player1=? THEN ? ELSE last_online1 END, last_online2=CASE WHEN player2=? THEN ? ELSE last_online2 END WHERE player1=? OR player2=?")) {
ps.setString(1, name);ps.setString(2,now);ps.setString(3,name);ps.setString(4,now);ps.setString(5,name);ps.setString(6,name);
ps.executeUpdate();
}catch(Exception ignore){}
}
}