让老旧的 Macromedia Director 游戏在现代硬件上运行
摘要
作者讲述了如何让老旧的 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`。
```
***...****...**...***...***...***...***...***...****...***...***...***...***...****...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...***...
相似文章
我爱软件渲染
一篇关于软件渲染的个人技术博文,作者阐述为何偏爱 DOS 时代游戏的软件渲染效果——锐利的纹理、颗粒感的色带,以及这才是游戏最初的构建方式——并讨论现代硬件如何让这些游戏在当年无法企及的高分辨率下运行。
Nintendo Wii U games running from a 1980's Bernoulli disk [video]
通过SCSI转USB适配器将1980年代的90MB伯努利磁盘连接到Wii U,成功安装并运行了Game Boy Advance游戏《银河战士 融合》,展示了复古存储介质与现代游戏机的兼容性实验。
25+ years of pathfinding problems with C++
《帝国时代》工程总监深入剖析了系列游戏 25 年来寻路系统的技术债,指出遗留代码、动态地图机制及 SIMD 指令集取代 x87 扩展精度导致的浮点误差是单位“穿墙”等经典 Bug 的根源。
@Saccc_c: 持续探索 Codex + HyperFrames 的应用边际 今天剪辑的主题是回忆杀:4399之那些我们玩过的热门游戏 我感觉现在很强,已经可以去闲鱼和抖音接商单了 Tips: 这类视频制作时间较长,不是一个小时能做完的
An exploration of using Codex and HyperFrames to create nostalgic gaming videos, noting the potential for commercial work despite long production times.
将遗留Flash应用程序转换为HTML5的技能(逆向工程)
一种AI技能,能在没有原始源代码的情况下反编译和重写遗留Flash应用程序为现代HTML5和JavaScript,帮助迁移已弃用的软件。