Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Legend: ✅ Confirmed working, ❔ Unconfirmed, - Not available in the store
| Trials of Mana | ✅ | ❔ |
| Wo Long: Fallen Dynasty | ❔ | - |
| Yakuza 0 | ✅ | - |
| Fallout 4 | ✅ | - |

## Incompatible games
These games use different save formats than the Steam/Epic version that can't be easily converted.
Expand Down
5 changes: 5 additions & 0 deletions games.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
"package": "MattMakesGamesInc.Celeste_79daxvg0dq3v6",
"handler": "1c1f"
},
{
"name": "Fallout 4",
"package": "BethesdaSoftworks.Fallout4-CoreGame_3275kfvn8vcwc",
"handler": "fallout4"
},
{
"name": "Doom Eternal",
"package": "BethesdaSoftworks.DOOMEternal-PC_3275kfvn8vcwc",
Expand Down
70 changes: 70 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,76 @@ def get_save_paths(

save_meta.append((sfs_name, sfs_path))

elif handler_name == "fallout4":
"""
Fallout 4 (Xbox Game Pass) uses Bethesda's chunked WGS save format.

Each save slot lives under:
Saves/<slot_name>/

Each slot contains exactly:
- toc
- ChunkData0 (currently always a single chunk)

This handler reconstructs each save slot into a single .fos file
suitable for Steam / non-WGS usage.
"""

pad_str = "padding\0" * 2

for container in containers:
path = PurePath(container["name"])

# Fallout 4 containers include both Saves/ and Settings/
# We only want actual save slots
if path.parent.name != "Saves":
continue

save_name = path.name # already ends in .fos

parts = {}

names = [f["name"] for f in container["files"]]
is_chunked = "toc" in names

for file in container["files"]:
name = file["name"]

if name == "toc":
continue

if not is_chunked:
continue

if name.startswith("ChunkData"):
idx = int(name.removeprefix("ChunkData"))
else:
# Ignore unexpected files safely
continue

parts[idx] = file["path"]

if not parts:
continue

# Reconstruct the .fos save
temp_folder = Path(temp_dir.name) / "Fallout4"
temp_folder.mkdir(exist_ok=True)

fos_path = temp_folder / save_name

with fos_path.open("wb") as fos_f:
for idx, part_path in sorted(parts.items()):
with open(part_path, "rb") as part_f:
data = part_f.read()
size = fos_f.write(data)
pad = 16 - (size % 16)
if pad != 16:
fos_f.write(pad_str[:pad].encode("ascii"))

save_meta.append((save_name, fos_path))


elif handler_name == "lies-of-p":
# Lies of P
for container in containers:
Expand Down