作者: RiK (3050/223)第一次参加 HG，明显感受到出题质量很高，做起来整体感觉很爽！（后面好难，一个都不会呃呃）取部分解出题目写一篇 WP。新人实力有限，有误望指正。1. 赛博井字棋打开题目玩了几局后发现不可能以正常方式解题。而后尝试伪造 POST 请求，发现比较复杂（作为备选方案）。打开浏览器开发人员工具 (F12) 注意到以下代码：// static/script.js async function setMove(x, y) { if (board\[x\]\[y\] != 0) { return; } if (frozen) { return; } let url = window.location.href; // 获取当前 URL let data = { x: x, y: y }; // 设置要发送的数据 return fetch(url, { method: "POST", // 设置方法为 POST headers: { "Content-Type": "application/json", // 设置内容类型为 JSON }, body: JSON.stringify(data), // 将数据转换为 JSON 格式 }).catch(errorHandler); }于是通过 Console 重新定义（覆盖）该函数并将前两个 if 语句删除，Restart，强制占用对手棋子位置，获胜得到 flag：flag{I_can_eat_your_pieces_53f95d1546}2. 组委会模拟器打开题目尝试手动撤回，不出意料消息发送过快，人工撤回不可能，于是编写 Python脚本自动撤回。（其实用 JS 直接监听更方便）import requests, re, json, asyncio async def send_delete(delay: float, id: int): await asyncio.sleep(delay) res = requests.post( "http://202.38.93.111:10021/api/deleteMessage", json={"id": id}, cookies={ "session": "[token]" }, headers={"Content-Type": "application/json"}, ) print(res.text) res = requests.post( "http://202.38.93.111:10021/api/getMessages", cookies={ "session": "[token]" }, headers={"Content-Type": "application/json"}, ) data = json.loads(res.text) illegal_req = [] id = 0 for msg in data["messages"]: if re.findall(r"hack\[[a-z]+\]", msg["text"]): illegal_req.append(send_delete(msg["delay"], id)) id += 1 await asyncio.gather(\*illegal_req) res = requests.post( "http://202.38.93.111:10021/api/getflag", cookies={ "session": "[token]" }, headers={"Content-Type": "application/json"}, ) print(res.text)由于单纯地 sleep() 后 requests.get() 的执行时间会因网络延时不断偏移预定值，可通过测量请求时间相减抵消，此处选用 asyncio 库并发解决。最后得到 flag：flag{Web_pr0gra_mm1ng_36a712733a_15fun}3. 虫由于以前玩过业余无限电，一看到这题当场乐了，使用安卓手机软件 Robot36，外录音频后自动解码 SSTV 图像结果如下：于是得到 flag：flag{SSssTV_y0u_W4NNa_HaV3_4_trY}4. Git? Git!一开始使用 git log 找了个遍发现根本没有包含 flag 的 commit，后来索性对所有文件暴力搜索关键词 "flag" 没有结果，后来意识到是它可能是以二进制形式存储。本想放弃，网上搜索发现 git reflog 可以显示存储在本地的更详细的操作历史：ea49f0c (HEAD -\> main) HEAD@{0}: commit: Trim trailing spaces 15fd0a1 (origin/main, origin/HEAD) HEAD@{1}: reset: moving to HEAD~ 505e1a3 HEAD@{2}: commit: Trim trailing spaces 15fd0a1 (origin/main, origin/HEAD) HEAD@{3}: clone: from https://github.com/dair-ai/ML-Course-Notes.git。执行命令git reset 15fd0a1 --hard后在 README.md 中第 19 行找到 flag：flag{TheRe5\_@lwAy5_a_R3GreT_pi1l_1n_G1t}5. LD_PRELOAD, love!LD_PRELOAD 是一个环境变量，可以实现在运行实际程序前更改程序的动态链接库。但是它并不能更改静态链接库。于是编写程序如下：// capture_flag.c #include <stdio.h> #include <stdlib.h> int main(void) { char *flag_str; size_t n; FILE *flag_file; flag_file = fopen("/flag", "r"); if (flag_file == NULL) { perror("Cannot open flag file"); exit(EXIT_FAILURE); } open_memstream(&flag_str, &n); getline(&flag_str, &n, flag_file); puts(flag_str); fclose(flag_file); return 0; }。执行命令gcc capture_flag.c -o capture_flag -static以静态链接编译程序，上传执行程序后输出 flag：flag{nande_ld_preload_yattano_6e6cacbe72}6. 流式星球被下面几个细节坑了好久（最难绷的一集）：使用 numpy.fromfile() 时一定要指定 dtype=numpy.uint8 啊啊啊啊。使用 np.hstack() 后同样要将数组转换为 uint8 型否则 opencv 报错。assert 语句的意思是条件不成立时异常，所以宽高不是 10 的倍数。视频宽高不用强行枚举凑，用 Hex 编辑器打开源文件发现周期大致为 1281 字节，宽高取其因数即可。最后得到 flag：flag{it-could-be-eazy-to-restore-video-with-haruhikage-even-without-metadata-0F7968CC}7. 奶奶的睡前 flag 故事用 pngcheck 发现有两个 IEND 后想方设法拼接、拆分；读 PNG Specification 用 Hex 编辑器手写 IHDR、IDAT、IEND 块，配合 pngcheck 算 CRC ……整了两天最后还是没做出来，libpng 一直报错（zlib 相关），下半部分就是加载不出来。结果赛后得知有现成工具？？？（我的评价是多读题干）
本文发布于 2026 年 3 月。想要搭建一个博客的想法从高中就有了，不过一直没有足够的动力完成这件事。直到后来在大学接触到 CTF 才重拾搭建个人网站的兴趣。在高中那段时间的一些简陋的小玩意就全都放在这里啦。（留作回忆）Minecraft Bukkit 声明式命令框架高中时（2021）和一个同班同学琢磨着开一个 Minecraft 插件服务器骗钱，在那段时间写了很多服务器插件。（记得当时经常需要逆向分析一些付费的闭源插件。）最终整个项目（PolarLand）因长期没有进展一直在折腾一些底层的架构而永久搁置。如果那时候有现在这样的 Agentic Coding 的话大概不会放弃吧。其中一个插件是将原本重复繁琐的处理玩家命令的功能写成一个声明式框架。说实话，里面有一些想法直到今天的我看来也是十分 hack (nerd) 的。比如为了将玩家传入的 x y z 坐标自动实例化为 Location 于是在运行时编译一个 Java 类来实现；还有一个 Array 转数组的魔法操作 parsedArgs.add(parsedArray.toArray((Object[]) Array.newInstance(type.getComponentType(), 0)))；以及后面的 tab 键补全嵌套的命令参数（参数的类型是命令）功能。核心源码：/* * Copyright 2022 pwnerik.cn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.polarmc.bukkitplugin.poleax.kit.command.parser; import com.itranswarp.compiler.JavaStringCompiler; import net.polarmc.bukkitplugin.poleax.kit.command.CommandModel; import net.polarmc.bukkitplugin.poleax.kit.command.NoSuchCommandException; import net.polarmc.bukkitplugin.poleax.kit.command.annotation.Optional; import net.polarmc.bukkitplugin.poleax.kit.command.annotation.*; import net.polarmc.bukkitplugin.poleax.kit.command.util.Util; import net.polarmc.bukkitplugin.poleax.utils.MessageUtil; import org.apache.commons.lang.StringUtils; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.command.PluginCommand; import org.bukkit.command.TabExecutor; import org.bukkit.entity.Player; import org.bukkit.help.HelpTopic; import org.bukkit.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; public class CommandBinder implements TabExecutor { // TODO text private final List<ParsedCommand> commands = new ArrayList<>(); private final String messagePrefix; private final StringBuilder helpText = new StringBuilder(); private final List<List<Object>> routeRaw = new ArrayList<>(); private final Map<String, Completer> customCompleters = new HashMap<>(); private static final List<Method> toBeWrappedMethods = new ArrayList<>(); public CommandBinder(String rootCommand, String messagePrefix) { this.messagePrefix = messagePrefix; // Bukkit.getPluginCommand(rootCommand).setExecutor(this); // Bukkit.getPluginCommand(rootCommand).setTabCompleter(this); } public CommandBinder bind(CommandModel model) { bind0(new BasicCommandModel(), new String[0]); bind0(model, new String[0]); parseTabComplete(); return this; } @SuppressWarnings("unused") public CommandBinder bindCompleter(String parameterId, Completer completer) { customCompleters.put(parameterId, completer); return this; } public boolean parse(CommandSender sender, String label, String @NotNull [] args) { if (args.length == 0) { args = new String[]{"help"}; //重定向到 help } ParsedCommand entry; try { entry = findEntry(args); } catch (NoSuchCommandException e) { sender.sendMessage(messagePrefix + ChatColor.RED + e.getMessage()); return false; } String[] realArgs = Arrays.copyOfRange(args, entry.getRoute().size(), args.length); if (sender instanceof Player) { if (Arrays.stream(entry.getPermissions()).anyMatch(permission -> !sender.hasPermission(permission))) { sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 权限不足."); return false; } } else if (entry.isPlayerSenderRequired()) { sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 该命令只能由玩家执行."); return false; } int optionalCount = 0; if (realArgs.length != entry.getParameters().size()) { //TODO bind时计算 for (Parameter arg : entry.getParameters()) { if (arg.isOptional()) { optionalCount++; } } if (entry.getParameters().get(entry.getParameters().size() - 1).getType().isArray()) { if (realArgs.length < entry.getParameters().size() - optionalCount) { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数长度有误, 需要至少 %d / 提供 %d.", entry.getParameters().size() - optionalCount, realArgs.length)); return false; } } else { if (optionalCount == 0) { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数长度有误, 需要 %d / 提供 %d.", entry.getParameters().size(), realArgs.length)); return false; } int requiredLength = entry.getParameters().size() - optionalCount; if (realArgs.length < requiredLength) { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数长度有误, 需要 %d 至 %d / 提供 %d.", requiredLength, entry.getParameters().size(), realArgs.length)); return false; } } } String[] fullCommandLine = new String[args.length + 1]; // 用于报错 fullCommandLine[0] = "/" + label; System.arraycopy(args, 0, fullCommandLine, 1, args.length); int i = 0; boolean arrayParameterPresented = false; List<Object> parsedArgs = new ArrayList<>(); Parameter parameter; Class<?> type; Object value; for (String arg : realArgs) { parameter = entry.getParameters().get(i); type = parameter.getType(); if (type.isArray()) { arrayParameterPresented = true; ArrayList<Object> parsedArray = new ArrayList<>(); for (; i < realArgs.length; i++) { //主动 index 由预处理 parameter 变为用户传入 argument try { if (String[].class.equals(type)) { parsedArray.add(realArgs[i]); } else { Object arrayElementValue = type.getComponentType().getMethod("valueOf", String.class) .invoke(null, realArgs[i]); // 静态方法无需依赖对象, 故传null. if (isNumberClass(type.getComponentType()) && (((Number) arrayElementValue).longValue() < parameter.getMin() || ((Number) arrayElementValue).longValue() > parameter.getMax())) { //忽略此警告, 因为参数类型已经确定 sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s[%d] 的值超出范围 ([%d, %d]).", parameter.getName(), i - entry.getParameters().size() + 1, parameter.getMin(), parameter.getMax())); sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine)); sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2)); return false; } parsedArray.add(arrayElementValue); } } catch (IllegalAccessException | NoSuchMethodException e) { sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x01)."); e.printStackTrace(); return false; } catch (InvocationTargetException e) { if (type.getComponentType().isEnum()) { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s[%d] 的值只能为 %s 之一", parameter.getName(), i - entry.getParameters().size() + 1, Util.simplify(Arrays.toString(Util.getOptions(type.getComponentType()))))); sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine)); sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2)); } else { if (isNumberClass(type.getComponentType()) && StringUtils.isNumeric(realArgs[i])) { if (parameter.getMin() != Long.MIN_VALUE || parameter.getMax() != Long.MAX_VALUE) { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s[%d] 的值超出范围 ([%d, %d]).", parameter.getName(), i - entry.getParameters().size() + 1, parameter.getMin(), parameter.getMax())); } else { try { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s[%d] 的值超出范围 ([%d, %d]).", parameter.getName(), i - entry.getParameters().size() + 1, type.getComponentType().getField("MIN_VALUE").get(null), //忽略此警告, 因为参数类型已经确定 type.getComponentType().getField("MAX_VALUE").get(null))); //忽略此警告, 因为参数类型已经确定 } catch (IllegalAccessException | NoSuchFieldException ex) { sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x03)."); e.printStackTrace(); return false; } } // TODO 不必要的 if bind 时处理 } else { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 传入参数 %s[%d] 类型有误, 应为 %s.", parameter.getName(), i - entry.getParameters().size() + 1, humanReadableTypeName(type.getComponentType()))); } sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine)); sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2)); } return false; } } parsedArgs.add(parsedArray.toArray((Object[]) Array.newInstance(type.getComponentType(), 0))); // 转换为对应类型数组并添加至参数列表 break; } try { if (String.class.equals(type)) { value = arg; } else { value = type.getMethod("valueOf", String.class).invoke(null, arg); // 静态方法无需依赖对象, 故传null. } } catch (IllegalAccessException | NoSuchMethodException e) { sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x01)."); e.printStackTrace(); return false; } catch (InvocationTargetException e) { if (type.isEnum()) { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s 的值只能为 %s 之一", parameter.getName(), Util.simplify(Arrays.toString(Util.getOptions(type))))); sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine)); sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2)); } else { if (isNumberClass(type) && StringUtils.isNumeric(arg)) { if (parameter.getMin() != Long.MIN_VALUE || parameter.getMax() != Long.MAX_VALUE) { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s 的值超出范围 ([%d, %d]).", parameter.getName(), parameter.getMin(), parameter.getMax())); } else { try { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s 的值超出范围 ([%d, %d]).", parameter.getName(), type.getField("MIN_VALUE").get(null), //忽略此警告, 因为参数类型已经确定 type.getField("MAX_VALUE").get(null))); //忽略此警告, 因为参数类型已经确定 } catch (IllegalAccessException | NoSuchFieldException ex) { sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x03)."); e.printStackTrace(); return false; } } // TODO 不必要的 if bind 时处理 } else { sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 传入参数 %s 类型有误, 应为 %s.", parameter.getName(), humanReadableTypeName(type))); } sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine)); sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2)); } return false; } if (isNumberClass(type) && (((Number) value).longValue() < parameter.getMin() || ((Number) value).longValue() > parameter.getMax())) { //忽略此警告, 因为参数类型已经确定 sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s 的值超出范围 ([%d, %d]).", parameter.getName(), parameter.getMin(), parameter.getMax())); sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine)); sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2)); return false; } parsedArgs.add(value); i++; } //填充可选参数null if (!arrayParameterPresented) { for (; i < entry.getParameters().size(); i++) { parsedArgs.add(null); } } entry.getModelObject().sender = sender; // 无需考虑线程安全 entry.getModelObject().label = label; try { return (boolean) entry.getHandler().invoke(entry.getModelObject(), parsedArgs.toArray()); } catch (IllegalAccessException e) { sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x02)."); e.printStackTrace(); return false; } catch (InvocationTargetException e) { sender.sendMessage( messagePrefix + ChatColor.RED + String.format("错误: 在执行命令时出现问题 (%s), 请报告给服务器管理员.", e.getCause())); e.printStackTrace(); return false; } } private void bind0(@NotNull CommandModel model, String @NotNull [] baseRoute) { Class<? extends CommandModel> modelClass = model.getClass(); if (baseRoute.length != 0 && modelClass.isAnnotationPresent(Help.class)) { helpText.append("=== ") .append(Util.simplify(Arrays.toString(baseRoute))) .append(" - ") .append(modelClass.getAnnotation(Help.class).value()) .append(" ===") .append("\n"); } boolean helped = false; command: for (Method method : modelClass.getMethods()) { if (!method.isAnnotationPresent(Command.class)) { continue; } if (!boolean.class.equals(method.getReturnType())) { MessageUtil.error(String.format("在类 %s 中的方法 %s 的返回值类型必须为 boolean.", modelClass.getSimpleName(), method.getName())); continue; } Command commandAnnotation = method.getAnnotation(Command.class); if (commandAnnotation.value().isEmpty()) { MessageUtil.error(String.format("在类 %s 中的方法 %s 的 @Command 中没有指定命令路径.", modelClass.getSimpleName(), method.getName())); continue; } for (java.lang.reflect.Parameter parameter : method.getParameters()) { if (Location.class.equals(parameter.getType())) { toBeWrappedMethods.add(method); JavaStringCompiler javaStringCompiler = new JavaStringCompiler(); StringBuilder code = new StringBuilder("public class A extends net.polarmc.bukkitplugin.poleax.kit.command.CommandModel{public boolean wrapped"); code.append(method.getName()); code.append("("); for (java.lang.reflect.Parameter parameter1 : method.getParameters()) { if (parameter1.getType().equals(Location.class)) { code.append("@net.polarmc.bukkitplugin.poleax.kit.command.annotation.Para(\"x\")Double x,@net.polarmc.bukkitplugin.poleax.kit.command.annotation.Para(\"y\")Double y,@net.polarmc.bukkitplugin.poleax.kit.command.annotation.Para(\"z\")Double z,"); continue; } code.append(generateParameterDeclaration(parameter1)).append(","); } code.deleteCharAt(code.length() - 1); code .append(")throws java.lang.IllegalAccessException, java.lang.reflect.InvocationTargetException{") .append("org.bukkit.Location $l;") .append("if(sender instanceof org.bukkit.entity.Entity)$l=new org.bukkit.Location(((org.bukkit.entity.Entity)sender).getLocation().getWorld(),x,y,z);") .append("else $l=new org.bukkit.Location(null,x,y,z);") .append("return (boolean) net.polarmc.bukkitplugin.poleax.kit.command.parser.CommandBinder.getToBeWrappedMethod(") .append(toBeWrappedMethods.indexOf(method)) .append(").invoke(this,"); for (java.lang.reflect.Parameter parameter1 : method.getParameters()) { if (parameter1.getType().equals(Location.class)) { code.append("$l,"); continue; } code.append(parameter1.getName()).append(","); } code.deleteCharAt(code.length() - 1); code.append(");}}"); Map<String, byte[]> results; Class<?> wrappedMethodClass; try { results = javaStringCompiler.compile("A.java", code.toString()); } catch (IOException e) { e.printStackTrace(); //TODO 异常处理 return; } try { wrappedMethodClass = javaStringCompiler.loadClass("A", results); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); //TODO 异常处理 return; } method = wrappedMethodClass.getDeclaredMethods()[0]; } } ParsedCommand.Builder command = new ParsedCommand.Builder(); command.appendRoute(baseRoute); command.appendRoute(commandAnnotation.value().split(" ")); if (method.isAnnotationPresent(Permission.class)) { command.setPermissions(method.getAnnotation(Permission.class).value()); } else if (method.isAnnotationPresent(PlayerSenderRequired.class) || modelClass.isAnnotationPresent(PlayerSenderRequired.class)) { command.setPlayerSenderRequired(true); } if (method.isAnnotationPresent(Help.class)) { helped = true; command.setDescription(method.getAnnotation(Help.class).value()); } command.setHandler(method); command.setModelObject(model); boolean hasOptionalParameterPresented = false; String parameterName; Class<?> parameterType; for (java.lang.reflect.Parameter parameter : method.getParameters()) { parameterName = getParameterName(parameter); if (parameterName == null) { MessageUtil.error(String.format("在类 %s 中的方法 %s 的参数 %s 没有指定命名. (使用 @Para 注解或在编译时使用 -parameters 选项)", modelClass.getSimpleName(), method.getName(), parameter.getName())); continue command; } parameterType = parameter.getType(); if (parameterType.isArray()) { // TODO 考虑数组类型可能不是可变参数 if (hasOptionalParameterPresented) { MessageUtil.error( String.format("在类 %s 中的方法 %s 中的可选 (@Optional) 参数与可变参数不可同时存在.", modelClass.getSimpleName(), method.getName())); continue command; } if (parameterType.getComponentType().isEnum() && Util.getOptions(parameterType.getComponentType()).length == 1) { MessageUtil.error(String.format("在类 %s 中的方法 %s 中的枚举类型可变参数 %s 的枚举项数必须大于一", modelClass.getSimpleName(), method.getName(), parameterName)); continue command; } try { if (!String[].class.equals(parameterType)) { parameterType.getComponentType().getMethod("valueOf", String.class); } } catch (NoSuchMethodException e) { MessageUtil.error(String.format( "在类 %s 中的方法 %s 的参数 %s 的数组的组成类型不包含静态方法 valueOf(String), 无法应用于命令参数 (不要使用基本类型数组).", modelClass.getSimpleName(), method.getName(), parameterName)); continue command; } } else { try { if (!String.class.equals(parameterType)) { parameterType.getMethod("valueOf", String.class); } } catch (NoSuchMethodException e) { MessageUtil.error( String.format("在类 %s 中的方法 %s 的参数 %s 的类型不包含静态方法 valueOf(String), 无法应用于命令参数 (不要使用基本类型).", modelClass.getSimpleName(), method.getName(), parameterName)); continue command; } } Parameter.Builder parameterBuilder = new Parameter.Builder(); if (parameter.isAnnotationPresent(Optional.class)) { hasOptionalParameterPresented = true; parameterBuilder.setOptional(true); } else if (hasOptionalParameterPresented) { MessageUtil.error(String.format("在类 %s 中的方法 %s 中的可选 (@Optional) 参数必须在所有参数中的末尾位置.", modelClass.getSimpleName(), method.getName())); continue command; } if (parameter.isAnnotationPresent(Tab.class)) { TabType tabType = parameter.getAnnotation(Tab.class).value(); parameterBuilder.setTabType(tabType); if (tabType == TabType.COMMAND && !parameterType.isArray()) { MessageUtil.error(String.format("在类 %s 中的方法 %s 的命令 (@Tab(TabType.COMMAND)) 参数 %s 必须是数组类型.", modelClass.getSimpleName(), method.getName(), parameterName)); continue command; } } if (parameter.isAnnotationPresent(CustomTab.class)) { parameterBuilder.setCustomParameterId(parameter.getAnnotation(CustomTab.class).value()); } if (parameter.isAnnotationPresent(Range.class)) { if (isNumberClass(parameterType) || (parameterType.getComponentType() != null && isNumberClass(parameterType.getComponentType()))) { parameterBuilder .setMin(parameter.getAnnotation(Range.class).min()) .setMax(parameter.getAnnotation(Range.class).max()); } else { MessageUtil.error(String.format("在类 %s 中的方法 %s 的参数 %s 的类型不是数字类型, 无法应用于命令参数 (不要使用基本类型).", modelClass.getSimpleName(), method.getName(), parameterName)); continue command; } } parameterBuilder .setName(parameterName) .setType(parameterType); command.addParameter(parameterBuilder.build()); } ParsedCommand result = command.build(); if (helped) { helpText.append("/") .append("%label") .append(" ") .append(result.toString()) .append(" - ") .append(result.getDescription()) .append("\n"); helped = false; } else { helpText.append("/") .append("%label") .append(" ") .append(result.toString()) .append("\n"); } commands.add(result); } for (Class<?> subModelClass : modelClass.getClasses()) { if (CommandModel.class.isAssignableFrom(subModelClass) && subModelClass.isAnnotationPresent(Command.class)) { try { String[] subRoute = subModelClass.getAnnotation(Command.class).value().split(" "); bind0((CommandModel) subModelClass.newInstance(), Util.mergeStringArray(baseRoute, subRoute)); } catch (IllegalAccessException | InstantiationException e) { e.printStackTrace(); } } } } private @NotNull ParsedCommand findEntry(String[] args) throws NoSuchCommandException { String lastTrial = null; // 用于"找不到命令..."报错. outer: for (ParsedCommand commandHandler : commands) { if (args.length < commandHandler.getRoute().size()) { continue; } int i = 0; for (String label : commandHandler.getRoute()) { if (!label.equals(args[i])) { if (i != 0) { lastTrial = args[i]; } continue outer; } i++; } return commandHandler; } if (lastTrial == null) { throw new NoSuchCommandException("找不到命令: " + args[0]); } else { throw new NoSuchCommandException("找不到命令: " + lastTrial); } } private void parseTabComplete() { for (ParsedCommand command : commands) { ArrayList<Object> fullRoute = new ArrayList<>(); fullRoute.addAll(command.getRoute()); fullRoute.addAll(command.getParameters()); routeRaw.add(fullRoute); } } private boolean checkType(String input, Class<?> type) throws NoSuchMethodException, IllegalAccessException { if (String.class.equals(type) || String[].class.equals(type) || type.isEnum() || (type.getComponentType() != null && type.getComponentType().isEnum())) { return true; } if (type.isEnum() || (type.getComponentType() != null && type.getComponentType().isEnum())) { return true; } try { type.getMethod("valueOf", String.class).invoke(null, input); } catch (InvocationTargetException e) { return false; } return true; } private boolean isNumberClass(Class<?> type) { return Number.class.isAssignableFrom(type); } private @NotNull String argsToString(String @NotNull [] args) { StringBuilder builder = new StringBuilder(); for (String arg : args) { builder.append(arg).append(" "); } return builder.toString().trim(); } private @NotNull String generateErrorWavyUnderline(String @NotNull [] args, int errorIndex) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < args.length; i++) { for (int j = 0; j < args[i].length(); j++) { if (i == errorIndex) { builder.append("~"); } else { builder.append(" "); } } builder.append(" "); } return builder.toString(); } // TODO 更换实现方式 (双s) private @NotNull String humanReadableTypeName(Class<?> type) { if (isNumberClass(type)) { if (Double.class.equals(type) || Float.class.equals(type)) { // 不可能为基本类型 return "小数"; } return "整数"; } // 不可能为字符串 return type.getSimpleName(); } private @NotNull String generateParameterDeclaration(java.lang.reflect.Parameter parameter) { StringBuilder builder = new StringBuilder("@net.polarmc.bukkitplugin.poleax.kit.command.annotation.Para(\""); builder.append(getParameterName(parameter)).append("\") "); for (Annotation declaredAnnotation : parameter.getDeclaredAnnotations()) { builder.append(declaredAnnotation.annotationType().getCanonicalName()).append(" "); } builder.append(parameter.getType().getCanonicalName()).append(" ").append(parameter.getName()); return builder.toString(); } private @Nullable String getParameterName(java.lang.reflect.Parameter parameter) { if (parameter.isNamePresent()) { return parameter.getName(); } else if (parameter.isAnnotationPresent(Para.class)) { return parameter.getAnnotation(Para.class).value(); } else { return null; } } @Override public boolean onCommand(CommandSender sender, org.bukkit.command.Command command, String label, String[] args) { return parse(sender, label, args); } @Override public List<String> onTabComplete(CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) { int i; List<String> result = new ArrayList<>(); List<List<Object>> copy = new ArrayList<>(routeRaw); List<List<Object>> removeQueue = new ArrayList<>(); //删除与遍历不能并行操作 for (List<Object> route : copy) { if (copy.size() == 1) { break; } i = 0; for (Object arg : route) { if (i >= args.length) { break; } if (arg instanceof String && !StringUtil.startsWithIgnoreCase((String) arg, args[i])) { removeQueue.add(route); break; } if (arg instanceof Parameter) { try { Class<?> type = ((Parameter) arg).getType(); if (!checkType(args[i], type)) { removeQueue.add(route); break; } } catch (NoSuchMethodException | IllegalAccessException e) { sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x01)."); e.printStackTrace(); return new ArrayList<>(0); } } i++; } } copy.removeAll(removeQueue); for (List<Object> route : copy) { Object temp = null; Object tail = route.get(route.size() - 1); if (tail instanceof Parameter && ((Parameter) tail).getType().isArray()) { if (route.size() < args.length) { temp = tail; } if (route.size() >= args.length) { temp = route.get(args.length - 1); } } else { if (route.size() < args.length) { continue; } temp = route.get(args.length - 1); } if (temp instanceof String) { result.add((String) temp); } else if (temp instanceof Parameter) { if (((Parameter) temp).getCustomParameterId() != null) { Completer completerFunction = customCompleters.get(((Parameter) temp).getCustomParameterId()); if (completerFunction != null) { result.addAll(completerFunction.complete()); } else { MessageUtil.error(String.format("未找到自定义 Tab 补全方法: %s.", ((Parameter) temp).getCustomParameterId())); return result; } } Class<?> type = ((Parameter) temp).getType(); TabType tabType = ((Parameter) temp).getTabType(); if (type == Boolean.class || (type.getComponentType() != null && type.getComponentType() == Boolean.class)) { // 不可能为基本类型 result.add("true"); result.add("false"); } else if (type.isEnum() || (type.getComponentType() != null && type.getComponentType().isEnum())) { result.addAll(Arrays.asList(Util.getOptions(type))); } else if (tabType == TabType.PLAYER) { StringUtil.copyPartialMatches(args[args.length - 1], Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList()), result); } else if (tabType == TabType.WORLD) { StringUtil.copyPartialMatches(args[args.length - 1], Bukkit.getWorlds().stream().map(World::getName).collect(Collectors.toList()), result); } else if (tabType == TabType.COMMAND) { List<String> commands = new ArrayList<>(); for (HelpTopic cmdLabel : Bukkit.getServer().getHelpMap().getHelpTopics()) { //TODO 取消在此获取 commands.add(cmdLabel.getName().replace("/", "")); } if (args.length == route.size()) { StringUtil.copyPartialMatches(args[args.length - 1], commands, result); } else { PluginCommand targetCommand = Bukkit.getPluginCommand(args[route.size() - 1].replace("/", "")); //之前已确保 TabType.COMMAND 参数是最后一个参数 if (targetCommand != null) { result.addAll(targetCommand.tabComplete(sender, args[route.size() - 1], Arrays.copyOfRange(args, route.size(), args.length))); } } } } } return result; } private class BasicCommandModel extends CommandModel { @SuppressWarnings("unused") @Command("help") @Help("显示命令帮助") public boolean help() { sender.sendMessage(helpText.toString().replace("%label", label).trim()); return true; } } @SuppressWarnings("unused") public static Method getToBeWrappedMethod(int index) { return toBeWrappedMethods.get(index); } }最终达成的效果是可以像这样编写命令处理函数而无需从字符串数组开始处理，类似另一个更加成熟且仍在更新的开源插件 ACF。（懒了，用 AI 生成一个用例。）// Generated by Claude Opus 4.6 import net.polarmc.bukkitplugin.poleax.kit.command.CommandModel; import net.polarmc.bukkitplugin.poleax.kit.command.annotation.*; import org.bukkit.Location; @Help("示例命令组") @PlayerSenderRequired public class ExampleModel extends CommandModel { // 自定义枚举, 用于演示枚举参数与枚举数组参数 public enum Mode { NORMAL, FAST, SLOW } // 1. 无参命令 + @Help + @Permission @Command("info") @Help("显示信息") @Permission({"example.info", "example.admin"}) public boolean info() { sender.sendMessage("label=" + label); return true; } // 2. 基本类型参数 + @Para命名 + @Range范围限制 + @Tab玩家补全 @Command("give") @Help("给予物品") public boolean give( @Para("玩家") @Tab(TabType.PLAYER) String target, @Para("数量") @Range(min = 1, max = 64) Integer amount ) { sender.sendMessage("give " + target + " x" + amount); return true; } // 3. @Optional可选参数 + 枚举参数 + Double类型 @Command("set mode") @Help("设置模式") public boolean setMode( @Para("模式") Mode mode, @Para("倍率") @Optional Double multiplier ) { sender.sendMessage("mode=" + mode + " mul=" + multiplier); return true; } // 4. @Tab(WORLD) 世界补全 + Boolean参数 @Command("tp world") @Help("传送到世界") @PlayerSenderRequired public boolean tpWorld( @Para("世界") @Tab(TabType.WORLD) String world, @Para("安全") @Optional Boolean safe ) { return true; } // 5. Location参数 (自动展开为x,y,z) + @Tab(COMMAND)命令数组补全 @Command("exec at") @Help("在指定位置执行命令") public boolean execAt( @Para("位置") Location loc, @Para("命令") @Tab(TabType.COMMAND) String[] cmd ) { sender.sendMessage("loc=" + loc + " cmd=" + String.join(" ", cmd)); return true; } // 6. 数组可变参数 + @Range + @CustomTab自定义补全 @Command("sum") @Help("求和") public boolean sum( @Para("数字") @Range(min = 0, max = 1000) @CustomTab("numberHints") Integer[] numbers ) { int s = 0; for (Integer n : numbers) s += n; sender.sendMessage("sum=" + s); return true; } // 7. 枚举数组可变参数 @Command("modes") @Help("设置多个模式") public boolean modes(@Para("模式列表") Mode[] modes) { return true; } // 8. 内部类作为子命令组 (@Command on TYPE) @Command("admin") @Help("管理员命令组") @PlayerSenderRequired public static class AdminSubModel extends CommandModel { @Command("reload") @Help("重载配置") @Permission({"example.admin"}) public boolean reload() { sender.sendMessage("reloaded"); return true; } @Command("debug") @Help("调试信息") public boolean debug(@Para("详细") @Optional Boolean verbose) { msg("debug verbose=" + verbose); // 使用继承的 msg() return true; } } }《复杂》捡罐子机器人 Roby —— C 语言实现高中时读到一本书叫《复杂》（Complexity），讲的是大量的简单个体表现出的复杂行为。似乎是目前 LLM 的理论基础之一？虽然很喜欢这本书但我现在没在做 LLM 相关的工作（不太符合我对计科的想象）。书中有一个例子是一种假想的机器人 Roby 在 2D 网格中移动，它只能看见自己前后左右以及脚下这五个格子。地图中某些格子会有一个“罐子”，地图边界是不能通过的墙壁。使用遗传算法机器学习反复模拟即可得到连人类也难以想出的策略，而这些“策略”只是一串数字。// SPDX-License-Identifier: MIT // Copyright (c) 2023 rik #define _GNU_SOURCE #include <assert.h> #include <math.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #define WIDTH 10 #define HEIGHT 10 #define CANS 50 static_assert(CANS <= WIDTH * HEIGHT, "Too many cans"); #define RULES 200 #define EPOCHS 1000 #define STEPS 200 #define EXECS 100 #define MIXES 2 #define WEIGHT M_E #define PICK_UP_CAN_REWARD 10 #define PICK_UP_AIR_PUNISH -1 #define KICK_WALL_PUNISH -5 #define PROGRESS_BAR_UNITS 10 static_assert(!(EPOCHS % PROGRESS_BAR_UNITS), "EPOCHS is not divisible by PROGRESS_BAR_UNITS"); #define WEIGHTED(X, MAX) \ ((size_t)(powf(WEIGHT, -((float)(X) / MAX * (logf(MAX) / logf(WEIGHT)))) * \ (MAX)) - \ 1) #define _SITUATIONS 243 #define _ACTIONS 7 #define _DIRECTIONS 4 enum action { move_north, move_south, move_east, move_west, stand_still, pick_up, move_random, }; struct result { enum action rule[_SITUATIONS]; long score; }; struct history { long strategy[_SITUATIONS][_ACTIONS][PROGRESS_BAR_UNITS]; enum action best_rule[_SITUATIONS]; long best_score; }; // static uint64_t state = 0x853c49e6748fea9bULL; // static uint32_t fast_rand(void) { // uint64_t oldstate = state; // state = oldstate * 6364136223846793005ULL + 0xda3e39cb94b95bdbULL; // uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u; // uint32_t rot = oldstate >> 59u; // return (xorshifted >> rot) | (xorshifted << ((-rot) & 31)); // } // static void fast_srand(const unsigned int seed) { // state = seed ^ 0x853c49e6748fea9bULL; // } // #define random fast_rand // #define srandom fast_srand static void create_random_world(bool world[restrict HEIGHT][WIDTH]) { bool *target; long i; for (i = 0; i < HEIGHT; i++) { memset(world[i], false, sizeof(bool) * WIDTH); } for (i = 0; i < CANS; i++) { do { target = world[random() % HEIGHT] + random() % WIDTH; } while (*target); *target = true; } } static void create_random_rule(enum action rule[restrict _SITUATIONS]) { for (long i = 0; i < _SITUATIONS; i++) { rule[i] = random() % _ACTIONS; } } static long get_situation(const long x, const long y, const bool world[restrict HEIGHT][WIDTH]) { long situation = 0; if (y != 0) { situation += world[y - 1][x] ? 1 : 2; } situation *= 3; if (y != HEIGHT - 1) { situation += world[y + 1][x] ? 1 : 2; } situation *= 3; if (x != WIDTH - 1) { situation += world[y][x + 1] ? 1 : 2; } situation *= 3; if (x != 0) { situation += world[y][x - 1] ? 1 : 2; } situation *= 3; situation += world[y][x] ? 1 : 2; return situation; } #define UNLIKELY(X) __builtin_expect((X), false) static long act(long *restrict x, long *restrict y, bool world[restrict HEIGHT][WIDTH], const enum action action) { switch (action) { case move_north: if (UNLIKELY(*y == 0)) { return KICK_WALL_PUNISH; } else { *y -= 1; return 0; } case move_south: if (UNLIKELY(*y == HEIGHT - 1)) { return KICK_WALL_PUNISH; } else { *y += 1; return 0; } case move_east: if (UNLIKELY(*x == WIDTH - 1)) { return KICK_WALL_PUNISH; } else { *x += 1; return 0; } case move_west: if (UNLIKELY(*x == 0)) { return KICK_WALL_PUNISH; } else { *x -= 1; return 0; } case stand_still: return 0; case pick_up: if (world[*y][*x]) { world[*y][*x] = false; return PICK_UP_CAN_REWARD; } else { return PICK_UP_AIR_PUNISH; } case move_random: return act(x, y, world, random() % _DIRECTIONS); } return 0; } static void mix_rules(const enum action rule1[restrict _SITUATIONS], const enum action rule2[restrict _SITUATIONS], enum action result1[restrict _SITUATIONS], enum action result2[restrict _SITUATIONS]) { long i; long split = random() % (_SITUATIONS - 1); memcpy(result1, rule1, split * sizeof(enum action)); memcpy(result2, rule2, split * sizeof(enum action)); memcpy(result1 + split, rule2 + split, (_SITUATIONS - split) * sizeof(enum action)); memcpy(result2 + split, rule1 + split, (_SITUATIONS - split) * sizeof(enum action)); for (i = 0; i < MIXES; i++) { result1[random() % _SITUATIONS] = random() % _ACTIONS; result2[random() % _SITUATIONS] = random() % _ACTIONS; } } static int compare_results(const void *restrict result1, const void *restrict result2) { return (int)(((struct result *)result2)->score - ((struct result *)result1)->score); } static void train(struct history *restrict history, FILE *restrict log_file) { long i, j, k, l; long x, y, score, progress; enum action rules[RULES][_SITUATIONS]; bool world[HEIGHT][WIDTH]; bool temp_world[HEIGHT][WIDTH]; struct result results[RULES]; progress = 0; for (i = 0; i < RULES; i++) { create_random_rule(rules[i]); } printf(" Progress: "); for (i = 0; i < PROGRESS_BAR_UNITS; i++) { putchar('_'); } for (i = 0; i < PROGRESS_BAR_UNITS; i++) { putchar('\b'); } fflush(stdout); for (i = 0; i < EPOCHS; i++) { for (k = 0; k < RULES; k++) { results[k].score = 0; } for (j = 0; j < EXECS; j++) { create_random_world(world); for (k = 0; k < RULES; k++) { memcpy(temp_world, world, sizeof(bool) * HEIGHT * WIDTH); x = 0; y = 0; score = 0; for (l = 0; l < STEPS; l++) { score += act(&x, &y, temp_world, rules[k][get_situation(x, y, temp_world)]); } results[k].score += score; } } for (j = 0; j < RULES; j++) { memcpy(results[j].rule, rules[j], sizeof(enum action) * _SITUATIONS); } qsort(results, RULES, sizeof(struct result), compare_results); if (results[0].score > history->best_score) { memcpy(history->best_rule, results[0].rule, sizeof(enum action) * _SITUATIONS); history->best_score = results[0].score; } fprintf(log_file, "%ld, ", results[0].score / EXECS); for (j = 0; j < _SITUATIONS; j++) { fprintf(log_file, "%u,", results[0].rule[j]); history->strategy[j][results[0].rule[j]][progress]++; } fputc('\n', log_file); if ((i + 1) % (EPOCHS / PROGRESS_BAR_UNITS) == 0) { putchar('*'); fflush(stdout); progress++; } if (i == EPOCHS - 1) { putchar('\n'); break; } for (j = 0; j < RULES - 1; j += 2) { mix_rules(results[WEIGHTED(random() % RULES, RULES)].rule, results[WEIGHTED(random() % RULES, RULES)].rule, rules[j], rules[j + 1]); } } } static void release_history(struct history *restrict history, FILE *restrict log_file) { static const char *block_state[] = {"WALL", "CAN", "AIR"}; long north, south, east, west, center; long i, _i, j; for (i = 0; i < _SITUATIONS; i++) { _i = i; center = _i % 3; _i -= center; _i /= 3; west = _i % 3; _i -= west; _i /= 3; east = _i % 3; _i -= east; _i /= 3; south = _i % 3; _i -= south; _i /= 3; north = _i % 3; fprintf(log_file, "\t%s\t\tmove_north: ", block_state[north]); for (j = 0; j < PROGRESS_BAR_UNITS; j++) { fprintf(log_file, "%ld", history->strategy[i][move_north][j]); if (j != PROGRESS_BAR_UNITS - 1) { fputs(" -> ", log_file); } } fputs(", move_south: ", log_file); for (j = 0; j < PROGRESS_BAR_UNITS; j++) { fprintf(log_file, "%ld", history->strategy[i][move_south][j]); if (j != PROGRESS_BAR_UNITS - 1) { fputs(" -> ", log_file); } } fprintf(log_file, "\n%s\t%s\t%s\tmove_east: ", block_state[west], block_state[center], block_state[east]); for (j = 0; j < PROGRESS_BAR_UNITS; j++) { fprintf(log_file, "%ld", history->strategy[i][move_east][j]); if (j != PROGRESS_BAR_UNITS - 1) { fputs(" -> ", log_file); } } fputs(", move_west: ", log_file); for (j = 0; j < PROGRESS_BAR_UNITS; j++) { fprintf(log_file, "%ld", history->strategy[i][move_west][j]); if (j != PROGRESS_BAR_UNITS - 1) { fputs(" -> ", log_file); } } fprintf(log_file, "\n\t%s\t\tpick_up: ", block_state[south]); for (j = 0; j < PROGRESS_BAR_UNITS; j++) { fprintf(log_file, "%ld", history->strategy[i][pick_up][j]); if (j != PROGRESS_BAR_UNITS - 1) { fputs(" -> ", log_file); } } fputs(", move_random: ", log_file); for (j = 0; j < PROGRESS_BAR_UNITS; j++) { fprintf(log_file, "%ld", history->strategy[i][move_random][j]); if (j != PROGRESS_BAR_UNITS - 1) { fputs(" -> ", log_file); } } fputs("\n\n\n", log_file); } } static void release_best_steps(struct history *restrict history, FILE *restrict log_file) { long i, j, k; long x, y; bool world[HEIGHT][WIDTH]; create_random_world(world); fprintf(log_file, "The best rule's result (average score = %ld/%u):\n\n", history->best_score / EXECS, CANS * PICK_UP_CAN_REWARD); x = 0; y = 0; for (i = 0; i <= STEPS; i++) { fprintf(log_file, "Step %ld:\n", i); for (j = 0; j < (WIDTH + 1) * 2; j++) { fputc('-', log_file); } fputc('\n', log_file); for (j = 0; j < HEIGHT; j++) { fputc('|', log_file); for (k = 0; k < WIDTH; k++) { if (j == y && k == x) { if (world[j][k]) { fputs("@ ", log_file); } else { fputs("O ", log_file); } } else { if (world[j][k]) { fputs(". ", log_file); } else { fputs(" ", log_file); } } } fputs("|\n", log_file); } for (j = 0; j < (WIDTH + 1) * 2; j++) { fputc('-', log_file); } fputs("\n\n\n", log_file); if (i == STEPS) { break; } act(&x, &y, world, history->best_rule[get_situation(x, y, world)]); } fputs("Done.", log_file); } int main(void) { struct history history = {0}; FILE *train_log, *history_log, *best_steps_log; unsigned int seed; const char *seed_str; puts("Initializing..."); train_log = fopen("train_log.csv", "w"); history_log = fopen("strategy.txt", "w"); best_steps_log = fopen("best_steps.txt", "w"); if (train_log == NULL || history_log == NULL || best_steps_log == NULL) { perror("Unable to open log file"); exit(EXIT_FAILURE); } seed_str = getenv("ROBY_SEED"); if (seed_str) { seed = atoi(seed_str); } else { seed = (unsigned int)time(NULL); } srandom(seed); puts("Training..."); train(&history, train_log); puts("Releasing..."); release_history(&history, history_log); release_best_steps(&history, best_steps_log); fclose(train_log); fclose(history_log); fclose(best_steps_log); puts("Done."); return 0; }物品分拣机器人模拟链接 我觉得盯着这玩意的感觉有点像盯着火焰或者滚筒洗衣机之类，可以看很久（