2024 - 31.05.2025 All Bumped.
Some checks failed
部署文档 / build (push) Failing after 2s

Thanks to the following contributors:
- @snowykami
- @frg2089
- Nya_Twisuki
This commit is contained in:
SilverAg.L 2025-05-31 14:30:19 +08:00
commit 4c90f70fc4
Signed by: AgxCOy
GPG Key ID: DC1A288A6AD79CC5
62 changed files with 10618 additions and 0 deletions

71
.github/workflows/deploy-docs.yml vendored Normal file
View File

@ -0,0 +1,71 @@
name: 部署文档
on:
push:
branches:
- main
paths:
- .github/**
- src/**
- docs/**
- public/**
- package.json
- vuepress.*.ts
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
permissions:
contents: write
statuses: write
env:
MELI_SITE: b9bc0b87-f062-4724-b2f5-55866ff12736
jobs:
build:
runs-on: [ubuntu-latest, public]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# lfs: true
# 如果你文档需要 Git 子模块,取消注释下一行
# submodules: true
- name: 设置 pnpm
uses: pnpm/action-setup@v4
# with:
# version: 8
- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: 23
cache: pnpm
- name: 安装依赖
run: |
corepack enable
pnpm install --frozen-lockfile
- name: 构建文档
env:
NODE_OPTIONS: --max_old_space_size=8192
run: |-
pnpm run docs:build
> dist/.nojekyll
- name: 推送 liteyuki pages
run: |
pnpx @getmeli/cli upload \
--branch main \
--url "https://dash.apage.dev" \
--site "$MELI_SITE" \
--token "$MELI_TOKEN" \
--release "$GITHUB_SHA" \
./dist
env:
MELI_TOKEN: ${{ secrets.MELI_TOKEN }}
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
.cache/
.temp/
dist/

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "针对 localhost 启动 Chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}",
}
]
}

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"files.exclude": {
"**/*.png": true,
"**/*.webp": true
}
}

17
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "docs:dev",
"problemMatcher": [],
"label": "npm: docs:dev",
"detail": "vuepress-vite dev src",
"isBackground": true,
"runOptions": {
"runOn": "folderOpen",
"instanceLimit": 1
}
}
]
}

438
LICENSE-CC Normal file
View File

@ -0,0 +1,438 @@
Attribution-NonCommercial-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-ShareAlike 4.0 International Public License
("Public License"). To the extent this Public License may be
interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the
Licensor receives from making the Licensed Material available under
these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-NC-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution, NonCommercial, and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
l. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
m. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
n. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-NC-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

21
LICENSE-MIT Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Kariko Lin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

17
README.md Normal file
View File

@ -0,0 +1,17 @@
## SilverAg.L 的玩具城
说人话就是咱的个人博客。不考虑英翻NO ENGLISH VERSION!
---
<p align="center">
<img src="https://img.shields.io/badge/license-MIT-blue" alt="license - MIT">
<img src="https://img.shields.io/badge/license-CC--BY--NC--SA--4.0-lightgrey" alt="license - CC-BY-NC-SA-4.0">
</p>
本博客收纳的文章统一采用 CC BY-NC-SA 4.0 协议共享。
**欢迎就本博客上传的「随记」「综述」中的错误、不足之处提出 Issue 批评指正;或者看不顺眼了,丢个 Pull Request也是极好的。**
### 特别鸣谢
- [神羽 @SnowyKami](https://github.com/snowykami)
- [岛风 @frg2089](https://github.com/frg2089)

View File

@ -0,0 +1,5 @@
// you can change config here
$colors: #c0392b, #d35400, #f39c12, #27ae60, #16a085, #2980b9, #8e44ad, #2c3e50,
#7f8c8d !default;
$theme-color: #66ccff;

View File

@ -0,0 +1,66 @@
// place your custom styles here
.vp-blog-hero-description {
text-align: center;
margin: 0.75rem auto 0;
}
.vp-nav-logo {
border-radius: 9999px;
}
.vp-blog-hero-image {
border-radius: 2rem;
}
.vp-blogger-avatar {
border-radius: 1rem;
}
$width: 48rem;
$height: 25rem;
.vp-page.vp-blog-home {
& .vp-blog-mask:before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: min($width, calc(100vw - 2rem));
height: min($height, 100vh);
z-index: 2;
display: block;
transform: translate(max(calc(min($width, calc(100vw - 2rem)) / -2), -50vw),
calc(min($height, 100vh) / -2));
backdrop-filter: blur(1rem);
/* 边框模糊效果 */
/* filter: blur(.25rem); */
border-radius: 1rem;
html[data-theme='dark'] & {
background: #3333337f;
box-shadow: 0.25rem 0.25rem #0000007f;
}
html[data-theme='light'] & {
background: #cccccc7f;
box-shadow: 0.25rem 0.25rem #6666667f;
}
}
& .vp-blog-mask::after {
opacity: unset;
background: unset;
html[data-theme='dark'] & {
background: #0000007f;
}
}
& .vp-blog-hero {
padding: 2rem;
html[data-theme='light'] & {
color: var(--grey-darker);
}
}
}

View File

@ -0,0 +1,3 @@
// force Noto Serif.
$vp-font: '"Noto Serif SC", serif';

16
docs/README.md Normal file
View File

@ -0,0 +1,16 @@
---
home: true
layout: BlogHome
icon: home
title: 主页
heroImage: /assets/images/avatar.webp
heroText: SilverAg.L
heroFullScreen: true
bgImage: https://www.loliapi.com/acg/
tagline: 欢迎光临我的玩具城……♡
---
::: center
Powered by [VuePress Theme Hope](https://theme-hope.vuejs.press/zh/),
Blogs Issued with [CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/).
:::

View File

@ -0,0 +1,476 @@
---
category:
- RA2
- 地图
tag:
- 地图编辑
- 触发
- 局部变量
star: true
---
# 浅谈红警 2 触发组件的运行逻辑
::: warning 观前注意
由于涉及一些编程知识点,本综述可能存在亿些阅读困难。
虽然经过与 Zero Fanker 等人的讨论后决定做些~~修缮~~重写,但难免仍有需要改进之处。
:::
## 绪论
### 研究背景
红警 2 的地图创作中,流程设计是战役最重要的一环,其中触发发挥着举足轻重的作用。好的剧情流程能够让人印象深刻,如何用触发设计出好的流程,就得凭借地图师们的逻辑智慧了。但长久以来,红红的地图教程偏实用性居多,大多数地图师对触发并没有明晰的认识,他们或许在脑内对任务流程有着天马行空的设想,但落到触发实现上往往束手无策。
本文基于已有的触发和编程实践,尝试阐明触发组件的运作**逻辑**,并就这套系统的一些缺陷提出个人的见解,希望能够对各位读者有所启发。如果能稍稍地推进触发设计的简化事业,那就再好不过了。
### 研究目的与意义
本文旨在用程序化的思想阐述触发的运作**逻辑**(而非原理,更不是底层原理),指出这套系统存在的**逻辑**缺陷,并试着给出可行的解决思路。通过对触发**逻辑**的分析,用不同的视角去看待触发,或许可以**一定程度上**简化触发的表达,增强地图师们的逻辑思维,启发他们对优秀设计模型(如状态机)的借鉴、化用,提升剧情的观赏性,为观众们带来更多精彩的“剧目”吧。
::: note 逻辑与实现
必须指出的一点是:逻辑与实现并不等同。逻辑关心客观事物的**本质、规律**,实现则关心符合这个规律的**解决方案**。
:::
## 一、触发组件相关概念
### 1.1 触发
地图触发是早在《命运与征服:泰伯利亚黎明》就引入的系统,负责处理地图当中的“事件”^1^。
一局游戏瞬息万变,其中总有一些**既成的、游戏引擎能感知的事,叫做事件**。比如什么关键建筑被打爆了啊,哪家缺电缺钱了,等等。
这些事件会被触发捕捉到,并驱动后者去执行相应的**行为,也就是游戏引擎能做到的各种效果**:可以是刷兵,改变光照,炸个桥,平地起心灵信标……诸如此类。
再次强调,捕获到的事件、要执行的行为,都仅限**引擎能做到的**范围之内。
::: note 事件和行为
有一些地编调整过用词,事件改称“条件”,行为改叫“结果”。你可以简单这么理解。
:::
<!-- 在红警 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”命题即条件命题非常类似。一个条件命题要能判断其真伪条件就必不可少同样的一个触发要能执行首先要等待前置事件完成。在程序框图中像这种具有前置条件的语义用**条件分支**表示(如下图),又称**选择结构**。
![流程图中的“选择结构”](https://image.woshipm.com/wp-files/2017/08/8Ynwb53uWeo9QMHb7Xi5.png =40%x40%)
从游戏实际运行的现象来看,触发会在条件满足(为真)后执行相应的“处理程序”(也就是行为),这一点符合基本印象。而当条件**尚未满足**时,触发则阻塞等待。
假如**逻辑上**就是希望判断条件不满足(为假),由于触发一直等不到条件满足,则直到游戏结束为止这个条件都得不到任何处理。如此看来,触发的逻辑本质就是如上左图的**条件单分支结构**。
而在程序语言里,这种条件单分支常用`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^。
然而两个触发之间常常在宏观上表现为异步:同样都是延时 10sA 触发和 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$ ,也是如法炮制。以此类推,就形成触发链:
![通过“允许触发”一级一级破坏“禁止”,把链条打通](trigger_chain.webp)
而在程序代码当中,顺序是通过代码行的先后次序来体现的:
```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>早期汉化也直接译作“类型”](fa2_trigger_ui.webp =50%x50%)
通常来说,选择 *2 - Repeating OR* 那一项便足以满足很多简单的重复需求。
::: info 重复类型
事实上,用重复这个概念描述触发(实际上是标签)的类型并不准确。由于本文内容不允许做过多展开,有机会再单独做个“实验”。
:::
而在程序代码中,`while`循环则取代`if`扮演这个执行重复主体的角色:
```python
while True:
print("Hello World")
```
将上述代码粘贴进 Python IDLE 回车运行,你也能看到终端里打出来一行行`Hello World`,除非用任务管理器干掉这个进程,否则它会无休止地输出下去。
分析这个运行表现不难发现,它是类似下图图二的流程:首先它走到`while`处执行判断,由于条件恒满足,往下执行`print`;然后循环并没有结束,它重新回到`while`重复执行前面说的流程,将坏掉的乐土打字机事业推进下去~~爱莉希雅死辣~~。
![流程图中的循环结构](https://image.woshipm.com/wp-files/2017/08/HRej5VdT9M9sRxXBYTJv.png =40%x40%)
上面的`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
:::

12
docs/archives/README.md Normal file
View File

@ -0,0 +1,12 @@
---
title: 综述
index: false
article: false
timeline: false
icon: folder-open
category: ''
---
“综述”这边的博文通常比较正式一些,编排上会参考大学论文、实验报告,当然也经常会有迭代(综述嘛,追求严谨)。
<Catalog />

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/diaries/Ag.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
docs/diaries/Ca.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
docs/diaries/Cl.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

88
docs/diaries/about-me.md Normal file
View File

@ -0,0 +1,88 @@
# 关于我的三个拟设
早在 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+^](Ca.webp =50%x50%)
最早启用的设定。虽然理论上任何钙离子组成的盐都可以指代我,但中学阶段最常见的沉淀果然还是 CaCO~3~ 吧。然后“碳酸钙摘掉两个氧”最初的名字——Caco 就确定下来了。后来又衍生出音译“卡扣”、Casheen 和相应音译“卡伸”。
老朋友们大抵还是愿意叫我“卡”这组名字,特别是接触过的红警 2 modder 和地图师。
猫少年啊……说实话现在回头想想,可能是受到初中同好的影响也说不定。不过印象中被说“可爱”确实是很早就开始了。(话说真的可爱吗?)
总之上图已经是我能找到尽可能完整的立绘了,看上去不需要额外添几笔细致的外貌描写。
> “他呀……是个很可爱的家伙呢。初见看着很腼腆,相谈甚欢了又开始滔滔不绝,和我独处又脸红得像熟透的苹果一样。
> “hmm当初怎么认识的啊……说来有点搞笑。他一开始的个人简介整了个莫名其妙的反应方程然后我看着好玩就和他聊上咯。再然后嘻嘻自己猜去。
> “搞起熟悉的东西来能心无旁骛搞个通宵……啧,有时候还挺羡慕他的。但…再怎么说也稍微陪陪我啊。明明他自己也很喜欢抱抱的说。
> “唉,明明说好了做我的专属的……到头来还是把我抛下了嘛……”
> ::: right
> ——ChlorideP
> :::
## 氯
- 生命周期2022-2025.3
- 种属:幽灵(♀)
- 瞳:黄绿,类人瞳孔
- 代表色:黄绿
- 性格:寡言、孤僻
- 配偶Ca
- *伴侣Ag
- 喜欢的:听故事和讲故事
- 讨厌的:毫无营养的信源
![Cl^-^ 的 Q 版头图](Cl.webp =25%x25%)
在 Caco 因故被一撮原神同人女攻击之后不久,钙的形象弃用,咱也随即改名为 Chloride Pussemi即 ChlorideP 了。而后有人因为末尾这个 P 以为我是 VOCALOID 曲师P 主),加上这个昵称全小写起来并不方便手写,遂又更名为 NyaCl.
氯的种属启发自氯单质(氯气)的物理性质。作为气体,它是有在空中的那种飘飞感的,不难想到幽灵也是在空中飘飞的存在。于是就决定是“黄绿色的幽灵”这样的形态。上图是 Q 版氯,其实还有一版正式立绘,是个大姐姐的形象。~~可惜弄丢了。~~
后来经历的一些事情让我的精神状态逐渐贴合氯气的化学性质:有毒、刺鼻。有时候的确觉得自己到处倒垃圾很困扰人;应激起来说话很大声,不也挺“刺”耳的。(苦笑)
> “你说她?我的食物罢了。知不知道一颗电子对阳离子来说有多么诱人?
> “刚找上她的时候她还在郁郁寡欢呢。结果呢?还不是顺从本能乖乖交粮。(舔唇)
> “不过还别说,强氧化的元素就是不一样,比什么碳酸根的美味多了。听说她已故的另一半也和碳酸根有点关系?乐。
> “后来一来二去的,她似乎也走出来了,愿意和我一起贴贴……嗯哼,就这点来说我还是很喜欢她的。
> “最后倒是和她贴了个痛快。不知道她觉得如何,我是觉得她这结局挺好的。好歹舒服地享受完最后一刻。
> “就是可惜……再也找不到这么好的食物咯……(叹气)”
> ::: right
> ——Ag.L
> :::
## 银
- 生命周期2025.4-
- 种属:兽娘·猫娘(亚种,魅魔混血)
- 瞳色:粉红,爱心瞳
- 代表色:银白(亮白)
- 性格:对外满不在乎,私底下很细腻
- 喜欢的:涩涩(无论主动被动)
- 讨厌的:烦心事
![暂时拿过来充数的猫娘图](Ag.webp =50%x50%)
> [!note]
> 将来有时间和闲钱的话,再为 Ag 这个形象重新约张稿吧。
银这个设定实际上直到去年才给她勾个轮廓——**白丝魅魔猫娘少女**。乍一听这四个词组合在一起很违和。说实话我也觉得(
首先说说“魅魔”吧。既然银这个字有着一层谐音关系(所谓“银梦”嘛),那么我就往涩涩的方向去考虑了。所以给她的定型是魅魔元素。
但一来我说话做不到大多数黄油的魅魔那么“诱人”,二来我养成的猫娘口癖根深蒂固,这就决定了**立绘主体仍然是猫娘**。那么怎么办呢?那就把猫尾巴替换成魅魔尾巴罢。“猫娘身上”的“魅魔尾巴”,很奇特的组合对吧(
剩下来的要素就很简单了:白丝一般显得清纯(相对地黑丝会显得诱惑一些),穿在“摇着魅魔尾巴的猫娘”身上增添些反差感;少女嘛……可以保留一些卡哇伊的感觉。(但实际想象起来好像更偏雌小鬼一点)
设定上魅魔尾巴非常敏感,只是碰触就会浑身战栗的程度。若是摸上那么一两下说不定就开始发情了吧。

View File

@ -0,0 +1,16 @@
---
date: 2024-12-31
article: false
---
# 炒冷饭:刻晴裤袜
第一张公开放出的女装(?)图。
> 感觉还是黑巧克力适合我(?)
> 白丝一是太胖了驾驭不住,二是腿毛没怎么修剪,容易露馅。
袜子淘宝随便买的,我也没什么钱和经验去仔细挑。
拍的时候主包可能已经 85+kg 了。最近有在减肥喵,但说实话对于女装,我不抱希望。
![刻晴裤袜](../../public/dress/keqing-202501.webp =60%x60%)

12
docs/diaries/readme.md Normal file
View File

@ -0,0 +1,12 @@
---
title: 随想
index: false
article: false
timeline: false
icon: paper-plane
category: ''
---
可能是小作文(或者说牢骚),可能是女装(不过主包太肥了,大概只能拍些并不好看的腿子),也可能是乱七八糟。
<Catalog />

15
docs/friends/index.md Normal file
View File

@ -0,0 +1,15 @@
---
layout: FriendsHome
icon: link
index: false
title: 友链
heroText: SilverAg.L
heroFullScreen: true
article: false
---
> [!note]
> ::: center
> 因服务迁移,现需要移步 Profile Repo [提交友链申请](https://github.com/AgxCOy/AgxCOy/issues),并待我核验添加。
> 非常抱歉给你带来不便。
> :::

View File

@ -0,0 +1,382 @@
---
category:
- 操作系统
- Linux
tag:
- Arch Linux
- KDE
- Wayland
- X11
star: true
---
# Arch Linux 个人安(折)装(腾)流程
<!-- https://www.glowmem.com/archives/archlinux-note -->
<!-- https://arch.icekylin.online/guide/-->
::: details 我选择 Arch 的理由
1. 比起“服务”,我还是更倾向于把操作系统当作纯正的工具。可能这就是旧信息时代遗老吧。
2. “缘,妙不可言”。
3. 具体工作具体分析吧。跨平台开发 Arch 也挺舒服的。但 Adobe 全家桶就显然不适合了。
:::
其实这篇笔记说是“流程”,更像是“避坑指南”。因为文中大部分实际操作步骤都是直接贴的参考外链。
我只是为了补充些注意事项,免得以后重装起来忘掉而已。
> [!important]
> 由于 Arch 更迭速度比较快,下面的参考链接以及这篇笔记本身的内容可能随时失效。
> 在安装、使用过程中遇到的,这里没有提及的问题,还请自行 Google、Bing 或 Baidu。
>
> 如果你觉得 Arch 滚动更新很累、玩不太明白,不妨还是先上手`Pop!_OS`或者`Ubuntu`
>
> 此外,也可以多多留意其他人总结的 Arch 折腾小技巧,说不定会有意外收获。
## 参考链接
本文有参考以下两篇安装教程:
1. [律回彼境Arch Linux 折腾指南&记录](https://www.glowmem.com/archives/archlinux-note)(以下简称「律回指南」)
2. [Nakano MikuArch 简明指南](https://arch.icekylin.online/guide/)以下简称「Miku 指南」)
我的笔记**以律回 MelodyEcho 的流程为主**,偶尔穿插一下 Miku 指南。
## 零、前期准备
这部分无需赘述,主要工作就是**下载 Arch 安装镜像**,并把它**刻录到 U 盘里**。
网上对此已有很多成熟的教程,恕不在这里浪费时间了。但有两件事稍微还是要注意一下。
首先是主板设置^1^。如今的主板应该都允许使用 UEFI 了,故本篇笔记也不会考虑传统 BIOS 引导,你需要**确保主板是 UEFI 启动**
除此之外,**需要关闭“安全启动”**Secure Boot。UEFI 的这一安全机制会拒绝执行未签名的启动文件而不幸的是Arch 食用的 Linux 内核以及 Grub 等加载器的确没有签名,并且签名的“手续”很麻烦。
其次是你的 WiFi 名字^2^。在 Arch 的 LiveCD维护环境下同大部分安装步骤都需要手敲命令来完成并且**无法输入、显示中文**。
如果你打算用 WiFi 连接,不妨提前更一下名。
> [!tip]
> 如果只是迁移系统,那么进入维护环境之后只需`rsync`做全盘搬运即可(当然前提是目标**盘**要比原**系统**的实际占用空间要大)。可参见 [lin.moe](https://lin.moe/tutorial/2020/04/arch_migrate/)。
## 一、联网并设置时区
U 盘启动 PE 相信很多人都操作过或者看过教程。LiveCD 也是这么启动的。但与 Ubuntu 可选联网不同Arch 的安装**必须联网**。
本章可直接阅读律回指南[第一章「连接网络和时区配置」](https://www.glowmem.com/archives/archlinux-note#toc-head-2)。直至 6 月初此法仍然可用(
## 二、硬盘分区、格式化和挂载
由于`ext4`调整分区大小并不是很方便,我改用了`Btrfs`文件系统,备份则采用`RSYNC`异盘同步和`Btrfs`同盘快照。你仍可以参照律回指南[第二章「分区、格式化与挂载」](https://www.glowmem.com/archives/archlinux-note#toc-head-3)去操作。
::: details 补充一下律回的 fdisk 操作流程
> 注:下列内容只考虑全新安装。
首先需要知道你要操作哪块硬盘:`lsblk`命令可以以树形结构呈现出当前都连着什么硬盘,都分了什么区。
然后`fdisk /dev/<disk_id>`进入操作控制台,比如`fdisk /dev/nvme0n1`
在控制台里可以敲`m`看帮助。清空重新分区大致的操作如下(是的,都是单字母喔):
- `g`:新建 GPT 分区表。
- `d`:如果你不想动分区表,那么就用 d 一个一个删除分区吧。
- `n`:用 n 逐个添加新分区。
> [!tip]
> 其中,`n`之后需要指定这是第几个分区,以及分区多大。
> 分区的大小默认用扇区表示,你可以改用`+120G` `+256M` `+512K`这种我们更熟知的`GB` `MB` `KB`单位。
> 如果你原先的硬盘不是“未分配”状态(也就是使用过,分过区),那么新建时可能会提示你删除文件系统标识。`y`允许即可。
- `t`:对于 EFI 引导分区,需要借此更改分区类型为 EFI System即 ESP
> [!info]
> 此举是出于兼容考虑。诚然较新的主板可以搜索任意 FAT 分区中的 EFI 文件但旧主板未必Windows 的`bootmgr`未必。
> 当然也有些固件可能只认 Windows 的`bootmgfw.efi`,如此不妨试试 [UKI](./ArchUEFI.md#统一内核映像-uki)。
- `w`保存并应用分区表更改。DiskGenius 改分区也不是设置完立马生效的嘛。
:::
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)
> [!tip]
> 我个人的分区方案是:
> - ESP挂载`/boot`
> - 系统分区:挂载`/`;若是 Btrfs另开子卷挂载`/home`
> - 交换分区
>
> 这么分可以相对灵活地调整交换分区,因为总体来说调整分区右端比调整左端容易。
分区用`fdisk``cfdisk`都可以。完事了格式化、挂载文件系统即可。两篇指南对此都有叙述。
## 三、安装系统
我个人偏向于律回指南。但在正式安装之前,还有一些事要做。
### 3.1 pacman 镜像源配置
Linux 的包管理器默认用的国外的软件源,`pacman`也一样。因此,非常建议先换用国内镜像,加快包下载速度。
编辑`/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 安装过程中被复制过去。这样后续就不需要再做一遍换源了。
### 3.2 包管理器配置
默认`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`进去进一步配置时,你需要重复做一遍上述操作。
### 3.3 正式部署
在换源、调整设置之后,`pacman -Sy`更新软件库,然后你就可以安装最新版的 Arch Linux 了。
具体的安装步骤参见律回指南的[第三章「安装系统」](https://www.glowmem.com/archives/archlinux-note#toc-head-4)
和[第四章「系统基本配置」](https://www.glowmem.com/archives/archlinux-note#toc-head-5)两章。以下仅作补充。
> [!note]
> 确保`pacstrap`只执行一次——我不清楚重复刷入系统会不会误覆盖些啥东西。
::: warning AUR 助手
Miku 指南强调过“不要(在 LiveCD 里)提前配置`archlinuxcn`源”,而`yay` `paru`等 AUR 助手恰好是 CN 源提供的软件包。
所以直到重启进入新系统之前,也千万不要试图安装 AUR 助手喔。
:::
## 四、系统基本配置
跟着律回指南的三、四章装好系统之后,重启登入新系统的终端。你现在应能通过`nmtui`连上 WiFi 了。
### 4.1 CN 源和 AUR 助手
在**联好网的新系统**里配置`archlinuxcn`源:`sudo nano /etc/pacman.conf`
```ini
# 末行添加
[archlinuxcn]
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch
```
并安装 CN 源的签名密钥和 AUR 助手:
```bash
sudo pacman-key --lsign-key "farseerfc@archlinux.org" # 为密钥环添加本地信任
sudo pacman -S archlinuxcn-keyring # 安装密钥环
sudo pacman -S yay paru # 安装 AUR 助手
```
::: info 关于本地信任 Key
简单来说就是给 CN 源密钥环签名的`farseerfc`他的 Key 掉信任了,包管理器“不敢”安装这个密钥环^2^。
:::
### 4.2 硬件(一)音频安装
音频分为固件(或者说驱动)和管理套件两部分:
```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
```
::: tip 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 仍需要这个包。
:::
显卡、蓝牙等其他硬件设施需要在装好桌面环境后再考虑。至少**到本小节为止你的系统里并没有蓝牙服务**,无法启用。
## 五、KDE 桌面环境
跟完我的第三章,律回指南的三、四章,还有我的第四章之后,你便拥有了一个无 GUI 的终端 Arch 系统。
但作为日常使用的话,图形桌面肯定必不可少。
本文与那两篇参考外链一样**采用 KDE 桌面环境**。当然除了 KDE 之外,你也可以考虑 GNOME 桌面环境 ~~(只是我用腻了)~~
也可以考虑散装方案(比如`hyprland`~~,只是我没装成功~~)。
::: info KDE 6 vs KDE 5
目前最新版本为 KDE 6。但律回指南发布于 23 年 11 月,介绍的是 KDE 5。
话虽如此,倒也不必惊慌。`pacman`以及`yay` `paru`之流均**默认安装最新版**,以下**安装 KDE 5 的步骤仍可用于安装 KDE 6**
```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
```
:::
重启后在用户登录界面输入密码回车,恭喜你,距离投入日常使用只剩几步之遥了。之后对 KDE
和系统的配置**大部分**仍可参考律回指南的[第六章「桌面环境配置」](https://www.glowmem.com/archives/archlinux-note#toc-head-7)。
### 5.1 关于 Wayland 和 X
KDE 的图形实现默认已经是 Wayland 了。在开机后输入用户密码的界面处,找找屏幕边角,你可以看到默认选用`Plasma (Wayland)`
点击它,你可以选择换用`Plasma (X11)`
尽管 X11 有个「锁屏黑屏」的问题,但目前来说我还是推荐换回 X11。
::: note X11 锁屏黑屏
KDE 的默认 Breeze 主题锁屏时大概率会出现黑屏、惟有鼠标的现象。在 7 月中旬时已经发现该现象已经蔓延到自定义主题了。查了下 Google 以及 Arch、Manjaro、KDE 的一些讨论帖,尚没有有效的解决方案。
当然有一些主题可能能够解除这个“病征”,像我之前装的 Nordic Dark以及现在的 Lavanda。这种 work around 可能还是因人而异。
:::
### 5.2 硬件(二)显卡驱动与蓝牙
> “so NVIDIA, F**K YOU! ”——Linus Torvalds
AMD 或 NVIDIA 显卡可参见律回指南[6.4 小节「显卡驱动安装」](https://www.glowmem.com/archives/archlinux-note#toc-head-11)
和 Miku 版指南的[新手进阶—显卡驱动](https://arch.icekylin.online/guide/rookie/graphic-driver.html)篇。
但我是锐炬核显捏,只需要在 Konsole 终端里`sudo pacman -S`安装图形 API
- `mesa` `lib32-mesa`OpenGL
- `vulkan-intel` `lib32-vulkan-intel`Vulkan
- `intel-media-driver`VAAPI 解码器OBS 需要)
如果有蓝牙的话,在 Konsole 里启用(并立即启动)蓝牙服务:
```bash
sudo systemctl enable --now bluetooth
```
> 之前误以为`bluetooth`是 Arch 本身就有的服务,结果发现是桌面环境依赖了蓝牙组件包。
### 5.3 额外中文字体和输入法
律回指南安装的字体分别是 Noto 系列Linux 常用的 Unicode 字体)和思源系列(也算是 Noto 系列的子集)。
其中 Noto 系列的汉字部分由于一些神秘的原因,不做额外配置的话,渲染出来只能说……能用。
::: info 参考资料
- [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
`wqy-zenhei`^extra^(文泉驿)和`misans`^aur^会在安装过程中自动帮你配置 fontconfig因此安装完这两款字体之后系统默认用这些字体显示。
如果你希望使用未经适配的字体,那么需要在 KDE 设置里装好字体后,额外做字体配置。
示例:[思源系列字体配置](../../shared/01-Prefer.conf) By [@Vescrity](https://github.com/Vescrity)
注意:用户级字体配置需放在`~/.config/fontconfig/conf.d`目录中。
另注:使用`fc-cache -vf`刷新字体缓存。
:::
至于输入法,律回指南推荐安装搜狗拼音`fcitx-sogoupinyin`^aur^
但它在星火 Wine 版网易云里似乎无法调出来,最近也无法安装(会卡在`build()`开头):
```bash
tar (child): data.tar.gz: Cannot open: No such file or directory
```
Miku 指南的[输入法介绍](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)则推荐直接安装`fcitx5`
::: warning 可能的版本冲突
若先前配置搜狗拼音失败,你需要排查并移除已经安装的 Fcitx 4 组件,它与`fcitx5`冲突:
```bash
# 查询本地local已安装的软件库
sudo pacman -Qs fcitx
# 逐个移除,以 fcitx 为例
sudo pacman -R fcitx
```
不用递归移除`-Rs`的原因是,递归可能移除掉你不希望干掉的包依赖。
:::
至此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 28800
pinentry-program /usr/bin/pinentry-qt
```
保存后重启`gpg-agent``gpg-connect-agent reloadagent /bye`
经测试,大部分终端均能在 SSH 连接中调出 CUIVSCode Remote-SSH 打开的终端可能比较特殊,仍然无法签名。个人还是建议单独开个终端作为 workaround。
### 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`)。

242
docs/notes/OS/ArchUEFI.md Normal file
View File

@ -0,0 +1,242 @@
---
category:
- 操作系统
- Linux
tag:
- Arch Linux
- Grub
- UEFI
- UKI
---
# Arch Linux UEFI 启动二三事
由于我对 Linux 乃至整个 UEFI 的启动机制尚且“浅尝辄止”,本文并不会展开很多硬核内容,只是对我个人使用过的启动方案做个总结。
::: tip 引导和启动
在维基百科中二者似乎是同一概念,搜索“启动程序”会跳转到[“引导程序”](https://zh.wikipedia.org/wiki/%E5%95%9F%E5%8B%95%E7%A8%8B%E5%BC%8F)的介绍。
> 另见英文维基:[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 引导那么脆弱(
### 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`,这两个 EFI 除了文件名以外并无区别。
> 前者即回退路径启动项,作为启动 Windows 的后备方案。
## 启动加载器(以 Grub 为主)
这也是最广泛使用的启动方式 ~~Windows 也干了~~。在 Linux 当中,最常用的加载器是 Grub。当然也有使用 rEFInd 的。
启动加载器bootloader本身作为跳板被 UEFI 固件加载后,需要根据配置找到真正的 Linux 内核,并经由内核引导用户硬盘上的 Arch 系统。而在 Windows 中,`bootmgfw.efi`会根据`BCD`配置文件,执行硬盘其中一个 Windows 副本中的`winload.exe`,并将该副本的其余加载流程交给它完成。
正常使用 Windows 单系统的用户可能对启动过程并无察觉,因为 Windows 为了确保能够启动,会时不时刷新启动项。但一旦与 Linux 混用,你就需要**留意 Linux 的加载器会不会被 Windows 刷下去(甚至被覆盖)**。除此之外,尽管因“机”而异,但 UEFI 固件**有可能会自动清理不再可用的启动项**。比如重新插拔固态,有可能会出现掉引导的情况。因此就个人来说,我不会再考虑 bootloader 了。
### i. 修复 Grub 引导
Windows 启不动我们会尝试修复引导Arch 亦然。修复 Grub 引导实际上就是**重走 Grub 安装流程**
- `mount`挂载相应分区;
- `genfstab`重建挂载表(如有必要);
> 个人建议无论如何都重建一遍`fstab`。反正刷完绝对是最新的。
- `arch-chroot`切换进硬盘上的系统;
- `grub-install`重建 grub 引导。
### ii. 补充回退启动项
事实上,需要反复重建 Grub 引导的一大原因就在于Grub 只会写入它自己的`grubx64.efi`,以及原生启动项:
![群友的 ESP 分区目录树](./esp_without_bootARCH.png =25%x25%)
那么办法也很简单:像 Windows 那样也建一个回退路径启动项。具体来说,在 ESP 分区里建立`EFI\BOOT`目录,复制`grubx64.efi`重命名成`bootx64.efi`嘛。~~Windows 不也干了(~~
当然如果是像图中那样不止一个 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 固件会无视外部传入的其余参数。
### ii. 预设文件
编辑`/etc/mkinitcpio.d/linux.preset`。我们前面说过“不走寻常路”,关键就在这里。
下面是我自用的`linux.preset`。~~想走寻常路的话还是抄别人的预设吧。~~
```properties
# mkinitcpio preset file for the 'linux' package
ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-linux"
PRESETS=('default' 'fallback')
#default_config="/etc/mkinitcpio.conf"
#default_image="/boot/initramfs-linux.img"
default_uki="/efi/EFI/BOOT/bootx64.efi"
#default_uki="/efi/EFI/Linux/arch-linux.efi"
#default_options="--splash /usr/share/systemd/bootctl/splash-arch.bmp"
#fallback_config="/etc/mkinitcpio.conf"
#fallback_image="/boot/initramfs-linux-fallback.img"
fallback_uki="/boot/arch-linux-fallback.efi"
#fallback_uki="/efi/EFI/Linux/arch-linux-fallback.efi"
fallback_options="-S autodetect"
```
> [!warning]
> 有的教程会出现`ALL_microcode=(/boot/*-ucode.img)`这一句。
> 这种指定微码的方法已经废弃,`/etc/mkinitcpio.conf`已经有针对微码的 Hook 了,无需手动指定。
正常来说 UKI 均会输出到`/efi/EFI/Linux/arch-linux*.efi`,据称`systemd-boot`可以扫到并引导 UKI。
但生成`bootx64.efi`让固件去加载不更直接?所以我选择直接导出到回退路径,如此连`efibootmgr`都不需要了。
另一方面,`fallback`也会生成 UKI`bootx64.efi`只有一个。注意到之前 EFIStub 一节我只给`initramfs-linux`构建了原生启动项,那么`fallback`自然丢回`/boot`里不管它。
> [!note]
> 把`fallback`移回`/boot`还有一重原因:尽量缩减 ESP 分区体积——本来`/efi`设计上也有减小 ESP 分区大小的意图。
> 我试跑完发现`fallback.efi`高达 140+MB`arch-linux.efi`不过 30MB。
>
> 当然,保险起见,我并没有尝试`default`生成 UKI、`fallback`仍生成内存盘的做法,也没有试图把`fallback`预设直接干掉。
> 如有勇士试验过一切正常,欢迎 Issues 反馈(
### iii. 创建映像
按需建立路径,并跑一遍生成:
```bash
mkdir -p /efi/EFI/BOOT/
mkinitcpio -p linux
```
还是那句话,想循规蹈矩的不要学我。
---
建完之后退出系统,重启按快捷键进入启动菜单,这下该有你硬盘的 UEFI 回退路径启动项了:
```
HDD: PM8512GPKTCB4BACE-E162
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -0,0 +1,220 @@
---
category:
- Linux
- RA2
tag:
- Wine
- Bottles
- 虚拟环境
---
# 在 Linux 中游玩《星辰之光》
因为一些机缘巧合,现在我改用 Linux 作为主力系统了。然鹅地图我还是得做的,我又没有两台电脑搞远程,那如何在 Linux 里游玩红红、制作地图首先就成了问题。
当然有人马上会想到虚拟机,比如熟悉的 VirtualBox、VMWare比如`winapps`。但虚拟机也好,双系统也罢,都太重量级了,我 128GB 的二手盘不堪重负。
那都玩 Linux 了Wine 兼容层怎么样呢?不也有过 Wine 跑《原神》的成功案例了嘛。但遗憾的是,自 9.17-1 版本开始,**原生 Wine** 的 DDraw 兼容都做得十分甚至九分的抽象,无论是红警 2 本体还是 FA2 地图编辑器都无法正常显示,原生 Wine 也就不再适用了。
> 现阶段也不再建议用 Wine 玩原神,容易被封号。可以试试云原神,但首先需要解决“鼠标无法转动视角”的问题。
所以,兜兜转转,还是回到了 Bottles。
::: important
本篇笔记仅以《星辰之光》这个红警 2 模组作为范例,因为它是我这里最早成功跑起来的红警 2 mod。
对于其他 mod乃至其他游戏和 Windows 程序,本篇笔记的方案可能有一定参考价值,**但不保证能够成功运行**。
:::
::: note 图片大小
本篇笔记的插图原图对于电脑端来说会偏大一些,因此我基本上都做了缩小处理——你可以点击图片查看原图。
如果您在用移动设备阅读,则这种“缩小”效果可能更明显些。还请见谅。
:::
那么正式开始之前,我有必要先说一下我的 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 安装,在沙箱里运行。但由于「懒加载」问题,游戏本体和地编都无法正常启动。因此还是改用`pacman`吧。
::: info 懒加载
经实测发现,单文件 exe 才可以在这种情况下直接在 Bottles 里启动。
但凡需要读同级文件、子文件夹的,都需要在 Bottle 里添加快捷方式,并在快捷方式的设置里手动指明工作目录。
:::
首先需要引入`archlinuxcn`源。具体步骤参见[《Arch 安装流程》](../OS/ArchInstall.md#_4-1-cn-源和-aur-助手),这里不再重复。
接着`sudo pacman -Sy bottles`安装。等待进度跑完,就可以从“应用程序菜单栏”运行了。
初次运行 Bottles 会弹出一个向导跟你 blabla无脑下一步即可。
到最后一步时 Bottles 会下载一些组件包。由于众所周知的原因,可能会花费比较长的时间。
![Bottles 主界面](bottles_main.webp =50%x50%)
## 二、部署 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](./bottles_new_venv.webp =50%x50%)
然后在右上角点击“创建”即可。
> [!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 详情](bottle_preferences.webp =65%x65%)
> [!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](linux_bottles_ES.webp)
::: info 再次启动客户端没有反应
可能是因为进程还驻留在 Wine 环境当中,需要“强制停止所有进程”手动干掉:
![位于详情页标题栏的“电源”图示](bottle_kill_proc.webp =103x87)
:::
## 附录:关于 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"
```

View File

@ -0,0 +1,301 @@
---
category:
- 逆向工程
- RA2
tag:
- Syringe
- 进程注入
---
# 基于 FA2sp 的逆向小记
参考资料:
- 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 完成了历史使命。
然鹅FA2sp 的主要开发者 [@secsome](https://github.com/secsome)(书伸)虽然承诺把扩展功能和 bug 修复移植到官方源码中,但由于三次元原因该计划迟迟未能推进。
考虑到《星辰之光》这边仍有对 FA2 的扩展需求,我便当起了接盘侠,继续 [FA2sp](https://github.com/ClLab-YR/FA2sp) 的维护。
那既然接了盘,总该干点事不是。于是才疏学浅的我结合自己备考 408 的粗略理解,尝试研究书伸留下来的逆向成果——`finalalert2yr.exe.idb`
## 复习一下寄组
> [!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 函数调用,那么栈帧可能会是这种分布:
|地址|...|备注|
|-|:--:|-|
|0x520|`main()`的 EBP|`example()`栈帧从这里开始|
|0x51C|int a = 10|
|0x518|int b|
|0x514|(空余 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`
当然了前面说过,书伸还没搬运完 FA2sp 的功能修复,改源码暂时没什么意义。
:::
> 开始之前赞美一下书伸,书门!(
> 没有书伸的成果,我不可能很快找出待修改函数的虚拟地址。
在 32 位 IDA 里新建一个反编译项目,打开 FA2 的主程序。我们案例要淦的函(方)数(法)位于`0x501D90`,在菜单栏`Jump`里找到`Jump to address`,把这个地址复制进去确认。
默认它会切换为 Graph View你需要右键改为 Text View
![IDA 默认的图表模式](ida_graph_view.webp)
往下翻到`.text:00501E58`,注意到`GetWindowTextA`这个 WinAPI。如果你翻看了上面的源码就会发现我们离目标不远了。
> [!tip]
> 引用的 API比如说 WinAPI 或者 CString 类的 API地址通常都比较靠后。
> 在 Text View 里双击那个`GetWindowTextA`,可以发现地址跑到`0x553134`去力(瞄完可以用工具栏上的左箭头返回我们正文看的位置)。
> 所以接下来不要认错函数调用咯。
借着上面的提示,同屏`GetWindowTextA`后面只剩两个怀疑对象:`sub_43C3C0``sub_43EA90`
![找出附近的函数调用](ida_find_calls.webp)
接下来看看这两个嫌疑函数的特征。直接菜单栏`View``Open subviews``Generate pseudocode (F5)`生成反汇编代码,于是我们得到案例方法的 C 式伪代码:
![辨认嫌疑伸](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`(空指令,什么也不做)填充。
![西瓜猫猫头](https://imgs.aixifan.com/content/2020_7_22/1.5954261313865685E9.gif =150x150)
:::
### 注意事项
我们这里针对的是函数调用,需要注意 C++ 的函数执行完成后**会触发栈区局部变量的析构函数**(通常是空间回收),因此并不建议把传参的汇编指令也给覆盖掉。
就这个例子而言,只需覆盖`call`指令:
> [!tip]
> 在 IDA 选项(`Options` > `General`)里,右上角勾选`Stack Pointer`,把`Number of opcode bytes`改为 8确认即可看到机器码视图。
> 然后你就会发现`call`指令刚好 5 个字节。
>
> ![call 指令的机器码](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`逆向成果自行意会。虽然函数调用的基本原理在计组那一块已有涉及,
但一个函数叫什么名字、里面什么寄存器对应什么变量,这些都是前辈们自行逆向出来的结论。对此,咱还是保留点最起码的尊重罢。

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

12
docs/notes/README.md Normal file
View File

@ -0,0 +1,12 @@
---
title: 笔记
index: false
article: false
timeline: false
icon: book
category: ''
---
笔记这边类似 B 站、知乎的“专栏”,有能成篇的发现就会考虑写写。
<Catalog />

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "chloridep-blog",
"version": "1.0.0",
"description": "ChlorideP Personal Blogs",
"license": "MIT",
"type": "module",
"scripts": {
"docs:build": "vuepress-vite build docs",
"docs:clean-dev": "vuepress-vite dev docs --clean-cache",
"docs:dev": "vuepress-vite dev docs",
"docs:update-package": "pnpm dlx vp-update"
},
"devDependencies": {
"@vuepress/bundler-vite": "2.0.0-rc.19",
"@vuepress/plugin-docsearch": "2.0.0-rc.76",
"@vuepress/plugin-pwa": "2.0.0-rc.76",
"katex": "^0.16.21",
"vue": "^3.5.13",
"vuepress": "2.0.0-rc.19",
"vuepress-theme-hope": "2.0.0-rc.71"
},
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
}

7752
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

1
public/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" class="icon" viewBox="0 0 3280.944 2800"><path fill="#41b883" d="M1645.332 601.004h375.675L1081.82 2238.478 142.636 601.004h718.477l220.708 379.704 216.013-379.704z"/><path fill="#41b883" d="M142.636 601.004l939.185 1637.474 939.186-1637.474h-375.675l-563.51 982.484-568.208-982.484z"/><path fill="#35495e" d="M513.188 601.004l568.207 987.23 563.511-987.23h-347.498l-216.013 379.704-220.708-379.704zM1607.792 1311.83l594.678 2.293 187.353-316.325-598.662 2.292zM2198.506 1909.57C2867.436 732.7 2939.502 605.426 2937.874 603.78c-.715-.723 45.303-1.314 102.262-1.314s103.562.428 103.562.951c0 .523-208.57 367.978-463.491 816.567L2216.715 2235.6l-102.1.596-102.102.596z"/><path fill="#41b883" d="M1680.563 2233.328c0-1.34 168.208-298.145 440.375-777.048a4135645.775 4135645.775 0 00337.619-594.19l146.13-257.25 170.746-.04 170.747-.04-5.536 9.741c-3.044 5.358-43.727 77.302-90.407 159.875-85.356 150.992-337.562 595.163-656.602 1156.373l-172 302.559-170.536.588c-93.795.322-170.536.069-170.536-.567z"/><path fill="#35495e" d="M1429.783 1625.351l594.679 2.292 187.353-316.324-598.662 2.292z"/><path fill="#41b883" d="M1524.207 1464.903l608.285 6.877 173.746-320.909h-619.072z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>sans-serif</family>
<prefer><family>Noto Sans CJK SC</family></prefer>
</alias>
<alias>
<family>serif</family>
<prefer><family>Noto Serif CJK SC</family></prefer>
</alias>
<alias>
<family>monospace</family>
<prefer><family>Noto Sans Mono CJK SC</family></prefer>
</alias>
</fontconfig>

26
src/friends.json Normal file
View File

@ -0,0 +1,26 @@
{
"#5": {
"link": "https://blog.shimakaze.dev",
"icon": "https://blog.shimakaze.dev/avatar.webp",
"desc": ".NET 壬,也会一些前端。比我厉害。",
"name": "frg2089 岛风"
},
"#6": {
"icon": "https://twis.uk/avatar.png",
"name": "Twisuki 苏阳",
"link": "https://twis.uk",
"desc": "一只喜欢到处贴贴的猫。"
},
"#7": {
"link": "https://sfkm.me",
"desc": "轻雪工作室主要负责人。非常厉害。",
"name": "SnowyKami 神羽",
"icon": "https://q.qlogo.cn/g?b=qq&nk=2751454815&s=640"
},
"#8": {
"desc": "可爱的 Python Dev 和音乐人律姐姐~",
"name": "MelodyEcho 律回",
"link": "https://glowmem.com/",
"icon": "https://github.com/AiCorein.png"
}
}

6
src/friends.ts Normal file
View File

@ -0,0 +1,6 @@
import { ThemeBlogHomeProjectOptions } from 'vuepress-theme-hope'
import friendsJson from './friends.json' with { type: 'json' }
const friends: ThemeBlogHomeProjectOptions[] = Object.values(friendsJson)
export default friends

View File

@ -0,0 +1,60 @@
<template>
<BlogWrapper>
<div class="vp-page vp-blog">
<BlogHero>
<template #info="{ tagline, isFullScreen, text, alt }">
<HitokotoBlogHero v-if="text" :text="text" :alt="alt" />
</template>
<template #bg>
<BingHeroBackground />
</template>
</BlogHero>
<div class="blog-page-wrapper">
<main id="main-content" class="vp-blog-main friends round-avatar">
<DropTransition appear :delay="0.16">
<div class="theme-hope-content">
<ProjectPanel :items="friends" />
</div>
</DropTransition>
<DropTransition appear :delay="0.28">
<MarkdownContent />
</DropTransition>
</main>
<DropTransition appear :delay="0.4">
<InfoPanel />
</DropTransition>
</div>
</div>
</BlogWrapper>
</template>
<script lang="ts" setup>
import BlogHero from 'vuepress-theme-hope/blog/components/BlogHero.js'
import BlogWrapper from 'vuepress-theme-hope/blog/components/BlogWrapper.js'
import InfoPanel from 'vuepress-theme-hope/blog/components/InfoPanel.js'
import ProjectPanel from 'vuepress-theme-hope/blog/components/ProjectPanel.js'
import MarkdownContent from 'vuepress-theme-hope/components/MarkdownContent.js'
import { DropTransition } from 'vuepress-theme-hope/components/transitions/DropTransition.js'
import BingHeroBackground from 'vuepress-theme-hope/presets/BingHeroBackground.js'
import HitokotoBlogHero from 'vuepress-theme-hope/presets/HitokotoBlogHero.js'
import 'vuepress-theme-hope/modules/blog/styles/home.scss'
import friends from "../friends"
</script>
<style lang="scss">
main.friends.round-avatar {
.vp-project-card {
img.vp-project-icon {
border-radius: 9999px;
height: 2em;
}
width: calc(50% - 40px);
@media (max-width: 959px) {
width: 100%
}
}
}
</style>

13
src/navbar.ts Normal file
View File

@ -0,0 +1,13 @@
import { navbar } from "vuepress-theme-hope";
export default navbar([
"/",
"/archives/",
"/notes/",
"/diaries/",
{
text: "友链",
icon: "heart",
link: "/friends/",
},
]);

27
src/sidebar.ts Normal file
View File

@ -0,0 +1,27 @@
import { sidebar } from "vuepress-theme-hope";
export default sidebar({
"/": [
// "",
{
text: "综述",
icon: "folder-open",
prefix: "archives/",
children: "structure",
},
{
text: "笔记",
icon: "book",
prefix: "notes/",
children: "structure",
},
{
text: "随想",
icon: "paper-plane",
prefix: "diaries/",
children: "structure",
},
// "intro",
// "friends/"
],
});

168
src/theme.hope.ts Normal file
View File

@ -0,0 +1,168 @@
import { hopeTheme } from "vuepress-theme-hope";
import navbar from "./navbar.js";
import sidebar from "./sidebar.js";
export default hopeTheme(
{
hostname: "https://agxcoy.shimakaze.org",
author: {
name: "SilverAg.L",
url: "https://github.com/AgxCOy",
},
logo: "/assets/images/avatar.webp",
// repo: "AgxCOy/blogs", // now becomes private.
docsDir: "docs",
// 导航栏
navbar,
// 侧边栏
sidebar,
// 页脚
footer: "喵?……嗯哼♡",
displayFooter: true,
// 博客相关
blog: {
description: "希望你我都能……“玩”得尽兴♡",
// intro: "/intro.html",
medias: {
BiliBili: "https://space.bilibili.com/301413212",
Email: "mailto:caclx@outlook.com",
GitHub: "https://github.com/AgxCOy",
Steam: "https://steamcommunity.com/id/silveraglin/",
},
},
// 加密配置
// encrypt: {
// config: {
// "/demo/encrypt.html": ["1234"],
// },
// },
markdown: {
align: true,
alert: true,
attrs: true,
codeTabs: true,
component: true,
demo: true,
figure: true,
footnote: true,
imgLazyload: true,
imgSize: true,
include: true,
mark: true,
plantuml: true,
spoiler: true,
stylize: [
{
matcher: "Recommended",
replacer: ({ tag }) => {
if (tag === "em")
return {
tag: "Badge",
attrs: { type: "tip" },
content: "Recommended",
};
},
},
],
sub: true,
sup: true,
tabs: true,
tasklist: true,
vPre: true,
// katex
math: true,
},
// 多语言配置
metaLocales: {
editLink: "在 GitHub 上编辑此页",
},
// 如果想要实时查看任何改变,启用它。注: 这对更新性能有很大负面影响
// hotReload: true,
// 在这里配置主题提供的插件
plugins: {
blog: {
excerptLength: 0,
},
icon: {
assets: "fontawesome-with-brands"
},
docsearch: {
apiKey: '665b95419abc56fb78211160bc331a81',
appId: 'JDO4FJXBV7',
indexName: 'agxcoy-shimakaze'
},
components: {
components: ["Badge", "VPCard"],
},
// 如果你需要 PWA。安装 @vuepress/plugin-pwa 并取消下方注释
pwa: {
favicon: "/favicon.ico",
cacheHTML: false,
cacheImage: false,
appendBase: true,
apple: {
icon: "/assets/icon/apple-icon-152.png",
statusBarColor: "black",
},
manifest: {
icons: [
{
src: "/assets/icon/chrome-mask-512.png",
sizes: "512x512",
purpose: "maskable",
type: "image/png",
},
{
src: "/assets/icon/chrome-mask-192.png",
sizes: "192x192",
purpose: "maskable",
type: "image/png",
},
{
src: "/assets/icon/chrome-512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "/assets/icon/chrome-192.png",
sizes: "192x192",
type: "image/png",
},
],
// shortcuts: [
// {
// name: "Demo",
// short_name: "Demo",
// url: "/demo/",
// icons: [
// {
// src: "/assets/icon/guide-maskable.png",
// sizes: "192x192",
// purpose: "maskable",
// type: "image/png",
// },
// ],
// },
// ],
},
},
},
},
{
"custom": true
}
);

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"resolveJsonModule": true
},
"include": [
"vuepress.*.ts",
"src/**/*.ts",
"src/**/*.vue"
],
"exclude": [
"node_modules"
]
}

6
vuepress.client.ts Normal file
View File

@ -0,0 +1,6 @@
import { defineClientConfig } from 'vuepress/client'
import FriendsHome from './src/layouts/FriendsHome.vue'
export default defineClientConfig({
layouts: { FriendsHome },
})

48
vuepress.config.ts Normal file
View File

@ -0,0 +1,48 @@
import { defineUserConfig } from "vuepress";
import theme from "./src/theme.hope.js";
export default defineUserConfig({
base: "/",
lang: "zh-CN",
title: "SilverAg.L",
description: "his personal blogs",
temp: ".temp",
cache: ".cache",
public: "public",
dest: "dist",
theme,
// 和 PWA 一起启用
shouldPrefetch: false,
head: [
// 导入相应链接
["link", { rel: "preconnect", href: "https://fonts.googleapis.com" }],
[
"link",
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" },
],
[
"link",
{
href: "https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400..700&display=swap",
as: "font",
crossorigin: "",
},
],
[
"link",
{
href: "https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400..700&display=swap",
rel: "stylesheet",
as: "font",
crossorigin: "",
media: "print",
onload: "this.media='all'",
},
],
],
});