diff --git a/README.md b/README.md index 111fc04..f91ec9e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/games.json b/games.json index 8dd0e86..7a43c26 100644 --- a/games.json +++ b/games.json @@ -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", diff --git a/main.py b/main.py index 15f80f5..7530b18 100644 --- a/main.py +++ b/main.py @@ -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// + + 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: