commit 55996f3b2b0fb9a6fecf0eb6ac8a7c529f148ef7 Author: ypsdf1 Date: Mon Jun 1 20:35:02 2026 +0800 feat: 新增 Sdf1_game 插件初始版本(快问块答小游戏,对接 Sdf1_login 债券系统) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..480bdf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..f6906f2 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..fdc35ea --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc49f11 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Sdf1_game插件说明文档 + +# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改! + +## 玩法介绍: + +| 名字 | 实现状态 | +| :---: | :---: | +| 快问块答 | 已实现 | +| 挖土掘金 | 待实现 | +| 寻宝 | 待实现 | + +## 必装依赖 +- [Sdf1_login](https://github.com/ypsdf1/Sdf1_login) 用途:经济提供者 + +## 指令介绍 +| 指令 | 参数1 | 参数2 | 参数3 | 结果 | +| :---: | :---: | :---: | :---: | :---: | +| quiz | 无 | 无 | 无 | 帮助 | +| quiz | start | 无 | 无 | 出题 | +| quiz | info | 无 | 无 | 状态 | +| quiz | reload | 无 | 无 | 重载 | +| quiz | start | 题型 | 题目内容/编号(从上到下第N题) | 出指定的题目 | + + +## 配置文件: +```text +# ===== 快问快答 主配置 ===== +# 支持 # // /* */ 注释 + +# 运行模式: 自动 / 手动 +# 模式区别:手动模式:纯手动出题,自动模式是手自一体化 +模式: 自动 + +# 最少在线人数 +最少在线人数: 3 + +# 出题间隔 +# 支持: 秒(s) 分钟(m) 小时(h) +最小间隔: 5分钟 +最大间隔: 15分钟 + +# 答题时间 +答题时间: 2分钟 + +# 蒙题防护 +# 选择题中,2秒内错误答案超过此数则罚时 +# 罚时期间答案无效 +蒙题检测数: 2 + +# 罚时时间(秒) +# 支持: 秒(s) 分钟(m) 小时(h) +罚时时间: 10秒 + +# 消息前缀 +消息前缀: §e§l【快问快答】§r + +# 蒙题防护提示 +罚时开始提示: §c你回答太快了,请冷静10秒! +罚时结束提示: §a罚时已结束,你可以继续答题了 + +``` +## 题目支持: +- 选择题 +- 填空题 +- 问答题 + +## 题目配置格式: + +```text +# 编号. 问题(赏金1)(赏金2)... +# 答案:答案1,答案2,... + +1. 钻石矿在( )层以下生成。(10) +答案:16 + +2. 末影龙被击败后掉落( )经验。(15) +答案:12000 + +# ===== 问答题 ===== +# | 问题 | 答案 | 赏金 | + +| 问题 | 答案 | 赏金 | +|------|------|------| +| MC的全称是什么? | Minecraft | 20 | +| 下界合金锭的英文名? | Netherite Ingot | 15 | +| 末影螨的英文名? | Endermite | 12 | +| 信标的英文名? | Beacon | 10 | + +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..04ec7ee --- /dev/null +++ b/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + sdf1 + Sdf1_game + 1.0.0 + jar + + + 21 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.21.4-R0.1-SNAPSHOT + provided + + + + + Sdf1_game + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + + + diff --git a/src/main/java/Sdf1_game/BondBridge.java b/src/main/java/Sdf1_game/BondBridge.java new file mode 100644 index 0000000..b337b77 --- /dev/null +++ b/src/main/java/Sdf1_game/BondBridge.java @@ -0,0 +1,112 @@ +package Sdf1_game; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class BondBridge { + + private Object bondManager; + private Method addBondsMethod; + private boolean hooked = false; + + public boolean hook(Main plugin) { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("Sdf1_login"); + + if (sdf1 == null || !sdf1.isEnabled()) { + plugin.getLogger().warning( + "[快问快答] 未找到Sdf1_login," + + "债券奖励不可用"); + return false; + } + + try { + bondManager = findManager(sdf1); + if (bondManager == null) { + plugin.getLogger().warning( + "[快问快答] 找不到BondManager"); + return false; + } + + addBondsMethod = + bondManager.getClass() + .getMethod("addBonds", + String.class, + int.class, + String.class, + String.class, + String.class, + String.class); + + hooked = true; + plugin.getLogger().info( + "[快问快答] 债券桥接成功"); + return true; + } catch (Exception e) { + plugin.getLogger().warning( + "[快问快答] 桥接失败: " + + e.getMessage()); + return false; + } + } + + private Object findManager(Plugin sdf1) { + Class c = sdf1.getClass(); + + // 策略1: getBondManager() + try { + Method m = c.getMethod("getBondManager"); + Object r = m.invoke(sdf1); + if (r != null) return r; + } catch (Exception ignored) { + } + + // 策略2: 字段 bondManager + try { + Field f = c.getDeclaredField( + "bondManager"); + f.setAccessible(true); + Object r = f.get(sdf1); + if (r != null) return r; + } catch (Exception ignored) { + } + + // 策略3: 遍历字段按类型名查找 + try { + for (Field f : c.getDeclaredFields()) { + if (f.getType().getSimpleName() + .equals("BondManager")) { + f.setAccessible(true); + Object r = f.get(sdf1); + if (r != null) return r; + } + } + } catch (Exception ignored) { + } + + return null; + } + + public boolean addBonds(String player, + int amount, + String reason) { + if (!hooked) return false; + try { + int r = (int) addBondsMethod.invoke( + bondManager, + player, amount, + "quiz_reward", "", + "快问快答", reason); + return r >= 0; + } catch (Exception e) { + return false; + } + } + + public boolean isHooked() { + return hooked; + } +} diff --git a/src/main/java/Sdf1_game/FileGenerator.java b/src/main/java/Sdf1_game/FileGenerator.java new file mode 100644 index 0000000..63f5bf1 --- /dev/null +++ b/src/main/java/Sdf1_game/FileGenerator.java @@ -0,0 +1,160 @@ +package Sdf1_game; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class FileGenerator { + + public static void ensureAll(File base, File qDir) { + writeIfAbsent( + new File(base, "配置.txt"), + defaultConfig()); + writeIfAbsent( + new File(qDir, "选择题.md"), + defaultChoice()); + writeIfAbsent( + new File(qDir, "填空题.md"), + defaultFill()); + writeIfAbsent( + new File(qDir, "问答题.md"), + defaultOpen()); + } + + + private static void writeIfAbsent(File f, + List lines) { + if (f.exists()) return; + try { + f.getParentFile().mkdirs(); + OutputStreamWriter w = new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8); + for (String line : lines) { + w.write(line); + w.write("\n"); + } + w.close(); + } catch (IOException ignored) { + } + } + + private static List defaultConfig() { + List l = new ArrayList<>(); + l.add("# ===== 快问快答 主配置 ====="); + l.add("# 支持 # // /* */ 注释"); + l.add(""); + l.add("# 运行模式: 自动 / 手动"); + l.add("# 模式区别:"); + l.add("# 手动模式,纯手动出题"); + l.add("# 自动模式:手自一体化,按下面配置规则自动出题"); + l.add("模式: 自动"); + l.add(""); + l.add("# 最少在线人数"); + l.add("最少在线人数: 3"); + l.add(""); + l.add("# 出题间隔"); + l.add("# 支持: 秒(s) 分钟(m) 小时(h)"); + l.add("最小间隔: 5分钟"); + l.add("最大间隔: 15分钟"); + l.add(""); + l.add("# 答题时间"); + l.add("答题时间: 2分钟"); + l.add(""); + l.add("# 蒙题防护"); + l.add("# 选择题中,2秒内错误答案超过此数则罚时"); + l.add("# 罚时期间答案无效"); + l.add("蒙题检测数: 2"); + l.add(""); + l.add("# 罚时时间(秒)"); + l.add("# 支持: 秒(s) 分钟(m) 小时(h)"); + l.add("罚时时间: 10秒"); + l.add(""); + l.add("# 消息前缀"); + l.add("消息前缀: §e§l【快问快答】§r"); + l.add(""); + l.add("# 蒙题防护提示"); + l.add("罚时开始提示: §c你回答太快了,请冷静10秒!"); + l.add("罚时结束提示: §a罚时已结束,你可以继续答题了"); + return l; + } + + + private static List defaultChoice() { + List l = new ArrayList<>(); + l.add("# ===== 选择题 ====="); + l.add("# 格式: | 问题 | 选项A | 选项B | [选项C] | [选项D] | 答案 | 赏金 |"); + l.add("# 空选项列自动跳过"); + l.add(""); + l.add("| 问题 | 选项A | 选项B | 选项C | 选项D | 答案 | 赏金 |"); + l.add("|------|-------|-------|-------|-------|------|------|"); + l.add("| 钻石矿在几层以下生成? | 4层 | 16层 | 32层 | 64层 | B | 10 |"); + l.add("| 末影龙的血量是多少? | 100 | 200 | 300 | 500 | B | 15 |"); + l.add("| 下界合金锭需要几个碎片? | 2个 | 3个 | 4个 | 5个 | C | 10 |"); + l.add("| 铁砧最多修复几次后损坏? | 3次 | 5次 | 6次 | 10次 | C | 10 |"); + l.add("| 下界合金剑的攻击力? | 6 | 7 | 8 | 9 | D | 12 |"); + l.add("| 哪种生物不在平原生成? | 苦力怕 | 僵尸 | 末影人 | 骷髅 | C | 8 |"); + l.add("| 信标最少几层方块? | 1层 | 2层 | 3层 | 4层 | C | 10 |"); + l.add("| 附魔台最多放几本书? | 4本 | 8本 | 12本 | 15本 | D | 10 |"); + l.add("| 熔岩在几格内造成伤害? | 1格 | 2格 | 3格 | | A | 8 |"); + l.add("| 末影珍珠冷却时间? | 0秒 | 1秒 | 5秒 | | B | 10 |"); + return l; + } + + private static List defaultFill() { + List l = new ArrayList<>(); + l.add("# ===== 填空题 ====="); + l.add("# 编号. 问题(赏金1)(赏金2)..."); + l.add("# 答案:答案1,答案2,..."); + l.add(""); + l.add("1. 钻石矿在( )层以下生成。(10)"); + l.add("答案:16"); + l.add(""); + l.add("2. 末影龙被击败后掉落( )经验。(15)"); + l.add("答案:12000"); + l.add(""); + l.add("3. ( )是游戏中最坚硬的方块。(15)"); + l.add("答案:基岩"); + l.add(""); + l.add("4. 下界合金锭由( )个碎片和( )个金锭合成。(10)(10)"); + l.add("答案:4,4"); + l.add(""); + l.add("5. MC一共有( )个维度。(5)"); + l.add("答案:3"); + l.add(""); + l.add("6. 信标最少需要( )层方块才能激活。(10)"); + l.add("答案:3"); + l.add(""); + l.add("7. 钻石剑攻击力( )点,下界合金剑( )点。(8)(8)"); + l.add("答案:7,8"); + l.add(""); + l.add("8. 饱和效果的英文ID是( )。(5)"); + l.add("答案:saturation"); + l.add(""); + l.add("9. 每个铁砧使用后消耗( )点耐久。(5)"); + l.add("答案:1"); + l.add(""); + l.add("10. 凋零Boss被击败后掉落( )个下界之星。(10)"); + l.add("答案:1"); + return l; + } + + private static List defaultOpen() { + List l = new ArrayList<>(); + l.add("# ===== 问答题 ====="); + l.add("# | 问题 | 答案 | 赏金 |"); + l.add(""); + l.add("| 问题 | 答案 | 赏金 |"); + l.add("|------|------|------|"); + l.add("| MC的全称是什么? | Minecraft | 20 |"); + l.add("| Minecraft的创始人是谁? | Notch | 20 |"); + l.add("| 下界合金锭的英文名? | Netherite Ingot | 15 |"); + l.add("| 末影螨的英文名? | Endermite | 12 |"); + l.add("| 信标的英文名? | Beacon | 10 |"); + return l; + } +} diff --git a/src/main/java/Sdf1_game/Main.java b/src/main/java/Sdf1_game/Main.java new file mode 100644 index 0000000..6b0072c --- /dev/null +++ b/src/main/java/Sdf1_game/Main.java @@ -0,0 +1,338 @@ +package Sdf1_game; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Main extends JavaPlugin + implements TabCompleter { + + private BondBridge bondBridge; + private QuizManager quizManager; + + private static final Pattern SPEC_NO_SPACE = + Pattern.compile("^(选择题|填空题|问答题)(\\d+)$"); + + private static final Pattern SPEC_WITH_SPACE = + Pattern.compile("^(选择题|填空题|问答题)\\s+(\\d+)$"); + + private static final List TYPES = + Arrays.asList("选择题", "单选题", + "多选题", "填空题", "问答题"); + @Override + public void onEnable() { + File base = new File(getDataFolder(), "快问快答"); + File qDir = new File(base, "题目"); + qDir.mkdirs(); + + FileGenerator.ensureAll(base, qDir); + + bondBridge = new BondBridge(); + bondBridge.hook(this); + + quizManager = new QuizManager(this, bondBridge); + quizManager.loadQuestions(); + + getServer().getPluginManager() + .registerEvents(quizManager, this); + + getCommand("quiz").setTabCompleter(this); + getCommand("出题").setTabCompleter(this); + + quizManager.startAuto(); + + getLogger().info("=== 快问快答 v1.0.0 ==="); + getLogger().info("模式: " + quizManager.getMode()); + getLogger().info("债券桥接: " + + (bondBridge.isHooked() ? "已连接" : "未连接")); + getLogger().info("\n" + + " ____ _ __ _ \n" + + " / ___| __| |/ _/ | __ _ __ _ _ __ ___ ___ \n" + + " \\___ \\ / _` | |_| | / _` |/ _` | '_ ` _ \\ / _ \\ \n" + + " ___) | (_| | _| | | (_| | (_| | | | | | | __/ \n" + + " |____/ \\__,_|_| |_|____\\__, |\\__,_|_| |_| |_|\\___| \n" + + " ____ _ __ |_____|___/ \n" + + " / ___| __| |/ _/ | \n" + + " \\___ \\ / _` | |_| | \n" + + " ___) | (_| | _| | \n" + + " |____/ \\__,_|_| |_| _ ____ _ __ _ \n" + + " _ __ _____ _____ _ __ | |__ _ _ / ___| __| |/ _/ |\n" + + " | '_ \\ / _ \\ \\ /\\ / / _ \\ '__| | '_ \\| | | | \\___ \\ / _` | |_| |\n" + + " | |_) | (_) \\ V V / __/ | | |_) | |_| | ___) | (_| | _| |\n" + + " | .__/ \\___/ \\_/\\_/ \\___|_| |_.__/ \\__, | |____/ \\__,_|_| |_|\n" + + " |_| |___/ "); + } + + @Override + public void onDisable() { + if (quizManager != null) { + quizManager.stopAuto(); + } + getLogger().info("[快问快答] 已卸载"); + } + + @Override + public boolean onCommand(CommandSender sender, + Command command, + String label, + String[] args) { + if (!sender.hasPermission("sdf1_quiz.admin")) { + sender.sendMessage("§c无权限"); + return true; + } + + String cmd = command.getName(); + + // ===== /出题 ===== + if (cmd.equals("出题") || label.equals("出题")) { + handleStart(sender, args); + return true; + } + + // ===== /quiz ===== + if (!cmd.equals("quiz")) { + return false; + } + + if (args.length == 0) { + sendHelp(sender); + return true; + } + + switch (args[0].toLowerCase()) { + case "start": + handleStart(sender, args); + return true; + case "stop": + quizManager.stopAuto(); + sender.sendMessage("§a已停止自动出题"); + return true; + case "reload": + quizManager.reload(); + sender.sendMessage("§a已重载"); + return true; + case "info": + sendInfo(sender); + return true; + case "test": + sender.sendMessage(bondBridge.isHooked() + ? "§a桥接: 已连接" + : "§c桥接: 未连接"); + return true; + default: + sendHelp(sender); + return true; + } + } + + private void sendHelp(CommandSender sender) { + sender.sendMessage("§e§l【快问快答】§r 命令:"); + sender.sendMessage("§7/出题 - 随机出题"); + sender.sendMessage("§7/出题 选择题1 - 指定题号"); + sender.sendMessage("§7/出题 多选题2 - 多选题"); + sender.sendMessage("§7/出题 填空题 钻石矿 - 按内容"); + sender.sendMessage("§7/quiz stop - 停止自动"); + sender.sendMessage("§7/quiz reload - 重载"); + sender.sendMessage("§7/quiz info - 查看状态"); + sender.sendMessage("§7/quiz test - 测试桥接"); + } + + private void sendInfo(CommandSender sender) { + sender.sendMessage("§e§l【快问快答】§r 状态:"); + sender.sendMessage("§7模式: §e" + quizManager.getMode()); + sender.sendMessage("§7答题中: §e" + + (quizManager.isActive() ? "是" : "否")); + sender.sendMessage("§7选择题: §e" + quizManager.getChoiceCount() + + " §7填空题: §e" + quizManager.getFillCount() + + " §7问答题: §e" + quizManager.getOpenCount()); + sender.sendMessage("§7债券桥接: " + + (bondBridge.isHooked() ? "§a已连接" : "§c未连接")); + } + + // ===== /出题 参数解析 ===== + + private void handleStart(CommandSender sender, String[] args) { + // 无参数 → 随机 + if (args.length == 0) { + doStart(sender, null, 0, null); + return; + } + + // /quiz start 无后续 → 随机 + if (args.length == 1 && args[0].equalsIgnoreCase("start")) { + doStart(sender, null, 0, null); + return; + } + + // 确定参数起始位置 + int offset = 0; + if (args.length >= 2 && args[0].equalsIgnoreCase("start")) { + offset = 1; + } + + // 拼接剩余参数 + StringBuilder sb = new StringBuilder(); + for (int i = offset; i < args.length; i++) { + if (sb.length() > 0) sb.append(" "); + sb.append(args[i]); + } + String spec = sb.toString().trim(); + + // 格式1: "选择题1" 无空格 + Matcher m1 = SPEC_NO_SPACE.matcher(spec); + if (m1.matches()) { + doStart(sender, m1.group(1), + Integer.parseInt(m1.group(2)), null); + return; + } + + // 格式2: "选择题 1" 有空格 + Matcher m2 = SPEC_WITH_SPACE.matcher(spec); + if (m2.matches()) { + doStart(sender, m2.group(1), + Integer.parseInt(m2.group(2)), null); + return; + } + + // 格式3: "选择题 钻石矿" 按内容搜索 + for (String typeName : TYPES) { + if (spec.startsWith(typeName)) { + String keyword = spec.substring(typeName.length()).trim(); + if (!keyword.isEmpty()) { + Question q = quizManager.findByKeyword(typeName, keyword); + if (q != null) { + doStart(sender, null, 0, q); + } else { + sender.sendMessage("§c未找到包含「" + keyword + "」的题目"); + } + return; + } + } + } + + sender.sendMessage("§c格式错误,示例:"); + sender.sendMessage("§7/出题 - 随机出题"); + sender.sendMessage("§7/出题 选择题1"); + sender.sendMessage("§7/出题 选择题 1"); + sender.sendMessage("§7/出题 选择题 钻石矿"); + } + + private void doStart(CommandSender sender, + String type, int index, + Question specific) { + // ★ 在线人数必须>=1 + if (Bukkit.getOnlinePlayers().size() < 1) { + sender.sendMessage("§c服务器无人,无法出题"); + return; + } + + if (quizManager.isActive()) { + sender.sendMessage("§c当前有题目进行中"); + return; + } + + if (specific != null) { + quizManager.forceQuestion(specific); + sender.sendMessage("§a已出题: [" + + specific.getTypeName() + "] " + + specific.getQuestion()); + } else if (type != null) { + quizManager.forceQuestion(type, index); + sender.sendMessage("§a已出题: " + + type + "第" + index + "题"); + } else { + quizManager.forceQuestion(); + sender.sendMessage("§a已随机出题"); + } + } + + // ===== Tab 补全 ===== + + @Override + public List onTabComplete(CommandSender sender, + Command command, + String label, + String[] args) { + if (!sender.hasPermission("sdf1_quiz.admin")) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + String cmd = command.getName(); + + boolean isStart = cmd.equals("quiz") + && args.length >= 1 + && args[0].equalsIgnoreCase("start"); + + // /出题 或 /quiz start 的参数 + boolean isQuizSpec = cmd.equals("出题") + || label.equals("出题") + || isStart; + + if (!isQuizSpec) { + // /quiz 的子命令补全 + if (cmd.equals("quiz") && args.length == 1) { + String input = args[0].toLowerCase(); + for (String s : Arrays.asList( + "start", "stop", "reload", "test", "info")) { + if (s.startsWith(input)) { + result.add(s); + } + } + } + return result; + } + + // 确定参数位置 + int argPos = 0; + if (cmd.equals("quiz") && isStart) { + argPos = args.length - 1; + } else { + argPos = args.length - 1; + } + + if (argPos == 0) { + // 补全题型 + String input = args[0].toLowerCase(); + for (String t : TYPES) { + if (t.startsWith(input) || input.isEmpty()) { + result.add(t); + } + } + } else if (argPos == 1) { + // 补全题号或关键词 + String typeName = args[0]; + if (TYPES.contains(typeName)) { + int count = getCountForType(typeName); + for (int i = 1; i <= count; i++) { + result.add(String.valueOf(i)); + } + } + } + + return result; + } + + private int getCountForType(String type) { + switch (type) { + case "选择题": + case "单选题": + return quizManager.getChoiceCount(); + case "多选题": + return quizManager.getMultiCount(); + case "填空题": + return quizManager.getFillCount(); + case "问答题": + return quizManager.getOpenCount(); + default: return 0; + } + }} \ No newline at end of file diff --git a/src/main/java/Sdf1_game/Question.java b/src/main/java/Sdf1_game/Question.java new file mode 100644 index 0000000..9516041 --- /dev/null +++ b/src/main/java/Sdf1_game/Question.java @@ -0,0 +1,111 @@ +package Sdf1_game; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Question { + + public enum Type { + CHOICE, MULTI, FILL, OPEN + } + + private static final Pattern BLANK = + Pattern.compile("\\(\\s*\\)"); + + private final Type type; + private final String question; + private final String answer; + private final int[] rewards; + private final List options; + + public Question(Type type, String question, + String answer, int[] rewards, + List options) { + this.type = type; + this.question = question; + this.answer = answer; + this.rewards = rewards; + this.options = options != null + ? new ArrayList<>(options) + : new ArrayList<>(); + } + + public Question(Type type, String question, + String answer, int reward, + List options) { + this(type, question, answer, + new int[]{reward}, options); + } + + public Question(Type type, String question, + String answer, int reward) { + this(type, question, answer, + new int[]{reward}, null); + } + + public Question(Type type, String question, + String answer, int[] rewards) { + this(type, question, answer, + rewards, new ArrayList<>()); + } + + public Type getType() { + return type; + } + + public String getQuestion() { + return question; + } + + public String getAnswer() { + return answer; + } + + public int[] getRewards() { + return rewards; + } + + public List getOptions() { + return options; + } + + public int getDisplayReward() { + int max = 0; + for (int r : rewards) { + max = Math.max(max, r); + } + return max; + } + + public int getRewardForCount(int count) { + if (count <= 0) return 0; + return rewards[Math.min(count, + rewards.length) - 1]; + } + + public int getBlankCount() { + Matcher m = BLANK.matcher(question); + int c = 0; + while (m.find()) c++; + return c; + } + + public int getCorrectCount() { + return answer.split(",").length; + } + + public String getTypeName() { + switch (type) { + case CHOICE: + return "选择题"; + case MULTI: + return "多选题"; + case FILL: + return "填空题"; + default: + return "问答题"; + } + } +} diff --git a/src/main/java/Sdf1_game/QuestionLoader.java b/src/main/java/Sdf1_game/QuestionLoader.java new file mode 100644 index 0000000..5d49634 --- /dev/null +++ b/src/main/java/Sdf1_game/QuestionLoader.java @@ -0,0 +1,357 @@ +package Sdf1_game; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class QuestionLoader { + + private static final Pattern REWARD_TRAIL = + Pattern.compile("\\((\\d+)\\)$"); + + public static List load(Main plugin, + String resourcePath) { + List questions = new ArrayList<>(); + + File file = new File( + plugin.getDataFolder(), resourcePath); + + if (!file.exists()) { + plugin.getLogger().warning( + "[快问快答] 文件不存在: " + + resourcePath); + return questions; + } + + Question.Type type; + if (resourcePath.contains("选择题")) { + type = Question.Type.CHOICE; + } else if (resourcePath.contains("填空题")) { + type = Question.Type.FILL; + } else { + type = Question.Type.OPEN; + } + + try (BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), + StandardCharsets.UTF_8))) { + + List cleaned = cleanComments(br); + + switch (type) { + case CHOICE: + parseChoice(cleaned, questions); + break; + case FILL: + parseFill(cleaned, questions); + break; + default: + parseOpen(cleaned, questions); + break; + } + + } catch (IOException e) { + plugin.getLogger().warning( + "[快问快答] 加载失败: " + + resourcePath + " - " + + e.getMessage()); + } + + return questions; + } + + private static List cleanComments( + BufferedReader br) { + List result = new ArrayList<>(); + try { + String line; + boolean inBlock = false; + boolean inHtml = false; + + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + if (inBlock) { + int end = line.indexOf("*/"); + if (end >= 0) { + inBlock = false; + line = line + .substring(end + 2) + .trim(); + if (line.isEmpty()) continue; + } else { + continue; + } + } + if (line.startsWith("/*")) { + int end = line.indexOf("*/", 2); + if (end >= 0) { + line = line + .substring(end + 2) + .trim(); + if (line.isEmpty()) continue; + } else { + inBlock = true; + continue; + } + } + + if (inHtml) { + int end = line.indexOf("-->"); + if (end >= 0) { + inHtml = false; + line = line + .substring(end + 3) + .trim(); + if (line.isEmpty()) continue; + } else { + continue; + } + } + if (line.startsWith("", 4); + if (end >= 0) { + line = line + .substring(end + 3) + .trim(); + if (line.isEmpty()) continue; + } else { + inHtml = true; + continue; + } + } + + if (line.startsWith("#")) continue; + + int h = line.indexOf('#'); + if (h > 0) { + line = line.substring(0, h).trim(); + } + int s = line.indexOf("//"); + if (s > 0) { + line = line.substring(0, s).trim(); + } + + if (!line.isEmpty()) { + result.add(line); + } + } + } catch (IOException ignored) { + } + return result; + } + + /** + * 选择题表格 + * 答案含逗号 → 多选题 + * 答案为"全选" → 所有选项 + */ + private static void parseChoice( + List lines, + List result) { + + for (String line : lines) { + if (!line.startsWith("|")) continue; + if (line.contains("---")) continue; + + String[] parts = line.split("\\|"); + if (parts.length < 5) continue; + + String question = parts[1].trim(); + String answer = + parts[parts.length - 2].trim(); + String rewardStr = + parts[parts.length - 1].trim(); + + if (question.equals("问题")) continue; + if (question.isEmpty() + || answer.isEmpty()) continue; + + // 收集非空选项 + List options = new ArrayList<>(); + for (int i = 2; + i <= parts.length - 3; i++) { + String opt = parts[i].trim(); + if (!opt.isEmpty()) { + options.add(opt); + } + } + if (options.isEmpty()) continue; + + // 判断是否多选 + boolean isMulti = + answer.contains(",") + || answer.contains(",") + || answer.equals("全选"); + + if (isMulti) { + String realAnswer; + if (answer.equals("全选")) { + // 生成全选答案 + StringBuilder sb = + new StringBuilder(); + char ch = 'A'; + for (int i = 0; + i < options.size(); i++) { + if (i > 0) sb.append(","); + sb.append(ch); + ch++; + } + realAnswer = sb.toString(); + } else { + realAnswer = answer; + } + + // 赏金解析 + String[] rp = rewardStr.split(","); + int correctCount = + realAnswer.split(",").length; + int[] rewards; + + if (rp.length > 1) { + rewards = new int[Math.min( + rp.length, correctCount)]; + for (int i = 0; + i < rewards.length; i++) { + rewards[i] = Integer.parseInt( + rp[i].trim()); + } + } else { + int base = Integer.parseInt( + rewardStr.trim()); + rewards = new int[correctCount]; + for (int i = 0; + i < correctCount; i++) { + rewards[i] = base * (i + 1); + } + } + + result.add(new Question( + Question.Type.MULTI, + question, realAnswer, + rewards, options)); + } else { + // 单选题 + int reward; + try { + reward = Integer.parseInt( + rewardStr); + } catch (NumberFormatException e) { + continue; + } + + result.add(new Question( + Question.Type.CHOICE, + question, answer, + reward, options)); + } + } + } + + private static void parseOpen( + List lines, + List result) { + + for (String line : lines) { + if (!line.startsWith("|")) continue; + if (line.contains("---")) continue; + + String[] parts = line.split("\\|"); + if (parts.length < 4) continue; + + String question = parts[1].trim(); + String answer = parts[2].trim(); + String rewardStr = parts[3].trim(); + + if (question.equals("问题")) continue; + if (question.isEmpty() + || answer.isEmpty()) continue; + + int reward; + try { + reward = Integer.parseInt(rewardStr); + } catch (NumberFormatException e) { + continue; + } + + result.add(new Question( + Question.Type.OPEN, + question, answer, reward)); + } + } + + private static void parseFill( + List lines, + List result) { + + int i = 0; + while (i < lines.size()) { + String line = lines.get(i); + if (!line.matches("\\d+\\.\\s*.*")) { + i++; + continue; + } + + int dot = line.indexOf('.'); + String content = + line.substring(dot + 1).trim(); + + List rewardList = + new ArrayList<>(); + while (true) { + Matcher m = REWARD_TRAIL + .matcher(content); + if (m.find()) { + rewardList.add(0, + Integer.parseInt( + m.group(1))); + content = content.substring(0, + m.start()).trim(); + } else { + break; + } + } + + if (content.isEmpty()) { + i++; + continue; + } + + String answer = ""; + if (i + 1 < lines.size()) { + String aLine = + lines.get(i + 1).trim(); + int ci = Math.max( + aLine.indexOf(':'), + aLine.indexOf(':')); + if (ci >= 0) { + answer = aLine + .substring(ci + 1).trim(); + } + i += 2; + } else { + i++; + continue; + } + + if (answer.isEmpty()) continue; + + int[] rewards = rewardList.stream() + .mapToInt(Integer::intValue) + .toArray(); + + result.add(new Question( + Question.Type.FILL, + content, answer, rewards)); + } + } +} diff --git a/src/main/java/Sdf1_game/QuizManager.java b/src/main/java/Sdf1_game/QuizManager.java new file mode 100644 index 0000000..db8dc69 --- /dev/null +++ b/src/main/java/Sdf1_game/QuizManager.java @@ -0,0 +1,940 @@ +package Sdf1_game; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; + +public class QuizManager implements Listener { + + private final Main plugin; + private final BondBridge bondBridge; + + private List choiceQ = new ArrayList<>(); + private List fillQ = new ArrayList<>(); + private List openQ = new ArrayList<>(); + + private BukkitTask autoTask; + private BukkitTask countTask; + private Question cur; + private boolean active = false; + private int timeLeft; + private boolean firstRun = true; + private long lastQuizTime = 0; + private boolean wasEmpty = false; + + private String mode = "自动"; + + private final Set answered = + Collections.synchronizedSet(new HashSet<>()); + private final Object lock = new Object(); + // 蒙题防护:记录每个玩家最近的错误答案时间 + private final Map> wrongTimes = + new ConcurrentHashMap<>(); + // 蒙题防护:罚时结束时间 + private final Map punishUntil = + new ConcurrentHashMap<>(); + + + private String prefix = "§e§l【快问快答】§r "; + private int minPlayers = 3; + private int minSec = 300; + private int maxSec = 900; + private int ansTime = 120; + private List multiQ = new ArrayList<>(); + private int spamThreshold = 2; + private int punishSeconds = 10; + private String punishStartMsg = + "§c你回答太快了,请冷静10秒!"; + private String punishEndMsg = + "§a罚时已结束,你可以继续答题了"; + + + public QuizManager(Main plugin, BondBridge bb) { + this.plugin = plugin; + this.bondBridge = bb; + } + + /** + * 时间单位自动换算 + * 支持: 纯数字(秒) / 30秒 / 5分钟 / 2小时 + * 单位: 秒(s) / 分钟(m) / 小时(h) + */ + private int parseTime(String input) { + String s = input.trim().toLowerCase(); + + // 纯数字 → 当作秒 + if (s.matches("\\d+")) { + return Integer.parseInt(s); + } + + // 提取数字部分和单位部分 + // "5分钟" "30s" "2小时" + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)\\s*(秒|s|分|分钟|m|时|小时|h)") + .matcher(s); + + if (m.find()) { + int val = Integer.parseInt(m.group(1)); + String unit = m.group(2); + switch (unit) { + case "秒": + case "s": + return val; + case "分": + case "分钟": + case "m": + return val * 60; + case "时": + case "小时": + case "h": + return val * 3600; + } + } + + // 解析失败,返回原值尝试 parseInt + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return 0; + } + } + + + // ========== 读配置 ========== + + public void loadConfig() { + File f = new File(plugin.getDataFolder(), + "快问快答/配置.txt"); + if (!f.exists()) return; + + try (BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + if (line.startsWith("#") + || line.startsWith("//") + || line.startsWith("