让老旧的 Macromedia Director 游戏在现代硬件上运行

Hacker News Top 新闻

摘要

作者讲述了如何让老旧的 Macromedia Director 冒险游戏在现代硬件上运行,包括解析 CloneCD 镜像、绕过基于畸形目录条目的文件级复制保护等逆向工程过程。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/10/11 00:35

# 让老旧的 Macromedia Director 游戏在现代硬件上运行 | WerWolv 来源:https://werwolv.net/posts/macromedia_copy_protection/ ## 引言 (https://werwolv.net/posts/macromedia_copy_protection/#introduction) 在 2000 年代初我还是个孩子的时候,我花了很多时间玩从当地图书馆借来的游戏。其中很多都是德语的点击式冒险游戏,因为那些游戏能在爸爸的 Windows 95 电脑上运行。即使在那个时候,我也对技术很感兴趣,总是想办法搞清楚怎么在把游戏还给图书馆之后还能继续玩。当然,我从来没有成功过。 前几天,我又翻到了其中一次失败的尝试:一叠当时我试图刻录《Findus bei den Mucklas》副本的 CD。我想,也许现在终于到了我具备相应技能的时候了! ## 获取光盘映像 (https://werwolv.net/posts/macromedia_copy_protection/#getting-a-disk-image) 显然,我当时刻的 CD 是无法使用的。一方面,那时的 CD 刻录机往往无法正确读取或写入带复制保护的 CD;另一方面,过去十年里我也没有一台带光驱的电脑了。 好消息是,一些热心的人把这系列所有游戏的完整光盘映像上传到了 archive.org(https://archive.org/details/cd-01_202304)上。放在那上面的东西不算盗版,对吧? ## 解包映像 (https://werwolv.net/posts/macromedia_copy_protection/#unpacking-the-image) 游戏以三个文件的形式出现:一个`.ccd`、一个`.cue`和一个`.img`文件。这些显然是由老牌的 [CloneCD 程序](https://en.wikipedia.org/wiki/CloneCD)生成的,包含光盘的原始数据以及带外(out-of-band)的附加信息,如轨道元数据。那个年代的游戏通常还带有音轨,所以你可以直接把 CD 放进 CD 播放器里听原声带或其他附带的媒体内容。 查看`.cue`文件可以看到,开头是一个包含游戏文件的 `MODE1` 轨道,后面跟着四个 `AUDIO` 轨道: ``` FILE "CD01.img" BINARY TRACK 1 MODE1/2352 INDEX 1 00:00:00 TRACK 2 AUDIO INDEX 1 45:08:37 TRACK 3 AUDIO INDEX 1 46:38:44 TRACK 4 AUDIO INDEX 1 47:08:50 TRACK 5 AUDIO INDEX 1 47:11:58 ``` 为了访问数据,我使用了老旧的 `ccd2iso` 工具,它在遇到第一个 `AUDIO` 轨道时失败了,但仍然能够将文件开头部分转换为一个可以挂载的普通 ISO 文件。 ## 复制保护第一层 (https://werwolv.net/posts/macromedia_copy_protection/#copy-protection-1) 挂载文件并尝试复制数据时,我遇到了我认为是第一层复制保护的措施: 解压归档时弹出的重复文件提示 解压归档时弹出的重复文件提示 有些文件似乎在磁盘映像中出现了多次——一次是真正的副本,另一次则是 0 字节或 256 字节长且包含随机垃圾数据的诱饵副本。我的猜测是,他们故意在磁盘上创建了格式异常的目录条目,以此来欺骗某些工具,使工具只显示诱饵副本而非真正的文件。Windows 似乎总能正确读取文件(否则它自己也无法正常工作),但我尝试的各种工具,每一个最终显示的文件组合都不同。 通过结合使用 `PeaZip`、`Ark`、`Dolphin` 和 `7z`,我最终成功从 ISO 中提取出了正确的文件,但这主要是体力活:选择大小正确的文件,跳过诱饵文件。 从 ISO 映像中最终提取出的文件 从 ISO 映像中最终提取出的文件 到这一步,直接运行安装程序似乎最简单,而令人惊讶的是它居然成功了。我使用 `WINEPREFIX=$(pwd)/prefix wine "Installiere Findus4.exe"` 来将其安装到一个单独的 Wine 前缀中,之后我再从那里复制安装好的数据。 ## 复制保护第二层 — 电光狂潮 (https://werwolv.net/posts/macromedia_copy_protection/#copy-protection-2---electric-boogaloo) 现在执行 `Findus4.exe`,当然不会直接运行,而是弹出一个漂亮的错误提示,说找不到 CD。 缺失 CD 错误弹窗 缺失 CD 错误弹窗 直接用 `rg -a "Lege die CD"` 搜索这条消息,发现他们居然直接从 `Pettson3.ini` 文件中读取这段文字。 *他们甚至懒得把那个文件从上一款游戏中重命名。* ``` [CDROM] CDROM=? [GAME1] Vind=0 [ALERT1] MESSG=Lege die CD Findus4 ein und versuche es noch einmal. [SCREEN] FULLSCREEN=1 ``` 搜索了几个字符串后,最终在 `Findus4.exe` 中找到了 `messg`。我的第一反应当然是直接把二进制文件扔进 Ghidra 开始分析。令人惊讶的是,使用 `Search -> For Strings...` 选项却找不到这个字符串——这通常意味着该字符串并不属于 PE 数据的一部分,而是某种附加在可执行文件末尾的额外载荷。 经过进一步挖掘,我发现这些游戏全部都是 `Macromedia Director Projector` 可执行文件。这些文件由 Macromedia Director 生成,基本上包含了解释 Director 文件所需的整个运行时环境,末尾拼接了一个 `.dxr` 文件。程序运行时实际执行的就是那个文件。 CD 检查逻辑属于内嵌的 Director 文件是很合理的,因为运行时为什么会去读取一个叫 `Pettson3.ini` 的配置文件呢? 在研究过程中,我发现了 [ProjectorRays](https://github.com/ProjectorRips/ProjectorRays),这是一个专门针对这些文件的反汇编器和反编译器。这些文件的文件签名是 `RIFX` 或 `XFIR`(取决于目标系统的字节序),这使得它们很容易被识别。 使用 ImHex,我在二进制文件中找到了几个匹配这些字符串的位置。根据 ProjectorRays 的源代码,这些 `.dxr` 文件的格式以类似以下内容开头: ``` enum RifxType : u32 { MV93 = le u32("MV93"), MV95 = le u32("MV95"), FGDM = le u32("FGDM"), FGDC = le u32("FGDC"), APPL = le u32("APPL") }; struct Content { type::Size length; RifxType type; // ... }; struct DirectorFile { char magic[4]; if (magic == "RIFX") be Content content [[inline]]; else if (magic == "XFIR") le Content content [[inline]]; else std::error("Invalid Director File!"); }; ``` 在所有 `XFIR` 出现的位置中,只有两个后面跟着有效的类型:一个是在地址 `0x00222151` 处的 `APPL`,根据 ScummVM 的说法,它标识一个 Director 应用程序包装器(也就是我们正在处理的可执行文件);另一个是在地址 `0x002222F0` 处的 `MV93`,这似乎是一个普通的、未压缩的 Director 影片脚本。 我最初尝试将该数据块从可执行文件中复制出来,然后用 ProjectorRays 单独处理,但事实证明,附加文件内部使用的所有偏移量都是绝对偏移,并且已经考虑到了 `.dxr` 文件并非从可执行文件开头开始这一因素。 因此,我转而为 CLI 添加了一个 `--offset` 选项,使其可以跳转到内嵌二进制文件的实际偏移位置。如果有其他人需要的话,PR 在这里:[ProjectorRays#63](https://github.com/ProjectorRays/ProjectorRays/pull/63)。 ## 反汇编 Director 文件 (https://werwolv.net/posts/macromedia_copy_protection/#disassembling-the-director-file) 经过这一番折腾之后,我们终于可以用以下命令导出内嵌 Director 文件的内容了。 ``` ./projectorrays decompile Findus4.exe -o output --dump-scripts --offset 0x002222F0 output ├── Findus4 │ └── casts │ └── Internal │ ├── MovieScript 1.lasm │ └── MovieScript 1.ls └── Findus4.dir ``` 令我惊讶的是,这不仅生成了反汇编文件(`.lasm` 文件),还生成了反编译版本的脚本(`.ls` 文件),而且其中仍然保留了所有原始的函数和变量名! 反编译后的 Director 脚本 反编译后的 Director 脚本 ## 分析脚本 (https://werwolv.net/posts/macromedia_copy_protection/#analyzing-the-script) 生成的文件只有大约 150 行,包含游戏的初始化逻辑。它是用一种叫做 [Lingo](https://en.wikipedia.org/wiki/Lingo_(programming_language)) 的编程语言编写的,这是 Macromedia Director 所使用的语言。即使不了解这门语言,代码也相当容易读懂。 没花多少时间,我就找到了那条 `"messg"` 字符串最初来源的函数。如代码所示,他们实现了一个简易的 `.ini` 文件解析器来解析 `Pettson3.ini` 文件并从中提取几个配置值,其中一个是找不到 CD 时显示的错误消息。 ``` -- ... on analyseIniText iniText repeat with mening = 1 to iniText.line.count ReddString = StripString(iniText.line[mening]) if ReddString contains "[" then ReddString = StripString(iniText.line[mening + 1]) end if antalBokstaver = ReddString.char.count if ReddString contains "cdrom" then cdromenhet = ReddString.char[7..antalBokstaver] end if if ReddString contains "Vind" then antalBokstaver = ReddString.char.count vindspelAktivt = integer(ReddString.char[6]) end if if ReddString contains "messg" then antalBokstaver = ReddString.char.count alertmessage = ReddString.char[7..antalBokstaver] end if if ReddString contains "fullscreen" then antalBokstaver = ReddString.char.count fullscreen = integer(ReddString.char[12]) end if end repeat resultat = [#cdromenhet: cdromenhet, #vindspelAktivt: vindspelAktivt, #alertmessage: alertmessage, #screen: fullscreen] return resultat end -- ... ``` 我总觉得阅读这些老脚本超级有趣——看着蹩脚的英文、用外语写的标识符(这次好像是瑞典语?)以及可能极度充满 bug 的代码混杂在一起。 再往下一点,我们还可以找到实际使用错误文本的函数: ``` on checkpccdrompath seperator = the last char in the applicationPath drivelista = ["d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] Retvar = VOID repeat with Drive in drivelista iscdrom = baDiskInfo(Drive, "type") if iscdrom = "CD-ROM" then Path = Drive & ":" & seperator & "p3media" if checkoncd(Path) then Retvar = Drive exit repeat end if end if end repeat return Retvar end -- ... ``` `setSearchPaths` 函数的部分代码 ``` driveletter = checkpccdrompath() if not voidp(driveletter) then ini[#cdromenhet] = driveletter & ":" hdMedia = mp & "p3media" hdljud = mp & "p3media\p3ljud" hdcast = mp & "p3media\p3cast" cdMedia = string(ini.cdromenhet & "\p3media") cdLjud = string(ini.cdromenhet & "\p3media\p3ljud") cdCast = string(ini.cdromenhet & "\p3media\p3cast") the searchPaths = [hdMedia, hdljud, hdcast, cdMedia, cdLjud, cdCast] else alert(string(ini.alertmessage)) quit() end if -- ... ``` 又是相当直白的代码。他们只是遍历所有可能的 Windows 驱动器,试图找到一个包含 CD-ROM 且其中有 `p3media` 文件夹的驱动器。如果找到了,就设置搜索路径;如果没找到,就显示我们之前看到的错误消息,然后游戏调用 `quit()`。 ## 修补脚本 (https://werwolv.net/posts/macromedia_copy_protection/#patching-the-script) 分析了 `setSearchPaths` 函数后,我想到了最简单的办法:在检查失败时,让 `if` 分支跳转到下一条指令,而不是跳转到 `else` 分支。这样就不会执行 `quit()` 分支,同时仍然能正确设置搜索路径。 这样会导致 CD 路径有点奇怪,但我认为应该还是能工作的,因为游戏允许你做"完整安装"——把所有需要的数据复制到硬盘上,然后大概从硬盘加载文件。 在汇编文件中,我们想要修补的指令如下: ``` < 95 00 60 > [246] jmpifz [342] ......... if not voidp(driveletter) then / else < 4C 08 > [249] getlocal 8 ........... ``` 可惜的是 ProjectorRays 还不支持将代码重新汇编回去,不过好在 Lingo 字节码极其简单,我们可以用 ImHex 手动完成。 每条指令以一个操作码字节开头,后面跟着一个大端序整数,长度为 1、2 或 4 字节。为了方便手动修补,我还让 ProjectorRays 输出构成每条指令的字节(这个改进也提交到了上游)。 现在来修补:`jmpifz` 的操作码字节是 `0x95`,后面跟着一个大端序的 16 位相对偏移 `0x0060`,即 96 字节,表示要跳转到的位置。 为了简单地跳转到下一条指令,我们需要向前跳 3 个字节(从偏移 246 到偏移 249),所以我们要把指令字节修补为 `< 95 00 03 >`。 搜索原始字节序列后,只在地址 `0x002252E8` 找到一个结果,我用新指令覆盖了它。 就这样,游戏的开场动画播出来了!🎉 游戏的开场动画正在播放 游戏的开场动画正在播放 ## 复制保护第三层 — 机器的崛起 (https://werwolv.net/posts/macromedia_copy_protection/#copy-protection-3---rise-of-the-machines) 遗憾的是,事情还没完。 同样的,直接搜索那条字符串得到了两个结果,我决定先从 `intro.dxr` 开始。 ``` ➜ rg -a 'Disc Error !' P3media/intro.dxr ... P3media/huset.dxr ... ``` 再次把它扔进 ProjectorRays,又得到了几个脚本,其中一个被非常直白地命名为 `BehaviorScript 36 - Call Copy-X CD protection DLL (OPTGRAPH.DLL).ls`。 *开发者,你们不需要这么明示的吧,不过还是谢谢了。* 正如文件名所示,里面是加载位于游戏可执行文件旁边的 `optgraph.dll` 并调用其 `initdisplay()` 函数的代码。然后它检查该函数的返回值,如果不匹配某个魔法值,游戏再次被中止。 ``` on exitFrame me if the platform contains "Mac" then nothing() else if Ok = 0 then alert("Disc Error !") avslutaSpelet() else if getReturnVal(me) <> 26079537 then alert("Disc Error !") avslutaSpelet() end if end if end if end ``` 有趣的是,他们甚至懒得为 Mac 实现复制保护…… 要通过这个检查,我们有几个选项:可以让游戏以为自己运行在 Mac 上;可以修补对魔法数的比较;或者修补 DLL 让它始终返回那个常量。 鉴于同样的检查在 `huset.dxr` 中又实现了一次,而且我也不想逆向运行时环境和修补平台检测逻辑,我决定选择修补 `optgraph.dll`。 ## 修补支持 DLL (https://werwolv.net/posts/macromedia_copy_protection/#patching-the-support-dll) 在 Ghidra 中打开该 DLL 后,发现它基本上只包含那一个函数,别无其他。 `initdisplay()` 本身是一段颇为混乱的代码,他们向 CD 驱动器发送一系列底层命令,以某种方式从中构造出那个魔法值。 ``` // ... FUN_10001e50(&local_44,0,0x10); local_3c = 3; mciSendCommandA(local_c,0x814,0x100); local_3c = 1; local_2c = local_40; mciSendCommandA(local_c,0x814,0x102,&local_44); local_28 = (uint *)(local_40 & 0xff); local_3c = 1; local_48 = (char *)(local_40 >> 8 & 0xff); local_38 = 4; local_40._0_2_ = local_40._1_2_; mciSendCommandA(local_c,0x814,0x112,&local_44); // ... ``` 我没有花太多时间去逆向这些内容,而是直接使用了 Ghidra 出色的 `Patch Instruction` 选项,将前几条指令替换为简单而无聊的 `MOV` 和 `RET`。 ``` ***...****...**...***...***...***...***...***...****...***...***...***...***...****...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...

相似文章

我爱软件渲染

Hacker News Top

一篇关于软件渲染的个人技术博文,作者阐述为何偏爱 DOS 时代游戏的软件渲染效果——锐利的纹理、颗粒感的色带,以及这才是游戏最初的构建方式——并讨论现代硬件如何让这些游戏在当年无法企及的高分辨率下运行。

25+ years of pathfinding problems with C++

Lobsters Hottest

《帝国时代》工程总监深入剖析了系列游戏 25 年来寻路系统的技术债,指出遗留代码、动态地图机制及 SIMD 指令集取代 x87 扩展精度导致的浮点误差是单位“穿墙”等经典 Bug 的根源。