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
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!
