第 1 卷第 1 章:1–9 年
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# 把本项目的 DFHack 脚本部署进游戏目录,并写入开机自动运行的 init 片段。
|
||||
# scripts/deploy-dfhack.sh /mnt/c/Users/23518/DwarfFortress/df_53_16_win
|
||||
set -euo pipefail
|
||||
|
||||
DF_DIR="${1:?用法: deploy-dfhack.sh <DF 目录>}"
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SRC="$HERE/scripts/dfhack/dfannals"
|
||||
|
||||
[[ -d "$DF_DIR" ]] || { echo "游戏目录不存在: $DF_DIR" >&2; exit 1; }
|
||||
[[ -d "$DF_DIR/hack" ]] || { echo "该目录里没有 hack/,DFHack 未安装?" >&2; exit 1; }
|
||||
|
||||
DEST="$DF_DIR/hack/scripts/dfannals"
|
||||
mkdir -p "$DEST"
|
||||
cp -v "$SRC"/*.lua "$DEST/"
|
||||
|
||||
INIT_DIR="$DF_DIR/dfhack-config/init"
|
||||
mkdir -p "$INIT_DIR"
|
||||
cat > "$INIT_DIR/dfannals.init" <<'EOF'
|
||||
# 由 df-annals 项目部署。默认只跑观测脚本;需要全自动时改成 dfannals/drive。
|
||||
dfannals/observe
|
||||
EOF
|
||||
|
||||
echo "已部署到:$DEST"
|
||||
echo "init 片段:$INIT_DIR/dfannals.init"
|
||||
ls -1 "$DEST"
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 DFHack 文本截图中按文字定位控件,并生成点击坐标。
|
||||
|
||||
配合 dfannals/screentext 与 dfannals/click 使用:
|
||||
1) dfhack-run dfannals/screentext
|
||||
2) scripts/df-screen.py click --label "History length" --text "250 years"
|
||||
|
||||
屏幕文本文件里第 1 行是表头,因此 屏幕 y = 文件行号 - 2。
|
||||
坐标为 0 起始的字符网格坐标,与 dfannals/click 一致。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCREEN = Path("/mnt/c/Users/23518/DwarfFortress/dfannals-screen.txt")
|
||||
|
||||
|
||||
def read_screen(path: Path) -> list[str]:
|
||||
if not path.is_file():
|
||||
sys.exit(f"找不到截图文件:{path}")
|
||||
raw = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
return raw[1:] # 去掉 "# focus=..." 表头,使下标 = 屏幕 y
|
||||
|
||||
|
||||
def char_index(line: str, needle: str) -> int:
|
||||
return line.find(needle)
|
||||
|
||||
|
||||
def find_row(lines: list[str], needle: str, start: int = 0) -> int:
|
||||
for i in range(start, len(lines)):
|
||||
if needle in lines[i]:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def cmd_list(args: argparse.Namespace) -> int:
|
||||
lines = read_screen(args.file)
|
||||
for y, line in enumerate(lines):
|
||||
if line.strip():
|
||||
print(f"{y:3} | {line.rstrip()}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_click(args: argparse.Namespace) -> int:
|
||||
lines = read_screen(args.file)
|
||||
start = 0
|
||||
if args.label:
|
||||
label_row = find_row(lines, args.label)
|
||||
if label_row < 0:
|
||||
sys.exit(f"界面上找不到标签:{args.label}")
|
||||
start = label_row
|
||||
row = find_row(lines, args.text, start)
|
||||
if row < 0:
|
||||
sys.exit(f"找不到文字:{args.text}" + (f"(在 {args.label} 之下)" if args.label else ""))
|
||||
|
||||
line = lines[row]
|
||||
idx = char_index(line, args.text)
|
||||
x = idx + len(args.text) // 2
|
||||
y = row
|
||||
print(f"{x} {y}")
|
||||
if args.verbose:
|
||||
print(f"# 第 {row} 行,列 {idx},点击中心 ({x}, {y})", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_where(args: argparse.Namespace) -> int:
|
||||
lines = read_screen(args.file)
|
||||
for pattern in args.patterns:
|
||||
row = find_row(lines, pattern)
|
||||
if row < 0:
|
||||
print(f"{pattern}: 未找到")
|
||||
continue
|
||||
idx = char_index(lines[row], pattern)
|
||||
print(f"{pattern}: x={idx + len(pattern) // 2} y={row}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="DF 文本截图定位工具")
|
||||
parser.add_argument("--file", type=Path, default=SCREEN)
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p = sub.add_parser("list", help="列出非空行(带屏幕 y 坐标)")
|
||||
p.set_defaults(func=cmd_list)
|
||||
|
||||
p = sub.add_parser("click", help="输出某段文字的中心坐标")
|
||||
p.add_argument("--label", help="先定位这个标签,再在它下面找目标文字")
|
||||
p.add_argument("--text", required=True, help="要点击的文字")
|
||||
p.add_argument("-v", "--verbose", action="store_true")
|
||||
p.set_defaults(func=cmd_click)
|
||||
|
||||
p = sub.add_parser("where", help="批量定位若干文字")
|
||||
p.add_argument("patterns", nargs="+")
|
||||
p.set_defaults(func=cmd_where)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,27 @@
|
||||
--@ module = true
|
||||
-- 向当前界面发送按键。用法:dfhack-run "dfannals/act SELECT"
|
||||
-- 多个按键按顺序发送:dfhack-run "dfannals/act STANDARDSCROLL_DOWN SELECT"
|
||||
local gui = require('gui')
|
||||
|
||||
local args = {...}
|
||||
local scr = dfhack.gui.getCurViewscreen()
|
||||
local sent = {}
|
||||
|
||||
for _, name in ipairs(args) do
|
||||
local key = df.interface_key[name]
|
||||
if key then
|
||||
scr:feed_key(key)
|
||||
sent[#sent + 1] = name
|
||||
else
|
||||
sent[#sent + 1] = '?未知按键:' .. name
|
||||
end
|
||||
end
|
||||
|
||||
local f = io.open('dfannals-act.log', 'a')
|
||||
if f then
|
||||
f:write(string.format('%s\t%s\tfocus=%s\n', os.date('%H:%M:%S'),
|
||||
table.concat(sent, ' '), table.concat(dfhack.gui.getCurFocus(), '/')))
|
||||
f:close()
|
||||
end
|
||||
|
||||
print('sent: ' .. table.concat(sent, ' '))
|
||||
@@ -0,0 +1,28 @@
|
||||
--@ module = true
|
||||
-- 在窗口坐标 (x, y) 模拟一次左键点击。用法:dfhack-run "dfannals/click 160 40"
|
||||
-- 方式与 DFHack 自带测试工具一致:设置鼠标位置后 simulateInput('_MOUSE_L')。
|
||||
local gui = require('gui')
|
||||
|
||||
local args = {...}
|
||||
local x, y = tonumber(args[1]), tonumber(args[2])
|
||||
if not x or not y then
|
||||
print('用法: dfannals/click <x> <y>')
|
||||
return
|
||||
end
|
||||
|
||||
df.global.gps.mouse_x = math.floor(x)
|
||||
df.global.gps.precise_mouse_x = math.floor(x) * df.global.gps.tile_pixel_x
|
||||
df.global.gps.mouse_y = math.floor(y)
|
||||
df.global.gps.precise_mouse_y = math.floor(y) * df.global.gps.tile_pixel_y
|
||||
|
||||
local scr = dfhack.gui.getCurViewscreen()
|
||||
gui.simulateInput(scr, '_MOUSE_L')
|
||||
|
||||
local f = io.open('dfannals-act.log', 'a')
|
||||
if f then
|
||||
f:write(string.format('%s\tclick %d,%d\tfocus=%s\n', os.date('%H:%M:%S'), x, y,
|
||||
table.concat(dfhack.gui.getCurFocus(), '/')))
|
||||
f:close()
|
||||
end
|
||||
|
||||
print(string.format('clicked %d,%d', x, y))
|
||||
@@ -0,0 +1,57 @@
|
||||
--@ module = true
|
||||
-- 把当前界面的字段结构导出到 dfannals-dump.txt,用于摸清 UI 元素。
|
||||
-- 用法:dfhack-run "dfannals/dump"
|
||||
local MAX_ITEMS = 400
|
||||
|
||||
local function is_scalar(v)
|
||||
local t = type(v)
|
||||
return t == 'number' or t == 'string' or t == 'boolean'
|
||||
end
|
||||
|
||||
local function describe(obj, max_depth)
|
||||
local out = {}
|
||||
local function rec(o, prefix, depth)
|
||||
if depth > max_depth or #out > MAX_ITEMS then return end
|
||||
local ok, iter, state, ctrl = pcall(pairs, o)
|
||||
if not ok then return end
|
||||
for k, v in iter, state, ctrl do
|
||||
if #out > MAX_ITEMS then return end
|
||||
local key = tostring(k)
|
||||
if is_scalar(v) then
|
||||
out[#out + 1] = string.format('%s%s = %s', prefix, key, tostring(v))
|
||||
else
|
||||
local t = type(v)
|
||||
out[#out + 1] = string.format('%s%s <%s>', prefix, key, t)
|
||||
if t == 'table' or t == 'userdata' then
|
||||
rec(v, prefix .. ' ', depth + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
rec(obj, '', 1)
|
||||
return out
|
||||
end
|
||||
|
||||
local f = io.open('dfannals-dump.txt', 'w')
|
||||
if not f then return end
|
||||
|
||||
local function write(line) f:write(line .. '\n') end
|
||||
|
||||
write('# focus=' .. table.concat(dfhack.gui.getCurFocus(), '/'))
|
||||
write('# date=' .. os.date('%Y-%m-%d %H:%M:%S'))
|
||||
|
||||
local scr = dfhack.gui.getCurViewscreen()
|
||||
local depth = 0
|
||||
while scr and depth < 3 do
|
||||
write('==== 屏幕层 ' .. depth .. ' ====')
|
||||
local ok, lines = pcall(describe, scr, 2)
|
||||
if ok then
|
||||
for _, l in ipairs(lines) do write(l) end
|
||||
else
|
||||
write('(失败: ' .. tostring(lines) .. ')')
|
||||
end
|
||||
scr = scr.parent
|
||||
depth = depth + 1
|
||||
end
|
||||
|
||||
f:close()
|
||||
@@ -0,0 +1,39 @@
|
||||
--@ module = true
|
||||
-- 观测模式:把界面焦点变化持续追加到 dfannals-observe.log。
|
||||
-- 用途:发现"新建世界 → 进入 legends → 导出"之间的真实界面序列。
|
||||
|
||||
local repeatUtil = require('repeat-util')
|
||||
|
||||
local LOG = 'dfannals-observe.log'
|
||||
local last = nil
|
||||
local ticks = 0
|
||||
|
||||
local function focus_string()
|
||||
local ok, focus = pcall(dfhack.gui.getCurFocus)
|
||||
if not ok or not focus then return '?' end
|
||||
return table.concat(focus, '/')
|
||||
end
|
||||
|
||||
function tick()
|
||||
ticks = ticks + 1
|
||||
local fs = focus_string()
|
||||
if fs ~= last then
|
||||
last = fs
|
||||
local f = io.open(LOG, 'a')
|
||||
if f then
|
||||
f:write(string.format('%s\t%s\n', os.date('%H:%M:%S'), fs))
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function start()
|
||||
repeatUtil.scheduleEvery('dfannals-observe', 10, tick)
|
||||
local f = io.open(LOG, 'a')
|
||||
if f then
|
||||
f:write('--- observe start ' .. os.date('%Y-%m-%d %H:%M:%S') .. '\n')
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
start()
|
||||
@@ -0,0 +1,56 @@
|
||||
--@ module = true
|
||||
-- 把当前界面状态写入 dfannals-probe.txt,供 WSL 侧读取。
|
||||
-- 用法(在 WSL 里):"Dwarf Fortress.exe" 所在目录的 dfhack-run.exe dfannals/probe
|
||||
|
||||
local function safe_type(obj)
|
||||
if not obj then return '?' end
|
||||
local ok, t = pcall(function() return obj._type end)
|
||||
if ok and t then return tostring(t) end
|
||||
return '?'
|
||||
end
|
||||
|
||||
local function viewstack()
|
||||
local out = {}
|
||||
local scr = dfhack.gui.getCurViewscreen()
|
||||
local depth = 0
|
||||
while scr and depth < 15 do
|
||||
out[#out + 1] = string.format('[%d] %s', depth, safe_type(scr))
|
||||
local ok, parent = pcall(function() return scr.parent end)
|
||||
if not ok then break end
|
||||
scr = parent
|
||||
depth = depth + 1
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function world_info()
|
||||
local lines = {}
|
||||
local ok, folder = pcall(dfhack.world.ReadWorldFolder)
|
||||
lines[#lines + 1] = 'world_folder=' .. (ok and tostring(folder) or '?')
|
||||
local ok2, year = pcall(dfhack.world.ReadCurrentYear)
|
||||
lines[#lines + 1] = 'current_year=' .. (ok2 and tostring(year) or '?')
|
||||
local ok3, legends = pcall(dfhack.world.isLegends)
|
||||
lines[#lines + 1] = 'is_legends=' .. (ok3 and tostring(legends) or '?')
|
||||
local ok4, fort = pcall(dfhack.world.isFortressMode)
|
||||
lines[#lines + 1] = 'is_fortress=' .. (ok4 and tostring(fort) or '?')
|
||||
local ok5, name = pcall(function() return dfhack.translation.translateName(df.global.world.world_data.name) end)
|
||||
lines[#lines + 1] = 'world_name=' .. (ok5 and tostring(name) or '?')
|
||||
return lines
|
||||
end
|
||||
|
||||
function dump()
|
||||
local lines = {}
|
||||
local ok, focus = pcall(dfhack.gui.getCurFocus)
|
||||
lines[#lines + 1] = 'focus=' .. ((ok and focus) and table.concat(focus, '/') or '?')
|
||||
for _, l in ipairs(world_info()) do lines[#lines + 1] = l end
|
||||
lines[#lines + 1] = 'viewstack:'
|
||||
for _, l in ipairs(viewstack()) do lines[#lines + 1] = ' ' .. l end
|
||||
|
||||
local f = io.open('dfannals-probe.txt', 'w')
|
||||
if f then
|
||||
f:write(table.concat(lines, '\n') .. '\n')
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
dump()
|
||||
@@ -0,0 +1,49 @@
|
||||
--@ module = true
|
||||
-- 遍历当前界面的控件树,导出控件名与矩形坐标,用于定位可点击目标。
|
||||
-- 用法:dfhack-run "dfannals/widgets"
|
||||
local MAX_DEPTH = 8
|
||||
local MAX_LINES = 600
|
||||
|
||||
local function rect_str(w)
|
||||
local ok, r = pcall(function() return w.rect end)
|
||||
if not ok or not r then return '' end
|
||||
local function g(k)
|
||||
local ok2, v = pcall(function() return r[k] end)
|
||||
return ok2 and tostring(v) or '?'
|
||||
end
|
||||
return string.format('rect=(%s,%s)-(%s,%s)', g('x1'), g('y1'), g('x2'), g('y2'))
|
||||
end
|
||||
|
||||
local out = {}
|
||||
local function walk(w, depth)
|
||||
if not w or depth > MAX_DEPTH or #out > MAX_LINES then return end
|
||||
local ok, name = pcall(function() return w.name end)
|
||||
local nm = ok and tostring(name) or ''
|
||||
local txt = ''
|
||||
local ok3, t = pcall(function() return w.text end)
|
||||
if ok3 and t then txt = tostring(t) end
|
||||
out[#out + 1] = string.format('%s%s %s %s', string.rep(' ', depth), nm, rect_str(w),
|
||||
txt ~= '' and ('text=' .. txt) or '')
|
||||
local ok2, children = pcall(function() return w.children end)
|
||||
if ok2 and children then
|
||||
for _, c in ipairs(children) do walk(c, depth + 1) end
|
||||
end
|
||||
end
|
||||
|
||||
local f = io.open('dfannals-widgets.txt', 'w')
|
||||
if not f then return end
|
||||
f:write('# focus=' .. table.concat(dfhack.gui.getCurFocus(), '/') .. '\n')
|
||||
|
||||
local sw, sh = dfhack.screen.getWindowSize()
|
||||
f:write(string.format('# window=%dx%d\n', sw, sh))
|
||||
|
||||
local scr = dfhack.gui.getCurViewscreen()
|
||||
local ok, widgets = pcall(function() return scr.widgets end)
|
||||
if ok and widgets then
|
||||
walk(widgets, 0)
|
||||
else
|
||||
out[#out + 1] = '(该界面没有 widgets 字段)'
|
||||
end
|
||||
|
||||
for _, l in ipairs(out) do f:write(l .. '\n') end
|
||||
f:close()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在 Windows 侧安装 矮人要塞 免费版 + DFHack(经 WSL 的 /mnt/c 操作,免管理员权限)
|
||||
#
|
||||
# scripts/install-df.sh 安装/校验
|
||||
# scripts/install-df.sh --download 重新下载缺的文件
|
||||
set -euo pipefail
|
||||
|
||||
WIN_USER="${WIN_USER:-23518}"
|
||||
ROOT="/mnt/c/Users/${WIN_USER}/DwarfFortress"
|
||||
DF_ZIP="df_53_16_win.zip"
|
||||
DFHACK_ZIP="dfhack-53.16-r2-Windows-64bit.zip"
|
||||
DF_URL="https://www.bay12games.com/dwarves/${DF_ZIP}"
|
||||
DFHACK_URL="https://github.com/DFHack/dfhack/releases/download/53.16-r2/${DFHACK_ZIP}"
|
||||
|
||||
mkdir -p "$ROOT"
|
||||
cd "$ROOT"
|
||||
|
||||
download() {
|
||||
local url="$1" out="$2"
|
||||
echo "== 下载 $out"
|
||||
for _ in $(seq 1 300); do
|
||||
wget -c -q --timeout=30 --tries=2 -O "$out" "$url" && return 0
|
||||
sleep 5
|
||||
done
|
||||
echo "下载失败:$url" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--download" ]]; then
|
||||
[[ -s "$DF_ZIP" ]] || download "$DF_URL" "$DF_ZIP"
|
||||
[[ -s "$DFHACK_ZIP" ]] || download "$DFHACK_URL" "$DFHACK_ZIP"
|
||||
fi
|
||||
|
||||
for f in "$DF_ZIP" "$DFHACK_ZIP"; do
|
||||
[[ -s "$f" ]] || { echo "缺少 $f(先跑 --download)" >&2; exit 1; }
|
||||
unzip -t "$f" >/dev/null || { echo "$f 已损坏" >&2; exit 1; }
|
||||
done
|
||||
|
||||
echo "== 解压游戏本体"
|
||||
DF_DIR="$(unzip -Z1 "$DF_ZIP" | head -1 | cut -d/ -f1)"
|
||||
if [[ ! -d "$DF_DIR" ]]; then
|
||||
unzip -q -o "$DF_ZIP"
|
||||
fi
|
||||
echo " 游戏目录:$ROOT/$DF_DIR"
|
||||
|
||||
echo "== 解压 DFHack(覆盖进游戏目录)"
|
||||
unzip -q -o "$DFHACK_ZIP" -d "$DF_DIR"
|
||||
|
||||
echo "== 校验"
|
||||
ls -1 "$DF_DIR" | head -20
|
||||
if [[ -f "$DF_DIR/Dwarf Fortress.exe" ]]; then
|
||||
echo " ✓ 启动器存在:Dwarf Fortress.exe"
|
||||
else
|
||||
echo " ✗ 未找到 Dwarf Fortress.exe" >&2
|
||||
fi
|
||||
if [[ -d "$DF_DIR/dfhack-config" && -d "$DF_DIR/hack" ]]; then
|
||||
echo " ✓ DFHack 已就位(hack/ + dfhack-config/)"
|
||||
else
|
||||
echo " ✗ DFHack 目录缺失" >&2
|
||||
fi
|
||||
echo "$ROOT/$DF_DIR"
|
||||
Reference in New Issue
Block a user