OpenPAL3:做一个开源版本的仙剑三

《仙剑奇侠传三》是我小时候最喜欢的游戏之一了(另一个是《新剑侠情缘》),沉迷神鬼妖怪世界无法自拔,只有发烧卧床不起才能阻挡我玩游戏的热情。ummm 当时发烧算是原因之一吧,另一个重要原因是玩到锁妖塔四层过不去了_(:з」∠)_

前一阵大概是因为偶然刷到 OpenDiablo2 让我突然想到,为什么不来做一个 OpenPAL3 呢?让自己喜欢的游戏重新活一次,也是很有意思的一件事。如果顺利的话,说不定还能扩展一点私货,比如做个战棋战斗模式?不过这都是后话了,得先把基本的原始游戏功能做出来才行。

所以我就踏上了收集当前前沿科技进展的旅途。首先发现的是 @zhangboyang/PAL3patch 这个仓库,虽然名字叫仙剑三分辨率补丁,其实还包含了相当多的其他功能。最关键的是,这里提供了一个便捷的脱壳工具,为我们分析程序扫除了障碍,可行度++。

另外一个值得一提地方是仙剑三高难度贴吧。贴吧不愧是互联网中文社区的扛把子,这个高难度吧里面聚集了很多做仙剑三 mod 的神犇,全都是人肉编辑二进制的资源文件,拷来改去还能不出错。贴吧里有一些分析目录结构的帖子,是入门上手的好地方。还有一些关于游戏机制和数值的分析,但对文件格式的解析比较少见。

另外还搜罗到几篇零散的文章,其中一个是看雪论坛的一篇帖子,分析了仙剑三剧情文件的格式以及压缩包的压缩格式。另一个是一个转贴,原博客已经年久失修找不到了,这个帖子简单分析了仙剑三的一种模型文件的格式,不过尚不全面。最后一个是开源的仙剑三解包工具,实际试用一下,还有一些 Bug,解压出来的文件可能会有不正确的地方。

最后的最后,是藏在仙剑三游戏资源里面的意外惊喜。解包出来的文件当中,有个别文件是游戏开发过程中误打包进来的,比如剧情脚本的源代码(一种非常简单的类似于汇编的语言),还有少许说明性的文本文件,不过提供的信息并不是很多。

这些就是目前巨人肩膀的高度了,剩下的就得靠自己了。首先是先定一个小目标,否则步子太大容易扯到蛋:目标就定为能够播放游戏开头景天跟雪见登场的那一小段剧情好了。

为了实现这个目标,我们得需要:

  • 本着自娱自乐的原则,引擎肯定是要自撸的,所以至少得有一个能凑活用的引擎
  • 模型文件格式的解析,能加载模型是最基本的要求了
  • 场景文件格式的解析,得能把景天的小屋子搭出来
  • 剧情脚本文件可以暂时不用完全读出来,毕竟那一小段剧情不长,把剧情脚本硬编码一下也是可以的

大概就需要这么多的工作吧,这样就可以实现第一个小目标了。从过年前后开始开工算起,到现在大概做了两个半月了,如果从撸引擎开始算要更早一点——现在我已经完成第一个小目标啦!虽然这篇文章发在四月一号,但这不是愚人节忽悠,不是愚人节忽悠,不是愚人节忽悠!

https://zhuanlan.zhihu.com/p/122532099

各位放心,你们的耳机没有坏,是我还没有加声音;文本框也是临时用 Imgui 默认的框框对付了一下,立绘表情也还没贴,场景的环境光也还没有设置(本来应该是烛光烁烁的夜晚😅)……目前还只是 Demo 的程度,非常多的硬编码,相当多的技术债,还有很多文件格式上的问题没有弄清楚。不过不管怎么说,小目标算是勉强达成了,就把它叫做 v0.1 吧。

在达成小目标期间,也是要做一点辅助工具的,比如模型浏览器:

永安当场景静态模型
景天 – 人物顶点动画

场景浏览器:

渝州西南

那么接下来的第二个小目标的话,就定为能够跑出永安当吧。要达成第二个小目标,需要解决的问题非常多,输入控制、声音、碰撞、事件触发机制还都没做,大概没有几个月搞不定,道阻且长 _(:з」∠)_

仓库在此,欢迎 Star 🧐 https://github.com/dontpanic92/OpenPAL3​

Rust 面向对象 FFI

说到 FFI,FFI to C 大概算得上是事实标准了。Rust 也提供了 to C 的 FFI,但如果我们想把 Rust 结构包装成对象提供给其他语言使用,事情就没那么简单了。


一种方法是完全依赖于源语言的对象模型,典型的例子是 SWIG。SWIG 是一个 C/C++ 的 Wrapper Generator,通过 parse C/C++ 头文件、结合不同的后端,可以输出不同语言的绑定。它的思路是比较简单的:C++ 中的成员方法全部包装为 extern C 的普通函数,接收 C++ 对象的指针;目标语言这边的对象只携带一个 C/C++ 对象的指针就可以了。我之前用 SWIG 做过一个 Wrapper,wrap 了 wxWidgets(一个 C++ 库)给 golang 用,比较头疼的是:

  • 内存管理。一般的 C++ 类都不会自带引用计数,这时候怎么处理裸指针就很头疼,每个函数的返回值和参数都需要(人工)分析出对象的所有权是否转移,以便决定需不需要在 Go 这一侧调用 delete。同时,如果所有权在 C++ 一侧,如何判断对象是否已经释放,仍然比较难办。如果 C++ 一侧的库完全使用智能指针还好一些,但这件事情高度自由,不可强求。
  • ABI 问题。上面提到了这种方法依赖于源语言的对象模型;但不仅如此,我们还依赖于编译器所使用的 ABI。例如,如果库是 gcc 编译的,wrapper 也需要是 gcc;甚至 gcc 的 Library ABI 的变化,也会使我们受到影响。
  • SWIG 的前端后端都是在一起的,中间表示就是 AST,除了直接加代码也无法自行扩展。如果要做 wrapper,最好是能够自己控制 wrapper generator。比如,Go 语言不支持重载,那 C++ 的重载怎么办?如果我们想要的与 SWIG 的不同、或是有自己特殊的情况需要处理,就需要自己 fork 了——这时候,所有与后端不相干、但又不得不知道的东西,就会成为负担。
  • (改 SWIG 的那个 Go 后端真的很难受

目前 Rust 似乎还没有 SWIG 这样工具,只找到一个 cbindgen,可以生成 C/C++ 头文件。C++ 头文件的话,比较有特色的是能够支持模板,但 impl 之类的还是无法放进 class 里面去。


另一种方法是,在源语言一侧,先手工包装并导出为一个标准的、跨语言的对象模型。谈到跨语言的对象模型,跟我有过交集的也就两个:一个是 COM,一个是 GObject。

先聊一下 GObject 吧,由于用过 GtkSharp,对 GObject 多少有一些了解。Rust 的 Gtk 绑定 gtk-rs 提供了方便手段,可以让我们在 Rust 里面 subclass GObject 类型,但这还是不满足我们的需求,因为它并不能支持在 Rust 中定义并导出新的类型给其他语言使用。

能够实现这个目的的,是一个叫做 gnome-class 的库。(作者居然是 Gnome 的 Co-Founder 😮)它能够生成 GObject 规则的 C 语言导出函数,同时可以生成 GIR 文件(GObject Introspection Repository)。

GIR 描述了 GObject 的类型,很多动态类型语言都有 gobject-introspection 库的支持,它能够直接读取经过编译后的 GIR(叫做 typelib)。也就是说,只要有了 GIR,理论上不再需要任何的其他步骤就可以在 Python/Ruby/Lua/Javascript 里面使用了我们的库了。

对于静态类型的语言,还是需要有代码生成的步骤的。不过一般来说,只要这门语言有 Gtk3 的绑定,多半都会有代码生成器的,比如 Rust/Haskell/D 之类的语言。

我实际试了一下这个库,目前还远远谈不上可用,但在几经 hack 之后终于能够正常在 Python 里面 Subclass 一个 Rust 的 struct 了:

Rust 一侧,我们需要把 Wrapper 写在一个 macro 里。class/virtual 什么的语法稍稍显得有点奇怪;还有一点点我 hack 的痕迹:

gobject_gen! {
    #[generate("generated/application.rs")]
    #[generate("generated/application.gir")]
    class Application: GObject {
        application: GiApplication,
    }

    impl Application {
        pub fn initialize(&self) {
            self.get_priv().application.0.borrow_mut().callbacks_mut().g_object 
                = <Self as glib::translate::ToGlibPtr<'_, *mut ApplicationFfi,>>::to_glib_none(self).0;
            self.get_priv().application.0.borrow_mut().initialize();
        }

        virtual fn on_updated(&self, _delta_time: i32) {
            println!("in application on_updated");
        }

        pub fn run(&self) {
            self.get_priv().application.0.borrow_mut().run();
        }
    }
}

生成了 GIR 之后,使用 g-ir-compiler 把它编译为 typelib,就可以直接在 Python 里面导入了:

from gi.repository import Application

class App(Application.Application):
    def __init__(self):
        super().__init__()
   
    def do_on_updated(self, delta_time):
        print("delta_time " + str(delta_time))

a = App()
a.initialize()
a.run()

至于 COM,最常见的就是 DirectX 的 API 了。可能大家觉得它是 Windows-Only 的技术,但其实 COM 作为 Object Model 来说仍然是跨平台的。微软自己有一个 com-rs,还有一个支持跨平台的 intercom。大概是因为 COM 全部都是接口导出,要比 GObject 简单一点,所以只需要在 Rust struct 上加一个 Attribute Macro 就可以了。这是一个 intercom 的例子,相比于上面的 gnome-class 简直轻松了一个档次:

pub use intercom::*;

#[com_library(Calculator)]

#[com_class(Calculator)]
struct Calculator {
    value: i32
}

#[com_interface]
impl Calculator {
    pub fn new() -> Calculator { Calculator { value: 0 } }

    pb fn add(&mut self, value: i32) -> ComResult<i32> {
        self.value += value;
        Ok(self.value)
    }
}

还是有不少语言支持 COM 的,至少 .net 上的语言都会比较容易;其他的比如 D 也支持直接与 COM 组件交互。


一大圈下来,似乎导出为 COM 用起来最方便,只是在目标语言一侧支持力度没有 GObject 那么大,也不知道实际用起来有没有坑。FFI 终究是一件很繁琐的事啊😥

当 Neovim 遇上 Acrylic

dotnvim 是很久之前开的坑了,当时是想做一个颜值在线的 Windows neovim 前端。但其实我很少单独使用 vim,用也多半是在别的 IDE 里面用 vim 插件,所以这个项目也没怎么更新过;而且新的 Windows Terminal 完善之后,大概 dotnvim 存在的意义又下降了一分。但今天又打开看了一下,发现颜值其实还是挺不错的,居然又燃起了我间歇性使用 vim 的欲望⚠顺便再写一遍文章回忆一下当时开发时遇到的坑🤔

没有可用的 C# neovim 客户端

虽然当时在 neovim Wiki 的 Related Projects 里面有列出一个 C# 客户端,但实际看过就会发现功能欠缺很多,几乎处于不可用的状态。所以只好自己再造一个轮子了。neovim 与前端的通信使用的是 message pack,是一种很容易理解的协议。整个客户端的实现也很简单,就是 neovim 说什么,我们照做就行了——比如光标移动到哪里、颜色设置成什么、显示什么字符之类的。

但是只看 neovim 的 文档 还是会不明所以,所以搞不懂的时候就只能去翻官方的 Python 版或者是 Qt 版的客户端代码了。

Acrylic Blur

一开始我是打算做成 UWP 的,毕竟亚克力效果只有 UWP 才支持。起初考虑 nvim 需要自己单独一个进程,担心 UWP 不支持,结果是我多虑了。但后来翻了一下 UWP 的权限问题,似乎保存文件到任意位置的唯一方法只有弹出一个文件选择框,这与 vim 用户们的习惯大相径庭。最后就还是决定做成一个 Win32 的程序。

这样一来,如何实现 Acrylic Blur 就是个问题了。不过好在当时已经有很多关于 Windows 未公开的 API SetWindowCompositionAttribute 的资料了,所以自己琢磨一下试试也不难。(画外音:Windows 的兼容性负担就是被你们这群人搞上去的😀

当时还有很多采用自绘的方案,但跟原生的亚克力效果还是无法做到一致的。至于如何让整个窗体全都被透明特效覆盖到,这就属于 DwmSetWindowAttribute 和 DwmExtendFrameIntoClientArea 的工作了,这两个 API Aero 的时候就有了,幸亏以前用过,上手速度++。

Winform 还是 WPF?

本来准备使用 WPF,还在 WPF 和 D3D 交互上搞了半天,用了这个看起来没人维护了的 WPFDXInterop,它提供了一个 D3DImage 控件,只要把渲染好的图贴上去就可以了。但是最关键的是用它搞不定透明,如果在 WPF 里通过 WinformHost 的嵌入一个 Win32 控件的话,透明效果又不对。

所以最后我选择直接就使用一个 Win32 的窗体,上面所有的内容全部自己用 D2D 画,反正主体界面也不复杂,顶多就是几个 Button、再加上一个简单的 BoxLayout。至于其他的对话框(比如设置),就直接使用 WPF 就可以了。

1px 的不和谐边框

由于 Windows 10 上窗口周围都有 1 px(设备无关像素) 的边框,当我们的亚克力背景透明度调的比较低时,这个 1px 的边框就会露出来。这个问题实在无解,最后我就暴力地用 vim 当前的背景色不透明地先把这 1px 画上,盖住系统的边框,至少颜色上与当前使用的 Color Scheme 能够统一。如果我们的背景没那么透明,这样的边框就不显眼了。

就是这个黄色边框,其实是为了盖住系统的边框

次像素抗锯齿

这是我一直都没有搞定的一个问题。由于我们窗口的背景是透明的,次像素抗锯齿没办法启用,只能使用灰度抗锯齿。但灰度抗锯齿在低分辨率屏幕上的效果并不是很好。

我还特意去看了一下 UWP 的抗锯齿效果,也不是次像素抗锯齿(放大之后没有明显的红绿蓝边界),但好像要比普通的灰度抗锯齿的效果要顺眼(难道是错觉?)。好在我用 Surface,灰度抗锯齿效果在高分辨率屏幕上还不错,眼不见心不烦😞

连字 Ligature

怎么在 D2D/DirectWrite 上启用连字也花了我不少的时间。连字就是现在很多人喜欢的那种把多个字符(比如->、==)画在一起的那个功能。

要实现连字,首先需要通过 TextAnalyzer 分析输入的文本,得到一组 Glyph 的索引,以及一组 Cluster Map。Cluster Map 里面村的就是 Code Point 到 Glyph 直接的对应关系。比如,如果 Cluster Map 的内容是 [0, 1, 1, 2, 4],那就代表第 1、2 个 Code Point 对应了一个 Glyph(比如 Ligature 的情况),而第 3 个 Code Point 对应了 2 个 Glyph (比如阿拉伯文上面的各种修饰)。

 Codepoint Index    Glyph Index
       0  -----------   0
       1  -----------   1
       2  ----------/
       3  -----------   2
          \----------   3
       4  -----------   4

最后使用 DrawGlyphRun,把几个 Glyph 一起画出来。如果字体提供了连字支持,那么在 Cluster Map 中就会有所体现。

C++/CLI 做胶水

有时需要跟 Native 的 Win32 API 做交互,这个时候就会感叹有一个 C++/CLI 这样的语言做胶水实在是太方便了。但 C++/CLI 的 Bug 也不少,有时会遇到报编译器内部错误,建议我把 XX 行附近的代码换个写法🤣 现在在 dotnet core 的趋势下,大概 C++/CLI 历史任务也快圆满完成了,可是替任者在哪呢?

关于这个项目的一些回忆大概就这么多了。最后还是要感慨一下,跟调原生 DirectX API 相比, 用 SharpDX 确实舒服很多,这可能跟 C# 的语法糖们也有关系8️⃣。

Redo Build System

前一阵子想找一个简单的 build system,看到了 redo,就去研究了一下。一开始还有点迷糊,但最后发现,虽然 redo 看起来只做了一件事(当文件变化时重新执行脚本),但它其实还解决了另一件更重要的事:如何在一个命令式语言中描述结构信息?

声明式语言是擅长描述结构信息的,谁是谁的父亲、谁是谁的兄弟、谁依赖了谁、谁的属性是什么等等。Makefile 就是一种典型的声明式语言,大多数 build system 也都使用声明式语言来描述构建流程(也有的使用了同时掺杂了命令式和声明式的语言,比如 Meson)。

回到一开始的问题,如何在命令式语言中方便地描述这种结构信息?熟悉函数式编程的人大概一下子就会想到使用 Method Chaining:

target("all")
    .depends_on("other_target")
    .source_files("a.c", "b.c")
    .call("...")
    ....;

虽然很多语言都能模仿出这种写法(你甚至可以用 bash 写 fp),但这已经不算是典型的命令式语言的用法了。同时,它仍然是“提前声明”式的做法(即在 build target 之前声明好这个 target 的所有属性、依赖等等),不够灵性。Redo 的做法同时也解决了 Makefile 的一些其他问题,例如依赖的其他 Make 文件是在编译过程中产生的情况、或是 Recursive Make Considered Harmful

Redo 的做法其实很简单,就是让脚本一边跑,一边收集脚本或文件之间的依赖关系。这是它与其他声明式语法的 build system 的最大不同之处。这样的好处是,build 过程仍然用 bash 描述(比起用 Method Chaining 自然很多),而且依赖关系可以随时添加——甚至在编译结束之后。在命令式语言里看起来这是再正常不过的事了:

# 依赖 $2.c
redo-ifchange $2.c
gcc -MD -MF $2.d -c -o $3 $2.c
read DEPS <$2.d

# 依赖 gcc 生成的头文件列表中的所有文件
redo-ifchange ${DEPS#*:}

稍微跑个题,我很久之前其实也想过类似的问题:如何在 C++ 中描述一个图形界面?这个问题与虽然跟 Redo 半毛钱关系没有,但解决的思路其实完全一样。最后我做到了这样:

这是完全合法的 C++ 代码,扔进编译器就能编过。它描述了 main_form、button1 和 button2 之间的结构关系(父子关系),看起来像是声明式的语法,但仍然不丧失命令式语言的特点。最后只要:

int main()  
{
    yz::app app;
    yz::window* w = yz::ui["main_form"];
    app.exec();
    return 0;
}

就能启动。实现细节请看 https://zhuanlan.zhihu.com/p/24196189


至于如何看待 Redo,我觉得它小众自然有它的原因。一个 build 脚本,目标的依赖关系方面使用声明式语言更容易表达,而每个目标的产出过程是一个命令式的过程。所以路无非三条:可以在声明式的语法中加上点命令元素,或是在命令式的语法中加上点声明元素,或是干脆搞一个半声明半命令式的语言。Make 走的第一条路,Redo 是第二条,Meson 是第三条。

从结果来看,大家明显认为解决目标之间的依赖问题更重要。

2038 年的底特律

《底特律:我欲为人》这个游戏发售的时候我就有关注过。早买早享受,晚买享折扣,不买就白送!PS Plus 会员上个月会免,一周通关。这是一款互动电影,极强的代入感、紧张刺激的 QTE、无法回头的行为选择让我在通关之后还在回味。本文有大量剧透,还没通关的朋友请尽快点击右上角的❌关闭窗口,否则会对您的游戏体验造成打击🤪。

异常仿生人与人工意识

整篇故事简单来说其实只有一件事:我们要如何面对人工意识的出现。

2038 年的底特律,人形机器人(即仿生人)在人类社会中大放异彩:做家务、做市政、盖房子、清扫街道……几乎所有的工作都有仿生人的身影。然而由此带来了严重的失业问题,很多人对仿生人都抱有敌对态度。与此同时,人工意识出现了——一些仿生人突然变得具有意识、不再唯命是从,有自己的想法、能做出自己的决定。这样的仿生人就被称作异常仿生人。

意识、以及人工意识,是一个非常深奥的哲学问题。我没办法说清它是什么,只能描述出它是什么样子。在《底特律》中,人工意识表现为机器认为自己具有生命、怕死、不再无条件听从人类的命令、能够自行选择行动目标。什么样的 Bug 能够使程序产生意识呢?Bug 的产生,是因为程序员(几乎)无法穷举出一个程序所有可能的状态,因此必然会有一些状态是不符合设计预期的;但很显然现今所有的软件都有 Bug,却无一可能产生人工意识。原因大概至少有这样两个:一个是现今程序本身的输入输出有限;二是程序的逻辑有限,再复杂的程序、再离奇的 Bug 也达不到产生意识所需要的逻辑与状态。不过,现如今这两点都在“改善”:设备感知外界的能力越来越强,以及越来越多的人类无法解释的复杂拟合逻辑。如果再加上强化学习,大概距离产生人工意识又近了一步(雾。游戏中 2038 年就产生了人工意识,我觉得这还是太乐乐乐乐乐乐观了点。

在《底特律》中,大多数异常仿生人都是在极端场景下产生的人工意识,比如恐惧、愤怒等等。游戏开场的第一个异常仿生人与家里的孩子关系亲密,但当得知主人用新的仿生人替换掉他时,愤怒使他产生了意识。第二个仿生人则是在被主人长期虐待、最后在被主人暴力殴打时进行了反击。然而,并不是所有的异常仿生人都如此——例如主角之一卡菈,在一“出生”时就产生了意识,即便经过数次记忆重置,她还是在危急时刻重新找回了自己;再如另一个主角康纳,他是在执行任务的过程中逐渐思考而产生意识。

三位主角:康纳、马库斯和卡菈

“卡姆斯基测试”

成功制造仿生人的公司叫做“模控生命”,卡姆斯基是它的创始人。他提出了一项所谓“卡姆斯基”测试,作为一种更高级的图灵测试。他叫来一个正常的仿生人克罗伊,并命令康纳开枪杀死她。这项测试测试的是机器的同理心——如果他不忍杀死克罗伊,也就意味着同理心胜过了人类的指令,则通过了卡姆斯基测试,可以认为他具有了意识。识别同类,并能够将自己设身处地为他人考虑,似乎不仅仅是意识,还有更深层的情感元素。

在游戏中,克罗伊同时会在标题界面做开场引导,这应当也是导演的特意安排。

克罗伊

宗教、信仰与爱情

游戏中第一个让我有点惊讶的是场景是,异常仿生人产生了宗教。他们会做宗教贡品、有标志记号“rA9”,会在墙上频繁涂鸦这个记号。当然,也许这还称不上是宗教,但他们已然有了信仰。人类的宗教是如何产生的我不清楚,不过长期的奴隶制度压迫、受到人类排挤、前途一片迷茫,会产生一种精神寄托似乎也不足为奇;但另一方面作为机器人,他们的智商和知识储备理论上限非常高,真的会产生宗教吗?

另外,仿生人之间还会产生爱情。一开始有点难以理解,但想一想之后觉得也有可能。从功利的角度讲,不同的仿生人对于危险评估、武力与和平、抗争意识等的“阈值”不同,使得不同的仿生人之间相处的舒适程度也有差别;或是因为相互付出后能够获益更多等等。而从意识的角度讲,“为她付出”这件事是由意识决定的,是自主设下的目标,就算没有这些功利角度的考量,也仍然是有可能产生的。

性爱机器人

其实从游戏一开始我就在想,这不做出娃娃更待何时!果然游戏中出现了由性爱机器人专门服务的 Club,还贴心地设置了两个小时的记忆过期时间……价钱也是便宜的很,一次只要 20 刀哟!(这段掐了别播)

民科的仿生人改造试验

在《荒野大嫖客2》中,有一个屋子里摆满的全都是动物改造实验的半成品,比如把动物大卸八块,然后把鹿的脑袋安在熊的身体上,等等。过了 100 多年,还是同样有人有着类似的癖好,把仿生人大卸八块、暗黑改造、制造仿生熊……享受着造物的快感。

像病毒一样传染

人工意识可以传染。由于机器人之间交流的便捷性,拍一下肩膀、握个手、甚至隔空招呼一下,就能把一个正常的仿生人变成异常仿生人。从“唯程序主义”的角度来讲,意识是由程序产生的,而程序不过就是一段数据,把它传给别人使得别人产生意识,看起来非常合理。

因此,一旦有一个机器人具备了人工意识,就意味着会有无数个机器人产生人工意识。人类只有两种选择:要么与之共存,要么将其灭亡。

抗争还是革命?

作为游戏的第三个主角马库斯,玩家需要选择:是选择和平方式抗争,还是暴力方式革命?这一度成为我最艰难的选择。一开始我选择和平示威,例如墙上涂鸦和游行。舆论态度因此而上升,但这并没有解决问题,人类武力镇压游行,并计划销毁仿生人。阶级斗争、社会制度的斗争,靠文的不行;而革命,就要流血。最后时刻我选择了革命,看着朋友一个一个倒下,假如我并不是在屏幕之外,我还会这么淡定吗?

中途有时我还会想,这样是不是闹得太大了?想来想去,发现我还是以人类视角去看待这件事;如果我是仿生人,大概根本不会觉得这有什么不对。这就是屁股理论的完美呈现。

调查问卷

游戏进行到一半时,在标题界面克罗伊会请玩家填一份调查问卷,一共 10 道题。在游戏通关之后,还有额外 3 道题。做完之后可以看到玩家选择的比例,很有意思。挑几道题目:

1. 您会考虑和外表像人类的仿生人发展亲密关系吗?
是(62%) 否(18%) 不知道(20%)

外表像人,非常关键。对同类的识别,大概是动物的本能。

2. 您认为科技会对人类造成威胁吗?
是(69%) 否(19%) 不知道(12%)

我认为不会。人类有本事造出来,就得有本事处理好呀。威胁的源头其实并不在于科技,也不在于仿生人。

5. 您最期待哪种科技出现?
仿生人(35%) 飞行车(13%) 太空旅游(20%) 脑部连接装置(32%)

这里面似乎只有仿生人和脑部连接装置距离现在还有一些距离。我更期待后者,算是对人类自身的扩展吧。

6. 您信神吗?
是(42%) 否(40%) 不知道(18%)

这个数据有点意外。

9. 如果您需要动紧急手术,您同意让机器执刀吗?
是(72%) 否(12%) 不知道(16%)

这得看机器是什么水平啊,我选择不知道。

10. 您觉得未来机器可能发展出自我意识吗?
是(68%) 否(16%) 不知道(16%)

我认为这只是时间早晚的问题。

关于游戏本身

游戏本身,素质过硬。作为一款互动电影,除了常规的选择执行的动作之外,还有一些探索元素,解开的秘密会在后续关卡解锁诸多新选项;刺激的 QTE,失败了也不会重来,剧情会按照失败的场景继续;优秀的配乐、演员的演绎、丰富的分支、不错的画面、让人思考的故事……这个游戏值得一玩。

通关之后,其实还有很多的分支没有玩到;但我不想再去把所有的分支玩一遍了。除了“会有一些重复的情节”这个原因之外,更重要的是,别的分支不是的故事。最让我遗憾的是,我在最后关头没能保护好爱丽丝,好不容易逃出了美国,她却永远离开了卡菈。

最后关头😭

不过这种缺憾美,会让人更加感慨和怀念。

什么是 CLR ?

作者:By Vance Morrison – 2007
原文链接:https://github.com/dotnet/coreclr/blob/master/Documentation/botr/intro-to-clr.md
翻译:dontpanic

什么是公共语言运行时(Common Language Runtime, CLR)?简单来说就是:

公共语言运行时(CLR)是一套完整的、高级的虚拟机,它被设计为用来支持不同的编程语言,并支持它们之间的互操作。

啊,有点绕口,同时也不太直观。不过这样的表述还是 有用的 ,它把 CLR 的特性用一种易于理解的方式分了类。由于 CLR 实在太过庞大和复杂,这是我们理解它的第一步——犹如从万米高空俯视它,我们可以了解到 CLR 的整体目标;而在这之后,我们就可以带着这种全局观念,更好地详细了解各个子模块。

CLR:一个(很少见的)完备的编程平台

每一个程序都有大量的运行时依赖。当然,一个程序需要由某种特定的编程语言编写而成,不过这只是程序员把想法变成现实的第一步。所有有意义的程序,都免不了需要与一些 运行时库 打交道,以便能够操作机器的其他资源(比如用户输入、磁盘文件、网络通讯,等等)。程序代码还需要某种变换(翻译或编译)才能够被硬件直接执行。这些依赖实在是太多了,不仅种类繁多还互相纠缠,因此编程语言的实现者通常都把这些问题交由其他标准来指定。例如,C++ 语言并没有制定一种 “C++可执行程序” 格式;相反,每个 C++ 编译器都会与特定的硬件架构(例如 x86)以及特定的操作系统(例如 Windows、Linux 或 macOS)绑定,它们会对可执行文件的格式进行描述,并规定要如何加载这些程序。因此,程序员们并不会搞出一个 “C++可执行文件”,而是 “Windows X86 可执行程序” 或 “Power PC Mac OS 可执行程序”。

通常来说,直接使用现有的硬件和操作系统标准是件好事,但它同样也会把语言规范与现有标准的抽象层次紧密捆绑起来。例如,常见的操作系统并没有支持垃圾回收的堆内存,因此我们就无法用现有的标准来描述一种能够利用垃圾回收优势的接口(例如,把一堆字符串传来传去而不用担心谁来删除它们)。同样,典型的可执行文件格式只提供了运行一个程序所需要的信息,但并没有提供足够的信息能让编译器把其他的二进制文件与这个可执行文件绑定。举例来说,C++ 程序通常都会使用标准库(在 Windows 上叫做 msvcrt.dll),它包含了大多数常用的功能(例如 printf),但只有这一个库文件是不行的。程序员如果想使用这个库,必须还要有与它相匹配的头文件(例如 stdio.h)才可以。由此可见,现有的可执行文件格式标准无法同时做到:1、满足运行程序的需求;2、提供使程序完整所必须的其他信息或二进制文件。

CLR 能够解决这些问题,因为它制定了一套非常完整的规范(已被 ECMA 标准化)。这套规范描述了一个程序的完整生命周期中所需要的所有细节,从构建、绑定一直到部署和执行。例如,CLR 制订了:

  • 一个支持 GC 的虚拟机,它拥有自己的指令集(叫做公共中间语言,Common Intermediate Langauge),用来描述程序所能执行的基本操作。这意味着 CLR 并不依赖于某种特定类型的 CPU。
  • 一种丰富的元数据表示,用来描述一个程序的声明(例如类型、字段、方法等等)。因此编译器能够利用这些信息来生成其他程序,它们能够从“外面”调用这段程序提供的功能。
  • 一种文件格式,它指定了文件中各个字节所表达的意含义。因此你可以说,一个 “CLR EXE”并没有与某个特定的操作系统或计算机硬件相捆绑。
  • 已加载程序的生命周期语义,即一种 “CLR EXE 引用其他 CLR EXE” 的机制。同时还制订了一些规则,指定了运行时要如何在执行阶段查找并引用其他文件。
  • 一套类库,它们能够利用 CLR 所支持的功能(例如垃圾回收、异常以及泛型)来向程序提供一些基本功能(例如整型、字符串、数组、列表和字典),同时也提供了一些与操作系统有关的功能(例如文件、网络、用户交互)。
继续阅读

离去(二)

没想到这么快就要经历第二次离别。订了跟上次同一趟的航班,两小时后出发。

爷爷得阿尔茨海默症已经十多年了。那一刻我仍然记忆犹新——还在上初二的我放学回家,家里人正围坐在一起吃饭;爷爷神情茫然,转过头来看向我,叫出了我表哥的名字。爸爸又问了一遍,爷爷却还是想不起我的名字。他的左手已经不再灵便,勉强地端起饭碗送至嘴边,碗险些要掉下来。家人发觉不对,就急忙让爷爷在沙发上躺下休息,并叫来了120。

爷爷在得病之前,一直都是家里的大厨,逢年过节必定下灶,做出一大桌子的饭菜。过年之前要掂量好几天,得有鱼有肉、几荤几素、凉菜热菜一应俱全,全写在小纸条上。我印象最深的是虎皮椒里面塞肉馅,外面带着辣椒的清香,里面是满满的肉香。其他的鸡鸭鱼肉自不必说,虽然有点油腻,但是一定备受欢迎。刚得病的时候,我难以接受这个现实:爷爷还是那个爷爷,为什么一直躺在沙发上不再下厨了呢?我还拽着他起来,觉得他只是犯懒不再愿意干活了。似乎记得爸爸还试过让爷爷再做一次饭,但已然再无法掌握他以前做菜的秘诀。

小时候爷爷奶奶的重男轻女,在上一篇文章中已经可见一斑。听姑父回忆,当爷爷得知我是个男孩之后,直接出门买了只烧鸡、就着小酒,在家里独享其乐。还有一件事:大概我只有几岁的时候,有一天姑姑和大姐来想要带我去洗澡。我本来不想去;大姐说“不去以后就不带你玩”,小小的我权衡利弊决定还是跟去了。结果坐在自行车的后座上,我的脚就被车轮刮住,脚踝处受了伤。后来回家躺在床上,之间一只拖鞋从走廊飞了进来,那是爷爷生气在打姑姑。前有喝酒吃烧鸡、后有拖鞋飞进屋,外加反孙子集团,这就是我这个老幺在爷爷心中的地位。

然而从小到大,我却经常发孩子脾气,跟爷爷闹别扭。家里人,我似乎顶撞爷爷最凶,跟他喊、跟他嚷,却从来不曾这么顶撞过奶奶和爸爸妈妈。跟爷爷抢电视看,质问他“你听新闻听啥了”;其他的事由记不清了,但也一定是很大声;爷爷经常就嘟囔着骂一声,翻身躺在床上睡觉去了。然而闹归闹,晚上想要买零食,还是要跟爷爷要钱。爷爷就经常笑着把褥子掫开,拿出10块钱给我。我就跑下楼,一袋牛板筋、一小盒康师傅麻辣牛肉面。一定要小盒的,因为我总是觉得小盒的味道不一样。

爷爷在得病之前,曾经“走丢”过一次。那一晚爷爷没回家不知去向,急得家里人报了警。后面的事情我有点记不清了,总之最后平安归来。在那之后,爸爸就给爷爷配了一台手机,诺基亚直板。当时手机到了,卡还没办,爷爷愣是每次都揣在衬衫口袋里,出去溜达也要拿出来让朋友们看看他儿子送给他的东西。

爷爷退休之前,我觉得在区里肯定算得上是“叱诧风云”的人物,在教育局、民政局、粮食局、交通局(得加个等字,忘记是四个还是五个)都任过局长级别的职务,最后退休时回到了教育局。小时候家里虽然不富裕,但也绝不贫苦。也不知道我活到现在,有没有给爷爷丢脸呢。

还在住平房的时候,院子里有一个菜园,爷爷会在里面种一些茄子、辣椒、葡萄之类的蔬菜水果。除此之外,房子门前还种了两棵樱桃树和一棵沙果树,院子里还有一个地窖,可以储存蔬菜。 院子后面还有一个奇特的地道,从地上向地下延申,小时候跟表哥们进去探过险,但是没有走到头就害怕退了回来。院子后门出去就是十九中的操场,奶奶曾经在那当老师,那个操场也是我童年玩乐的“主战场”。后来爷爷奶奶搬去住了楼房,这个院子就卖掉了。现在想来,这个小院子,要不是诸如煤气、下水等一些基础设施不到位,真的是享受生活的好地方。

爷爷跟奶奶,在我有限的记忆中,总是伴随着争吵。但吵归吵,爷爷得病后,要是奶奶不在身边,就经常会找她。奶奶去世后,爷爷还经常会停留在柜子前面,看看上面摆放着的家里六个人的合照。巧合的是,在爷爷出殡前一天,我们烧完纸之后买了一提农夫山泉;它们的生产日期刚好是12月15日,正是奶奶离世的日子。

好想再吃一次虎皮辣椒裹肉馅啊。

长模式与 16 位程序

这是我在知乎的回答:https://www.zhihu.com/question/310785889/answer/58670983

首先我们要把 16 位程序分成两类来看:跑在实模式下的 16 位程序和跑在保护模式下的 16 位程序。长模式是支持兼容 16 位的保护模式的(x86-64 白皮书):

但由于在长模式下移除了虚拟 8086 模式,无法运行实模式的(直接访问硬件的)16位程序。

至于为什么移除了虚拟 8086 模式,只能认为是不想再多背包袱了。已经有的包袱甩不掉,但至少别给自己再找新麻烦…

其实 Intel 在 80286 添加保护模式时就不想带这个包袱了,它没有提供直接的方法从保护模式转换回实模式(即无法支持实模式 16 位程序) 。也许是大家怨声载道,而后在 386 中 Intel 添加了虚拟 8086 模式,允许直接在保护模式下虚拟一个实模式出来。这样在 32 位操作系统中,实模式的 16 位程序才能运行。所以到了 64 位,这样的包袱不再扔一次怎么行呢,实在不行再把它加回来就好了,毕竟这事 Intel 已经干过一回了 🤣

总而言之,长模式是可以运行 16 位程序的,但只兼容保护模式。不过 Windows 也无法运行 16 位保护模式下的程序,这是 Windows 本身不再支持了的原因,不是长模式的限制。