Re-Enable Achievements...

Post all other topics which do not belong to any other category.
Coolshrimp
Manual Inserter
Manual Inserter
Posts: 4
Joined: Sat Oct 30, 2021 7:42 pm
Contact:

Re: Re-Enable Achievements...

Post by Coolshrimp »

feel free to test out the tool it will now patch and enable achievements again thanks for your code it works on my saves hope it works for the rest of you.

https://github.com/coolshrimp/Factorio- ... erver-Tool

Image
jakeroxs
Manual Inserter
Manual Inserter
Posts: 4
Joined: Mon Oct 28, 2024 11:08 pm
Contact:

Re: Re-Enable Achievements...

Post by jakeroxs »

xrobau wrote: Mon Nov 04, 2024 2:01 am
Pain12345 wrote: Thu Oct 31, 2024 5:22 pm I wrote a small Console application to patch the save game: https://github.com/Rainson12/FactorioSa ... hievements
This doesn't appear to work when purely enabling the editor. There is no 'command-ran' when simply starting a new game and then /editor.
Ha that might be what it wasn't working for me, I have so far only enabled the editor as we had vastly underestimated what was needed to go to a new planet and tp'd myself back to nauvis, RIP first space platform.

Edit:
Definitely why it's not working for me, did the same and started a new game, did a console command to disable achievements, ran tool, achievements re-enabled.

Ran /editor to disable achievements, ran tool, error message I posted before, reloaded game and achievements still disabled (as expected), resaved game, re ran tool and no error, achievements still disabled.

For fun I then tried on the same save to run a command that would have disabled achievements thinking maybe it was a separate flag that could clear up both, reran tool and no error message but achievements still disabled.

So it definitely appears that /editor is somehow differently stored in the save for achievement disabling :(
sneednman
Manual Inserter
Manual Inserter
Posts: 1
Joined: Sat Nov 16, 2024 6:00 pm
Contact:

Re: Re-Enable Achievements...

Post by sneednman »

I just wanted to say Pain12345's tool still works for patching, however I had to re-run a command in order for it to work. If it's not working for any of you, try re-running a new command, saving your world, and then running the tool as this fixed my issue.
xevioni
Manual Inserter
Manual Inserter
Posts: 1
Joined: Sat Aug 03, 2024 5:48 am
Contact:

Re: Re-Enable Achievements...

Post by xevioni »

I'm far from an expert in this stuff, but the above replies were immensely helpful in building my solution.

I hold a 25MB save with 120 or so hours in it, so I was kinda desperate to re-enable achievements.

Here's what I've found so far:
- The marker is a set of 1 byte booleans that are either 0x00 or 0x01 depending on their state. The command marker comes before the map editor marker, but they are always together it seems.
- After a certain amount of bytes (16 or less, roughly), two pairs of 8 byte sequences of just 0xFF occur. In older saves, they probably won't be right next to each other, but in newer saves, they likely WILL be.
- You can enable the map editor flag while disabling the command flag.

Image

Here's a little printout from a program I was making to try and automate this process (especially the ZLib decoding/ZIP repackaging).
Most of the above images don't have a 0x01 0x00 0x01 sequence like this (I think). My program latched onto the Red byte, but it's irrelevant.
The green byte is the critical 'Command' flag that needs to be disabled. I just hardcoded the offset from the Red in my program, and it re-enables achievements just fine.
The blue byte, of course, is the map editor flag. And yeah, I set it to 0x01 and it changed the achievements disabled message to fit.

You can also see the 'command-run' text near the bottom of the range, which is the first thing my program latches onto.
- It first searches for the ASCII pattern 'command-ran' (although 'horizontal_flow' works too, it's just farther away, about 1,100 to 1,200 bytes away, this seems to be useful in the case of Map Editor).
- Then, it searches backward TWICE to find the 8-byte 0xFF sequences. It accounts for the possibility that they might be touching each other (like in the case above).
- Then, it looks for the first 0x01 byte. I don't know enough about it's possible offset or patterns, so this is where me hardcoding in the behavior came in handy.

I hope this comes in a little handy; I don't know whether I'd want to publish a specific program or anything as the critical section is really case-by-case. If you know what you're doing, it would be a shortcut for a solution, but if you don't know what you're doing - the above text is more helpful than my code.
This message will be edited later on IF I end up publishing something. Assuming I can edit my posts...
jakeroxs
Manual Inserter
Manual Inserter
Posts: 4
Joined: Mon Oct 28, 2024 11:08 pm
Contact:

Re: Re-Enable Achievements...

Post by jakeroxs »

xevioni wrote: Fri Nov 22, 2024 12:25 pm I'm far from an expert in this stuff, but the above replies were immensely helpful in building my solution.

I hold a 25MB save with 120 or so hours in it, so I was kinda desperate to re-enable achievements.

Here's what I've found so far:
- The marker is a set of 1 byte booleans that are either 0x00 or 0x01 depending on their state. The command marker comes before the map editor marker, but they are always together it seems.
- After a certain amount of bytes (16 or less, roughly), two pairs of 8 byte sequences of just 0xFF occur. In older saves, they probably won't be right next to each other, but in newer saves, they likely WILL be.
- You can enable the map editor flag while disabling the command flag.

Image

Here's a little printout from a program I was making to try and automate this process (especially the ZLib decoding/ZIP repackaging).
Most of the above images don't have a 0x01 0x00 0x01 sequence like this (I think). My program latched onto the Red byte, but it's irrelevant.
The green byte is the critical 'Command' flag that needs to be disabled. I just hardcoded the offset from the Red in my program, and it re-enables achievements just fine.
The blue byte, of course, is the map editor flag. And yeah, I set it to 0x01 and it changed the achievements disabled message to fit.

You can also see the 'command-run' text near the bottom of the range, which is the first thing my program latches onto.
- It first searches for the ASCII pattern 'command-ran' (although 'horizontal_flow' works too, it's just farther away, about 1,100 to 1,200 bytes away, this seems to be useful in the case of Map Editor).
- Then, it searches backward TWICE to find the 8-byte 0xFF sequences. It accounts for the possibility that they might be touching each other (like in the case above).
- Then, it looks for the first 0x01 byte. I don't know enough about it's possible offset or patterns, so this is where me hardcoding in the behavior came in handy.

I hope this comes in a little handy; I don't know whether I'd want to publish a specific program or anything as the critical section is really case-by-case. If you know what you're doing, it would be a shortcut for a solution, but if you don't know what you're doing - the above text is more helpful than my code.
This message will be edited later on IF I end up publishing something. Assuming I can edit my posts...
Nice, I will see if I can get this working, I resigned myself to just using CE to re-enable the achievements every time I launch the game, which works a bit oddly sometimes.

Edit: With lots of trial and error, and some modifications to the program Rainson12 wrote, I was able to find which .dat file I needed to hexedit to get this working.

Ultimately I found that because I ran /editor before running any other commands, command-ran was not found in my save so it couldn't find the right place, so I adjusted it to look for 'horizontal_flow' per your comment, then searched backwards for the hex string FFFFFFFFFFFFFFFF, found what looked to be the right section per your screenshot, adjusted the values to 00 and seems like achievements are enabled in the save!
TheCherry
Burner Inserter
Burner Inserter
Posts: 13
Joined: Fri Jan 18, 2019 10:00 am
Contact:

Re: Re-Enable Achievements...

Post by TheCherry »

I made a patcher too. Its catch the cases with editor, cheat and command.
Works for 2.0.21

https://github.com/dakiba/factorio_achi ... _space_age

EDIT: Sadly its seems to catch only local save games. We play on a dedicated server and use forces (the pvp scenario, but just for teams and bases). Chat messages are not recorded in the level.dat files there ... I see no way to find that important byte ....
KnightDemons
Manual Inserter
Manual Inserter
Posts: 1
Joined: Thu Dec 05, 2024 10:52 am
Contact:

Re: Re-Enable Achievements...

Post by KnightDemons »

Pain12345 wrote: Thu Oct 31, 2024 5:22 pm I wrote a small Console application to patch the save game: https://github.com/Rainson12/FactorioSa ... hievements
It will
1. unzip the savegame
2. decompress all .dat files
3. screen the unzipped files for occurrence of "command-ran"
4. patch the 01 closest to the found occurrence by searching for "00 00 00 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" binary pattern
5. pack the dat file again
6. Backing up original save file
7. Creating new save file by zipping everything again
8. Cleaning up the temporary folder
Thank you for this. I kept trying to fix the files on my own but it wasn't undoing anything in the save. (I just was using /c game.player.force.rechart() to update map for Solid Ore Colors in Map View mod but I guess that's a 'cheat' command.) Ran the console app and Boom Boom Boom Boom. Like I never disabled achievements. 8-)
User avatar
louanbastos
Burner Inserter
Burner Inserter
Posts: 5
Joined: Tue Dec 24, 2024 9:31 pm
Contact:

Re: Re-Enable Achievements...

Post by louanbastos »

Image
Image
Image
Image
Image

There are some achievements that are locked by time or with other requirements, it would be interesting to have a way to reactivate these achievements and if possible set a longer time or remove that time.

Is there any mod/program/script that fixes and resets this?
naibudeshinu
Manual Inserter
Manual Inserter
Posts: 1
Joined: Thu Jan 16, 2025 8:17 pm
Contact:

Re: Re-Enable Achievements...

Post by naibudeshinu »

xrobau wrote: Mon Nov 04, 2024 2:01 am
Pain12345 wrote: Thu Oct 31, 2024 5:22 pm I wrote a small Console application to patch the save game: https://github.com/Rainson12/FactorioSa ... hievements
This doesn't appear to work when purely enabling the editor. There is no 'command-ran' when simply starting a new game and then /editor.

There IS 'editor-will-disable-achievements', which has '00000001000100000000000a0a00000000ffffffffffff' a fair way back from it, but that's not the 6/7 byte offset you have in your script. Where did you get those numbers from, out of curiosity?
So after some digging I was able to re-enable my achievements after using both commands and running /editor

As discussed above, you are suppose to find 16 consecutive F's and flip the 1 to a zero but for me, I had a different line of code as shown here. What I found out was that the leftmost "1" was for the editor flag and the right-most "1" was for command flag. I tested this by flipping them and saw that my achievements were either disabled because "commands were used" or "map editor was used". So if you have used both like me, you will have to flip the first two one's in that sequence.

Image

I tried this on two fresh saves and the sequence was the same.This might be different for others but it took a little bit of testing to finally restore my save. Hope this helps.
Attachments
yo.png
yo.png (64.58 KiB) Viewed 1613 times
hraukr
Manual Inserter
Manual Inserter
Posts: 1
Joined: Sat Jan 25, 2025 7:36 am
Contact:

Re: Re-Enable Achievements...

Post by hraukr »

Guide for Windows 11 users that have trouble with the previously mentioned tools.

To summarise above, in my understanding, you are looking for a string in the level.dat files that contains 8 hexadecimal "FF"s followed closely (within 8 to 16 hex bytes) by another set of 8 "FF"s. Immediately preceeding the first set of "FF"s are the two (three?) sets that determine whether achievements have been disabled and why.

EX.

Code: Select all

01 84 04 00 00 00 00 00 00 01 01 ff ff ff ff ff 
ff ff ff f9 23 12 04 20 20 20 20 ff ff ff ff ff 
ff ff ff 50 46 00 00 00 00 00 00 00 00 01 00 00
See on line 1, the two "01"s before the first "FF". Those represent the map editor and command line use, respectively. When either is set to "01", achievements are disabled. When both are set to "00", achievements are re-enabled for that save only. Global/Steam achievements are still disabled as far as I can tell.

Edit: I had ChatGPT work its magic...horrifying horrifying magic...and assembled this into a script that I have tested to work with my save file anyways. Not sure about edge cases but...lets say I am scared of AI/LLMs.

I also feel like I should say...NO WARRANTY OF THE BELOW CODE IS PROVIDED. USE AT YOUR OWN RISK!
Automated Method
Basically, . Create the following script in the folder. Open terminal in the temp directory. Run the script. Make sure to keep backups of your save. The save in the folder is recreated, copy this save to the Factorio save directory. Run the game and test the save.

Step 1. Install the latest Python from the microsoft store - https://apps.microsoft.com/detail/9PNRBTZXMB4Z

Step 2. Create a temp folder on your desktop and copy over your save from the Factorio save folder.

Step 3. Rename the save in the Factorio save folder <savename>.zip.bak as a backup. Save it in other places as well for safety!

Step 4. Using a text editor, create the following file in the temp folder with the save file. Name it something like 'achievementsfix.py'

Code: Select all

#!/usr/bin/env python3
import os
import re
import shutil
import zipfile
import tempfile
import zlib
import sys

# =======================
# Utility Functions
# =======================
def list_zip_files():
    files = [f for f in os.listdir('.') if f.lower().endswith('.zip')]
    if not files:
        print("No .zip files found in the current directory.")
        sys.exit(1)
    print("Found the following .zip files:")
    for i, file in enumerate(files, 1):
        print(f"  {i}: {file}")
    return files

def choose_zip_file(files):
    while True:
        choice = input("Enter the number of the .zip file to process: ").strip()
        if not choice.isdigit() or not (1 <= int(choice) <= len(files)):
            print("Invalid selection. Try again.")
        else:
            return files[int(choice)-1]

def backup_file(filepath):
    backup_path = filepath + ".bak"
    shutil.copy2(filepath, backup_path)
    print(f"Backup created: {backup_path}")

def extract_zip(zip_path, extract_dir):
    print(f"Extracting {zip_path} to temporary directory...")
    with zipfile.ZipFile(zip_path, 'r') as zip_ref:
        zip_ref.extractall(extract_dir)
    print("Extraction complete.")

def find_save_folder(extract_dir, zip_filename):
    # The save folder is assumed to be named as the zip filename without extension.
    savename = os.path.splitext(zip_filename)[0]
    save_folder = os.path.join(extract_dir, savename)
    if not os.path.isdir(save_folder):
        # If not found, try listing directories in extract_dir and choose the first one.
        dirs = [d for d in os.listdir(extract_dir) if os.path.isdir(os.path.join(extract_dir, d))]
        if dirs:
            save_folder = os.path.join(extract_dir, dirs[0])
            print(f"Could not find folder matching '{savename}'. Using '{dirs[0]}' instead.")
        else:
            print("No folder found inside the extracted zip!")
            sys.exit(1)
    return save_folder

def copy_level_dat_files(save_folder, dest_folder):
    os.makedirs(dest_folder, exist_ok=True)
    # Copy files that match level.dat<number>
    pattern = re.compile(r'^level\.dat\d+$')
    count = 0
    for f in sorted(os.listdir(save_folder)):
        if pattern.match(f):
            shutil.copy2(os.path.join(save_folder, f), os.path.join(dest_folder, f))
            count += 1
    print(f"Copied {count} level.dat files from {save_folder} to {dest_folder}.")

# =======================
# Step: Inflate files
# =======================
def inflate_files(input_dir, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    files = sorted(os.listdir(input_dir))
    print("Inflating level.dat files...")
    for file in files:
        if file.startswith("level.dat") and file[len("level.dat"):].isdigit():
            input_path = os.path.join(input_dir, file)
            output_path = os.path.join(output_dir, f"{file}.inflated")
            try:
                with open(input_path, "rb") as f:
                    compressed_data = f.read()
                    inflated_data = zlib.decompress(compressed_data)
                with open(output_path, "wb") as f:
                    f.write(inflated_data)
                print(f"Inflated {file} -> {output_path}")
            except Exception as e:
                print(f"Failed to inflate {file}: {e}")

# =======================
# Step: Search and Edit
# =======================
def hex_dump(data, start, end, bytes_per_line=16):
    lines = []
    for offset in range(start, end, bytes_per_line):
        line_bytes = data[offset:offset+bytes_per_line]
        hex_part = ' '.join(f"{b:02X}" for b in line_bytes)
        lines.append(f"{offset:08X}  {hex_part}")
    return "\n".join(lines)

def search_and_edit_inflated_files(inflated_dir):
    pattern = re.compile(b'(?P<before>.{2})(?P<first>(?:\xFF){8})(?P<middle>.{7,12})(?P<second>(?:\xFF){8})', re.DOTALL)
    edited_files = set()
    files = sorted(os.listdir(inflated_dir))
    print("Searching for hex pattern in inflated files...")
    for file in files:
        if not file.endswith(".inflated"):
            continue
        file_path = os.path.join(inflated_dir, file)
        try:
            with open(file_path, "rb") as f:
                data = f.read()
        except Exception as e:
            print(f"Error reading {file}: {e}")
            continue

        modified = False
        for m in pattern.finditer(data):
            before = m.group('before')
            # Check if at least one byte in 'before' is 0x01
            if b'\x01' in before:
                # Determine context for hex dump: one line (16 bytes) above and below the match region
                match_start = m.start()
                match_end = m.end()
                context_start = max(0, match_start - 16)
                context_end = min(len(data), match_end + 16)
                print("\n--- Context for file:", file, "---")
                print(hex_dump(data, context_start, context_end))
                print("--- End Context ---")
                answer = input("Edit the two bytes preceding the first set of FF's (change any 0x01 to 0x00)? (y/n): ").strip().lower()
                if answer == 'y':
                    # Replace the two bytes (positions m.start() to m.start()+2) with 0x00 0x00.
                    print(f"Editing {file} at offset {m.start()} ...")
                    data = data[:m.start()] + b'\x00\x00' + data[m.start()+2:]
                    modified = True
                    edited_files.add(file)
                    # For now, only modify the first occurrence per file.
                    break

        if modified:
            try:
                with open(file_path, "wb") as f:
                    f.write(data)
                print(f"File {file} edited successfully.")
            except Exception as e:
                print(f"Error writing edited data to {file}: {e}")
    if not edited_files:
        print("No files required editing based on the search criteria.")
    return edited_files

# =======================
# Step: Deflate files
# =======================
def deflate_files(input_dir, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    pattern = re.compile(r"level\.dat(\d+)\.inflated$")
    files = sorted(os.listdir(input_dir))
    print("Deflating files...")
    for filename in files:
        match = pattern.match(filename)
        if match:
            file_number = match.group(1)
            input_path = os.path.join(input_dir, filename)
            output_path = os.path.join(output_dir, f"level.dat{file_number}")
            try:
                with open(input_path, "rb") as f_in:
                    inflated_data = f_in.read()
                    deflated_data = zlib.compress(inflated_data)
                with open(output_path, "wb") as f_out:
                    f_out.write(deflated_data)
                print(f"Deflated: {filename} -> {output_path}")
            except Exception as e:
                print(f"Error processing {input_path}: {e}")

# =======================
# Step: Update the original zip file
# =======================
def update_zip_with_deflated_files(deflated_dir, extracted_save_folder, original_zip_path):
    print("Updating the save with edited files...")
    # Overwrite the level.dat files in the extracted save folder with our deflated ones.
    count = 0
    for file in os.listdir(deflated_dir):
        source_file = os.path.join(deflated_dir, file)
        target_file = os.path.join(extracted_save_folder, file)
        if os.path.isfile(source_file):
            shutil.copy2(source_file, target_file)
            print(f"Updated {target_file}")
            count += 1
    print(f"Updated {count} files in the extracted save folder.")

    # Now, rezip the extracted save folder (keeping original folder structure)
    temp_zip = original_zip_path + ".temp"
    with zipfile.ZipFile(temp_zip, 'w', zipfile.ZIP_DEFLATED) as zip_out:
        # Walk the extracted directory (its parent is the temp extraction dir)
        base_dir = os.path.dirname(extracted_save_folder)
        for root, dirs, files in os.walk(base_dir):
            for file in files:
                full_path = os.path.join(root, file)
                # Arcname should be relative to base_dir
                arcname = os.path.relpath(full_path, base_dir)
                zip_out.write(full_path, arcname)
    # Replace the original zip with the new zip.
    shutil.move(temp_zip, original_zip_path)
    print(f"Original zip '{original_zip_path}' updated successfully.")

# =======================
# Cleanup function
# =======================
def cleanup_temp_dirs(dirs):
    for d in dirs:
        if os.path.isdir(d):
            try:
                shutil.rmtree(d)
                print(f"Deleted temporary directory: {d}")
            except Exception as e:
                print(f"Failed to delete {d}: {e}")

# =======================
# Main process
# =======================
def main():
    print("Factorio Save Editor Script Starting...\n")
    zip_files = list_zip_files()
    zip_choice = choose_zip_file(zip_files)
    original_zip_path = os.path.abspath(zip_choice)
    backup_file(original_zip_path)
    
    # Create a temporary extraction directory
    temp_extract_dir = tempfile.mkdtemp(prefix="factorio_extract_")
    extract_zip(original_zip_path, temp_extract_dir)
    
    extracted_save_folder = find_save_folder(temp_extract_dir, zip_choice)
    
    # Create a working directory for level.dat files extraction
    level_dat_workdir = os.path.join(os.getcwd(), "extracted_level_dat")
    os.makedirs(level_dat_workdir, exist_ok=True)
    copy_level_dat_files(extracted_save_folder, level_dat_workdir)
    
    # Inflate the level.dat files
    inflated_dir = os.path.join(os.getcwd(), "inflated_files")
    inflate_files(level_dat_workdir, inflated_dir)
    
    # Search and interactively edit inflated files
    search_and_edit_inflated_files(inflated_dir)
    
    # Deflate the (edited) files
    deflated_dir = os.path.join(os.getcwd(), "deflated_files")
    deflate_files(inflated_dir, deflated_dir)
    
    # Update the extracted save folder with the deflated files and rezip
    update_zip_with_deflated_files(deflated_dir, extracted_save_folder, original_zip_path)
    
    # Ask user if they want to clean up temporary files.
    answer = input("Do you want to delete temporary files and directories? (y/n): ").strip().lower()
    if answer == 'y':
        cleanup_temp_dirs([temp_extract_dir, level_dat_workdir, inflated_dir, deflated_dir])
    else:
        print("Temporary files kept. They are located in:")
        print(f"  Extraction dir: {temp_extract_dir}")
        print(f"  Level.dat working dir: {level_dat_workdir}")
        print(f"  Inflated files dir: {inflated_dir}")
        print(f"  Deflated files dir: {deflated_dir}")
    
    print("Processing complete. Save file updated.")

if __name__ == "__main__":
    main()
Step 5. Right click in a blank area of the temp folder and choose Open in Terminal

Step 6. Run the command below and follow the prompts

Code: Select all

python .\achievementfix.py
That should do it!
ChatGPT wrote:Explanation of the Script

File Selection & Backup:
The script lists all .zip files in the current directory, lets you choose one, and creates a backup (with a “.bak” extension).

Extraction:
It extracts the entire .zip to a temporary directory. It then determines the save folder (expected to be the folder named like the zip file).

Extracting level.dat Files:
All files matching the pattern level.dat<number> are copied from the save folder to a working directory.

Inflate:
The script inflates these files (using zlib.decompress) into an “inflated_files” directory.

Search & Edit:
It searches each inflated file for the hex pattern (8 consecutive FF bytes, followed within 7–12 bytes by another 8 FF bytes) and checks the two bytes immediately preceding the first FF group. If either of those bytes is 0x01, it shows a hex dump context and asks for confirmation to change them to 0x00.

Deflate:
The edited files are recompressed (using zlib.compress) into a “deflated_files” directory.

Update .zip:
The deflated files are copied back into the extracted save folder, and the entire save is rezipped—overwriting the original .zip file.

Cleanup:
Finally, the script prompts whether to delete the temporary directories created during the process.

Run the script from a terminal and follow the prompts. Always test on a backup save first!
Manual Method
To manually disable these bits on Windows 11 , if the script above does not work:

Step 1. Install the latest Python from the microsoft store - https://apps.microsoft.com/detail/9PNRBTZXMB4Z

Step 2. Create a save of your game with a new name. Leave the game and go to the Factorio save folder, and copy the new save to a new temporary folder on your desktop or elsewhere unimportant.

Step 3. Using 7zip or another archive tool, open the save zip file in copy the level.dat# files from to a new folder within the temp folder

Step 4. Using a text editor, create the following file in the new folder with the level.dat files. Name it something like 'inflate.py'

Code: Select all

import os
import zlib

def inflate_files():
    current_dir = os.getcwd()
    output_dir = os.path.join(current_dir, "inflated_files")
    os.makedirs(output_dir, exist_ok=True)
    
    # Iterate through files matching level.dat# pattern
    for file in sorted(os.listdir(current_dir)):
        if file.startswith("level.dat") and file[len("level.dat"):].isdigit():
            input_path = os.path.join(current_dir, file)
            output_path = os.path.join(output_dir, f"{file}.inflated")
            
            try:
                with open(input_path, "rb") as f:
                    compressed_data = f.read()
                    inflated_data = zlib.decompress(compressed_data)
                    
                with open(output_path, "wb") as f:
                    f.write(inflated_data)
                    
                print(f"Inflated {file} -> {output_path}")
            except Exception as e:
                print(f"Failed to inflate {file}: {e}")

if __name__ == "__main__":
    inflate_files()
Step 5. Right click in a blank area of the folder with the .dat files and choose Open in Terminal

Step 6. Run the command: python .\inflate.py

Step 7. Using a text editor, create the following file in the new inflated_files folder with the level.dat.inflated files. Name it something like 'search.py'

Code: Select all

import os

def search_word(term):
    current_dir = os.getcwd()
    search_bytes = term.encode("utf-8")
    
    # Iterate through all files in the current directory
    for file in sorted(os.listdir(current_dir)):
        file_path = os.path.join(current_dir, file)
        
        if os.path.isfile(file_path):
            try:
                with open(file_path, "rb") as f:
                    data = f.read()
                    
                # Search for the given term in the binary data
                if search_bytes in data:
                    print(f"Found '{term}' in {file}")
            except Exception as e:
                print(f"Failed to process {file}: {e}")

if __name__ == "__main__":
    search_term = input("Enter the search term: ")
    search_word(search_term)
Step 8. Right click in a blank area of the inflated_files folder and choose Open in Terminal

Step 9. Run the command: python .\search.py and enter a search term to try and find the terms mentioned above, either 'editor', 'achievement', 'command', or others I may not have mentioned.

The results should hopefully find a mention of one of these words in one of the .dat files that is relatively at the point in your playtime that achievements were disabled. IE. If achievements were disabled recently, expect to find it in the highest numbered .dat files. If achievements were disabled roughly halfway through your saves play time, and you have 50 level.dat files, expect it to be around file 25.

Step 10. Open one/all of the found dat files with a hex editor (Notepad ++ with Hex editor plugin, or a freeware hex editor called HxD) and run a search for "FF FF FF FF FF FF FF FF". There may be quite a few, find the one that is almost immediately followed by another set of "FF FF FF FF FF FF FF FF", within 7 to 12 bytes.

If you have found it, you should see the two bytes preceding the first set of "FF"s with at least one of them being "01". Change both to "00". Save the file.

Step 11. Using a text editor, create the following file in the inflated_files folder with the level.dat.inflated files. Name it something like 'deflate.py'

Code: Select all

import os
import re
import zlib

def deflate_files():
    # Define input pattern and output directory
    pattern = re.compile(r"level\.dat(\d+)\.inflated$")
    output_dir = "deflated_files"
    os.makedirs(output_dir, exist_ok=True)
    
    # Iterate through files in the current directory
    for filename in sorted(os.listdir()):
        match = pattern.match(filename)
        if match:
            file_number = match.group(1)
            input_path = filename
            output_path = os.path.join(output_dir, f"level.dat{file_number}")
            
            try:
                with open(input_path, "rb") as f_in:
                    inflated_data = f_in.read()
                    deflated_data = zlib.compress(inflated_data)
                    
                with open(output_path, "wb") as f_out:
                    f_out.write(deflated_data)
                
                print(f"Deflated: {input_path} -> {output_path}")
            except Exception as e:
                print(f"Error processing {input_path}: {e}")

if __name__ == "__main__":
    deflate_files()
Step 12. Right click in a blank area of the inflated_files folder and choose Open in Terminal (Or use the already open terminal window if you didn't close it)

Step 13. Run the command: python .\deflate.py

Step 14. Go back to/re-open the save.zip in the temp folder. And copy the respective level.dat# files that you edited from inflated_files back into the save, overwriting the old .dat file.

Step 15. Copy the edited save to the Factorio save game folder.

If everything worked, you should have a save game with achievements enabled!

Sorry Wube :? But I had to use the editor to delete Aquilo to get the Dredgeworks: Frozen Reaches mod to work. 30 hours later I realised no achivements! ;)
User avatar
louanbastos
Burner Inserter
Burner Inserter
Posts: 5
Joined: Tue Dec 24, 2024 9:31 pm
Contact:

Re: Re-Enable Achievements...

Post by louanbastos »

hraukr wrote: Mon Mar 31, 2025 4:41 am Guide for Windows 11 users that have trouble with the previously mentioned tools.

To summarise above, in my understanding, you are looking for a string in the level.dat files that contains 8 hexadecimal "FF"s followed closely (within 8 to 16 hex bytes) by another set of 8 "FF"s. Immediately preceeding the first set of "FF"s are the two (three?) sets that determine whether achievements have been disabled and why.

EX.

Code: Select all

01 84 04 00 00 00 00 00 00 01 01 ff ff ff ff ff 
ff ff ff f9 23 12 04 20 20 20 20 ff ff ff ff ff 
ff ff ff 50 46 00 00 00 00 00 00 00 00 01 00 00
See on line 1, the two "01"s before the first "FF". Those represent the map editor and command line use, respectively. When either is set to "01", achievements are disabled. When both are set to "00", achievements are re-enabled for that save only. Global/Steam achievements are still disabled as far as I can tell.

Edit: I had ChatGPT work its magic...horrifying horrifying magic...and assembled this into a script that I have tested to work with my save file anyways. Not sure about edge cases but...lets say I am scared of AI/LLMs.

I also feel like I should say...NO WARRANTY OF THE BELOW CODE IS PROVIDED. USE AT YOUR OWN RISK!
Automated Method
Basically, . Create the following script in the folder. Open terminal in the temp directory. Run the script. Make sure to keep backups of your save. The save in the folder is recreated, copy this save to the Factorio save directory. Run the game and test the save.

Step 1. Install the latest Python from the microsoft store - https://apps.microsoft.com/detail/9PNRBTZXMB4Z

Step 2. Create a temp folder on your desktop and copy over your save from the Factorio save folder.

Step 3. Rename the save in the Factorio save folder <savename>.zip.bak as a backup. Save it in other places as well for safety!

Step 4. Using a text editor, create the following file in the temp folder with the save file. Name it something like 'achievementsfix.py'

Code: Select all

#!/usr/bin/env python3
import os
import re
import shutil
import zipfile
import tempfile
import zlib
import sys

# =======================
# Utility Functions
# =======================
def list_zip_files():
    files = [f for f in os.listdir('.') if f.lower().endswith('.zip')]
    if not files:
        print("No .zip files found in the current directory.")
        sys.exit(1)
    print("Found the following .zip files:")
    for i, file in enumerate(files, 1):
        print(f"  {i}: {file}")
    return files

def choose_zip_file(files):
    while True:
        choice = input("Enter the number of the .zip file to process: ").strip()
        if not choice.isdigit() or not (1 <= int(choice) <= len(files)):
            print("Invalid selection. Try again.")
        else:
            return files[int(choice)-1]

def backup_file(filepath):
    backup_path = filepath + ".bak"
    shutil.copy2(filepath, backup_path)
    print(f"Backup created: {backup_path}")

def extract_zip(zip_path, extract_dir):
    print(f"Extracting {zip_path} to temporary directory...")
    with zipfile.ZipFile(zip_path, 'r') as zip_ref:
        zip_ref.extractall(extract_dir)
    print("Extraction complete.")

def find_save_folder(extract_dir, zip_filename):
    # The save folder is assumed to be named as the zip filename without extension.
    savename = os.path.splitext(zip_filename)[0]
    save_folder = os.path.join(extract_dir, savename)
    if not os.path.isdir(save_folder):
        # If not found, try listing directories in extract_dir and choose the first one.
        dirs = [d for d in os.listdir(extract_dir) if os.path.isdir(os.path.join(extract_dir, d))]
        if dirs:
            save_folder = os.path.join(extract_dir, dirs[0])
            print(f"Could not find folder matching '{savename}'. Using '{dirs[0]}' instead.")
        else:
            print("No folder found inside the extracted zip!")
            sys.exit(1)
    return save_folder

def copy_level_dat_files(save_folder, dest_folder):
    os.makedirs(dest_folder, exist_ok=True)
    # Copy files that match level.dat<number>
    pattern = re.compile(r'^level\.dat\d+$')
    count = 0
    for f in sorted(os.listdir(save_folder)):
        if pattern.match(f):
            shutil.copy2(os.path.join(save_folder, f), os.path.join(dest_folder, f))
            count += 1
    print(f"Copied {count} level.dat files from {save_folder} to {dest_folder}.")

# =======================
# Step: Inflate files
# =======================
def inflate_files(input_dir, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    files = sorted(os.listdir(input_dir))
    print("Inflating level.dat files...")
    for file in files:
        if file.startswith("level.dat") and file[len("level.dat"):].isdigit():
            input_path = os.path.join(input_dir, file)
            output_path = os.path.join(output_dir, f"{file}.inflated")
            try:
                with open(input_path, "rb") as f:
                    compressed_data = f.read()
                    inflated_data = zlib.decompress(compressed_data)
                with open(output_path, "wb") as f:
                    f.write(inflated_data)
                print(f"Inflated {file} -> {output_path}")
            except Exception as e:
                print(f"Failed to inflate {file}: {e}")

# =======================
# Step: Search and Edit
# =======================
def hex_dump(data, start, end, bytes_per_line=16):
    lines = []
    for offset in range(start, end, bytes_per_line):
        line_bytes = data[offset:offset+bytes_per_line]
        hex_part = ' '.join(f"{b:02X}" for b in line_bytes)
        lines.append(f"{offset:08X}  {hex_part}")
    return "\n".join(lines)

def search_and_edit_inflated_files(inflated_dir):
    pattern = re.compile(b'(?P<before>.{2})(?P<first>(?:\xFF){8})(?P<middle>.{7,12})(?P<second>(?:\xFF){8})', re.DOTALL)
    edited_files = set()
    files = sorted(os.listdir(inflated_dir))
    print("Searching for hex pattern in inflated files...")
    for file in files:
        if not file.endswith(".inflated"):
            continue
        file_path = os.path.join(inflated_dir, file)
        try:
            with open(file_path, "rb") as f:
                data = f.read()
        except Exception as e:
            print(f"Error reading {file}: {e}")
            continue

        modified = False
        for m in pattern.finditer(data):
            before = m.group('before')
            # Check if at least one byte in 'before' is 0x01
            if b'\x01' in before:
                # Determine context for hex dump: one line (16 bytes) above and below the match region
                match_start = m.start()
                match_end = m.end()
                context_start = max(0, match_start - 16)
                context_end = min(len(data), match_end + 16)
                print("\n--- Context for file:", file, "---")
                print(hex_dump(data, context_start, context_end))
                print("--- End Context ---")
                answer = input("Edit the two bytes preceding the first set of FF's (change any 0x01 to 0x00)? (y/n): ").strip().lower()
                if answer == 'y':
                    # Replace the two bytes (positions m.start() to m.start()+2) with 0x00 0x00.
                    print(f"Editing {file} at offset {m.start()} ...")
                    data = data[:m.start()] + b'\x00\x00' + data[m.start()+2:]
                    modified = True
                    edited_files.add(file)
                    # For now, only modify the first occurrence per file.
                    break

        if modified:
            try:
                with open(file_path, "wb") as f:
                    f.write(data)
                print(f"File {file} edited successfully.")
            except Exception as e:
                print(f"Error writing edited data to {file}: {e}")
    if not edited_files:
        print("No files required editing based on the search criteria.")
    return edited_files

# =======================
# Step: Deflate files
# =======================
def deflate_files(input_dir, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    pattern = re.compile(r"level\.dat(\d+)\.inflated$")
    files = sorted(os.listdir(input_dir))
    print("Deflating files...")
    for filename in files:
        match = pattern.match(filename)
        if match:
            file_number = match.group(1)
            input_path = os.path.join(input_dir, filename)
            output_path = os.path.join(output_dir, f"level.dat{file_number}")
            try:
                with open(input_path, "rb") as f_in:
                    inflated_data = f_in.read()
                    deflated_data = zlib.compress(inflated_data)
                with open(output_path, "wb") as f_out:
                    f_out.write(deflated_data)
                print(f"Deflated: {filename} -> {output_path}")
            except Exception as e:
                print(f"Error processing {input_path}: {e}")

# =======================
# Step: Update the original zip file
# =======================
def update_zip_with_deflated_files(deflated_dir, extracted_save_folder, original_zip_path):
    print("Updating the save with edited files...")
    # Overwrite the level.dat files in the extracted save folder with our deflated ones.
    count = 0
    for file in os.listdir(deflated_dir):
        source_file = os.path.join(deflated_dir, file)
        target_file = os.path.join(extracted_save_folder, file)
        if os.path.isfile(source_file):
            shutil.copy2(source_file, target_file)
            print(f"Updated {target_file}")
            count += 1
    print(f"Updated {count} files in the extracted save folder.")

    # Now, rezip the extracted save folder (keeping original folder structure)
    temp_zip = original_zip_path + ".temp"
    with zipfile.ZipFile(temp_zip, 'w', zipfile.ZIP_DEFLATED) as zip_out:
        # Walk the extracted directory (its parent is the temp extraction dir)
        base_dir = os.path.dirname(extracted_save_folder)
        for root, dirs, files in os.walk(base_dir):
            for file in files:
                full_path = os.path.join(root, file)
                # Arcname should be relative to base_dir
                arcname = os.path.relpath(full_path, base_dir)
                zip_out.write(full_path, arcname)
    # Replace the original zip with the new zip.
    shutil.move(temp_zip, original_zip_path)
    print(f"Original zip '{original_zip_path}' updated successfully.")

# =======================
# Cleanup function
# =======================
def cleanup_temp_dirs(dirs):
    for d in dirs:
        if os.path.isdir(d):
            try:
                shutil.rmtree(d)
                print(f"Deleted temporary directory: {d}")
            except Exception as e:
                print(f"Failed to delete {d}: {e}")

# =======================
# Main process
# =======================
def main():
    print("Factorio Save Editor Script Starting...\n")
    zip_files = list_zip_files()
    zip_choice = choose_zip_file(zip_files)
    original_zip_path = os.path.abspath(zip_choice)
    backup_file(original_zip_path)
    
    # Create a temporary extraction directory
    temp_extract_dir = tempfile.mkdtemp(prefix="factorio_extract_")
    extract_zip(original_zip_path, temp_extract_dir)
    
    extracted_save_folder = find_save_folder(temp_extract_dir, zip_choice)
    
    # Create a working directory for level.dat files extraction
    level_dat_workdir = os.path.join(os.getcwd(), "extracted_level_dat")
    os.makedirs(level_dat_workdir, exist_ok=True)
    copy_level_dat_files(extracted_save_folder, level_dat_workdir)
    
    # Inflate the level.dat files
    inflated_dir = os.path.join(os.getcwd(), "inflated_files")
    inflate_files(level_dat_workdir, inflated_dir)
    
    # Search and interactively edit inflated files
    search_and_edit_inflated_files(inflated_dir)
    
    # Deflate the (edited) files
    deflated_dir = os.path.join(os.getcwd(), "deflated_files")
    deflate_files(inflated_dir, deflated_dir)
    
    # Update the extracted save folder with the deflated files and rezip
    update_zip_with_deflated_files(deflated_dir, extracted_save_folder, original_zip_path)
    
    # Ask user if they want to clean up temporary files.
    answer = input("Do you want to delete temporary files and directories? (y/n): ").strip().lower()
    if answer == 'y':
        cleanup_temp_dirs([temp_extract_dir, level_dat_workdir, inflated_dir, deflated_dir])
    else:
        print("Temporary files kept. They are located in:")
        print(f"  Extraction dir: {temp_extract_dir}")
        print(f"  Level.dat working dir: {level_dat_workdir}")
        print(f"  Inflated files dir: {inflated_dir}")
        print(f"  Deflated files dir: {deflated_dir}")
    
    print("Processing complete. Save file updated.")

if __name__ == "__main__":
    main()
Step 5. Right click in a blank area of the temp folder and choose Open in Terminal

Step 6. Run the command below and follow the prompts

Code: Select all

python .\achievementfix.py
That should do it!
ChatGPT wrote:Explanation of the Script

File Selection & Backup:
The script lists all .zip files in the current directory, lets you choose one, and creates a backup (with a “.bak” extension).

Extraction:
It extracts the entire .zip to a temporary directory. It then determines the save folder (expected to be the folder named like the zip file).

Extracting level.dat Files:
All files matching the pattern level.dat<number> are copied from the save folder to a working directory.

Inflate:
The script inflates these files (using zlib.decompress) into an “inflated_files” directory.

Search & Edit:
It searches each inflated file for the hex pattern (8 consecutive FF bytes, followed within 7–12 bytes by another 8 FF bytes) and checks the two bytes immediately preceding the first FF group. If either of those bytes is 0x01, it shows a hex dump context and asks for confirmation to change them to 0x00.

Deflate:
The edited files are recompressed (using zlib.compress) into a “deflated_files” directory.

Update .zip:
The deflated files are copied back into the extracted save folder, and the entire save is rezipped—overwriting the original .zip file.

Cleanup:
Finally, the script prompts whether to delete the temporary directories created during the process.

Run the script from a terminal and follow the prompts. Always test on a backup save first!
Manual Method
To manually disable these bits on Windows 11 , if the script above does not work:

Step 1. Install the latest Python from the microsoft store - https://apps.microsoft.com/detail/9PNRBTZXMB4Z

Step 2. Create a save of your game with a new name. Leave the game and go to the Factorio save folder, and copy the new save to a new temporary folder on your desktop or elsewhere unimportant.

Step 3. Using 7zip or another archive tool, open the save zip file in copy the level.dat# files from to a new folder within the temp folder

Step 4. Using a text editor, create the following file in the new folder with the level.dat files. Name it something like 'inflate.py'

Code: Select all

import os
import zlib

def inflate_files():
    current_dir = os.getcwd()
    output_dir = os.path.join(current_dir, "inflated_files")
    os.makedirs(output_dir, exist_ok=True)
    
    # Iterate through files matching level.dat# pattern
    for file in sorted(os.listdir(current_dir)):
        if file.startswith("level.dat") and file[len("level.dat"):].isdigit():
            input_path = os.path.join(current_dir, file)
            output_path = os.path.join(output_dir, f"{file}.inflated")
            
            try:
                with open(input_path, "rb") as f:
                    compressed_data = f.read()
                    inflated_data = zlib.decompress(compressed_data)
                    
                with open(output_path, "wb") as f:
                    f.write(inflated_data)
                    
                print(f"Inflated {file} -> {output_path}")
            except Exception as e:
                print(f"Failed to inflate {file}: {e}")

if __name__ == "__main__":
    inflate_files()
Step 5. Right click in a blank area of the folder with the .dat files and choose Open in Terminal

Step 6. Run the command: python .\inflate.py

Step 7. Using a text editor, create the following file in the new inflated_files folder with the level.dat.inflated files. Name it something like 'search.py'

Code: Select all

import os

def search_word(term):
    current_dir = os.getcwd()
    search_bytes = term.encode("utf-8")
    
    # Iterate through all files in the current directory
    for file in sorted(os.listdir(current_dir)):
        file_path = os.path.join(current_dir, file)
        
        if os.path.isfile(file_path):
            try:
                with open(file_path, "rb") as f:
                    data = f.read()
                    
                # Search for the given term in the binary data
                if search_bytes in data:
                    print(f"Found '{term}' in {file}")
            except Exception as e:
                print(f"Failed to process {file}: {e}")

if __name__ == "__main__":
    search_term = input("Enter the search term: ")
    search_word(search_term)
Step 8. Right click in a blank area of the inflated_files folder and choose Open in Terminal

Step 9. Run the command: python .\search.py and enter a search term to try and find the terms mentioned above, either 'editor', 'achievement', 'command', or others I may not have mentioned.

The results should hopefully find a mention of one of these words in one of the .dat files that is relatively at the point in your playtime that achievements were disabled. IE. If achievements were disabled recently, expect to find it in the highest numbered .dat files. If achievements were disabled roughly halfway through your saves play time, and you have 50 level.dat files, expect it to be around file 25.

Step 10. Open one/all of the found dat files with a hex editor (Notepad ++ with Hex editor plugin, or a freeware hex editor called HxD) and run a search for "FF FF FF FF FF FF FF FF". There may be quite a few, find the one that is almost immediately followed by another set of "FF FF FF FF FF FF FF FF", within 7 to 12 bytes.

If you have found it, you should see the two bytes preceding the first set of "FF"s with at least one of them being "01". Change both to "00". Save the file.

Step 11. Using a text editor, create the following file in the inflated_files folder with the level.dat.inflated files. Name it something like 'deflate.py'

Code: Select all

import os
import re
import zlib

def deflate_files():
    # Define input pattern and output directory
    pattern = re.compile(r"level\.dat(\d+)\.inflated$")
    output_dir = "deflated_files"
    os.makedirs(output_dir, exist_ok=True)
    
    # Iterate through files in the current directory
    for filename in sorted(os.listdir()):
        match = pattern.match(filename)
        if match:
            file_number = match.group(1)
            input_path = filename
            output_path = os.path.join(output_dir, f"level.dat{file_number}")
            
            try:
                with open(input_path, "rb") as f_in:
                    inflated_data = f_in.read()
                    deflated_data = zlib.compress(inflated_data)
                    
                with open(output_path, "wb") as f_out:
                    f_out.write(deflated_data)
                
                print(f"Deflated: {input_path} -> {output_path}")
            except Exception as e:
                print(f"Error processing {input_path}: {e}")

if __name__ == "__main__":
    deflate_files()
Step 12. Right click in a blank area of the inflated_files folder and choose Open in Terminal (Or use the already open terminal window if you didn't close it)

Step 13. Run the command: python .\deflate.py

Step 14. Go back to/re-open the save.zip in the temp folder. And copy the respective level.dat# files that you edited from inflated_files back into the save, overwriting the old .dat file.

Step 15. Copy the edited save to the Factorio save game folder.

If everything worked, you should have a save game with achievements enabled!

Sorry Wube :? But I had to use the editor to delete Aquilo to get the Dredgeworks: Frozen Reaches mod to work. 30 hours later I realised no achivements! ;)
Did not work for me on 2.0.43

Anyway to fix?
User avatar
louanbastos
Burner Inserter
Burner Inserter
Posts: 5
Joined: Tue Dec 24, 2024 9:31 pm
Contact:

Re: Re-Enable Achievements...

Post by louanbastos »

Hey guys, I created a mod where you can use commands, and other things that can help you edit your map without worrying about Achievements.

https://mods.factorio.com/mod/factorio- ... and-center
oriek
Manual Inserter
Manual Inserter
Posts: 1
Joined: Wed May 14, 2025 10:54 am
Contact:

Re: Re-Enable Achievements...

Post by oriek »

hraukr wrote: Mon Mar 31, 2025 4:41 am Guide for Windows 11 users that have trouble with the previously mentioned tools.

To summarise above, in my understanding, you are looking for a string in the level.dat files that contains 8 hexadecimal "FF"s followed closely (within 8 to 16 hex bytes) by another set of 8 "FF"s. Immediately preceeding the first set of "FF"s are the two (three?) sets that determine whether achievements have been disabled and why.

EX.

Code: Select all

01 84 04 00 00 00 00 00 00 01 01 ff ff ff ff ff 
ff ff ff f9 23 12 04 20 20 20 20 ff ff ff ff ff 
ff ff ff 50 46 00 00 00 00 00 00 00 00 01 00 00
See on line 1, the two "01"s before the first "FF". Those represent the map editor and command line use, respectively. When either is set to "01", achievements are disabled. When both are set to "00", achievements are re-enabled for that save only. Global/Steam achievements are still disabled as far as I can tell.

Edit: I had ChatGPT work its magic...horrifying horrifying magic...and assembled this into a script that I have tested to work with my save file anyways. Not sure about edge cases but...lets say I am scared of AI/LLMs.

I also feel like I should say...NO WARRANTY OF THE BELOW CODE IS PROVIDED. USE AT YOUR OWN RISK!
Automated Method
Basically, . Create the following script in the folder. Open terminal in the temp directory. Run the script. Make sure to keep backups of your save. The save in the folder is recreated, copy this save to the Factorio save directory. Run the game and test the save.

Step 1. Install the latest Python from the microsoft store - https://apps.microsoft.com/detail/9PNRBTZXMB4Z

Step 2. Create a temp folder on your desktop and copy over your save from the Factorio save folder.

Step 3. Rename the save in the Factorio save folder <savename>.zip.bak as a backup. Save it in other places as well for safety!

Step 4. Using a text editor, create the following file in the temp folder with the save file. Name it something like 'achievementsfix.py'

Code: Select all

#!/usr/bin/env python3
import os
import re
import shutil
import zipfile
import tempfile
import zlib
import sys

# =======================
# Utility Functions
# =======================
def list_zip_files():
    files = [f for f in os.listdir('.') if f.lower().endswith('.zip')]
    if not files:
        print("No .zip files found in the current directory.")
        sys.exit(1)
    print("Found the following .zip files:")
    for i, file in enumerate(files, 1):
        print(f"  {i}: {file}")
    return files

def choose_zip_file(files):
    while True:
        choice = input("Enter the number of the .zip file to process: ").strip()
        if not choice.isdigit() or not (1 <= int(choice) <= len(files)):
            print("Invalid selection. Try again.")
        else:
            return files[int(choice)-1]

def backup_file(filepath):
    backup_path = filepath + ".bak"
    shutil.copy2(filepath, backup_path)
    print(f"Backup created: {backup_path}")

def extract_zip(zip_path, extract_dir):
    print(f"Extracting {zip_path} to temporary directory...")
    with zipfile.ZipFile(zip_path, 'r') as zip_ref:
        zip_ref.extractall(extract_dir)
    print("Extraction complete.")

def find_save_folder(extract_dir, zip_filename):
    # The save folder is assumed to be named as the zip filename without extension.
    savename = os.path.splitext(zip_filename)[0]
    save_folder = os.path.join(extract_dir, savename)
    if not os.path.isdir(save_folder):
        # If not found, try listing directories in extract_dir and choose the first one.
        dirs = [d for d in os.listdir(extract_dir) if os.path.isdir(os.path.join(extract_dir, d))]
        if dirs:
            save_folder = os.path.join(extract_dir, dirs[0])
            print(f"Could not find folder matching '{savename}'. Using '{dirs[0]}' instead.")
        else:
            print("No folder found inside the extracted zip!")
            sys.exit(1)
    return save_folder

def copy_level_dat_files(save_folder, dest_folder):
    os.makedirs(dest_folder, exist_ok=True)
    # Copy files that match level.dat<number>
    pattern = re.compile(r'^level\.dat\d+$')
    count = 0
    for f in sorted(os.listdir(save_folder)):
        if pattern.match(f):
            shutil.copy2(os.path.join(save_folder, f), os.path.join(dest_folder, f))
            count += 1
    print(f"Copied {count} level.dat files from {save_folder} to {dest_folder}.")

# =======================
# Step: Inflate files
# =======================
def inflate_files(input_dir, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    files = sorted(os.listdir(input_dir))
    print("Inflating level.dat files...")
    for file in files:
        if file.startswith("level.dat") and file[len("level.dat"):].isdigit():
            input_path = os.path.join(input_dir, file)
            output_path = os.path.join(output_dir, f"{file}.inflated")
            try:
                with open(input_path, "rb") as f:
                    compressed_data = f.read()
                    inflated_data = zlib.decompress(compressed_data)
                with open(output_path, "wb") as f:
                    f.write(inflated_data)
                print(f"Inflated {file} -> {output_path}")
            except Exception as e:
                print(f"Failed to inflate {file}: {e}")

# =======================
# Step: Search and Edit
# =======================
def hex_dump(data, start, end, bytes_per_line=16):
    lines = []
    for offset in range(start, end, bytes_per_line):
        line_bytes = data[offset:offset+bytes_per_line]
        hex_part = ' '.join(f"{b:02X}" for b in line_bytes)
        lines.append(f"{offset:08X}  {hex_part}")
    return "\n".join(lines)

def search_and_edit_inflated_files(inflated_dir):
    pattern = re.compile(b'(?P<before>.{2})(?P<first>(?:\xFF){8})(?P<middle>.{7,12})(?P<second>(?:\xFF){8})', re.DOTALL)
    edited_files = set()
    files = sorted(os.listdir(inflated_dir))
    print("Searching for hex pattern in inflated files...")
    for file in files:
        if not file.endswith(".inflated"):
            continue
        file_path = os.path.join(inflated_dir, file)
        try:
            with open(file_path, "rb") as f:
                data = f.read()
        except Exception as e:
            print(f"Error reading {file}: {e}")
            continue

        modified = False
        for m in pattern.finditer(data):
            before = m.group('before')
            # Check if at least one byte in 'before' is 0x01
            if b'\x01' in before:
                # Determine context for hex dump: one line (16 bytes) above and below the match region
                match_start = m.start()
                match_end = m.end()
                context_start = max(0, match_start - 16)
                context_end = min(len(data), match_end + 16)
                print("\n--- Context for file:", file, "---")
                print(hex_dump(data, context_start, context_end))
                print("--- End Context ---")
                answer = input("Edit the two bytes preceding the first set of FF's (change any 0x01 to 0x00)? (y/n): ").strip().lower()
                if answer == 'y':
                    # Replace the two bytes (positions m.start() to m.start()+2) with 0x00 0x00.
                    print(f"Editing {file} at offset {m.start()} ...")
                    data = data[:m.start()] + b'\x00\x00' + data[m.start()+2:]
                    modified = True
                    edited_files.add(file)
                    # For now, only modify the first occurrence per file.
                    break

        if modified:
            try:
                with open(file_path, "wb") as f:
                    f.write(data)
                print(f"File {file} edited successfully.")
            except Exception as e:
                print(f"Error writing edited data to {file}: {e}")
    if not edited_files:
        print("No files required editing based on the search criteria.")
    return edited_files

# =======================
# Step: Deflate files
# =======================
def deflate_files(input_dir, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    pattern = re.compile(r"level\.dat(\d+)\.inflated$")
    files = sorted(os.listdir(input_dir))
    print("Deflating files...")
    for filename in files:
        match = pattern.match(filename)
        if match:
            file_number = match.group(1)
            input_path = os.path.join(input_dir, filename)
            output_path = os.path.join(output_dir, f"level.dat{file_number}")
            try:
                with open(input_path, "rb") as f_in:
                    inflated_data = f_in.read()
                    deflated_data = zlib.compress(inflated_data)
                with open(output_path, "wb") as f_out:
                    f_out.write(deflated_data)
                print(f"Deflated: {filename} -> {output_path}")
            except Exception as e:
                print(f"Error processing {input_path}: {e}")

# =======================
# Step: Update the original zip file
# =======================
def update_zip_with_deflated_files(deflated_dir, extracted_save_folder, original_zip_path):
    print("Updating the save with edited files...")
    # Overwrite the level.dat files in the extracted save folder with our deflated ones.
    count = 0
    for file in os.listdir(deflated_dir):
        source_file = os.path.join(deflated_dir, file)
        target_file = os.path.join(extracted_save_folder, file)
        if os.path.isfile(source_file):
            shutil.copy2(source_file, target_file)
            print(f"Updated {target_file}")
            count += 1
    print(f"Updated {count} files in the extracted save folder.")

    # Now, rezip the extracted save folder (keeping original folder structure)
    temp_zip = original_zip_path + ".temp"
    with zipfile.ZipFile(temp_zip, 'w', zipfile.ZIP_DEFLATED) as zip_out:
        # Walk the extracted directory (its parent is the temp extraction dir)
        base_dir = os.path.dirname(extracted_save_folder)
        for root, dirs, files in os.walk(base_dir):
            for file in files:
                full_path = os.path.join(root, file)
                # Arcname should be relative to base_dir
                arcname = os.path.relpath(full_path, base_dir)
                zip_out.write(full_path, arcname)
    # Replace the original zip with the new zip.
    shutil.move(temp_zip, original_zip_path)
    print(f"Original zip '{original_zip_path}' updated successfully.")

# =======================
# Cleanup function
# =======================
def cleanup_temp_dirs(dirs):
    for d in dirs:
        if os.path.isdir(d):
            try:
                shutil.rmtree(d)
                print(f"Deleted temporary directory: {d}")
            except Exception as e:
                print(f"Failed to delete {d}: {e}")

# =======================
# Main process
# =======================
def main():
    print("Factorio Save Editor Script Starting...\n")
    zip_files = list_zip_files()
    zip_choice = choose_zip_file(zip_files)
    original_zip_path = os.path.abspath(zip_choice)
    backup_file(original_zip_path)
    
    # Create a temporary extraction directory
    temp_extract_dir = tempfile.mkdtemp(prefix="factorio_extract_")
    extract_zip(original_zip_path, temp_extract_dir)
    
    extracted_save_folder = find_save_folder(temp_extract_dir, zip_choice)
    
    # Create a working directory for level.dat files extraction
    level_dat_workdir = os.path.join(os.getcwd(), "extracted_level_dat")
    os.makedirs(level_dat_workdir, exist_ok=True)
    copy_level_dat_files(extracted_save_folder, level_dat_workdir)
    
    # Inflate the level.dat files
    inflated_dir = os.path.join(os.getcwd(), "inflated_files")
    inflate_files(level_dat_workdir, inflated_dir)
    
    # Search and interactively edit inflated files
    search_and_edit_inflated_files(inflated_dir)
    
    # Deflate the (edited) files
    deflated_dir = os.path.join(os.getcwd(), "deflated_files")
    deflate_files(inflated_dir, deflated_dir)
    
    # Update the extracted save folder with the deflated files and rezip
    update_zip_with_deflated_files(deflated_dir, extracted_save_folder, original_zip_path)
    
    # Ask user if they want to clean up temporary files.
    answer = input("Do you want to delete temporary files and directories? (y/n): ").strip().lower()
    if answer == 'y':
        cleanup_temp_dirs([temp_extract_dir, level_dat_workdir, inflated_dir, deflated_dir])
    else:
        print("Temporary files kept. They are located in:")
        print(f"  Extraction dir: {temp_extract_dir}")
        print(f"  Level.dat working dir: {level_dat_workdir}")
        print(f"  Inflated files dir: {inflated_dir}")
        print(f"  Deflated files dir: {deflated_dir}")
    
    print("Processing complete. Save file updated.")

if __name__ == "__main__":
    main()
Step 5. Right click in a blank area of the temp folder and choose Open in Terminal

Step 6. Run the command below and follow the prompts

Code: Select all

python .\achievementfix.py
That should do it!
ChatGPT wrote:Explanation of the Script

File Selection & Backup:
The script lists all .zip files in the current directory, lets you choose one, and creates a backup (with a “.bak” extension).

Extraction:
It extracts the entire .zip to a temporary directory. It then determines the save folder (expected to be the folder named like the zip file).

Extracting level.dat Files:
All files matching the pattern level.dat<number> are copied from the save folder to a working directory.

Inflate:
The script inflates these files (using zlib.decompress) into an “inflated_files” directory.

Search & Edit:
It searches each inflated file for the hex pattern (8 consecutive FF bytes, followed within 7–12 bytes by another 8 FF bytes) and checks the two bytes immediately preceding the first FF group. If either of those bytes is 0x01, it shows a hex dump context and asks for confirmation to change them to 0x00.

Deflate:
The edited files are recompressed (using zlib.compress) into a “deflated_files” directory.

Update .zip:
The deflated files are copied back into the extracted save folder, and the entire save is rezipped—overwriting the original .zip file.

Cleanup:
Finally, the script prompts whether to delete the temporary directories created during the process.

Run the script from a terminal and follow the prompts. Always test on a backup save first!
Manual Method
To manually disable these bits on Windows 11 , if the script above does not work:

Step 1. Install the latest Python from the microsoft store - https://apps.microsoft.com/detail/9PNRBTZXMB4Z

Step 2. Create a save of your game with a new name. Leave the game and go to the Factorio save folder, and copy the new save to a new temporary folder on your desktop or elsewhere unimportant.

Step 3. Using 7zip or another archive tool, open the save zip file in copy the level.dat# files from to a new folder within the temp folder

Step 4. Using a text editor, create the following file in the new folder with the level.dat files. Name it something like 'inflate.py'

Code: Select all

import os
import zlib

def inflate_files():
    current_dir = os.getcwd()
    output_dir = os.path.join(current_dir, "inflated_files")
    os.makedirs(output_dir, exist_ok=True)
    
    # Iterate through files matching level.dat# pattern
    for file in sorted(os.listdir(current_dir)):
        if file.startswith("level.dat") and file[len("level.dat"):].isdigit():
            input_path = os.path.join(current_dir, file)
            output_path = os.path.join(output_dir, f"{file}.inflated")
            
            try:
                with open(input_path, "rb") as f:
                    compressed_data = f.read()
                    inflated_data = zlib.decompress(compressed_data)
                    
                with open(output_path, "wb") as f:
                    f.write(inflated_data)
                    
                print(f"Inflated {file} -> {output_path}")
            except Exception as e:
                print(f"Failed to inflate {file}: {e}")

if __name__ == "__main__":
    inflate_files()
Step 5. Right click in a blank area of the folder with the .dat files and choose Open in Terminal

Step 6. Run the command: python .\inflate.py

Step 7. Using a text editor, create the following file in the new inflated_files folder with the level.dat.inflated files. Name it something like 'search.py'

Code: Select all

import os

def search_word(term):
    current_dir = os.getcwd()
    search_bytes = term.encode("utf-8")
    
    # Iterate through all files in the current directory
    for file in sorted(os.listdir(current_dir)):
        file_path = os.path.join(current_dir, file)
        
        if os.path.isfile(file_path):
            try:
                with open(file_path, "rb") as f:
                    data = f.read()
                    
                # Search for the given term in the binary data
                if search_bytes in data:
                    print(f"Found '{term}' in {file}")
            except Exception as e:
                print(f"Failed to process {file}: {e}")

if __name__ == "__main__":
    search_term = input("Enter the search term: ")
    search_word(search_term)
Step 8. Right click in a blank area of the inflated_files folder and choose Open in Terminal

Step 9. Run the command: python .\search.py and enter a search term to try and find the terms mentioned above, either 'editor', 'achievement', 'command', or others I may not have mentioned.

The results should hopefully find a mention of one of these words in one of the .dat files that is relatively at the point in your playtime that achievements were disabled. IE. If achievements were disabled recently, expect to find it in the highest numbered .dat files. If achievements were disabled roughly halfway through your saves play time, and you have 50 level.dat files, expect it to be around file 25.

Step 10. Open one/all of the found dat files with a hex editor (Notepad ++ with Hex editor plugin, or a freeware hex editor called HxD) and run a search for "FF FF FF FF FF FF FF FF". There may be quite a few, find the one that is almost immediately followed by another set of "FF FF FF FF FF FF FF FF", within 7 to 12 bytes.

If you have found it, you should see the two bytes preceding the first set of "FF"s with at least one of them being "01". Change both to "00". Save the file.

Step 11. Using a text editor, create the following file in the inflated_files folder with the level.dat.inflated files. Name it something like 'deflate.py'

Code: Select all

import os
import re
import zlib

def deflate_files():
    # Define input pattern and output directory
    pattern = re.compile(r"level\.dat(\d+)\.inflated$")
    output_dir = "deflated_files"
    os.makedirs(output_dir, exist_ok=True)
    
    # Iterate through files in the current directory
    for filename in sorted(os.listdir()):
        match = pattern.match(filename)
        if match:
            file_number = match.group(1)
            input_path = filename
            output_path = os.path.join(output_dir, f"level.dat{file_number}")
            
            try:
                with open(input_path, "rb") as f_in:
                    inflated_data = f_in.read()
                    deflated_data = zlib.compress(inflated_data)
                    
                with open(output_path, "wb") as f_out:
                    f_out.write(deflated_data)
                
                print(f"Deflated: {input_path} -> {output_path}")
            except Exception as e:
                print(f"Error processing {input_path}: {e}")

if __name__ == "__main__":
    deflate_files()
Step 12. Right click in a blank area of the inflated_files folder and choose Open in Terminal (Or use the already open terminal window if you didn't close it)

Step 13. Run the command: python .\deflate.py

Step 14. Go back to/re-open the save.zip in the temp folder. And copy the respective level.dat# files that you edited from inflated_files back into the save, overwriting the old .dat file.

Step 15. Copy the edited save to the Factorio save game folder.

If everything worked, you should have a save game with achievements enabled!

Sorry Wube :? But I had to use the editor to delete Aquilo to get the Dredgeworks: Frozen Reaches mod to work. 30 hours later I realised no achivements! ;)
Created a forum account just so I could say thank you, you absolute legend! Can confirm the python script worked for me on v2.0.49. I was using editor mode to test some designs like I often do, but in my sleep deprived state I saved over my file through muscle memory. Which was a terrifying prospect as this is a 100+ hour save with no mods going for all achievements!! Thank you again :)
Post Reply

Return to “General discussion”