UIPosts.java v2.0
支持PlaceholderAPI和多UI自定义,配置驱动贴子系统,Chest+书本,数据库存储,指令分页
命令列表
- uiposts打开或操作多UI贴子界面,参数支持chest/book/reload等
package org.abc.untitled1;
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.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import javax.sql.DataSource;
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 UIPosts
* @author ScriptIrc Engine
* @version 2.0
* @description 支持PlaceholderAPI和多UI自定义,配置驱动贴子系统,Chest+书本,数据库存储,指令分页
* [command]uiposts|打开或操作多UI贴子界面,参数支持chest/book/reload等[/command]
* [Permission]uiposts.admin|UIPosts管理权限[/Permission]
*/
public class UIPosts extends JavaPlugin implements Listener, TabCompleter {
private static final String DB_FILE = "uiposts.sqlite";
private DataSource dataSource;
private Map<String, ConfigurationSection> guiConfig = new HashMap<>();
private final Map<UUID, Boolean> awaitingPostInput = new ConcurrentHashMap<>();
private final Map<UUID, Integer> awaitingReply = new ConcurrentHashMap<>();
private final Map<Integer, List<Reply>> replyCache = new ConcurrentHashMap<>();
private String cachedTime;
private static final int MAX_POST_LENGTH = 100;
private static final int MAX_REPLY_LENGTH = 50;
private static final int CACHE_TTL = 300;
private static final int MAX_PAGES = 100;
@Override
public void onEnable() {
saveDefaultConfig();
reloadConfig();
Bukkit.getPluginManager().registerEvents(this, this);
Objects.requireNonNull(getCommand("uiposts")).setTabCompleter(this);
initDatabase();
loadConfig();
startScheduledTasks();
getLogger().info("UIPosts v2.0 已启动 - 配置已加载,数据库已连接");
}
@Override
public void onDisable() {
replyCache.clear();
getLogger().info("UIPosts 已安全关闭");
}
private void loadConfig() {
FileConfiguration config = getConfig();
guiConfig.put("chest", config.getConfigurationSection("gui.chest"));
guiConfig.put("book", config.getConfigurationSection("gui.book"));
getLogger().info("已加载GUI配置");
}
private void reloadPluginConfig() {
reloadConfig();
loadConfig();
getLogger().info("配置已重载");
}
private void initDatabase() {
try {
File dbFile = new File(getDataFolder(), DB_FILE);
if (!getDataFolder().exists()) { getDataFolder().mkdirs(); }
boolean isNewDB = !dbFile.exists();
if (isNewDB) { dbFile.createNewFile(); }
dataSource = new org.sqlite.SQLiteDataSource();
((org.sqlite.SQLiteDataSource) dataSource).setUrl("jdbc:sqlite:" + dbFile.getPath());
try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
if (isNewDB) {
st.executeUpdate("CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, player VARCHAR(16) NOT NULL, content TEXT NOT NULL, time VARCHAR(20) NOT NULL)");
st.executeUpdate("CREATE TABLE replies (id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER NOT NULL, player VARCHAR(16) NOT NULL, content TEXT NOT NULL, time VARCHAR(20) NOT NULL, FOREIGN KEY(post_id) REFERENCES posts(id))");
st.executeUpdate("CREATE INDEX idx_post_id ON replies(post_id)");
getLogger().info("已创建新数据库");
}
}
} 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(ChatColor.RED + "仅玩家可以使用此命令");
return true;
}
Player player = (Player) sender;
if (args.length > 0 && "reload".equalsIgnoreCase(args[0]) && player.hasPermission("uiposts.admin")) {
reloadPluginConfig();
player.sendMessage(ChatColor.GREEN + "配置已重载");
return true;
}
String type = (args.length == 0) ? "chest" : args[0].toLowerCase();
int page = 1;
if (args.length >= 2) {
try {
page = Integer.parseInt(args[1]);
page = Math.max(1, Math.min(page, MAX_PAGES));
} catch (NumberFormatException e) {
player.sendMessage(ChatColor.RED + "无效的页码");
return true;
}
}
switch (type) {
case "chest":
openChestGUI(player, page);
break;
case "book":
openBookGUI(player, page);
break;
default:
player.sendMessage(ChatColor.YELLOW + "用法: /uiposts [chest|book] [页码]");
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) {
if (args.length == 1) {
return Arrays.asList("chest", "book", "reload");
}
return Collections.emptyList();
}
private void openChestGUI(Player player, int page) {
ConfigurationSection cfg = guiConfig.get("chest");
int size = cfg.getInt("size", 27);
String title = parsePlaceholders(cfg.getString("title", "§b§l帖子广场"), player, page);
Inventory inv = Bukkit.createInventory(null, size, title);
int postsPerPage = cfg.getIntegerList("post_slot").size();
List<Post> posts = getPostsByPage(page, postsPerPage);
List<Integer> postSlots = cfg.getIntegerList("post_slot");
for (int i = 0; i < Math.min(posts.size(), postSlots.size()); i++) {
Post post = posts.get(i);
inv.setItem(postSlots.get(i), createPostItem(post));
}
inv.setItem(cfg.getInt("post_button_slot", 4), createButton(
Material.ANVIL,
cfg.getString("post_button", "§a§l我要发帖")
));
inv.setItem(cfg.getInt("book_button_slot", 22), createButton(
Material.WRITTEN_BOOK,
cfg.getString("jump_book", "§6查看书本模式")
));
if (hasNextPage(page, postsPerPage)) {
inv.setItem(cfg.getInt("next_slot", 25), createButton(
Material.ARROW,
"§e下一页"
));
}
if (page > 1) {
inv.setItem(cfg.getInt("prev_slot", 19), createButton(
Material.ARROW,
"§e上一页"
));
}
player.openInventory(inv);
storePlayerState(player, "chest", page);
}
private void openBookGUI(Player player, int page) {
ConfigurationSection cfg = guiConfig.get("book");
int postsPerPage = cfg.getInt("posts_per_page", 5);
ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
BookMeta meta = (BookMeta) book.getItemMeta();
meta.setTitle(parsePlaceholders(cfg.getString("title", "§b§l帖子广场"), player, page));
meta.setAuthor("UIPosts");
List<String> pages = generateBookPages(page, postsPerPage);
meta.setPages(pages);
book.setItemMeta(meta);
ItemStack oldItem = player.getInventory().getItemInMainHand().clone();
player.getInventory().setItemInMainHand(book);
new BukkitRunnable() {
@Override
public void run() {
player.openBook(book);
player.getInventory().setItemInMainHand(oldItem);
}
}.runTaskLater(this, 2L);
storePlayerState(player, "book", page);
}
private List<String> generateBookPages(int currentPage, int postsPerPage) {
List<String> pages = new ArrayList<>();
List<Post> posts = getPostsByPage(currentPage, postsPerPage);
if (posts.isEmpty()) {
pages.add("§7暂无帖子\n§a请使用箱子界面发帖!");
return pages;
}
StringBuilder currentContent = new StringBuilder();
for (Post post : posts) {
String postText = formatPostForBook(post);
if (currentContent.length() + postText.length() > 1500) {
pages.add(currentContent.toString());
currentContent = new StringBuilder();
}
currentContent.append(postText);
}
if (currentContent.length() > 0) {
pages.add(currentContent.toString());
}
pages.add(generateNavigationPage(currentPage));
return pages;
}
private String generateNavigationPage(int currentPage) {
int totalPages = getTotalPages(guiConfig.get("book").getInt("posts_per_page", 5));
StringBuilder sb = new StringBuilder("§l§n导航菜单§r\n\n");
sb.append("§7当前页: §e").append(currentPage).append("§7/§e").append(totalPages).append("\n\n");
sb.append("§6[命令导航]\n");
if (currentPage > 1) {
sb.append("§a/up book ").append(currentPage - 1).append(" §7- 上一页\n");
}
if (currentPage < totalPages) {
sb.append("§a/up book ").append(currentPage + 1).append(" §7- 下一页\n");
}
sb.append("§a/up chest §7- 返回箱子界面\n");
return sb.toString();
}
@EventHandler
public void onInventoryClick(org.bukkit.event.inventory.InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player)) return;
Player player = (Player) event.getWhoClicked();
ItemStack clicked = event.getCurrentItem();
if (clicked == null || !clicked.hasItemMeta()) return;
if (!isUIPostsInventory(event.getView().getTitle())) return;
event.setCancelled(true);
PlayerState state = getPlayerState(player);
if (state == null) return;
String displayName = ChatColor.stripColor(clicked.getItemMeta().getDisplayName());
if (clicked.getType() == Material.ANVIL && displayName.contains("发帖")) {
player.closeInventory();
askForPostInput(player);
} else if (clicked.getType() == Material.WRITTEN_BOOK && displayName.contains("书本模式")) {
player.closeInventory();
openBookGUI(player, 1);
} else if (clicked.getType() == Material.ARROW) {
handlePaginationClick(player, state, displayName);
} else if (clicked.getType() == Material.WRITABLE_BOOK) {
handlePostClick(player, event.getSlot(), state.page);
}
}
@EventHandler
public void onChatInput(org.bukkit.event.player.AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
UUID uuid = player.getUniqueId();
String message = event.getMessage();
if (awaitingPostInput.containsKey(uuid)) {
event.setCancelled(true);
if ("取消".equalsIgnoreCase(message)) {
player.sendMessage(ChatColor.RED + "已取消发帖");
awaitingPostInput.remove(uuid);
return;
}
Bukkit.getScheduler().runTask(this, () -> {
savePost(player, message);
awaitingPostInput.remove(uuid);
openChestGUI(player, 1);
});
}
if (awaitingReply.containsKey(uuid)) {
event.setCancelled(true);
if ("取消".equalsIgnoreCase(message)) {
player.sendMessage(ChatColor.RED + "已取消留言");
awaitingReply.remove(uuid);
return;
}
int postId = awaitingReply.get(uuid);
Bukkit.getScheduler().runTask(this, () -> {
saveReply(player, postId, message);
awaitingReply.remove(uuid);
showBookForPost(player, getPost(postId));
});
}
}
private List<Post> getPostsByPage(int page, int perPage) {
int offset = (page - 1) * perPage;
List<Post> posts = new ArrayList<>();
if (perPage <= 0) return posts;
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT id, player, content, time FROM posts ORDER BY id DESC LIMIT ? OFFSET ?")) {
ps.setInt(1, perPage);
ps.setInt(2, offset);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
posts.add(new Post(
rs.getInt("id"),
rs.getString("player"),
rs.getString("content"),
rs.getString("time")
));
}
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "查询帖子失败", e);
}
return posts;
}
private List<Reply> getReplies(int postId) {
if (replyCache.containsKey(postId)) {
return replyCache.get(postId);
}
List<Reply> replies = new ArrayList<>();
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT player, content, time FROM replies WHERE post_id = ? ORDER BY id ASC")) {
ps.setInt(1, postId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
replies.add(new Reply(
rs.getString("player"),
rs.getString("content"),
rs.getString("time")
));
}
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "查询留言失败", e);
}
replyCache.put(postId, replies);
return replies;
}
private void savePost(Player player, String content) {
String time = getCurrentTime();
String playerName = player.getName();
if (content.length() > MAX_POST_LENGTH) {
content = content.substring(0, MAX_POST_LENGTH);
}
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO posts(player, content, time) VALUES(?, ?, ?)")) {
ps.setString(1, playerName);
ps.setString(2, content);
ps.setString(3, time);
ps.executeUpdate();
replyCache.clear();
player.sendMessage(ChatColor.GREEN + "帖子发布成功!");
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "保存帖子失败", e);
player.sendMessage(ChatColor.RED + "发帖失败,请稍后再试");
}
}
private void saveReply(Player player, int postId, String content) {
String time = getCurrentTime();
String playerName = player.getName();
if (content.length() > MAX_REPLY_LENGTH) {
content = content.substring(0, MAX_REPLY_LENGTH);
}
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO replies(post_id, player, content, time) VALUES(?, ?, ?, ?)")) {
ps.setInt(1, postId);
ps.setString(2, playerName);
ps.setString(3, content);
ps.setString(4, time);
ps.executeUpdate();
replyCache.remove(postId);
player.sendMessage(ChatColor.GREEN + "留言成功!");
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "保存留言失败", e);
player.sendMessage(ChatColor.RED + "留言失败,请稍后再试");
}
}
private ItemStack createPostItem(Post post) {
ItemStack item = new ItemStack(Material.WRITABLE_BOOK);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(ChatColor.GREEN + "【" + post.player + "】 - " + post.time);
List<String> lore = new ArrayList<>();
lore.add(ChatColor.WHITE + truncate(post.content, 25));
lore.add(ChatColor.GRAY + "点击查看/留言");
meta.setLore(lore);
item.setItemMeta(meta);
return item;
}
private ItemStack createButton(Material material, String name) {
ItemStack item = new ItemStack(material);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(name);
item.setItemMeta(meta);
return item;
}
private String formatPostForBook(Post post) {
StringBuilder sb = new StringBuilder();
sb.append(ChatColor.BLUE).append("【").append(post.player).append("】 ")
.append(ChatColor.DARK_GRAY).append(post.time).append("\n");
sb.append(ChatColor.BLACK).append(post.content).append("\n\n");
List<Reply> replies = getReplies(post.id);
if (!replies.isEmpty()) {
sb.append(ChatColor.DARK_GRAY).append("--- 留言 ---\n");
int maxReplies = Math.min(replies.size(), 3);
for (int i = 0; i < maxReplies; i++) {
Reply reply = replies.get(i);
sb.append(ChatColor.GRAY).append("• ").append(reply.player)
.append(": ").append(ChatColor.DARK_GREEN).append(reply.content)
.append("\n");
}
if (replies.size() > 3) {
sb.append(ChatColor.GRAY).append("... 还有")
.append(replies.size() - 3).append("条留言\n");
}
sb.append("\n").append(ChatColor.DARK_AQUA)
.append("点击书本外区域输入留言内容");
} else {
sb.append(ChatColor.GRAY).append("暂无留言\n");
}
sb.append("\n");
return sb.toString();
}
private void askForPostInput(Player player) {
player.sendMessage(ChatColor.GOLD + "请在聊天栏输入您的帖子内容(最多100字)");
player.sendMessage(ChatColor.GRAY + "输入" + ChatColor.RED + "取消" + ChatColor.GRAY + "来取消操作");
awaitingPostInput.put(player.getUniqueId(), true);
}
private void askForReplyInput(Player player, int postId) {
player.sendMessage(ChatColor.GOLD + "请输入您的留言内容(最多50字)");
player.sendMessage(ChatColor.GRAY + "输入" + ChatColor.RED + "取消" + ChatColor.GRAY + "来取消操作");
awaitingReply.put(player.getUniqueId(), postId);
}
private void showBookForPost(Player player, Post post) {
if (post == null) {
player.sendMessage(ChatColor.RED + "帖子不存在或已被删除");
return;
}
ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
BookMeta meta = (BookMeta) book.getItemMeta();
meta.setTitle("帖子详情");
meta.setAuthor("UIPosts");
meta.setPages(Collections.singletonList(formatPostForBook(post)));
book.setItemMeta(meta);
ItemStack oldItem = player.getInventory().getItemInMainHand().clone();
player.getInventory().setItemInMainHand(book);
new BukkitRunnable() {
@Override
public void run() {
player.openBook(book);
player.getInventory().setItemInMainHand(oldItem);
}
}.runTaskLater(this, 2L);
askForReplyInput(player, post.id);
}
private String parsePlaceholders(String text, Player player, int page) {
String parsed = text
.replace("{player}", player.getName())
.replace("{page}", String.valueOf(page))
.replace("{pages}", String.valueOf(getTotalPages(guiConfig.get("chest").getIntegerList("post_slot").size())));
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
try {
return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, parsed);
} catch (Exception e) {
getLogger().warning("PlaceholderAPI 处理失败: " + e.getMessage());
}
}
return parsed;
}
private String getCurrentTime() {
if (cachedTime != null) {
return cachedTime;
}
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
private void startScheduledTasks() {
new BukkitRunnable() {
@Override
public void run() {
updateTimeCache();
}
}.runTaskTimerAsynchronously(this, 0, 20 * 60 * 60);
new BukkitRunnable() {
@Override
public void run() {
replyCache.clear();
}
}.runTaskTimerAsynchronously(this, 0, 20 * CACHE_TTL);
}
private void updateTimeCache() {
try {
cachedTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
} catch (Exception e) {
getLogger().warning("时间更新失败: " + e.getMessage());
}
}
private static class Post {
final int id;
final String player;
final String content;
final String time;
Post(int id, String player, String content, String time) {
this.id = id;
this.player = player;
this.content = content;
this.time = time;
}
}
private static class Reply {
final String player;
final String content;
final String time;
Reply(String player, String content, String time) {
this.player = player;
this.content = content;
this.time = time;
}
}
private static class PlayerState {
final String guiType;
final int page;
PlayerState(String guiType, int page) {
this.guiType = guiType;
this.page = page;
}
}
private void storePlayerState(Player player, String type, int page) {
player.setMetadata("uiposts_state", new org.bukkit.metadata.FixedMetadataValue(
this, type + ":" + page
));
}
private PlayerState getPlayerState(Player player) {
if (!player.hasMetadata("uiposts_state")) return null;
String[] parts = player.getMetadata("uiposts_state").get(0).asString().split(":");
if (parts.length != 2) return null;
return new PlayerState(parts[0], Integer.parseInt(parts[1]));
}
private boolean isUIPostsInventory(String title) {
return title.contains("帖子广场");
}
private int getTotalPages(int perPage) {
if (perPage <= 0) return 1;
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT COUNT(*) AS total FROM posts")) {
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
int totalPosts = rs.getInt("total");
return (int) Math.ceil((double) totalPosts / perPage);
}
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "计算总页数失败", e);
}
return 1;
}
private boolean hasNextPage(int currentPage, int perPage) {
int totalPages = getTotalPages(perPage);
return currentPage < totalPages;
}
private void handlePaginationClick(Player player, PlayerState state, String buttonName) {
if (buttonName.contains("上一页") && state.page > 1) {
if ("chest".equals(state.guiType)) {
openChestGUI(player, state.page - 1);
} else {
openBookGUI(player, state.page - 1);
}
} else if (buttonName.contains("下一页")) {
if ("chest".equals(state.guiType)) {
openChestGUI(player, state.page + 1);
} else {
openBookGUI(player, state.page + 1);
}
}
}
private void handlePostClick(Player player, int slot, int page) {
ConfigurationSection cfg = guiConfig.get("chest");
List<Integer> postSlots = cfg.getIntegerList("post_slot");
if (!postSlots.contains(slot)) return;
int index = postSlots.indexOf(slot);
int postsPerPage = postSlots.size();
int offset = (page - 1) * postsPerPage + index;
Post post = getPostByOffset(offset);
if (post != null) {
showBookForPost(player, post);
}
}
private Post getPostByOffset(int offset) {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT id, player, content, time FROM posts ORDER BY id DESC LIMIT 1 OFFSET ?")) {
ps.setInt(1, offset);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new Post(
rs.getInt("id"),
rs.getString("player"),
rs.getString("content"),
rs.getString("time")
);
}
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "按偏移量查询帖子失败", e);
}
return null;
}
private Post getPost(int postId) {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT player, content, time FROM posts WHERE id = ?")) {
ps.setInt(1, postId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new Post(
postId,
rs.getString("player"),
rs.getString("content"),
rs.getString("time")
);
}
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "查询单个帖子失败", e);
}
return null;
}
private String truncate(String text, int maxLength) {
return text.length() > maxLength ? text.substring(0, maxLength) + "..." : text;
}
}