<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Ag's Playground</title>
        <link>https://agxcoy.shimakaze.org</link>
        <description>Silver=Ag, L is Lin.</description>
        <lastBuildDate>Wed, 13 May 2026 15:43:23 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Released under the CC BY-SA 4.0 license.</copyright>
        <item>
            <title><![CDATA[Arch Linux 个人安（折）装（腾）流程]]></title>
            <link>https://agxcoy.shimakaze.org/posts/arch-install</link>
            <guid>https://agxcoy.shimakaze.org/posts/arch-install</guid>
            <pubDate>Wed, 13 May 2026 12:36:50 GMT</pubDate>
            <description><![CDATA[
::: details 我选择 Arch 的理由
1. 比起“服务”，我还是更倾向于把操作系统当作纯正的工具。可能这就是旧信息时代遗老吧。
2. “缘，妙不可言”。
3. 具体工作具体分析吧。跨平台开发 Arch 也挺舒服的。但 Adobe 全家桶就显然不适合了。
:::

最近重新组织了下家里的设备，咱的笔记本也是先后经历了 Arch 转 Win11 再转 Arch 的路子。时至今日，我曾经跟着律回指南安装 Arch 的步骤有些不再适用，有些需要补充。总而言之，还是重新整理一下我的流程吧。

> [!important]
> 由于 Arch 更迭速度比较快，下面的参考链接以及这篇笔记本身的内容可能随时失效。  
> 在安装、使用过程中遇到的，这里没有提及的问题，还请自行 Google、Bing 或 Baidu。
>
> 如果你觉得 Arch 滚动更新很累、玩不太明白，不妨还是先上手`Pop!_OS`或者`Ubuntu`。
>
> 此外，也可以多多留意其他人总结的 Arch 折腾小技巧，说不定会有意外收获。

## 参考链接

本文有参考以下的安装教程：

1. [律回彼境：Arch Linux 折腾指南&记录](https://www.glowmem.com/archives/archlinux-note)（以下简称“律回指南”）
2. [Nakano Miku：Arch 简明指南](https://arch.icekylin.online/guide/)（以下简称“Miku 指南”）
3. [Arch Wiki：Installation Guide](https://wiki.archlinux.org/title/Installation_guide)

## I. 前期准备

- 下载安装镜像：[清华镜像 (最新版本)](https://mirrors.tuna.tsinghua.edu.cn/archlinux/iso/latest)、[官方下载](https://archlinux.org/download/)。

> 烧录过程不再赘述，推荐用 Ventoy 统一管理安装镜像。[参见 Ventoy 中文主页](https://www.ventoy.net/cn/)

- 固件^1^：启用 UEFI、禁用安全启动（Secure Boot）。

> 近十几年的主板大都支持 UEFI，我也懒得花篇幅去讲传统 BIOS 引导。对于 Arch Linux 的 UEFI 引导方式，我[另有一篇笔记](./arch-uefi.md)讨论，可供部署阶段参考。  
> 至于 Secure Boot，不用想了，给`.efi`启动文件签名着实是件麻烦事。~~对我而言折腾这个没有意义。~~

- 网络^2^：如需连接 WiFi，提前把 WiFi 名字（SSID）改成英文。

> 安装全程在命令行（CLI）环境进行，并且 LiveCD（维护环境，下同）**显示不出中文，也打不出中文**。

> [!tip]
> 如果只是迁移系统，那么进入维护环境之后只需`rsync`做全盘搬运即可（当然前提是目标**盘**要比原**系统**的实际占用空间要大）。可参见 [lin.moe](https://lin.moe/tutorial/2020/04/arch_migrate/)。

## II. LiveCD 基础配置

与[律回指南 Ch.1](https://glowmem.com/archives/archlinux-note#%E4%B8%80%E8%BF%9E%E6%8E%A5%E7%BD%91%E7%BB%9C%E5%92%8C%E6%97%B6%E5%8C%BA%E9%85%8D%E7%BD%AE) 相同，但省略了分配固定 IP 的一步（我完全可以`ip addr`猹询）。

## III. 分区
对于固态硬盘，**不提倡建立过多的分区**。那么在 UEFI 启动、GPT 分区表的系统盘上，至少可以这么分：

- EFI 启动分区[^esp]：挂载`/efi`或`/boot`[^esp_mountpoint]
- 系统分区：挂载根目录`/`

我个人在此基础上，倾向于*在硬盘（逻辑扇区的）末端*开多一个交换分区。

[^esp]: GPT 分区表有专门的“EFI System”分区类型（即 ESP），当然新款的主板也会*连带扫描 FAT 分区*；对于 MBR 分区表，则扫描**活动**的 FAT 分区。参见 [（译）UEFI 启动的实际工作原理](https://www.cnblogs.com/mahocon/p/5691348.html)。

[^esp_mountpoint]: ESP 装载`/boot`系经典分区法。后来有说法称“直接暴露 Linux 内核并不安全”，所以又有将 ESP 装入`/boot/efi`的分法（Ubuntu Server 24.04.2 即如此）。现在（至少在 Arch 里）则推荐直接挂载到`/efi`。

详细的分区步骤参见 Miku 指南。由于该指南假定保留 Windows 系统分区，其对分区方案的介绍实际上拆成了两部分：

- :new: [全新安装](https://arch.icekylin.online/guide/rookie/basic-install-detail#%F0%9F%86%95-%E5%85%A8%E6%96%B0%E5%AE%89%E8%A3%85)
- [7. 分区和格式化（使用 Btrfs 文件系统）](https://arch.icekylin.online/guide/rookie/basic-install.html#_7-%E5%88%86%E5%8C%BA%E5%92%8C%E6%A0%BC%E5%BC%8F%E5%8C%96-%E4%BD%BF%E7%94%A8-btrfs-%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F)

然后是挂载。务必注意**先挂载`/`**！此外，**不要把不同分区、子卷挂到同一个挂载点上**；**有分交换分区的话记得`swapon`**。

## IV. pacman 配置

### i. 换源

Linux 的包管理器默认食用国外的软件源，`pacman`也一样。因此，非常建议先换用国内镜像，加快包下载速度。

然鹅在更换之前可能需要等待片刻：一经联网，`reflector`会自动筛选**最新**的 20 个镜像站，然后才从快到慢排序^3^。如果换源过早，很可能会被`reflector`摘桃子。

编辑`/etc/pacman.d/mirrorlist`（`vim`还是`nano`请自便）：
```ini
# 在文件开头起一空行，复制下列镜像源：
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
Server = https://mirrors.aliyun.com/archlinux/$repo/os/$arch
```
> [!tip]
> 变更后的`mirrorlist`会在 Arch 安装过程中被复制过去。这样后续就不需要再做一遍换源了。

### ii. 自身配置
默认`pacman`是逐个下载软件的。但哪怕是 1MB/s 小水管，并行下载四、五个软件包也绰绰有余了。

编辑`/etc/pacman.conf`：

1. 找到`# Misc options`，删掉`Color` `ParallelDownloads = 5`前面的注释`#`：
```ini
# Misc options
#UseSyslog
Color            # 输出彩色日志
#NoProgressBar
CheckSpace
#VerbosePkgLists
ParallelDownloads = 5   # 最大并行下载数（根据你的网速自行斟酌，不建议写太大）
```

2. 翻页到文件末尾，删掉`[multilib]`和底下`Include =`这两行的注释`#`。
> `multilib`是 32 位软件源。默认下载的包都是`x86_64`的，而有一些程序仍需要 32 位的库。

> [!note]
> 很遗憾，经实测 pacman 配置并不会复制过去。在安装完系统`arch-chroot`进去进一步配置时，你需要重复做一遍上述操作。

## V. 正式部署

参见 [Miku 指南—基础安装—9. 安装系统](https://arch.icekylin.online/guide/rookie/basic-install.html#_9-%E5%AE%89%E8%A3%85%E7%B3%BB%E7%BB%9F)。或者像律回指南那样顺手装一些工具也无何不可：

```sh
pacstrap -K /mnt base linux linux-firmware base-devel linux-headers \
  vi vim nano git wget tmux openssh networkmanager htop ntfs-3g \
  intel-ucode    # or `amd-ucode`
```

::: warning “密钥环不可写”报错
之前出现过`pacman-init.service`意外暴死，导致找不到密钥环的情况：
```log
(126/126) checking keys in keyring
warning: Public keyring not found; have you run `pacman-key --init`?
downloading required keys...
error: keyring is not writable
...
error: failed to commit transaction (unexpected error)
```
对此，[这篇讨论贴](https://bbs.archlinux.org/viewtopic.php?id=283075)中有人提供了个取巧的方案：
```sh
pacman-key --init
pacman-key --populate
```
实测可用。至于成因，贴中有人指出是系统时钟未校准导致，但我当时是来不及`timedatectl`就已经暴死了，具体原因不明。
:::

然后`genfstab -U /mnt > /mnt/etc/fstab`生成挂载表；  
再`arch-chroot /mnt`切换进新系统里，继续配置吧。

---

接下来在`chroot`环境里的配置我参考了[律回指南 Ch.4](https://glowmem.com/archives/archlinux-note#%E5%9B%9B%E7%B3%BB%E7%BB%9F%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE)，大体也符合官方文档的流程：调整时区、系统编码、设置主机名、root 密码、新建用户、配置 EFI 引导。

不过在`visudo`那一步我没有启用“免密`sudo`”（如下），律回本人也意识到免密`sudo`并不安全。
```
## Same thing without a password
#%wheel ALL=(ALL:ALL) NOPASSWD: ALL
```

> [!note]
> 除了上述标准流程之外，后续有些步骤也可以提前在`chroot`里完成。但**如果你是初次上手，还是一步一步慢慢来吧**。

## VI. 新系统的配置
跟着律回指南的三、四章装好系统之后，重启登入新系统的终端。  
首先通过`nmtui`连上 WiFi。

### i. CN 源和 AUR 助手
在**联好网的新系统**里配置`archlinuxcn`源：再次打开`/etc/pacman.conf`，末尾添加如下小节
```ini
[archlinuxcn]
Server = https://mirrors.cernet.edu.cn/archlinuxcn/$arch
```
并安装 CN 源的签名密钥和 AUR 助手：
```bash
sudo pacman -S archlinuxcn-keyring  # 安装密钥环
sudo pacman -S yay paru   # 安装 AUR 助手
```
::: warning 密钥环缺少信任
若 CN 源的包安装失败，遭遇`signature ... is marginal trust`报错，可本地信任那个人的 Key。之前`farseerfc`掉过一次，以他为例：
```sh
sudo pacman-key --lsign-key "farseerfc@archlinux.org"
```
然后重试即可。不过截至 25 年国庆为止，`farseerfc`的信任已经恢复，我安装时并未遭遇类似情况。
:::

### ii. 硬件（一）音频安装

音频分为固件（或者说驱动）和管理套件两部分：
```bash
# 音频固件
sudo pacman -S sof-firmware alsa-firmware alsa-ucm-conf
# pipewire 及其音频管理套件
sudo pacman -S pipewire gst-plugin-pipewire pipewire-alsa pipewire-jack pipewire-pulse wireplumber
```
::: details pulseaudio
除了 pipewire 音频方案之外另有`pulseaudio`可供选择。但务必注意：音频管理套件**只能二选一，不可以混装**。

另外，由于 pipewire 本身不单只负责音频管理的工作，如需装 pulseaudio 仍需安装`pipewire` `gst-plugin-pipewire`两个包。
相应地，其余的包可换用如下平替：

- `pipewire-alsa` → `pulseaudio-alsa`
- `pipewire-jack` → `jack2`
- `pipewire-pulse` → `pulseaudio`
- `wireplumber` → `pipewire-media-session`（pipewire 弃用）

> 由于 pipewire 那边有 wireplumber 代替，所以这个包被他们自行标记为“过时”。  
> 但 pulseaudio 仍需要这个包。
:::

显卡驱动等其他硬件设施需要等装好桌面环境再考虑，在只有字符色块滚过的纯终端环境里也没有折腾显卡驱动的必要。

### iii. KDE 桌面环境

跟完前面的内容之后，你便拥有了一个无 GUI 的终端 Arch 系统。但作为日常使用的话，图形桌面肯定必不可少。

本文与那两篇参考外链一样**采用 KDE 桌面环境**。当然除了 KDE 之外，你也可以考虑 GNOME 桌面环境 ~~（只是我用腻了）~~；
也可以考虑散装方案（比如`niri`，部分配置可参见 [aglab.dotfiles](https://git.liteyuki.org/AgxCOy/aglab.dotfiles)）。

```bash
# 分别安装 xorg 套件、sddm 登录管理器、KDE 桌面环境，以及配套软件
sudo pacman -S xorg
sudo pacman -S plasma sddm konsole dolphin kate okular spectacle partitionmanager ark filelight gwenview
# 启用 sddm 服务，重启进 SDDM 用户登录
sudo systemctl enable sddm
sudo reboot now
```

::: info KDE 6 vs KDE 5？
目前最新版本为 KDE 6。但律回指南发布于 23 年 11 月，介绍的是 KDE 5。
话虽如此，倒也不必惊慌。`pacman`以及`yay` `paru`之流均**默认安装最新版**，上述**安装 KDE 5 的步骤仍可用于安装 KDE 6**。
:::

重启后在用户登录界面输入密码回车，恭喜你，距离投入日常使用只剩几步之遥了。之后对 KDE
和系统的配置**大部分**仍可参考[律回指南 Ch.6](https://glowmem.com/archives/archlinux-note#%E5%85%AD%E6%A1%8C%E9%9D%A2%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE)。

<!-- ::: note 关于 Wayland 和 X -->
> [!note] 关于 Wayland 和 X
> KDE 的图形实现默认已经是 Wayland 了。在开机后输入用户密码的界面处，找找屏幕边角，你可以看到默认选用`Plasma (Wayland)`。
> 点击它，你可以选择换用`Plasma (X11)`。  
> 尽管 X11 有个“锁屏黑屏”[^x11_lockscreen]的问题，但目前来说我还是推荐换回 X11。
>
> [^x11_lockscreen]: KDE 的默认 Breeze 主题锁屏时大概率会出现黑屏、惟有鼠标的现象。在 7 月中旬时已经发现该现象已经蔓延到自定义主题了。查了下 Google 以及 Arch、Manjaro、KDE 的一些讨论帖，尚没有有效的解决方案。  
> 当然有一些主题可能能够解除这个“病征”，像 Nordic Dark 以及 Lavanda。但这种 work around 可能还是因人而异。
<!-- ::: -->

### iv. 硬件（二）显卡驱动与蓝牙

> “so NVIDIA, F**K YOU! ”——Linus Torvalds

AMD 或 NVIDIA 显卡可参见[律回指南 &sect;6.4](https://glowmem.com/archives/archlinux-note#4%E6%98%BE%E5%8D%A1%E9%A9%B1%E5%8A%A8%E5%AE%89%E8%A3%85)
和 [Miku 指南—进阶安装—显卡驱动](https://arch.icekylin.online/guide/rookie/graphic-driver.html)篇。
但我是锐炬核显捏，只需要在 Konsole 终端里`sudo pacman -S`安装英特尔的驱动：

- `mesa` `lib32-mesa`（OpenGL）
- `vulkan-intel` `lib32-vulkan-intel`（Vulkan）
- `intel-media-driver`（VAAPI 解码器，OBS 需要）

如果有蓝牙的话，在 Konsole 里启用（并立即启动）蓝牙服务：
```bash
sudo systemctl enable --now bluetooth
```
> 之前误以为`bluetooth`是 Arch 本身就有的服务，结果发现是桌面环境依赖了蓝牙组件包`bluez`。

### v. 额外中文字体和输入法

律回指南安装的字体分别是 Noto 系列（Linux 常用的 Unicode 字体）和思源系列（也算是 Noto 系列的子集）。
其中 Noto 系列的汉字部分由于一些神秘的原因[^cjk_issues_ref]，不做额外配置的话，渲染出来只能说……能用。

[^cjk_issues_ref]: 参见 [Arch Wiki：关于中文字被异常渲染成日文异体字的说明](https://wiki.archlinux.org/title/Localization/Simplified_Chinese#Chinese_characters_displayed_as_variant_(Japanese)_glyphs)^2^以及 [Arch 中文论坛：noto-fonts-cjk 打包变化可能导致的回落字体选取问题](https://bbs.archlinuxcn.org/viewtopic.php?pid=60100)^2^。

<!-- ::: note fontconfig -->
> [!note] fontconfig
> `wqy-zenhei`^extra^（文泉驿）和`misans`^aur^会在安装过程中自动帮你配置 fontconfig，因此安装完这两款字体之后系统默认用这些字体显示。
> 如果你希望使用未经适配的字体，那么需要在 KDE 设置里装好字体后，额外做字体配置。
>
> 示例：[思源系列字体配置](../../shared/01-Prefer.conf) By [@Vescrity](https://github.com/Vescrity)  
> 注意：用户级字体配置需放在`~/.config/fontconfig/conf.d`目录中。  
> 另注：使用`fc-cache -vf`刷新字体缓存。
<!-- ::: -->

至于输入法，现阶段推荐直接安装`fcitx5`，参见 [Miku 指南—进阶安装—常用应用—10. 输入法](https://arch.icekylin.online/guide/rookie/desktop-env-and-app.html#_10-%E5%AE%89%E8%A3%85%E8%BE%93%E5%85%A5%E6%B3%95)。

至此，Arch 的安装告一段落，你可以像捣腾 Windows 那样玩转 Arch 了。

---

## 附录：系统美化

> “爱美之心，人皆有之。”

> [!tip]
> - **风格统一**是美观的必要条件。
> - 少搞“侵入性”美化。或者说，需要**修改系统文件、注入系统进程、破坏系统稳定的美化尽量少做**。
> - **谨遵发布页面附送的安装指引**（KDE、GNOME 主题可以参考项目 GitHub），否则可能安装不全。

### I. 主题
主题这边我也没啥好推荐的，虽然 KDE 6 现在也出现了一些比较好看的主题，但终究是因人而异吧。

我想说明的是，KDE 商店的多数主题在 **X11 会话、125% 甚至更高缩放率**下会出现“非常粗窗口边框，使我的窗口肥胖”的现象（至少我的笔记本如此）。  
我个人目前是直接修改主题 Aurorae 配置文件，利用二分法逐步找到四条边的最适 Padding。
网上貌似也有“把缩放调回 100%，但是更改字体 DPI”的做法，但个人觉得显示效果应该好不到哪去（

### II. 仿 Mac 上下双栏布局
KDE 原生的桌面 UI 就挺 Windows 的，但胜在自由度足够高。
我**个人觉得** Mac OS 那种双栏比较好看、比较方便，所以稍微按照如下配置调整了面板布局。

仅供参考咯。

::: details Dock 栏
即原本的任务栏。
- 位于底部、居中、适宜宽度、取消悬浮、避开窗口
- 除“图标任务管理器”外，其余组件全部移除。
:::

::: details Finder 栏
即“应用程序菜单栏”（可在 *编辑模式—添加面板* 处找到）
- 位于顶部、居中、填满宽度、取消悬浮、常驻显示
- 自左到右依次为：
  - 应用程序启动器（类比开始菜单）
  - 窗口列表
  - 全局菜单（默认提供）
  - “面板间距”留白
  - 数字时钟
    - 日期保持在时间旁边，而不是上下两行
    - 字号略小于菜单栏高度，凭感觉捏
  - “面板间距”留白
  - 系统监视传感器
    - 横向柱状图（平均 CPU 温度、最高 CPU 温度）
    - 仅文字（网络上行、下行速度；网络上传、下载的总流量）
  - 系统托盘
:::

除了 Finder 栏外，可以在系统设置里更改屏幕四周的鼠标表现。
比如，鼠标移动到左上角可以自动弹出“应用程序启动器”，移到右上角可以切换你的桌面，等等。

## 附录：GPG 密钥配置
主要讨论配置提交签名（Commit Signing）时遇到的问题。

### I. VSCode 提交签名
大体上跟着 [Commit Signing - VSCode Wiki](https://github.com/microsoft/vscode/wiki/Commit-Signing) 就可以了。唯一需要留意的是`pinentry`。

VSCode 的主侧栏“源代码管理”页提交时并不会走终端，也就莫得 pinentry 的 CUI；莫得 pinentry 输密码验证，提交就签不了名。
虽然有人好像搞了个`pinentry-extension`出来，但 6 月初我去看的时候它连说明书都莫得，也没有上架，那用集贸。

所以我选择编辑`~/.gnupg/gpg-agent.conf`：
```properties
default-cache-ttl 21600
pinentry-program /usr/bin/pinentry-qt
```
保存后重启`gpg-agent`：`gpg-connect-agent reloadagent /bye`。

::: info 其他端 pinentry
理论上`pinentry`自身可以根据 XDG 后端选择适用的版本：
```sh
ls /usr/bin | grep 'pinentry'
echo GETPIN | pinentry
```
你可以用这两条命令看看都支持什么后端，以及它会自动选择哪个。像 niri 通常会装 gnome 后端，测试结果也的确会出`pinentry-gnome3`的密码框。
:::

除此之外，`pinentry`需要指定 tty，否则找不到 IO 设备也会炸。解法：`export GPG_TTY=$(tty)`。

经测试，大部分终端均能在 SSH 连接中调出 CUI。若 VSCode Remote-SSH 打开的终端签不了名，检查一下是不是终端分割得太小了。

### II. GPG 密钥备份（导出导入）
之前并没有意识到备份 key 的重要性，结果重装 Arch 重新配置提交签名时，
我发现 GitHub 和腾讯 Coding 会重置提交验证（同一个邮箱只能上传一个公钥），届时就是我痛苦的 rebase 重签了。
~~不过好在受影响的多数只是我的个人项目，变基无伤大雅。~~

```bash
gpg --list-secret-keys --keyid-format LONG
# export
gpg -a -o public-file.key --export <keyid>
gpg -a -o private-file.key --export-secret-keys <keyid>
# import
gpg --import ~/public-file.key
gpg --allow-secret-key-import --import ~/private-file.key
```
重新导入 Key 之后，可能还需要`gpg --edit-key`更改密码（`passwd`）、重设信任（`trust`）。
]]></description>
            <content:encoded><![CDATA[<details class="details custom-block"><summary>我选择 Arch 的理由</summary>
<ol>
<li>比起“服务”，我还是更倾向于把操作系统当作纯正的工具。可能这就是旧信息时代遗老吧。</li>
<li>“缘，妙不可言”。</li>
<li>具体工作具体分析吧。跨平台开发 Arch 也挺舒服的。但 Adobe 全家桶就显然不适合了。</li>
</ol>
</details>
<p>最近重新组织了下家里的设备，咱的笔记本也是先后经历了 Arch 转 Win11 再转 Arch 的路子。时至今日，我曾经跟着律回指南安装 Arch 的步骤有些不再适用，有些需要补充。总而言之，还是重新整理一下我的流程吧。</p>
<div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p>
<p>由于 Arch 更迭速度比较快，下面的参考链接以及这篇笔记本身的内容可能随时失效。<br>
在安装、使用过程中遇到的，这里没有提及的问题，还请自行 Google、Bing 或 Baidu。</p>
<p>如果你觉得 Arch 滚动更新很累、玩不太明白，不妨还是先上手<code>Pop!_OS</code>或者<code>Ubuntu</code>。</p>
<p>此外，也可以多多留意其他人总结的 Arch 折腾小技巧，说不定会有意外收获。</p>
</div>
<h2 id="参考链接" tabindex="-1">参考链接 <a class="header-anchor" href="#参考链接" aria-label="Permalink to &quot;参考链接&quot;"></a></h2>
<p>本文有参考以下的安装教程：</p>
<ol>
<li><a href="https://www.glowmem.com/archives/archlinux-note" target="_blank" rel="noreferrer">律回彼境：Arch Linux 折腾指南&amp;记录</a>（以下简称“律回指南”）</li>
<li><a href="https://arch.icekylin.online/guide/" target="_blank" rel="noreferrer">Nakano Miku：Arch 简明指南</a>（以下简称“Miku 指南”）</li>
<li><a href="https://wiki.archlinux.org/title/Installation_guide" target="_blank" rel="noreferrer">Arch Wiki：Installation Guide</a></li>
</ol>
<h2 id="i-前期准备" tabindex="-1">I. 前期准备 <a class="header-anchor" href="#i-前期准备" aria-label="Permalink to &quot;I. 前期准备&quot;"></a></h2>
<ul>
<li>下载安装镜像：<a href="https://mirrors.tuna.tsinghua.edu.cn/archlinux/iso/latest" target="_blank" rel="noreferrer">清华镜像 (最新版本)</a>、<a href="https://archlinux.org/download/" target="_blank" rel="noreferrer">官方下载</a>。</li>
</ul>
<blockquote>
<p>烧录过程不再赘述，推荐用 Ventoy 统一管理安装镜像。<a href="https://www.ventoy.net/cn/" target="_blank" rel="noreferrer">参见 Ventoy 中文主页</a></p>
</blockquote>
<ul>
<li>固件<sup>1</sup>：启用 UEFI、禁用安全启动（Secure Boot）。</li>
</ul>
<blockquote>
<p>近十几年的主板大都支持 UEFI，我也懒得花篇幅去讲传统 BIOS 引导。对于 Arch Linux 的 UEFI 引导方式，我<a href="./arch-uefi">另有一篇笔记</a>讨论，可供部署阶段参考。<br>
至于 Secure Boot，不用想了，给<code>.efi</code>启动文件签名着实是件麻烦事。<s>对我而言折腾这个没有意义。</s></p>
</blockquote>
<ul>
<li>网络<sup>2</sup>：如需连接 WiFi，提前把 WiFi 名字（SSID）改成英文。</li>
</ul>
<blockquote>
<p>安装全程在命令行（CLI）环境进行，并且 LiveCD（维护环境，下同）<strong>显示不出中文，也打不出中文</strong>。</p>
</blockquote>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>如果只是迁移系统，那么进入维护环境之后只需<code>rsync</code>做全盘搬运即可（当然前提是目标<strong>盘</strong>要比原<strong>系统</strong>的实际占用空间要大）。可参见 <a href="https://lin.moe/tutorial/2020/04/arch_migrate/" target="_blank" rel="noreferrer">lin.moe</a>。</p>
</div>
<h2 id="ii-livecd-基础配置" tabindex="-1">II. LiveCD 基础配置 <a class="header-anchor" href="#ii-livecd-基础配置" aria-label="Permalink to &quot;II. LiveCD 基础配置&quot;"></a></h2>
<p>与<a href="https://glowmem.com/archives/archlinux-note#%E4%B8%80%E8%BF%9E%E6%8E%A5%E7%BD%91%E7%BB%9C%E5%92%8C%E6%97%B6%E5%8C%BA%E9%85%8D%E7%BD%AE" target="_blank" rel="noreferrer">律回指南 Ch.1</a> 相同，但省略了分配固定 IP 的一步（我完全可以<code>ip addr</code>猹询）。</p>
<h2 id="iii-分区" tabindex="-1">III. 分区 <a class="header-anchor" href="#iii-分区" aria-label="Permalink to &quot;III. 分区&quot;"></a></h2>
<p>对于固态硬盘，<strong>不提倡建立过多的分区</strong>。那么在 UEFI 启动、GPT 分区表的系统盘上，至少可以这么分：</p>
<ul>
<li>EFI 启动分区<sup class="footnote-ref"><a href="#footnote1">[1]</a><a class="footnote-anchor" id="footnote-ref1"></a></sup>：挂载<code>/efi</code>或<code>/boot</code><sup class="footnote-ref"><a href="#footnote2">[2]</a><a class="footnote-anchor" id="footnote-ref2"></a></sup></li>
<li>系统分区：挂载根目录<code>/</code></li>
</ul>
<p>我个人在此基础上，倾向于<em>在硬盘（逻辑扇区的）末端</em>开多一个交换分区。</p>
<p>详细的分区步骤参见 Miku 指南。由于该指南假定保留 Windows 系统分区，其对分区方案的介绍实际上拆成了两部分：</p>
<ul>
<li>🆕 <a href="https://arch.icekylin.online/guide/rookie/basic-install-detail#%F0%9F%86%95-%E5%85%A8%E6%96%B0%E5%AE%89%E8%A3%85" target="_blank" rel="noreferrer">全新安装</a></li>
<li><a href="https://arch.icekylin.online/guide/rookie/basic-install.html#_7-%E5%88%86%E5%8C%BA%E5%92%8C%E6%A0%BC%E5%BC%8F%E5%8C%96-%E4%BD%BF%E7%94%A8-btrfs-%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F" target="_blank" rel="noreferrer">7. 分区和格式化（使用 Btrfs 文件系统）</a></li>
</ul>
<p>然后是挂载。务必注意<strong>先挂载<code>/</code></strong>！此外，<strong>不要把不同分区、子卷挂到同一个挂载点上</strong>；<strong>有分交换分区的话记得<code>swapon</code></strong>。</p>
<h2 id="iv-pacman-配置" tabindex="-1">IV. pacman 配置 <a class="header-anchor" href="#iv-pacman-配置" aria-label="Permalink to &quot;IV. pacman 配置&quot;"></a></h2>
<h3 id="i-换源" tabindex="-1">i. 换源 <a class="header-anchor" href="#i-换源" aria-label="Permalink to &quot;i. 换源&quot;"></a></h3>
<p>Linux 的包管理器默认食用国外的软件源，<code>pacman</code>也一样。因此，非常建议先换用国内镜像，加快包下载速度。</p>
<p>然鹅在更换之前可能需要等待片刻：一经联网，<code>reflector</code>会自动筛选<strong>最新</strong>的 20 个镜像站，然后才从快到慢排序<sup>3</sup>。如果换源过早，很可能会被<code>reflector</code>摘桃子。</p>
<p>编辑<code>/etc/pacman.d/mirrorlist</code>（<code>vim</code>还是<code>nano</code>请自便）：</p>
<div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 在文件开头起一空行，复制下列镜像源：</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">Server</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">Server</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">Server</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> https://mirrors.aliyun.com/archlinux/$repo/os/$arch</span></span></code></pre>
</div><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>变更后的<code>mirrorlist</code>会在 Arch 安装过程中被复制过去。这样后续就不需要再做一遍换源了。</p>
</div>
<h3 id="ii-自身配置" tabindex="-1">ii. 自身配置 <a class="header-anchor" href="#ii-自身配置" aria-label="Permalink to &quot;ii. 自身配置&quot;"></a></h3>
<p>默认<code>pacman</code>是逐个下载软件的。但哪怕是 1MB/s 小水管，并行下载四、五个软件包也绰绰有余了。</p>
<p>编辑<code>/etc/pacman.conf</code>：</p>
<ol>
<li>找到<code># Misc options</code>，删掉<code>Color</code> <code>ParallelDownloads = 5</code>前面的注释<code>#</code>：</li>
</ol>
<div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># Misc options</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">#UseSyslog</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">Color            </span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 输出彩色日志</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">#NoProgressBar</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">CheckSpace</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">#VerbosePkgLists</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">ParallelDownloads</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> 5   </span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 最大并行下载数（根据你的网速自行斟酌，不建议写太大）</span></span></code></pre>
</div><ol start="2">
<li>翻页到文件末尾，删掉<code>[multilib]</code>和底下<code>Include =</code>这两行的注释<code>#</code>。</li>
</ol>
<blockquote>
<p><code>multilib</code>是 32 位软件源。默认下载的包都是<code>x86_64</code>的，而有一些程序仍需要 32 位的库。</p>
</blockquote>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>很遗憾，经实测 pacman 配置并不会复制过去。在安装完系统<code>arch-chroot</code>进去进一步配置时，你需要重复做一遍上述操作。</p>
</div>
<h2 id="v-正式部署" tabindex="-1">V. 正式部署 <a class="header-anchor" href="#v-正式部署" aria-label="Permalink to &quot;V. 正式部署&quot;"></a></h2>
<p>参见 <a href="https://arch.icekylin.online/guide/rookie/basic-install.html#_9-%E5%AE%89%E8%A3%85%E7%B3%BB%E7%BB%9F" target="_blank" rel="noreferrer">Miku 指南—基础安装—9. 安装系统</a>。或者像律回指南那样顺手装一些工具也无何不可：</p>
<div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">pacstrap</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -K</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> /mnt</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> base</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> linux</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> linux-firmware</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> base-devel</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> linux-headers</span><span style="--shiki-light:#EA76CB;--shiki-dark:#F5C2E7"> \</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">  vi</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> vim</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> nano</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> git</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> wget</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> tmux</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> openssh</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> networkmanager</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> htop</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> ntfs-3g</span><span style="--shiki-light:#EA76CB;--shiki-dark:#F5C2E7"> \</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">  intel-ucode</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">    # or `amd-ucode`</span></span></code></pre>
</div><div class="warning custom-block"><p class="custom-block-title">“密钥环不可写”报错</p>
<p>之前出现过<code>pacman-init.service</code>意外暴死，导致找不到密钥环的情况：</p>
<div class="language-log vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">log</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">(</span><span style="--shiki-light:#D20F39;--shiki-dark:#F38BA8">126</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">/</span><span style="--shiki-light:#D20F39;--shiki-dark:#F38BA8">126</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">) checking keys in keyring</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">warning: Public keyring not found; have you run `pacman-key --init`?</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">downloading required keys...</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">error:</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> keyring is not writable</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">...</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">error:</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> failed to commit transaction (unexpected error)</span></span></code></pre>
</div><p>对此，<a href="https://bbs.archlinux.org/viewtopic.php?id=283075" target="_blank" rel="noreferrer">这篇讨论贴</a>中有人提供了个取巧的方案：</p>
<div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">pacman-key</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --init</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">pacman-key</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --populate</span></span></code></pre>
</div><p>实测可用。至于成因，贴中有人指出是系统时钟未校准导致，但我当时是来不及<code>timedatectl</code>就已经暴死了，具体原因不明。</p>
</div>
<p>然后<code>genfstab -U /mnt &gt; /mnt/etc/fstab</code>生成挂载表；<br>
再<code>arch-chroot /mnt</code>切换进新系统里，继续配置吧。</p>
<hr>
<p>接下来在<code>chroot</code>环境里的配置我参考了<a href="https://glowmem.com/archives/archlinux-note#%E5%9B%9B%E7%B3%BB%E7%BB%9F%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE" target="_blank" rel="noreferrer">律回指南 Ch.4</a>，大体也符合官方文档的流程：调整时区、系统编码、设置主机名、root 密码、新建用户、配置 EFI 引导。</p>
<p>不过在<code>visudo</code>那一步我没有启用“免密<code>sudo</code>”（如下），律回本人也意识到免密<code>sudo</code>并不安全。</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span>## Same thing without a password</span></span>
<span class="line"><span>#%wheel ALL=(ALL:ALL) NOPASSWD: ALL</span></span></code></pre>
</div><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>除了上述标准流程之外，后续有些步骤也可以提前在<code>chroot</code>里完成。但<strong>如果你是初次上手，还是一步一步慢慢来吧</strong>。</p>
</div>
<h2 id="vi-新系统的配置" tabindex="-1">VI. 新系统的配置 <a class="header-anchor" href="#vi-新系统的配置" aria-label="Permalink to &quot;VI. 新系统的配置&quot;"></a></h2>
<p>跟着律回指南的三、四章装好系统之后，重启登入新系统的终端。<br>
首先通过<code>nmtui</code>连上 WiFi。</p>
<h3 id="i-cn-源和-aur-助手" tabindex="-1">i. CN 源和 AUR 助手 <a class="header-anchor" href="#i-cn-源和-aur-助手" aria-label="Permalink to &quot;i. CN 源和 AUR 助手&quot;"></a></h3>
<p>在<strong>联好网的新系统</strong>里配置<code>archlinuxcn</code>源：再次打开<code>/etc/pacman.conf</code>，末尾添加如下小节</p>
<div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">[</span><span style="--shiki-light:#DF8E1D;--shiki-dark:#F9E2AF">archlinuxcn</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">]</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">Server</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> https://mirrors.cernet.edu.cn/archlinuxcn/$arch</span></span></code></pre>
</div><p>并安装 CN 源的签名密钥和 AUR 助手：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pacman</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -S</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> archlinuxcn-keyring</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 安装密钥环</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pacman</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -S</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> yay</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> paru</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">   # 安装 AUR 助手</span></span></code></pre>
</div><div class="warning custom-block"><p class="custom-block-title">密钥环缺少信任</p>
<p>若 CN 源的包安装失败，遭遇<code>signature ... is marginal trust</code>报错，可本地信任那个人的 Key。之前<code>farseerfc</code>掉过一次，以他为例：</p>
<div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pacman-key</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --lsign-key</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "farseerfc@archlinux.org"</span></span></code></pre>
</div><p>然后重试即可。不过截至 25 年国庆为止，<code>farseerfc</code>的信任已经恢复，我安装时并未遭遇类似情况。</p>
</div>
<h3 id="ii-硬件-一-音频安装" tabindex="-1">ii. 硬件（一）音频安装 <a class="header-anchor" href="#ii-硬件-一-音频安装" aria-label="Permalink to &quot;ii. 硬件（一）音频安装&quot;"></a></h3>
<p>音频分为固件（或者说驱动）和管理套件两部分：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 音频固件</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pacman</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -S</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> sof-firmware</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> alsa-firmware</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> alsa-ucm-conf</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># pipewire 及其音频管理套件</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pacman</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -S</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pipewire</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> gst-plugin-pipewire</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pipewire-alsa</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pipewire-jack</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pipewire-pulse</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> wireplumber</span></span></code></pre>
</div><details class="details custom-block"><summary>pulseaudio</summary>
<p>除了 pipewire 音频方案之外另有<code>pulseaudio</code>可供选择。但务必注意：音频管理套件<strong>只能二选一，不可以混装</strong>。</p>
<p>另外，由于 pipewire 本身不单只负责音频管理的工作，如需装 pulseaudio 仍需安装<code>pipewire</code> <code>gst-plugin-pipewire</code>两个包。
相应地，其余的包可换用如下平替：</p>
<ul>
<li><code>pipewire-alsa</code> → <code>pulseaudio-alsa</code></li>
<li><code>pipewire-jack</code> → <code>jack2</code></li>
<li><code>pipewire-pulse</code> → <code>pulseaudio</code></li>
<li><code>wireplumber</code> → <code>pipewire-media-session</code>（pipewire 弃用）</li>
</ul>
<blockquote>
<p>由于 pipewire 那边有 wireplumber 代替，所以这个包被他们自行标记为“过时”。<br>
但 pulseaudio 仍需要这个包。</p>
</blockquote>
</details>
<p>显卡驱动等其他硬件设施需要等装好桌面环境再考虑，在只有字符色块滚过的纯终端环境里也没有折腾显卡驱动的必要。</p>
<h3 id="iii-kde-桌面环境" tabindex="-1">iii. KDE 桌面环境 <a class="header-anchor" href="#iii-kde-桌面环境" aria-label="Permalink to &quot;iii. KDE 桌面环境&quot;"></a></h3>
<p>跟完前面的内容之后，你便拥有了一个无 GUI 的终端 Arch 系统。但作为日常使用的话，图形桌面肯定必不可少。</p>
<p>本文与那两篇参考外链一样<strong>采用 KDE 桌面环境</strong>。当然除了 KDE 之外，你也可以考虑 GNOME 桌面环境 <s>（只是我用腻了）</s>；
也可以考虑散装方案（比如<code>niri</code>，部分配置可参见 <a href="https://git.liteyuki.org/AgxCOy/aglab.dotfiles" target="_blank" rel="noreferrer">aglab.dotfiles</a>）。</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 分别安装 xorg 套件、sddm 登录管理器、KDE 桌面环境，以及配套软件</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pacman</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -S</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> xorg</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pacman</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -S</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> plasma</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> sddm</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> konsole</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> dolphin</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> kate</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> okular</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> spectacle</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> partitionmanager</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> ark</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> filelight</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> gwenview</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 启用 sddm 服务，重启进 SDDM 用户登录</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> systemctl</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> enable</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> sddm</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> reboot</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> now</span></span></code></pre>
</div><div class="info custom-block"><p class="custom-block-title">KDE 6 vs KDE 5？</p>
<p>目前最新版本为 KDE 6。但律回指南发布于 23 年 11 月，介绍的是 KDE 5。
话虽如此，倒也不必惊慌。<code>pacman</code>以及<code>yay</code> <code>paru</code>之流均<strong>默认安装最新版</strong>，上述<strong>安装 KDE 5 的步骤仍可用于安装 KDE 6</strong>。</p>
</div>
<p>重启后在用户登录界面输入密码回车，恭喜你，距离投入日常使用只剩几步之遥了。之后对 KDE
和系统的配置<strong>大部分</strong>仍可参考<a href="https://glowmem.com/archives/archlinux-note#%E5%85%AD%E6%A1%8C%E9%9D%A2%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE" target="_blank" rel="noreferrer">律回指南 Ch.6</a>。</p>
<!-- ::: note 关于 Wayland 和 X -->
<div class="note custom-block github-alert"><p class="custom-block-title">关于 Wayland 和 X</p>
<p>KDE 的图形实现默认已经是 Wayland 了。在开机后输入用户密码的界面处，找找屏幕边角，你可以看到默认选用<code>Plasma (Wayland)</code>。
点击它，你可以选择换用<code>Plasma (X11)</code>。<br>
尽管 X11 有个“锁屏黑屏”<sup class="footnote-ref"><a href="#footnote3">[3]</a><a class="footnote-anchor" id="footnote-ref3"></a></sup>的问题，但目前来说我还是推荐换回 X11。</p>
</div>
<!-- ::: -->
<h3 id="iv-硬件-二-显卡驱动与蓝牙" tabindex="-1">iv. 硬件（二）显卡驱动与蓝牙 <a class="header-anchor" href="#iv-硬件-二-显卡驱动与蓝牙" aria-label="Permalink to &quot;iv. 硬件（二）显卡驱动与蓝牙&quot;"></a></h3>
<blockquote>
<p>“so NVIDIA, F**K YOU! ”——Linus Torvalds</p>
</blockquote>
<p>AMD 或 NVIDIA 显卡可参见<a href="https://glowmem.com/archives/archlinux-note#4%E6%98%BE%E5%8D%A1%E9%A9%B1%E5%8A%A8%E5%AE%89%E8%A3%85" target="_blank" rel="noreferrer">律回指南 &sect;6.4</a>
和 <a href="https://arch.icekylin.online/guide/rookie/graphic-driver.html" target="_blank" rel="noreferrer">Miku 指南—进阶安装—显卡驱动</a>篇。
但我是锐炬核显捏，只需要在 Konsole 终端里<code>sudo pacman -S</code>安装英特尔的驱动：</p>
<ul>
<li><code>mesa</code> <code>lib32-mesa</code>（OpenGL）</li>
<li><code>vulkan-intel</code> <code>lib32-vulkan-intel</code>（Vulkan）</li>
<li><code>intel-media-driver</code>（VAAPI 解码器，OBS 需要）</li>
</ul>
<p>如果有蓝牙的话，在 Konsole 里启用（并立即启动）蓝牙服务：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> systemctl</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> enable</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --now</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> bluetooth</span></span></code></pre>
</div><blockquote>
<p>之前误以为<code>bluetooth</code>是 Arch 本身就有的服务，结果发现是桌面环境依赖了蓝牙组件包<code>bluez</code>。</p>
</blockquote>
<h3 id="v-额外中文字体和输入法" tabindex="-1">v. 额外中文字体和输入法 <a class="header-anchor" href="#v-额外中文字体和输入法" aria-label="Permalink to &quot;v. 额外中文字体和输入法&quot;"></a></h3>
<p>律回指南安装的字体分别是 Noto 系列（Linux 常用的 Unicode 字体）和思源系列（也算是 Noto 系列的子集）。
其中 Noto 系列的汉字部分由于一些神秘的原因<sup class="footnote-ref"><a href="#footnote4">[4]</a><a class="footnote-anchor" id="footnote-ref4"></a></sup>，不做额外配置的话，渲染出来只能说……能用。</p>
<!-- ::: note fontconfig -->
<div class="note custom-block github-alert"><p class="custom-block-title">fontconfig</p>
<p><code>wqy-zenhei</code><sup>extra</sup>（文泉驿）和<code>misans</code><sup>aur</sup>会在安装过程中自动帮你配置 fontconfig，因此安装完这两款字体之后系统默认用这些字体显示。
如果你希望使用未经适配的字体，那么需要在 KDE 设置里装好字体后，额外做字体配置。</p>
<p>示例：<a href="../../shared/01-Prefer.conf">思源系列字体配置</a> By <a href="https://github.com/Vescrity" target="_blank" rel="noreferrer">@Vescrity</a><br>
注意：用户级字体配置需放在<code>~/.config/fontconfig/conf.d</code>目录中。<br>
另注：使用<code>fc-cache -vf</code>刷新字体缓存。</p>
</div>
<!-- ::: -->
<p>至于输入法，现阶段推荐直接安装<code>fcitx5</code>，参见 <a href="https://arch.icekylin.online/guide/rookie/desktop-env-and-app.html#_10-%E5%AE%89%E8%A3%85%E8%BE%93%E5%85%A5%E6%B3%95" target="_blank" rel="noreferrer">Miku 指南—进阶安装—常用应用—10. 输入法</a>。</p>
<p>至此，Arch 的安装告一段落，你可以像捣腾 Windows 那样玩转 Arch 了。</p>
<hr>
<h2 id="附录-系统美化" tabindex="-1">附录：系统美化 <a class="header-anchor" href="#附录-系统美化" aria-label="Permalink to &quot;附录：系统美化&quot;"></a></h2>
<blockquote>
<p>“爱美之心，人皆有之。”</p>
</blockquote>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p></p>
<ul>
<li><strong>风格统一</strong>是美观的必要条件。</li>
<li>少搞“侵入性”美化。或者说，需要<strong>修改系统文件、注入系统进程、破坏系统稳定的美化尽量少做</strong>。</li>
<li><strong>谨遵发布页面附送的安装指引</strong>（KDE、GNOME 主题可以参考项目 GitHub），否则可能安装不全。</li>
</ul>
</div>
<h3 id="i-主题" tabindex="-1">I. 主题 <a class="header-anchor" href="#i-主题" aria-label="Permalink to &quot;I. 主题&quot;"></a></h3>
<p>主题这边我也没啥好推荐的，虽然 KDE 6 现在也出现了一些比较好看的主题，但终究是因人而异吧。</p>
<p>我想说明的是，KDE 商店的多数主题在 <strong>X11 会话、125% 甚至更高缩放率</strong>下会出现“非常粗窗口边框，使我的窗口肥胖”的现象（至少我的笔记本如此）。<br>
我个人目前是直接修改主题 Aurorae 配置文件，利用二分法逐步找到四条边的最适 Padding。
网上貌似也有“把缩放调回 100%，但是更改字体 DPI”的做法，但个人觉得显示效果应该好不到哪去（</p>
<h3 id="ii-仿-mac-上下双栏布局" tabindex="-1">II. 仿 Mac 上下双栏布局 <a class="header-anchor" href="#ii-仿-mac-上下双栏布局" aria-label="Permalink to &quot;II. 仿 Mac 上下双栏布局&quot;"></a></h3>
<p>KDE 原生的桌面 UI 就挺 Windows 的，但胜在自由度足够高。
我<strong>个人觉得</strong> Mac OS 那种双栏比较好看、比较方便，所以稍微按照如下配置调整了面板布局。</p>
<p>仅供参考咯。</p>
<details class="details custom-block"><summary>Dock 栏</summary>
<p>即原本的任务栏。</p>
<ul>
<li>位于底部、居中、适宜宽度、取消悬浮、避开窗口</li>
<li>除“图标任务管理器”外，其余组件全部移除。</li>
</ul>
</details>
<details class="details custom-block"><summary>Finder 栏</summary>
<p>即“应用程序菜单栏”（可在 <em>编辑模式—添加面板</em> 处找到）</p>
<ul>
<li>位于顶部、居中、填满宽度、取消悬浮、常驻显示</li>
<li>自左到右依次为：
<ul>
<li>应用程序启动器（类比开始菜单）</li>
<li>窗口列表</li>
<li>全局菜单（默认提供）</li>
<li>“面板间距”留白</li>
<li>数字时钟
<ul>
<li>日期保持在时间旁边，而不是上下两行</li>
<li>字号略小于菜单栏高度，凭感觉捏</li>
</ul>
</li>
<li>“面板间距”留白</li>
<li>系统监视传感器
<ul>
<li>横向柱状图（平均 CPU 温度、最高 CPU 温度）</li>
<li>仅文字（网络上行、下行速度；网络上传、下载的总流量）</li>
</ul>
</li>
<li>系统托盘</li>
</ul>
</li>
</ul>
</details>
<p>除了 Finder 栏外，可以在系统设置里更改屏幕四周的鼠标表现。
比如，鼠标移动到左上角可以自动弹出“应用程序启动器”，移到右上角可以切换你的桌面，等等。</p>
<h2 id="附录-gpg-密钥配置" tabindex="-1">附录：GPG 密钥配置 <a class="header-anchor" href="#附录-gpg-密钥配置" aria-label="Permalink to &quot;附录：GPG 密钥配置&quot;"></a></h2>
<p>主要讨论配置提交签名（Commit Signing）时遇到的问题。</p>
<h3 id="i-vscode-提交签名" tabindex="-1">I. VSCode 提交签名 <a class="header-anchor" href="#i-vscode-提交签名" aria-label="Permalink to &quot;I. VSCode 提交签名&quot;"></a></h3>
<p>大体上跟着 <a href="https://github.com/microsoft/vscode/wiki/Commit-Signing" target="_blank" rel="noreferrer">Commit Signing - VSCode Wiki</a> 就可以了。唯一需要留意的是<code>pinentry</code>。</p>
<p>VSCode 的主侧栏“源代码管理”页提交时并不会走终端，也就莫得 pinentry 的 CUI；莫得 pinentry 输密码验证，提交就签不了名。
虽然有人好像搞了个<code>pinentry-extension</code>出来，但 6 月初我去看的时候它连说明书都莫得，也没有上架，那用集贸。</p>
<p>所以我选择编辑<code>~/.gnupg/gpg-agent.conf</code>：</p>
<div class="language-properties vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">properties</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">default-cache-ttl 21600</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">pinentry-program /usr/bin/pinentry-qt</span></span></code></pre>
</div><p>保存后重启<code>gpg-agent</code>：<code>gpg-connect-agent reloadagent /bye</code>。</p>
<div class="info custom-block"><p class="custom-block-title">其他端 pinentry</p>
<p>理论上<code>pinentry</code>自身可以根据 XDG 后端选择适用的版本：</p>
<div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">ls</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> /usr/bin</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> |</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> grep</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> 'pinentry'</span></span>
<span class="line"><span style="--shiki-light:#D20F39;--shiki-light-font-style:italic;--shiki-dark:#F38BA8;--shiki-dark-font-style:italic">echo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> GETPIN</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> |</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> pinentry</span></span></code></pre>
</div><p>你可以用这两条命令看看都支持什么后端，以及它会自动选择哪个。像 niri 通常会装 gnome 后端，测试结果也的确会出<code>pinentry-gnome3</code>的密码框。</p>
</div>
<p>除此之外，<code>pinentry</code>需要指定 tty，否则找不到 IO 设备也会炸。解法：<code>export GPG_TTY=$(tty)</code>。</p>
<p>经测试，大部分终端均能在 SSH 连接中调出 CUI。若 VSCode Remote-SSH 打开的终端签不了名，检查一下是不是终端分割得太小了。</p>
<h3 id="ii-gpg-密钥备份-导出导入" tabindex="-1">II. GPG 密钥备份（导出导入） <a class="header-anchor" href="#ii-gpg-密钥备份-导出导入" aria-label="Permalink to &quot;II. GPG 密钥备份（导出导入）&quot;"></a></h3>
<p>之前并没有意识到备份 key 的重要性，结果重装 Arch 重新配置提交签名时，
我发现 GitHub 和腾讯 Coding 会重置提交验证（同一个邮箱只能上传一个公钥），届时就是我痛苦的 rebase 重签了。
<s>不过好在受影响的多数只是我的个人项目，变基无伤大雅。</s></p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">gpg</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --list-secret-keys</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --keyid-format</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> LONG</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># export</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">gpg</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -a</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -o</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> public-file.key</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --export</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> &#x3C;</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">keyi</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">d</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">></span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">gpg</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -a</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -o</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> private-file.key</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --export-secret-keys</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> &#x3C;</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">keyi</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">d</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">></span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># import</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">gpg</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --import</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> ~/public-file.key</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">gpg</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --allow-secret-key-import</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --import</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> ~/private-file.key</span></span></code></pre>
</div><p>重新导入 Key 之后，可能还需要<code>gpg --edit-key</code>更改密码（<code>passwd</code>）、重设信任（<code>trust</code>）。</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="footnote1" class="footnote-item"><p>GPT 分区表有专门的“EFI System”分区类型（即 ESP），当然新款的主板也会<em>连带扫描 FAT 分区</em>；对于 MBR 分区表，则扫描<strong>活动</strong>的 FAT 分区。参见 <a href="https://www.cnblogs.com/mahocon/p/5691348.html" target="_blank" rel="noreferrer">（译）UEFI 启动的实际工作原理</a>。 <a href="#footnote-ref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote2" class="footnote-item"><p>ESP 装载<code>/boot</code>系经典分区法。后来有说法称“直接暴露 Linux 内核并不安全”，所以又有将 ESP 装入<code>/boot/efi</code>的分法（Ubuntu Server 24.04.2 即如此）。现在（至少在 Arch 里）则推荐直接挂载到<code>/efi</code>。 <a href="#footnote-ref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote3" class="footnote-item"><p>KDE 的默认 Breeze 主题锁屏时大概率会出现黑屏、惟有鼠标的现象。在 7 月中旬时已经发现该现象已经蔓延到自定义主题了。查了下 Google 以及 Arch、Manjaro、KDE 的一些讨论帖，尚没有有效的解决方案。<br>
当然有一些主题可能能够解除这个“病征”，像 Nordic Dark 以及 Lavanda。但这种 work around 可能还是因人而异。 <a href="#footnote-ref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote4" class="footnote-item"><p>参见 <a href="https://wiki.archlinux.org/title/Localization/Simplified_Chinese#Chinese_characters_displayed_as_variant_(Japanese)_glyphs" target="_blank" rel="noreferrer">Arch Wiki：关于中文字被异常渲染成日文异体字的说明</a><sup>2</sup>以及 <a href="https://bbs.archlinuxcn.org/viewtopic.php?pid=60100" target="_blank" rel="noreferrer">Arch 中文论坛：noto-fonts-cjk 打包变化可能导致的回落字体选取问题</a><sup>2</sup>。 <a href="#footnote-ref4" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Linux 中游玩《星辰之光》]]></title>
            <link>https://agxcoy.shimakaze.org/posts/extreme-starry-linux</link>
            <guid>https://agxcoy.shimakaze.org/posts/extreme-starry-linux</guid>
            <pubDate>Wed, 13 May 2026 12:36:50 GMT</pubDate>
            <description><![CDATA[
因为一些机缘巧合，现在我改用 Linux 作为主力系统了。然鹅地图我还是得做的，我又没有两台电脑搞远程，那如何在 Linux 里游玩红红、制作地图首先就成了问题。

当然有人马上会想到虚拟机，比如熟悉的 VirtualBox、VMWare，比如`winapps`。但虚拟机也好，双系统也罢，都太重量级了，我个人并不喜欢。

那都玩 Linux 了，Wine 兼容层怎么样呢？不也有过 Wine 跑《原神》的成功案例了嘛。但遗憾的是，自 9.17-1 版本开始，**原生 Wine** 的 DDraw 兼容都做得十分甚至九分的抽象，无论是红警 2 本体还是 FA2 地图编辑器都无法正常显示，原生 Wine 也就不再适用了。

> 现阶段也不再建议用 Wine 玩原神，容易被封号。可以试试云原神，但首先需要解决“鼠标无法转动视角”的问题。

所以，兜兜转转，还是回到了 Bottles。

<!-- ::: important -->
> [!important]
> 本篇笔记仅以《星辰之光》这个红警 2 模组作为范例，因为它是我这里最早成功跑起来的红警 2 mod。
> 对于其他 mod，乃至其他游戏和 Windows 程序，本篇笔记的方案可能有一定参考价值，**但不保证能够成功运行**。
<!-- ::: -->

那么正式开始之前，我有必要先说一下我的 Linux 环境。由于 Linux 发行版众多，我**无法保证别的包源、别的发行版能否这么操作**。

- 操作系统：Arch Linux
- 桌面环境：KDE 6

## 一、Bottles

Bottles 是由 [bottlesdevs](https://github.com/bottlesdevs) 开发的可视化 Wine 配置工具，旨在“让用户便利地在喜欢的发行版里运行 Windows 程序”。

> 参考链接：[GitHub](https://github.com/bottlesdevs/Bottles) [官网](https://usebottles.com/)

官方推荐通过 Flatpak 安装，在沙箱里运行。但由于“懒加载”[^lazy_loading]问题，游戏本体和地编都无法正常启动。因此还是改用`pacman`吧。

[^lazy_loading]: 经实测发现，单文件 exe 才可以在这种情况下直接在 Bottles 里启动。但凡需要读同级文件、子文件夹的，都需要在 Bottle 里添加快捷方式，并在快捷方式的设置里手动指明工作目录。

首先需要引入`archlinuxcn`源。具体步骤参见[《Arch 安装流程》](./arch-install.md#i-cn-源和-aur-助手)，这里不再重复。  
接着`sudo pacman -Sy bottles-bwrap`安装。等待进度跑完，就可以从“应用程序菜单栏”运行了。

初次运行 Bottles 会弹出一个向导跟你 blabla，无脑下一步即可。
到最后一步时 Bottles 会下载一些组件包。由于众所周知的原因，可能会花费比较长的时间。

![Bottles 主界面 =350x](.img/es_linux-bottles_main.webp)

## 二、部署 Bottle

### 2.1 新建
进入 Bottles 的主界面，点击“新建 Bottle……”（或者窗口左上角的加号），填些基本信息：

> [!note]
> 我的系统语言是英文，中文翻译仅供参考。

- 名称`Name`：自拟（为便于说明，后面用`$venv`表示）；
- 环境`Environment`：建议选自定义`Custom`。

> 应用程序`Application`和游戏`Gaming`这两个预设，初次新建 Bottle 时会下载巨量的依赖。
> 如果你网不是特别好，也没走代理，直接“自定义”就可以了。

- 打开共享用户目录`Share User Directory`选项
- 兼容层，或者说运行器`Runner`选择 soda-9.0-1（以最新版为准）

> 如果你选了预设，这里是改不了兼容层的，得等创建好 Bottle 之后进设置再改。

> 此外，cn 源的 Bottles 使用系统中装的原生 Wine。在文章开头我就强调过原生 Wine 已不适用红红。所以务必换用别的。

- Bottle 目录`Bottle Directory`可改可不改（为便于说明，后面用`$bottles`表示）。

> 默认你的环境位于`~/.local/share/bottles/bottles`目录下。

> [!warning]
> 如果你在全局设置里改过默认目录，千万不要在新建这里又改到同一个位置，否则会报**符号占用，创建失败**。

![新建 Bottle =350x](.img/es_linux-bottles_new_venv.webp)

然后在右上角点击“创建”即可。

> [!note]
> 在 Linux 中，`~`和`$HOME`通常指代`/home/<user_id>` [^home_dir]，比如`/home/nyacl`。
> 类比下 Win7 的`%UserProfile%`和`C:\Users\nyacl`就知道了。
> 
> [^home_dir]: Linux 的路径是**区分大小写**的，终端里的环境变量（通常全大写）也是。  
> 即`YURI.exe` &ne; `yuri.EXE`；`$HOME` &ne; `$home`。

### 2.2 Bottle 选项

点击刚建好的 Bottle 进入详情页，点开设置`Settings`：

1. 需要开启 DirectX 翻译——将组件`Components`部分的 DXVK 和 VKD3D 打开；
2. 可以考虑在显示`Display`部分启用独立显卡`Discrete Graphics`（我的笔记本没有捏）；
3. 性能`Performance`部分的同步`Synchronization`可以考虑 Fsync，除此之外的选项建议不动；

做完设置，退回上一页把依赖`Dependencies`装上：

::: tip 红警 2 推荐依赖

- 客户端需要：`mono` (Wine mono .NET 依赖) （耗时较长，建议最后安装）
- 中日韩字体：`cjkfonts`（避免“口口文学”）

> 你也可以手动下载（或复制 C:\Windows\Fonts 里的）msyh.ttc 和 simsun.ttc，
> 并复制到`$bottles/$venv/drive_c/windows/Fonts`里。

- 游戏本体需要：`cnc-ddraw`
- Reshade 特效层需要：`d3dcompiler_*.dll` `d3dx*`

> 这里的 * 代表全都要，比如 d3dx11 和 d3dx9。
<!-- - gdiplus -->
:::

## 三、部署游戏环境

下载好《星辰之光》大版本包体（如有必要，额外再下载小更新包），用`unzip`解压：
```bash
sudo pacman -S unzip
# 请根据实际情况替换压缩包路径
unzip -O GBK -o '~/Documents/Extreme Starry v0.6.zip'
# 如果网络不好，不方便更新，并且群里恰有离线更新包，也可以直接下载、覆盖更新
unzip -O GBK -o '~/Documents/0.6.2 离线更新包.zip' -d '~/Documents/Extreme Starry'
```

::: details unzip 命令行解释
`unzip [opt] </path/to/zip> [-d extract_dir]`

- `-O encoding`：指定在 Windows 里打包的 ZIP 采用什么编码打开。
- `-o`（注意大小写不一样）：有相同文件名的，一律覆盖。
- `/path/to/zip`：zip 路径。
> 遇到空格需要加反斜杠转义，或者像我那样直接打引号。
- `-d extract_dir`：解压到单独的文件夹。
> 像上面离线包直接解压出来是散装跟`Extreme Starry`并列放的。而`~/Documents`可能不止放《星辰之光》。

更多细节还请自行`unzip -h`。虽然解说都是英文。
:::

然后点开你的 Bottle 进入详情页，为客户端`Extreme Starry.exe`添加快捷方式，这样就不需要每次都点“运行可执行程序”找半天了。  

![Bottle 详情 =350x](.img/es_linux-bottle_pref.webp)

> [!tip]
> 在“选择可执行文件”对话框中，若找不到 exe，请在“过滤”那里改为`Supported Executables`。

## 四、渲染补丁
我们知道，红警 2 是个 Windows 游戏，但众所周知，由于系统调用的不同，Windows 程序无法直接在 Linux 上跑，这点对于“渲染补丁”也是一样。
所以客户端设置也好，玩家自备`ddraw.dll`也罢，**均无法在 Wine 里使用**。

### 4.1 游戏本体
可能你会有疑问：前面不是让装`cnc-ddraw`了吗？怎么又有问题捏？因为文中的 Bottles 以及用于原生 Wine 的 Winetricks 均只提供这个。换言之，你基本上**只有`cnc-ddraw`类补丁可以选**。

除此之外，Bottle 容器与 Windows 类似，**默认从游戏目录（即“内建`Builtin`”）加载 DLL**。所以，还需要调整`ddraw.dll`加载次序。  
- 找到 Bottle 详情的“工具”一栏；
- 点开`Legacy Wine Tools`找到`Configuration`，打开`winecfg`。
- 选中函数库`Libraries`页面，在列表中选中`ddraw`，点击编辑`Edit`；  
  若找不到，**先**在上面的输入框里手打`ddraw.dll`，点击添加`Add`。
- 在弹出的 5 个选项中，选择**原装`Native` (Windows)**。

而对于 Reshade，国内有一些 Reshade 会伪装成`d3d*.dll`。由于上面提到的默认规则，这种 Reshade 实际仍能配合`ddraw.dll`运作，在游戏中显示出 Reshade 版本提示。当然具体特效显示成什么样就未经细致测试了。

::: details Wine 的 DLL 查找
经查证，前面说的`soda`、`proton`均为 Wine 的变种。所以只需讨论 Wine 的做法即可。  
总的来说，Wine 的查找与 Windows 的 KnownDlls 机制类似，但做了简化[^wine_forum_dll]：

- 内建（Builtin）：（默认优先）在程序的**当前目录**（或者叫**工作目录**，在本文中又称**游戏目录**）下查找、加载。
- 原装（Native）：（默认备选）在 Wine 容器（即`$venv`虚拟 C 盘的`System32`，可能还有`SysWOW64`）中查找。

[^wine_forum_dll]: 参见帖子 _[Wine can't find/load DLLs in the same dir](https://forum.winehq.org/viewtopic.php?t=36023)_。
:::

### 4.2 FA2 及其扩展（FA2sp 等）
开篇提到，我还有做地图的需求。

目前圈子里所谓“FA2 防卡补丁”实际是 DxWnd，它仍会加载系统目录的`ddraw.dll`。那么对本随记而言，便只需讨论“原装”的 DDraw。经过测试，刚建好的 Wine 环境其`ddraw.dll`恰可以为 FA2 所用。

> 原生 9.16-1 那版对我来说刚好，但是无视缩放比；  
> 9.17-1 及往后的新版本则会因屏幕缩放有一些拉扯感，不知高分屏用户觉得如何。  
> Proton 等 Wine 改版的表现与 9.16-1 一致。推测是并未跟进最新版本。

那需要做的就很简单了：**另起一个 Bottle 跑地编**。或者，在`cnc-ddraw`安装之前先提取出`$venv/drive_c/windows/System32`（也可能是`SysWOW64`，如果有的话）里面的`ddraw.dll`，**覆盖 DxWnd**。

## 五、开玩

在做完全部配置之后，点击你建过的快捷方式右边的`▶`图标，开耍。……虽然，读条可能会比较慢。

![在 Arch 里游玩《星辰之光》（图为尚处内测的萌 03）](.img/es_linux-preview.webp)

::: info 再次启动客户端没有反应
可能是因为进程还驻留在 Wine 环境当中，需要“强制停止所有进程”手动干掉：

![位于详情页标题栏的“电源”图示 =128x](.img/es_linux-bottle_kill_proc.webp)
:::

## 附录：关于 Syringe 命令行
> [!tip]
> 像《星辰之光》这种有独立客户端的 mod 无需查阅此附录，客户端本身就负责了命令行解析。

Linux 的文件名允许英文引号（如`"game"md.exe`），在终端里，这会给 Syringe 带来歧义：
```log
Syringe.exe "\"gamemd.exe\"" -SPAWN ...
```
解法也很简单，把它扔进批处理即可：
```cmd
PUSHD %~dp0
Syringe.exe "gamemd.exe" -SPAWN -log -cd -speedcontrol
```
然后把批处理扔进游戏目录（或者说和`gamemd.exe`放在一起），让 Wine 去启动批处理即可：
```bash
# wine 运行时会把 Linux 根目录挂载到 Z 盘。
wine cmd /c "Z:/home/agxcoy/Documents/ES-FA2/launch.cmd"
```
]]></description>
            <content:encoded><![CDATA[<p>因为一些机缘巧合，现在我改用 Linux 作为主力系统了。然鹅地图我还是得做的，我又没有两台电脑搞远程，那如何在 Linux 里游玩红红、制作地图首先就成了问题。</p>
<p>当然有人马上会想到虚拟机，比如熟悉的 VirtualBox、VMWare，比如<code>winapps</code>。但虚拟机也好，双系统也罢，都太重量级了，我个人并不喜欢。</p>
<p>那都玩 Linux 了，Wine 兼容层怎么样呢？不也有过 Wine 跑《原神》的成功案例了嘛。但遗憾的是，自 9.17-1 版本开始，<strong>原生 Wine</strong> 的 DDraw 兼容都做得十分甚至九分的抽象，无论是红警 2 本体还是 FA2 地图编辑器都无法正常显示，原生 Wine 也就不再适用了。</p>
<blockquote>
<p>现阶段也不再建议用 Wine 玩原神，容易被封号。可以试试云原神，但首先需要解决“鼠标无法转动视角”的问题。</p>
</blockquote>
<p>所以，兜兜转转，还是回到了 Bottles。</p>
<!-- ::: important -->
<div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p>
<p>本篇笔记仅以《星辰之光》这个红警 2 模组作为范例，因为它是我这里最早成功跑起来的红警 2 mod。
对于其他 mod，乃至其他游戏和 Windows 程序，本篇笔记的方案可能有一定参考价值，<strong>但不保证能够成功运行</strong>。</p>
</div>
<!-- ::: -->
<p>那么正式开始之前，我有必要先说一下我的 Linux 环境。由于 Linux 发行版众多，我<strong>无法保证别的包源、别的发行版能否这么操作</strong>。</p>
<ul>
<li>操作系统：Arch Linux</li>
<li>桌面环境：KDE 6</li>
</ul>
<h2 id="一、bottles" tabindex="-1">一、Bottles <a class="header-anchor" href="#一、bottles" aria-label="Permalink to &quot;一、Bottles&quot;"></a></h2>
<p>Bottles 是由 <a href="https://github.com/bottlesdevs" target="_blank" rel="noreferrer">bottlesdevs</a> 开发的可视化 Wine 配置工具，旨在“让用户便利地在喜欢的发行版里运行 Windows 程序”。</p>
<blockquote>
<p>参考链接：<a href="https://github.com/bottlesdevs/Bottles" target="_blank" rel="noreferrer">GitHub</a> <a href="https://usebottles.com/" target="_blank" rel="noreferrer">官网</a></p>
</blockquote>
<p>官方推荐通过 Flatpak 安装，在沙箱里运行。但由于“懒加载”<sup class="footnote-ref"><a href="#footnote1">[1]</a><a class="footnote-anchor" id="footnote-ref1"></a></sup>问题，游戏本体和地编都无法正常启动。因此还是改用<code>pacman</code>吧。</p>
<p>首先需要引入<code>archlinuxcn</code>源。具体步骤参见<a href="./arch-install#i-cn-源和-aur-助手">《Arch 安装流程》</a>，这里不再重复。<br>
接着<code>sudo pacman -Sy bottles-bwrap</code>安装。等待进度跑完，就可以从“应用程序菜单栏”运行了。</p>
<p>初次运行 Bottles 会弹出一个向导跟你 blabla，无脑下一步即可。
到最后一步时 Bottles 会下载一些组件包。由于众所周知的原因，可能会花费比较长的时间。</p>
<p><img src="https://agxcoy.shimakaze.org/assets/es_linux-bottles_main.DQsJgB38.webp" alt="Bottles 主界面" width="350" loading="lazy"></p>
<h2 id="二、部署-bottle" tabindex="-1">二、部署 Bottle <a class="header-anchor" href="#二、部署-bottle" aria-label="Permalink to &quot;二、部署 Bottle&quot;"></a></h2>
<h3 id="_2-1-新建" tabindex="-1">2.1 新建 <a class="header-anchor" href="#_2-1-新建" aria-label="Permalink to &quot;2.1 新建&quot;"></a></h3>
<p>进入 Bottles 的主界面，点击“新建 Bottle……”（或者窗口左上角的加号），填些基本信息：</p>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>我的系统语言是英文，中文翻译仅供参考。</p>
</div>
<ul>
<li>名称<code>Name</code>：自拟（为便于说明，后面用<code>$venv</code>表示）；</li>
<li>环境<code>Environment</code>：建议选自定义<code>Custom</code>。</li>
</ul>
<blockquote>
<p>应用程序<code>Application</code>和游戏<code>Gaming</code>这两个预设，初次新建 Bottle 时会下载巨量的依赖。
如果你网不是特别好，也没走代理，直接“自定义”就可以了。</p>
</blockquote>
<ul>
<li>打开共享用户目录<code>Share User Directory</code>选项</li>
<li>兼容层，或者说运行器<code>Runner</code>选择 soda-9.0-1（以最新版为准）</li>
</ul>
<blockquote>
<p>如果你选了预设，这里是改不了兼容层的，得等创建好 Bottle 之后进设置再改。</p>
</blockquote>
<blockquote>
<p>此外，cn 源的 Bottles 使用系统中装的原生 Wine。在文章开头我就强调过原生 Wine 已不适用红红。所以务必换用别的。</p>
</blockquote>
<ul>
<li>Bottle 目录<code>Bottle Directory</code>可改可不改（为便于说明，后面用<code>$bottles</code>表示）。</li>
</ul>
<blockquote>
<p>默认你的环境位于<code>~/.local/share/bottles/bottles</code>目录下。</p>
</blockquote>
<div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p>
<p>如果你在全局设置里改过默认目录，千万不要在新建这里又改到同一个位置，否则会报<strong>符号占用，创建失败</strong>。</p>
</div>
<p><img src="https://agxcoy.shimakaze.org/assets/es_linux-bottles_new_venv.Fo3s9gYU.webp" alt="新建 Bottle" width="350" loading="lazy"></p>
<p>然后在右上角点击“创建”即可。</p>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>在 Linux 中，<code>~</code>和<code>$HOME</code>通常指代<code>/home/&lt;user_id&gt;</code> <sup class="footnote-ref"><a href="#footnote2">[2]</a><a class="footnote-anchor" id="footnote-ref2"></a></sup>，比如<code>/home/nyacl</code>。
类比下 Win7 的<code>%UserProfile%</code>和<code>C:\Users\nyacl</code>就知道了。</p>
</div>
<h3 id="_2-2-bottle-选项" tabindex="-1">2.2 Bottle 选项 <a class="header-anchor" href="#_2-2-bottle-选项" aria-label="Permalink to &quot;2.2 Bottle 选项&quot;"></a></h3>
<p>点击刚建好的 Bottle 进入详情页，点开设置<code>Settings</code>：</p>
<ol>
<li>需要开启 DirectX 翻译——将组件<code>Components</code>部分的 DXVK 和 VKD3D 打开；</li>
<li>可以考虑在显示<code>Display</code>部分启用独立显卡<code>Discrete Graphics</code>（我的笔记本没有捏）；</li>
<li>性能<code>Performance</code>部分的同步<code>Synchronization</code>可以考虑 Fsync，除此之外的选项建议不动；</li>
</ol>
<p>做完设置，退回上一页把依赖<code>Dependencies</code>装上：</p>
<div class="tip custom-block"><p class="custom-block-title">红警 2 推荐依赖</p>
<ul>
<li>客户端需要：<code>mono</code> (Wine mono .NET 依赖) （耗时较长，建议最后安装）</li>
<li>中日韩字体：<code>cjkfonts</code>（避免“口口文学”）</li>
</ul>
<blockquote>
<p>你也可以手动下载（或复制 C:\Windows\Fonts 里的）msyh.ttc 和 simsun.ttc，
并复制到<code>$bottles/$venv/drive_c/windows/Fonts</code>里。</p>
</blockquote>
<ul>
<li>游戏本体需要：<code>cnc-ddraw</code></li>
<li>Reshade 特效层需要：<code>d3dcompiler_*.dll</code> <code>d3dx*</code></li>
</ul>
<blockquote>
<p>这里的 * 代表全都要，比如 d3dx11 和 d3dx9。</p>
</blockquote>
<!-- - gdiplus -->
</div>
<h2 id="三、部署游戏环境" tabindex="-1">三、部署游戏环境 <a class="header-anchor" href="#三、部署游戏环境" aria-label="Permalink to &quot;三、部署游戏环境&quot;"></a></h2>
<p>下载好《星辰之光》大版本包体（如有必要，额外再下载小更新包），用<code>unzip</code>解压：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pacman</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -S</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> unzip</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 请根据实际情况替换压缩包路径</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">unzip</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -O</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> GBK</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -o</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> '~/Documents/Extreme Starry v0.6.zip'</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 如果网络不好，不方便更新，并且群里恰有离线更新包，也可以直接下载、覆盖更新</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">unzip</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -O</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> GBK</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -o</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> '~/Documents/0.6.2 离线更新包.zip'</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -d</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> '~/Documents/Extreme Starry'</span></span></code></pre>
</div><details class="details custom-block"><summary>unzip 命令行解释</summary>
<p><code>unzip [opt] &lt;/path/to/zip&gt; [-d extract_dir]</code></p>
<ul>
<li><code>-O encoding</code>：指定在 Windows 里打包的 ZIP 采用什么编码打开。</li>
<li><code>-o</code>（注意大小写不一样）：有相同文件名的，一律覆盖。</li>
<li><code>/path/to/zip</code>：zip 路径。</li>
</ul>
<blockquote>
<p>遇到空格需要加反斜杠转义，或者像我那样直接打引号。</p>
</blockquote>
<ul>
<li><code>-d extract_dir</code>：解压到单独的文件夹。</li>
</ul>
<blockquote>
<p>像上面离线包直接解压出来是散装跟<code>Extreme Starry</code>并列放的。而<code>~/Documents</code>可能不止放《星辰之光》。</p>
</blockquote>
<p>更多细节还请自行<code>unzip -h</code>。虽然解说都是英文。</p>
</details>
<p>然后点开你的 Bottle 进入详情页，为客户端<code>Extreme Starry.exe</code>添加快捷方式，这样就不需要每次都点“运行可执行程序”找半天了。</p>
<p><img src="https://agxcoy.shimakaze.org/assets/es_linux-bottle_pref.BDK3peib.webp" alt="Bottle 详情" width="350" loading="lazy"></p>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>在“选择可执行文件”对话框中，若找不到 exe，请在“过滤”那里改为<code>Supported Executables</code>。</p>
</div>
<h2 id="四、渲染补丁" tabindex="-1">四、渲染补丁 <a class="header-anchor" href="#四、渲染补丁" aria-label="Permalink to &quot;四、渲染补丁&quot;"></a></h2>
<p>我们知道，红警 2 是个 Windows 游戏，但众所周知，由于系统调用的不同，Windows 程序无法直接在 Linux 上跑，这点对于“渲染补丁”也是一样。
所以客户端设置也好，玩家自备<code>ddraw.dll</code>也罢，<strong>均无法在 Wine 里使用</strong>。</p>
<h3 id="_4-1-游戏本体" tabindex="-1">4.1 游戏本体 <a class="header-anchor" href="#_4-1-游戏本体" aria-label="Permalink to &quot;4.1 游戏本体&quot;"></a></h3>
<p>可能你会有疑问：前面不是让装<code>cnc-ddraw</code>了吗？怎么又有问题捏？因为文中的 Bottles 以及用于原生 Wine 的 Winetricks 均只提供这个。换言之，你基本上<strong>只有<code>cnc-ddraw</code>类补丁可以选</strong>。</p>
<p>除此之外，Bottle 容器与 Windows 类似，<strong>默认从游戏目录（即“内建<code>Builtin</code>”）加载 DLL</strong>。所以，还需要调整<code>ddraw.dll</code>加载次序。</p>
<ul>
<li>找到 Bottle 详情的“工具”一栏；</li>
<li>点开<code>Legacy Wine Tools</code>找到<code>Configuration</code>，打开<code>winecfg</code>。</li>
<li>选中函数库<code>Libraries</code>页面，在列表中选中<code>ddraw</code>，点击编辑<code>Edit</code>；<br>
若找不到，<strong>先</strong>在上面的输入框里手打<code>ddraw.dll</code>，点击添加<code>Add</code>。</li>
<li>在弹出的 5 个选项中，选择<strong>原装<code>Native</code> (Windows)</strong>。</li>
</ul>
<p>而对于 Reshade，国内有一些 Reshade 会伪装成<code>d3d*.dll</code>。由于上面提到的默认规则，这种 Reshade 实际仍能配合<code>ddraw.dll</code>运作，在游戏中显示出 Reshade 版本提示。当然具体特效显示成什么样就未经细致测试了。</p>
<details class="details custom-block"><summary>Wine 的 DLL 查找</summary>
<p>经查证，前面说的<code>soda</code>、<code>proton</code>均为 Wine 的变种。所以只需讨论 Wine 的做法即可。<br>
总的来说，Wine 的查找与 Windows 的 KnownDlls 机制类似，但做了简化<sup class="footnote-ref"><a href="#footnote3">[3]</a><a class="footnote-anchor" id="footnote-ref3"></a></sup>：</p>
<ul>
<li>内建（Builtin）：（默认优先）在程序的<strong>当前目录</strong>（或者叫<strong>工作目录</strong>，在本文中又称<strong>游戏目录</strong>）下查找、加载。</li>
<li>原装（Native）：（默认备选）在 Wine 容器（即<code>$venv</code>虚拟 C 盘的<code>System32</code>，可能还有<code>SysWOW64</code>）中查找。</li>
</ul>
</details>
<h3 id="_4-2-fa2-及其扩展-fa2sp-等" tabindex="-1">4.2 FA2 及其扩展（FA2sp 等） <a class="header-anchor" href="#_4-2-fa2-及其扩展-fa2sp-等" aria-label="Permalink to &quot;4.2 FA2 及其扩展（FA2sp 等）&quot;"></a></h3>
<p>开篇提到，我还有做地图的需求。</p>
<p>目前圈子里所谓“FA2 防卡补丁”实际是 DxWnd，它仍会加载系统目录的<code>ddraw.dll</code>。那么对本随记而言，便只需讨论“原装”的 DDraw。经过测试，刚建好的 Wine 环境其<code>ddraw.dll</code>恰可以为 FA2 所用。</p>
<blockquote>
<p>原生 9.16-1 那版对我来说刚好，但是无视缩放比；<br>
9.17-1 及往后的新版本则会因屏幕缩放有一些拉扯感，不知高分屏用户觉得如何。<br>
Proton 等 Wine 改版的表现与 9.16-1 一致。推测是并未跟进最新版本。</p>
</blockquote>
<p>那需要做的就很简单了：<strong>另起一个 Bottle 跑地编</strong>。或者，在<code>cnc-ddraw</code>安装之前先提取出<code>$venv/drive_c/windows/System32</code>（也可能是<code>SysWOW64</code>，如果有的话）里面的<code>ddraw.dll</code>，<strong>覆盖 DxWnd</strong>。</p>
<h2 id="五、开玩" tabindex="-1">五、开玩 <a class="header-anchor" href="#五、开玩" aria-label="Permalink to &quot;五、开玩&quot;"></a></h2>
<p>在做完全部配置之后，点击你建过的快捷方式右边的<code>▶</code>图标，开耍。……虽然，读条可能会比较慢。</p>
<p><img src="https://agxcoy.shimakaze.org/assets/es_linux-preview.4nk7OPDF.webp" alt="在 Arch 里游玩《星辰之光》（图为尚处内测的萌 03）" loading="lazy"></p>
<div class="info custom-block"><p class="custom-block-title">再次启动客户端没有反应</p>
<p>可能是因为进程还驻留在 Wine 环境当中，需要“强制停止所有进程”手动干掉：</p>
<p><img src="./.img/es_linux-bottle_kill_proc.webp" alt="位于详情页标题栏的“电源”图示" width="128" loading="lazy"></p>
</div>
<h2 id="附录-关于-syringe-命令行" tabindex="-1">附录：关于 Syringe 命令行 <a class="header-anchor" href="#附录-关于-syringe-命令行" aria-label="Permalink to &quot;附录：关于 Syringe 命令行&quot;"></a></h2>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>像《星辰之光》这种有独立客户端的 mod 无需查阅此附录，客户端本身就负责了命令行解析。</p>
</div>
<p>Linux 的文件名允许英文引号（如<code>&quot;game&quot;md.exe</code>），在终端里，这会给 Syringe 带来歧义：</p>
<div class="language-log vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">log</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D20F39;--shiki-dark:#F38BA8">Syringe.exe</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "\"</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">gamemd.exe\</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">""</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> -SPAWN ...</span></span></code></pre>
</div><p>解法也很简单，把它扔进批处理即可：</p>
<div class="language-cmd vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">cmd</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">PUSHD </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">%~</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">dp0</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">Syringe.exe </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"gamemd.exe"</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> -</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">SPAWN </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">-</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">log</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> -</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">cd </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">-</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">speedcontrol</span></span></code></pre>
</div><p>然后把批处理扔进游戏目录（或者说和<code>gamemd.exe</code>放在一起），让 Wine 去启动批处理即可：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># wine 运行时会把 Linux 根目录挂载到 Z 盘。</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">wine</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> cmd</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> /c</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "Z:/home/agxcoy/Documents/ES-FA2/launch.cmd"</span></span></code></pre>
</div><hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="footnote1" class="footnote-item"><p>经实测发现，单文件 exe 才可以在这种情况下直接在 Bottles 里启动。但凡需要读同级文件、子文件夹的，都需要在 Bottle 里添加快捷方式，并在快捷方式的设置里手动指明工作目录。 <a href="#footnote-ref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote2" class="footnote-item"><p>Linux 的路径是<strong>区分大小写</strong>的，终端里的环境变量（通常全大写）也是。<br>
即<code>YURI.exe</code> &ne; <code>yuri.EXE</code>；<code>$HOME</code> &ne; <code>$home</code>。 <a href="#footnote-ref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote3" class="footnote-item"><p>参见帖子 <em><a href="https://forum.winehq.org/viewtopic.php?t=36023" target="_blank" rel="noreferrer">Wine can't find/load DLLs in the same dir</a></em>。 <a href="#footnote-ref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
            <enclosure url="https://agxcoy.shimakaze.org/assets/es_linux-bottles_main.DQsJgB38.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Arch Linux UEFI 启动二三事]]></title>
            <link>https://agxcoy.shimakaze.org/posts/arch-uefi</link>
            <guid>https://agxcoy.shimakaze.org/posts/arch-uefi</guid>
            <pubDate>Sun, 10 May 2026 13:45:46 GMT</pubDate>
            <description><![CDATA[
由于我对 Linux 乃至整个 UEFI 的启动机制尚且“浅尝辄止”，本文并不会展开很多硬核内容，只是对我个人使用过的启动方案做个总结。

::: tip 引导和启动
在维基百科中二者似乎是同一概念[^wikipedia_booting]，搜索“启动程序”会跳转到[“引导程序”](https://zh.wikipedia.org/wiki/%E5%95%9F%E5%8B%95%E7%A8%8B%E5%BC%8F)的介绍。

[^wikipedia_booting]: 另见英文维基：[Booting](https://en.wikipedia.org/wiki/Booting)。

国内很多折腾 WinPE 的人（包括我）对此也并没有很明确的区分；当然有些博客则对开机装载 Linux 的过程拆分成引导、启动两个阶段。本文为了方便起见，用词不作区分。
:::

在此感谢岛风 [@frg2089](https://github.com/frg2089) 指路。

## UEFI 启动简述：启动项管理

> UEFI 规范定义了名为“UEFI 启动管理器”的一项功能……（它）是一种固件策略引擎，可通过修改固件架构中定义的全局 NVRAM 变量来进行配置。启动管理器将尝试按全局 NVRAM 变量定义的顺序依次加载 UEFI 驱动和 UEFI 应用程序（包括 UEFI 操作系统启动装载程序）。……
> ::: right
> ——[（译）UEFI 启动：实际工作原理](https://www.cnblogs.com/mahocon/p/5691348.html)
> :::

本“议题”只讨论 UEFI 原生启动项和回退路径启动项。恕不对 BIOS 兼容的部分作详细展开。

### i. 原生启动项
用 Windows 7 及更高版本系统的朋友肯定知道这个东西：Windows Boot Manager。`bootmgr`它代替了`ntldr`，从此便沿用至今。

事实上，Windows Boot Manager 是系统安装完成后，初次加载系统时为其创建的**原生启动项**。它明确指出需要启动**指定设备中**的**指定引导文件**（即`bootmgfw.efi`）。

即便 WinToGo 也是如此——在以 U 盘身份进入 WTG 系统时，Windows 也会悄悄地把原生启动项建立好。然后重启之后再按快捷键进入启动菜单，你**可能**会在**部分主板上**发现有两个启动项，指向同一个设备：
```
Windows Boot Manager ( Koi Series Pro ...)
USB HDD: Koi Series Pro ...
```
需要注意的是，原生启动项是**存储在主板里的**（更准确的说，是全局 NVRAM 变量）。有些主板在检测到原生启动项失效（找不到指定引导文件）后，会自行删除该启动项。比如安装 Grub 后更换过硬盘，后来又把原盘插回去，可能仍然找不到 Grub 启动项。

### ii. 回退路径启动项
对于 WinPE、Windows 安装镜像而言，它们并非用于长线运行，不可能到处添加原生启动项，那么 UEFI 如何认出它们捏？
还记得上面提到的同一设备双启动项吗？UEFI 固件是能够找到可启动设备，并且尝试启动的。但它是依据什么去找的捏？

UEFI 固件首先会**遍历各硬盘的 ESP 分区**，并在其中查找`\EFI\BOOT\boot{cpu_arch}.efi`。前面的这一固定路径就称为**回退路径**，通过查找回退路径建立的启动项就称作**回退路径启动项**。其中，`cpu_arch`即 CPU 架构，已知的有：
- `x64`：x86-64
- `ia32`：x86-32
- `ia64`：Itanium
- `arm`：AArch32，即 arm32 ~~（胳膊 32）~~
- `aa64`：AArch64，即 arm64 ~~（64 条胳膊）~~

> [!note]
> UEFI 的路径系统与 Windows 类似：以`\`分隔，不区分大小写。

如果同一硬盘、同一 CPU 架构存在多个匹配的 EFI 文件（比如，可能有两个 ESP 分区，分开装不同系统的 EFI），那么**只会选第一个有效的**去执行。

对于 WinPE U 盘，通常它们是 MBR 分区表，那么会考虑更泛用的搜索：采用 **FAT** 文件系统的**活动分区**；
对于 GPT 分区表，可以直接搜索 ESP 分区。当然如今的主板并不会卡那么死，哪怕只是普通的 FAT 分区，也会尝试搜索、执行。

也就是说，哪怕原生启动项意外被固件扬了，只要还有回退启动项，便仍可从同一个硬盘启动系统。

::: info
实际上`bcdboot`工具会在 ESP 分区里同时写入`bootx64.efi`和`bootmgfw.efi`。前者即回退路径启动项。

有关`bootx64.efi`、`bootmgr(.efi)`和`bootmgfw.efi`的关系可能有些复杂，谷歌了一圈各种观点都有。本着实事求是的原则，我不会糅合这些观点提出假设，只附上几个问题：
- `bootx64.efi`、`bootmgfw.efi`（或`bootmgr`）分别在本机 Windows、WinToGo 和 WinPE 中起到什么作用？三者之间是否存在等价（即功能上可以替代，乃至文件哈希相同）？
- fwbootmgr（即`bootmgfw.efi`）与 bootmgr，是谁“悄悄地”为 UEFI NVRAM 添加原生启动项？
:::

## 启动加载器（以 Grub 为主）
这也是最广泛使用的启动方式 ~~，Windows 也干了~~。在 Linux 当中，最常用的加载器是 Grub。当然，也有使用 rEFInd 的。

启动加载器（bootloader）本身作为跳板，被 UEFI 固件加载后，需要根据配置找到真正的 Linux 内核，并经由内核引导用户硬盘上的 Arch 系统。而在 Windows 中，`boot[a-z]{3,4}.efi`会根据`BCD`配置文件，执行硬盘其中一个 Windows 副本中的`winload.efi`，并将该副本的其余加载流程交给它完成。

正常使用 Windows 单系统的用户可能对启动过程并无察觉，但一旦与 Linux 混用，你就需要**留意 Linux 的加载器会不会被 Windows 刷下去（甚至被覆盖）**。除此之外，固件和内核之间隔着加载器这么一块跳板，势必会拖慢引导流程。因此就个人来说，我不会再考虑 Grub 这类方案了。

### i. 修复 Grub 引导
Windows 启不动我们会尝试修复引导，Arch 亦然。修复 Grub 引导实际上就是**重走 Grub 安装流程**：

- `mount`挂载相应分区；
- `genfstab`重建挂载表（如有必要）；
> 个人建议无论如何都重建一遍`fstab`。反正刷完绝对是最新的。
- `arch-chroot`切换进硬盘上的系统；
- `grub-install`重建 grub 引导。
- `grub-mkconfig`重建 grub 配置（可能不需要……？）。

### ii. 改用回退启动项
事实上，需要反复重建 Grub 引导的一大原因就在于，Grub 只会写入它自己的`grubx64.efi`，以及原生启动项：

![群友的 ESP 分区目录树 =200x](.img/arch_uefi-esp.webp)

那么办法也很简单：像 Windows 那样也建一个回退路径启动项。最简单的做法当然是复制改名，若求稳妥可以考虑用`grub-install`刷：
```sh
grub-install --target=x86_64-efi --efi-directory=/efi --bootloader-id=GRUB --removable
```
当然如果是像图中那样不止一个 Grub，甚至同盘 Windows 和 Arch 双系统，那我不推荐你这么做。

> [!warning]
> 不要在这里试图用软链接节省空间！

## 固件直接引导（EFIStub）
Grub 本身写入 ESP 的内容不多，配置啊、Linux 内核啊都在`/boot`。有人便主张把`/boot`还给`/`，ESP 分区实际挂载`/efi`。
而岛风则提出了更激进的主张：让固件直接引导内核。

> An EFI boot stub (aka EFI stub) is **a kernel that is an EFI executable**,
> i.e. that can directly be booted from the UEFI.
> ::: right
> ——[Arch Wiki: EFIStub](https://wiki.archlinux.org/title/EFISTUB)
> :::

根据 Wiki，**默认情况下** Arch Linux 的内核本身就是可启动 EFI，只是需要附加[**内核参数**](https://wiki.archlinux.org/title/Kernel_parameters#Parameter_list)：
```
# 为便于阅读，这里分了三行。
root=UUID=7a6afcd0-a25a-4a6c-bf7b-920b53097eae
resume=UUID=b84ae173-edbc-442c-b00b-5c47eef203f1
rw loglevel=3 quiet initrd=\intel-ucode.img initrd=\initramfs-linux.img
```
::: details 内核参数详解
Grub 等启动加载器的本职工作就是帮你引导内核，因此它们的配置文件已经包含完整的内核参数了。
我上面列的内核参数是参照 Wiki 自行搭配，确认可行的参数。你也可以查 Wiki 自行组合。
- `root`：`/`分区。目前只见到 UUID 填法。
- `rw rootflags=subvol=@`：对`/`分区挂载的附加属性，比如可读写、指定 Btrfs 子卷。
- `resume`：休眠使用的交换分区，同样只见到 UUID 填法。休眠时会在指定 Swap 里创建内存映像。
- `loglevel=3 quiet`：内核加载时的附加属性，如日志等级之类。
- `initrd=\intel-ucode.img`：加载的初始化内存盘 (Init RAM Disk)。  
  一个`.img`一条`initrd=`，路径用`\`分隔，顺序自左向右（可以参见 grub 的配置文件）

> [!note]
> 个人觉得这里 initrd 称作“初始化映像”更合适，毕竟需要填`.img`嘛。
:::

LiveCD 里的`efibootmgr`工具可以直接操作固件的启动项。当然若是遵照律回指南和 Miku 指南，那么`efibootmgr`业已安装到你的系统中，你可以在运行中的本机 Arch 系统中折腾：
```bash
# 首先确定你要操作的硬盘和分区，不要搞错。UUID 马上就会用到。
lsblk -o name,mountpoint,uuid
# 参见 Wiki，以 Btrfs 为例，仅供参考
sudo efibootmgr --create --disk /dev/nvme0n1 --part 1 \
  --label "Arch Linux" --loader /vmlinuz-linux \
  --unicode 'root=UUID=f6419b76-c55b-4d7b-92f7-99c3b04a2a6f rw rootflags=subvol=@  loglevel=3 quiet initrd=\intel-ucode.img initrd=\initramfs-linux.img'
```
> [!NOTE] 创建启动项命令详解
> - `--part 1`：你的 ESP 分区序号。根据`lsblk`的树状图顺序判别。
> - `--label "Arch Linux"`：启动项名称。大多数固件并不支持中文。
> - `--unicode`后面跟内核参数。>


归根结底，EFIStub 代替了启动加载器，由我们用户手动建立 UEFI 原生启动项。但这种方式硬要说优点吧……可能也就比 Grub 快那么几秒而已。维护起来并不比 Grub 轻松多少。

## 统一内核映像（UKI）
在应用 EFIStub 的时候我就在想，有没有可能写一个`bootx64.efi`，直接带内核参数启动`vmlinuz-linux`呢。后面偶然找到了“统一内核映像”的介绍，豁然开朗。

> A unified kernel image (UKI) is a **single executable** which can be **booted directly from UEFI firmware**, or automatically sourced by boot loaders with little or no configuration.
> ::: right
> ——[Arch Wiki: Unified Kernel Image](https://wiki.archlinux.org/title/Unified_kernel_image)
> :::

根据介绍，UKI 实际上就是将内核引导的资源整合起来，打包而成的 EFI 可执行文件。某种意义上这也算是一种「固件直接引导」，只不过 EFIStub 只创建原生启动项，而它两种启动项都可以做。

::: info UKI 通常包含……
> 摘自 [UAPI Group Specifications](https://uapi-group.org/specifications/specs/unified_kernel_image/)。

- EFI 执行代码（决定它“可执行 EFI”的本质）
- Linux 内核
- 【可选】内核参数
- 【可选】初始化内存盘
- 【可选】CPU 微码
- 【可选】描述信息、启动屏幕图、设备树……（不重要）

只要集成了 EFI 执行代码和 Linux 内核，就可以称作统一内核映像了。
:::

接下来以`mkinitcpio`为例。

### i. 内核参数
Wiki 中介绍了两种方法：
- 向`/etc/cmdline.d/`里投喂`.conf`配置（文件名随意）。比如`root.conf`决定`/`如何挂载，等等。
- 直接把所有参数搓成一行 echo 喂给`/etc/kernel/cmdline`文件。

于我而言，显然第二种更方便。
```bash
# 我在 LiveCD 里 arch-chroot 进去做的。别问我为什么没权限。
echo 'root=UUID=... resume=UUID=... rw loglevel=3 quiet' > /etc/kernel/cmdline
```
与 EFIStub 不同，这里不需要指定`initrd=`——工具会自己打包。

> [!warning]
> 若启用“安全启动”，且 UKI 封装了内核参数，则 UEFI 固件会无视外部传入的其余参数。

::: info [GPT 分区自动挂载](https://wiki.archlinuxcn.org/wiki/Systemd#GPT%E5%88%86%E5%8C%BA%E8%87%AA%E5%8A%A8%E6%8C%82%E8%BD%BD)
跟 [@Vescrity](https://github.com/Vescrity)讨论的时候我俩都觉得分区 UUID 太长了，于是他尝试省略掉`root=`参数。  
就结果来看还真可行，顺带附上他的折腾记录：[《从统一内核镜像启动》](https://vescrity.github.io/post/UKI/)。
:::

### ii. 预设文件
编辑`/etc/mkinitcpio.d/linux.preset`。
```properties
#PRESETS=('default' 'fallback')
PRESETS=('default')

#default_config="/etc/mkinitcpio.conf"
#default_image="/boot/initramfs-linux.img"
#default_uki="/efi/EFI/Linux/arch-linux.efi"
default_uki="/efi/EFI/BOOT/bootx64.efi"
default_options="--splash /usr/share/systemd/bootctl/splash-arch.bmp"
```
本身 UKI 默认是丢到`esp\EFI\Linux\arch-linux*.efi`里的，相对来说已经比较通用（Grub 可以直接读，也可以用作原生启动项）。但我尝试 UKI 本就是为了摒弃前面两种方案，殊途同归反倒不值得这么整了。所以我个人选择让 UEFI 固件直接加载回退路径启动项。

### iii. 创建映像
按需建立路径，并跑一遍生成：
```bash
mkdir -p /efi/EFI/BOOT/
mkinitcpio -p linux
```
如有必要，清理系统中废旧的启动文件（`grubx64.efi`、`refind_x64.efi`等），并用`efibootmgr`手动清理遗留的原生启动项。

---

建完之后退出系统，重启按快捷键进入启动菜单，这下该有你硬盘的 UEFI 回退路径启动项了：
```
HDD: PM8512GPKTCB4BACE-E162
```
]]></description>
            <content:encoded><![CDATA[<p>由于我对 Linux 乃至整个 UEFI 的启动机制尚且“浅尝辄止”，本文并不会展开很多硬核内容，只是对我个人使用过的启动方案做个总结。</p>
<div class="tip custom-block"><p class="custom-block-title">引导和启动</p>
<p>在维基百科中二者似乎是同一概念<sup class="footnote-ref"><a href="#footnote1">[1]</a><a class="footnote-anchor" id="footnote-ref1"></a></sup>，搜索“启动程序”会跳转到<a href="https://zh.wikipedia.org/wiki/%E5%95%9F%E5%8B%95%E7%A8%8B%E5%BC%8F" target="_blank" rel="noreferrer">“引导程序”</a>的介绍。</p>
<p>国内很多折腾 WinPE 的人（包括我）对此也并没有很明确的区分；当然有些博客则对开机装载 Linux 的过程拆分成引导、启动两个阶段。本文为了方便起见，用词不作区分。</p>
</div>
<p>在此感谢岛风 <a href="https://github.com/frg2089" target="_blank" rel="noreferrer">@frg2089</a> 指路。</p>
<h2 id="uefi-启动简述-启动项管理" tabindex="-1">UEFI 启动简述：启动项管理 <a class="header-anchor" href="#uefi-启动简述-启动项管理" aria-label="Permalink to &quot;UEFI 启动简述：启动项管理&quot;"></a></h2>
<blockquote>
<p>UEFI 规范定义了名为“UEFI 启动管理器”的一项功能……（它）是一种固件策略引擎，可通过修改固件架构中定义的全局 NVRAM 变量来进行配置。启动管理器将尝试按全局 NVRAM 变量定义的顺序依次加载 UEFI 驱动和 UEFI 应用程序（包括 UEFI 操作系统启动装载程序）。……</p>
<div style="text-align:right">
<p>——<a href="https://www.cnblogs.com/mahocon/p/5691348.html" target="_blank" rel="noreferrer">（译）UEFI 启动：实际工作原理</a></p>
</div>
</blockquote>
<p>本“议题”只讨论 UEFI 原生启动项和回退路径启动项。恕不对 BIOS 兼容的部分作详细展开。</p>
<h3 id="i-原生启动项" tabindex="-1">i. 原生启动项 <a class="header-anchor" href="#i-原生启动项" aria-label="Permalink to &quot;i. 原生启动项&quot;"></a></h3>
<p>用 Windows 7 及更高版本系统的朋友肯定知道这个东西：Windows Boot Manager。<code>bootmgr</code>它代替了<code>ntldr</code>，从此便沿用至今。</p>
<p>事实上，Windows Boot Manager 是系统安装完成后，初次加载系统时为其创建的<strong>原生启动项</strong>。它明确指出需要启动<strong>指定设备中</strong>的<strong>指定引导文件</strong>（即<code>bootmgfw.efi</code>）。</p>
<p>即便 WinToGo 也是如此——在以 U 盘身份进入 WTG 系统时，Windows 也会悄悄地把原生启动项建立好。然后重启之后再按快捷键进入启动菜单，你<strong>可能</strong>会在<strong>部分主板上</strong>发现有两个启动项，指向同一个设备：</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span>Windows Boot Manager ( Koi Series Pro ...)</span></span>
<span class="line"><span>USB HDD: Koi Series Pro ...</span></span></code></pre>
</div><p>需要注意的是，原生启动项是<strong>存储在主板里的</strong>（更准确的说，是全局 NVRAM 变量）。有些主板在检测到原生启动项失效（找不到指定引导文件）后，会自行删除该启动项。比如安装 Grub 后更换过硬盘，后来又把原盘插回去，可能仍然找不到 Grub 启动项。</p>
<h3 id="ii-回退路径启动项" tabindex="-1">ii. 回退路径启动项 <a class="header-anchor" href="#ii-回退路径启动项" aria-label="Permalink to &quot;ii. 回退路径启动项&quot;"></a></h3>
<p>对于 WinPE、Windows 安装镜像而言，它们并非用于长线运行，不可能到处添加原生启动项，那么 UEFI 如何认出它们捏？
还记得上面提到的同一设备双启动项吗？UEFI 固件是能够找到可启动设备，并且尝试启动的。但它是依据什么去找的捏？</p>
<p>UEFI 固件首先会<strong>遍历各硬盘的 ESP 分区</strong>，并在其中查找<code>\EFI\BOOT\boot{cpu_arch}.efi</code>。前面的这一固定路径就称为<strong>回退路径</strong>，通过查找回退路径建立的启动项就称作<strong>回退路径启动项</strong>。其中，<code>cpu_arch</code>即 CPU 架构，已知的有：</p>
<ul>
<li><code>x64</code>：x86-64</li>
<li><code>ia32</code>：x86-32</li>
<li><code>ia64</code>：Itanium</li>
<li><code>arm</code>：AArch32，即 arm32 <s>（胳膊 32）</s></li>
<li><code>aa64</code>：AArch64，即 arm64 <s>（64 条胳膊）</s></li>
</ul>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>UEFI 的路径系统与 Windows 类似：以<code>\</code>分隔，不区分大小写。</p>
</div>
<p>如果同一硬盘、同一 CPU 架构存在多个匹配的 EFI 文件（比如，可能有两个 ESP 分区，分开装不同系统的 EFI），那么<strong>只会选第一个有效的</strong>去执行。</p>
<p>对于 WinPE U 盘，通常它们是 MBR 分区表，那么会考虑更泛用的搜索：采用 <strong>FAT</strong> 文件系统的<strong>活动分区</strong>；
对于 GPT 分区表，可以直接搜索 ESP 分区。当然如今的主板并不会卡那么死，哪怕只是普通的 FAT 分区，也会尝试搜索、执行。</p>
<p>也就是说，哪怕原生启动项意外被固件扬了，只要还有回退启动项，便仍可从同一个硬盘启动系统。</p>
<div class="info custom-block"><p class="custom-block-title">INFO</p>
<p>实际上<code>bcdboot</code>工具会在 ESP 分区里同时写入<code>bootx64.efi</code>和<code>bootmgfw.efi</code>。前者即回退路径启动项。</p>
<p>有关<code>bootx64.efi</code>、<code>bootmgr(.efi)</code>和<code>bootmgfw.efi</code>的关系可能有些复杂，谷歌了一圈各种观点都有。本着实事求是的原则，我不会糅合这些观点提出假设，只附上几个问题：</p>
<ul>
<li><code>bootx64.efi</code>、<code>bootmgfw.efi</code>（或<code>bootmgr</code>）分别在本机 Windows、WinToGo 和 WinPE 中起到什么作用？三者之间是否存在等价（即功能上可以替代，乃至文件哈希相同）？</li>
<li>fwbootmgr（即<code>bootmgfw.efi</code>）与 bootmgr，是谁“悄悄地”为 UEFI NVRAM 添加原生启动项？</li>
</ul>
</div>
<h2 id="启动加载器-以-grub-为主" tabindex="-1">启动加载器（以 Grub 为主） <a class="header-anchor" href="#启动加载器-以-grub-为主" aria-label="Permalink to &quot;启动加载器（以 Grub 为主）&quot;"></a></h2>
<p>这也是最广泛使用的启动方式 <s>，Windows 也干了</s>。在 Linux 当中，最常用的加载器是 Grub。当然，也有使用 rEFInd 的。</p>
<p>启动加载器（bootloader）本身作为跳板，被 UEFI 固件加载后，需要根据配置找到真正的 Linux 内核，并经由内核引导用户硬盘上的 Arch 系统。而在 Windows 中，<code>boot[a-z]{3,4}.efi</code>会根据<code>BCD</code>配置文件，执行硬盘其中一个 Windows 副本中的<code>winload.efi</code>，并将该副本的其余加载流程交给它完成。</p>
<p>正常使用 Windows 单系统的用户可能对启动过程并无察觉，但一旦与 Linux 混用，你就需要<strong>留意 Linux 的加载器会不会被 Windows 刷下去（甚至被覆盖）</strong>。除此之外，固件和内核之间隔着加载器这么一块跳板，势必会拖慢引导流程。因此就个人来说，我不会再考虑 Grub 这类方案了。</p>
<h3 id="i-修复-grub-引导" tabindex="-1">i. 修复 Grub 引导 <a class="header-anchor" href="#i-修复-grub-引导" aria-label="Permalink to &quot;i. 修复 Grub 引导&quot;"></a></h3>
<p>Windows 启不动我们会尝试修复引导，Arch 亦然。修复 Grub 引导实际上就是<strong>重走 Grub 安装流程</strong>：</p>
<ul>
<li><code>mount</code>挂载相应分区；</li>
<li><code>genfstab</code>重建挂载表（如有必要）；</li>
</ul>
<blockquote>
<p>个人建议无论如何都重建一遍<code>fstab</code>。反正刷完绝对是最新的。</p>
</blockquote>
<ul>
<li><code>arch-chroot</code>切换进硬盘上的系统；</li>
<li><code>grub-install</code>重建 grub 引导。</li>
<li><code>grub-mkconfig</code>重建 grub 配置（可能不需要……？）。</li>
</ul>
<h3 id="ii-改用回退启动项" tabindex="-1">ii. 改用回退启动项 <a class="header-anchor" href="#ii-改用回退启动项" aria-label="Permalink to &quot;ii. 改用回退启动项&quot;"></a></h3>
<p>事实上，需要反复重建 Grub 引导的一大原因就在于，Grub 只会写入它自己的<code>grubx64.efi</code>，以及原生启动项：</p>
<p><img src="https://agxcoy.shimakaze.org/assets/arch_uefi-esp.BWRmagHd.webp" alt="群友的 ESP 分区目录树" width="200" loading="lazy"></p>
<p>那么办法也很简单：像 Windows 那样也建一个回退路径启动项。最简单的做法当然是复制改名，若求稳妥可以考虑用<code>grub-install</code>刷：</p>
<div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">grub-install</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --target=x86_64-efi</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --efi-directory=/efi</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --bootloader-id=GRUB</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --removable</span></span></code></pre>
</div><p>当然如果是像图中那样不止一个 Grub，甚至同盘 Windows 和 Arch 双系统，那我不推荐你这么做。</p>
<div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p>
<p>不要在这里试图用软链接节省空间！</p>
</div>
<h2 id="固件直接引导-efistub" tabindex="-1">固件直接引导（EFIStub） <a class="header-anchor" href="#固件直接引导-efistub" aria-label="Permalink to &quot;固件直接引导（EFIStub）&quot;"></a></h2>
<p>Grub 本身写入 ESP 的内容不多，配置啊、Linux 内核啊都在<code>/boot</code>。有人便主张把<code>/boot</code>还给<code>/</code>，ESP 分区实际挂载<code>/efi</code>。
而岛风则提出了更激进的主张：让固件直接引导内核。</p>
<blockquote>
<p>An EFI boot stub (aka EFI stub) is <strong>a kernel that is an EFI executable</strong>,
i.e. that can directly be booted from the UEFI.</p>
<div style="text-align:right">
<p>——<a href="https://wiki.archlinux.org/title/EFISTUB" target="_blank" rel="noreferrer">Arch Wiki: EFIStub</a></p>
</div>
</blockquote>
<p>根据 Wiki，<strong>默认情况下</strong> Arch Linux 的内核本身就是可启动 EFI，只是需要附加<a href="https://wiki.archlinux.org/title/Kernel_parameters#Parameter_list" target="_blank" rel="noreferrer"><strong>内核参数</strong></a>：</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span># 为便于阅读，这里分了三行。</span></span>
<span class="line"><span>root=UUID=7a6afcd0-a25a-4a6c-bf7b-920b53097eae</span></span>
<span class="line"><span>resume=UUID=b84ae173-edbc-442c-b00b-5c47eef203f1</span></span>
<span class="line"><span>rw loglevel=3 quiet initrd=\intel-ucode.img initrd=\initramfs-linux.img</span></span></code></pre>
</div><details class="details custom-block"><summary>内核参数详解</summary>
<p>Grub 等启动加载器的本职工作就是帮你引导内核，因此它们的配置文件已经包含完整的内核参数了。
我上面列的内核参数是参照 Wiki 自行搭配，确认可行的参数。你也可以查 Wiki 自行组合。</p>
<ul>
<li><code>root</code>：<code>/</code>分区。目前只见到 UUID 填法。</li>
<li><code>rw rootflags=subvol=@</code>：对<code>/</code>分区挂载的附加属性，比如可读写、指定 Btrfs 子卷。</li>
<li><code>resume</code>：休眠使用的交换分区，同样只见到 UUID 填法。休眠时会在指定 Swap 里创建内存映像。</li>
<li><code>loglevel=3 quiet</code>：内核加载时的附加属性，如日志等级之类。</li>
<li><code>initrd=\intel-ucode.img</code>：加载的初始化内存盘 (Init RAM Disk)。<br>
一个<code>.img</code>一条<code>initrd=</code>，路径用<code>\</code>分隔，顺序自左向右（可以参见 grub 的配置文件）</li>
</ul>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>个人觉得这里 initrd 称作“初始化映像”更合适，毕竟需要填<code>.img</code>嘛。</p>
</div>
</details>
<p>LiveCD 里的<code>efibootmgr</code>工具可以直接操作固件的启动项。当然若是遵照律回指南和 Miku 指南，那么<code>efibootmgr</code>业已安装到你的系统中，你可以在运行中的本机 Arch 系统中折腾：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 首先确定你要操作的硬盘和分区，不要搞错。UUID 马上就会用到。</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">lsblk</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -o</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> name,mountpoint,uuid</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 参见 Wiki，以 Btrfs 为例，仅供参考</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> efibootmgr</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --create</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --disk</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> /dev/nvme0n1</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --part</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span><span style="--shiki-light:#EA76CB;--shiki-dark:#F5C2E7"> \</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">  --label</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "Arch Linux"</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --loader</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> /vmlinuz-linux</span><span style="--shiki-light:#EA76CB;--shiki-dark:#F5C2E7"> \</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">  --unicode</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> 'root=UUID=f6419b76-c55b-4d7b-92f7-99c3b04a2a6f rw rootflags=subvol=@  loglevel=3 quiet initrd=\intel-ucode.img initrd=\initramfs-linux.img'</span></span></code></pre>
</div><div class="note custom-block github-alert"><p class="custom-block-title">创建启动项命令详解</p>
<p></p>
<ul>
<li><code>--part 1</code>：你的 ESP 分区序号。根据<code>lsblk</code>的树状图顺序判别。</li>
<li><code>--label &quot;Arch Linux&quot;</code>：启动项名称。大多数固件并不支持中文。</li>
<li><code>--unicode</code>后面跟内核参数。&gt;</li>
</ul>
</div>
<p>归根结底，EFIStub 代替了启动加载器，由我们用户手动建立 UEFI 原生启动项。但这种方式硬要说优点吧……可能也就比 Grub 快那么几秒而已。维护起来并不比 Grub 轻松多少。</p>
<h2 id="统一内核映像-uki" tabindex="-1">统一内核映像（UKI） <a class="header-anchor" href="#统一内核映像-uki" aria-label="Permalink to &quot;统一内核映像（UKI）&quot;"></a></h2>
<p>在应用 EFIStub 的时候我就在想，有没有可能写一个<code>bootx64.efi</code>，直接带内核参数启动<code>vmlinuz-linux</code>呢。后面偶然找到了“统一内核映像”的介绍，豁然开朗。</p>
<blockquote>
<p>A unified kernel image (UKI) is a <strong>single executable</strong> which can be <strong>booted directly from UEFI firmware</strong>, or automatically sourced by boot loaders with little or no configuration.</p>
<div style="text-align:right">
<p>——<a href="https://wiki.archlinux.org/title/Unified_kernel_image" target="_blank" rel="noreferrer">Arch Wiki: Unified Kernel Image</a></p>
</div>
</blockquote>
<p>根据介绍，UKI 实际上就是将内核引导的资源整合起来，打包而成的 EFI 可执行文件。某种意义上这也算是一种「固件直接引导」，只不过 EFIStub 只创建原生启动项，而它两种启动项都可以做。</p>
<div class="info custom-block"><p class="custom-block-title">UKI 通常包含……</p>
<blockquote>
<p>摘自 <a href="https://uapi-group.org/specifications/specs/unified_kernel_image/" target="_blank" rel="noreferrer">UAPI Group Specifications</a>。</p>
</blockquote>
<ul>
<li>EFI 执行代码（决定它“可执行 EFI”的本质）</li>
<li>Linux 内核</li>
<li>【可选】内核参数</li>
<li>【可选】初始化内存盘</li>
<li>【可选】CPU 微码</li>
<li>【可选】描述信息、启动屏幕图、设备树……（不重要）</li>
</ul>
<p>只要集成了 EFI 执行代码和 Linux 内核，就可以称作统一内核映像了。</p>
</div>
<p>接下来以<code>mkinitcpio</code>为例。</p>
<h3 id="i-内核参数" tabindex="-1">i. 内核参数 <a class="header-anchor" href="#i-内核参数" aria-label="Permalink to &quot;i. 内核参数&quot;"></a></h3>
<p>Wiki 中介绍了两种方法：</p>
<ul>
<li>向<code>/etc/cmdline.d/</code>里投喂<code>.conf</code>配置（文件名随意）。比如<code>root.conf</code>决定<code>/</code>如何挂载，等等。</li>
<li>直接把所有参数搓成一行 echo 喂给<code>/etc/kernel/cmdline</code>文件。</li>
</ul>
<p>于我而言，显然第二种更方便。</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 我在 LiveCD 里 arch-chroot 进去做的。别问我为什么没权限。</span></span>
<span class="line"><span style="--shiki-light:#D20F39;--shiki-light-font-style:italic;--shiki-dark:#F38BA8;--shiki-dark-font-style:italic">echo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> 'root=UUID=... resume=UUID=... rw loglevel=3 quiet'</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> ></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> /etc/kernel/cmdline</span></span></code></pre>
</div><p>与 EFIStub 不同，这里不需要指定<code>initrd=</code>——工具会自己打包。</p>
<div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p>
<p>若启用“安全启动”，且 UKI 封装了内核参数，则 UEFI 固件会无视外部传入的其余参数。</p>
</div>
<div class="info custom-block"><p class="custom-block-title"><a href="https://wiki.archlinuxcn.org/wiki/Systemd#GPT%E5%88%86%E5%8C%BA%E8%87%AA%E5%8A%A8%E6%8C%82%E8%BD%BD" target="_blank" rel="noreferrer">GPT 分区自动挂载</a></p>
<p>跟 <a href="https://github.com/Vescrity" target="_blank" rel="noreferrer">@Vescrity</a>讨论的时候我俩都觉得分区 UUID 太长了，于是他尝试省略掉<code>root=</code>参数。<br>
就结果来看还真可行，顺带附上他的折腾记录：<a href="https://vescrity.github.io/post/UKI/" target="_blank" rel="noreferrer">《从统一内核镜像启动》</a>。</p>
</div>
<h3 id="ii-预设文件" tabindex="-1">ii. 预设文件 <a class="header-anchor" href="#ii-预设文件" aria-label="Permalink to &quot;ii. 预设文件&quot;"></a></h3>
<p>编辑<code>/etc/mkinitcpio.d/linux.preset</code>。</p>
<div class="language-properties vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">properties</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">#PRESETS=('default' 'fallback')</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">PRESETS</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">'default'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">#default_config="/etc/mkinitcpio.conf"</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">#default_image="/boot/initramfs-linux.img"</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">#default_uki="/efi/EFI/Linux/arch-linux.efi"</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">default_uki</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"/efi/EFI/BOOT/bootx64.efi"</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">default_options</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"--splash /usr/share/systemd/bootctl/splash-arch.bmp"</span></span></code></pre>
</div><p>本身 UKI 默认是丢到<code>esp\EFI\Linux\arch-linux*.efi</code>里的，相对来说已经比较通用（Grub 可以直接读，也可以用作原生启动项）。但我尝试 UKI 本就是为了摒弃前面两种方案，殊途同归反倒不值得这么整了。所以我个人选择让 UEFI 固件直接加载回退路径启动项。</p>
<h3 id="iii-创建映像" tabindex="-1">iii. 创建映像 <a class="header-anchor" href="#iii-创建映像" aria-label="Permalink to &quot;iii. 创建映像&quot;"></a></h3>
<p>按需建立路径，并跑一遍生成：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">mkdir</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -p</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> /efi/EFI/BOOT/</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">mkinitcpio</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -p</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> linux</span></span></code></pre>
</div><p>如有必要，清理系统中废旧的启动文件（<code>grubx64.efi</code>、<code>refind_x64.efi</code>等），并用<code>efibootmgr</code>手动清理遗留的原生启动项。</p>
<hr>
<p>建完之后退出系统，重启按快捷键进入启动菜单，这下该有你硬盘的 UEFI 回退路径启动项了：</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span>HDD: PM8512GPKTCB4BACE-E162</span></span></code></pre>
</div><hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="footnote1" class="footnote-item"><p>另见英文维基：<a href="https://en.wikipedia.org/wiki/Booting" target="_blank" rel="noreferrer">Booting</a>。 <a href="#footnote-ref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
            <enclosure url="https://agxcoy.shimakaze.org/assets/arch_uefi-esp.BWRmagHd.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[关于我的三个拟设]]></title>
            <link>https://agxcoy.shimakaze.org/posts/my-nickname</link>
            <guid>https://agxcoy.shimakaze.org/posts/my-nickname</guid>
            <pubDate>Sun, 10 May 2026 13:45:46 GMT</pubDate>
            <description><![CDATA[
早在 2015 年的时候，我就开始亦步亦趋地制作 ~~（仿造）~~ 一些东西。早期说实话也没想过把这些东西发布出去，顶多在初中同学之间那里分享着玩。直到两年后的一节化学课上，我才突发奇想地决定挪用三个常见的元素——Ca（钙）、Cl（氯）、Ag（银）——作为我昵称的基底，一直沿用至今。

最近也是经历了许多烂事，我也好久没有机会去维护自己的博客了。既然 Ag 这个网名也终于启用，我想是时候补全一下那三个基于化学元素拟定的网络形象了。

> [!note]
> 由于一些意外，原有的所有拟设图均已佚失，只能从用过的 QQ 头像里找到裁剪版。
>
> 尽管如此，还是非常感谢当年无偿为我绘制立绘的亲友们。

## 钙
- 生命周期：2017-2021
- 种属：兽娘·猫郎[^moewiki_catboy]
- 瞳：青绿，猫瞳
- 代表色：白
- 性格：内向
- 配偶：Cl（氯）
- 喜欢的：苹果；尝试、摸索、折腾
- 讨厌的：阅读理解（

[^moewiki_catboy]: 这种分类取自[萌娘百科](https://zh.moegirl.org.cn/%E7%8C%AB%E9%83%8E)。简而言之就是“猫少年”，或者说“具有猫部分特征的男孩子”。

![Ca^2+^ =200x](.img/my_nickname-Ca.webp)

最早启用的设定。虽然理论上任何钙离子组成的盐都可以指代我，但中学阶段最常见的沉淀果然还是 CaCO~3~ 吧。然后“碳酸钙摘掉两个氧”，最初的名字——Caco 就确定下来了。后来又衍生出音译“卡扣”、Casheen 和相应音译“卡伸”。  
老朋友们大抵还是愿意叫我“卡”这组名字，特别是接触过的红警 2 modder 和地图师。

猫少年啊……说实话现在回头想想，可能是受到初中同好的影响也说不定。不过印象中被说“可爱”确实是很早就开始了。（话说真的可爱吗？）  
总之上图已经是我能找到尽可能完整的立绘了，看上去不需要额外添几笔细致的外貌描写。

> “他呀……是个很可爱的家伙呢。初见看着很腼腆，相谈甚欢了又开始滔滔不绝，和我独处又脸红得像熟透的苹果一样。  
> “hmm？当初怎么认识的啊……说来有点搞笑。他一开始的个人简介整了个莫名其妙的反应方程，然后我看着好玩就和他聊上咯。再然后？嘻嘻，自己猜去。  
> “搞起熟悉的东西来能心无旁骛搞个通宵……啧，有时候还挺羡慕他的。但…再怎么说也稍微陪陪我啊。明明他自己也很喜欢抱抱的说。  
> “唉，明明说好了做我的专属的……到头来还是把我抛下了嘛……”
> ::: right
> ——ChlorideP
> :::

## 氯
- 生命周期：2022-2025.3
- 种属：幽灵（♀）
- 瞳：黄绿，类人瞳孔
- 代表色：黄绿
- 性格：寡言、孤僻
- 配偶：Ca（钙）
- *伴侣：Ag（银）
- 喜欢的：听故事和讲故事
- 讨厌的：毫无营养的信源

![Cl^-^ 的 Q 版头图 =128x](.img/my_nickname-Cl.webp)

在 Caco 因故被一小撮原神同人女~~散兵厨~~攻击之后不久，钙的形象弃用，咱也随即改名为 Chloride Pussemi，即 ChlorideP 了。而后有人因为末尾这个 P 以为我是 VOCALOID 曲师（P 主），加上这个昵称全小写起来并不方便手写，遂又更名为 NyaCl.

氯的种属启发自氯单质（氯气）的物理性质。作为气体，它是有在空中的那种飘浮感的，不难想到幽灵也是在空中飘飞的存在。于是就决定是“黄绿色的幽灵”这样的形态。上图是 Q 版氯，其实还有一版正式立绘，是个大姐姐的形象。~~可惜弄丢了。~~

后来经历的一些事情让我的精神状态逐渐贴合氯气的化学性质：有毒、刺鼻。有时候的确觉得自己到处倒垃圾很困扰人；应激起来说话很大声，不也挺“刺”耳的。（苦笑）

> “你说她？我的食物罢了。知不知道一颗电子对阳离子来说有多么诱人？  
> “刚找上她的时候她还在郁郁寡欢呢。结果呢？还不是顺从本能乖乖交粮。（舔唇）  
> “不过还别说，强氧化的元素就是不一样，比什么碳酸根的美味多了。听说她已故的另一半也和碳酸根有点关系？乐。  
> “后来一来二去的，她似乎也走出来了，愿意和我一起贴贴……嗯哼，就这点来说我还是很喜欢她的。  
> “最后倒是和她贴了个痛快。不知道她觉得如何，我是觉得她这结局挺好的。好歹舒服地享受完最后一刻。  
> “就是可惜……再也找不到这么好的食物咯……（叹气）”
> ::: right
> ——SilverAg.L
> :::

## 银
- 生命周期：2025.4-
- 种属：兽娘·猫娘（亚种，魅魔混血）
- 瞳色：粉红，爱心瞳
- 代表色：银白（亮白）
- 性格：对外满不在乎，私底下很细腻
- 喜欢的：涩涩（无论主动被动）
- 讨厌的：烦心事

![暂时拿过来充数的猫娘图 =200x](.img/my_nickname-Ag.webp)

> [!note]
> 将来有时间和闲钱的话，再为 Ag 这个形象重新约张稿吧。

银这个设定实际上直到去年才给她勾个轮廓——**白丝魅魔猫娘少女**。乍一听这四个词组合在一起很违和。说实话我也觉得（

相比前面两个反映我不同时期的、特点鲜明的自设，“银”这一形象就相对一以贯之，同时又很随性了。大约在初中我就误入当时流传的所谓“本子库”[^benziku]，所以涩涩也算是我一直以来的隐藏属性，如今启用这个自设也有种借谐音梗的题发挥的意思。

[^benziku]: 具体的经过已经淡忘了，我的博客对涩涩的话题也比较含蓄。可以确定的是，“本子库”、“兔纱子”、“魔法少女”这些关键词是我早期的朦胧印象，后来形成的题材偏好（或者说 xp）也是基于这个印象所做的建构。

至于这个轮廓为什么这么左右脑互搏，只能说是基于自身经历导致的历史惯性吧。钙设是猫少年嘛，加上我认识的老朋友们普遍爱发猫猫表情包，所以我的口癖不可避免地也倾向于喵来喵去，忽然摘掉“猫”这个元素反倒不知道怎么表达了（毕竟我做不到真像魅魔那样妩媚）。但我又希望能够突出涩涩的要素，所以最终形象就是点缀上白丝、魅魔尾巴、猫耳猫爪的少女啦。~~但自己想象的时候感觉更像雌小鬼一点（）~~

设定上魅魔尾巴非常敏感，只是碰触就会浑身战栗的程度。若是摸上那么一两下说不定就开始发情了吧。

> “你是怎么看我的呢，氯喵？你会要我吗？  
> “好想再被你抱着……抱在怀里，就像…就像……”
> ::: right
> ——Ag
> :::

---

## 后记
事实上我确实没想过它们之间会有什么社会关系，毕竟本质上都是我在网络上的皮套而已，它们都是我，却又不完全是。但真的做出决定，“既然我本来就没活，索性把身为创作者的我，叫做‘氯’的我给埋葬了吧”，这么做的时候，我的内心还是会觉得痛苦，仿佛真的失去了重要的人一般。

基于这样的感情，我才决定写下这一篇随笔，完善这几个自设的形象。至于每个设定底下的自白，更多地是对[友链 PR](https://github.com/Twisuki/blog/pull/4) 里的小对话做一个扩写，很抱歉我并不擅长写一个完整的故事，只能像这样侧面地刻画 CaCl~2~ 和 AgCl 这两对之间的亲密关系。

不论钙、氯还是银，都是我精神世界的投射，也都寄托了我的愿望或是欲求。也许我就是很享受被人家说可爱呢？哪怕最终只是在博客里发癫、写一篇篇的碎碎念。
]]></description>
            <content:encoded><![CDATA[<p>早在 2015 年的时候，我就开始亦步亦趋地制作 <s>（仿造）</s> 一些东西。早期说实话也没想过把这些东西发布出去，顶多在初中同学之间那里分享着玩。直到两年后的一节化学课上，我才突发奇想地决定挪用三个常见的元素——Ca（钙）、Cl（氯）、Ag（银）——作为我昵称的基底，一直沿用至今。</p>
<p>最近也是经历了许多烂事，我也好久没有机会去维护自己的博客了。既然 Ag 这个网名也终于启用，我想是时候补全一下那三个基于化学元素拟定的网络形象了。</p>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>由于一些意外，原有的所有拟设图均已佚失，只能从用过的 QQ 头像里找到裁剪版。</p>
<p>尽管如此，还是非常感谢当年无偿为我绘制立绘的亲友们。</p>
</div>
<h2 id="钙" tabindex="-1">钙 <a class="header-anchor" href="#钙" aria-label="Permalink to &quot;钙&quot;"></a></h2>
<ul>
<li>生命周期：2017-2021</li>
<li>种属：兽娘·猫郎<sup class="footnote-ref"><a href="#footnote1">[1]</a><a class="footnote-anchor" id="footnote-ref1"></a></sup></li>
<li>瞳：青绿，猫瞳</li>
<li>代表色：白</li>
<li>性格：内向</li>
<li>配偶：Cl（氯）</li>
<li>喜欢的：苹果；尝试、摸索、折腾</li>
<li>讨厌的：阅读理解（</li>
</ul>
<p><img src="https://agxcoy.shimakaze.org/assets/my_nickname-Ca.zOZcbeeU.webp" alt="Ca2+" width="200" loading="lazy"></p>
<p>最早启用的设定。虽然理论上任何钙离子组成的盐都可以指代我，但中学阶段最常见的沉淀果然还是 CaCO<sub>3</sub> 吧。然后“碳酸钙摘掉两个氧”，最初的名字——Caco 就确定下来了。后来又衍生出音译“卡扣”、Casheen 和相应音译“卡伸”。<br>
老朋友们大抵还是愿意叫我“卡”这组名字，特别是接触过的红警 2 modder 和地图师。</p>
<p>猫少年啊……说实话现在回头想想，可能是受到初中同好的影响也说不定。不过印象中被说“可爱”确实是很早就开始了。（话说真的可爱吗？）<br>
总之上图已经是我能找到尽可能完整的立绘了，看上去不需要额外添几笔细致的外貌描写。</p>
<blockquote>
<p>“他呀……是个很可爱的家伙呢。初见看着很腼腆，相谈甚欢了又开始滔滔不绝，和我独处又脸红得像熟透的苹果一样。<br>
“hmm？当初怎么认识的啊……说来有点搞笑。他一开始的个人简介整了个莫名其妙的反应方程，然后我看着好玩就和他聊上咯。再然后？嘻嘻，自己猜去。<br>
“搞起熟悉的东西来能心无旁骛搞个通宵……啧，有时候还挺羡慕他的。但…再怎么说也稍微陪陪我啊。明明他自己也很喜欢抱抱的说。<br>
“唉，明明说好了做我的专属的……到头来还是把我抛下了嘛……”</p>
<div style="text-align:right">
<p>——ChlorideP</p>
</div>
</blockquote>
<h2 id="氯" tabindex="-1">氯 <a class="header-anchor" href="#氯" aria-label="Permalink to &quot;氯&quot;"></a></h2>
<ul>
<li>生命周期：2022-2025.3</li>
<li>种属：幽灵（♀）</li>
<li>瞳：黄绿，类人瞳孔</li>
<li>代表色：黄绿</li>
<li>性格：寡言、孤僻</li>
<li>配偶：Ca（钙）</li>
<li>*伴侣：Ag（银）</li>
<li>喜欢的：听故事和讲故事</li>
<li>讨厌的：毫无营养的信源</li>
</ul>
<p><img src="https://agxcoy.shimakaze.org/assets/my_nickname-Cl.DxJcWaOq.webp" alt="Cl- 的 Q 版头图" width="128" loading="lazy"></p>
<p>在 Caco 因故被一小撮原神同人女<s>散兵厨</s>攻击之后不久，钙的形象弃用，咱也随即改名为 Chloride Pussemi，即 ChlorideP 了。而后有人因为末尾这个 P 以为我是 VOCALOID 曲师（P 主），加上这个昵称全小写起来并不方便手写，遂又更名为 NyaCl.</p>
<p>氯的种属启发自氯单质（氯气）的物理性质。作为气体，它是有在空中的那种飘浮感的，不难想到幽灵也是在空中飘飞的存在。于是就决定是“黄绿色的幽灵”这样的形态。上图是 Q 版氯，其实还有一版正式立绘，是个大姐姐的形象。<s>可惜弄丢了。</s></p>
<p>后来经历的一些事情让我的精神状态逐渐贴合氯气的化学性质：有毒、刺鼻。有时候的确觉得自己到处倒垃圾很困扰人；应激起来说话很大声，不也挺“刺”耳的。（苦笑）</p>
<blockquote>
<p>“你说她？我的食物罢了。知不知道一颗电子对阳离子来说有多么诱人？<br>
“刚找上她的时候她还在郁郁寡欢呢。结果呢？还不是顺从本能乖乖交粮。（舔唇）<br>
“不过还别说，强氧化的元素就是不一样，比什么碳酸根的美味多了。听说她已故的另一半也和碳酸根有点关系？乐。<br>
“后来一来二去的，她似乎也走出来了，愿意和我一起贴贴……嗯哼，就这点来说我还是很喜欢她的。<br>
“最后倒是和她贴了个痛快。不知道她觉得如何，我是觉得她这结局挺好的。好歹舒服地享受完最后一刻。<br>
“就是可惜……再也找不到这么好的食物咯……（叹气）”</p>
<div style="text-align:right">
<p>——SilverAg.L</p>
</div>
</blockquote>
<h2 id="银" tabindex="-1">银 <a class="header-anchor" href="#银" aria-label="Permalink to &quot;银&quot;"></a></h2>
<ul>
<li>生命周期：2025.4-</li>
<li>种属：兽娘·猫娘（亚种，魅魔混血）</li>
<li>瞳色：粉红，爱心瞳</li>
<li>代表色：银白（亮白）</li>
<li>性格：对外满不在乎，私底下很细腻</li>
<li>喜欢的：涩涩（无论主动被动）</li>
<li>讨厌的：烦心事</li>
</ul>
<p><img src="https://agxcoy.shimakaze.org/assets/my_nickname-Ag.Pa-S5g7x.webp" alt="暂时拿过来充数的猫娘图" width="200" loading="lazy"></p>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>将来有时间和闲钱的话，再为 Ag 这个形象重新约张稿吧。</p>
</div>
<p>银这个设定实际上直到去年才给她勾个轮廓——<strong>白丝魅魔猫娘少女</strong>。乍一听这四个词组合在一起很违和。说实话我也觉得（</p>
<p>相比前面两个反映我不同时期的、特点鲜明的自设，“银”这一形象就相对一以贯之，同时又很随性了。大约在初中我就误入当时流传的所谓“本子库”<sup class="footnote-ref"><a href="#footnote2">[2]</a><a class="footnote-anchor" id="footnote-ref2"></a></sup>，所以涩涩也算是我一直以来的隐藏属性，如今启用这个自设也有种借谐音梗的题发挥的意思。</p>
<p>至于这个轮廓为什么这么左右脑互搏，只能说是基于自身经历导致的历史惯性吧。钙设是猫少年嘛，加上我认识的老朋友们普遍爱发猫猫表情包，所以我的口癖不可避免地也倾向于喵来喵去，忽然摘掉“猫”这个元素反倒不知道怎么表达了（毕竟我做不到真像魅魔那样妩媚）。但我又希望能够突出涩涩的要素，所以最终形象就是点缀上白丝、魅魔尾巴、猫耳猫爪的少女啦。<s>但自己想象的时候感觉更像雌小鬼一点（）</s></p>
<p>设定上魅魔尾巴非常敏感，只是碰触就会浑身战栗的程度。若是摸上那么一两下说不定就开始发情了吧。</p>
<blockquote>
<p>“你是怎么看我的呢，氯喵？你会要我吗？<br>
“好想再被你抱着……抱在怀里，就像…就像……”</p>
<div style="text-align:right">
<p>——Ag</p>
</div>
</blockquote>
<hr>
<h2 id="后记" tabindex="-1">后记 <a class="header-anchor" href="#后记" aria-label="Permalink to &quot;后记&quot;"></a></h2>
<p>事实上我确实没想过它们之间会有什么社会关系，毕竟本质上都是我在网络上的皮套而已，它们都是我，却又不完全是。但真的做出决定，“既然我本来就没活，索性把身为创作者的我，叫做‘氯’的我给埋葬了吧”，这么做的时候，我的内心还是会觉得痛苦，仿佛真的失去了重要的人一般。</p>
<p>基于这样的感情，我才决定写下这一篇随笔，完善这几个自设的形象。至于每个设定底下的自白，更多地是对<a href="https://github.com/Twisuki/blog/pull/4" target="_blank" rel="noreferrer">友链 PR</a> 里的小对话做一个扩写，很抱歉我并不擅长写一个完整的故事，只能像这样侧面地刻画 CaCl<sub>2</sub> 和 AgCl 这两对之间的亲密关系。</p>
<p>不论钙、氯还是银，都是我精神世界的投射，也都寄托了我的愿望或是欲求。也许我就是很享受被人家说可爱呢？哪怕最终只是在博客里发癫、写一篇篇的碎碎念。</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="footnote1" class="footnote-item"><p>这种分类取自<a href="https://zh.moegirl.org.cn/%E7%8C%AB%E9%83%8E" target="_blank" rel="noreferrer">萌娘百科</a>。简而言之就是“猫少年”，或者说“具有猫部分特征的男孩子”。 <a href="#footnote-ref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote2" class="footnote-item"><p>具体的经过已经淡忘了，我的博客对涩涩的话题也比较含蓄。可以确定的是，“本子库”、“兔纱子”、“魔法少女”这些关键词是我早期的朦胧印象，后来形成的题材偏好（或者说 xp）也是基于这个印象所做的建构。 <a href="#footnote-ref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
            <enclosure url="https://agxcoy.shimakaze.org/assets/my_nickname-Ca.zOZcbeeU.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[基于 FA2sp 的逆向小记]]></title>
            <link>https://agxcoy.shimakaze.org/posts/ra2-hooking</link>
            <guid>https://agxcoy.shimakaze.org/posts/ra2-hooking</guid>
            <pubDate>Sun, 10 May 2026 13:45:46 GMT</pubDate>
            <description><![CDATA[
参考资料：
- Zero-Fanker：[Ares Wiki](https://gitee.com/Zero_Fanker/Ares/wikis)
- 王道论坛：[2023 考研 408 学习资料](https://github.com/ddy-ddy/cs-408)

## 背景

FA2sp 是为了改善红警 2 地图编辑器 FinalAlert2（下面简称 FA2）的使用体验而开发的扩展库。它通过 Syringe 注入 Hook 的方式，无需修改 FA2 本体便能享受到扩展的功能和修复。

2024年3月8日，EA 在发布 Steam 版红警 2 时，终于把 FA2 的源码放了出来，自此 FA2sp 完成了历史使命。彼时我的考研刚刚坠机，本着“用进废退”的思想[^using_knowledge]，我打算温习下学习 408 得来的船新经验：寄组的汇编和操作系统的进程并发。于是才疏学浅的我结合自学来的粗略理解，尝试研究 FA2sp 项目作者 [@secsome](https://github.com/secsome) 留下来的逆向成果——`finalalert2yr.exe.idb`。

[^using_knowledge]: 我学习的一大原动力就是应用。我要用什么，所以我学什么。反过来也差不多——有些知识太久没用到了，也就淡忘了。也算是种实用主义？

## 复习一下寄组

> [!note]
> 我事非科班生，对汇编的认识仅限考研 408 计算机组成原理对“指令系统”的考察。  
> ~~其实我参加的是 24 考研（2023.12.23-24），但回去翻考研群已经只剩 23 考研的资料了。~~
>
> FA2 显然是 Intel x86 架构的程序，恰好 24 考研主要考 x86 汇编。

### 寄存器

除了考研常考的通用寄存器`e[abcd]x`、帧指针`ebp`栈指针`esp`外，
在遇到 Fatal Error 时，我们还重点关注`except.txt`里的`eip`寄存器：
```
EIP: 00534096	ESP: 013A89D4	EBP: 013A89FC
EAX: 00000000	EBX: 00886240	ECX: 00886240
EDX: 003F5000	ESI: 00886230	EDI: 2A3C0000
```
在 FA2sp 里，这些寄存器可以通过 [Syringe](https://github.com/Ares-Developers/YRpp/blob/master/Syringe.h) Hook 定义里的`REGISTERS *R`指针参数存取。

### 跳转汇编指令

> [!important]
> 考虑到王道书里介绍的多数基本运算指令在分析 FA2 中意义并不大，这里就直接跳过了。  
> 完整版的 x86 汇编指令介绍还请移步《汇编语言程序设计》或者《汇编原理》之类的课程，恕不浪费太多时间咯。

#### 常规：Jump 系列

分为 jmp 无条件跳转，和 j*condition* 有条件跳转两种。其中条件跳转可以**部分**参考 pwsh 的比较：
|条件跳转指令字|PowerShell 比较
|-|-|
|`je` (Equal `==`)|`-eq`|
|`jne` (Not Equal `!=`)|`-ne`|
|`jz` (Zero `== 0`)|`-eq 0`|
|`jg` (Greater than `>`)|`-gt`|
|`jge` (Greater than or Equal to `>=`)|`-ge`|
|...|

在 IDA 中，`jmp` `j...`通常跟的是标签（如`LABEL_20`），标签用于指代某一个虚拟地址（32 位程序基址`0x400000`）。
跳转指令认出标签指代的地址后，将 EIP 寄存器设为该地址，CPU 从那里继续取指、间址（可能跳过）、执行、中断（可能跳过）四部曲。

#### 特殊：函数调用

主要是`call`和`ret`这一对。

`call lbl`是父级函数去“调用”。它会把函数参数、下一指令地址压入栈，然后无条件跳转到`lbl`标签指代的地址（同时改变 EBP 的值，以便建立新的栈帧）；

相对的，`ret`是子函数要“返回”。在回收子函数栈帧、还原 EBP 之后，`ret`指令会无条件跳转回先前执行到的位置。

> [!info]
> 回收栈帧、还原回父级函数的 EBP 这两步由`leave`指令完成，  
> 相当于`mov esp, ebp`再`pop ebp`，详见「栈帧」。

::: details 栈帧

函数的执行是由进程的栈空间管理的（相应的，`malloc` `new`之类则从堆空间申请内存），正所谓“函数调用栈”。
栈帧通常会记录局部变量等临时用到的数据，同时也是实现函数调用的重要跳板。

设有这么两段代码：
```c
int eg_sub(int x, int y) { return x * y; }
int example() {
  int a = 10;
  int b = eg_sub(a, 1024);
  return b - a;
}
```
又假设`example()`被 main 函数调用，那么栈帧可能会是这种分布：
|地址|...|备注|
|-|:--:|-|
|0x524|...||
|0x520|（`main()`的 EBP）|`example()`栈帧从这里开始|
|0x51C|int a = 10|
|0x518|int b|
|0x510|（空余 8B）|`gcc`要求栈帧大小为 16B 的整数倍|
|0x50C|1024|参数 y|
|0x508|10|参数 x，即复制 a 的值|
|0x504|调用`eg_sub()`时 EIP 指向的下一指令地址|亦即被`call`压栈、`ret`返回的地址；<br>`example()`栈帧到此结束|
|0x500|（`example()`函数的 EBP）|这里是`eg_sub()`的栈帧了|
|...|...||

调用`eg_sub()`前，首先把参数`y` `x`压栈（`cdecl`约定采取反向入栈）。
对于`int b`那一行语句，我们不妨拆成这样的汇编指令：
```
push ecx     # 设 a=10 位于 ecx
call eg_sub  # 函数调用，返回值在 eax
mov ebx, eax # 假设 b 在 ebx，把返回值赋给 b
```
那么执行到`call`指令时，EIP 指向下一条`mov`指令，于是`call`指令保存（入栈）EIP 的值，放心地跳转到`eg_sub`的指令地址去了。

在进入`eg_sub`那里之后，首先建立它自己的栈帧：
```
push ebp
...
mov ecx, [ebp + 12]  # 假设 ecx 存 y
mov edx, [ebp + 8]   # 假设 edx 存 x
...
```
执行完之后保存返回值`mov eax, ...`，回收栈帧、还原现场`leave`，然后`ret`指令跳转回`example()`。
`ret`指令把执行`call`指令时的“下一指令地址”弹回 EIP 寄存器，然后 CPU 就若无其事地继续跑 example 函数了。
:::

> [!note]
> 王道计组书和视频课「过程调用的机器级表示」那一节对于`call` `ret`指令以及栈帧的介绍可能更清楚一点。
> 24 考研距写作日期也有半年余了，恕我没有办法准确地复述出来。

### 寻址方式
上面讲栈帧出现了个`[ebp + 12]`，涉及到两种寻址：寄存器间接寻址和 EBP“相对寻址”。  

> [!important]
> 注意我这“相对寻址”是打了引号的，因为并不是以 PC（或者说 IP、EIP 寄存器）为基准的相对，而是 EBP。

首先是 EBP 寻址。进程由操作系统管理，其堆栈空间在内存中开辟。既然如此，EBP 和 ESP 的值实际上就是指向内存中栈空间的地址。
比如在上面「栈帧」里举的例子，执行`example()`函数主体时，\[EBP\]=0x520，\[ESP\]=0x504；
进入`eg_sub()`函数调用后，\[EBP\] 则变为 0x500。

于是，我们可以对栈指针 ESP 和帧指针 EBP 做加减运算，找出函数参数、局部变量等信息。
例如上面建立`eg_sub`的栈帧时把函数参数从栈里读出来（不是`pop`出栈），就用`eg_sub`的 EBP 往上加。
由于两个栈帧之间总隔着一个“返回地址”，所以第一个参数并不是`+4`，而是`+8`。
而相对的，访问局部变量可以用 EBP 往下减，`EBP - 4`，`EBP - 8`，之类的。

> 通常来说，ESP 容易受`pop` `push`指令的影响，比较“多动”；而 EBP 相比起来更“安稳”一些。  
> 当然 ESP 寻址肯定是有的，Syringe 里的`XXX_STACK`就是 ESP 寻址。只是 EBP 寻址我讨论起来方便。

其次是寄存器间接寻址。对 EBP 指针做加减运算，找到参数、局部的地址之后，还需要做一次间接寻址，去内存里把真正的数据抓出来。  
间接寻址不需要你操心，我只是让你注意寄存器旁边的中括号而已：
```
mov eax, ebx    # 把 EBX 寄存器里的值直接传给 EAX
mov eax, [ebx]  # 把 EBX 里的内存地址取出来，再读那个内存地址，把数据传进 EAX。
```

## 初探 IDA

### 案例

FA2 的“国家”和“所属”是靠后缀区分的，国家直接取自 Rules*.ini，所属则是在国家基础上添加了` House`后缀，比如国家`YuriCountry`和所属`YuriCountry House`。  
默认在触发编辑器属性页里，触发所属方会截断空格，只许你选“国家”。现要求把这个碍事的截断给干掉，方便我们实现多人合作地图的“所属”关联。

### 逆向分析

::: details 有源码做题就是快
注意到`TriggerOptionsDlg.cpp`里关于“触发所属方”的事件定义：
```cpp {14,}
void CTriggerOptionsDlg::OnEditchangeHouse()
{
    // ... 前面忘了

	CString newHouse;
	m_House.GetWindowText(newHouse);  // 实际是 GetWindowTextA

	// FA2 读完所属会用 CSF 本地化这些窗口控件的所属名字（但是非常鸡肋）
    // 这一步又把本地化的所属翻译回 INI 的所属 ID
	newHouse=TranslateHouse(newHouse);

	newHouse.TrimLeft();
    // 如果你英语好一点，空格 => space，你便已经找到要淦的位置了：
	TruncSpace(newHouse);

    // ... 后面忘了
}
```
右键对`TruncSpace`转到定义，可以在`functions.cpp`发现：
```cpp
void TruncSpace(CString& str)
{
	str.TrimLeft();
	str.TrimRight();
	if(str.Find(" ")>=0) str.Delete(str.Find(" "), str.GetLength()-str.Find(" "));
}
```
于是确定我们要干掉的就是这个`TruncSpace`。

当然现状是红警 2 的地图创作仍然离不开 [handama/FA2sp](https://github.com/handama/fa2sp)，改源码没什么意义。
:::

> 开始之前赞美一下书伸，书门！（  
> 没有书伸的成果，我不可能很快找出待修改函数的虚拟地址。

在 32 位 IDA 里新建一个反编译项目，打开 FA2 的主程序。我们案例要淦的函（方）数（法）位于`0x501D90`，在菜单栏`Jump`里找到`Jump to address`，把这个地址复制进去确认。  
默认它会切换为 Graph View，你需要右键改为 Text View：

![IDA 默认的图表模式](.img/ida_graph_view.webp)

往下翻到`.text:00501E58`，注意到`GetWindowTextA`这个 WinAPI。如果你翻看了上面的源码，就会发现我们离目标不远了。  

> [!tip]
> 引用的 API，比如说 WinAPI 或者 CString 类的 API，地址通常都比较靠后。
> 在 Text View 里双击那个`GetWindowTextA`，可以发现地址跑到`0x553134`去力（瞄完可以用工具栏上的左箭头返回我们正文看的位置）。
> 所以接下来不要认错函数调用咯。

借着上面的提示，同屏`GetWindowTextA`后面只剩两个怀疑对象：`sub_43C3C0`和`sub_43EA90`。

![找出附近的函数调用](.img/ida_find_calls.webp)

接下来看看这两个嫌疑函数的特征。直接菜单栏`View`，`Open subviews`，`Generate pseudocode (F5)`生成反汇编代码，于是我们得到案例方法的 C 式伪代码：

![辨认嫌疑伸](.img/ida_recog_func_calls.webp)

由上面的源码可得，截断空格的函数`TruncSpace`只有一个参数，至此我们确定是`sub_43EA90`背锅。

## 编写 Hook

目前我已知两种 Hook 用法，我们这里写的 Hook 是第二种用途：

- 在原函数里新增内容实现扩展（`return 0`）
- 绕过（或覆盖）原函数的执行流程（`return`到目标地址）

### 背景芝士：Syringe

`Syringe.h`提供了定义 Hook 的宏：
```cpp
#define EXPORT_FUNC(name) extern "C" __declspec(dllexport) DWORD __cdecl name (REGISTERS *R)

#define DEFINE_HOOK(hook, funcname, size) \
declhook(hook, funcname, size) \
EXPORT_FUNC(funcname)
```

更详细的介绍可以翻 Zero Fanker 的 Ares Wiki。这里只需要知道，写 Hook 靠`DEFINE_HOOK`准没错。  
然后解释一下`DEFINE_HOOK`这个宏要补的三个参数：

- `hook`：即你要灌注（覆盖）的地址。

> 毕竟你外部定义的 Hook 不可能凭空插入原程序里，肯定需要遮掉原有的一部分指令机器码，才有机会跳转到你的 Hook。

- `funcname`：即你的 Hook 名字。

> [!warning]
> 虽然 Hook 名字实际上就是 DLL 导出的函数名字，但并不推荐随性的命名。最好还是讲清楚你淦的原函数叫什么，或者你写这个 Hook 要做什么。

- `size`：即 Hook 覆盖多少字节的原函数指令码（bixv >= 5B）

::: info 简单提一嘴 Syringe 如何“灌注”Hook：
完整版可以参考 Thomas 写的[高阶知识：Syringe 的工作原理](https://gitee.com/Zero_Fanker/Ares/wikis/%E9%AB%98%E9%98%B6%E7%9F%A5%E8%AF%86/Syringe%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86)。

浓缩版就是，向`hook`的地址那里写入`jmp`无条件跳转指令。由于`jmp`指令码本身占 1B，后面跟的虚拟地址总是占 4B，故`size`至少得是 5。
那倘若要覆盖超过 5B 的机器代码呢？答案是多余部分用`nop`（空指令，什么也不做）填充。

![西瓜猫猫头 =150x150](https://imgs.aixifan.com/content/2020_7_22/1.5954261313865685E9.gif)
:::

### 注意事项

我们这里针对的是函数调用，需要注意 C++ 的函数执行完成后**会触发栈区局部变量的析构函数**（通常是空间回收），因此并不建议把传参的汇编指令也给覆盖掉。

就这个例子而言，只需覆盖`call`指令：

> [!tip]
> 在 IDA 选项（`Options` > `General`）里，右上角勾选`Stack Pointer`，把`Number of opcode bytes`改为 8，确认即可看到机器码视图。  
> 然后你就会发现`call`指令刚好 5 个字节。
>
> ![call 指令的机器码](.img/ida_call_opcode.webp)

### 实战

> 都有现成项目 FA2sp 了，你不会想着要白手起家吧？

在 FA2sp 项目里依次打开`FA2sp\Ext\CTriggerOption`，在`Hooks.cpp`里添一个 Hook：
```cpp
DEFINE_HOOK(501EAD, CTriggerOption_OnCBHouseChanged, 5)
{
  // 这里什么都不用做，我们只是跳过 FA2 截断空格那一步而已。
  return 0x501EB2;  
}
```
由于`declhook`宏设置 Hook 位置时已经标了`0x`（可以在 Visual Studio 里把鼠标移到宏上面预览展开的代码），
这里`DEFINE_HOOK`后面设置的地址就不需要再补`0x`了。

## 补充

### REGISTERS 寄存器类
在上面的「背景芝士」中，注意到导出的 Hook 函数只有一个`REGISTERS`类的指针参数 R。  
有时我们会需要获取原函数的实参、局部变量等信息，并加以修改，这时就要靠 R 指针获取了：

[进阶知识：Hook 函数的用法](https://gitee.com/Zero_Fanker/Ares/wikis/%E8%BF%9B%E9%98%B6%E7%9F%A5%E8%AF%86/HOOK%E5%87%BD%E6%95%B0%E7%9A%84%E7%94%A8%E6%B3%95)

具体的例子还要结合已有的`idb`逆向成果自行意会。虽然函数调用的基本原理在计组那一块已有涉及，
但一个函数叫什么名字、里面什么寄存器对应什么变量，这些都是前辈们自行逆向出来的结论。对此，咱还是保留点最起码的尊重罢。

<!-- <style>
.VPDoc {
  img {
    padding: 0% 40px;
  }
}
.info.custom-block {
  img {
    padding: 0%;
  }
}
</style> -->
]]></description>
            <content:encoded><![CDATA[<p>参考资料：</p>
<ul>
<li>Zero-Fanker：<a href="https://gitee.com/Zero_Fanker/Ares/wikis" target="_blank" rel="noreferrer">Ares Wiki</a></li>
<li>王道论坛：<a href="https://github.com/ddy-ddy/cs-408" target="_blank" rel="noreferrer">2023 考研 408 学习资料</a></li>
</ul>
<h2 id="背景" tabindex="-1">背景 <a class="header-anchor" href="#背景" aria-label="Permalink to &quot;背景&quot;"></a></h2>
<p>FA2sp 是为了改善红警 2 地图编辑器 FinalAlert2（下面简称 FA2）的使用体验而开发的扩展库。它通过 Syringe 注入 Hook 的方式，无需修改 FA2 本体便能享受到扩展的功能和修复。</p>
<p>2024年3月8日，EA 在发布 Steam 版红警 2 时，终于把 FA2 的源码放了出来，自此 FA2sp 完成了历史使命。彼时我的考研刚刚坠机，本着“用进废退”的思想<sup class="footnote-ref"><a href="#footnote1">[1]</a><a class="footnote-anchor" id="footnote-ref1"></a></sup>，我打算温习下学习 408 得来的船新经验：寄组的汇编和操作系统的进程并发。于是才疏学浅的我结合自学来的粗略理解，尝试研究 FA2sp 项目作者 <a href="https://github.com/secsome" target="_blank" rel="noreferrer">@secsome</a> 留下来的逆向成果——<code>finalalert2yr.exe.idb</code>。</p>
<h2 id="复习一下寄组" tabindex="-1">复习一下寄组 <a class="header-anchor" href="#复习一下寄组" aria-label="Permalink to &quot;复习一下寄组&quot;"></a></h2>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>我事非科班生，对汇编的认识仅限考研 408 计算机组成原理对“指令系统”的考察。<br>
<s>其实我参加的是 24 考研（2023.12.23-24），但回去翻考研群已经只剩 23 考研的资料了。</s></p>
<p>FA2 显然是 Intel x86 架构的程序，恰好 24 考研主要考 x86 汇编。</p>
</div>
<h3 id="寄存器" tabindex="-1">寄存器 <a class="header-anchor" href="#寄存器" aria-label="Permalink to &quot;寄存器&quot;"></a></h3>
<p>除了考研常考的通用寄存器<code>e[abcd]x</code>、帧指针<code>ebp</code>栈指针<code>esp</code>外，
在遇到 Fatal Error 时，我们还重点关注<code>except.txt</code>里的<code>eip</code>寄存器：</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span>EIP: 00534096	ESP: 013A89D4	EBP: 013A89FC</span></span>
<span class="line"><span>EAX: 00000000	EBX: 00886240	ECX: 00886240</span></span>
<span class="line"><span>EDX: 003F5000	ESI: 00886230	EDI: 2A3C0000</span></span></code></pre>
</div><p>在 FA2sp 里，这些寄存器可以通过 <a href="https://github.com/Ares-Developers/YRpp/blob/master/Syringe.h" target="_blank" rel="noreferrer">Syringe</a> Hook 定义里的<code>REGISTERS *R</code>指针参数存取。</p>
<h3 id="跳转汇编指令" tabindex="-1">跳转汇编指令 <a class="header-anchor" href="#跳转汇编指令" aria-label="Permalink to &quot;跳转汇编指令&quot;"></a></h3>
<div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p>
<p>考虑到王道书里介绍的多数基本运算指令在分析 FA2 中意义并不大，这里就直接跳过了。<br>
完整版的 x86 汇编指令介绍还请移步《汇编语言程序设计》或者《汇编原理》之类的课程，恕不浪费太多时间咯。</p>
</div>
<h4 id="常规-jump-系列" tabindex="-1">常规：Jump 系列 <a class="header-anchor" href="#常规-jump-系列" aria-label="Permalink to &quot;常规：Jump 系列&quot;"></a></h4>
<p>分为 jmp 无条件跳转，和 j<em>condition</em> 有条件跳转两种。其中条件跳转可以<strong>部分</strong>参考 pwsh 的比较：</p>
<table tabindex="0">
<thead>
<tr>
<th>条件跳转指令字</th>
<th>PowerShell 比较</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>je</code> (Equal <code>==</code>)</td>
<td><code>-eq</code></td>
</tr>
<tr>
<td><code>jne</code> (Not Equal <code>!=</code>)</td>
<td><code>-ne</code></td>
</tr>
<tr>
<td><code>jz</code> (Zero <code>== 0</code>)</td>
<td><code>-eq 0</code></td>
</tr>
<tr>
<td><code>jg</code> (Greater than <code>&gt;</code>)</td>
<td><code>-gt</code></td>
</tr>
<tr>
<td><code>jge</code> (Greater than or Equal to <code>&gt;=</code>)</td>
<td><code>-ge</code></td>
</tr>
<tr>
<td>...</td>
<td></td>
</tr>
</tbody>
</table>
<p>在 IDA 中，<code>jmp</code> <code>j...</code>通常跟的是标签（如<code>LABEL_20</code>），标签用于指代某一个虚拟地址（32 位程序基址<code>0x400000</code>）。
跳转指令认出标签指代的地址后，将 EIP 寄存器设为该地址，CPU 从那里继续取指、间址（可能跳过）、执行、中断（可能跳过）四部曲。</p>
<h4 id="特殊-函数调用" tabindex="-1">特殊：函数调用 <a class="header-anchor" href="#特殊-函数调用" aria-label="Permalink to &quot;特殊：函数调用&quot;"></a></h4>
<p>主要是<code>call</code>和<code>ret</code>这一对。</p>
<p><code>call lbl</code>是父级函数去“调用”。它会把函数参数、下一指令地址压入栈，然后无条件跳转到<code>lbl</code>标签指代的地址（同时改变 EBP 的值，以便建立新的栈帧）；</p>
<p>相对的，<code>ret</code>是子函数要“返回”。在回收子函数栈帧、还原 EBP 之后，<code>ret</code>指令会无条件跳转回先前执行到的位置。</p>
<div class="info custom-block github-alert"><p class="custom-block-title">INFO</p>
<p>回收栈帧、还原回父级函数的 EBP 这两步由<code>leave</code>指令完成，<br>
相当于<code>mov esp, ebp</code>再<code>pop ebp</code>，详见「栈帧」。</p>
</div>
<details class="details custom-block"><summary>栈帧</summary>
<p>函数的执行是由进程的栈空间管理的（相应的，<code>malloc</code> <code>new</code>之类则从堆空间申请内存），正所谓“函数调用栈”。
栈帧通常会记录局部变量等临时用到的数据，同时也是实现函数调用的重要跳板。</p>
<p>设有这么两段代码：</p>
<div class="language-c vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">c</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">int</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> eg_sub</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">int</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> x</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> int</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> y</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2"> {</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> x </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">*</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> y</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2"> }</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">int</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> example</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">()</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2"> {</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">  int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> a </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 10</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">  int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> b </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> eg_sub</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">a</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1024</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">);</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">  return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> b </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">-</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> a</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">}</span></span></code></pre>
</div><p>又假设<code>example()</code>被 main 函数调用，那么栈帧可能会是这种分布：</p>
<table tabindex="0">
<thead>
<tr>
<th>地址</th>
<th style="text-align:center">...</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>0x524</td>
<td style="text-align:center">...</td>
<td></td>
</tr>
<tr>
<td>0x520</td>
<td style="text-align:center">（<code>main()</code>的 EBP）</td>
<td><code>example()</code>栈帧从这里开始</td>
</tr>
<tr>
<td>0x51C</td>
<td style="text-align:center">int a = 10</td>
<td></td>
</tr>
<tr>
<td>0x518</td>
<td style="text-align:center">int b</td>
<td></td>
</tr>
<tr>
<td>0x510</td>
<td style="text-align:center">（空余 8B）</td>
<td><code>gcc</code>要求栈帧大小为 16B 的整数倍</td>
</tr>
<tr>
<td>0x50C</td>
<td style="text-align:center">1024</td>
<td>参数 y</td>
</tr>
<tr>
<td>0x508</td>
<td style="text-align:center">10</td>
<td>参数 x，即复制 a 的值</td>
</tr>
<tr>
<td>0x504</td>
<td style="text-align:center">调用<code>eg_sub()</code>时 EIP 指向的下一指令地址</td>
<td>亦即被<code>call</code>压栈、<code>ret</code>返回的地址；<br><code>example()</code>栈帧到此结束</td>
</tr>
<tr>
<td>0x500</td>
<td style="text-align:center">（<code>example()</code>函数的 EBP）</td>
<td>这里是<code>eg_sub()</code>的栈帧了</td>
</tr>
<tr>
<td>...</td>
<td style="text-align:center">...</td>
<td></td>
</tr>
</tbody>
</table>
<p>调用<code>eg_sub()</code>前，首先把参数<code>y</code> <code>x</code>压栈（<code>cdecl</code>约定采取反向入栈）。
对于<code>int b</code>那一行语句，我们不妨拆成这样的汇编指令：</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span>push ecx     # 设 a=10 位于 ecx</span></span>
<span class="line"><span>call eg_sub  # 函数调用，返回值在 eax</span></span>
<span class="line"><span>mov ebx, eax # 假设 b 在 ebx，把返回值赋给 b</span></span></code></pre>
</div><p>那么执行到<code>call</code>指令时，EIP 指向下一条<code>mov</code>指令，于是<code>call</code>指令保存（入栈）EIP 的值，放心地跳转到<code>eg_sub</code>的指令地址去了。</p>
<p>在进入<code>eg_sub</code>那里之后，首先建立它自己的栈帧：</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span>push ebp</span></span>
<span class="line"><span>...</span></span>
<span class="line"><span>mov ecx, [ebp + 12]  # 假设 ecx 存 y</span></span>
<span class="line"><span>mov edx, [ebp + 8]   # 假设 edx 存 x</span></span>
<span class="line"><span>...</span></span></code></pre>
</div><p>执行完之后保存返回值<code>mov eax, ...</code>，回收栈帧、还原现场<code>leave</code>，然后<code>ret</code>指令跳转回<code>example()</code>。
<code>ret</code>指令把执行<code>call</code>指令时的“下一指令地址”弹回 EIP 寄存器，然后 CPU 就若无其事地继续跑 example 函数了。</p>
</details>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>王道计组书和视频课「过程调用的机器级表示」那一节对于<code>call</code> <code>ret</code>指令以及栈帧的介绍可能更清楚一点。
24 考研距写作日期也有半年余了，恕我没有办法准确地复述出来。</p>
</div>
<h3 id="寻址方式" tabindex="-1">寻址方式 <a class="header-anchor" href="#寻址方式" aria-label="Permalink to &quot;寻址方式&quot;"></a></h3>
<p>上面讲栈帧出现了个<code>[ebp + 12]</code>，涉及到两种寻址：寄存器间接寻址和 EBP“相对寻址”。</p>
<div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p>
<p>注意我这“相对寻址”是打了引号的，因为并不是以 PC（或者说 IP、EIP 寄存器）为基准的相对，而是 EBP。</p>
</div>
<p>首先是 EBP 寻址。进程由操作系统管理，其堆栈空间在内存中开辟。既然如此，EBP 和 ESP 的值实际上就是指向内存中栈空间的地址。
比如在上面「栈帧」里举的例子，执行<code>example()</code>函数主体时，[EBP]=0x520，[ESP]=0x504；
进入<code>eg_sub()</code>函数调用后，[EBP] 则变为 0x500。</p>
<p>于是，我们可以对栈指针 ESP 和帧指针 EBP 做加减运算，找出函数参数、局部变量等信息。
例如上面建立<code>eg_sub</code>的栈帧时把函数参数从栈里读出来（不是<code>pop</code>出栈），就用<code>eg_sub</code>的 EBP 往上加。
由于两个栈帧之间总隔着一个“返回地址”，所以第一个参数并不是<code>+4</code>，而是<code>+8</code>。
而相对的，访问局部变量可以用 EBP 往下减，<code>EBP - 4</code>，<code>EBP - 8</code>，之类的。</p>
<blockquote>
<p>通常来说，ESP 容易受<code>pop</code> <code>push</code>指令的影响，比较“多动”；而 EBP 相比起来更“安稳”一些。<br>
当然 ESP 寻址肯定是有的，Syringe 里的<code>XXX_STACK</code>就是 ESP 寻址。只是 EBP 寻址我讨论起来方便。</p>
</blockquote>
<p>其次是寄存器间接寻址。对 EBP 指针做加减运算，找到参数、局部的地址之后，还需要做一次间接寻址，去内存里把真正的数据抓出来。<br>
间接寻址不需要你操心，我只是让你注意寄存器旁边的中括号而已：</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span>mov eax, ebx    # 把 EBX 寄存器里的值直接传给 EAX</span></span>
<span class="line"><span>mov eax, [ebx]  # 把 EBX 里的内存地址取出来，再读那个内存地址，把数据传进 EAX。</span></span></code></pre>
</div><h2 id="初探-ida" tabindex="-1">初探 IDA <a class="header-anchor" href="#初探-ida" aria-label="Permalink to &quot;初探 IDA&quot;"></a></h2>
<h3 id="案例" tabindex="-1">案例 <a class="header-anchor" href="#案例" aria-label="Permalink to &quot;案例&quot;"></a></h3>
<p>FA2 的“国家”和“所属”是靠后缀区分的，国家直接取自 Rules*.ini，所属则是在国家基础上添加了<code> House</code>后缀，比如国家<code>YuriCountry</code>和所属<code>YuriCountry House</code>。<br>
默认在触发编辑器属性页里，触发所属方会截断空格，只许你选“国家”。现要求把这个碍事的截断给干掉，方便我们实现多人合作地图的“所属”关联。</p>
<h3 id="逆向分析" tabindex="-1">逆向分析 <a class="header-anchor" href="#逆向分析" aria-label="Permalink to &quot;逆向分析&quot;"></a></h3>
<details class="details custom-block"><summary>有源码做题就是快</summary>
<p>注意到<code>TriggerOptionsDlg.cpp</code>里关于“触发所属方”的事件定义：</p>
<div class="language-cpp vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">cpp</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">void</span><span style="--shiki-light:#DF8E1D;--shiki-dark:#F9E2AF"> CTriggerOptionsDlg</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">::</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">OnEditchangeHouse</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">()</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">{</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">    // ... 前面忘了</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">	CString newHouse</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">	m_House</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">GetWindowText</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">newHouse</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">);</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  // 实际是 GetWindowTextA</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">	// FA2 读完所属会用 CSF 本地化这些窗口控件的所属名字（但是非常鸡肋）</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">    // 这一步又把本地化的所属翻译回 INI 的所属 ID</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">	newHouse</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">TranslateHouse</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">newHouse</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">	newHouse</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">TrimLeft</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">();</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">    // 如果你英语好一点，空格 => space，你便已经找到要淦的位置了：</span></span>
<span class="line highlighted"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">	TruncSpace</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">newHouse</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">    // ... 后面忘了</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">}</span></span></code></pre>
</div><p>右键对<code>TruncSpace</code>转到定义，可以在<code>functions.cpp</code>发现：</p>
<div class="language-cpp vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">cpp</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">void</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> TruncSpace</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#DF8E1D;--shiki-light-font-style:italic;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic">CString</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">&#x26;</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> str</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">{</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">	str</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">TrimLeft</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">();</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">	str</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">TrimRight</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">();</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">	if</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">str</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">Find</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">" "</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">>=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">0</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> str</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">Delete</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">str</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">Find</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">" "</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">),</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> str</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">GetLength</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">()</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">-</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">str</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">Find</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">" "</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">));</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">}</span></span></code></pre>
</div><p>于是确定我们要干掉的就是这个<code>TruncSpace</code>。</p>
<p>当然现状是红警 2 的地图创作仍然离不开 <a href="https://github.com/handama/fa2sp" target="_blank" rel="noreferrer">handama/FA2sp</a>，改源码没什么意义。</p>
</details>
<blockquote>
<p>开始之前赞美一下书伸，书门！（<br>
没有书伸的成果，我不可能很快找出待修改函数的虚拟地址。</p>
</blockquote>
<p>在 32 位 IDA 里新建一个反编译项目，打开 FA2 的主程序。我们案例要淦的函（方）数（法）位于<code>0x501D90</code>，在菜单栏<code>Jump</code>里找到<code>Jump to address</code>，把这个地址复制进去确认。<br>
默认它会切换为 Graph View，你需要右键改为 Text View：</p>
<p><img src="https://agxcoy.shimakaze.org/assets/ida_graph_view.BzCW4EFA.webp" alt="IDA 默认的图表模式" loading="lazy"></p>
<p>往下翻到<code>.text:00501E58</code>，注意到<code>GetWindowTextA</code>这个 WinAPI。如果你翻看了上面的源码，就会发现我们离目标不远了。</p>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>引用的 API，比如说 WinAPI 或者 CString 类的 API，地址通常都比较靠后。
在 Text View 里双击那个<code>GetWindowTextA</code>，可以发现地址跑到<code>0x553134</code>去力（瞄完可以用工具栏上的左箭头返回我们正文看的位置）。
所以接下来不要认错函数调用咯。</p>
</div>
<p>借着上面的提示，同屏<code>GetWindowTextA</code>后面只剩两个怀疑对象：<code>sub_43C3C0</code>和<code>sub_43EA90</code>。</p>
<p><img src="https://agxcoy.shimakaze.org/assets/ida_find_calls.D5X16B1h.webp" alt="找出附近的函数调用" loading="lazy"></p>
<p>接下来看看这两个嫌疑函数的特征。直接菜单栏<code>View</code>，<code>Open subviews</code>，<code>Generate pseudocode (F5)</code>生成反汇编代码，于是我们得到案例方法的 C 式伪代码：</p>
<p><img src="https://agxcoy.shimakaze.org/assets/ida_recog_func_calls.Qn21jyNX.webp" alt="辨认嫌疑伸" loading="lazy"></p>
<p>由上面的源码可得，截断空格的函数<code>TruncSpace</code>只有一个参数，至此我们确定是<code>sub_43EA90</code>背锅。</p>
<h2 id="编写-hook" tabindex="-1">编写 Hook <a class="header-anchor" href="#编写-hook" aria-label="Permalink to &quot;编写 Hook&quot;"></a></h2>
<p>目前我已知两种 Hook 用法，我们这里写的 Hook 是第二种用途：</p>
<ul>
<li>在原函数里新增内容实现扩展（<code>return 0</code>）</li>
<li>绕过（或覆盖）原函数的执行流程（<code>return</code>到目标地址）</li>
</ul>
<h3 id="背景芝士-syringe" tabindex="-1">背景芝士：Syringe <a class="header-anchor" href="#背景芝士-syringe" aria-label="Permalink to &quot;背景芝士：Syringe&quot;"></a></h3>
<p><code>Syringe.h</code>提供了定义 Hook 的宏：</p>
<div class="language-cpp vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">cpp</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#DF8E1D;--shiki-dark:#F9E2AF">#define</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> EXPORT_FUNC</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic">name</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> extern</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "C"</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> __declspec</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">dllexport</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> DWORD __cdecl </span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">name</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2"> (</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">REGISTERS </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">*</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">R</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#DF8E1D;--shiki-dark:#F9E2AF">#define</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic"> DEFINE_HOOK</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic">hook</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> funcname</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> size</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#EA76CB;--shiki-dark:#F5C2E7"> \</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">declhook</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">hook</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> funcname</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> size</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#EA76CB;--shiki-dark:#F5C2E7"> \</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">EXPORT_FUNC</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">funcname</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span></code></pre>
</div><p>更详细的介绍可以翻 Zero Fanker 的 Ares Wiki。这里只需要知道，写 Hook 靠<code>DEFINE_HOOK</code>准没错。<br>
然后解释一下<code>DEFINE_HOOK</code>这个宏要补的三个参数：</p>
<ul>
<li><code>hook</code>：即你要灌注（覆盖）的地址。</li>
</ul>
<blockquote>
<p>毕竟你外部定义的 Hook 不可能凭空插入原程序里，肯定需要遮掉原有的一部分指令机器码，才有机会跳转到你的 Hook。</p>
</blockquote>
<ul>
<li><code>funcname</code>：即你的 Hook 名字。</li>
</ul>
<div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p>
<p>虽然 Hook 名字实际上就是 DLL 导出的函数名字，但并不推荐随性的命名。最好还是讲清楚你淦的原函数叫什么，或者你写这个 Hook 要做什么。</p>
</div>
<ul>
<li><code>size</code>：即 Hook 覆盖多少字节的原函数指令码（bixv &gt;= 5B）</li>
</ul>
<div class="info custom-block"><p class="custom-block-title">简单提一嘴 Syringe 如何“灌注”Hook：</p>
<p>完整版可以参考 Thomas 写的<a href="https://gitee.com/Zero_Fanker/Ares/wikis/%E9%AB%98%E9%98%B6%E7%9F%A5%E8%AF%86/Syringe%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86" target="_blank" rel="noreferrer">高阶知识：Syringe 的工作原理</a>。</p>
<p>浓缩版就是，向<code>hook</code>的地址那里写入<code>jmp</code>无条件跳转指令。由于<code>jmp</code>指令码本身占 1B，后面跟的虚拟地址总是占 4B，故<code>size</code>至少得是 5。
那倘若要覆盖超过 5B 的机器代码呢？答案是多余部分用<code>nop</code>（空指令，什么也不做）填充。</p>
<p><img src="https://imgs.aixifan.com/content/2020_7_22/1.5954261313865685E9.gif" alt="西瓜猫猫头" width="150" height="150" loading="lazy"></p>
</div>
<h3 id="注意事项" tabindex="-1">注意事项 <a class="header-anchor" href="#注意事项" aria-label="Permalink to &quot;注意事项&quot;"></a></h3>
<p>我们这里针对的是函数调用，需要注意 C++ 的函数执行完成后<strong>会触发栈区局部变量的析构函数</strong>（通常是空间回收），因此并不建议把传参的汇编指令也给覆盖掉。</p>
<p>就这个例子而言，只需覆盖<code>call</code>指令：</p>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>在 IDA 选项（<code>Options</code> &gt; <code>General</code>）里，右上角勾选<code>Stack Pointer</code>，把<code>Number of opcode bytes</code>改为 8，确认即可看到机器码视图。<br>
然后你就会发现<code>call</code>指令刚好 5 个字节。</p>
<p><img src="https://agxcoy.shimakaze.org/assets/ida_call_opcode.Dcbur93F.webp" alt="call 指令的机器码" loading="lazy"></p>
</div>
<h3 id="实战" tabindex="-1">实战 <a class="header-anchor" href="#实战" aria-label="Permalink to &quot;实战&quot;"></a></h3>
<blockquote>
<p>都有现成项目 FA2sp 了，你不会想着要白手起家吧？</p>
</blockquote>
<p>在 FA2sp 项目里依次打开<code>FA2sp\Ext\CTriggerOption</code>，在<code>Hooks.cpp</code>里添一个 Hook：</p>
<div class="language-cpp vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">cpp</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">DEFINE_HOOK</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">501EAD</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> CTriggerOption_OnCBHouseChanged</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 5</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">{</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  // 这里什么都不用做，我们只是跳过 FA2 截断空格那一步而已。</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">  return</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> 0x</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">501EB2</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  </span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">}</span></span></code></pre>
</div><p>由于<code>declhook</code>宏设置 Hook 位置时已经标了<code>0x</code>（可以在 Visual Studio 里把鼠标移到宏上面预览展开的代码），
这里<code>DEFINE_HOOK</code>后面设置的地址就不需要再补<code>0x</code>了。</p>
<h2 id="补充" tabindex="-1">补充 <a class="header-anchor" href="#补充" aria-label="Permalink to &quot;补充&quot;"></a></h2>
<h3 id="registers-寄存器类" tabindex="-1">REGISTERS 寄存器类 <a class="header-anchor" href="#registers-寄存器类" aria-label="Permalink to &quot;REGISTERS 寄存器类&quot;"></a></h3>
<p>在上面的「背景芝士」中，注意到导出的 Hook 函数只有一个<code>REGISTERS</code>类的指针参数 R。<br>
有时我们会需要获取原函数的实参、局部变量等信息，并加以修改，这时就要靠 R 指针获取了：</p>
<p><a href="https://gitee.com/Zero_Fanker/Ares/wikis/%E8%BF%9B%E9%98%B6%E7%9F%A5%E8%AF%86/HOOK%E5%87%BD%E6%95%B0%E7%9A%84%E7%94%A8%E6%B3%95" target="_blank" rel="noreferrer">进阶知识：Hook 函数的用法</a></p>
<p>具体的例子还要结合已有的<code>idb</code>逆向成果自行意会。虽然函数调用的基本原理在计组那一块已有涉及，
但一个函数叫什么名字、里面什么寄存器对应什么变量，这些都是前辈们自行逆向出来的结论。对此，咱还是保留点最起码的尊重罢。</p>
<!-- <style>
.VPDoc {
  img {
    padding: 0% 40px;
  }
}
.info.custom-block {
  img {
    padding: 0%;
  }
}
</style> -->
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="footnote1" class="footnote-item"><p>我学习的一大原动力就是应用。我要用什么，所以我学什么。反过来也差不多——有些知识太久没用到了，也就淡忘了。也算是种实用主义？ <a href="#footnote-ref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
            <enclosure url="https://agxcoy.shimakaze.org/assets/ida_graph_view.BzCW4EFA.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[浅谈红警 2 触发组件的运行逻辑]]></title>
            <link>https://agxcoy.shimakaze.org/posts/ra2-trigger-logic</link>
            <guid>https://agxcoy.shimakaze.org/posts/ra2-trigger-logic</guid>
            <pubDate>Sun, 10 May 2026 13:45:46 GMT</pubDate>
            <description><![CDATA[
::: warning 观前注意
由于涉及一些编程知识点，本文可能存在亿些阅读困难。
虽然经过与 Zero Fanker 等人的讨论后决定做些~~修缮~~重写，但难免仍有需要改进之处。
:::

## 绪论

### 研究背景
红警 2 的地图创作中，流程设计是战役最重要的一环，其中触发发挥着举足轻重的作用。好的剧情流程能够让人印象深刻，如何用触发设计出好的流程，就得凭借地图师们的逻辑智慧了。但长久以来，红红的地图教程偏实用性居多，大多数地图师对触发并没有明晰的认识，他们或许在脑内对任务流程有着天马行空的设想，但落到触发实现上往往束手无策。

本文基于已有的触发和编程实践，尝试阐明触发组件的运作**逻辑**，并就这套系统的一些缺陷提出个人的见解，希望能够对各位读者有所启发。如果能稍稍地推进触发设计的简化事业，那就再好不过了。

### 研究目的与意义
本文旨在用程序化的思想阐述触发的运作**逻辑**（而非原理，更不是底层原理），指出这套系统存在的**逻辑**缺陷，并试着给出可行的解决思路。通过对触发**逻辑**的分析，用不同的视角去看待触发，或许可以**一定程度上**简化触发的表达，增强地图师们的逻辑思维，启发他们对优秀设计模型（如状态机）的借鉴、化用，提升剧情的观赏性，为观众们带来更多精彩的“剧目”吧。

> 必须指出的一点是：逻辑与实现并不等同。逻辑关心客观事物的**本质、规律**，实现则关心符合这个规律的**解决方案**。
<!-- ::: -->

## 一、触发组件相关概念

### 1.1 触发

地图触发是早在《命运与征服：泰伯利亚黎明》就引入的系统，负责处理地图当中的“事件”^1^。

一局游戏瞬息万变，其中总有一些**既成的、游戏引擎能感知的事，叫做事件**。比如什么关键建筑被打爆了啊，哪家缺电缺钱了，等等。
这些事件会被触发捕捉到，并驱动后者去执行相应的**行为，也就是游戏引擎能做到的各种效果**：可以是刷兵，改变光照，炸个桥，平地起心灵信标……诸如此类。

再次强调，捕获到的事件、要执行的行为，都仅限**引擎能做到的**范围之内。

> 有一些地编调整过用词，事件改称“条件”，行为改叫“结果”。你可以简单这么理解。

<!-- 在红警 2，触发更类似于高中数学中的“命题”：若 p 则 q。其中事件 p 和行为 q 都可以不止一条，并且 p1 p2 p... 之间、q1 q2 q... 之间有一定的连接关系。 -->

### 1.2 局部变量

变量系统则在《命运与征服：泰伯利亚之日》才开始出现，根据`[VariableNames]`小节所处的位置不同，
分为 Rules 里的**全局变量**，和地图里的**局部变量**^2^。本文主要讨论局部变量。

在地图创作中，局部变量就是**对设计者有具体意义的**，**会因触发**（和动作脚本）**做出改变的**，**临时在某一局游戏起作用的**数值。

值得一提的是，从原版一直到 Ares 扩展平台，局部变量均有如下限制：

- **存在数目上限**：至多能设置 100 个；
- **取值范围有限**：变量的值只能为`0`（清除）和`1`（设置）两种。

一直到 [@secsome](https://github.com/secsome) 在 Phobos 平台扩展了变量系统之后，上述限制才被打破。

> 参考链接：
> [pr#321](https://github.com/Phobos-developers/Phobos/pull/321),
> [pr#424](https://github.com/Phobos-developers/Phobos/pull/424),
> [pr#425](https://github.com/Phobos-developers/Phobos/pull/425).

::: info 扩展变量系统
船新的变量系统大大扩充了局部变量池，几乎可以认为是“无限量”使用。~~真的有人会堆料到要用 2^31^ - 1 个以上的变量吗？~~
除此之外，变量取值范围也极大地延展了。但官方文档指出，这种延展并不对等：

- **纯触发组件中**，范围可达 $[-2^{31}, 2^{31} - 1]$（即上至`2147483647`）；
- **动作脚本中**参数位有限，范围**缩减**为 $[-2^{15}, 2^{15} - 1]$（上至`32767`）。出界不保证预期效果。

至于为何会有这种缩减，还请移步《计算机组成原理》第二章：数据的表示与运算。
:::

## 二、触发组件的程序逻辑分析

> [!IMPORTANT]
> 由于本博客使用的`katex`插件不支持编写 LaTeX 伪代码，因此本文的伪代码仍然基于 Python 语法。
> 但考虑到 Python 的很多特性存在理解门槛，本文将只着眼于`if`等通用表达。

### 2.1 触发的逻辑本质
提及触发，各位地图师看过教程、初步上手后应该会建立这样的基本印象：

- 触发有一定**前置事件**。
- 触发指明了**执行行为**。
- 触发要等**前置事件（条件）完成**，才会**执行相应行为**，表现出各种结果。  

> 比如简单的延时字幕，你确实需要等上那一段时间，然后才会冒出来一句“必须重新集结部队”。

这种“等待条件”的现象与高中数学讨论的“若 p 则 q”命题（即条件命题）非常类似。一个条件命题要能判断其真伪，条件就必不可少；同样的，一个触发要能执行，首先要等待前置事件完成。在程序框图中，像这种具有前置条件的语义用**条件分支**表示（如下图），又称**选择结构**。

![流程图中的“选择结构” =480x](https://image.woshipm.com/wp-files/2017/08/8Ynwb53uWeo9QMHb7Xi5.png)

从游戏实际运行的现象来看，触发会在条件满足（为真）后执行相应的“处理程序”（也就是行为），这一点符合基本印象。而当条件**尚未满足**时，触发则阻塞等待。  
假如**逻辑上**就是希望判断条件不满足（为假），由于触发一直等不到条件满足，则直到游戏结束为止这个条件都得不到任何处理。如此看来，触发的逻辑本质就是如上左图的**条件单分支结构**。

而在程序语言里，这种条件单分支常用`if`表示：

```python
if condition:  # 条件
  do_sth()  # 结果
```

在这段代码中，只有当条件`condition`满足（为真），才会去走`do_sth()`那一步，执行出结果来。
而触发也是等到条件满足，才会执行结果。既然如此，抛开触发的所属、难度开关等等其他属性，不妨就把一个触发当成这种`if`结构。

> [!note]
> 1. 为了方便讨论，本章 *Q53/Q54 允许/禁止触发* 不会直接使用`do_sth()`这种函数表示。
> 2. 如没有文字说明，文中使用到的触发条件、结果会在号码前面标明 P、Q 加以区分。

至于事件`condition`和行为`do_sth()`，事实上它们可以不止一条。多条件姑且不论，有些朋友可能很喜欢在单一触发里塞十几条结果。
那么多个条件、多个结果之间是如何串起来的呢？你可以自行翻阅各扩展平台的 YRpp，或是在 FA2 等地图编辑器中实验一下，这边就直接说结论了：

- 多个条件之间**以逻辑且（AND）连接**，所有条件**必须全部满足**，才可以执行对应的结果；
- 多个结果之间形成队列**按序执行**，但因为每一条结果执行耗时很短，表面上看似乎是同时完成。

至此，可以用这样的伪代码描述一个触发的 $p_1 \land \ldots \land p_n \to Q$ 逻辑了：
```python
if event1 and event2() and ...:
  action1()
  action2()
  ...
```

::: details 参考资料：触发行为在 INI 和引擎中的实际表示

以触发行为为例，触发行为在地图里是这种 INI 表示：
```ini
01019810 = len_actions, a1_type, a1p1, a1p2, ..., a1p6, a1_wp, a2_type, ...
```
在游戏引擎中，一条 Action 由`TActionClass`管理（下列声明有所省略，详见 [YRpp](https://github.com/Phobos-developers/YRpp/blob/c8d4da4f57a80a3cc2b9ecbde56c335e082c8335/TActionClass.h)）：
```cpp
class TActionClass : public AbstractClass {
public:
	TriggerAction      ActionKind;  // aX_type
	union {
		RectangleStruct    Bounds; // map bounds for use with action 40
		struct {
			int Param3;
			int Param4;
			int Param5;
			int Param6;
		};
	}; // It's enough for calling Bounds.X, just use a union here now. - secsome
	int                Waypoint;   // aX_wp
	int                Value2; // multipurpose  // aXp2
	int                Value; // multipurpose   // aXp1
};
```
其中 P1 决定了 P2 参数的类型。由于 P3-P6 均为`int`类型，无法满足文本、数值、触发等多种类型需求，所以由 Value (P1) 采取类似`enum`的设计，Value2 则记录真实参数 P2 的指针地址。
:::

::: details 参考资料：触发行为的具体实现
游戏引擎仍然用`TActionClass`声明和实现原版的行为（也就是地编靠前的 100 多号），扩展平台则用`TActionExt`实现扩展。
以 Phobos 的 *编辑变量* 这一结果为例：
```cpp
bool TActionExt::EditVariable(
  TActionClass* pThis, HouseClass* pHouse, ObjectClass* pObject,
  TriggerClass* pTrigger, CellStruct const& location)
{
  // blabla
  return true;
}
```
- `pThis`为`TActionClass`的指针，在参考资料「触发行为在 INI 和引擎中的实际表示」中已经介绍过，它可以记录行为参数；  
- `pHouse`系触发所属方，比如行为 *36 全部更改所属* 要变走\*触发所属方\*的全部东西。  

其余的函数形参本人没有具体研究过，恕不做介绍。
:::

### 2.2 顺序结构

顺序结构是最简单、最基本、最符合思维直觉的结构。大部分战役任务的流程也都是线性、有序的流程。以被广为改编的《脑死》为例，它的任务流程具有很典型的顺序性：

1. 建立一座~~锅盖~~苏军雷达
2. 超时空援军就位后，摧毁最后一座心灵控制器
3. 秋风扫落叶，清除残余敌军。

在线性系统中，**按时间先后执行的流程**我们认为就是顺序结构^3^。

然而两个触发之间常常在宏观上表现为异步：同样都是延时 10s，A 触发和 B 触发看起来好像是同时执行、互不相干的。那要如何保证顺序呢？
一个简单的办法是用行为 *53 允许触发*。假定有两个触发 $t_0$ 和 $t_1$，要求先触发 $t_0$ 后触发 $t_1$。只需在触发编辑器里完成如下步骤：
- 为 $t_1$ 触发勾上“禁止”选项；（$t_0$ 无需再做处理）
- 在 $t_0$ 触发的行为（或者结果）页面中，添加一项（行为类型选中 53，参数选择 $t_1$ 触发）。

如需链式允许一系列触发，比如 $t_1 \to t_2 \to \ldots \to t_n$ ，也是如法炮制。以此类推，就形成触发链：

```mermaid
flowchart LR
    A(["intro.0"]) -- “允许” --> TD(["[x] intro.1"])
    TD -- “允许” --> n1(["[x] intro.2"])
```


而在程序代码当中，顺序是通过代码行的先后次序来体现的：
```python
x = input("输入 x：")
y = input("输入 y：")
print(x + y)
```
可以很清楚地看出，这段程序先读入`x`，后读入`y`，最后输出它们两相加 ~~（实际上是拼接）~~ 的结果。**自上而下、逐行执行**，这就是程序代码的顺序结构。

从 [2.1 一节](#_2-1-触发的逻辑本质)的讨论可知，触发在逻辑上可以表达成`if`单分支的伪代码。那么不妨将这些`if`也按照地图师期望的顺序，自上而下地排列起来：
```python
# t0. start
if anyEvent:  # P8 任何事件（当*单独使用*时，它会令触发*立即执行*）
  lock_input()  # Q46 禁止用户输入
  play_speech("EVA_EstablishBattlefieldControl")  # Q21 播放 EVA 语音

  # t1. intro.brief.A
  if elapsedTime(6):  # Q13 流逝时间
    text_trigger("mission:naosi_A")  # Q11 文本触发
```
其中，$t_0$ 触发的条件是`anyEvent`。结合右边的注解和前面对于`if`执行过程的讨论，这个条件肯定是恒满足的，也就是为真（`True`）。

上述代码段的排布与前面的案例一致，$t_1$ 是排在 $t_0$ 后面的，这样 $t_1$ 必定会等 $t_0$ 先触发；除此之外，$t_1$ 还往右缩进，与 $t_0$ 的那两条结果对齐，意味着 $t_0$ 不触发时，绝对不会触发 $t_1$，以此避免独立触发和触发链的歧义。

但随着触发链越来越长，这种层层缩进的形式也会因为不同结果的穿插变得混乱不堪。除此之外，这种表示也很难解释行为*54 禁止触发*。由于本人水平有限，如何解决这些缺陷、让逻辑表达更贴近 Q53、Q54 的表述，就权当是留给有兴趣的读者一道思考题吧。

### 2.3 循环结构
早期扩展平台对于屏幕界面的利用并不充分，地图师只能每隔一段时间在屏幕左上方提示玩家当前的任务目标。那么像这种需要**重复执行同一个流程**的结构，就叫循环结构。

对于循环结构，[已知的触发教程](https://www.bilibili.com/video/BV1Zw411y7DZ?spm_id_from=333.1387.collection.video_card.click)大概会让你关注触发的「重复类型」：

![英文原版直接称作 Type<br>早期汉化也直接译作“类型” =408x](.img/ra2_trigger_logic-trigger_editor.webp)

通常来说，选择 *2 - Repeating OR* 那一项便足以满足很多简单的重复需求。

::: info 重复类型
事实上，用重复这个概念描述触发（实际上是标签）的类型并不准确。由于本文内容不允许做过多展开，有机会再单独做个“实验”。
:::

而在程序代码中，`while`循环则取代`if`扮演这个执行重复主体的角色：
```python
while True:
  print("Hello World")
```
将上述代码粘贴进 Python IDLE 回车运行，你也能看到终端里打出来一行行`Hello World`，除非用任务管理器干掉这个进程，否则它会无休止地输出下去。

分析这个运行表现不难发现，它是类似下图图二的流程：首先它走到`while`处执行判断，由于条件恒满足，往下执行`print`；然后循环并没有结束，它重新回到`while`重复执行前面说的流程，将坏掉的乐土打字机事业推进下去~~爱莉希雅死辣~~。

![流程图中的循环结构 =400x](https://image.woshipm.com/wp-files/2017/08/HRej5VdT9M9sRxXBYTJv.png)

上面的`Hello World`案例也是类似图二的流程。于是，重复触发可以用`while`循环表示：
```python
while elapsedTime(6):  # 每隔 6 秒
  text_trigger("mission:naosi_enemyarmada")  # 输出文本
  reinforcement("01014514")  # Q7 援军（小队）
```

### 2.4 线性多分支选择结构
在触发设计中，有的时候还会希望在某一个节点，根据不同的情况分别做出不同的反应，这就涉及到了选择。选择结构是创建分支流程的重要组成部分，可用于实现**条件发生变化时流程也随之发生改变**的效果^3^。

选择结构其实前面 [2.1 一节](#_2-1-触发的逻辑本质)已经介绍过，并且基于触发运行的现象指出触发本质是“条件单分支结构”。由于存在阻塞等待，触发并不会主动判断条件不满足（为假）的情形，所以在实际应用中比起去实现双分支，地图师更倾向于关注**多个条件分支**之间如何组织。

> [!tip]
> 实际上借助局部变量也可以实现双分支选择，但并不是本章的重点。详见 [3.3.1 小节](#_3-3-1-门电路的搭建)。

当大量的选择结构简单串联之后，实际上就构成了线性多分支结构^3^。而在涉及到多个条件时，不同条件之间的关系也会影响选择结构的实际效果。具体来说，是互斥与否的关系。

**互斥**是说，这组条件分支**不可能**同时满足，因此必然只会运行**其中一个**处理程序。用程序语言来说，就是`if-elif-else`。比如用公式法求一元二次方程，不可能 $\Delta > 0$ 还说方程找不到实根。  
**非互斥**是指，多条件中**至少有两条**同时满足，有可能**同时经过若干个**处理程序。比如说重叠区间：

- 若 $x \in (0, 233]$，则令 $x = -x$；
- （反之）若 $x \in (220, 512]$，则又令 $x = x^3$。

如果没有加上那个“反之”，考虑输入一个特殊值 $x = 230$：首先 $230 < 233$，$x$ 变换成 $-230$；随后 $230 > 220$，对 $-x$ 做幂运算得出 $-12,167,000$。  
而一旦加上“反之”，上述处理就是互斥的：**“反之”要求 $0 < x \le 233$ 这一条件首先不满足**。对于特殊值 $230$，显然 $230 < 233$ 满足条件，$x$ 变成 $-230$ 就直接结束了。

由于触发的 $p \to q$ 语义无法通过逻辑上互斥实现选择，因此实践中更倾向物理实现这个互斥：一旦某一分支成功触发，就需要**尽快阻止触发其他分支**。实践中通常会考虑 *Q54 禁止触发*、摧毁选择器等方式，让用户**来不及**进入另外的分支。

比如近年来的子阵营指挥：设有三支部队分别从三个方向开进，你需要**选择**指挥其中一支。这种桥段通常会刷三个假单位充当选择信标，玩家选中一个就把那一路部队更改所属。那么这种情况会在玩家选择之后*立刻摧毁其他选择信标*（*Q119 摧毁所属方*）。

```python
if objSelected(BeaconA):
  del BeaconA, BeaconB, BeaconC
  ...
# B、C 信标也是一样，就不再展开了。
```

非互斥的多分支实际上就是**顺序地经过这些条件分支**，如此便又回归顺序结构的处理方式。其中比较典型的设计类似数学上的“大、小前提”。
考虑一个争夺战：玩家与敌军互相争夺油田，但狡猾的敌军可能直接摧毁油田。假如有两个 Phobos 扩展局部变量充当计数器：
- `remaining`计算地图上还有多少油田。若余量小于玩家应占数目，任务失败；
- `player_owns`计算玩家占了多少油田。大于等于应占数目，任务完成。

那么当一个油田 *P48 被摧毁* 时，首先想到余量`remaining`减一。*如果这个油田先前是玩家持有*，那就再对`player_owns`减一。简单的允许触发即可：

```python
if objDestroyed(OilA):
  remaining -= 1  # 编辑变量 (Phobos)
  if oil_A_captured and objDestroyed(OilA):
    player_owns -= 1
```

## 三、局部变量的程序逻辑分析
前面的分析都是基于触发系统本身，贴的代码也只是对假想条件和结果的调用。而在地图创作中，除了直接利用已有的条件库，还会用局部变量间接地做条件判断。为了讲清楚它如何参与到触发逻辑当中，不妨引入经典案例“运输船找妈妈”。

### 3.1 案例实现思路
“运输船找妈妈”简单来说，就是在**乘客还有事要做**的情况下，让运载乘客的**载具打哪来，回哪去**。观察一下 YR S01：时空转移的相关演出，易得流程如下：

- **满载**的运输船从地图外刷出，移动到定点；
- 等待所有乘客下船。然后乘客有可能还会打光几秒、更改所属方……；
- **空载**的运输船从定点返回地图外。

难点就在于，如何得知**卸出乘客后船是空的**。翻看条件库，好像也没有类似的选项。  
于是考虑设`apc unload`变量初始为 0，然后在运输船卸出乘客之后*Q56 设置局部变量*，触发得知 *P36 局部变量被设置* 了，就建一个空船小队把它拉走。

> [!note]
> 篇幅原因，具体实现步骤我就不贴出来了。这种实用向教程应该也很容易找到。

### 3.2 案例逻辑分析
已知这一经典例子中运用了局部变量，那么引入局部变量后触发逻辑有什么变化呢？

首先考虑局部变量的位置。从语义、代码语法上说，不可能未经定义就使用一个变量——好比解应用题，只设了未知数`x`却凭空跑出个`y`来。

在 FA2 的局部变量窗口中，你需要为局部变量起名（声明），然后 FA2 默认会令它等于 0（初始化，当然你可以改这个初始值）。那么这些局部变量保存到地图里肯定也是带着初始值的。  
这些局部变量最终又被引擎读进内存，组成数列（或者说数组）。所以，**在游戏开始之前，这些变量就已经就位了**。

那么不妨把局部变量放在程序代码的开头：
```python
apc_unload = 0  # 多数编程语言并不允许变量名带空格
if ...:
  ...
```
既然声明了局部变量，那么就要用起来。触发里有一组事件和一组结果分别读写局部变量（设待操作的局部变量为 $x$）：

- P36：局部变量被设定（为 1），即当 $x = 1$ 时
- P37：局部变量被清除（0），即当 $x = 0$ 时

* Q56：设置局部变量（值为 1），即令 $x = 1$
* Q57：清除局部变量（值），即令 $x = 0$

::: info 赋值与相等
在数学中描述等量关系用等号：`a = 0`时，……。同时，赋值也用等号：令`x = 1`。  
而在编程中二者是不同的运算。为了避免混淆，你经常会看到用`==`指代相等，用`=`来赋值。
:::

那么上述案例便可以用如下伪代码表示：
```python
apc_unload = 0

if elapsedTime(20):
  play_speech("EVA_ReinforcementsHaveArrived")
  reinforcement("LCRFCome")

if apc_unload == 1:
  create_team("LCRFBack")  # Q4 建立小队
```

触发就是通过对局部变量值的变动，来实现一些较为复杂的逻辑判断。并且随着 Phobos 扩展了变量系统，触发对变量的判断也不再局限于 0 和 1 的“左手倒右手”，而是与科技类型、超级武器、随机数等联系了起来，实现更加精细的随机机制和流程控制。

### 3.3 高阶用法：逻辑门电路

触发条件仅以**逻辑“且”** 相连。其阻塞等待决定了它没有类似“否则”的设计，也就莫得直给的逻辑“非”；已知的触发实践也表明，多条件不可能在仅满足其中一个的情况下执行触发，无法直接判断逻辑“或”。那是否说明，触发就是做不到逻辑完备，如同早期面对平台限制一样无解呢？非也。

事实上，依据软硬件的逻辑等价性原理，触发确实可以做到或、非逻辑的判定。但显然，用累加去实现相乘，比起直接列个竖式配合九九乘法表，总是麻烦得多。自行实现的或、非逻辑也一样。

#### 3.3.1 门电路的搭建

> “逻辑智能所有的基础，都不过是这小小的开关。”
> ::: right
> ——ElectroBOOM
> :::

在 [1.2 一节](#_1-2-局部变量)中已知，原版的局部变量只有`0`和`1`两种取值，恰如一个“开关”。前面两个小节的实例分析也展示了局部变量作为“开关”，在触发系统中的**辅助判断**作用。有了开关，就可以人为建立起“或”、“非”，乃至更为复杂的逻辑通路。

我们不妨从先前一直遗漏的“条件双分支结构”入手。很显然，双分支摘去一支就退化为了单分支；反过来，双分支的真、假两路恰好是一个开关的两极。那么有没有可能，可以通过局部变量连结两个单分支，组合成一个“条件双分支”呢？这就是接下来要讨论的单刀双掷开关（SPDT）设计。

从 [3.2 一节](#_3-2-案例逻辑分析)的介绍可以看出，触发是取局部变量的**瞬时值**做的判断，变量值为`0`还是为`1`是两个独立事件。既然**逻辑上**通过局部变量反映了条件的真假，不妨假设经过这个双分支时，该局部变量的值“保持恒定”，于是得出以下的实现思路：
```python
# t0: if-else 双分支：条件判断器
if elapsedTime(114514) and ...:
  your_local_var = 1
# t1: 条件为真时的处理
if your_local_var == 1:
  ...
# t2: 条件为假时的处理
if your_local_var == 0:
  ...
```
注意前面 2.4 讨论多分支时，还提到要“尽快阻止触发其他分支”。在上面这个简单模型中，令 $t_1$ 第一时间“禁止”$t_2$，反过来也一样，就可以了。

事实上，不单是局部变量，像 YR A03：集中攻击那关争抢电厂的桥段，“电厂是否属于玩家”也可以采用 SPDT 设计，直接判断关联电厂的归属是敌是友，并相应地允许（或禁止）敌军反占电厂的演出。

像这样将条件映射到局部变量，用变量代为影响执行流的“逻辑电路”思想，接下来会多次体现。

#### 3.3.2 或运算——殊途同归
如今的任务有一个利好玩家的设计：如果你实在没钱（假定低于 $100），**或者**你的矿车被打没了，就给你派送几个钱箱子救急，所谓“战争援助”。不妨以这个设计为例。

那首先判断“缺钱”是有现成事件*52 金钱低于* 的；至于判断“缺少矿车”，在原版中可反向考虑用事件*20 生产特定类型的载具* 判断玩家生产了矿车。如果任务流程足够简单，玩家和其他 AI 阵营*绝不可能生产出相同种类的矿车*，也可以用 YR 的事件*61 科技类型不存在*。

条件输入、结果输出两端都 OK，就可以用局部变量搭建“或门”电路了：
- 设一局部变量`player low funds`，**初值为 0**；
- 触发 $I_1$：若玩家\*金钱低于\* $100，则令该变量值为 1；
- 触发 $I_2$：若玩家补牛（没有牛车），也令该变量值为 1；
- 触发 $O$：若该变量**值为 1**，则在指定路径点刷出奖励箱子。

对应的伪代码如下：
```python
player_low_funds = 0
if creditsBelow(100):
  player_low_funds = 1
if noMiner:  # 取决于你怎么判断玩家缺牛车
  player_low_funds = 1
if player_low_funds == 1:
  create_crate(0, waypoint=81)  # 参数写 0 表示*大量金钱*
```

“一真皆真”，这就是或门的精髓。当然 Python 里真正的`or`有短路特性，这里就不再展开了。

#### 3.3.3 非运算——逆向思维
原版 RA2 A08 有这样一个任务流程：在心灵信标影响到谭雅之前摧毁心灵信标。换言之，要求指定时间内摧毁目标（也就是计时器**没有超时**，并且目标被摧毁）。

在开始之前，不妨先捋一捋这个条件组的逻辑。这实际上是个必要不充分条件：要保证*完成流程*，必需满足以下两个分条件：（一）没有超时，计时器没有走完；（二）目标被摧毁。但反过来，单纯“没有超时”推不出任务完成——目标可能还在。

由于*目标被歼灭* 这个分条件不需要非运算，因此重点关注 *流程能完成 $\Rightarrow$ 没超时* 这一分路。这一路条件按照设计得是真命题，原命题为真，其逆否命题也为真：*超时了 $\Rightarrow$ 流程完不成*。这样就得到非运算的关键实现了。

有思路之后打开触发编辑器，现有这些条件与计时器有关：流逝时间、计时器时间到、流逝游戏时间……无不判断一个计时器*超时*。根据刚刚得到的逆否命题，用局部变量实现“非门”电路：

- 设一局部变量`obj1 reachable`，**初始为 1**；
- 触发 $I$：若计时器超时，则令该变量值为 0；
- 触发 $O$：若变量值**仍为 1**（即未超时），且目标不再存在，则宣布任务完成。

对应的伪代码如下：
```python
obj1_reachable = 1
if timeout:  # 取决于你用 P13 P14 还是 P47
  obj1_reachable = 0
if obj1_reachable == 1 and techTypeNotExist("NAPSYB"):
  play_speech("EVA_ObjectiveComplete")
  ...
```
---

当然，虽然理想很美好，但从原版一直到纯 Ares（特别是 Hares 平台），100 个局部变量的限制依然不容忽视。对于有限的资源，还是需要做出合理的分配。

<!-- ## 四、触发时序与标签 -->

## 结论

综上所述，红警 2 的触发组件在任务设计当中举足轻重，把握其运行的逻辑，有利于互相交流（至少不需要甩一堆截图）、有利于把更精妙的脑洞付诸实践、有利于优秀触发设计的提炼与发掘，对于触发编写乃至地图创作都有重要意义。

触发系统在逻辑层面上类似`if`单分支语句的设计，使其就入门而言并无太大门槛；支持顺序、选择、循环三种结构，可以实现大部分任务所需的线性叙事。然而其在逻辑运算上又有所欠缺，稍微复合一点的或、非逻辑判断必须通过局部变量绕路实现，对于地图师的逻辑思维能力是一大考验。

## 参考文献

1. ModEnc. [Triggers](https://modenc.renegadeprojects.com/Triggers) \[EB/OL\], 1.31.2024, 6.17.2024.
2. ModEnc. [VariableNames](https://modenc.renegadeprojects.com/VariableNames) \[EB/OL\], 5.16.2024, 6.17.2024.
3. RN Studio. [map_tutorial](https://github.com/revengenowstudio/map_tutorial) \[EB/OL\], 4.29.2024, 5.6.2024.

> [!NOTE]
> 便利起见，文献附注格式基于 GB/T 7714 规范作了简化。

## 致谢

时光荏苒，距离最开始做大白板雪原、遭遇战一样的战役地图应该也有九个年头了。能够写到这里，全拜各位大佬、同行、玩家朋友所赐。Lin 在此谢过诸位。

首先，感谢 RN Studio 指导本次“实验”。感谢制作组的地图教程，为“选题”指引方向；感谢 Zero Fanker 等人提出的指导和改进意见，以及触发实际执行的补充、佐证和更正。

其次，感谢曾经与仍在为红警 2 模组创作贡献智慧的各路人才，为论证提供各种帮助。特别感谢地图师同行们对触发系统所作的各种实地测试和表述修订，同时感谢 Phobos 团队开源触发组件的扩展实现，以及 Heli 等人为简化地图开发所做的各种尝试。

最后，感谢各位喜爱战役的红红玩家，特别是《星辰之光》的测试员和玩家朋友们拨冗测试和体验。

## 后记
比起“逻辑”，modder 们始终更在乎切实的、能实装进游戏和编辑器的改善。除此之外，红警 2 的触发逻辑本身就与“引擎实现”密不可分。比如：

- 越靠近`[Triggers]`小节头的触发越容易触发；
- *通过所属方的启动是经由所属方的某个函数执行的，此函数调用为**每 8 帧一次**，根据所属方列表从上而下依次执行。*（[FA2spHDM](https://github.com/handama/FA2sp/releases) 附文档）
- *包含两个**非持续伴随事件**的触发永远不会启动，因为这两个事件是相继发生的，而不是同时发生的，因此永远不可能同时满足条件。*（FA2spHDM 附文档，有关概念参见 [Ares 文档](https://ares-developers.github.io/Ares-docs/new/triggerevents.html)）
- ……

凡此种种，都加深了我个人的自我怀疑——我写这些真的有意义吗？  
但至少我是很想找到“存在的意义”的。所以哪怕本文多么“空中楼阁”，至少就让它烂在这里吧。

::: right
04.19.2025
:::
]]></description>
            <content:encoded><![CDATA[<div class="warning custom-block"><p class="custom-block-title">观前注意</p>
<p>由于涉及一些编程知识点，本文可能存在亿些阅读困难。
虽然经过与 Zero Fanker 等人的讨论后决定做些<s>修缮</s>重写，但难免仍有需要改进之处。</p>
</div>
<h2 id="绪论" tabindex="-1">绪论 <a class="header-anchor" href="#绪论" aria-label="Permalink to &quot;绪论&quot;"></a></h2>
<h3 id="研究背景" tabindex="-1">研究背景 <a class="header-anchor" href="#研究背景" aria-label="Permalink to &quot;研究背景&quot;"></a></h3>
<p>红警 2 的地图创作中，流程设计是战役最重要的一环，其中触发发挥着举足轻重的作用。好的剧情流程能够让人印象深刻，如何用触发设计出好的流程，就得凭借地图师们的逻辑智慧了。但长久以来，红红的地图教程偏实用性居多，大多数地图师对触发并没有明晰的认识，他们或许在脑内对任务流程有着天马行空的设想，但落到触发实现上往往束手无策。</p>
<p>本文基于已有的触发和编程实践，尝试阐明触发组件的运作<strong>逻辑</strong>，并就这套系统的一些缺陷提出个人的见解，希望能够对各位读者有所启发。如果能稍稍地推进触发设计的简化事业，那就再好不过了。</p>
<h3 id="研究目的与意义" tabindex="-1">研究目的与意义 <a class="header-anchor" href="#研究目的与意义" aria-label="Permalink to &quot;研究目的与意义&quot;"></a></h3>
<p>本文旨在用程序化的思想阐述触发的运作<strong>逻辑</strong>（而非原理，更不是底层原理），指出这套系统存在的<strong>逻辑</strong>缺陷，并试着给出可行的解决思路。通过对触发<strong>逻辑</strong>的分析，用不同的视角去看待触发，或许可以<strong>一定程度上</strong>简化触发的表达，增强地图师们的逻辑思维，启发他们对优秀设计模型（如状态机）的借鉴、化用，提升剧情的观赏性，为观众们带来更多精彩的“剧目”吧。</p>
<blockquote>
<p>必须指出的一点是：逻辑与实现并不等同。逻辑关心客观事物的<strong>本质、规律</strong>，实现则关心符合这个规律的<strong>解决方案</strong>。</p>
</blockquote>
<!-- ::: -->
<h2 id="一、触发组件相关概念" tabindex="-1">一、触发组件相关概念 <a class="header-anchor" href="#一、触发组件相关概念" aria-label="Permalink to &quot;一、触发组件相关概念&quot;"></a></h2>
<h3 id="_1-1-触发" tabindex="-1">1.1 触发 <a class="header-anchor" href="#_1-1-触发" aria-label="Permalink to &quot;1.1 触发&quot;"></a></h3>
<p>地图触发是早在《命运与征服：泰伯利亚黎明》就引入的系统，负责处理地图当中的“事件”<sup>1</sup>。</p>
<p>一局游戏瞬息万变，其中总有一些<strong>既成的、游戏引擎能感知的事，叫做事件</strong>。比如什么关键建筑被打爆了啊，哪家缺电缺钱了，等等。
这些事件会被触发捕捉到，并驱动后者去执行相应的<strong>行为，也就是游戏引擎能做到的各种效果</strong>：可以是刷兵，改变光照，炸个桥，平地起心灵信标……诸如此类。</p>
<p>再次强调，捕获到的事件、要执行的行为，都仅限<strong>引擎能做到的</strong>范围之内。</p>
<blockquote>
<p>有一些地编调整过用词，事件改称“条件”，行为改叫“结果”。你可以简单这么理解。</p>
</blockquote>
<!-- 在红警 2，触发更类似于高中数学中的“命题”：若 p 则 q。其中事件 p 和行为 q 都可以不止一条，并且 p1 p2 p... 之间、q1 q2 q... 之间有一定的连接关系。 -->
<h3 id="_1-2-局部变量" tabindex="-1">1.2 局部变量 <a class="header-anchor" href="#_1-2-局部变量" aria-label="Permalink to &quot;1.2 局部变量&quot;"></a></h3>
<p>变量系统则在《命运与征服：泰伯利亚之日》才开始出现，根据<code>[VariableNames]</code>小节所处的位置不同，
分为 Rules 里的<strong>全局变量</strong>，和地图里的<strong>局部变量</strong><sup>2</sup>。本文主要讨论局部变量。</p>
<p>在地图创作中，局部变量就是<strong>对设计者有具体意义的</strong>，<strong>会因触发</strong>（和动作脚本）<strong>做出改变的</strong>，<strong>临时在某一局游戏起作用的</strong>数值。</p>
<p>值得一提的是，从原版一直到 Ares 扩展平台，局部变量均有如下限制：</p>
<ul>
<li><strong>存在数目上限</strong>：至多能设置 100 个；</li>
<li><strong>取值范围有限</strong>：变量的值只能为<code>0</code>（清除）和<code>1</code>（设置）两种。</li>
</ul>
<p>一直到 <a href="https://github.com/secsome" target="_blank" rel="noreferrer">@secsome</a> 在 Phobos 平台扩展了变量系统之后，上述限制才被打破。</p>
<blockquote>
<p>参考链接：
<a href="https://github.com/Phobos-developers/Phobos/pull/321" target="_blank" rel="noreferrer">pr#321</a>,
<a href="https://github.com/Phobos-developers/Phobos/pull/424" target="_blank" rel="noreferrer">pr#424</a>,
<a href="https://github.com/Phobos-developers/Phobos/pull/425" target="_blank" rel="noreferrer">pr#425</a>.</p>
</blockquote>
<div class="info custom-block"><p class="custom-block-title">扩展变量系统</p>
<p>船新的变量系统大大扩充了局部变量池，几乎可以认为是“无限量”使用。<s>真的有人会堆料到要用 2<sup>31</sup> - 1 个以上的变量吗？</s>
除此之外，变量取值范围也极大地延展了。但官方文档指出，这种延展并不对等：</p>
<ul>
<li><strong>纯触发组件中</strong>，范围可达 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mo>[</mo><mo>−</mo><msup><mn>2</mn><mrow><mn>3</mn><mn>1</mn></mrow></msup><mo separator="true">,</mo><msup><mn>2</mn><mrow><mn>3</mn><mn>1</mn></mrow></msup><mo>−</mo><mn>1</mn><mo>]</mo></mrow><annotation encoding="application/x-tex">[-2^{31}, 2^{31} - 1]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.8141079999999999em;"></span><span class="strut bottom" style="height:1.064108em;vertical-align:-0.25em;"></span><span class="base textstyle uncramped"><span class="mopen">[</span><span class="mord">−</span><span class="mord"><span class="mord mathrm">2</span><span class="vlist"><span style="top:-0.363em;margin-right:0.05em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle uncramped"><span class="mord scriptstyle uncramped"><span class="mord mathrm">3</span><span class="mord mathrm">1</span></span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span><span class="mpunct">,</span><span class="mord"><span class="mord mathrm">2</span><span class="vlist"><span style="top:-0.363em;margin-right:0.05em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle uncramped"><span class="mord scriptstyle uncramped"><span class="mord mathrm">3</span><span class="mord mathrm">1</span></span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span><span class="mbin">−</span><span class="mord mathrm">1</span><span class="mclose">]</span></span></span></span>（即上至<code>2147483647</code>）；</li>
<li><strong>动作脚本中</strong>参数位有限，范围<strong>缩减</strong>为 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mo>[</mo><mo>−</mo><msup><mn>2</mn><mrow><mn>1</mn><mn>5</mn></mrow></msup><mo separator="true">,</mo><msup><mn>2</mn><mrow><mn>1</mn><mn>5</mn></mrow></msup><mo>−</mo><mn>1</mn><mo>]</mo></mrow><annotation encoding="application/x-tex">[-2^{15}, 2^{15} - 1]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.8141079999999999em;"></span><span class="strut bottom" style="height:1.064108em;vertical-align:-0.25em;"></span><span class="base textstyle uncramped"><span class="mopen">[</span><span class="mord">−</span><span class="mord"><span class="mord mathrm">2</span><span class="vlist"><span style="top:-0.363em;margin-right:0.05em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle uncramped"><span class="mord scriptstyle uncramped"><span class="mord mathrm">1</span><span class="mord mathrm">5</span></span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span><span class="mpunct">,</span><span class="mord"><span class="mord mathrm">2</span><span class="vlist"><span style="top:-0.363em;margin-right:0.05em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle uncramped"><span class="mord scriptstyle uncramped"><span class="mord mathrm">1</span><span class="mord mathrm">5</span></span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span><span class="mbin">−</span><span class="mord mathrm">1</span><span class="mclose">]</span></span></span></span>（上至<code>32767</code>）。出界不保证预期效果。</li>
</ul>
<p>至于为何会有这种缩减，还请移步《计算机组成原理》第二章：数据的表示与运算。</p>
</div>
<h2 id="二、触发组件的程序逻辑分析" tabindex="-1">二、触发组件的程序逻辑分析 <a class="header-anchor" href="#二、触发组件的程序逻辑分析" aria-label="Permalink to &quot;二、触发组件的程序逻辑分析&quot;"></a></h2>
<div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p>
<p>由于本博客使用的<code>katex</code>插件不支持编写 LaTeX 伪代码，因此本文的伪代码仍然基于 Python 语法。
但考虑到 Python 的很多特性存在理解门槛，本文将只着眼于<code>if</code>等通用表达。</p>
</div>
<h3 id="_2-1-触发的逻辑本质" tabindex="-1">2.1 触发的逻辑本质 <a class="header-anchor" href="#_2-1-触发的逻辑本质" aria-label="Permalink to &quot;2.1 触发的逻辑本质&quot;"></a></h3>
<p>提及触发，各位地图师看过教程、初步上手后应该会建立这样的基本印象：</p>
<ul>
<li>触发有一定<strong>前置事件</strong>。</li>
<li>触发指明了<strong>执行行为</strong>。</li>
<li>触发要等<strong>前置事件（条件）完成</strong>，才会<strong>执行相应行为</strong>，表现出各种结果。</li>
</ul>
<blockquote>
<p>比如简单的延时字幕，你确实需要等上那一段时间，然后才会冒出来一句“必须重新集结部队”。</p>
</blockquote>
<p>这种“等待条件”的现象与高中数学讨论的“若 p 则 q”命题（即条件命题）非常类似。一个条件命题要能判断其真伪，条件就必不可少；同样的，一个触发要能执行，首先要等待前置事件完成。在程序框图中，像这种具有前置条件的语义用<strong>条件分支</strong>表示（如下图），又称<strong>选择结构</strong>。</p>
<p><img src="https://image.woshipm.com/wp-files/2017/08/8Ynwb53uWeo9QMHb7Xi5.png" alt="流程图中的“选择结构”" width="480" loading="lazy"></p>
<p>从游戏实际运行的现象来看，触发会在条件满足（为真）后执行相应的“处理程序”（也就是行为），这一点符合基本印象。而当条件<strong>尚未满足</strong>时，触发则阻塞等待。<br>
假如<strong>逻辑上</strong>就是希望判断条件不满足（为假），由于触发一直等不到条件满足，则直到游戏结束为止这个条件都得不到任何处理。如此看来，触发的逻辑本质就是如上左图的<strong>条件单分支结构</strong>。</p>
<p>而在程序语言里，这种条件单分支常用<code>if</code>表示：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> condition</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 条件</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  do_sth</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">()</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 结果</span></span></code></pre>
</div><p>在这段代码中，只有当条件<code>condition</code>满足（为真），才会去走<code>do_sth()</code>那一步，执行出结果来。
而触发也是等到条件满足，才会执行结果。既然如此，抛开触发的所属、难度开关等等其他属性，不妨就把一个触发当成这种<code>if</code>结构。</p>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p></p>
<ol>
<li>为了方便讨论，本章 <em>Q53/Q54 允许/禁止触发</em> 不会直接使用<code>do_sth()</code>这种函数表示。</li>
<li>如没有文字说明，文中使用到的触发条件、结果会在号码前面标明 P、Q 加以区分。</li>
</ol>
</div>
<p>至于事件<code>condition</code>和行为<code>do_sth()</code>，事实上它们可以不止一条。多条件姑且不论，有些朋友可能很喜欢在单一触发里塞十几条结果。
那么多个条件、多个结果之间是如何串起来的呢？你可以自行翻阅各扩展平台的 YRpp，或是在 FA2 等地图编辑器中实验一下，这边就直接说结论了：</p>
<ul>
<li>多个条件之间<strong>以逻辑且（AND）连接</strong>，所有条件<strong>必须全部满足</strong>，才可以执行对应的结果；</li>
<li>多个结果之间形成队列<strong>按序执行</strong>，但因为每一条结果执行耗时很短，表面上看似乎是同时完成。</li>
</ul>
<p>至此，可以用这样的伪代码描述一个触发的 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>p</mi><mn>1</mn></msub><mo>∧</mo><mo>…</mo><mo>∧</mo><msub><mi>p</mi><mi>n</mi></msub><mo>→</mo><mi>Q</mi></mrow><annotation encoding="application/x-tex">p_1 \land \ldots \land p_n \to Q</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.68333em;"></span><span class="strut bottom" style="height:0.8777699999999999em;vertical-align:-0.19444em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">p</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span><span class="mbin">∧</span><span class="minner">…</span><span class="mbin">∧</span><span class="mord"><span class="mord mathit">p</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathit">n</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span><span class="mrel">→</span><span class="mord mathit">Q</span></span></span></span> 逻辑了：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> event1 </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">and</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> event2</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">()</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> and</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> ...</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  action1</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">()</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  action2</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">()</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  ...</span></span></code></pre>
</div><details class="details custom-block"><summary>参考资料：触发行为在 INI 和引擎中的实际表示</summary>
<p>以触发行为为例，触发行为在地图里是这种 INI 表示：</p>
<div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">01019810</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> len_actions, a1_type, a1p1, a1p2, ..., a1p6, a1_wp, a2_type, ...</span></span></code></pre>
</div><p>在游戏引擎中，一条 Action 由<code>TActionClass</code>管理（下列声明有所省略，详见 <a href="https://github.com/Phobos-developers/YRpp/blob/c8d4da4f57a80a3cc2b9ecbde56c335e082c8335/TActionClass.h" target="_blank" rel="noreferrer">YRpp</a>）：</p>
<div class="language-cpp vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">cpp</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">class</span><span style="--shiki-light:#DF8E1D;--shiki-light-font-style:italic;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic"> TActionClass</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2"> :</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> public</span><span style="--shiki-light:#DF8E1D;--shiki-light-font-style:italic;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic"> AbstractClass</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2"> {</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">public</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">	TriggerAction      ActionKind</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  // aX_type</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">	union</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">		RectangleStruct    Bounds</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"> // map bounds for use with action 40</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">		struct</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2"> {</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">			int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> Param3</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">			int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> Param4</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">			int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> Param5</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">			int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> Param6</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">		};</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">	};</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"> // It's enough for calling Bounds.X, just use a union here now. - secsome</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">	int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">                Waypoint</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">   // aX_wp</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">	int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">                Value2</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"> // multipurpose</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  // aXp2</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">	int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">                Value</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"> // multipurpose</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">   // aXp1</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">};</span></span></code></pre>
</div><p>其中 P1 决定了 P2 参数的类型。由于 P3-P6 均为<code>int</code>类型，无法满足文本、数值、触发等多种类型需求，所以由 Value (P1) 采取类似<code>enum</code>的设计，Value2 则记录真实参数 P2 的指针地址。</p>
</details>
<details class="details custom-block"><summary>参考资料：触发行为的具体实现</summary>
<p>游戏引擎仍然用<code>TActionClass</code>声明和实现原版的行为（也就是地编靠前的 100 多号），扩展平台则用<code>TActionExt</code>实现扩展。
以 Phobos 的 <em>编辑变量</em> 这一结果为例：</p>
<div class="language-cpp vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">cpp</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">bool</span><span style="--shiki-light:#DF8E1D;--shiki-dark:#F9E2AF"> TActionExt</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">::</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">EditVariable</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span></span>
<span class="line"><span style="--shiki-light:#DF8E1D;--shiki-light-font-style:italic;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic">  TActionClass</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">*</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> pThis</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#DF8E1D;--shiki-light-font-style:italic;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic"> HouseClass</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">*</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> pHouse</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#DF8E1D;--shiki-light-font-style:italic;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic"> ObjectClass</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">*</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> pObject</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span></span>
<span class="line"><span style="--shiki-light:#DF8E1D;--shiki-light-font-style:italic;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic">  TriggerClass</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">*</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> pTrigger</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#DF8E1D;--shiki-light-font-style:italic;--shiki-dark:#F9E2AF;--shiki-dark-font-style:italic"> CellStruct</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> const</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">&#x26;</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> location</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">{</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  // blabla</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">  return</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> true</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">;</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">}</span></span></code></pre>
</div><ul>
<li><code>pThis</code>为<code>TActionClass</code>的指针，在参考资料「触发行为在 INI 和引擎中的实际表示」中已经介绍过，它可以记录行为参数；</li>
<li><code>pHouse</code>系触发所属方，比如行为 <em>36 全部更改所属</em> 要变走*触发所属方*的全部东西。</li>
</ul>
<p>其余的函数形参本人没有具体研究过，恕不做介绍。</p>
</details>
<h3 id="_2-2-顺序结构" tabindex="-1">2.2 顺序结构 <a class="header-anchor" href="#_2-2-顺序结构" aria-label="Permalink to &quot;2.2 顺序结构&quot;"></a></h3>
<p>顺序结构是最简单、最基本、最符合思维直觉的结构。大部分战役任务的流程也都是线性、有序的流程。以被广为改编的《脑死》为例，它的任务流程具有很典型的顺序性：</p>
<ol>
<li>建立一座<s>锅盖</s>苏军雷达</li>
<li>超时空援军就位后，摧毁最后一座心灵控制器</li>
<li>秋风扫落叶，清除残余敌军。</li>
</ol>
<p>在线性系统中，<strong>按时间先后执行的流程</strong>我们认为就是顺序结构<sup>3</sup>。</p>
<p>然而两个触发之间常常在宏观上表现为异步：同样都是延时 10s，A 触发和 B 触发看起来好像是同时执行、互不相干的。那要如何保证顺序呢？
一个简单的办法是用行为 <em>53 允许触发</em>。假定有两个触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 和 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span>，要求先触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 后触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span>。只需在触发编辑器里完成如下步骤：</p>
<ul>
<li>为 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 触发勾上“禁止”选项；（<span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 无需再做处理）</li>
<li>在 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 触发的行为（或者结果）页面中，添加一项（行为类型选中 53，参数选择 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 触发）。</li>
</ul>
<p>如需链式允许一系列触发，比如 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub><mo>→</mo><msub><mi>t</mi><mn>2</mn></msub><mo>→</mo><mo>…</mo><mo>→</mo><msub><mi>t</mi><mi>n</mi></msub></mrow><annotation encoding="application/x-tex">t_1 \to t_2 \to \ldots \to t_n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span><span class="mrel">→</span><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">2</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span><span class="mrel">→</span><span class="minner">…</span><span class="mrel">→</span><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathit">n</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> ，也是如法炮制。以此类推，就形成触发链：</p>

      <Suspense> 
      <template #default>
      <Mermaid id="mermaid-268" class="mermaid" graph="flowchart%20LR%0A%20%20%20%20A(%5B%22intro.0%22%5D)%20--%20%E2%80%9C%E5%85%81%E8%AE%B8%E2%80%9D%20--%3E%20TD(%5B%22%5Bx%5D%20intro.1%22%5D)%0A%20%20%20%20TD%20--%20%E2%80%9C%E5%85%81%E8%AE%B8%E2%80%9D%20--%3E%20n1(%5B%22%5Bx%5D%20intro.2%22%5D)%0A"></Mermaid>
      </template>
        <!-- loading state via #fallback slot -->
        <template #fallback>
          Loading...
        </template>
      </Suspense><p>而在程序代码当中，顺序是通过代码行的先后次序来体现的：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">x </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-light-font-style:italic;--shiki-dark:#FAB387;--shiki-dark-font-style:italic"> input</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"输入 x："</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">y </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-light-font-style:italic;--shiki-dark:#FAB387;--shiki-dark-font-style:italic"> input</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"输入 y："</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"><span style="--shiki-light:#FE640B;--shiki-light-font-style:italic;--shiki-dark:#FAB387;--shiki-dark-font-style:italic">print</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">x </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">+</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> y</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span></code></pre>
</div><p>可以很清楚地看出，这段程序先读入<code>x</code>，后读入<code>y</code>，最后输出它们两相加 <s>（实际上是拼接）</s> 的结果。<strong>自上而下、逐行执行</strong>，这就是程序代码的顺序结构。</p>
<p>从 <a href="#_2-1-触发的逻辑本质">2.1 一节</a>的讨论可知，触发在逻辑上可以表达成<code>if</code>单分支的伪代码。那么不妨将这些<code>if</code>也按照地图师期望的顺序，自上而下地排列起来：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># t0. start</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> anyEvent</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # P8 任何事件（当*单独使用*时，它会令触发*立即执行*）</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  lock_input</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">()</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # Q46 禁止用户输入</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  play_speech</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"EVA_EstablishBattlefieldControl"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # Q21 播放 EVA 语音</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # t1. intro.brief.A</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">  if</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> elapsedTime</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">6</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">):</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # Q13 流逝时间</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">    text_trigger</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"mission:naosi_A"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # Q11 文本触发</span></span></code></pre>
</div><p>其中，<span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 触发的条件是<code>anyEvent</code>。结合右边的注解和前面对于<code>if</code>执行过程的讨论，这个条件肯定是恒满足的，也就是为真（<code>True</code>）。</p>
<p>上述代码段的排布与前面的案例一致，<span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 是排在 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 后面的，这样 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 必定会等 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 先触发；除此之外，<span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 还往右缩进，与 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 的那两条结果对齐，意味着 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">t_0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">0</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 不触发时，绝对不会触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span>，以此避免独立触发和触发链的歧义。</p>
<p>但随着触发链越来越长，这种层层缩进的形式也会因为不同结果的穿插变得混乱不堪。除此之外，这种表示也很难解释行为<em>54 禁止触发</em>。由于本人水平有限，如何解决这些缺陷、让逻辑表达更贴近 Q53、Q54 的表述，就权当是留给有兴趣的读者一道思考题吧。</p>
<h3 id="_2-3-循环结构" tabindex="-1">2.3 循环结构 <a class="header-anchor" href="#_2-3-循环结构" aria-label="Permalink to &quot;2.3 循环结构&quot;"></a></h3>
<p>早期扩展平台对于屏幕界面的利用并不充分，地图师只能每隔一段时间在屏幕左上方提示玩家当前的任务目标。那么像这种需要<strong>重复执行同一个流程</strong>的结构，就叫循环结构。</p>
<p>对于循环结构，<a href="https://www.bilibili.com/video/BV1Zw411y7DZ?spm_id_from=333.1387.collection.video_card.click" target="_blank" rel="noreferrer">已知的触发教程</a>大概会让你关注触发的「重复类型」：</p>
<p><img src="https://agxcoy.shimakaze.org/assets/ra2_trigger_logic-trigger_editor.DDDukCcV.webp" alt="英文原版直接称作 Type&lt;br&gt;早期汉化也直接译作“类型”" width="408" loading="lazy"></p>
<p>通常来说，选择 <em>2 - Repeating OR</em> 那一项便足以满足很多简单的重复需求。</p>
<div class="info custom-block"><p class="custom-block-title">重复类型</p>
<p>事实上，用重复这个概念描述触发（实际上是标签）的类型并不准确。由于本文内容不允许做过多展开，有机会再单独做个“实验”。</p>
</div>
<p>而在程序代码中，<code>while</code>循环则取代<code>if</code>扮演这个执行重复主体的角色：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">while</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> True</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#FE640B;--shiki-light-font-style:italic;--shiki-dark:#FAB387;--shiki-dark-font-style:italic">  print</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"Hello World"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span></code></pre>
</div><p>将上述代码粘贴进 Python IDLE 回车运行，你也能看到终端里打出来一行行<code>Hello World</code>，除非用任务管理器干掉这个进程，否则它会无休止地输出下去。</p>
<p>分析这个运行表现不难发现，它是类似下图图二的流程：首先它走到<code>while</code>处执行判断，由于条件恒满足，往下执行<code>print</code>；然后循环并没有结束，它重新回到<code>while</code>重复执行前面说的流程，将坏掉的乐土打字机事业推进下去<s>爱莉希雅死辣</s>。</p>
<p><img src="https://image.woshipm.com/wp-files/2017/08/HRej5VdT9M9sRxXBYTJv.png" alt="流程图中的循环结构" width="400" loading="lazy"></p>
<p>上面的<code>Hello World</code>案例也是类似图二的流程。于是，重复触发可以用<code>while</code>循环表示：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">while</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> elapsedTime</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">6</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">):</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 每隔 6 秒</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  text_trigger</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"mission:naosi_enemyarmada"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 输出文本</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  reinforcement</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"01014514"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # Q7 援军（小队）</span></span></code></pre>
</div><h3 id="_2-4-线性多分支选择结构" tabindex="-1">2.4 线性多分支选择结构 <a class="header-anchor" href="#_2-4-线性多分支选择结构" aria-label="Permalink to &quot;2.4 线性多分支选择结构&quot;"></a></h3>
<p>在触发设计中，有的时候还会希望在某一个节点，根据不同的情况分别做出不同的反应，这就涉及到了选择。选择结构是创建分支流程的重要组成部分，可用于实现<strong>条件发生变化时流程也随之发生改变</strong>的效果<sup>3</sup>。</p>
<p>选择结构其实前面 <a href="#_2-1-触发的逻辑本质">2.1 一节</a>已经介绍过，并且基于触发运行的现象指出触发本质是“条件单分支结构”。由于存在阻塞等待，触发并不会主动判断条件不满足（为假）的情形，所以在实际应用中比起去实现双分支，地图师更倾向于关注<strong>多个条件分支</strong>之间如何组织。</p>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>实际上借助局部变量也可以实现双分支选择，但并不是本章的重点。详见 <a href="#_3-3-1-门电路的搭建">3.3.1 小节</a>。</p>
</div>
<p>当大量的选择结构简单串联之后，实际上就构成了线性多分支结构<sup>3</sup>。而在涉及到多个条件时，不同条件之间的关系也会影响选择结构的实际效果。具体来说，是互斥与否的关系。</p>
<p><strong>互斥</strong>是说，这组条件分支<strong>不可能</strong>同时满足，因此必然只会运行<strong>其中一个</strong>处理程序。用程序语言来说，就是<code>if-elif-else</code>。比如用公式法求一元二次方程，不可能 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi mathvariant="normal">Δ</mi><mo>&gt;</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">\Delta &gt; 0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.68333em;"></span><span class="strut bottom" style="height:0.72243em;vertical-align:-0.0391em;"></span><span class="base textstyle uncramped"><span class="mord mathrm">Δ</span><span class="mrel">&gt;</span><span class="mord mathrm">0</span></span></span></span> 还说方程找不到实根。<br>
<strong>非互斥</strong>是指，多条件中<strong>至少有两条</strong>同时满足，有可能<strong>同时经过若干个</strong>处理程序。比如说重叠区间：</p>
<ul>
<li>若 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>∈</mo><mo>(</mo><mn>0</mn><mo separator="true">,</mo><mn>2</mn><mn>3</mn><mn>3</mn><mo>]</mo></mrow><annotation encoding="application/x-tex">x \in (0, 233]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.75em;"></span><span class="strut bottom" style="height:1em;vertical-align:-0.25em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">∈</span><span class="mopen">(</span><span class="mord mathrm">0</span><span class="mpunct">,</span><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">3</span><span class="mclose">]</span></span></span></span>，则令 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>=</mo><mo>−</mo><mi>x</mi></mrow><annotation encoding="application/x-tex">x = -x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.58333em;"></span><span class="strut bottom" style="height:0.66666em;vertical-align:-0.08333em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">=</span><span class="mord">−</span><span class="mord mathit">x</span></span></span></span>；</li>
<li>（反之）若 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>∈</mo><mo>(</mo><mn>2</mn><mn>2</mn><mn>0</mn><mo separator="true">,</mo><mn>5</mn><mn>1</mn><mn>2</mn><mo>]</mo></mrow><annotation encoding="application/x-tex">x \in (220, 512]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.75em;"></span><span class="strut bottom" style="height:1em;vertical-align:-0.25em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">∈</span><span class="mopen">(</span><span class="mord mathrm">2</span><span class="mord mathrm">2</span><span class="mord mathrm">0</span><span class="mpunct">,</span><span class="mord mathrm">5</span><span class="mord mathrm">1</span><span class="mord mathrm">2</span><span class="mclose">]</span></span></span></span>，则又令 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>=</mo><msup><mi>x</mi><mn>3</mn></msup></mrow><annotation encoding="application/x-tex">x = x^3</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.8141079999999999em;"></span><span class="strut bottom" style="height:0.8141079999999999em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">=</span><span class="mord"><span class="mord mathit">x</span><span class="vlist"><span style="top:-0.363em;margin-right:0.05em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle uncramped"><span class="mord mathrm">3</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span>。</li>
</ul>
<p>如果没有加上那个“反之”，考虑输入一个特殊值 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>=</mo><mn>2</mn><mn>3</mn><mn>0</mn></mrow><annotation encoding="application/x-tex">x = 230</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.64444em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">=</span><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">0</span></span></span></span>：首先 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mn>2</mn><mn>3</mn><mn>0</mn><mo>&lt;</mo><mn>2</mn><mn>3</mn><mn>3</mn></mrow><annotation encoding="application/x-tex">230 &lt; 233</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.68354em;vertical-align:-0.0391em;"></span><span class="base textstyle uncramped"><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">0</span><span class="mrel">&lt;</span><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">3</span></span></span></span>，<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.43056em;"></span><span class="strut bottom" style="height:0.43056em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span></span></span></span> 变换成 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mo>−</mo><mn>2</mn><mn>3</mn><mn>0</mn></mrow><annotation encoding="application/x-tex">-230</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.72777em;vertical-align:-0.08333em;"></span><span class="base textstyle uncramped"><span class="mord">−</span><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">0</span></span></span></span>；随后 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mn>2</mn><mn>3</mn><mn>0</mn><mo>&gt;</mo><mn>2</mn><mn>2</mn><mn>0</mn></mrow><annotation encoding="application/x-tex">230 &gt; 220</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.68354em;vertical-align:-0.0391em;"></span><span class="base textstyle uncramped"><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">0</span><span class="mrel">&gt;</span><span class="mord mathrm">2</span><span class="mord mathrm">2</span><span class="mord mathrm">0</span></span></span></span>，对 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mo>−</mo><mi>x</mi></mrow><annotation encoding="application/x-tex">-x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.58333em;"></span><span class="strut bottom" style="height:0.66666em;vertical-align:-0.08333em;"></span><span class="base textstyle uncramped"><span class="mord">−</span><span class="mord mathit">x</span></span></span></span> 做幂运算得出 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mo>−</mo><mn>1</mn><mn>2</mn><mo separator="true">,</mo><mn>1</mn><mn>6</mn><mn>7</mn><mo separator="true">,</mo><mn>0</mn><mn>0</mn><mn>0</mn></mrow><annotation encoding="application/x-tex">-12,167,000</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.8388800000000001em;vertical-align:-0.19444em;"></span><span class="base textstyle uncramped"><span class="mord">−</span><span class="mord mathrm">1</span><span class="mord mathrm">2</span><span class="mpunct">,</span><span class="mord mathrm">1</span><span class="mord mathrm">6</span><span class="mord mathrm">7</span><span class="mpunct">,</span><span class="mord mathrm">0</span><span class="mord mathrm">0</span><span class="mord mathrm">0</span></span></span></span>。<br>
而一旦加上“反之”，上述处理就是互斥的：<strong>“反之”要求 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mn>0</mn><mo>&lt;</mo><mi>x</mi><mo>≤</mo><mn>2</mn><mn>3</mn><mn>3</mn></mrow><annotation encoding="application/x-tex">0 &lt; x \le 233</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.78041em;vertical-align:-0.13597em;"></span><span class="base textstyle uncramped"><span class="mord mathrm">0</span><span class="mrel">&lt;</span><span class="mord mathit">x</span><span class="mrel">≤</span><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">3</span></span></span></span> 这一条件首先不满足</strong>。对于特殊值 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mn>2</mn><mn>3</mn><mn>0</mn></mrow><annotation encoding="application/x-tex">230</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.64444em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">0</span></span></span></span>，显然 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mn>2</mn><mn>3</mn><mn>0</mn><mo>&lt;</mo><mn>2</mn><mn>3</mn><mn>3</mn></mrow><annotation encoding="application/x-tex">230 &lt; 233</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.68354em;vertical-align:-0.0391em;"></span><span class="base textstyle uncramped"><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">0</span><span class="mrel">&lt;</span><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">3</span></span></span></span> 满足条件，<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.43056em;"></span><span class="strut bottom" style="height:0.43056em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span></span></span></span> 变成 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mo>−</mo><mn>2</mn><mn>3</mn><mn>0</mn></mrow><annotation encoding="application/x-tex">-230</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.72777em;vertical-align:-0.08333em;"></span><span class="base textstyle uncramped"><span class="mord">−</span><span class="mord mathrm">2</span><span class="mord mathrm">3</span><span class="mord mathrm">0</span></span></span></span> 就直接结束了。</p>
<p>由于触发的 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>p</mi><mo>→</mo><mi>q</mi></mrow><annotation encoding="application/x-tex">p \to q</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.43056em;"></span><span class="strut bottom" style="height:0.625em;vertical-align:-0.19444em;"></span><span class="base textstyle uncramped"><span class="mord mathit">p</span><span class="mrel">→</span><span class="mord mathit" style="margin-right:0.03588em;">q</span></span></span></span> 语义无法通过逻辑上互斥实现选择，因此实践中更倾向物理实现这个互斥：一旦某一分支成功触发，就需要<strong>尽快阻止触发其他分支</strong>。实践中通常会考虑 <em>Q54 禁止触发</em>、摧毁选择器等方式，让用户<strong>来不及</strong>进入另外的分支。</p>
<p>比如近年来的子阵营指挥：设有三支部队分别从三个方向开进，你需要<strong>选择</strong>指挥其中一支。这种桥段通常会刷三个假单位充当选择信标，玩家选中一个就把那一路部队更改所属。那么这种情况会在玩家选择之后<em>立刻摧毁其他选择信标</em>（<em>Q119 摧毁所属方</em>）。</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> objSelected</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">BeaconA</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">):</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">  del</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> BeaconA</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> BeaconB</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> BeaconC</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  ...</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># B、C 信标也是一样，就不再展开了。</span></span></code></pre>
</div><p>非互斥的多分支实际上就是<strong>顺序地经过这些条件分支</strong>，如此便又回归顺序结构的处理方式。其中比较典型的设计类似数学上的“大、小前提”。
考虑一个争夺战：玩家与敌军互相争夺油田，但狡猾的敌军可能直接摧毁油田。假如有两个 Phobos 扩展局部变量充当计数器：</p>
<ul>
<li><code>remaining</code>计算地图上还有多少油田。若余量小于玩家应占数目，任务失败；</li>
<li><code>player_owns</code>计算玩家占了多少油田。大于等于应占数目，任务完成。</li>
</ul>
<p>那么当一个油田 <em>P48 被摧毁</em> 时，首先想到余量<code>remaining</code>减一。<em>如果这个油田先前是玩家持有</em>，那就再对<code>player_owns</code>减一。简单的允许触发即可：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> objDestroyed</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">OilA</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">):</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  remaining </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">-=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 编辑变量 (Phobos)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">  if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> oil_A_captured </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">and</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> objDestroyed</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">OilA</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">):</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">    player_owns </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">-=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span></span></code></pre>
</div><h2 id="三、局部变量的程序逻辑分析" tabindex="-1">三、局部变量的程序逻辑分析 <a class="header-anchor" href="#三、局部变量的程序逻辑分析" aria-label="Permalink to &quot;三、局部变量的程序逻辑分析&quot;"></a></h2>
<p>前面的分析都是基于触发系统本身，贴的代码也只是对假想条件和结果的调用。而在地图创作中，除了直接利用已有的条件库，还会用局部变量间接地做条件判断。为了讲清楚它如何参与到触发逻辑当中，不妨引入经典案例“运输船找妈妈”。</p>
<h3 id="_3-1-案例实现思路" tabindex="-1">3.1 案例实现思路 <a class="header-anchor" href="#_3-1-案例实现思路" aria-label="Permalink to &quot;3.1 案例实现思路&quot;"></a></h3>
<p>“运输船找妈妈”简单来说，就是在<strong>乘客还有事要做</strong>的情况下，让运载乘客的<strong>载具打哪来，回哪去</strong>。观察一下 YR S01：时空转移的相关演出，易得流程如下：</p>
<ul>
<li><strong>满载</strong>的运输船从地图外刷出，移动到定点；</li>
<li>等待所有乘客下船。然后乘客有可能还会打光几秒、更改所属方……；</li>
<li><strong>空载</strong>的运输船从定点返回地图外。</li>
</ul>
<p>难点就在于，如何得知<strong>卸出乘客后船是空的</strong>。翻看条件库，好像也没有类似的选项。<br>
于是考虑设<code>apc unload</code>变量初始为 0，然后在运输船卸出乘客之后<em>Q56 设置局部变量</em>，触发得知 <em>P36 局部变量被设置</em> 了，就建一个空船小队把它拉走。</p>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>篇幅原因，具体实现步骤我就不贴出来了。这种实用向教程应该也很容易找到。</p>
</div>
<h3 id="_3-2-案例逻辑分析" tabindex="-1">3.2 案例逻辑分析 <a class="header-anchor" href="#_3-2-案例逻辑分析" aria-label="Permalink to &quot;3.2 案例逻辑分析&quot;"></a></h3>
<p>已知这一经典例子中运用了局部变量，那么引入局部变量后触发逻辑有什么变化呢？</p>
<p>首先考虑局部变量的位置。从语义、代码语法上说，不可能未经定义就使用一个变量——好比解应用题，只设了未知数<code>x</code>却凭空跑出个<code>y</code>来。</p>
<p>在 FA2 的局部变量窗口中，你需要为局部变量起名（声明），然后 FA2 默认会令它等于 0（初始化，当然你可以改这个初始值）。那么这些局部变量保存到地图里肯定也是带着初始值的。<br>
这些局部变量最终又被引擎读进内存，组成数列（或者说数组）。所以，<strong>在游戏开始之前，这些变量就已经就位了</strong>。</p>
<p>那么不妨把局部变量放在程序代码的开头：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">apc_unload </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 0</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 多数编程语言并不允许变量名带空格</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> ...</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  ...</span></span></code></pre>
</div><p>既然声明了局部变量，那么就要用起来。触发里有一组事件和一组结果分别读写局部变量（设待操作的局部变量为 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.43056em;"></span><span class="strut bottom" style="height:0.43056em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span></span></span></span>）：</p>
<ul>
<li>P36：局部变量被设定（为 1），即当 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>=</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">x = 1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.64444em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">=</span><span class="mord mathrm">1</span></span></span></span> 时</li>
<li>P37：局部变量被清除（0），即当 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>=</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">x = 0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.64444em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">=</span><span class="mord mathrm">0</span></span></span></span> 时</li>
</ul>
<ul>
<li>Q56：设置局部变量（值为 1），即令 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>=</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">x = 1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.64444em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">=</span><span class="mord mathrm">1</span></span></span></span></li>
<li>Q57：清除局部变量（值），即令 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>x</mi><mo>=</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">x = 0</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.64444em;"></span><span class="strut bottom" style="height:0.64444em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit">x</span><span class="mrel">=</span><span class="mord mathrm">0</span></span></span></span></li>
</ul>
<div class="info custom-block"><p class="custom-block-title">赋值与相等</p>
<p>在数学中描述等量关系用等号：<code>a = 0</code>时，……。同时，赋值也用等号：令<code>x = 1</code>。<br>
而在编程中二者是不同的运算。为了避免混淆，你经常会看到用<code>==</code>指代相等，用<code>=</code>来赋值。</p>
</div>
<p>那么上述案例便可以用如下伪代码表示：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">apc_unload </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 0</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> elapsedTime</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">20</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">):</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  play_speech</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"EVA_ReinforcementsHaveArrived"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  reinforcement</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"LCRFCome"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> apc_unload </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">==</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  create_team</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"LCRFBack"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # Q4 建立小队</span></span></code></pre>
</div><p>触发就是通过对局部变量值的变动，来实现一些较为复杂的逻辑判断。并且随着 Phobos 扩展了变量系统，触发对变量的判断也不再局限于 0 和 1 的“左手倒右手”，而是与科技类型、超级武器、随机数等联系了起来，实现更加精细的随机机制和流程控制。</p>
<h3 id="_3-3-高阶用法-逻辑门电路" tabindex="-1">3.3 高阶用法：逻辑门电路 <a class="header-anchor" href="#_3-3-高阶用法-逻辑门电路" aria-label="Permalink to &quot;3.3 高阶用法：逻辑门电路&quot;"></a></h3>
<p>触发条件仅以<strong>逻辑“且”</strong> 相连。其阻塞等待决定了它没有类似“否则”的设计，也就莫得直给的逻辑“非”；已知的触发实践也表明，多条件不可能在仅满足其中一个的情况下执行触发，无法直接判断逻辑“或”。那是否说明，触发就是做不到逻辑完备，如同早期面对平台限制一样无解呢？非也。</p>
<p>事实上，依据软硬件的逻辑等价性原理，触发确实可以做到或、非逻辑的判定。但显然，用累加去实现相乘，比起直接列个竖式配合九九乘法表，总是麻烦得多。自行实现的或、非逻辑也一样。</p>
<h4 id="_3-3-1-门电路的搭建" tabindex="-1">3.3.1 门电路的搭建 <a class="header-anchor" href="#_3-3-1-门电路的搭建" aria-label="Permalink to &quot;3.3.1 门电路的搭建&quot;"></a></h4>
<blockquote>
<p>“逻辑智能所有的基础，都不过是这小小的开关。”</p>
<div style="text-align:right">
<p>——ElectroBOOM</p>
</div>
</blockquote>
<p>在 <a href="#_1-2-局部变量">1.2 一节</a>中已知，原版的局部变量只有<code>0</code>和<code>1</code>两种取值，恰如一个“开关”。前面两个小节的实例分析也展示了局部变量作为“开关”，在触发系统中的<strong>辅助判断</strong>作用。有了开关，就可以人为建立起“或”、“非”，乃至更为复杂的逻辑通路。</p>
<p>我们不妨从先前一直遗漏的“条件双分支结构”入手。很显然，双分支摘去一支就退化为了单分支；反过来，双分支的真、假两路恰好是一个开关的两极。那么有没有可能，可以通过局部变量连结两个单分支，组合成一个“条件双分支”呢？这就是接下来要讨论的单刀双掷开关（SPDT）设计。</p>
<p>从 <a href="#_3-2-案例逻辑分析">3.2 一节</a>的介绍可以看出，触发是取局部变量的<strong>瞬时值</strong>做的判断，变量值为<code>0</code>还是为<code>1</code>是两个独立事件。既然<strong>逻辑上</strong>通过局部变量反映了条件的真假，不妨假设经过这个双分支时，该局部变量的值“保持恒定”，于是得出以下的实现思路：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># t0: if-else 双分支：条件判断器</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> elapsedTime</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">114514</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> and</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> ...</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  your_local_var </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># t1: 条件为真时的处理</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> your_local_var </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">==</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  ...</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># t2: 条件为假时的处理</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> your_local_var </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">==</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 0</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  ...</span></span></code></pre>
</div><p>注意前面 2.4 讨论多分支时，还提到要“尽快阻止触发其他分支”。在上面这个简单模型中，令 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">t_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span> 第一时间“禁止”<span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>t</mi><mn>2</mn></msub></mrow><annotation encoding="application/x-tex">t_2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.61508em;"></span><span class="strut bottom" style="height:0.76508em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit">t</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:0em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">2</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span>，反过来也一样，就可以了。</p>
<p>事实上，不单是局部变量，像 YR A03：集中攻击那关争抢电厂的桥段，“电厂是否属于玩家”也可以采用 SPDT 设计，直接判断关联电厂的归属是敌是友，并相应地允许（或禁止）敌军反占电厂的演出。</p>
<p>像这样将条件映射到局部变量，用变量代为影响执行流的“逻辑电路”思想，接下来会多次体现。</p>
<h4 id="_3-3-2-或运算——殊途同归" tabindex="-1">3.3.2 或运算——殊途同归 <a class="header-anchor" href="#_3-3-2-或运算——殊途同归" aria-label="Permalink to &quot;3.3.2 或运算——殊途同归&quot;"></a></h4>
<p>如今的任务有一个利好玩家的设计：如果你实在没钱（假定低于 $100），<strong>或者</strong>你的矿车被打没了，就给你派送几个钱箱子救急，所谓“战争援助”。不妨以这个设计为例。</p>
<p>那首先判断“缺钱”是有现成事件<em>52 金钱低于</em> 的；至于判断“缺少矿车”，在原版中可反向考虑用事件<em>20 生产特定类型的载具</em> 判断玩家生产了矿车。如果任务流程足够简单，玩家和其他 AI 阵营<em>绝不可能生产出相同种类的矿车</em>，也可以用 YR 的事件<em>61 科技类型不存在</em>。</p>
<p>条件输入、结果输出两端都 OK，就可以用局部变量搭建“或门”电路了：</p>
<ul>
<li>设一局部变量<code>player low funds</code>，<strong>初值为 0</strong>；</li>
<li>触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>I</mi><mn>1</mn></msub></mrow><annotation encoding="application/x-tex">I_1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.68333em;"></span><span class="strut bottom" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit" style="margin-right:0.07847em;">I</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:-0.07847em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">1</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span>：若玩家*金钱低于* $100，则令该变量值为 1；</li>
<li>触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>I</mi><mn>2</mn></msub></mrow><annotation encoding="application/x-tex">I_2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.68333em;"></span><span class="strut bottom" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="base textstyle uncramped"><span class="mord"><span class="mord mathit" style="margin-right:0.07847em;">I</span><span class="vlist"><span style="top:0.15em;margin-right:0.05em;margin-left:-0.07847em;"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span><span class="reset-textstyle scriptstyle cramped"><span class="mord mathrm">2</span></span></span><span class="baseline-fix"><span class="fontsize-ensurer reset-size5 size5"><span style="font-size:0em;">​</span></span>​</span></span></span></span></span></span>：若玩家补牛（没有牛车），也令该变量值为 1；</li>
<li>触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>O</mi></mrow><annotation encoding="application/x-tex">O</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.68333em;"></span><span class="strut bottom" style="height:0.68333em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit" style="margin-right:0.02778em;">O</span></span></span></span>：若该变量<strong>值为 1</strong>，则在指定路径点刷出奖励箱子。</li>
</ul>
<p>对应的伪代码如下：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">player_low_funds </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 0</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> creditsBelow</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">100</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">):</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  player_low_funds </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> noMiner</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 取决于你怎么判断玩家缺牛车</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  player_low_funds </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> player_low_funds </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">==</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  create_crate</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">0</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">,</span><span style="--shiki-light:#E64553;--shiki-light-font-style:italic;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic"> waypoint</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387">81</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 参数写 0 表示*大量金钱*</span></span></code></pre>
</div><p>“一真皆真”，这就是或门的精髓。当然 Python 里真正的<code>or</code>有短路特性，这里就不再展开了。</p>
<h4 id="_3-3-3-非运算——逆向思维" tabindex="-1">3.3.3 非运算——逆向思维 <a class="header-anchor" href="#_3-3-3-非运算——逆向思维" aria-label="Permalink to &quot;3.3.3 非运算——逆向思维&quot;"></a></h4>
<p>原版 RA2 A08 有这样一个任务流程：在心灵信标影响到谭雅之前摧毁心灵信标。换言之，要求指定时间内摧毁目标（也就是计时器<strong>没有超时</strong>，并且目标被摧毁）。</p>
<p>在开始之前，不妨先捋一捋这个条件组的逻辑。这实际上是个必要不充分条件：要保证<em>完成流程</em>，必需满足以下两个分条件：（一）没有超时，计时器没有走完；（二）目标被摧毁。但反过来，单纯“没有超时”推不出任务完成——目标可能还在。</p>
<p>由于<em>目标被歼灭</em> 这个分条件不需要非运算，因此重点关注 <em>流程能完成 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mo>⇒</mo></mrow><annotation encoding="application/x-tex">\Rightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.36687em;"></span><span class="strut bottom" style="height:0.36687em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mrel">⇒</span></span></span></span> 没超时</em> 这一分路。这一路条件按照设计得是真命题，原命题为真，其逆否命题也为真：<em>超时了 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mo>⇒</mo></mrow><annotation encoding="application/x-tex">\Rightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.36687em;"></span><span class="strut bottom" style="height:0.36687em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mrel">⇒</span></span></span></span> 流程完不成</em>。这样就得到非运算的关键实现了。</p>
<p>有思路之后打开触发编辑器，现有这些条件与计时器有关：流逝时间、计时器时间到、流逝游戏时间……无不判断一个计时器<em>超时</em>。根据刚刚得到的逆否命题，用局部变量实现“非门”电路：</p>
<ul>
<li>设一局部变量<code>obj1 reachable</code>，<strong>初始为 1</strong>；</li>
<li>触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>I</mi></mrow><annotation encoding="application/x-tex">I</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.68333em;"></span><span class="strut bottom" style="height:0.68333em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit" style="margin-right:0.07847em;">I</span></span></span></span>：若计时器超时，则令该变量值为 0；</li>
<li>触发 <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>O</mi></mrow><annotation encoding="application/x-tex">O</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="strut" style="height:0.68333em;"></span><span class="strut bottom" style="height:0.68333em;vertical-align:0em;"></span><span class="base textstyle uncramped"><span class="mord mathit" style="margin-right:0.02778em;">O</span></span></span></span>：若变量值<strong>仍为 1</strong>（即未超时），且目标不再存在，则宣布任务完成。</li>
</ul>
<p>对应的伪代码如下：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">obj1_reachable </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> timeout</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">:</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 取决于你用 P13 P14 还是 P47</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  obj1_reachable </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 0</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4"> obj1_reachable </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">==</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 1</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7"> and</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA"> techTypeNotExist</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"NAPSYB"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">):</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  play_speech</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1">"EVA_ObjectiveComplete"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2">)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">  ...</span></span></code></pre>
</div><hr>
<p>当然，虽然理想很美好，但从原版一直到纯 Ares（特别是 Hares 平台），100 个局部变量的限制依然不容忽视。对于有限的资源，还是需要做出合理的分配。</p>
<!-- ## 四、触发时序与标签 -->
<h2 id="结论" tabindex="-1">结论 <a class="header-anchor" href="#结论" aria-label="Permalink to &quot;结论&quot;"></a></h2>
<p>综上所述，红警 2 的触发组件在任务设计当中举足轻重，把握其运行的逻辑，有利于互相交流（至少不需要甩一堆截图）、有利于把更精妙的脑洞付诸实践、有利于优秀触发设计的提炼与发掘，对于触发编写乃至地图创作都有重要意义。</p>
<p>触发系统在逻辑层面上类似<code>if</code>单分支语句的设计，使其就入门而言并无太大门槛；支持顺序、选择、循环三种结构，可以实现大部分任务所需的线性叙事。然而其在逻辑运算上又有所欠缺，稍微复合一点的或、非逻辑判断必须通过局部变量绕路实现，对于地图师的逻辑思维能力是一大考验。</p>
<h2 id="参考文献" tabindex="-1">参考文献 <a class="header-anchor" href="#参考文献" aria-label="Permalink to &quot;参考文献&quot;"></a></h2>
<ol>
<li>ModEnc. <a href="https://modenc.renegadeprojects.com/Triggers" target="_blank" rel="noreferrer">Triggers</a> [EB/OL], 1.31.2024, 6.17.2024.</li>
<li>ModEnc. <a href="https://modenc.renegadeprojects.com/VariableNames" target="_blank" rel="noreferrer">VariableNames</a> [EB/OL], 5.16.2024, 6.17.2024.</li>
<li>RN Studio. <a href="https://github.com/revengenowstudio/map_tutorial" target="_blank" rel="noreferrer">map_tutorial</a> [EB/OL], 4.29.2024, 5.6.2024.</li>
</ol>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>便利起见，文献附注格式基于 GB/T 7714 规范作了简化。</p>
</div>
<h2 id="致谢" tabindex="-1">致谢 <a class="header-anchor" href="#致谢" aria-label="Permalink to &quot;致谢&quot;"></a></h2>
<p>时光荏苒，距离最开始做大白板雪原、遭遇战一样的战役地图应该也有九个年头了。能够写到这里，全拜各位大佬、同行、玩家朋友所赐。Lin 在此谢过诸位。</p>
<p>首先，感谢 RN Studio 指导本次“实验”。感谢制作组的地图教程，为“选题”指引方向；感谢 Zero Fanker 等人提出的指导和改进意见，以及触发实际执行的补充、佐证和更正。</p>
<p>其次，感谢曾经与仍在为红警 2 模组创作贡献智慧的各路人才，为论证提供各种帮助。特别感谢地图师同行们对触发系统所作的各种实地测试和表述修订，同时感谢 Phobos 团队开源触发组件的扩展实现，以及 Heli 等人为简化地图开发所做的各种尝试。</p>
<p>最后，感谢各位喜爱战役的红红玩家，特别是《星辰之光》的测试员和玩家朋友们拨冗测试和体验。</p>
<h2 id="后记" tabindex="-1">后记 <a class="header-anchor" href="#后记" aria-label="Permalink to &quot;后记&quot;"></a></h2>
<p>比起“逻辑”，modder 们始终更在乎切实的、能实装进游戏和编辑器的改善。除此之外，红警 2 的触发逻辑本身就与“引擎实现”密不可分。比如：</p>
<ul>
<li>越靠近<code>[Triggers]</code>小节头的触发越容易触发；</li>
<li><em>通过所属方的启动是经由所属方的某个函数执行的，此函数调用为<strong>每 8 帧一次</strong>，根据所属方列表从上而下依次执行。</em>（<a href="https://github.com/handama/FA2sp/releases" target="_blank" rel="noreferrer">FA2spHDM</a> 附文档）</li>
<li><em>包含两个<strong>非持续伴随事件</strong>的触发永远不会启动，因为这两个事件是相继发生的，而不是同时发生的，因此永远不可能同时满足条件。</em>（FA2spHDM 附文档，有关概念参见 <a href="https://ares-developers.github.io/Ares-docs/new/triggerevents.html" target="_blank" rel="noreferrer">Ares 文档</a>）</li>
<li>……</li>
</ul>
<p>凡此种种，都加深了我个人的自我怀疑——我写这些真的有意义吗？<br>
但至少我是很想找到“存在的意义”的。所以哪怕本文多么“空中楼阁”，至少就让它烂在这里吧。</p>
<div style="text-align:right">
<p>04.19.2025</p>
</div>
]]></content:encoded>
            <enclosure url="https://image.woshipm.com/wp-files/2017/08/8Ynwb53uWeo9QMHb7Xi5.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[自组服务：让老机器“物尽其用”]]></title>
            <link>https://agxcoy.shimakaze.org/posts/self-hosted</link>
            <guid>https://agxcoy.shimakaze.org/posts/self-hosted</guid>
            <pubDate>Sun, 10 May 2026 13:45:46 GMT</pubDate>
            <description><![CDATA[
今年的 618 读者们都有过什么盘算呢？换机还是装新机？我嘛……显然是没有条件（米）去换代的，所以如何利用好现有的条件首先就是个问题。之前偶然借助 Hyper-V 得知了 RemoteApps 这项功能，但跑虚拟机[^vm]始终是我个人比较抵触的事情（问就是配置不够分），于是我将目光盯向了 17 年买的老华硕笔记本。这就是咱“自组服务”最初的尝试了。

[^vm]: 这里仅指传统意义的虚拟机，也就是 VMWare、VirtualBox 之类“模拟一套硬件”的硬件抽象层虚拟化。广义的虚拟机实际上就是虚拟化：Java 语言也有我们熟悉的 JVM 虚拟机，这是编程语言层的虚拟。此外还有 QEMU、Wine、Linux Docker 等在别的层级实现的虚拟化。

## 基础设施

首先除非你想要亲自呆在老机器旁边操作，否则肯定需要远程连接到那台老机器上的，也就需要保证**别的设备能和老机器互通**。

如果两台机同在一个内网（比如以 WiFi、有线等方式，设备间连接同一个路由器），那么通常来说需要**固定老机器的 IP**，否则刚搭的“自组服务”可能没几天就飘不知道哪去，得登进路由器里查咯。固定 IP 可以在老机器的系统里操作，等会具体到系统上再分别讨论；一些路由器也支持用户登录进去固定主机的 IP，但我家网关莫得这个功能。

若并非同一内网，则更麻烦亿些。我个人折腾过可行的最简方案是 Zerotier **虚拟组网**，毕竟当时对寄网了解有限，搜**内网穿透**也只能搜出来“喂我花生”之类的东西（我家网关也只支持某某壳子）。

:::: details 配置 Zerotier One
首先当然得有那么个内网。在 [Zerotier](https://my.zerotier.com/login) 上注册/登录账号（可能比较卡，毕竟国外平台），`Create A Network`创建虚拟内网。免费版（截至目前）至多允许同一网络 25 个设备，但对于我来说足够了。

接下来图省事的朋友点进新建的网络，记一下`Network ID`就可以直接安装 Zerotier 客户端了。

::: details 或者，进一步配置虚拟内网
主要关注以下项：

- `Basics` 网络基础设置
  - `Name` 给你的内网重新起个拟人的名字。
  - `Access Control`一般来说保持默认的`Private`（也就是必须你在账号上同意设备加入）即可。
  - `Advanced` 高级设置
    - `Manage Routes`相当于路由表了（左边是被访的内网网段，右边是 Zerotier 虚拟 IP）。  
    可以看看[这篇博客](https://stray.love/jiao-cheng/zerotier-zhong-jie-jiao-cheng)了解一下。
    - `IPv4 Auto-Assign`里挑一个容易记的 IP 池。比如我就选`10.147.17.*`。
    - `IPv6 Auto-Assign`就随意了。说来 IPv6 现阶段不就是公网吗（
    - `DNS`参见[英文文档](https://docs.zerotier.com/dns/)。

然后在设备加入虚拟内网之后，可以在`Members`里找到这个设备，手工给它分配个静态 IP。
:::

<!-- ::: tabs#zerotierInstall -->
::: tabs
== Windows
Windows 安装之后在开始菜单找到 ZeroTier，然后可以看到任务栏里出现了 ZeroTier 的图标。右键图标，在菜单里直接`Join Network`，粘贴刚刚的`Network ID`，回车即可。

顺带一提，记得在右键菜单里勾上`Start UI at Login`，也就是开机启动。

== Linux systemd
可以通过官网提供的[命令行](https://www.zerotier.com/download/#linux)、部分包管理器等方式安装 Zerotier One。

安装之后记得确认一下 Zerotier 服务是否开启：
```bash
sudo systemctl status zerotier-one.service
```
如`systemctl`显示该服务`disabled`、`inactive (dead)`，说明没有启动，启用并立即启动它：
```bash
sudo systemctl enable --now zerotier-one.service
```
于是就可以使用 Zerotier CLI 了。

我的场景比较简单，不考虑自建 moon 的情况下直接加入虚拟内网：
```sh
sudo zerotier-cli join Network_ID
sudo zerotier-cli listnetworks
```

== Docker
[官方文档](https://docs.zerotier.com/docker/)的做法是拉一个 centos 在里面用 systemd 方式安装，属于是脱裤子放屁。

网上有一些现成的镜像，像`zerotier`和`zerotier-synology`（群晖用的），这里摘录一下用`zerotier/zerotier`的流程：

> [!warning]
> **此方案尚未测试，谨慎复制**！建议在其他**有测试结果的**博客、专栏中获取适合的方案！

```sh
docker pull zerotier/zerotier
# 务必注意网络通信方式！
docker run -d --name zt --network host zerotier/zerotier
docker exec -it zerotier bash
zerotier-cli join Network_ID
zerotier-cli listnetworks
```
:::

在设备加入虚拟内网之后，需要再次登上账号同意设备加入（默认建立的是私有内网，需要号主同意加入），并且给它分配固定 IP。
::::

> [!warning]
> 在配置基础设施、远程连接的时候，还请**不要**断开老机器的屏幕——至少等能连上了再拔线。

## Windows——性能分摊

相信大多数人玩电脑接触最多的还是 Windows 系统，那么对于如何使用它，想必都是很有经验的罢。根据老机器配置的不同，它承担负载的多寡也自然各有参差。

### 固定 IP 

- Win11 参考[电脑屋](https://www.diannaowu.com/jc/212.html)的教程；
- Win8.1 及以下通常还是从控制面板固定，也参考[电脑屋](https://www.diannaowu.com/jc/231.html)。
- Win10 除了控制面板法，也可以直接在“设置—网络和 Internet—状态”中找到上网的网络（通常标以太网或者 WiFi），点进底下的属性，往下划拉到 IP 设置那边手动分配 IP。

### 远程连接

白嫖国产软件的免费服务也可以，最常见的当属向日葵了吧。但我是受不了手机端向日葵广告满天飞，所以还是折腾 Windows 自带的 RDP 吧。

欲使用 RDP 连接，需要启用 Windows 的远程控制功能。据微软称家庭版只能它控别人，不能别人控它。但我都 LTSC 2021 起手了，又有何惧。
远程控制选项可在以下两个入口找到（以 Win10、11 为准，不推荐“开发人员设置”）：

- 设置—系统—远程桌面，启用之（顺便根据指引把“睡眠”禁用掉、把“网络发现”打开）；
- 打开“系统属性”[^sysdm]的“远程”选项卡，允许远程连接、勾上“仅允许……”选项（恕我懒得打全称）。

[^sysdm]: 通常是在“控制面板—系统”（早期 Windows）或“设置—系统—关于”（Win10、11）里跳转。也可`Win+R`直接执行`%windir%\system32\sysdm.cpl`。

然后就可以使用 RDP 连接了。控制端可以用`mstsc`（Windows）、Windows App（Android，原“RD 桌面”）、FreeRDP 以及 Remmina（Linux），等等。

::: warning
微软账号现在推行免密登录，取而代之的是用 2FA 工具验证登录码，这种方式[**不适用于**](https://support.microsoft.com/zh-cn/account-billing/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-microsoft-%E5%B8%90%E6%88%B7%E8%BF%9B%E8%A1%8C%E6%97%A0%E5%AF%86%E7%A0%81%E8%AE%BF%E9%97%AE-674ce301-3574-4387-a93d-916751764c43) RDP 连接。  
此外，实测发现，微软文档所述的“应用密码”也无法作为凭据。**推荐用本地、带密码的账户托管老机器，密码留空进行远控需要额外做组策略配置，恕不展开。**
:::

然后该干嘛干嘛。顺带一提，*远控的鼠标一般是不记录位移、只改变坐标的*，也就是说《原神》这类游戏远程玩效果并不佳；但《剑网三》重制版则更建议远程玩，因为接下来讲的 Moonlight 串流没法向它传递键盘输入。

### 串流
这里的串流主要是指类似“云原神”那样的——服务端跑游戏，其他设备接收画面、回传操作的——类似看别人直播的模式。顺带一提，自己直播也是一种串流，只不过方向相反：是你这边把音画上传[^limited_upload]到直播平台。

串流自建云游戏的主要方式是 Moonlight+Sunshine 和 Parsec，其中前者结合 Zerotier 虚拟组网，甚至可以异地云玩。有关的配置方式可以参考[知乎专栏](https://zhuanlan.zhihu.com/p/718510054)或者 B 站搜索“自建云原神”之类的关键词（也可以看看“阿西西”这个 UP 主的魔改 Moonlight 客户端）。咱已经很久没搞了，讲出来也不一定对，就略过吧。

[^limited_upload]: 目前国内的 ISP（又或者运营商）对上传带宽限制得很死，个人通常不会允许大规模的上传（尤其 BT、PT、PCDN，其中以 PCDN 为甚——很多软件都会在运行时偷偷跑上传），否则容易吃传单（据说可以投诉，但我这人懒得扯皮）；企业商宽、专线通常会给多一点，但就极客湾的遭遇来看似乎也仅仅是“一点”，而且价格比较高昂。可能云游戏亏损就是亏损在这吧。

### 文件共享

最简单的就是 Windows 自带的 Samba（SMB）文件共享，也就是右键文件/文件夹属性里边的“共享”选项卡。

此外还有其他参考：

- 网络文件系统 NFS：参见[微软文档](https://learn.microsoft.com/zh-cn/windows-server/storage/nfs/nfs-overview)
- WebDav 服务器（需要 IIS）：参见[微软文档](https://learn.microsoft.com/zh-cn/iis/install/installing-publishing-technologies/installing-and-configuring-webdav-on-iis#enabling-webdav-publishing-by-using-iis-manager)，或参考[《Windows 开启 WebDAV》（少数派）](https://sspai.com/post/78540)

### 其他服务

- 推流机（挂着直播平台的直播软件，开第三方推流接收内网 OBS 推过来的 rtmp 串流）
- 挂下载（主要是各种限速客户端的下载，或者数据量大的下载）
- 自动化脚本
- RemoteApps（迫真“家庭”版 Office，当然还有 Adobe 全家桶，等等） 
- ……

## Linux——Web 接口

Linux 通常还是作为服务器而非日用的系统使用，所以相比费劲折腾 VNC 等远程桌面，不妨就让它挂一些长线运行的服务好了。而既然莫得远程桌面，这些长线服务很显然只剩 WebUI 这一种可视化的选择。当然如果你更偏好 CLI（命令行界面），那也不是不行。

### 固定 IP

不同的发行版主要用到的网络管理工具都不太一样。

- 像 Arch Linux 安装阶段会用`systemd-networkd`分配固定 IP；
- 而 Ubuntu 虽然也有上述服务，但更多在用`Netplan`（参见 [FreeCodeCamp](https://www.freecodecamp.org/chinese/news/setting-a-static-ip-in-ubuntu-linux-ip-address-tutorial/)）；
- 还可以用`ifconfig`做**临时**配置：

```bash
ifconfig  # 查看所有网卡的配置信息
ifconfig eth0  # 查看某网卡的配置信息，如 eth0
ifconfig eth0 172.16.129.108 netmask 255.255.255.0  # 配置网卡的临时生效的IP地址
route add default gw 172.16.129.254  # 配置网关
```

总之，具体问题（发行版）具体分析，像`/etc/sysconfig/network-scripts`这种上古教程还是不要无脑跟了。~~我的 Ubuntu Server 24.04.2 LTS 连`sysconfig`都莫得。~~

### SSH 远程连接

安装`openssh`包，并启用`sshd`服务。多数软件包管理器应该都支持 SSH。以咱的 Ubuntu Server 为例：

```bash
sudo apt update
sudo apt install openssh
sudo systemctl enable --now sshd
sudo systemctl status sshd
```

### 部署服务（分层讨论）

首先说一下分层怎么回事。有些服务可能是通过容器分发的，那么 Docker 就是这些服务的下层，为这些服务提供支持（有点绕口？）；而 Docker、npm 之类通常又依赖国外的源（像 DockerHub、npmjs），于是代理又是容器的下层。~~当然正经分类起来这些都属于应用层。只是细化一下粒度便于讨论。~~

::: details 代理层（以 Mihomo 为例）
Mihomo 可以理解为 Clash 的后继，所幸 Clash 的许多配置还是能兼容的（至少我的机场订阅可以直接覆盖使用）。但更好的办法还是编写配置文件了。

我参考了“虚空终端”[^yuanshen]文档的 GeoX [快捷配置](https://wiki.metacubex.one/example/conf/)：
```yaml
geox-url:
  geoip: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat"
  geosite: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat"
  mmdb: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb"
  asn: "https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb"
```
但遗憾的是在代理尚未配置好的情况下，似乎哪怕是`jsdelivr`镜像都很难获取到 Geo 信息。那么还是手动下载吧。  
实际上只需下载`geoip.dat`并更名`GeoIP.dat`、`geosite.dat`更名`GeoSite.dat`（Linux 对文件名大小写敏感），丢进 mihomo 配置目录即可（也就是`config.yaml`所在位置）。

[^yuanshen]: 文档简介就是这么写的：“‘虚空终端’是一个基于开源项目‘原神’的二次开发版本”。或许是起名灵感，又或者是避嫌。
:::

<!-- ::: note 应用支持层 -->
> [!note] 应用支持层
> 主要就是 Docker。Docker 的安装参见官方文档。出于一些原因，现行很多镜像源并不可靠也并不完整，不如直接配置代理（参见[《如何配置 docker 通过代理服务器拉取镜像》（博客园）](https://www.cnblogs.com/abc1069/p/17496240.html)）。
>
> `pnpm`或者说`node`直接上官网复制 Linux 安装脚本就彳亍，但**不建议去装 Docker 镜像**[^pnpm_docker_image]！
>
> [^pnpm_docker_image]: 个人理解吧……Docker 镜像通常是部署用的，比如 CI/CD runner 跑工作流，需要拉`node`容器生成我这博客的`dist`成品。直接拿来开发个人觉得不太妥。
<!-- ::: -->

::: details 应用层
这块不是重点，各种服务有它自己的参考文档。

- `webmin`：提供 WebUI 以配置服务器的系统，以及监测服务器的性能占用。
- `aria2`与`AriaNg`：提供直链、BT、PT 下载支持。参见[《手把手教你使用 Docker 搭建 aria2+AriaNg，打造自己的离线下载服务器》（博客园）](https://www.cnblogs.com/wqp001/p/14709997.html) [^ariang_baidupan]。
- `jellyfin` `emby`：影音服务器。个人觉得就*资源管理的便利性*而言，Jellyfin 并不算好；但 Emby 白嫖着用也不见得操作有多舒服。
- `vscode-server`：控制端可利用 VSCode 配合 Remote-SSH 插件连上服务器，做些跨平台开发……或者 Linux Native 开发。~~真有人在 Linux 编译 MSVC x86-64 吗？有的话浇我。~~
- `ssh`：Xshell 做些命令行活计，Xftp 做些文件交换活计。
- `nfs`：和前面 Windows 一样，可以挂共享文件系统。
- ……

[^ariang_baidupan]: 自从 !!Pandownload!! 被赐似之后，这种套取直链的油猴插件就多半是骗钱引流的“浑水”了，**不建议尝试**！但当年这玩意能火，或许也说明有些慢速下载获取到合适的直链、搭配代理，确实能提速吧。
:::

## 后记

通篇看下来，我的服务场景都挺简单的，不是吗？说实话……我并不熟悉寄网（哪怕为了跨考自学过 408），平时也没有太多 Web 编程的实践和需求。能简单搭这么一个自用的服务便已经方便我不少了。若是真正的大佬，或许还会利用反代、软路由等种种轮子实现更加 NB 的东西吧。

不过反正我这人也没什么水平，笔记也不过抛砖引玉。菜逼有菜逼的天马行空，简单点写，懂的都懂。
]]></description>
            <content:encoded><![CDATA[<p>今年的 618 读者们都有过什么盘算呢？换机还是装新机？我嘛……显然是没有条件（米）去换代的，所以如何利用好现有的条件首先就是个问题。之前偶然借助 Hyper-V 得知了 RemoteApps 这项功能，但跑虚拟机<sup class="footnote-ref"><a href="#footnote1">[1]</a><a class="footnote-anchor" id="footnote-ref1"></a></sup>始终是我个人比较抵触的事情（问就是配置不够分），于是我将目光盯向了 17 年买的老华硕笔记本。这就是咱“自组服务”最初的尝试了。</p>
<h2 id="基础设施" tabindex="-1">基础设施 <a class="header-anchor" href="#基础设施" aria-label="Permalink to &quot;基础设施&quot;"></a></h2>
<p>首先除非你想要亲自呆在老机器旁边操作，否则肯定需要远程连接到那台老机器上的，也就需要保证<strong>别的设备能和老机器互通</strong>。</p>
<p>如果两台机同在一个内网（比如以 WiFi、有线等方式，设备间连接同一个路由器），那么通常来说需要<strong>固定老机器的 IP</strong>，否则刚搭的“自组服务”可能没几天就飘不知道哪去，得登进路由器里查咯。固定 IP 可以在老机器的系统里操作，等会具体到系统上再分别讨论；一些路由器也支持用户登录进去固定主机的 IP，但我家网关莫得这个功能。</p>
<p>若并非同一内网，则更麻烦亿些。我个人折腾过可行的最简方案是 Zerotier <strong>虚拟组网</strong>，毕竟当时对寄网了解有限，搜<strong>内网穿透</strong>也只能搜出来“喂我花生”之类的东西（我家网关也只支持某某壳子）。</p>
<details class="details custom-block"><summary>配置 Zerotier One</summary>
<p>首先当然得有那么个内网。在 <a href="https://my.zerotier.com/login" target="_blank" rel="noreferrer">Zerotier</a> 上注册/登录账号（可能比较卡，毕竟国外平台），<code>Create A Network</code>创建虚拟内网。免费版（截至目前）至多允许同一网络 25 个设备，但对于我来说足够了。</p>
<p>接下来图省事的朋友点进新建的网络，记一下<code>Network ID</code>就可以直接安装 Zerotier 客户端了。</p>
<details class="details custom-block"><summary>或者，进一步配置虚拟内网</summary>
<p>主要关注以下项：</p>
<ul>
<li><code>Basics</code> 网络基础设置
<ul>
<li><code>Name</code> 给你的内网重新起个拟人的名字。</li>
<li><code>Access Control</code>一般来说保持默认的<code>Private</code>（也就是必须你在账号上同意设备加入）即可。</li>
<li><code>Advanced</code> 高级设置
<ul>
<li><code>Manage Routes</code>相当于路由表了（左边是被访的内网网段，右边是 Zerotier 虚拟 IP）。<br>
可以看看<a href="https://stray.love/jiao-cheng/zerotier-zhong-jie-jiao-cheng" target="_blank" rel="noreferrer">这篇博客</a>了解一下。</li>
<li><code>IPv4 Auto-Assign</code>里挑一个容易记的 IP 池。比如我就选<code>10.147.17.*</code>。</li>
<li><code>IPv6 Auto-Assign</code>就随意了。说来 IPv6 现阶段不就是公网吗（</li>
<li><code>DNS</code>参见<a href="https://docs.zerotier.com/dns/" target="_blank" rel="noreferrer">英文文档</a>。</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>然后在设备加入虚拟内网之后，可以在<code>Members</code>里找到这个设备，手工给它分配个静态 IP。</p>
</details>
<!-- ::: tabs#zerotierInstall -->
<PluginTabs >
<PluginTabsTab label="Windows">
<p>Windows 安装之后在开始菜单找到 ZeroTier，然后可以看到任务栏里出现了 ZeroTier 的图标。右键图标，在菜单里直接<code>Join Network</code>，粘贴刚刚的<code>Network ID</code>，回车即可。</p>
<p>顺带一提，记得在右键菜单里勾上<code>Start UI at Login</code>，也就是开机启动。</p>
</PluginTabsTab>
<PluginTabsTab label="Linux systemd">
<p>可以通过官网提供的<a href="https://www.zerotier.com/download/#linux" target="_blank" rel="noreferrer">命令行</a>、部分包管理器等方式安装 Zerotier One。</p>
<p>安装之后记得确认一下 Zerotier 服务是否开启：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> systemctl</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> status</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> zerotier-one.service</span></span></code></pre>
</div><p>如<code>systemctl</code>显示该服务<code>disabled</code>、<code>inactive (dead)</code>，说明没有启动，启用并立即启动它：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> systemctl</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> enable</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --now</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> zerotier-one.service</span></span></code></pre>
</div><p>于是就可以使用 Zerotier CLI 了。</p>
<p>我的场景比较简单，不考虑自建 moon 的情况下直接加入虚拟内网：</p>
<div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> zerotier-cli</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> join</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> Network_ID</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> zerotier-cli</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> listnetworks</span></span></code></pre>
</div></PluginTabsTab>
<PluginTabsTab label="Docker">
<p><a href="https://docs.zerotier.com/docker/" target="_blank" rel="noreferrer">官方文档</a>的做法是拉一个 centos 在里面用 systemd 方式安装，属于是脱裤子放屁。</p>
<p>网上有一些现成的镜像，像<code>zerotier</code>和<code>zerotier-synology</code>（群晖用的），这里摘录一下用<code>zerotier/zerotier</code>的流程：</p>
<div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p>
<p><strong>此方案尚未测试，谨慎复制</strong>！建议在其他<strong>有测试结果的</strong>博客、专栏中获取适合的方案！</p>
</div>
<div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">docker</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> pull</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> zerotier/zerotier</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic"># 务必注意网络通信方式！</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">docker</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> run</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -d</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --name</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> zt</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --network</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> host</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> zerotier/zerotier</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">docker</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> exec</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> -it</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> zerotier</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> bash</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">zerotier-cli</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> join</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> Network_ID</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">zerotier-cli</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> listnetworks</span></span></code></pre>
</div></PluginTabsTab>
</PluginTabs>
<p>在设备加入虚拟内网之后，需要再次登上账号同意设备加入（默认建立的是私有内网，需要号主同意加入），并且给它分配固定 IP。</p>
</details>
<div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p>
<p>在配置基础设施、远程连接的时候，还请<strong>不要</strong>断开老机器的屏幕——至少等能连上了再拔线。</p>
</div>
<h2 id="windows——性能分摊" tabindex="-1">Windows——性能分摊 <a class="header-anchor" href="#windows——性能分摊" aria-label="Permalink to &quot;Windows——性能分摊&quot;"></a></h2>
<p>相信大多数人玩电脑接触最多的还是 Windows 系统，那么对于如何使用它，想必都是很有经验的罢。根据老机器配置的不同，它承担负载的多寡也自然各有参差。</p>
<h3 id="固定-ip" tabindex="-1">固定 IP <a class="header-anchor" href="#固定-ip" aria-label="Permalink to &quot;固定 IP&quot;"></a></h3>
<ul>
<li>Win11 参考<a href="https://www.diannaowu.com/jc/212.html" target="_blank" rel="noreferrer">电脑屋</a>的教程；</li>
<li>Win8.1 及以下通常还是从控制面板固定，也参考<a href="https://www.diannaowu.com/jc/231.html" target="_blank" rel="noreferrer">电脑屋</a>。</li>
<li>Win10 除了控制面板法，也可以直接在“设置—网络和 Internet—状态”中找到上网的网络（通常标以太网或者 WiFi），点进底下的属性，往下划拉到 IP 设置那边手动分配 IP。</li>
</ul>
<h3 id="远程连接" tabindex="-1">远程连接 <a class="header-anchor" href="#远程连接" aria-label="Permalink to &quot;远程连接&quot;"></a></h3>
<p>白嫖国产软件的免费服务也可以，最常见的当属向日葵了吧。但我是受不了手机端向日葵广告满天飞，所以还是折腾 Windows 自带的 RDP 吧。</p>
<p>欲使用 RDP 连接，需要启用 Windows 的远程控制功能。据微软称家庭版只能它控别人，不能别人控它。但我都 LTSC 2021 起手了，又有何惧。
远程控制选项可在以下两个入口找到（以 Win10、11 为准，不推荐“开发人员设置”）：</p>
<ul>
<li>设置—系统—远程桌面，启用之（顺便根据指引把“睡眠”禁用掉、把“网络发现”打开）；</li>
<li>打开“系统属性”<sup class="footnote-ref"><a href="#footnote2">[2]</a><a class="footnote-anchor" id="footnote-ref2"></a></sup>的“远程”选项卡，允许远程连接、勾上“仅允许……”选项（恕我懒得打全称）。</li>
</ul>
<p>然后就可以使用 RDP 连接了。控制端可以用<code>mstsc</code>（Windows）、Windows App（Android，原“RD 桌面”）、FreeRDP 以及 Remmina（Linux），等等。</p>
<div class="warning custom-block"><p class="custom-block-title">WARNING</p>
<p>微软账号现在推行免密登录，取而代之的是用 2FA 工具验证登录码，这种方式<a href="https://support.microsoft.com/zh-cn/account-billing/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-microsoft-%E5%B8%90%E6%88%B7%E8%BF%9B%E8%A1%8C%E6%97%A0%E5%AF%86%E7%A0%81%E8%AE%BF%E9%97%AE-674ce301-3574-4387-a93d-916751764c43" target="_blank" rel="noreferrer"><strong>不适用于</strong></a> RDP 连接。<br>
此外，实测发现，微软文档所述的“应用密码”也无法作为凭据。<strong>推荐用本地、带密码的账户托管老机器，密码留空进行远控需要额外做组策略配置，恕不展开。</strong></p>
</div>
<p>然后该干嘛干嘛。顺带一提，<em>远控的鼠标一般是不记录位移、只改变坐标的</em>，也就是说《原神》这类游戏远程玩效果并不佳；但《剑网三》重制版则更建议远程玩，因为接下来讲的 Moonlight 串流没法向它传递键盘输入。</p>
<h3 id="串流" tabindex="-1">串流 <a class="header-anchor" href="#串流" aria-label="Permalink to &quot;串流&quot;"></a></h3>
<p>这里的串流主要是指类似“云原神”那样的——服务端跑游戏，其他设备接收画面、回传操作的——类似看别人直播的模式。顺带一提，自己直播也是一种串流，只不过方向相反：是你这边把音画上传<sup class="footnote-ref"><a href="#footnote3">[3]</a><a class="footnote-anchor" id="footnote-ref3"></a></sup>到直播平台。</p>
<p>串流自建云游戏的主要方式是 Moonlight+Sunshine 和 Parsec，其中前者结合 Zerotier 虚拟组网，甚至可以异地云玩。有关的配置方式可以参考<a href="https://zhuanlan.zhihu.com/p/718510054" target="_blank" rel="noreferrer">知乎专栏</a>或者 B 站搜索“自建云原神”之类的关键词（也可以看看“阿西西”这个 UP 主的魔改 Moonlight 客户端）。咱已经很久没搞了，讲出来也不一定对，就略过吧。</p>
<h3 id="文件共享" tabindex="-1">文件共享 <a class="header-anchor" href="#文件共享" aria-label="Permalink to &quot;文件共享&quot;"></a></h3>
<p>最简单的就是 Windows 自带的 Samba（SMB）文件共享，也就是右键文件/文件夹属性里边的“共享”选项卡。</p>
<p>此外还有其他参考：</p>
<ul>
<li>网络文件系统 NFS：参见<a href="https://learn.microsoft.com/zh-cn/windows-server/storage/nfs/nfs-overview" target="_blank" rel="noreferrer">微软文档</a></li>
<li>WebDav 服务器（需要 IIS）：参见<a href="https://learn.microsoft.com/zh-cn/iis/install/installing-publishing-technologies/installing-and-configuring-webdav-on-iis#enabling-webdav-publishing-by-using-iis-manager" target="_blank" rel="noreferrer">微软文档</a>，或参考<a href="https://sspai.com/post/78540" target="_blank" rel="noreferrer">《Windows 开启 WebDAV》（少数派）</a></li>
</ul>
<h3 id="其他服务" tabindex="-1">其他服务 <a class="header-anchor" href="#其他服务" aria-label="Permalink to &quot;其他服务&quot;"></a></h3>
<ul>
<li>推流机（挂着直播平台的直播软件，开第三方推流接收内网 OBS 推过来的 rtmp 串流）</li>
<li>挂下载（主要是各种限速客户端的下载，或者数据量大的下载）</li>
<li>自动化脚本</li>
<li>RemoteApps（迫真“家庭”版 Office，当然还有 Adobe 全家桶，等等）</li>
<li>……</li>
</ul>
<h2 id="linux——web-接口" tabindex="-1">Linux——Web 接口 <a class="header-anchor" href="#linux——web-接口" aria-label="Permalink to &quot;Linux——Web 接口&quot;"></a></h2>
<p>Linux 通常还是作为服务器而非日用的系统使用，所以相比费劲折腾 VNC 等远程桌面，不妨就让它挂一些长线运行的服务好了。而既然莫得远程桌面，这些长线服务很显然只剩 WebUI 这一种可视化的选择。当然如果你更偏好 CLI（命令行界面），那也不是不行。</p>
<h3 id="固定-ip-1" tabindex="-1">固定 IP <a class="header-anchor" href="#固定-ip-1" aria-label="Permalink to &quot;固定 IP&quot;"></a></h3>
<p>不同的发行版主要用到的网络管理工具都不太一样。</p>
<ul>
<li>像 Arch Linux 安装阶段会用<code>systemd-networkd</code>分配固定 IP；</li>
<li>而 Ubuntu 虽然也有上述服务，但更多在用<code>Netplan</code>（参见 <a href="https://www.freecodecamp.org/chinese/news/setting-a-static-ip-in-ubuntu-linux-ip-address-tutorial/" target="_blank" rel="noreferrer">FreeCodeCamp</a>）；</li>
<li>还可以用<code>ifconfig</code>做<strong>临时</strong>配置：</li>
</ul>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">ifconfig</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 查看所有网卡的配置信息</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">ifconfig</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> eth0</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 查看某网卡的配置信息，如 eth0</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">ifconfig</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> eth0</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 172.16.129.108</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> netmask</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 255.255.255.0</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 配置网卡的临时生效的IP地址</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">route</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> add</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> default</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> gw</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387"> 172.16.129.254</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic">  # 配置网关</span></span></code></pre>
</div><p>总之，具体问题（发行版）具体分析，像<code>/etc/sysconfig/network-scripts</code>这种上古教程还是不要无脑跟了。<s>我的 Ubuntu Server 24.04.2 LTS 连<code>sysconfig</code>都莫得。</s></p>
<h3 id="ssh-远程连接" tabindex="-1">SSH 远程连接 <a class="header-anchor" href="#ssh-远程连接" aria-label="Permalink to &quot;SSH 远程连接&quot;"></a></h3>
<p>安装<code>openssh</code>包，并启用<code>sshd</code>服务。多数软件包管理器应该都支持 SSH。以咱的 Ubuntu Server 为例：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> apt</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> update</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> apt</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> install</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> openssh</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> systemctl</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> enable</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> --now</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> sshd</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic">sudo</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> systemctl</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> status</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> sshd</span></span></code></pre>
</div><h3 id="部署服务-分层讨论" tabindex="-1">部署服务（分层讨论） <a class="header-anchor" href="#部署服务-分层讨论" aria-label="Permalink to &quot;部署服务（分层讨论）&quot;"></a></h3>
<p>首先说一下分层怎么回事。有些服务可能是通过容器分发的，那么 Docker 就是这些服务的下层，为这些服务提供支持（有点绕口？）；而 Docker、npm 之类通常又依赖国外的源（像 DockerHub、npmjs），于是代理又是容器的下层。<s>当然正经分类起来这些都属于应用层。只是细化一下粒度便于讨论。</s></p>
<details class="details custom-block"><summary>代理层（以 Mihomo 为例）</summary>
<p>Mihomo 可以理解为 Clash 的后继，所幸 Clash 的许多配置还是能兼容的（至少我的机场订阅可以直接覆盖使用）。但更好的办法还是编写配置文件了。</p>
<p>我参考了“虚空终端”<sup class="footnote-ref"><a href="#footnote4">[4]</a><a class="footnote-anchor" id="footnote-ref4"></a></sup>文档的 GeoX <a href="https://wiki.metacubex.one/example/conf/" target="_blank" rel="noreferrer">快捷配置</a>：</p>
<div class="language-yaml vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">yaml</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">geox-url</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">:</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  geoip</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat"</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  geosite</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat"</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  mmdb</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb"</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA">  asn</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1"> "https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb"</span></span></code></pre>
</div><p>但遗憾的是在代理尚未配置好的情况下，似乎哪怕是<code>jsdelivr</code>镜像都很难获取到 Geo 信息。那么还是手动下载吧。<br>
实际上只需下载<code>geoip.dat</code>并更名<code>GeoIP.dat</code>、<code>geosite.dat</code>更名<code>GeoSite.dat</code>（Linux 对文件名大小写敏感），丢进 mihomo 配置目录即可（也就是<code>config.yaml</code>所在位置）。</p>
</details>
<!-- ::: note 应用支持层 -->
<div class="note custom-block github-alert"><p class="custom-block-title">应用支持层</p>
<p>主要就是 Docker。Docker 的安装参见官方文档。出于一些原因，现行很多镜像源并不可靠也并不完整，不如直接配置代理（参见<a href="https://www.cnblogs.com/abc1069/p/17496240.html" target="_blank" rel="noreferrer">《如何配置 docker 通过代理服务器拉取镜像》（博客园）</a>）。</p>
<p><code>pnpm</code>或者说<code>node</code>直接上官网复制 Linux 安装脚本就彳亍，但<strong>不建议去装 Docker 镜像</strong><sup class="footnote-ref"><a href="#footnote5">[5]</a><a class="footnote-anchor" id="footnote-ref5"></a></sup>！</p>
</div>
<!-- ::: -->
<details class="details custom-block"><summary>应用层</summary>
<p>这块不是重点，各种服务有它自己的参考文档。</p>
<ul>
<li><code>webmin</code>：提供 WebUI 以配置服务器的系统，以及监测服务器的性能占用。</li>
<li><code>aria2</code>与<code>AriaNg</code>：提供直链、BT、PT 下载支持。参见<a href="https://www.cnblogs.com/wqp001/p/14709997.html" target="_blank" rel="noreferrer">《手把手教你使用 Docker 搭建 aria2+AriaNg，打造自己的离线下载服务器》（博客园）</a> <sup class="footnote-ref"><a href="#footnote6">[6]</a><a class="footnote-anchor" id="footnote-ref6"></a></sup>。</li>
<li><code>jellyfin</code> <code>emby</code>：影音服务器。个人觉得就<em>资源管理的便利性</em>而言，Jellyfin 并不算好；但 Emby 白嫖着用也不见得操作有多舒服。</li>
<li><code>vscode-server</code>：控制端可利用 VSCode 配合 Remote-SSH 插件连上服务器，做些跨平台开发……或者 Linux Native 开发。<s>真有人在 Linux 编译 MSVC x86-64 吗？有的话浇我。</s></li>
<li><code>ssh</code>：Xshell 做些命令行活计，Xftp 做些文件交换活计。</li>
<li><code>nfs</code>：和前面 Windows 一样，可以挂共享文件系统。</li>
<li>……</li>
</ul>
</details>
<h2 id="后记" tabindex="-1">后记 <a class="header-anchor" href="#后记" aria-label="Permalink to &quot;后记&quot;"></a></h2>
<p>通篇看下来，我的服务场景都挺简单的，不是吗？说实话……我并不熟悉寄网（哪怕为了跨考自学过 408），平时也没有太多 Web 编程的实践和需求。能简单搭这么一个自用的服务便已经方便我不少了。若是真正的大佬，或许还会利用反代、软路由等种种轮子实现更加 NB 的东西吧。</p>
<p>不过反正我这人也没什么水平，笔记也不过抛砖引玉。菜逼有菜逼的天马行空，简单点写，懂的都懂。</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="footnote1" class="footnote-item"><p>这里仅指传统意义的虚拟机，也就是 VMWare、VirtualBox 之类“模拟一套硬件”的硬件抽象层虚拟化。广义的虚拟机实际上就是虚拟化：Java 语言也有我们熟悉的 JVM 虚拟机，这是编程语言层的虚拟。此外还有 QEMU、Wine、Linux Docker 等在别的层级实现的虚拟化。 <a href="#footnote-ref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote2" class="footnote-item"><p>通常是在“控制面板—系统”（早期 Windows）或“设置—系统—关于”（Win10、11）里跳转。也可<code>Win+R</code>直接执行<code>%windir%\system32\sysdm.cpl</code>。 <a href="#footnote-ref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote3" class="footnote-item"><p>目前国内的 ISP（又或者运营商）对上传带宽限制得很死，个人通常不会允许大规模的上传（尤其 BT、PT、PCDN，其中以 PCDN 为甚——很多软件都会在运行时偷偷跑上传），否则容易吃传单（据说可以投诉，但我这人懒得扯皮）；企业商宽、专线通常会给多一点，但就极客湾的遭遇来看似乎也仅仅是“一点”，而且价格比较高昂。可能云游戏亏损就是亏损在这吧。 <a href="#footnote-ref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote4" class="footnote-item"><p>文档简介就是这么写的：“‘虚空终端’是一个基于开源项目‘原神’的二次开发版本”。或许是起名灵感，又或者是避嫌。 <a href="#footnote-ref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote5" class="footnote-item"><p>个人理解吧……Docker 镜像通常是部署用的，比如 CI/CD runner 跑工作流，需要拉<code>node</code>容器生成我这博客的<code>dist</code>成品。直接拿来开发个人觉得不太妥。 <a href="#footnote-ref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="footnote6" class="footnote-item"><p>自从 <span class="spoiler" tabindex="-1">Pandownload</span> 被赐似之后，这种套取直链的油猴插件就多半是骗钱引流的“浑水”了，<strong>不建议尝试</strong>！但当年这玩意能火，或许也说明有些慢速下载获取到合适的直链、搭配代理，确实能提速吧。 <a href="#footnote-ref6" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
        </item>
    </channel>
</rss>