nooikko/serpents-eyes

GitHub: nooikko/serpents-eyes

「Serpent's Gaze」游戏的非官方只读收藏品图鉴浏览器,通过解析本地存档以卡片形式展示玩家的解锁进度与未解锁提示。

Stars: 0 | Forks: 0

# Serpent's Eyes **Serpent's Gaze** 的收藏品浏览器 —— 查看所有你已解锁和未解锁的内容。 [![Build](https://img.shields.io/github/actions/workflow/status/nooikko/serpents-eyes/ci.yml?branch=main&label=build&logo=github)](https://github.com/nooikko/serpents-eyes/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/nooikko/serpents-eyes?label=release&logo=github&sort=semver)](https://github.com/nooikko/serpents-eyes/releases/latest) [![Downloads](https://img.shields.io/github/downloads/nooikko/serpents-eyes/total?label=downloads&logo=github)](https://github.com/nooikko/serpents-eyes/releases) [![License](https://img.shields.io/github/license/nooikko/serpents-eyes)](LICENSE) [![Platform](https://img.shields.io/badge/platform-Windows-0078D4?logo=windows)](https://github.com/nooikko/serpents-eyes/releases)

The collection browser, showing unlocked Aspects with the game's own card art and a locked one greyed out

Serpent's Eyes 会读取你的存档文件,并使用游戏内的真实美术素材,将你的进度以卡片图鉴的形式展示出来。它绝对不会写入(修改)你的存档。 ## 功能特性 - **包含所有类别及完成计数** —— Aspects、Weapons(包含 Weapon Masteries)、Seeds、Blessings、Callings、Relics、Curses、Quests、Shortcuts、Boss Kills、Locations、Emotes 以及七位 Divinities。 - **展示而非隐藏未解锁内容。** 缺失的条目会显示为灰色并附带解锁提示,因此该图鉴也可当作核对清单使用。 - **将原始数值转化为直观文本。** 比如显示“Obtained ×4”和“45 boss kills while blessed”,而不是干瘪的整数。 - **以易读格式渲染缩放公式。** `3+2*{l}` 会变成 `3 + (2 × Lv)`,并附带你当前等级下的计算结果。 - **查看当前进行的 Run** —— 当前地图、位置以及装备的 Loadout。 - **设计上即为只读。** 应用程序以共享模式打开存档文件,因此可以在游戏运行时使用,且绝对不会对其进行任何写入操作。 ## 下载 从 [**Releases** 页面](https://github.com/nooikko/serpents-eyes/releases/latest) 获取最新的构建版本,解压到任意位置,然后运行 `SerpentsEyes.exe` 即可。无需安装程序,也不依赖 .NET 运行时。 由于 Serpent's Gaze 是一款 Windows 平台的游戏,因此发布的构建版本仅支持 Windows。不过,核心库是跨平台的,你可以在 Linux 和 macOS 上从源码构建并运行该应用 —— 如果你通过 Proton 进行游戏,这将非常有用,因为存档发现机制能够识别 Wine prefix。 ### 从源码构建 需要 [.NET 10 SDK](https://dotnet.microsoft.com/download)。 ``` git clone https://github.com/nooikko/serpents-eyes.git cd serpents-eyes dotnet run --project src/SerpentsEyes.App ``` 如果要生成独立可执行文件: ``` dotnet publish src/SerpentsEyes.App -c Release -r win-x64 ``` Release 构建默认启用了 AOT。输出结果为 `SerpentsEyes.exe` 以及三个原生渲染 DLL(Skia、HarfBuzz 和 ANGLE)—— 请将这四个文件放在一起复制。`.pdb` 文件是可选的调试符号。 ## 使用说明 Serpent's Eyes 会自动在 `%LOCALAPPDATA%\SerpentsGaze\Saved\SaveGames\Steam` 路径下查找你的存档并打开 `profile_0`。使用标题栏中的下拉菜单可以在不同配置文件、自动存档和备份之间进行切换。 如果你的存档位于其他位置 —— 比如更改了安装路径、从另一台机器复制过来的,或者是 Proton prefix —— 请使用 **Open…** 功能,或者直接将 `.sav` 文件拖放到程序窗口中。 **Reload** 会从磁盘重新读取当前文件。这在 Serpent's Gaze 正在运行时同样有效,因此你可以切出游戏,点击 Reload,即可查看刚刚那次 run 解锁了什么。 ## 使用核心库 `SerpentsEyes.Core` 是一个零依赖且 AOT 安全的 .NET 库。它完整封装了存档格式,完全独立于 UI 层。 ``` using SerpentsEyes.Core; var profile = SaveProfile.Load(path); // or SaveProfile.Parse(bytes) foreach (TagRecord r in profile.Records) // "Progression.Class.WellRounded" = 1 Console.WriteLine($"{r.Category}/{r.Name} = {r.Value}"); RunSnapshot run = profile.RunSnapshot; // map, position, equipped loadout profile.Find("Progression.Meta.Run.Started")!.Value = 43; // records are mutable byte[] bytes = profile.ToBytes(); profile.Save(path); // write-capable (back up first!) ``` `SaveLocator.FindProfiles()` 会在当前平台的所有可能位置查找存档文件,包括 Proton prefix;而 `SaveLocator.DefaultSaveDirectory` 返回的是标准的 Windows 路径,适合用于错误提示信息。 ### 往返保证 `SaveProfile.Parse(bytes).ToBytes()` 会**原封不动地**复现其输入内容。你没有修改的区域会从源缓冲区中被逐字拷贝回原处,因此未知的字段、罕见的字符串编码以及末尾的填充数据都能完好无损地保留下来 —— 解析器不需要理解某个字节的具体含义也能将其完整保留。 在执行编辑操作后,只有发生更改的区域会被重新编码。仅修改某一条记录的 `Value` 只会改变这四个字节,其他一切都不会受到影响,这正是基于此库进行存档编辑安全可靠的原因。 ### 游戏数据 ``` using SerpentsEyes.Core.GameData; TagDatabase.Find("Progression.Blessing.ChanceHoT")?.DisplayName; // "Daydreams" TagDatabase.FindByInternalId("Tree_Warhammer")?.DisplayName; // "Gatekeeper's Warhammer" TagDatabase.MapTitle("Majin_HolyCity"); // "Namah, City of Pilgrims" ``` 同样位于 Core 库中的还有:`TagSemantics`(将计数器转换为 Unlocked/InProgress/Locked 以及直观的文本描述)、`TagDatabase.Gods`(包含七位 Divinities 的名称、背景故事、神像提示以及游戏内的祝福锁定规则)、`UeRichText.Parse`(将游戏的富文本标记转换为带类型的片段)以及 `ScalingMath.TryEvaluate`(用于处理 `3+2*{l}` 等公式)。 关于游戏本身的专有名词体系(本应用程序也遵循这一点):mushrooms 被称为 **Callings**,utilities 被称为 **Relics**,而 items 被称为 **Seeds**。内部标签名称通常与显示名称不同 —— 例如标签 `Curse.Jester` 实际存在于资源 `CA_BardBoy` 中并显示为“The Jester”;标签 `Curse.HordeCaller` 则是“Dreamcallers”的诅咒。 ## 存档格式 (`NG_SaveFormat_4`) 该格式是通过逆向工程分析真实存档得出的;它并非标准的 Unreal GVAS。所有整数均采用小端序。字符串采用 FString 风格:包含末尾 NUL 的 `int32 length`(长度值),随后是单字节字符和 `\0`。如果长度为负数,则表示采用 UTF-16LE 编码,且字符数量为 `-length`。 单字节载荷会以 Latin-1 而非 ASCII 进行解码,因为 Latin-1 能够与全部 256 个字节值一一映射(双射)—— 如果使用 ASCII 解码器,会静默地将每个大于等于 0x80 的字节替换为 `?`。 ``` HEADER int32 unknown observed 522 int32 unknown observed 1013 int16×3 version triplet observed 5, 5, 4 byte[4] unknown observed 52 3C 00 80 fstring build id "++NinjaGarden+live" byte unknown observed 0x03 fstring format id "/Script/NinjaGarden.NG_SaveFormat_4" RECORDS int32 count count × { fstring tag; int32 value } tag = "Progression.." value = counter (1 = unlocked/done once, N = count, 0 = reached but not done) TRAILER (current-run snapshot) — absent entirely in some saves int32 pending tag count observed 0 or 1 count × fstring progression tags; see below fstring map name "Majin_HolyCity", or "None" between runs double X, Y, Z player world position float unknown observed 73.0 (possibly health) pairs × { fstring slot type ("Item"); fstring id ("Class_Stronk", …) } until a zero/implausible length is read zero padding, then FF FF FF FF ``` 有两个细节很容易弄错,而且这里也曾存在过相关的 bug: - **末尾结构中的第一个 `int32` 是一个计数值,而不是一个未知字段。** 在任何于两次 run 之间保存的存档文件中(这也占了绝大多数情况),它的值都是 `0`,因此将其视为不透明字段看起来能正常工作。然而,当其值不为零时,其后方的每一个字段都会整体偏移一个字符串的位置,导致解析出的地图名称、位置和 Loadout 全部出错。它所计数的正是进度标签 —— 例如,在刚解锁 `Progression.Item.BasicCrit` 后保存的文件中,就会确切地列出该标签。 - **`"None"` 是 Unreal 的空 `FName`,而不是地图名。** 在两次 run 之间,游戏会写入该字面量字符串而不是清空字段,因此,仅凭一个非空的地图名称,并不能证明当前正处于某次 run 进行中。 ## 重新生成游戏数据 `src/SerpentsEyes.Core/GameData/TagDatabase.g.cs` 以及 `src/SerpentsEyes.App/Assets/` 下的图标都是自动生成的。**请勿手动编辑它们。** 这通常只在游戏更新后才需要执行。 游戏将其资源打包在 UE5 IoStore 容器中(`NinjaGarden-Windows.utoc` / `.ucas`,体积约 6.8 GB),并且采用了 Oodle 压缩。提取器无法直接读取这些文件,因此第一步是使用第三方工具(例如 [retoc](https://github.com/trumank/retoc))对其进行解包。 ``` # 1. 解包游戏(第三方工具),生成 .../NinjaGarden/Content # 2. 重新生成 tag 数据库和 icon manifest dotnet run --project tools/SerpentsEyes.Extractor -- # 3. 重新导出 icons dotnet run --project tools/SerpentsEyes.Extractor -- --icons ``` 内容根目录也可以通过 `SERPENTS_GAZE_CONTENT` 环境变量来指定。报告和图标清单默认会输出到 `artifacts/` 目录;你可以使用 `--out ` 进行覆盖。使用 `--help` 运行可以查看所有指令,包括用于导出已知资源中原始字符串的 `--probe`。 提取器会解析游戏的 18 个 StringTable 资源以及约 140 个物品定义资源,并生成相应的 C# 代码和一份 JSON 报告。`--icons` 步骤会直接解码已打包的 UI 贴图 —— 包括内联的 `PF_B8G8R8A8` 以及带有 `.ubulk` 的 DXT1/5 格式 —— 因此不需要 `.usmap` 文件,接着会将它们缩小至 384 像素,并编码为 WebP 格式。这依然是它们在界面上所能绘制出的最大尺寸的两倍多,并且能将图标集的体积从 42.8 MB 骤减至 2.4 MB。 ## 开发说明 ``` dotnet build dotnet test ``` 要求使用 .NET 10(版本已在 `global.json` 中锁定)。在所有项目中,警告(Warnings)都会被视为错误(Errors)。 测试固件是真实存档的冻结副本,因此即使游戏不断向实时存档目录写入数据,测试依然能保持通过。`profile_pending_tags.sav` 专门用于覆盖非空的待处理标签列表场景,这是其他测试固件所不具备的。 欢迎参与贡献 —— 详见 [CONTRIBUTING.md](.github/CONTRIBUTING.md)。 ## 免责声明 Serpent's Eyes 是一个非官方的、粉丝制作的工具。它不隶属于、未经过授权、未获得认可,也未以任何官方形式与 Serpent's Gaze 的开发者或发行商建立联系。“Serpent's Gaze”以及所有相关的名称、标识、美术素材和游戏内容均归其各自所有者所有。 本工具仅读取已经存在于你个人电脑上的存档文件。它不会修改、规避(破解)或重新分发游戏本体。 如果你是版权方且对本仓库中的任何内容有异议,请[提交一个 issue](https://github.com/nooikko/serpents-eyes/issues),我会妥善解决。如果情况紧急,请在标题中注明。 ## 项目状态 就我个人而言,项目已基本完工 —— 它实现了我最初设想的全部功能,我也没在积极添加新特性。 不过,它并未被抛弃。我真诚地欢迎 Bug 报告和 Pull Request,而且我也会认真阅读它们。我大约每周查看一次 GitHub,因此请耐心等待几天以获取回复。 ## 许可证 源代码基于 [MIT 许可证](LICENSE) 授权。 该许可仅涵盖代码部分。它不包含以下内容,具体细节已在 [NOTICE](NOTICE) 文件中完整列出: - **游戏内容。** 游戏内的名称、描述、背景故事以及位于 `src/SerpentsEyes.App/Assets/Icons` 下的图标均为 Serpent's Gaze 版权所有者的财产,此处出于互操作性的目的进行再现。它们并未在 MIT 许可证下授权。 - **衍生自 Wiki 的文本。** 经过精心整理的解锁提示改编自 [Serpent's Gaze 社区 Wiki](https://serpents-gaze.fandom.com/),并基于 [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) 协议使用,由于该协议具有“相同方式共享”的传染性,因此与 MIT 协议不兼容。 ## 鸣谢 - [Serpent's Gaze 社区 Wiki](https://serpents-gaze.fandom.com/),提供了那些无法从游戏文件中直接恢复的解锁条件。 - [Avalonia](https://avaloniaui.net/),提供的 UI 框架。 - [retoc](https://github.com/trumank/retoc),让提取游戏资源成为可能。
标签:存档读取, 收藏浏览器, 游戏工具, 进度追踪