JulianOzelRose/TRR-SaveMaster

GitHub: JulianOzelRose/TRR-SaveMaster

一个功能强大的《古墓丽影》复刻版存档编辑器,支持跨平台深度修改物品、坐标及统计数据。

Stars: 36 | Forks: 2

# Tomb Raider I-VI Remastered 存档编辑器 这是一个适用于 Tomb Raider I-VI Remastered 的存档编辑器。使用此编辑器,您可以编辑物品、生命值、武器、弹药、统计数据和位置。 该编辑器兼容 PC、PS4 和 Nintendo Switch 的存档。但是,主机格式的 Tomb Raider I-III 存档必须先解密。 您可以在[此处](https://github.com/JulianOzelRose/TombExtract/issues/1#issuecomment-1978837071)找到更多关于如何操作的信息。 有关如何下载和使用此编辑器的说明,请向下滚动至下方的章节。如果您对逆向工程感兴趣,本 README 的底部有一个技术部分。 如果您需要一个可以在文件之间传输单个存档、将存档转换为 PC/PS4/Nintendo Switch 格式以及重新排序/删除存档的工具,请查看 [TombExtract](https://github.com/JulianOzelRose/TombExtract)。 TRR-SaveMaster-UI ## 安装和使用 要下载并使用此存档编辑器,只需转到 [Releases](https://github.com/JulianOzelRose/TRR-SaveMaster/releases) 页面, 然后在 "Assets" 下下载最新版本的 .exe 文件。您可以将其保存在计算机上的任何位置。下载后,打开该文件。 编辑器将提示您选择存档路径,点击 "Yes"。您的存档路径应如下所示: #### 《Tomb Raider I-III Remastered》 `C:\Users\USERNAME\AppData\Roaming\TRX\77777777777777777\savegame.dat` #### 《Tomb Raider IV-VI Remastered》 `C:\Users\USERNAME\AppData\Roaming\TRX2\77777777777777777\savegame.dat` 只需将 "USERNAME" 替换为您的实际用户名,并将 "77777777777777777" 替换为您看到的任何数字 ID。该数字是您的 Steam Community ID,因此如果 您有多个拥有 Tomb Raider Remastered 的帐户,可能会有多个文件夹。由于存档文件位于隐藏目录中,您必须在 Windows Explorer 中启用 "Show hidden files, folders, or drives"。选择存档路径后,您的存档应会显示在编辑器中。编辑器会记住 您的存档路径,因此无需每次都重新输入。 默认情况下,此存档编辑器假定存档为 PC 格式。要更改存档平台,点击 "Settings",然后点击 "Platform",再选择您的存档平台。 当前支持的平台是 PC、PS4 和 Nintendo Switch。一旦存档显示在编辑器中,您可以使用右上角标记为 "Savegame" 的组合框选择它们。 切换选项卡或点击存档组合框时,编辑器将自动刷新存档数据。如果添加了另一个存档但未显示, 您可以点击 "Refresh" 重新填充存档。完成更改后,点击 "Save" 应用它们。由于游戏会将存档缓存到内存中, 您必须重启游戏才能使更改生效。 此存档编辑器具有自动备份功能,它会在写入之前自动创建存档文件的备份。该功能默认启用。您可以通过点击 "File",然后选中 "Backup before saving" 来打开或关闭此功能。备份将保存在与存档文件相同的目录中,文件名后缀为 `.bak`。强烈建议您 保持此功能启用。虽然此存档编辑器已经过彻底测试并采用错误处理来防止写入错误,但没有系统是完美的。 定期备份可以在编辑过程中发生不可预见的问题或错误时保护您的进度。如果您想手动创建存档文件的备份, 您也可以通过点击 "File" 然后点击 "Create backup" 来完成。 ## 使用位置编辑器 PositionForm-UI 此存档编辑器包含位置编辑器功能。要使用它,点击 "Edit",然后选择 "Position"。为了正确解析 Lara 的坐标,必须定位生命值字节。如果找不到生命值字节,请尝试在 Lara 站立时保存游戏。进入位置编辑器菜单后,您可以传送到预定坐标,例如关卡开始、关卡结束或秘密地点。 - **X 坐标**代表 Lara 在游戏中的水平位置。减小其值会将其向左移动,而增加其值会将其向右移动。 - **Y 坐标**代表 Lara 在游戏中的垂直位置。减小其值会将其向上移动,而增加其值会将其向下移动。 - **Z 坐标**代表 Lara 在游戏中的深度位置。增加其值会将其向前移动,而减小其值会将其向后移动。 - **朝向**值决定 Lara 面对的方向,以度为单位。 - **房间/区域**值代表 Lara 或 Kurtis 当前所在的唯一房间编号/已加载区域。 房间/区域编号必须与 Lara 的当前坐标匹配,否则游戏将无法正确判断她的位置。在此菜单中点击 "Save" 应用更改,或点击 "Cancel" 保留 Lara 的当前 位置。 ## 使用统计编辑器 StatisticsForm-UI 此存档编辑器还包含统计编辑器功能。要使用它,点击 "Edit",然后选择 "Statistics"。对于 Tomb Raider I-III 和 VI,显示的统计数据是特定于关卡的,这意味着每个关卡都有自己的独立统计数据,例如所用时间、 消灭敌人和发现的秘密。对于 Tomb Raider IV 和 V,统计数据是全局的,这意味着它们跟踪所有关卡的累计进度,包括总游戏时间、总击杀数和总拾取数。 ## 暗黑模式 DarkMode-UI 如果您喜欢较暗的界面,可以从程序顶部的 Settings 菜单启用暗黑模式。 请注意,当使用非常高或非常低的 DPI 设置时,暗黑模式可能无法正确显示。 ## Tomb Raider I-III Remastered 存档格式 本节详细介绍 Tomb Raider I-III Remastered 三部曲存档逆向工程的技术方面。所有存档都存储在 `savegame.dat` 文件中。 扩展包的存档存储在与原游戏相同的槽位中。每个游戏的每个存档槽位在文件中的特定偏移量处开始,每个游戏最多 32 个槽位。请参阅下表。 | Game | Offset | |:-----------------------------------|:--------| | Tomb Raider I | 0x02000 | | Tomb Raider II | 0x72000 | | Tomb Raider III | 0xE2000 | 由于每个存档的固定大小为 0x3800 字节,因此在循环存档时可以将该值用作迭代器。 当存档槽位被占用时,偏移量 `0x004` 处的值设置为 1。当存档槽位为空时, 该值为 0。请参阅下面的代码。 ``` for (int i = 0; i < MAX_SAVEGAMES; i++) { int currentSavegameOffset = BASE_SAVEGAME_OFFSET_TR3 + (i * SAVEGAME_SIZE); byte slotStatus = fileData[currentSavegameOffset + SLOT_STATUS_OFFSET]; byte levelIndex = fileData[currentSavegameOffset + LEVEL_INDEX_OFFSET]; Int32 saveNumber = BitConverter.ToInt32(fileData, currentSavegameOffset + SAVE_NUMBER_OFFSET); bool savegamePresent = slotStatus != 0; if (savegamePresent && levelNames.ContainsKey(levelIndex) && saveNumber >= 0) { string levelName = levelNames[levelIndex]; int slot = (currentSavegameOffset - BASE_SAVEGAME_OFFSET_TR3) / SAVEGAME_SIZE; GameMode gameMode = fileData[currentSavegameOffset + GAME_MODE_OFFSET] == 0 ? GameMode.Normal : GameMode.Plus; Savegame savegame = new Savegame(currentSavegameOffset, slot, saveNumber, levelName, gameMode); cmbSavegames.Items.Add(savegame); numSaves++; } } ``` 由于您要处理存储在单个文件中的多个存档,因此需要使用相对偏移量并相应地计算它们。您可以在下面的各节中找到有关每个游戏 的更多详细信息。下表列出了所有 3 个游戏的静态偏移量。请注意,它们是相对偏移量。 因此在计算时,您必须将它们添加到基础存档偏移量中。 #### Tomb Raider I | Offset | Type | Description | |:----------|:--------|:-------------------| | 0x004 | UInt8 | Slot Occupied | | 0x008 | UInt8 | Game Mode | | 0x00C | Int32 | Save Number | | 0x4C2 | UInt16 | Magnum Ammo 1 | | 0x4C4 | UInt16 | Uzi Ammo 1 | | 0x4C6 | UInt16 | Shotgun Ammo 1 | | 0x4C8 | UInt8 | Small Medipack | | 0x4C9 | UInt8 | Large Medipack | | 0x4EC | UInt8 | Weapons | | 0x610 | Int32 | Crystals Used | | 0x614 | Int32 | Time Taken | | 0x618 | Int32 | Ammo Used | | 0x61C | Int32 | Hits | | 0x620 | Int32 | Kills | | 0x624 | UInt32 | Distance Travelled | | 0x628 | UInt16 | Secrets Found | | 0x62A | Int8 | Pickups | | 0x62B | Int8 | Medi Packs Used | | 0x62C | UInt8 | Level Index | #### Tomb Raider II | Offset | Type | Description | |:----------|:--------|:-------------------| | 0x004 | UInt8 | Slot Occupied | | 0x008 | UInt8 | Game Mode | | 0x00C | Int32 | Save Number | | 0x610 | Int32 | Time Taken | | 0x614 | Int32 | Ammo Used | | 0x618 | Int32 | Hits | | 0x61C | Int32 | Kills | | 0x620 | UInt32 | Distance Travelled | | 0x624 | UInt16 | Secrets Found | | 0x626 | Int8 | Pickups | | 0x627 | Int8 | Medi Packs Used | | 0x628 | UInt8 | Level Index | #### Tomb Raider III | Offset | Type | Description | |:----------|:--------|:-------------------| | 0x004 | UInt8 | Slot Occupied | | 0x008 | UInt8 | Game Mode | | 0x00C | Int32 | Save Number | | 0x8A4 | Int32 | Crystals Found | | 0x8A8 | Int32 | Crystals Used | | 0x8AC | Int32 | Time Taken | | 0x8B0 | Int32 | Ammo Used | | 0x8B4 | Int32 | Hits | | 0x8B8 | Int32 | Kills | | 0x8BC | UInt32 | Distance Travelled | | 0x8C0 | UInt16 | Secrets Found | | 0x8C2 | Int8 | Pickups | | 0x8C3 | Int8 | Medi Packs Used | | 0x8D6 | UInt8 | Level Index | ## 使用启发式方法查找生命值偏移量 在所有 3 个游戏中,生命值存储为 UInt16 值,范围从 1(最低可能的健康值)到 1000(最大生命值)。因为 生命值是动态分配的,所以需要使用启发式方法来确定其位置。当额外的实体变为活动状态时,生命值偏移量倾向于移动 到更高的地址,而当实体变为非活动状态或死亡时,则移动到更低的地址。 由于生命值始终存储在角色移动数据旁边,因此可以通过检查周围数据的 角色移动字节标志来确定当前的生命值偏移量。以下算法用于 Tomb Raider III 生命值检测。 首先,它遍历关卡的预定生命值偏移量范围。接下来,它检查当前偏移量的值 是否落在有效的生命值范围内(1 到 1000,含)。如果该值落在有效范围内,它会通过 检查周围数据的角色移动字节标志再执行一次启发式检查。如果找到有效模式,则返回该 偏移量作为当前生命值偏移量。 该算法能够在约 96% 的时间内确定正确的生命值偏移量。虽然理论上可以通过 添加更多角色移动字节标志来提高检测率,但这样做会导致误报,因为某些角色移动 字节标志与空填充或其他无关数据重合。 ``` public int GetHealthOffset() { byte[] savegameData; using (FileStream fs = new FileStream(savegamePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { savegameData = new byte[fs.Length]; fs.Read(savegameData, 0, savegameData.Length); } for (int offset = MIN_HEALTH_OFFSET; offset <= MAX_HEALTH_OFFSET; offset++) { int valueIndex = savegameOffset + offset; if (valueIndex + 1 >= savegameData.Length) { break; } UInt16 value = BitConverter.ToUInt16(savegameData, valueIndex); if (value >= MIN_HEALTH_VALUE && value <= MAX_HEALTH_VALUE) { int flagIndex1 = savegameOffset + offset - 7; int flagIndex2 = savegameOffset + offset - 6; int flagIndex3 = savegameOffset + offset - 5; int flagIndex4 = savegameOffset + offset - 4; if (flagIndex4 >= savegameData.Length) { continue; } byte byteFlag1 = savegameData[flagIndex1]; byte byteFlag2 = savegameData[flagIndex2]; byte byteFlag3 = savegameData[flagIndex3]; byte byteFlag4 = savegameData[flagIndex4]; if (IsKnownByteFlagPattern(byteFlag1, byteFlag2, byteFlag3, byteFlag4)) { return savegameOffset + offset; } } } return -1; } ``` ## 使用位运算确定和写入当前武器 在所有 3 个游戏中,武器信息存储在单个偏移量上,在此编辑器的代码中称为 `weaponsConfigNum`。它的基准数为 1, 表示不存在武器。添加的每个武器对应一个唯一的字节标志,可以在下面的章节中找到。要确定 库存中存在哪些武器,您可以对武器配置编号使用位运算。下面的代码演示了如何为 Tomb Raider II 执行此操作。 ``` private const byte WEAPON_PISTOLS = 2; private const byte WEAPON_AUTOMATIC_PISTOLS = 4; private const byte WEAPON_UZIS = 8; private const byte WEAPON_SHOTGUN = 16; private const byte WEAPON_M16 = 32; private const byte WEAPON_GRENADE_LAUNCHER = 64; private const byte WEAPON_HARPOON_GUN = 128; byte weaponsConfigNum = GetWeaponsConfigNum(fileData); if (weaponsConfigNum == 1) { chkPistols.Checked = false; chkAutomaticPistols.Checked = false; chkUzis.Checked = false; chkShotgun.Checked = false; chkM16.Checked = false; chkGrenadeLauncher.Checked = false; chkHarpoonGun.Checked = false; } else { chkPistols.Checked = (weaponsConfigNum & WEAPON_PISTOLS) != 0; chkAutomaticPistols.Checked = (weaponsConfigNum & WEAPON_AUTOMATIC_PISTOLS) != 0; chkUzis.Checked = (weaponsConfigNum & WEAPON_UZIS) != 0; chkShotgun.Checked = (weaponsConfigNum & WEAPON_SHOTGUN) != 0; chkM16.Checked = (weaponsConfigNum & WEAPON_M16) != 0; chkGrenadeLauncher.Checked = (weaponsConfigNum & WEAPON_GRENADE_LAUNCHER) != 0; chkHarpoonGun.Checked = (weaponsConfigNum & WEAPON_HARPOON_GUN) != 0; } ``` 写入此变量时,逻辑相同,只是相反。从 基准数 1 开始,并根据界面中勾选的武器有条件地递增。 请参阅下面的代码。 ``` byte newWeaponsConfigNum = 1; if (chkPistols.Checked) newWeaponsConfigNum += WEAPON_PISTOLS; if (chkAutomaticPistols.Checked) newWeaponsConfigNum += WEAPON_AUTOMATIC_PISTOLS; if (chkUzis.Checked) newWeaponsConfigNum += WEAPON_UZIS; if (chkShotgun.Checked) newWeaponsConfigNum += WEAPON_SHOTGUN; if (chkM16.Checked) newWeaponsConfigNum += WEAPON_M16; if (chkGrenadeLauncher.Checked) newWeaponsConfigNum += WEAPON_GRENADE_LAUNCHER; if (chkHarpoonGun.Checked) newWeaponsConfigNum += WEAPON_HARPOON_GUN; WriteWeaponsConfigNum(fileData, newWeaponsConfigNum); ``` ## Tomb Raider I 存档格式 由于 Tomb Raider I 中的几乎所有偏移量都是静态的,因此它是三部曲中最容易逆向的游戏。武器库存配置 存储在单个偏移量上,在此编辑器的代码中称为 `weaponsConfigNum`。它的基准数为 1,表示不存在武器。 您可以使用位运算来确定库存中存在哪些武器。每种武器对应一个特定的字节标志。请参阅下表。 | Weapon | Byte flag | |:---------|:----------| | Pistols | 2 | | Magnums | 4 | | Uzis | 8 | | Shotgun | 16 | 弹药存储在最多两个偏移量上。如果未装备武器,则仅存储在一个偏移量上(主要)。装备了武器,则存储在 两个偏移量上(主要和次要)。Tomb Raider I 中的主要偏移量是静态的。虽然次要偏移量是动态的,但它们仅根据 关卡变化——因此一旦根据关卡索引确定它们,就无需重新计算它们。从库存中移除武器时,编辑器 将次要弹药字节清零以释放其地址空间。请参阅下面的代码。 ``` private void WriteShotgunAmmo(byte[] fileData, bool isPresent, UInt16 ammo) { WriteUInt16ToBuffer(fileData, savegameOffset + SHOTGUN_AMMO_OFFSET, ammo); if (isPresent) { WriteUInt16ToBuffer(fileData, savegameOffset + shotgunAmmoOffset2, ammo); } else { WriteUInt16ToBuffer(fileData, savegameOffset + shotgunAmmoOffset2, 0); } } ``` ## Tomb Raider II 存档格式 逆向 Tomb Raider II 比 Tomb Raider I 提出了更多挑战。这是因为游戏的大部分偏移量都是动态的。然而,武器的存储方式 与 Tomb Raider I 相同;在单个偏移量上。您可以像在 Tomb Raider I 中一样使用位运算来提取库存中存在的武器。 请参阅下表以获取武器字节标志。 | Weapon | Byte flag | |:-----------------|:-----------------| | Pistols | 2 | | Automatic Pistols| 4 | | Uzis | 8 | | Shotgun | 16 | | M16 | 32 | | Grenade Launcher | 64 | | Harpoon Gun | 128 | Tomb Raider II 存档中只有很少的静态偏移量;只有关卡索引、存档编号和统计数据是静态存储的。其他一切都必须 动态计算。这可以仅基于关卡索引来完成。请参阅下面的代码。 ``` byte levelIndex = GetLevelIndex(fileData); AUTOMATIC_PISTOLS_AMMO_OFFSET = 0x12 + (levelIndex * 0x30); UZI_AMMO_OFFSET = 0x14 + (levelIndex * 0x30); SHOTGUN_AMMO_OFFSET = 0x16 + (levelIndex * 0x30); M16_AMMO_OFFSET = 0x18 + (levelIndex * 0x30); GRENADE_LAUNCHER_AMMO_OFFSET = 0x1A + (levelIndex * 0x30); HARPOON_GUN_AMMO_OFFSET = 0x1C + (levelIndex * 0x30); SMALL_MEDIPACK_OFFSET = 0x1E + (levelIndex * 0x30); LARGE_MEDIPACK_OFFSET = 0x1F + (levelIndex * 0x30); FLARES_OFFSET = 0x21 + (levelIndex * 0x30); WEAPONS_CONFIG_NUM_OFFSET = 0x3C + (levelIndex * 0x30); ``` 请注意,该代码片段中计算的弹药偏移量只是主要弹药偏移量。次要弹药偏移量必须以不同的方式计算。不仅 次要弹药偏移量是根据关卡动态分配的,而且它们还在整个关卡中动态分配。换句话说,它们会发生移动。 为了计算次要弹药偏移量,您需要找到弹药索引。弹药索引与游戏中活动实体的数量相关。如果 有 0 个实体,则索引为 0。如果有 2 个实体,则索引为 2,依此类推。 有一个由 `{0xFF, 0xFF, 0xFF, 0xFF}` 组成的 4 字节数组,它位于存档的空填充之前,并随次要弹药偏移量一致移动。 此数组的位置可用于计算基础次要弹药偏移量以及次要弹药索引本身。虽然距离大致一致, 但有一些例外。每个索引对应 0xFF 数组的两个可能位置。第二个位置距离第一个位置 +0xA 字节。请参阅下面的代码。 ``` private int GetSecondaryAmmoIndex(byte[] fileData) { byte levelIndex = GetLevelIndex(fileData); Dictionary ammoIndexData = platform == Platform.PC ? ammoIndexDataPC : ammoIndexDataConsole; if (ammoIndexData.ContainsKey(levelIndex)) { int[] indexData = ammoIndexData[levelIndex]; int[] offsets1 = new int[indexData.Length]; int[] offsets2 = new int[indexData.Length]; for (int index = 0; index < MAX_ENTITY_COUNT; index++) { Array.Copy(indexData, offsets1, indexData.Length); for (int i = 0; i < indexData.Length; i++) { offsets2[i] = offsets1[i] + 0xA; offsets1[i] += savegameOffset + (index * 0xC); offsets2[i] += savegameOffset + (index * 0xC); } if (offsets1.All(offset => fileData[offset] == 0xFF)) { return index; } if (offsets2.All(offset => fileData[offset] == 0xFF)) { return index; } } } return -1; } ``` 一旦确定了次要弹药索引,剩下的就是计算偏移量并写入弹药值。与 Tomb Raider I 实现类似,编辑器在移除武器时也会将次要弹药字节清零以释放其地址空间。然而,由于 Tomb Raider II 中 弹药索引的动态性质,重要的是要考虑无法找到弹药索引的边缘情况。在这种情况下,编辑器 仅写入主要偏移量以避免损坏存档。 ``` private void WriteAutomaticPistolsAmmo(byte[] fileData, bool isPresent, UInt16 ammo) { WriteUInt16ToBuffer(fileData, savegameOffset + AUTOMATIC_PISTOLS_AMMO_OFFSET, ammo); if (isPresent && secondaryAmmoIndex != -1) { WriteUInt16ToBuffer(fileData, savegameOffset + automaticPistolsAmmoOffset2, ammo); } else if (!isPresent && secondaryAmmoIndex != -1) { WriteUInt16ToBuffer(fileData, savegameOffset + automaticPistolsAmmoOffset2, 0); } } ``` ## Tomb Raider III 存档格式 与 Tomb Raider II 类似,Tomb Raider III 中的大部分偏移量都是动态的。唯一的例外是存档编号、关卡索引和统计数据。 您可以像在 Tomb Raider II 中一样,根据关卡索引计算大多数剩余的偏移量。请参阅下面的代码。 ``` byte levelIndex = GetLevelIndex(fileData); DEAGLE_AMMO_OFFSET = 0x66 + (levelIndex * 0x40); UZI_AMMO_OFFSET = 0x68 + (levelIndex * 0x40); SHOTGUN_AMMO_OFFSET = 0x6A + (levelIndex * 0x40); MP5_AMMO_OFFSET = 0x6C + (levelIndex * 0x40); ROCKET_LAUNCHER_AMMO_OFFSET = 0x6E + (levelIndex * 0x40); HARPOON_GUN_AMMO_OFFSET = 0x70 + (levelIndex * 0x40); GRENADE_LAUNCHER_AMMO_OFFSET = 0x72 + (levelIndex * 0x40); SMALL_MEDIPACK_OFFSET = 0x74 + (levelIndex * 0x40); LARGE_MEDIPACK_OFFSET = 0x75 + (levelIndex * 0x40); FLARES_OFFSET = 0x77 + (levelIndex * 0x40); COLLECTIBLE_CRYSTALS_OFFSET = 0x78 + (levelIndex * 0x40); WEAPONS_CONFIG_NUM_OFFSET = 0xA0 + (levelIndex * 0x40); HARPOON_GUN_OFFSET = 0xA1 + (levelIndex * 0x40); ``` 武器信息也存储在单个偏移量上,与 Tomb Raider II 相同。唯一的例外是鱼叉枪,它存储 在自己的偏移量上作为布尔值,距离武器配置编号 +1 字节。您可以使用位运算来提取 库存中存在的武器以及字节标志。请参阅下表以获取 Tomb Raider III 武器字节标志。 | Weapon | Byte flag | |:-----------------|:-----------------| | Pistols | 2 | | Desert Eagle | 4 | | Uzis | 8 | | Shotgun | 16 | | MP5 | 32 | | Rocket Launcher | 64 | | Grenade Launcher | 128 | 弹药的存储方式与 Tomb Raider II 类似。主要偏移量和次要偏移量的逻辑仍然适用;未装备的武器 仅在主要偏移量上存储弹药,而装备的武器在两个偏移量上都存储弹药。唯一的例外似乎是鱼叉枪,无论 是否装备,它都将弹药值存储在两个偏移量上。次要弹药索引也与活动实体的数量相关,并且 可以通过 `{0xFF, 0xFF, 0xFF, 0xFF}` 数组的位置确定索引。 Tomb Raider III 中的弹药索引通常移动 0x1A 的值。然而,就像在 Tomb Raider II 中一样,这种模式有一些例外。每个索引对应 0xFF 数组的两个可能位置,第二个数组距离第一个数组 +0xA 字节。请参阅下面的代码以计算次要弹药索引。 ``` private int GetSecondaryAmmoIndex(byte[] fileData) { byte levelIndex = GetLevelIndex(fileData); Dictionary ammoIndexData = platform == Platform.PC ? ammoIndexDataPC : ammoIndexDataConsole; if (ammoIndexData.ContainsKey(levelIndex)) { int[] indexData = ammoIndexData[levelIndex]; int[] offsets1 = new int[indexData.Length]; int[] offsets2 = new int[indexData.Length]; for (int index = 0; index < MAX_ENTITY_COUNT; index++) { Array.Copy(indexData, offsets1, indexData.Length); for (int i = 0; i < indexData.Length; i++) { offsets2[i] = offsets1[i] + 0xA; offsets1[i] += savegameOffset + (index * 0x1A); offsets2[i] += savegameOffset + (index * 0x1A); } if (offsets1.All(offset => fileData[offset] == 0xFF)) { return index; } if (offsets2.All(offset => fileData[offset] == 0xFF)) { return index; } } } return -1; } ``` 一旦确定了次要弹药索引并计算了偏移量,写入弹药偏移量的过程与 Tomb Raider II 中的相同。 移除武器时,必须将次要弹药字节清零以释放其地址空间。如果无法确定弹药索引,编辑器将仅写入 主要偏移量以避免损坏存档。 ``` private void WriteRocketLauncherAmmo(byte[] fileData, bool isPresent, UInt16 ammo) { WriteUInt16ToBuffer(fileData, savegameOffset + ROCKET_LAUNCHER_AMMO_OFFSET, ammo); if (isPresent && secondaryAmmoIndex != -1) { WriteUInt16ToBuffer(fileData, savegameOffset + rocketLauncherAmmoOffset2, ammo); } else if (!isPresent && secondaryAmmoIndex != -1) { WriteUInt16ToBuffer(fileData, savegameOffset + rocketLauncherAmmoOffset2, 0); } } ``` ## Tomb Raider IV-VI Remastered 存档格式 本节详细介绍 Tomb Raider IV-VI Remastered 三部曲存档逆向工程的技术方面。与第一部三部曲一样,所有存档都存储在 `savegame.dat` 文件中。 每个游戏的每个存档槽位在文件中的特定偏移量处开始,每个游戏最多 32 个槽位。每个存档的固定大小为 0xA470 字节。请参阅下表。 | Game | Offset | |:-----------------------------------|:---------| | Tomb Raider IV | 0x002000 | | Tomb Raider V | 0x14AE00 | | Tomb Raider VI | 0x293C00 | 下面是 Tomb Raider IV-VI 的偏移量表。除生命值外,大多数偏移量都是静态的。对于 Tomb Raider VI,该表仅显示头部偏移量,因为存档数据非常动态。 #### Tomb Raider IV | Offset | Type | Description | |:----------|:--------|:------------------------| | 0x004 | Int32 | Slot Status | | 0x008 | Int32 | Save Number | | 0x01C | Int32 | Game Mode | | 0x26F | UInt8 | Level Index | | 0x1BE | UInt16 | Small Medipack | | 0x1C0 | UInt16 | Large Medipack | | 0x1C2 | UInt16 | Flares | | 0x194 | UInt8 | Pistols | | 0x195 | UInt8 | Uzi | | 0x196 | UInt8 | Shotgun | | 0x197 | UInt8 | Crossbow | | 0x199 | UInt8 | Grenade Gun | | 0x19A | UInt8 | Revolver | | 0x1C6 | UInt16 | Uzi Ammo | | 0x1C8 | UInt16 | Revolver Ammo | | 0x1CA | UInt16 | Shotgun Normal Ammo | | 0x1CC | UInt16 | Shotgun Wideshot Ammo | | 0x1D0 | UInt16 | Grenade Gun Normal Ammo | | 0x1D2 | UInt16 | Grenade Gun Super Ammo | | 0x1D4 | UInt16 | Grenade Gun Flash Ammo | | 0x1D6 | UInt16 | Crossbow Normal Ammo | | 0x1D8 | UInt16 | Crossbow Poison Ammo | | 0x1DA | UInt16 | Crossbow Explosive Ammo | | 0x230 | Int32 | Time Taken | | 0x234 | UInt32 | Distance Travelled | | 0x238 | Int16 | Ammo Used | | 0x240 | Int32 | Pickups | | 0x244 | UInt16 | Kills | | 0x246 | UInt8 | Secrets Found | | 0x247 | UInt8 | Health Packs Used | | 0x280 | Int32 | Vessels Broken | #### Tomb Raider V | Offset | Type | Description | |:----------|:--------|:-----------------------------| | 0x004 | Int32 | Slot Status | | 0x008 | Int32 | Save Number | | 0x01C | Int32 | Game Mode | | 0x26F | UInt8 | Level Index | | 0x1BE | UInt16 | Small Medipack | | 0x1C0 | UInt16 | Large Medipack | | 0x1C2 | UInt16 | Flares | | 0x194 | UInt8 | Pistols | | 0x195 | UInt8 | Uzi | | 0x196 | UInt8 | Shotgun | | 0x197 | UInt8 | Grappling Gun | | 0x198 | UInt8 | HK Gun | | 0x19A | UInt8 | Revolver / Desert Eagle | | 0x1C6 | UInt16 | Uzi Ammo | | 0x1C8 | UInt16 | Revolver / Desert Eagle Ammo | | 0x1CA | UInt16 | Shotgun Normal Ammo | | 0x1CC | UInt16 | Shotgun Wideshot Ammo | | 0x1CE | UInt16 | HK Gun Ammo | | 0x1D6 | UInt16 | Grappling Gun Ammo | | 0x230 | Int32 | Time Taken | | 0x234 | UInt32 | Distance Travelled | | 0x238 | Int16 | Ammo Used | | 0x240 | Int32 | Pickups | | 0x244 | UInt16 | Kills | | 0x246 | UInt8 | Secrets Found | | 0x247 | UInt8 | Health Packs Used | #### Tomb Raider VI | Offset | Type | Description | |:----------|:--------|:-----------------------------| | 0x004 | Int32 | Slot Status | | 0x014 | UInt8 | Level Index | | 0x11C | Int32 | Save Number | | 0x240 | Int32 | Time Taken | | 0x244 | UInt32 | Distance Travelled | | 0x248 | Int32 | Ammo Used | | 0x24C | Int32 | Hits | | 0x250 | UInt16 | Pickups | | 0x252 | UInt16 | Health Items Found | | 0x254 | UInt8 | Chocobars Found | | 0x256 | UInt16 | Kills | | 0x258 | UInt8 | Health Restored | | 0x35C | Int32 | Game Mode | | 0x364 | Int32 | Compressed Block Size | ## Tomb Raider IV 存档格式 Tomb Raider IV 基于前三款游戏所使用引擎的深度修改版本。有一些相似之处,但由于它包含 较少的动态数据,因此更容易解读数据结构。在 Tomb Raider IV 中,武器均以 UInt8 格式存储在静态偏移量上。`0x9` 是 “存在”标志,`0xD` 是“带瞄准镜存在”标志。 与 Tomb Raider I-III 一样,生命值偏移量始终存储在角色动画数据之后。 然而,Tomb Raider IV 的生命值是否有条件地存储在存档中,取决于它是满(或“默认”)还是部分。换句话说, 满生命值 (1000) 永远不会存储在存档中。它的存在由存储在生命值偏移量之前 0x13 字节处的字节标志指示。 | Flag | Meaning | |:----------|:---------------| | 0x008 | Full health | | 0x00C | Partial health | 因此,仅仅写入新的生命值并相应地更改标志是不够的。如果您从部分生命值切换到满生命值, 游戏将不再期望生命值字节存储在缓冲区中。因此,必须相应地移动生命值偏移量之后的字节。 请参阅下面的代码。 ``` private void WriteHealthValue(byte[] fileData, UInt16 newHealth) { int healthOffset = GetHealthOffset(); if (healthOffset != -1) { int toggleOffset = healthOffset - 0x13; byte currentToggle = fileData[toggleOffset]; bool currentlyFull = (currentToggle == FULL_HEALTH_TOGGLE_BYTE); bool currentlyPartial = (currentToggle == PARTIAL_HEALTH_TOGGLE_BYTE); bool newIsPartial = newHealth < MAX_HEALTH_VALUE; if (currentlyFull && newIsPartial) { // Full health -> Partial health fileData[toggleOffset] = (byte)(currentToggle + TOGGLE_DELTA); WriteUInt16ToBuffer(fileData, healthOffset, newHealth); ShiftBytesRight(ref fileData, healthOffset); } else if (currentlyPartial && !newIsPartial) { // Partial health -> Full health fileData[toggleOffset] = (byte)(currentToggle - TOGGLE_DELTA); WriteUInt16ToBuffer(fileData, healthOffset, 0); ShiftBytesLeft(ref fileData, healthOffset); } else if (currentlyFull && !newIsPartial) { // Already full health WriteUInt16ToBuffer(fileData, healthOffset, 0); } else { // Partial health -> Partial health WriteUInt16ToBuffer(fileData, healthOffset, newHealth); } } } ``` ## Tomb Raider V 存档格式 Tomb Raider V 使用的引擎与 Tomb Raider IV 引擎几乎相同。这反映在存档中,几乎所有内容都是 相同的;武器以 UInt8 形式静态存储,弹药以 UInt16 形式静态存储。Tomb Raider 和 V 存档格式之间的唯一区别是 存储生命值的方式。Tomb Raider V 不会根据生命值是否已满而进行有条件的存储。无论 其值如何,都会存储生命值。与 Tomb Raider IV 和之前的游戏一样,生命值也存储在角色动画数据之后,因此这可用于检测动态偏移量。 ## Tomb Raider VI 存档格式 Tomb Raider VI 使用的引擎与前五个版本明显不同。头部主要存储存档元数据,例如存档编号、关卡编号、时间戳 和统计数据。其余存档数据使用无损 [LZW](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch) 压缩算法的定制变体进行压缩。 存档数据的压缩部分从头部的偏移量 `0x36C` 处开始。 库存块通常是主要的关注块。但是,库存块存储在缓冲区的末尾。没有指向库存起始偏移量的指针。 因此,准确确定库存偏移量的唯一方法是复制游戏为到达库存块而执行的每次读取操作。游戏确实会根据 存档数据执行一些有条件的读取,因此需要正确复制这些有条件的读取。游戏还会根据运行时分配的实体数据执行有条件的读取。这是逆向 Tomb Raider VI 格式最具挑战性的方面。下表显示了压缩缓冲区结构。 | Block | Size | |:----------------|:-----------------| | Header | 0x009 | | Inv | 0x12F | | Map | Dynamic | | Cam1 | 0x044 | | Cam2 | 0x044 | | Cam3 | 0x044 | | FX | Dynamic | | Audio | Dynamic | | Pickup | Dynamic | | Inv2 | Dynamic | 首先是头部块(不要与压缩缓冲区外部的存档头部混淆),它存储静态的 "TOMB" 签名字符串,后跟关卡 (UInt8) 和已加载区域 (Int32)。 接下来是 `Inv` 块,它存储更多的游戏状态元数据,例如现金和对话标志。接下来是 `Map` 块,它是迄今为止最大且最动态的。下面的项目符号列表描述了 实体在 Map 块中存储/加载的层次结构。 1. **Actors** 2. **Objects** 3. **Triggers** 4. **Emitters** 5. **Water** 6. **Audio Locators** 7. **Rooms** 除了 Water 之外,所有这些实体都是在运行时分配的。由于游戏根据运行时实体的属性执行有条件的读取,因此还需要逆向 游戏的 WAD 文件格式 (GMX),特别是对于有条件读取所需的属性。即 Actors 和 Objects 的 APB 值,Actors 的“活动标志”,以及 特定 Actor 是否是活动玩家。对于 Triggers、Emitters 和 Audio Locators,只需要实体计数。 `Inv2` 块存储 Lara 和 Kurtis 的库存数据以及活动玩家的生命值。物品计数存储为 UInt8 值。各个玩家的实际库存数组存储 在物品计数之后。Lara 的库存数组首先存储,然后是 Kurtis 的库存。以下是 Tomb Raider VI 的库存物品结构。 ``` struct InventoryItem { uint16_t ClassId; int Type; int Quantity; }; ``` `ClassId` 字段代表与特定物品关联的唯一 ID。`Type` 字段代表物品将存储在哪个库存字段中(即 Health、Item、Weapons、Notebook)。下表 显示了 `Type` 对应的内容。 | Type | Description | |:-----------|:-----------------| | -1 | Notebook Item | | 2 | Item | | 3 | Weapon | | 4 | Health Item | | 7 | Ammo |
标签:Nintendo Switch, PC游戏, PS4, Steam, Tomb Raider, 二进制编辑, 云资产清单, 作弊工具, 古墓丽影, 多人体追踪, 存档修改器, 存档管理, 存档编辑, 数据解析, 数据转换, 武器编辑, 游戏修改, 游戏工具, 物品修改, 界面工具, 血量修改, 逆向工程