Achievement manager/editor?
I know this may sound trivial, and I hope to not waste anyone’s time, but is it possible to manually edit achievements? I managed to transfer my og xbox 360 save file from like 2009, but some achievements will never unlock as I’m obviously way past the flags for activation. If the only option is hex editing, is there any documentation available? Again this is very trivial but I figured asking wouldn’t hurt anyone u.u
If you're looking for a short-term answer to this question, I had this exact same problem and I was able to reverse-engineer how to edit the 'ACH-DATA' file to unlock everything. I used an online hex editor (https://hexed.it/). The first column of the file is an index that identifies the achievement, starting at 24 (decimal) and going up to 83 (decimal), skipping 30 and 55-63 (all also decimal). So the indices range from 24-29, 31-54, 64-83 (but remember to convert them to hex before entering). Then the next few columns are a datestamp, which you can put in anything for. Finally, there is a checksum value which is a single byte in the first row, ninth column. This is just a binary XOR of everything past the first row.
Hope that helps!
FYI, the relevant bits of code that helped me figure this out are here:
- https://github.com/hedge-dev/UnleashedRecomp/blob/main/UnleashedRecomp/user/achievement_manager.cpp
- https://github.com/hedge-dev/UnleashedRecomp/blob/main/UnleashedRecomp/user/achievement_data.cpp
Finally, there is a checksum value which is a single byte in the first row, ninth column. This is just a binary XOR of everything past the first row.
I know nothing about this stuff, but I'm trying to do the same thing and I'm pretty sure the checksum is the only thing messing me up. How exactly does it work? What is the checksum based on? Again, this type of stuff isn't my area of expertise, but I'd like to know how exactly this checksum works
A binary XOR is basically just an operation that takes the binary representation of two numbers, and checks bit-by-bit if one OR the other is a 1 (but not both - the 'X' stands for 'exclusive'). If so, the output is a 1, otherwise it's a 0. Repeat for each bit. So for example, 10101 XOR 01100 = 11001. Then you go through each value in the ACH-DATA file and compute the XOR with the next value. The result gives you your checksum. Obviously this is tedious to calculate by hand, so here's the python script I used to calculate it for me.
import os
# (NOTE: Assumes you're running on a Windows system)
# Reads in your appdata folder, which is where ACH-DATA is held
appdata = os.getenv('APPDATA')
# Reads in the ACH-DATA file
ach_path = os.path.join(appdata, 'UnleashedRecomp', 'save', 'ACH-DATA')
with open(ach_path, 'rb') as f:
ach_contents = f.read()
# Extract everything past the first row of the file
# (i.e. the first 16 bytes)
ach_hdr = ach_contents[:16]
ach_data = ach_contents[16:]
# Extract the current checksum value that's already in the file
checksum_current = hex(ach_hdr[8])
# Compute the checksum as the binary XOR of the ach_data
checksum = 0
for i in range(len(ach_data)):
checksum ^= ach_data[i]
# Convert the checksum to a hex value and print
checksum = hex(checksum)
print(f'The current checksum value in your ACH-DATA file is: {checksum_current}')
print(f'The checksum value for your ACH-DATA file should be: {checksum}')
You should be able to just save this as a python file and run it. Save it as a .py file in your home directory (C:\Users\your_username\file_name.py replacing "your_username" and "file_name" as appropriate) and then open up a terminal and run it with "python file_name.py", assuming you have python installed. If you don't, it's very easy to install - the first time you try calling "python" in the terminal it will prompt you to install it through the windows store. It's just a 1-click install and then it should work. This script will print out what the current value of your checksum is, and then it will print out what it should be. Or, you can always just calculate it the ol' fashioned way. YMMV.
Thank you, it works! I only know so because it gives the right checksum for everything that I already know the checksum for, which is good. However, it still shows up as corrupted to the game whenever I add a new achievement. I figured out which achievements correspond with which hex thanks to a 100% save, and I'll likely post that info somewhere when I have the time. But even after inserting the checksum (and quadruple checking that it's right because you can never be too sure), it still gets corrupted. Could it be columns 3-6? The ones that seem to be datestamps? It seems like it might be, but idk.
Edit: it works if I get rid of my unobtained achievements in the 100% save, meaning it likely is columns 3-6. No clue why or how, but at least I got my achievements! And your script really helped with the checksum. Thanks again for that!
Hm, are you making sure that the checksum is the very last thing you change? Because if you make any changes elsewhere in the file, that will change the checksum (that's the point of the checksum value -- to check that the contents of the file are as expected). If you're already doing that, then I'm not sure why it's still saying it's corrupted. In my (albeit minimal) testing, it seemed like whatever I put in for the datestamps just worked (even for ones in the future, lol).
Yeah I'm not really sure what was going on with that. I checked a functioning file to see if changing the datestamp caused issues and, sure enough, it did, even after changing the checksum value as well to be correct. Regardless, I got my achievements back, and that's all that matters, though it does seem a little weird. It's likely that certain values in one part of the datestamp cause problems with other values. But I can't really be sure about that. I do know that they are ordered based on time achieved, so maybe misordering them could have been the issue. Again, I'm not fully sure, but it seems likely to be the case.
Sorry to bump this, but I'm trying to do the same thing on a Linux system. I too used an old save file from my 360 and some achievements are locked off for me as well. I asked the devs if they would consider alternate ways of unlocking achievements from files like mine, but they said no.
Looking at this script, it made my head spin, so perhaps there's some alternative? (I tried using HxD but I wasn't sure what to edit)
I think trying to compute the checksum manually will become cumbersome. Editing the script I sent to work with Linux I think isn't too difficult. I don't have a Linux system to test this on, but looking at the documentation, it seems like the save path on Linux systems is in ~/.config/UnleashedRecomp.
Try this out. I've edited it so that it should work on any operating system:
import os
# Check operating system
if os.name == 'nt':
# Reads in your appdata folder, which is where ACH-DATA is held on Windows
parent = os.getenv('APPDATA')
elif os.name == 'posix':
# Read in your home folder, which is where ACH-DATA is held on Linux
parent = os.getenv('HOME')
parent = os.path.join(parent, '.config')
else:
raise ValueError(f'Unrecognized OS: {os.name}')
# The UnleashedRecomp config folder that contains the save & achievement data
parent = os.path.join(parent, 'UnleashedRecomp')
# Check if there is an 'mlsave' folder, else use the regular 'save' folder
if os.path.exists(os.path.join(parent, 'mlsave')):
parent = os.path.join(parent, 'mlsave')
else:
parent = os.path.join(parent, 'save')
# Reads in the ACH-DATA file
ach_path = os.path.join(parent, 'ACH-DATA')
with open(ach_path, 'rb') as f:
ach_contents = f.read()
# Extract everything past the first row of the file
# (i.e. the first 16 bytes)
ach_hdr = ach_contents[:16]
ach_data = ach_contents[16:]
# Extract the current checksum value that's already in the file
checksum_current = hex(ach_hdr[8])
# Compute the checksum as the binary XOR of the ach_data
checksum = 0
for i in range(len(ach_data)):
checksum ^= ach_data[i]
# Convert the checksum to a hex value and print
checksum = hex(checksum)
print(f'The current checksum value in your ACH-DATA file is: {checksum_current}')
print(f'The checksum value for your ACH-DATA file should be: {checksum}')
But remember that the checksum is the absolute last thing you should change. I don't have an automatic way to edit the rest of the file because I only needed to do it once and it was easier to just do the rest by hand. The main thing is to fill in the first column with the index that identifies each achievement (see my previous comment for how these numbers should be filled in). What you're actually filling in is a hex value, not a decimal value. You can find converters from decimal to hex online, and you can also check that you've filled it in correctly by looking at what number shows up under "8-bit integer" on the left panel in https://hexed.it/. Then, for the date stamp values in columns 3-6, you can just copy the newest one that's been filled in, paste it into the next row, and increment it by 1 second. You can check these by highlighting a value in the 3rd column and looking under "Unix 32-bit datetime" on the left panel. You should have all 50 rows (+1 header row) filled in to get all of the achievements.
All that being said, if all that you're looking for at the end of the day is a 100% filled in achievement file, there are people who have posted these on GameBanana and such. From what I can tell, there's nothing in the ACH-DATA file that links it to a particular SYS-DATA file, so you could just download one of those (https://gamebanana.com/mods/348883). It won't affect the rest of your save data if you only take the ACH-DATA file (I'm pretty sure anyways).
Actually, as I was describing how you would edit the ACH-DATA file above, the thought occurred to me that it could be a fun little coding challenge to try to make a script that just autocompletes your ACH-DATA file, no matter how complete it currently is. So...I've done just that.
Check it out: same instructions as before. Just copy and paste this code into a .py file somewhere you can easily access, and run it with python <filename>.py. It'll automatically fill in your ACH-DATA file with the remaining achievements that aren't already in there. No extra work necessary! (Your original, unmodified ACH-DATA file is backed up just in case, so don't worry about losing anything). Let me know if this works for you.
import os
import sys
# Check operating system
if os.name == 'nt':
# Reads in your appdata folder, which is where ACH-DATA is held on Windows
parent = os.getenv('APPDATA')
elif os.name == 'posix':
# Read in your home folder, which is where ACH-DATA is held on Linux
parent = os.getenv('HOME')
parent = os.path.join(parent, '.config')
else:
raise ValueError(f'Unrecognized OS: {os.name}')
# The UnleashedRecomp config folder that contains the save & achievement data
parent = os.path.join(parent, 'UnleashedRecomp')
# Check if there is an 'mlsave' folder, else use the regular 'save' folder
if os.path.exists(os.path.join(parent, 'mlsave')):
parent = os.path.join(parent, 'mlsave')
else:
parent = os.path.join(parent, 'save')
# Reads in the ACH-DATA file
ach_path = os.path.join(parent, 'ACH-DATA')
with open(ach_path, 'rb') as f:
ach_contents = f.read()
# Extract everything past the first row of the file
# (i.e. the first 16 bytes)
ach_hdr = ach_contents[:16]
ach_data = ach_contents[16:]
# Create a bytearray of the file contents so we can edit them without overwriting the original
ach_new = bytearray(ach_contents)
# Prepare achievement indices
indices_all = [i for i in range(24,84) if i != 30 and not (54 < i < 64)]
# Check which achievements have already been unlocked
indices_unlocked = list(ach_data[::16])
# find the first index where the value is 0 - this is where we will need to start filling in the new data
# (add 1 to account for the header row in the full file)
start_index = 0
if 0 in indices_unlocked:
start_index = indices_unlocked.index(0) + 1
# then we can remove all the 0s from this list since we dont need them
while 0 in indices_unlocked:
indices_unlocked.remove(0)
# Remove the unlocked indices from all indices to obtain the list of achievements which still need
# to be unlocked
indices_locked = indices_all.copy()
for unl in indices_unlocked:
indices_locked.remove(unl)
print(f'The achievements you have already unlocked have the following indices:')
print(indices_unlocked)
print(f'The remaining achievements have the following indices:')
print(indices_locked)
if len(indices_locked) == 0:
print('Looks like you\'ve already unlocked everything. Way to go!')
sys.exit()
# Grab the newest datestamp, assuming there is one
latest_datestamp = [0, 0, 0, 0]
if start_index > 1:
i1 = (start_index-1)*16 + 2
i2 = (start_index-1)*16 + 6
latest_datestamp = list(ach_contents[i1:i2])
# Loop through the rows in the file and fill in the new achievements one by one
ia = 0
for irow in range(start_index, len(ach_contents)//16):
# the indices we are changing
i1 = irow*16
i2 = i1 + 16
ach_row = ach_contents[i1:i2]
# fill in the achievement index
ach_new[i1] = indices_locked[ia]
# fill in the new datestamp
new_datestamp = latest_datestamp.copy()
new_datestamp[0] += 1
ach_new[i1+2:i1+6] = new_datestamp
print(f'--- Changing row {irow} in ACH-DATA ---')
print(f'- Original Entry: {list(ach_row)} -')
print(f'- New Entry : {list(ach_new[i1:i2])} -')
# update the latest datestamp
latest_datestamp = new_datestamp
# increment the index indentifier
ia += 1
# Extract the current checksum value that's already in the file
checksum_current = hex(ach_hdr[8])
# Compute the checksum as the binary XOR of the ach_new
checksum = 0
for i in range(len(ach_new[16:])):
checksum ^= ach_new[16:][i]
# Convert the checksum to a hex value and print
ach_new[8] = checksum
checksum = hex(checksum)
print(f'--- Changing checksum value in ACH-DATA ---')
print(f'- Original Value: {checksum_current} -')
print(f'- New Value : {checksum} -')
# Save a backup of the original ACH-DATA, just in case
os.rename(ach_path, ach_path.replace('ACH-DATA', 'ACH-DATA.bak'))
# Save the new ACH-DATA file
with open(ach_path, 'wb') as f:
f.write(ach_new)
Well, that did it, except I only want to unlock the following:
- All continent achievements
- The Apotos hot dog achievement
- Both souvenir achievements
Those are the ones I've unlocked on my 360 but can't unlock on Recompiled.