MagogCartel/OddworldMap

GitHub: MagogCartel/OddworldMap

从 PS1 光盘镜像直接提取关卡数据的 Oddworld 系列游戏交互式地图查看器,支持对象解码、路线规划和碰撞线可视化。

Stars: 1 | Forks: 0

# Oddworld 地图 《**Oddworld: Abe's Oddysee**》与《**Abe's Exoddus**》(PS1, NTSC-U)的交互式地图,直接从游戏光盘提取:每个摄像机背景、每个对象(门、控制杆、Mudokon、LCD 状态板、危险物、敌人等)以及每条碰撞线,均按照游戏真实的摄像机网格排列。数据取自 PS1 光盘,但关卡布局、屏幕和对象位置在 PC 版本中完全相同,因此本地图适用于游戏的各个版本。 在 **[oddworldmap.com](https://oddworldmap.com/)** 浏览 —— 或使用任意静态 Web 服务器(如 `python3 -m http.server`)托管仓库根目录以在本地运行。 ![在 Oddysee 的 Stock Yards 中查看旅行鸟传送门的查看器](https://static.pigsec.cn/wp-content/uploads/repos/cas/36/363a89b66068c0dba8d0d2314eff85909391459f473b87da6789eff44687edd6.png) ## 操作说明 - **AO / AE** 按钮用于切换两款游戏 - **拖拽**或**方向键**进行平移,**鼠标滚轮**(以光标位置为中心)或**`+` / `-`** 进行缩放;**`[` / `]`** 切换关卡中的路径,**`g` / `c` / `f` / `a`** 切换网格、碰撞线、前景遮罩与连接箭头,**`r`** / **`m`** 激活路线规划器与标尺 —— 按 **`?`** 查看完整的快捷键列表 - **悬停**在任意对象上可查看解码详情 —— 门的目的地(关卡/路径/门编号)、开关 ID、路径转换目标、续关点区域、Mudokon 状态(Oddysee 中的职务 / Exoddus 中的工作状态及情绪),以及敌人行为(例如 Slig 的起始状态及其攻击方式 —— `shoot_on_sight_delay=0` 表示它一看到 Abe 就会立即射击,且没有任何“FREEZE!”警告;Slog 是否起始处于休眠状态,以及激怒它的开关)。每种对象类型也会显示其特有的关键字段 —— 门的锁定状态、活板门或带电墙壁的开关状态、鸟传送门的类型、Glukkon 或 Crawling Slig 的类型/状态。在“设置”中开启“显示更多对象字段”,即可选择每种对象类型需展示哪些游戏内置字段(搜索功能始终覆盖所有字段)。在屏幕列表和字段选择器中,带有虚线下划线的字段名称附带悬停释义 —— 解释其含义及可能的取值 —— 相关内容维护于 [glossary.json](glossary.json) 中 - **点击**门、路径转换、旅行传送门、快速升降井、传送器或手形石可跟随至其目的地(对于手形石,则是它们展示的摄像机),甚至可以跨关卡跟随;在悬停时,只要成对的对象位于当前路径上,与其配对的对象(即你将会从该处出来的对象)就会获得虚线轮廓。指向地图外关卡(如 Oddysee 的主菜单关卡)的罕见转换,会在其提示框中说明情况,而不是提供跟随选项 - 在屏幕的其他任意位置**点击/触摸**可列出其中的所有内容,并按类别分组 —— 悬停在列表某一行可在地图上勾勒出该对象,点击则可跳转至该处。点击对象会打开列表并滚动至该对象的高亮行:在无法进行悬停操作的触摸设备上,这是查看对象详情的方法。在手机上,该面板会作为底部抽屉打开,使地图在其上方保持可见;如果你不希望这些点击触发任何操作,可以在“设置”中关闭“点击时列出屏幕对象” - 左上角有关卡与路径按钮;下方是带有计数的对象类别过滤器 —— 悬停在路径按钮上可查看其区域名称:如果 [annotations.json](annotations.json) 中定义了专属名称(刻意覆盖),则显示该社区命名;否则显示游戏内名称(如 Rupture Farms Return 的 Zulag 1–4,以及 Exoddus 的结局区域) - “显示”与“对象”标题处的**重置**按钮可将相应部分恢复为默认设置 - **碰撞线**:绿色代表地面,红色/橙色代表墙壁,蓝色代表天花板,虚线代表背景层 - **背景平面对象**:放置在半比例背景平面上的对象在绘制时会变暗并带有虚线轮廓 —— 这与碰撞线使用的“虚线 = 背景”提示相同 —— 以便在规划路线时区分前景和背景对象 - **前景遮罩 (FG1)**:高亮显示绘制在玩家前方的景物 —— 让你一眼就能看到所有可隐藏/可在后方行走的地点(与“暗化背景”搭配使用效果更佳) - **连接箭头**:绘制路径的完整循环 —— 将每扇门、快速升降井、鸟传送门、传送器和路径转换连接至其目标。双头箭头表示双向配对;当无法解析确切的到达对象时,指向到达摄像机的箭头会以虚线表示;标记为 `→ MI P7` 的 45° 短线用于指向其他路径(开启对象标签显示时,在放大状态下可看到短线标签)。颜色用于区分类型:门为黄色,升降井为粉色,鸟传送门为淡紫色,传送器为青色,路径转换为白色。悬停某个对象时,仅会高亮显示该对象的箭头 - URL 哈希值(`#GAME/LEVEL/path/x/y/zoom`,其中 x/y 代表视图中心点)始终反映当前视图,包括任何已规划的路线 —— 复制它即可分享确切位置;无论打开它的窗口尺寸如何,它都会定位到该坐标点;浏览器的后退按钮可回溯之前的跟随操作。右上角的链条按钮会复制相同的链接,方便在没有地址栏的手机或安装版应用模式下使用 - 在 URL 后添加 **`?embed=1`**,即可获得专为维基和论坛上的 iframe 设计的视图:地图会填满整个框架,并保持完全交互性(悬停、跟随、屏幕列表面板),侧边栏初始为关闭状态,但可通过菜单按钮打开;角落的按钮可在完全相同的视图下打开完整网站。将其与任何永久链接哈希结合使用,即可嵌入特定的屏幕,例如 `https://oddworldmap.com/?embed=1#AO/R1/15/…` - **右键点击**对象可复制其直接链接 —— 打开该链接会将对象居中显示,并在其上保留一个标记,方便你查看和交互 - **搜索**(按 `/` 聚焦)可匹配两款游戏中的对象名称与解码字段 —— 试试 `mudokon`、`lcdstatusboard` 或 `switch_id=70`。组合搜索词:空格表示必须全部匹配(`Mudokon state=chisle` 只会找到正在凿刻的 Mudokon),逗号或单词 `or` 表示匹配任意一项(`Slig, Slog`)。结果按上下文分组(当前路径、当前关卡,然后按游戏划分),并优先对完全匹配名称的结果进行排名;点击(或按回车键)可直接跳转至结果处;范围栏可缩小搜索范围至当前游戏/关卡/路径 - **标尺**:启用(或按 **`m`**),然后拖拽以进行测量 —— 显示 Δx × Δy、以真实世界单位和 25 单位网格方块为基准的长度(Oddysee 的单位等于一个 PS1 屏幕像素;Exoddus 的图像对其 375×260 单位的屏幕进行了略微压缩,测量时已将此计算在内);悬停在碰撞线上会以相同方式显示其类型和长度;进行其他操作(切换路径、跟随门、更改游戏)会清除测量结果 - **路线规划器**:激活它(或按 **`r`**),然后点击路径点以规划路线 —— 每段路线都会标注其长度,顶部的一个进度条会以相同单位汇总总距离。**Backspace**(或进度条的撤销按钮)可移除最后一个路径点;清除按钮可重新开始,浏览器的后退按钮则可恢复已清除的路线。路线数据包含在 URL 中,因此复制链接即可分享与规划完全一致的路线 —— 打开时它会保持可见状态,且地图完全可浏览,无需激活任何模式 —— 它也会出现在导出的 PNG 图像中;切换路径会将其清除。如果聊天应用或论坛缩短了链接,它会使用保留下来的路径点打开,并告知缺失的数量,而不会凭空编造最后一段路线 - 在触摸设备上,单指滑动进行平移,双指捏合进行缩放;侧边栏在窄屏幕上会折叠隐藏在菜单按钮后 - **最新动态**:右上角的按钮会打开带有日期的近期更新日志;自你上次打开后新增的条目会以圆点标记 - **设置**:侧边栏顶部的齿轮按钮 —— 默认情况下,“显示”开关和“对象”过滤器会在不同访问间被记忆;关闭“记忆显示和对象过滤器”即可在每次访问时都从默认设置开始。“记住上次位置”(默认关闭)会在 URL 不包含永久链接时,在你上次离开的位置重新打开地图 —— 分享链接始终具有最高优先级。“显示全名”会将游戏、关卡和路径按钮展开为一个标注有全名的列表(例如“MI (Necrum Mines)”)。“点击时列出屏幕对象”(默认开启)会在点击/触摸未找到可跟随对象时打开屏幕清单面板。“显示更多对象字段”(默认关闭)会在侧边栏显示一个“字段”面板,你可以在其中为每种对象类型选择要在提示框和屏幕列表中显示的字段(关键项已预先勾选)—— 屏幕列表中对象旁边的 ⚙ 可在该面板中打开该类型的对应行;关闭此选项时,仅显示关键字段,且面板保持隐藏状态。“显示原始字段值”(默认关闭)会以游戏存储的原始数字(1/0、15 等)显示字段值,而不是翻译后的文本(左/右、巡逻、真/假)—— 专为自定义关卡构建者和具有开发思维的读者设计;搜索功能会匹配你当前正在查看的表示形式 —— 开启时匹配原始数字,关闭时匹配翻译文本。“在此设备上缓存屏幕图像”(默认关闭)会将访问过的屏幕存储在本地(最多约 150 MB),这样下次访问时就无需重新下载;关闭它会释放存储空间 一个网格单元 = 游戏内的一个摄像机。每个摄像机在游戏世界网格中占据一个 1024×480 单元的网格,但可见屏幕是一个 368×240 单位的窗口,中心位于 (cell·1024+440, cell·480+240) —— 世界单位与 PS1 屏幕像素呈 1:1 映射。查看器将可见窗口并排放置,并相应地转换所有对象/碰撞坐标,以确保标记准确落在图像上。 ## 从光盘镜像重新构建 仓库根目录下的所有内容(`cams/`、`map_data_ao.json`、`map_data_ae.json`)均由 [tools/build_map.py](tools/build_map.py) 从原始 PS1 光盘镜像(2352 字节扇区,例如 `.cue/.bin` 转储中的 `.bin` 文件)生成: ``` python3 tools/build_map.py --game AO --disc "/path/to/Abe's Oddysee.bin" python3 tools/build_map.py --game AE --disc "/path/to/Exoddus (Disc 1).bin" "/path/to/Exoddus (Disc 2).bin" python3 tools/build_map.py --levels R2,R6 # subset while iterating ``` 如果 `$ODDWORLD_DISC_AO` / `$ODDWORLD_DISC_AE` 指向了镜像文件,则可省略 `--disc` 参数;`$ODDWORLD_DISC_AE` 可同时保存两个光盘镜像,以 `:` 分隔(即 PATH 分隔符): ``` export ODDWORLD_DISC_AO="$HOME/games/Abe's Oddysee.bin" export ODDWORLD_DISC_AE="$HOME/games/Exoddus (Disc 1).bin:$HOME/games/Exoddus (Disc 2).bin" ``` 该脚本会在首次运行时自动编译 `tools/cam2rgba`(需要 C++17 编译器),并且需要安装 [oxipng](https://github.com/oxipng/oxipng) —— 生成的每个 PNG 都经过无损重压缩,以确保重新构建的结果与已提交的图像在字节级保持完全一致。关卡/路径表的布局被缓存在 [tools/data/pathdata_ao.json](tools/data/pathdata_ao.json) 中;只有在删除该缓存时才需要重新生成(这需要将 [alive_reversing](https://github.com/AliveTeam/alive_reversing) 代码库作为同级目录签出)。 如果重新构建导致任何已提交的摄像机 PNG 发生更改,请在同一次提交中更新 [sw.js](sw.js) 中的 `CACHE_NAME`:对于那些开启了图像缓存功能的访客,系统会通过 service worker 优先从缓存中提供摄像机数据,且从不重新验证;如果不进行版本更新,他们将无限期保留旧图像。 ## 工作原理 - **光盘 → 文件**:将原始的 MODE2/FORM1 扇区解析为 ISO9660 格式;每个关卡都是一个 `.LVL` 归档文件(32 字节标头 + 24 字节文件记录),其中包含路径数据(`xxPATH.BND`)以及每个摄像机对应的 `.CAM` 文件。 - **Path 数据块**按顺序保存:摄像机名称表(每个单元格 8 字节)、碰撞线(20 字节,包含坐标和类型),以及打包的 TLV 对象记录(在 AO 中为 0x18 字节标头,在 AE 中为 0x10 字节标头,加上特定 payload),构建器会线性遍历这些记录并按世界坐标进行放置。关卡/路径表和 AE 类型枚举是从 [alive_reversing](https://github.com/AliveTeam/alive_reversing) 反编译项目中解析并缓存到 `tools/data/` 中的。 - **摄像机背景**采用 MDEC 压缩:每个屏幕分为 12 个条带,每个条带包含一个 `u16 length` 和标准的 PS1 BS v3 比特流,解码为 32×240 尺寸后,拼接为 384×240 的图像并输出为 PNG(包含 368 列可见列 + 16 列宏块填充,由查看器裁剪)。解码器(`tools/cam2rgba.cpp`)基于 [alive_reversing](https://github.com/AliveTeam/alive_reversing) 项目中的 `PSXMDECDecoder` 构建,并进行了补丁修复,以实现对摄像机条带流的边界安全解码。 - 该查看器是零依赖的原生 JS,无需构建步骤:[index.html](index.html) 以及 [css/main.css](css/main.css) 和 [js/](js/) 目录下的 ES 模块([js/main.js](js/main.js) 负责启动应用);`map_data_ao.json` / `map_data_ae.json` 承载关卡/路径/TLV/碰撞数据。一个可选的 service worker([sw.js](sw.js),默认关闭 —— 位于设置 →“在此设备上缓存屏幕图像”)可将访问过的屏幕图像缓存在设备本地,从而在再次访问时瞬间渲染,无需重新下载。`npm run lint`(ESLint)和 `npm test`(针对无 DOM 逻辑的 `node --test` 单元测试)是唯一的开发工具;CI 会运行这两者。[annotations.json](annotations.json) 是手工维护的(非自动生成):包含光盘未提供的名称和注释 —— 为游戏中未命名的约 160 条路径提供社区命名,并为地图未渲染的关卡(如 AO 的 `S1` 主菜单关卡)提供注释。当两者并存时,精心维护的名称会刻意覆盖游戏内的名称,但必须保持游戏内标签在其内部依然可见(例如显示为“Zulag 2 — Lobby”,绝不完全抹除原标签 —— 测试代码会强制执行此规则);提取的地图数据本身永远不会被篡改。应用内的“最新动态”面板读取 [changelog.json](changelog.json)(手工维护,按时间倒序排列);[tools/changelog.py](tools/changelog.py) 会从 git log 中生成候选条目(过滤掉内部变动,并打印每个提交作为参考背景)供人工挑选维护,但该脚本本身从不写入文件。 结构布局(TLV 类型、路径表、碰撞记录)来源于 alive_reversing 反编译项目(AO 对应提交 `c1ba4c6c8`,AE 对应当前源码),与 PS1 数据格式完全匹配。AO 摄像机占据 1024×480 单位的世界网格,显示 368×240 的窗口(世界单位与像素为 1:1 比例);AE 摄像机占据 375×260 单元的网格,并在显示时进行缩放。 ## 致谢与许可 - 游戏数据格式由 [AliveTeam / alive_reversing](https://github.com/AliveTeam/alive_reversing) 项目逆向工程得出。 - [annotations.json](annotations.json) 中精心整理的路径名称参考了《Oddworld: New 'n' Tasty》的官方章节名称,以及 Steam 上的 [Barebones walkthrough](https://steamcommunity.com/sharedfiles/filedetails/?id=1812678216) 攻略,Oddysee 的大部分名称都遵循了该攻略中针对各个路径设定的标题;手工维护的名称可能会对游戏原本粗糙的标签进行润色,但提取的原始数据绝不改动。 - `tools/PSXMDECDecoder.{cpp,h}` 遵循 GPL-2.0 协议(详见文件标头;最初源自 libbs / psxdev)。其余的工具和查看器均为本项目专门编写。 - 《Oddworld: Abe's Oddysee》与《Abe's Exoddus》版权归 Oddworld Inhabitants 所有。本项目不包含任何游戏代码,仅用于研究、速通和存档目的;提取出的图像仍归其版权所有者所有。
标签:交互式地图, 前端, 数据可视化, 游戏, 游戏资源提取, 自定义脚本, 逆向工具