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

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.xrobau wrote: Mon Nov 04, 2024 2:01 amThis doesn't appear to work when purely enabling the editor. There is no 'command-ran' when simply starting a new game and then /editor.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
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.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.
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...
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.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
So after some digging I was able to re-enable my achievements after using both commands and running /editorxrobau wrote: Mon Nov 04, 2024 2:01 amThis doesn't appear to work when purely enabling the editor. There is no 'command-ran' when simply starting a new game and then /editor.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
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?
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
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()
Code: Select all
python .\achievementfix.py
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!
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()
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)
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()
Did not work for me on 2.0.43hraukr 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.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.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
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'
Step 5. Right click in a blank area of the temp folder and choose Open in TerminalCode: 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 6. Run the command below and follow the promptsThat should do it!Code: Select all
python .\achievementfix.py
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'
Step 5. Right click in a blank area of the folder with the .dat files and choose Open in TerminalCode: 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 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'
Step 8. Right click in a blank area of the inflated_files folder and choose Open in TerminalCode: 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 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'
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)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 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 WubeBut 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 againhraukr 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.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.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
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'
Step 5. Right click in a blank area of the temp folder and choose Open in TerminalCode: 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 6. Run the command below and follow the promptsThat should do it!Code: Select all
python .\achievementfix.py
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'
Step 5. Right click in a blank area of the folder with the .dat files and choose Open in TerminalCode: 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 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'
Step 8. Right click in a blank area of the inflated_files folder and choose Open in TerminalCode: 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 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'
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)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 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 WubeBut 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!
![]()