|
Post by picoleet on Aug 19, 2021 4:00:44 GMT 10
Greetings! I am a new member here, so, hello to everyone! I could not find any save editor for steam version of Resident Evil 5 so I decided to create one myself: re5_save_editor.py (Disclaimer: I play and work in Linux so no binaries and/or C#,VB code) Here's how to use it: - Find your 8-byte steam ID. For example, this page describes how to get it.
- Make a backup copy of your savedata.bin (location of savedata.bin).
- Run the script with appropriate command: To decode the data run:
./re5_save_editor.py --key <your-key> decode savedata.bin . The decoded data will be saved in a new file with name "decoded_"<input_file_name> -- "decoded_savedata.bin" in the running example. Now you can edit per your wishes (see below for description of bytes/bits). Once you are content with your changes, you need to encode it back:./re5_save_editor.py --key <your-key> encode decoded_savedata.bin . The output file will be named "encoded_"<input_file_name> -- "encoded_decoded_savedata.bin" in the running example. - Overwrite the savedata.bin with your version.
- Run the game and verify the changes!
Description of decoded savedata.bin file formatI am still working on understanding the semantics of the file format. Here's the full list of formatting I know so far: [Offset] : Data type : name // comment ---------------------------------------
[0x0] : uint64 : magic? [0x8] : uint32 : checksum [0xc] : uint32 : ??? // always zero? // date [0x10] : uint32 : year // 0x7e5 == 2021 [0x14] : uint32 : month // 0x8 == Aug [0x18] : uint32 : day // 0x12 == 18 // time [0x1c] : uint32 : hours // 0x9 == 9 [0x24] : uint32 : minutes // 0xf == 15 [0x28] : uint32 : seconds // 0x17 == 23 .... [0x50] : uint32 : chris_costume // { bsaa = 0, safari = 1, stars = 2 } [0x54] : uint32 : sheva_costume // { bsaa = 0, clubbin = 1, tribal = 2 } [0x58] : uint32 : filter // { default = 0, classic = 1, retro = 2 } ... [0x130] : inf ammo start? [0x138] : figures start [0x13d] : upto 6 bits -- figures end [0x131] :4: S75 inf ammo [0x131] :5: Dragunov SVD inf ammo [0x131] :6: H&K PSG-1 inf ammo [0x131] :7: S&W M29 inf ammo ... [0x194] : int32 : money [0x198] : int32 : pts
A full documentation of the format will obviously will take some effort, not to mention the tediousness of the task. But I will try to document as much as possible in coming days. Any feedback/comments would be appreciated! Thanks!
|
|
|
Post by picoleet on Sept 5, 2021 23:20:59 GMT 10
I have covered more ground in this reverse engineering effort. I have figured out options, bonus features, costumes, filters, infinite ammo, level completion, records, inventory, treasures and related codes and offsets. What is remaining is mostly completionist thing: level timer, enemies killed so far in the level, etc.
The following gist details my findings. I have used C-like syntax for describing the structure/records and offset is put inside square brackets in hexadecimal like this: [0x8] : uint32 : checksum
struct savedata.bin { [0x0] : magic? [0x8] : uint32 : checksum [0xc] : uint32 : ??? // always zero? // date [0x10] : uint32 : year // 0x7e5 == 2021 [0x14] : uint32 : month // 0x8 == Aug [0x18] : uint32 : day // 0x12 == 18 // time [0x1c] : uint32 : hours // 0x9 == 9 [0x20] : uint32 : minutes // 0xf == 15 [0x24] : uint32 : seconds // 0x17 == 23 //--- options start // controller settings [0x28] : uint32 : vibration // { on = 0, off = 1 } [0x2c] : uint32 : aiming_ctrl // { default = 0, invert_y = 1, invert_x = 2, invert_both = 3 } [0x30] : uint32 : aiming_speed // { slowest = 0, slow = 1, default = 2, fast = 3, fastest = 4 } [0x34] : uint32 : ctrl_type // { type_a = 0, type_b = 1, type_c = 2, type_d = 3 } // screen settings [0x38] : uint32 : brightness // lowest = 0, highest = 40 // game settings [0x3c] : uint32 : tutorial // { on = 0, off = 1 } [0x40] : uint32 : subtitles // { on = 0, off = 1 } [0x44] : uint32 : ???? // audio settings [0x48] : uint32 : bgm_vol // lowest = 0, highest = 100 [0x4c] : uint32 : sfx_vol // lowest = 0, highest = 100 // --- options end // --- special settings start [0x50] : uint32 : chris_costume // { bsaa = 0, safari = 1, stars = 2 } see below for DLC [0x54] : uint32 : sheva_costume // { bsaa = 0, clubbin = 1, tribal = 2 } see below for DLC [0x58] : uint32 : filter // { default = 0, classic(b&w) = 1, retro(sephia) = 2, noise(grainy) = 3 } [0x5c] : uint32 : inf ammo // { off = 0, on = 1 } [0x60] : uint32 : coop_mode // { no_limits = 0, invite_only = 1, rogue = 2 } [0x60] : uint32 : attack_reaction // { off = 0, on = 1 } // --- special settings end [0x64] : uint32 : ???? // padding, always 0??? // kb & mouse settings [0x68] : uint32 : ctrl_type // { mouse_look = 0, cursor_aiming = 1 } [0x6c] : uint32 : mouse_sensitivity // { one = 0, ..., ten = 9 } [0x70] : uint32 : invert_mouse // { default = 0, invert_y = 1, invert_x = 2, invert_both = 2 } [0x74] : uint32 : swap_buttons // { off = 0, on = 1 } [0x78] : uint32 : item_menu_prior // { numerical = 0, dpad = 1 } [0x7c] : uint32 : key_move_fwd [0x80] : uint32 : key_move_bkwd ... // keys [0x114] : uint32 : key_taunt // kb & mouse settings end?? [0x118] : ??? [0x11c] : uint32 : ??? // 0 in new save file, non-zero (0x2d78) in older [0x120] : uint32 : ??? // 0 in new save file, non-zero (0xf) in older // --- bonus features start [0x124] : uint32 : chris costumes purchased // total 5, 1 per bit: [bsaa, safari, stars, warrior, heavy_metal ] XXX does unlocking happen here? [0x128] : uint32 : sheva costumes purchased // total 5, 1 per bit: [ bsaa, clubbin, tribal, business, fairy_tale ] [0x12c] : uint32 : filters // total 3, 1 per bit: [ default, classic, retro, noise ] [0x130] : uint32 : inf ammo // total 18, 1 per bit, 0 -> locked, 1 -> unlocked // --- library start [0x134] : uint32 : files // total 12 files, 1 per bit, 0 -> locked, 1 -> unlocked // figures -- total 46, 1 per bit, 0 -> locked, 1 -> unlocked [0x138] : [ chris(bsaa), sheva(bsaa), josh, jill(brainwashed), wesker, excella, irving, spencer ] [0x139] : [ ... ] [0x13a] : [ ... ] [0x13b] : [ ... ] [0x13c] : [ ... ] [0x13d] : [ ndesu, irving(transformed), chris(rare), sheva(rare), jill(rare), wesker(rare) ] // cutscenes -- total 53, 1 per bit, 0 -> locked, 1 -> unlocked [0x140] : [ ... ] [0x141] : [ ... ] [0x142] : [ ... ] [0x143] : [ ... ] [0x144] : [ ... ] [0x145] : [ ... ] [0x146] : [ ... ] // last 5 // --- library end // guns unlocked [0x148] : uint8 : guns_unlocked // available for purchase? [0x149] : [ ... ] [0x150] :5: Gattling gun ... // bonus features unlocked -- total 71, 1 per bit, XXX gun upgrades computed from somewhere else? [0x158] : [ ... ] ... [ ... ] [0x15f] :7: AK-74 [0x160] :6: M&K MP5 // upto 7 bits // bonus features unlocked end // --- bonus features start // bonus features "NEW" bits -- total 71, 1 per bit, 0 -> seen, 1 -> unseen [0x164] : [ ... ] ... : [ ... ] [0x16c] : [ ... ] //upto 7 bits // bonus features "NEW" bits end ... [0x18c] : int32 : total_playtime_secs [0x190] : int32 : random_seed??? // changed value from 0x092e69 -> 0x03813c on playing for a while [0x194] : int32 : money [0x198] : int32 : pts ... [0x1bc] : uint8 : ??? // changed from 0x0 -> 0x1 on unlocking/purchasing gattling gun and bull's-eye achievement ... [0x1d4] : int32 : times_rescued_partner [0x1d8] : int32 : times_rescued_by_partner [0x1dc] : float : avg_time_rescued_partner [0x1e0] : float : avg_time_rescued_by_partner [0x1e4] : int32 : times_saved_from_dying_partner [0x1e8] : int32 : times_saved_from_dying_by_partner [0x1ec] : float : avg_time_saved_partner_from_dying [0x1f0] : float : avg_time_saved_by_partner_from_dying [0x1f4] : int32 : times_played_online?? // weapon usage records start -- 32 elements [0x1f8] : weapon_usage_record : hand-to-hand // ... [0x388] : weapon_usage_record : hydra (sg) [0x6d0] : weapon_usage_record : proximity bomb // weapon usage records end -- // enemy records start -- 55 elements [0x6f8] : enemy_record : ??? // something is different in first 23 elements // ... [0x80c] : enemy_record : executioner majini [0x818] : enemy_record : chainsaw majini [0x824] : enemy_record : ??? [0x830] : enemy_record : motorcycle majini // ... [0x860] : enemy_record : cephalo [0x86c] : enemy_record : duvalia [0x878] : enemy_record : bui kichwa // ... [0x89c] : enemy_record : reaper // ... [0x908] : enemy_record : wesker [0x914] : enemy_record : adjule [0x920] : enemy_record : crocodile // ... [0x980] : enemy_record : ??? // enemy records end // level_records start [0x98c] : level_record[16] : level_records_amateur [0xa4c] : level_record[16] : level_records_normal [0xb0c] : level_record[16] : level_records_veteran // level_records end // sea of zeros start [0xbcc] : ??? : zero ... [0x6f8] : ??? ... [0x258c] : ??? // non-zero [0x2590] : ??? // non-zero [0x2594] : ??? // non-zero [0x2598] : ??? // non-zero // sea of zeros contd ... // sea of zeros end // inventory start [0x384c] : inventory_item[54] : inventory_slots // inventory end // treasures start [0x3ad4] : treasure_item[54] : treasure_slots // treasures end [0x3bac] : ??? // game state structure start?? [0x3bb0] : uint32 : state_type? // { new_game = 0, continue = 1, new_game_plus = 2 } [0x3bb4] : uint32 : character // { chris = 0, sheva = 1 } [0x3bb8] : uint32 : difficulty // { amateur = 0, normal = 1, veteran = 2 } [0x3bc0] : uint32 : level_number // starts from 0 [0x3bcc] : uint32 : ??? [0x3bdc] : uint32 : checkpoint_number??? // either this or 0x3bcc [0x3be8] : uint64 : ??? // magic value repeated at 4 locations [0x3c00] : uint64 : ??? // magic value repeated at 4 locations [0x3c18] : uint64 : ??? // magic value repeated at 4 locations [0x3c30] : uint64 : ??? // magic value repeated at 4 locations [0x3c60] : uint8[12] : ??? // magical byte sequence #1 [0x3cb0] : uint8[12] : ??? // magical byte sequence #2 [0x3cd8] : ??? // game state structure end?? [0x3cdc] : ??? ... // chris case start [0x3d80] : case[9] : chris_case // chris case end [0x3f0c] : ??? ... // sheva case start [0x41a0] : case[9] : sheva_case // sheva case end [0x432c] : ??? ... // individual treasures start [0x4e70] : treasure_item[54] : treasure_slots_chris [0x4f48] : treasure_item[54] : treasure_slots_sheva // individual treasures end [0x5020] : ??? ... [0x51d0] : uint32 : ??? // non-zero ... [0x51f0] : uint32 : ??? // non-zero ... [0x5270] : uint32 : ??? // non-zero [0x5274] : uint32 : ??? // non-zero ... [0x5290] : uint32 : ??? // non-zero ... [0x52a0] : uint32 : ??? // non-zero ... [0x5d30] : uint32 : ??? // always 1? ... [0x5d44] : uint32 : chris costume DLC // { warrior = 3, heavy_metal = 4 } [0x5d48] : uint?? : sheva costume DLC // { business = 3, fairy_tale = 4 } }
keys = { A = 0x21, ..., Z = 0x3a, ..., '.' = 0x5a }
struct inventory_item { [0x0] : uint8 : item_code [0x1] : uint8 : item_type { invalid = 0, gun = 1, ammo = 2, health = 3, treasure = 4, armor = 6, ... } [0x2] : int16 : value (ammo, item count, ...) // INT!! kek! you can have negative ammo! [0x4] : uint8[8] : upgrade_levels // 1 byte for each upgrade, each upgrade increments the level { firepower = 0, reload_speed = 1, capacity = 2, piercing = 4, attack_range = 5, ... } {{{size = 12 bytes}}} }
struct treasure_item { [0x0] : uint8 : item_code [0x1] : uint8 : item_type // 4 for all? [0x2] : int16 : item_count // INT?? {{{size = 4 bytes}}} }
struct weapon_usage_record { [0x0] : uint8 : item_code [0x1] : uint8 : item_type [0x2] : uint16 : ??? [0x4] : int32 : enemies_killed [0x8] : int32 : shots_fired/times_used [0xc] : int32 : shots_on_target [0x10] : int32 : headshots [0x24] : int32 : shots_fired_in_last_session?? {{{size = 40 bytes}}} }
struct enemy_record { [0x0] : uint8 : enemy_type?? // always 0x3 [0x1] : uint8 : enemy_code/enemy_class [0x2] : uint16 : subclass_type?? [0x4] : int32 : times_defeated [0x8] : int32 : times_caused_death {{{size = 12 bytes}}} }
struct level_record { [0x0] : uint32 : finished?? // { not_finished = 0, finished = 1 } [0x4] : uint32 : level_time_ms // level time counter (in ms) [0x8] : uint32 : level_rank // { S = 0, A = 1, B = 2, ... } }
struct case { [0x0] : uint8 : item code [0x1] : uint8 : item type [0x4] : int32? : current value (ammo, item count etc.?) [0x8] : int32? : max value [0x18] : uint8? : equipped flag ... {{{size = 44 bytes}}} }
Chris case: 0 : 0x3d80 1 : 0x3dac 2 : 0x3dd8 3 : 0x3e04 4 : 0x3e30 5 : 0x3e5c 6 : 0x3e88 7 : 0x3eb4 8 : 0x3ee0
Sheva case: 0 : 0x41a0 1 : 0x41cc 2 : 0x41f8 3 : 0x4224 4 : 0x4250 5 : 0x427c 6 : 0x42a8 7 : 0x42d4 8 : 0x4300
Ammo codes: 0x0 : Invalid 0x1 : Handgun 0x2 : Machine gun 0x3 : Shotgun 0x4 : Rifle 0x6 : Explosive rounds 0x7 : Acid rounds 0x8 : Nitrogen rounds 0x9 : Magnum 0xe : Flame rounds 0xf : Flash rounds 0x10 : Electric rounds Health codes: 0x0 : Invalid 0x4 : First Aid 0x5 : Green Herb 0x6 : Red Herb 0x7 : G+G Herb 0x9 : G+R Herb Gun codes: 0x0 : Invalid (Hand-to-Hand?) 0x1 : Knife 0x2 : M92F Hand gun 0x3 : VZ61 Machine gun 0x4 : Ithaca M37 Shotgun 0x5 : S75 Rifle 0x6 : Hand Grenade 0x7 : Incendiary Grenade 0x8 : Flash Grenade 0x9 : SIG 556 Machine gun 0xa : Proximity Bomb 0xb : S&W M29 Magnum 0xd : Single Shot RL 0xe : Grenade Launcher [Empty/invalid] 0xf : Longbow 0x10 : AK-74 Machine gun 0x13 : H&K MP5 Machine gun 0x16 : M3 Shotgun 0x17 : Jail Breaker Shotgun 0x19 : Hydra Shotgun 0x1b : S&W M500 Magnum 0x1c : H&K PSG-1 Rifle 0x1e : M93R Hand gun 0x20 : Dragunov SVD Rifle 0x22 : Stun Rod 0x25 : Grenade Launcher (Explosive) 0x26 : Grenade Launcher (Acid) 0x27 : Grenade Launcher (Nitrogen) 0x29 : Samurai Edge Hand gun 0x36 : Egg (Rotten) 0x39 : Grenade Launcher (Flame) 0x3a : Grenade Launcher (Flash) 0x3b : Grenade Launcher (Electric) 0x3c : Egg (White) 0x3d : Egg (Brown) 0x3e : Egg (Gold) Armor codes: 0x1 : Melee Vest 0x6 : Bulletproof Vest
Enemy codes: 0x80 : Wesker 0x34 : Reaper
Now I just need to put a GUI around this and it will be ready for mass consumption!
|
|
|
Post by sh1n4 on Dec 25, 2021 8:31:47 GMT 10
Hello picoleetthe link for download is offline
|
|
|
Post by picoleet on Jan 22, 2022 21:53:06 GMT 10
Hello picoleet the link for download is offline Sorry I failed to reply earlier. Please find the script below. Also, I noticed that steam ID is not really required so you can skip the --key <your_key> part. #!/usr/bin/python3 # coding: utf-8
import sys import argparse
magic=bytes([0x00, 0x21, 0x11, 0x08, 0xC0, 0x4B, 0x00, 0x00])
def xor(data,key): return bytes([i^j for i,j in zip(data,key)])
def decode_savedata(in_fname, out_fname): global magic key = magic with open(in_fname, "rb") as fin: with open(out_fname, "wb") as fout: while (encoded_data := fin.read(8)): decoded_data = xor(encoded_data,key) fout.write(decoded_data) key = encoded_data
def encode_savedata(in_fname, out_fname): global magic key = magic with open(in_fname, "rb") as fin: with open(out_fname, "wb") as fout: while (decoded_data := fin.read(8)): encoded_data = xor(decoded_data,key) fout.write(encoded_data) key = encoded_data
def calculate_checksum(in_fname): def chksum(fin, seekpos, iters, ret): fin.seek(seekpos, 0) for i in range(iters): b = fin.read(4) val = int.from_bytes(b, byteorder='little') ret = (ret+val)%2**32 return ret with open(in_fname, "rb") as fin: start1, start2 = 0x10, 0x3bb0 len1, len2 = 0xee7, 0x5c0 return chksum(fin, start2, len2, chksum(fin, start1, len1, 0))
def read_bytes_at_offset(in_fname, offset, count): with open(in_fname, "rb") as fout: fout.seek(offset, 0) return fout.read(count)
def write_bytes_at_offset(in_fname, in_bytes, offset): with open(in_fname, "rb+") as fout: fout.seek(offset, 0) fout.write(in_bytes)
if __name__ == '__main__': ap = argparse.ArgumentParser() ap.add_argument('mode', choices=["decode", "encode", "update_checksum", "show_checksum"]) ap.add_argument('files', nargs='+', type=str) args = ap.parse_args()
if args.mode == "decode": for f in args.files: outf = "decoded_" + f decode_savedata(f, outf) elif args.mode == "encode": for f in args.files: outf = "encoded_" + f calc_chksum = calculate_checksum(f) file_chksum_bytes = read_bytes_at_offset(f, 0x8, 4) file_chksum = int.from_bytes(file_chksum_bytes, byteorder='little') if calc_chksum != file_chksum: print(f"{f}: checksum mismatch detected. Updating checksum to {hex(calc_chksum)}") write_bytes_at_offset(f, calc_chksum.to_bytes(4, byteorder='little'), 0x8) encode_savedata(f, outf) elif args.mode == "show_checksum": for f in args.files: chksum = calculate_checksum(f) print(f"{f}: {hex(chksum)}") elif args.mode == "update_checksum": for f in args.files: chksum = calculate_checksum(f) print(f"{f}: {hex(chksum)}") write_bytes_at_offset(f, chksum.to_bytes(4, byteorder='little'), 0x8) exit(0)
|
|
|
Post by emiliaaaa on Feb 6, 2022 20:16:48 GMT 10
Hi picoleet, thank you so much for this! You made my day :3 I was trying to transfer my save file to my girlfriend so we could play mercenaries (we did our playthrough on my side so she didn't unlock anything). Just copying files does not work, so I tried to use this to edit the mercenaries flag in her save file to no avail (couldnt figure out the address of the flag). Then I figured the game actually uses the steam id along with the checksum to verify the save data, so I decoded the file with your script and replaced the first 8 bytes with her steam id and she was able to load my save. So the first version of script is useful for transferring saves, but got lost as the link stopped working. This is really useful, I want to write a steam guide using this script. I'm updating it to restore the key argument so the steam id can be updated and include it in the guide and credit you. If you have github can u please upload it as a github gist or repository so it doesn't get lost? So then I can link the guide to your version of the script since you are the author. EDIT: I published the guide steamcommunity.com/sharedfiles/filedetails/?id=2744879449Thank u so much :3
|
|
|
Post by picoleet on Feb 10, 2022 3:57:05 GMT 10
Hi picoleet, thank you so much for this! You made my day :3 I was trying to transfer my save file to my girlfriend so we could play mercenaries (we did our playthrough on my side so she didn't unlock anything). Just copying files does not work, so I tried to use this to edit the mercenaries flag in her save file to no avail (couldnt figure out the address of the flag). Then I figured the game actually uses the steam id along with the checksum to verify the save data, so I decoded the file with your script and replaced the first 8 bytes with her steam id and she was able to load my save. So the first version of script is useful for transferring saves, but got lost as the link stopped working. This is really useful, I want to write a steam guide using this script. I'm updating it to restore the key argument so the steam id can be updated and include it in the guide and credit you. If you have github can u please upload it as a github gist or repository so it doesn't get lost? So then I can link the guide to your version of the script since you are the author. EDIT: I published the guide steamcommunity.com/sharedfiles/filedetails/?id=2744879449Thank u so much :3 Glad you found it useful!
|
|
|
Post by sh1n4 on Mar 14, 2022 17:34:51 GMT 10
|
|
|
Post by picoleet on Mar 30, 2022 4:39:36 GMT 10
sh1n4: Good work friend! I wanted to create something like this myself but never got time to do it. Here's an unsolicited suggestion: put screenshots in your README/repo; this would encourage more people to check it out
|
|
|
Post by Biohazard4X on Apr 23, 2022 13:12:28 GMT 10
This is great information! You can check out my PS4 guide for more information on certain aspects but can only use as base. Definitely these versions are different.
I will add some offsets to you list.
0x3C40 uint8 - Chris Present - 01 Enabled/00 Disabled 0x3C48 uint8 - Chris Character ID 0x3C50 uint32 - Chris Current Health 0x3c54 uint32 - Chris Max Health
0x3C90 uint8 - Sheva Present - 01 Enabled/00 Disabled 0x3C98 uint8 - Sheva Character ID 0x3CA0 Uint32 - Sheva Current Health 0x3CA4 uint32 - Sheva Max Health
|
|
|
Post by ding on Sept 12, 2022 12:05:48 GMT 10
picoleet is it safe to run?
|
|
Getting pissed for no reason
Posts: 317
|
Post by Cᴏɴᴅ3ᴍɴᴇᴅ on Oct 21, 2022 18:29:49 GMT 10
how to use it ?
|
|
|
Post by picoleet on Nov 18, 2022 15:28:15 GMT 10
picoleet is it safe to run? You can backup your original savefile and restore it in case your editing breaks something.
|
|
|
Post by picoleet on Nov 18, 2022 15:30:21 GMT 10
If you don't want to do hex-editing you can perhaps use the GUI created by sh1n4 above.
|
|