Last active
June 22, 2024 07:59
-
-
Save Bluefissure/f277a3409cdaf09cddbad5983fd01f68 to your computer and use it in GitHub Desktop.
Fix broken palworld save caused by existing guild & too many capture logs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# author: Bluefissure | |
# License: MIT License | |
# Description: Fixes Palworld brokwn save files corrupted by someone existing the guild | |
# Based on the work of https://github.com/cheahjs/palworld-save-tools/releases/tag/v0.13.0 | |
import argparse | |
import codecs | |
import os | |
import json | |
from lib.gvas import GvasFile | |
from lib.noindent import CustomEncoder | |
from lib.palsav import compress_gvas_to_sav, decompress_sav_to_gvas | |
from lib.paltypes import PALWORLD_CUSTOM_PROPERTIES, PALWORLD_TYPE_HINTS | |
def main(): | |
parser = argparse.ArgumentParser( | |
prog="palworld-fix-tools", | |
description="Fixes Palworld brokwn save files corrupted by someone existing the guild", | |
) | |
parser.add_argument("filename") | |
parser.add_argument( | |
"--analyze", | |
action="store_true", | |
help="Analyzes the file and prints out the missing characters", | |
) | |
parser.add_argument( | |
"--export", | |
action="store_true", | |
help="Export the content of Level.sav.json", | |
) | |
parser.add_argument( | |
"--fix-missing", | |
action="store_true", | |
help="Fix the missing players caused by exiting guild by restoring the missing characters from backup", | |
) | |
parser.add_argument( | |
"--fix-capture", | |
action="store_true", | |
help="Fix the too many capture logs", | |
) | |
parser.add_argument( | |
"--backup", | |
help="The backup file to be read from", | |
) | |
parser.add_argument( | |
"--output", | |
"-o", | |
help="Output file (default: <filename>_fixed.sav)", | |
) | |
args = parser.parse_args() | |
if not os.path.exists(args.filename): | |
print(f"{args.filename} does not exist") | |
exit(1) | |
if not os.path.isfile(args.filename): | |
print(f"{args.filename} is not a file") | |
exit(1) | |
if args.analyze: | |
analyze_gvas(args.filename, args.export) | |
if args.fix_missing: | |
if not args.backup: | |
print("Backup file is required for fixing") | |
exit(1) | |
if not args.output: | |
output_path = args.filename.replace(".sav", "_fixed.sav") | |
else: | |
output_path = args.output | |
fix_missing(args.filename, args.backup, output_path) | |
if args.fix_capture: | |
if not args.output: | |
output_path = args.filename.replace(".sav", "_fixed.sav") | |
else: | |
output_path = args.output | |
fix_capture(args.filename, output_path) | |
def analyze_gvas(filename, export=False) -> (GvasFile, dict, set[str], set[str]): | |
print(f"Analyzing {filename}") | |
all_players_in_guild = {} | |
exist_players_uid = set() | |
with open(filename, "rb") as f: | |
data = f.read() | |
raw_gvas, _ = decompress_sav_to_gvas(data) | |
gvas_file = GvasFile.read(raw_gvas, PALWORLD_TYPE_HINTS, PALWORLD_CUSTOM_PROPERTIES) | |
if export: | |
for (key, value) in gvas_file.properties["worldSaveData"]["value"].items(): | |
file_path = filename.replace(".sav", "") | |
export_file = f"{file_path}_{key}.json" | |
print(f"Exporting {key} to {export_file}") | |
with codecs.open(export_file, "w", "utf8") as f: | |
json.dump(value, f, indent=4, cls=CustomEncoder) | |
for group in gvas_file.properties["worldSaveData"]["value"]["GroupSaveDataMap"]["value"]: | |
if group["value"]["GroupType"]["value"]["value"] == "EPalGroupType::Guild": | |
group_name = group["value"]["RawData"]["value"]["guild_name"] | |
group_players = group["value"]["RawData"]["value"]["players"] | |
group_players = group["value"]["RawData"]["value"]["players"] | |
group_handle_ids = group["value"]["RawData"]["value"]["individual_character_handle_ids"] | |
if group_name == "Unnamed Guild" and len(group_players) <= 1: | |
continue | |
print(f"Analyzing Guild: {group_name} ({len(group_handle_ids)})") | |
for player in group_players: | |
player_uid = player["player_uid"] | |
player_name = player["player_info"]["player_name"] | |
all_players_in_guild[player_uid] = player | |
print(f" {player_name}: {player_uid}") | |
all_instances = gvas_file.properties["worldSaveData"]["value"]["CharacterSaveParameterMap"]["value"] | |
all_instances_uid = set() | |
print("Total instances of players/pals/etc: ", len(all_instances)) | |
for player in all_instances: | |
instance_uid = player["key"]["InstanceId"]["value"] | |
player_uid = player["key"]["PlayerUId"]["value"] | |
all_instances_uid.add(instance_uid) | |
if player_uid == "00000000-0000-0000-0000-000000000000": | |
continue | |
exist_players_uid.add(player_uid) | |
missing_players_uid = set(all_players_in_guild.keys()) - exist_players_uid | |
if missing_players_uid: | |
print("Missing players:") | |
for uid in missing_players_uid: | |
player = all_players_in_guild[uid] | |
player_name = player["player_info"]["player_name"] | |
print(f" {player_name}: {uid}") | |
else: | |
print("No missing players") | |
return gvas_file, all_players_in_guild, missing_players_uid, all_instances_uid | |
def fix_missing(filename, backup, output_path): | |
if os.path.exists(output_path): | |
print(f"{output_path} already exists, this will overwrite the file") | |
if not confirm_prompt("Are you sure you want to continue?"): | |
exit(1) | |
fixed_players = {} | |
broken_gvas, all_players_in_guild, missing_players_uid, __ = analyze_gvas(filename) | |
if not missing_players_uid: | |
print("No missing players, nothing to fix") | |
exit(1) | |
print(f"Fixing {filename} to {output_path} from backup {backup}") | |
with open(backup, "rb") as f: | |
data = f.read() | |
raw_gvas, _ = decompress_sav_to_gvas(data) | |
backup_gvas = GvasFile.read(raw_gvas, PALWORLD_TYPE_HINTS, PALWORLD_CUSTOM_PROPERTIES) | |
backup_players = backup_gvas.properties["worldSaveData"]["value"]["CharacterSaveParameterMap"]["value"] | |
for player in backup_players: | |
player_uid = player["key"]["PlayerUId"]["value"] | |
if player_uid in missing_players_uid: | |
broken_gvas.properties["worldSaveData"]["value"]["CharacterSaveParameterMap"]["value"].append(player) | |
fixed_players[player_uid] = player | |
if fixed_players: | |
print("Fixed players:") | |
for uid, player in fixed_players.items(): | |
player_name = all_players_in_guild[uid]["player_info"]["player_name"] | |
print(f" {player_name}: {uid}") | |
print("Generating SAV file") | |
if ( | |
"Pal.PalWorldSaveGame" in broken_gvas.header.save_game_class_name | |
or "Pal.PalLocalWorldSaveGame" in broken_gvas.header.save_game_class_name | |
): | |
save_type = 0x32 | |
else: | |
save_type = 0x31 | |
sav_file = compress_gvas_to_sav( | |
broken_gvas.write(PALWORLD_CUSTOM_PROPERTIES), save_type | |
) | |
print(f"Writing SAV file to {output_path}") | |
with open(output_path, "wb") as f: | |
f.write(sav_file) | |
def fix_capture(filename, output_path): | |
if os.path.exists(output_path): | |
print(f"{output_path} already exists, this will overwrite the file") | |
if not confirm_prompt("Are you sure you want to continue?"): | |
exit(1) | |
(broken_gvas, __, __, all_instances_uid) = analyze_gvas(filename) | |
print(f"all_instances_uid: {len(all_instances_uid)}") | |
for (idx, group) in enumerate(broken_gvas.properties["worldSaveData"]["value"]["GroupSaveDataMap"]["value"]): | |
if group["value"]["GroupType"]["value"]["value"] == "EPalGroupType::Guild": | |
group_name = group["value"]["RawData"]["value"]["guild_name"] | |
group_handle_ids = group["value"]["RawData"]["value"]["individual_character_handle_ids"] | |
temp_instances = [] | |
for instance in group["value"]["RawData"]["value"]["individual_character_handle_ids"]: | |
instance_uid = instance['instance_id'] | |
if instance_uid in all_instances_uid: | |
temp_instances.append(instance) | |
broken_gvas.properties["worldSaveData"]["value"]["GroupSaveDataMap"]["value"][idx]\ | |
["value"]["RawData"]["value"]["individual_character_handle_ids"] = temp_instances | |
print(f"Fixed capture logs for Guild {group_name}: {len(group_handle_ids)} -> {len(temp_instances)}") | |
print("Generating SAV file") | |
if ( | |
"Pal.PalWorldSaveGame" in broken_gvas.header.save_game_class_name | |
or "Pal.PalLocalWorldSaveGame" in broken_gvas.header.save_game_class_name | |
): | |
save_type = 0x32 | |
else: | |
save_type = 0x31 | |
sav_file = compress_gvas_to_sav( | |
broken_gvas.write(PALWORLD_CUSTOM_PROPERTIES), save_type | |
) | |
print(f"Writing SAV file to {output_path}") | |
with open(output_path, "wb") as f: | |
f.write(sav_file) | |
def confirm_prompt(question: str) -> bool: | |
reply = None | |
while reply not in ("y", "n"): | |
reply = input(f"{question} (y/n): ").casefold() | |
return reply == "y" | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks bro. You really quick to answer my question I am very appreciated.