文档与脚本:世界创建自动化 make-world.sh、完整运行文档与失败排查
This commit is contained in:
@@ -2,53 +2,150 @@
|
||||
|
||||
把《矮人要塞》(Dwarf Fortress) 世界生成的历史数据,写成**中文战报体连载故事**,自动推送到这个仓库。
|
||||
|
||||
本仓库是**成品仓**:只放小说章节、迷你年表与人物索引。
|
||||
原始 `legends` 导出数据与游戏存档留在本地,不入库。
|
||||
本仓库是**成品仓**:放小说章节、迷你年表、人物索引,以及生成它们的代码。
|
||||
原始 `legends` 导出数据(数百 MB)与游戏存档留在本地,不入库。
|
||||
|
||||
## 这本"书"是怎么来的
|
||||
---
|
||||
|
||||
## 一、它是怎么工作的
|
||||
|
||||
```
|
||||
Windows 侧 DF 53.16 + DFHack 53.16-r2
|
||||
│ ① 生成 Medium 世界 / 250 年历史
|
||||
Windows 侧:Dwarf Fortress 53.16 + DFHack 53.16-r2
|
||||
│ ① 生成世界(Medium 129×129 / 250 年历史)
|
||||
│ ② 进入 legends 模式,导出 legends.xml + legends_plus.xml
|
||||
▼ ③ 经 /mnt/c 文件交换(无需网络互访)
|
||||
▼ ③ 经 /mnt/c 文件交换(不需要 WSL↔Windows 网络互访)
|
||||
WSL2 Ubuntu 侧管道
|
||||
│ ④ 解析史料 → 构建年表与人物索引
|
||||
│ ⑤ 按事件链切章(每章 800–1500 字)
|
||||
│ ⑥ 组 prompt(要塞编年史官口吻 / 战报体)→ 调用 LLM 写作
|
||||
│ ⑦ 专名事实校验:正文里的专有名词必须出现在史料中,可疑处标注
|
||||
▼ ⑧ git commit & push
|
||||
Gitea: gitadmin/dwarf-fortress-annals(本仓库)
|
||||
│ ④ 预处理:剔除 DF 塞进名字里的非法控制字符,CP437 → UTF-8
|
||||
│ ⑤ 解析史料:40 万条事件 / 4 万位人物 → 事件流 + 年表 + 人物索引
|
||||
│ ⑥ 切章:10 年一窗,每窗按显著度取前 12 条,同类事件≤4(避免整章讣告)
|
||||
│ ⑦ 写作:要塞编年史官口吻 / 战报体 / 允许自由演绎
|
||||
│ ⑧ 校验:正文专名必须能在史料里找到,可疑处标注但不阻断
|
||||
▼ ⑨ git commit & push
|
||||
Gitea:gitadmin/dwarf-fortress-annals(本仓库)
|
||||
```
|
||||
|
||||
## 目录
|
||||
## 二、目录
|
||||
|
||||
| 路径 | 内容 | 是否入库 |
|
||||
| 路径 | 内容 | 入库 |
|
||||
|---|---|---|
|
||||
| `output/chapters/` | 连载正文(Markdown) | ✅ |
|
||||
| `output/timeline.md` | 迷你年表 | ✅ |
|
||||
| `output/figures.md` | 人物索引 | ✅ |
|
||||
| `src/dfannals/` | 管道源码 | ✅ |
|
||||
| `prompts/` | 写作规范(决定文风与人设) | ✅ |
|
||||
| `data/exports/` | 原始 legends 数据 | ❌ |
|
||||
| `vendor/` | 游戏与 DFHack 安装包 | ❌ |
|
||||
| `output/volumes/v<N>/chapters/` | 连载正文 | ✅ |
|
||||
| `output/volumes/v<N>/timeline.md` | 迷你年表 | ✅ |
|
||||
| `output/volumes/v<N>/figures.md` | 人物索引 | ✅ |
|
||||
| `dfannals/` | 管道源码 | ✅ |
|
||||
| `scripts/` | 安装、部署、驱动脚本 | ✅ |
|
||||
| `prompts/chronicler.md` | 写作规范(决定文风与人设) | ✅ |
|
||||
| `tests/` | 事实校验回归测试(含合成史料样例) | ✅ |
|
||||
| `data/exports/` | 原始 legends 数据与预处理缓存 | ❌ |
|
||||
| `data/state.json` | 进度状态(当前世界 / 卷号 / 已写章节) | ❌ |
|
||||
| `data/GRILL-ME.md` | 立项访谈记录 | ❌ |
|
||||
|
||||
## 写作设定
|
||||
## 三、依赖
|
||||
|
||||
- **叙述者**:要塞里的编年史官,在场记录,负责把事写下来
|
||||
- **口吻**:战报体——严肃的骨架,诙谐的血肉
|
||||
- **语言**:中文;专有名词一律保留英文原文(Urist、Boatmurdered……)
|
||||
- **演绎**:允许虚构对白、心理与吐槽;但**不许发明史料里没有的人名地名**,也不许改写史实骨架
|
||||
- **连载**:同一世界长期连载;一部史写完则换新世界开新卷,由同一位"编年史官"承接
|
||||
| 依赖 | 版本 / 说明 |
|
||||
|---|---|
|
||||
| Dwarf Fortress | **53.16** 免费版(Bay 12)。必须与 DFHack 严格配对 |
|
||||
| DFHack | **53.16-r2**。Windows 版经 `dfhooks.dll` 注入,不需要替换启动器 |
|
||||
| Python | 3.12(用到 `X \| None` 语法) |
|
||||
| defusedxml | `sudo apt install python3-defusedxml`(解析不可信 XML 用) |
|
||||
| 写作模型 | 经 `https://wb2api.hajim1.art/v1`(OpenAI 兼容)调用 `cn:deepseek-v4-pro` |
|
||||
| 发布 | 现有 `~/.ssh/id_ed25519`(已注册到 Gitea 账号 gitadmin) |
|
||||
|
||||
## 运行
|
||||
> 无独显也能跑:DF 本体不吃 GPU;写故事走云端 API,不做本地推理。
|
||||
|
||||
## 四、安装
|
||||
|
||||
```bash
|
||||
# 凭据:API key 运行时从 Pi 的 ~/.pi/agent/auth.json 读取,不落本仓库
|
||||
python3 -m dfannals.cli --help
|
||||
# 1) Windows 侧装游戏与 DFHack(经 WSL 的 /mnt/c 操作,免管理员权限)
|
||||
scripts/install-df.sh --download # Bay12 与 GitHub 在国内较慢,会断点续传
|
||||
# GitHub 直连不通时改用镜像:https://gh-proxy.com/<原地址>
|
||||
|
||||
# 2) 装依赖
|
||||
sudo apt install -y python3-defusedxml
|
||||
|
||||
# 3) 把驱动脚本部署进游戏目录
|
||||
scripts/deploy-dfhack.sh /mnt/c/Users/$USER_WIN/DwarfFortress
|
||||
```
|
||||
|
||||
## 声明
|
||||
## 五、产出一卷(生成新世界 → 导出 → 连载)
|
||||
|
||||
《Dwarf Fortress》版权归 Bay 12 Games 所有。本仓库仅包含由其生成数据的**衍生文本**,
|
||||
```bash
|
||||
# 1) 启动游戏,停在标题界面
|
||||
"/mnt/c/Users/23518/DwarfFortress/Dwarf Fortress.exe" &
|
||||
|
||||
# 2) 自动生成世界(Medium / 250 年)并导出 legends 数据到 data/exports/
|
||||
scripts/make-world.sh 3 3
|
||||
|
||||
# 3) 一条命令产出下一章并推送
|
||||
python3 -m dfannals.cli run
|
||||
```
|
||||
|
||||
`make-world.sh` 全程从外部驱动游戏,不需要人工点击:
|
||||
|
||||
- 用 `dfannals/screentext` 把画面读成字符网格,`scripts/df-screen.py` 按文字定位按钮
|
||||
- 用 `dfannals/click` 模拟鼠标、`dfannals/setparams` 直接设定世界参数
|
||||
- 用 `dfannals/legends` 进入 legends 模式(做法取自 DFHack 官方 `open-legends.lua`)
|
||||
- 用 `dfannals/act` / `devel/send-key` 发按键
|
||||
|
||||
> 备选:DF 自带命令行世界生成器 `"Dwarf Fortress.exe" -gen 1 RANDOM "MEDIUM ISLAND"`,
|
||||
> 可静默生成并导出,但**不留存档**,因此无法继续进 legends 导出,只适合快速验证参数。
|
||||
|
||||
## 六、子命令
|
||||
|
||||
```bash
|
||||
python3 -m dfannals.cli status # 当前世界 / 卷号 / 已写章节
|
||||
python3 -m dfannals.cli plan # 只看切章方案,不调模型
|
||||
python3 -m dfannals.cli index # 解析 exports → 年表 + 人物索引
|
||||
python3 -m dfannals.cli next # 生成下一章(--dry-run 只生成不推送)
|
||||
python3 -m dfannals.cli run # index + next,一条命令走完
|
||||
python3 tests/test_factcheck.py # 校验器回归测试
|
||||
```
|
||||
|
||||
实测:**约 2.5 分钟 / 章**(解析 33 秒 + 模型 1–2 分钟),单章 1000–1400 字。
|
||||
|
||||
## 七、配置在哪
|
||||
|
||||
| 想改什么 | 改哪 |
|
||||
|---|---|
|
||||
| 世界大小 / 历史长度 | `scripts/make-world.sh` 的两个档位参数 |
|
||||
| 切章粒度(几年一章、每章几条、同类上限) | `dfannals/slice.py` 顶部常量 |
|
||||
| 哪些事件值得写(显著度评分) | `dfannals/score.py` 的 `NARRATIVE` 表 |
|
||||
| 文风、人设、演绎边界 | `prompts/chronicler.md` |
|
||||
| 章节字数区间 | `dfannals/config.py` 的 `CHAPTER_MIN/MAX_CHARS` |
|
||||
| 模型与网关 | `dfannals/config.py` 的 `PROVIDER` / `MODEL` |
|
||||
| 发布目标、仓库名 | `dfannals/config.py` 的 `GITEA_*` |
|
||||
|
||||
**API key 不入库**:脚本运行时从 `~/.pi/agent/auth.json` 读取,只在内存中,不打印、不落盘、不进 git。
|
||||
|
||||
## 八、踩过的坑(失败排查参考)
|
||||
|
||||
| 症状 | 原因与解法 |
|
||||
|---|---|
|
||||
| `not well-formed (invalid token)` | DF 在名字里嵌了 `\x10`/`\x11` 等 CP437 控制字符,XML 1.0 禁止。管道开头的预处理会剔除并转 UTF-8,缓存到 `data/exports/clean/` |
|
||||
| 事件数莫名翻倍 | 预处理缓存若与原始文件同目录,会被 `*.xml` 扫描当成第二份史料。缓存因此放在子目录 |
|
||||
| 素材里出现 `HF#-1`、`Site#-1` | DF 用负数占位表示"无此项"。查询接口对负 id 返回空串,渲染时跳过 |
|
||||
| 校验器把真名报成可疑 | 史料里是全小写程序名(`yemi deermoths`),正文做了首字母大写。已知名字集同时收录两种形式(有回归测试) |
|
||||
| `dfhack-run "cmd a b"` 报 not recognized | 参数要分开传:`dfhack-run dfannals/click 10 2`,不要整体加引号 |
|
||||
| 切出几千章 | 40 万条事件里绝大多数是流水账(`change hf state` 7.3 万条)。必须先按显著度筛选,再按时间窗分桶 |
|
||||
| Bay12 下载只有 ~10KB/s | 官网无国内镜像。放后台断点续传即可(16MB 约 27 分钟) |
|
||||
| GitHub release 下载 0 字节 | 直连被断。用 `https://gh-proxy.com/` 前缀 |
|
||||
| 观测 / init 脚本不生效 | 53.16 只执行固定几个 init 文件。改用 `dfhack-run` 从外部驱动,更可靠 |
|
||||
|
||||
## 九、写作设定
|
||||
|
||||
- **叙述者**:世界档案库里的编年史官——少数亲历,多数事后依据档案与口述追述,因此对陈年旧账有看法
|
||||
- **口吻**:战报体,严肃的骨架 + 诙谐的血肉,允许吐槽
|
||||
- **语言**:中文;专有名词保留英文原文(首字母大写以便阅读)
|
||||
- **演绎边界**:允许虚构对白、心理、场景;**不许发明史料里没有的人名地名**,不许改写史实骨架(年份、参与者、胜负)
|
||||
- **连载**:同一世界长期连载;一部史写完则用 `make-world.sh` 生成新世界开新卷
|
||||
|
||||
## 十、当前状态与限制
|
||||
|
||||
- **已完成**:3 卷框架 + 第 1、2 章上线(Mon Sagus 世界,1–250 年,26 章规划)
|
||||
- **定时任务未启用**:`cron` / 开机自启等稳定运行一段时间后再落地;目前按需手动执行
|
||||
- **原始数据不入库**:`data/` 已在 `.gitignore` 中
|
||||
- **诚实说明**:世界创建各步骤均已逐步实测通过,并已整合为 `make-world.sh`;但该脚本尚未作为**单次完整调用**重跑验证(单次约 15 分钟),首次使用时请留意各阶段日志
|
||||
|
||||
## 十一、声明
|
||||
|
||||
《Dwarf Fortress》版权归 Bay 12 Games 所有。本仓库只包含由其生成数据的**衍生文本**,
|
||||
不含游戏本体、美术资源或存档。
|
||||
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# 自动生成一个新世界并导出 legends 数据。
|
||||
#
|
||||
# scripts/make-world.sh [世界大小档位] [历史年数档位]
|
||||
# 世界大小:0=Pocket 1=Smaller 2=Small 3=Medium 4=Large (默认 3 = Medium)
|
||||
# 历史长度:0=5年 1=50年 2=100年 3=250年 4=500年 (默认 3 = 250 年)
|
||||
#
|
||||
# 前置:游戏已启动到标题界面(Dwarf Fortress.exe,DFHack 已通过 dfhooks 注入)。
|
||||
# 原理:全程用 hack/dfhack-run.exe 从外部驱动,不依赖人工点击:
|
||||
# 1. 文本截图定位按钮(scripts/df-screen.py)
|
||||
# 2. 模拟鼠标点击 / 直接改内存里的世界生成参数
|
||||
# 3. 用 DFHack 的 open-legends 原语进入 legends 模式
|
||||
# 4. 点 "Export XML" 导出原生 legends.xml,并跑 exportlegends 导出扩展数据
|
||||
set -euo pipefail
|
||||
|
||||
WIN_USER="${WIN_USER:-23518}"
|
||||
DF_DIR="${DF_DIR:-/mnt/c/Users/${WIN_USER}/DwarfFortress}"
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
RUN="$DF_DIR/hack/dfhack-run.exe"
|
||||
WORLD_SIZE="${1:-3}"
|
||||
HISTORY="${2:-3}"
|
||||
|
||||
[[ -x "$RUN" ]] || { echo "找不到 dfhack-run.exe:$RUN" >&2; exit 1; }
|
||||
cd "$DF_DIR"
|
||||
|
||||
say() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||
probe() { timeout 30 "$RUN" dfannals/probe >/dev/null 2>&1 || true; head -1 dfannals-probe.txt | cut -d= -f2; }
|
||||
shot() { timeout 60 "$RUN" dfannals/screentext >/dev/null 2>&1; }
|
||||
click_text() { # click_text <文字> [标签]
|
||||
local coord
|
||||
coord=$(python3 "$HERE/scripts/df-screen.py" click ${2:+--label "$2"} --text "$1")
|
||||
say "点击「$1」@ $coord"
|
||||
timeout 30 "$RUN" dfannals/click $coord >/dev/null 2>&1
|
||||
}
|
||||
|
||||
say "当前界面:$(probe)"
|
||||
[[ "$(probe)" == title* ]] || { echo "请先把游戏停在标题界面" >&2; exit 1; }
|
||||
|
||||
say "—— 进入世界创建"
|
||||
shot; click_text "Create new world"
|
||||
sleep 3
|
||||
|
||||
say "—— 设定参数:大小档 $WORLD_SIZE / 历史档 $HISTORY"
|
||||
timeout 30 "$RUN" dfannals/setparams "$WORLD_SIZE" "$HISTORY" >/dev/null 2>&1
|
||||
cat dfannals-params.txt
|
||||
|
||||
say "—— 开始生成(Medium+250 年实测约 7 分钟)"
|
||||
shot; click_text "Create world"
|
||||
|
||||
for _ in $(seq 1 60); do
|
||||
sleep 30
|
||||
shot
|
||||
if ! grep -q "Recounting legends" dfannals-screen.txt && ! grep -q "Generating world" dfannals-screen.txt; then
|
||||
say "地形与历史生成结束"
|
||||
break
|
||||
fi
|
||||
say " 生成中… 年份 $(grep current_year dfannals-probe.txt | cut -d= -f2)"
|
||||
done
|
||||
grep -m1 -oE "year [0-9]+" dfannals-screen.txt || true
|
||||
|
||||
say "—— 保存世界并回到主菜单"
|
||||
shot; click_text "Keep world and return to main menu"
|
||||
for _ in $(seq 1 30); do
|
||||
sleep 20
|
||||
[[ "$(probe)" == export_region ]] || break
|
||||
done
|
||||
say "存档完成,当前界面:$(probe)"
|
||||
|
||||
say "—— 进入 legends 模式"
|
||||
timeout 40 "$RUN" dfannals/legends >/dev/null 2>&1
|
||||
sleep 4
|
||||
[[ "$(probe)" == legends* ]] || { echo "未能进入 legends,界面=$(probe)" >&2; exit 1; }
|
||||
|
||||
say "—— 导出(原生 Export XML + 扩展数据)"
|
||||
shot; click_text "Export XML"
|
||||
for _ in $(seq 1 60); do
|
||||
sleep 20
|
||||
n=$(ls -1 region*-legends.xml 2>/dev/null | wc -l)
|
||||
if [ "$n" -gt 0 ]; then
|
||||
a=$(stat -c%s "$(ls -1t region*-legends.xml | head -1)" 2>/dev/null || echo 0)
|
||||
sleep 20
|
||||
b=$(stat -c%s "$(ls -1t region*-legends.xml | head -1)" 2>/dev/null || echo 0)
|
||||
[ "$a" = "$b" ] && { say "legends.xml 已停止增长($b 字节),导出完成"; break; }
|
||||
fi
|
||||
say " 导出中…"
|
||||
done
|
||||
|
||||
EXPORTS="$HERE/data/exports"
|
||||
mkdir -p "$EXPORTS"
|
||||
cp -v region*-legends.xml region*-world_history.txt region*-world_sites_and_pops.txt "$EXPORTS"/ 2>/dev/null | tail -5
|
||||
say "导出文件已复制到 $EXPORTS"
|
||||
say "接下来跑:python3 -m dfannals.cli run"
|
||||
Reference in New Issue
Block a user