Repository navigation
Expand file tree
/
Copy pathcheck-progress.py
More file actions
executable file
·84 lines (69 loc) · 3.36 KB
/
Copy pathcheck-progress.py
File metadata and controls
executable file
·84 lines (69 loc) · 3.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env python3
"""Prove a migrated Players/*.sav still carries the per-player progress records.
Exists because "my map is black" reads exactly like "my progress is gone", and
it is not: everything below lives in the player .sav that import-world.sh
installs. The map MASK lives in the client-side LocalData.sav, which is a
separate, client-side restore -- so this answers "did my waypoints, bosses and
quests survive?" without needing to log in and squint at a fogged map.
Usage: check-progress.py <Players/*.sav> [...]
Read-only. Exits non-zero only if a save is empty across EVERY record, which is
what a fresh level-1 character looks like. Deliberately no per-record minimum:
a player who never cleared a tower legitimately has 0 tower bosses.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "tools"))
from palsav.core import decompress_sav_to_gvas
from palsav.gvas import GvasFile
from palsav.paltypes import PALWORLD_CUSTOM_PROPERTIES, PALWORLD_TYPE_HINTS
# label, key, key lives in RecordData (else top-level SaveData)
COUNTS = [
("fast travel points", "FastTravelPointUnlockFlag", True),
("tower bosses", "TowerBossDefeatFlag", True),
("field bosses", "NormalBossDefeatFlag", True),
("paldeck entries", "PaldeckUnlockFlag", True),
("map areas found", "FindAreaFlagMap", True),
("dungeons cleared", "NormalDungeonClearCount", True),
("relics found", "RelicObtainForInstanceFlag", True),
("completed quests", "CompletedQuestArray_FullRelease", False),
]
def count(entries) -> int:
"""Records come in three shapes: flag map, array, or a plain scalar count."""
if isinstance(entries, list): # MapProperty: [{key, value}]
return sum(1 for e in entries if e.get("value") is not False)
if isinstance(entries, dict) and "values" in entries: # ArrayProperty
return len(entries["values"])
if isinstance(entries, int): # IntProperty, e.g. dungeon clears
return entries
return 0
def main() -> None:
paths = sys.argv[1:]
if not paths:
raise SystemExit(f"usage: {sys.argv[0]} <Players/*.sav> [...]")
failures = []
for path in paths:
gvas_bytes, _ = decompress_sav_to_gvas(open(path, "rb").read())
sd = GvasFile.read(gvas_bytes, PALWORLD_TYPE_HINTS, PALWORLD_CUSTOM_PROPERTIES)
sd = sd.properties["SaveData"]["value"]
rd = sd["RecordData"]["value"]
print(f"\n{os.path.basename(path)} uid {sd['PlayerUId']['value']}")
total = 0
for label, key, in_record in COUNTS:
src = rd if in_record else sd
n = count(src[key]["value"]) if key in src else 0
total += n
print(f" {label:<20} {n}")
# Deliberately NOT per-record thresholds: a casual player legitimately
# has 0 tower bosses. An all-zero save is the only unambiguous failure --
# that is what a fresh level-1 character looks like, i.e. progress lost.
if total == 0:
failures.append(f"{os.path.basename(path)}: every progress record empty")
if failures:
print("\nPROGRESS MISSING:")
for f in failures:
print(f" {f}")
raise SystemExit(1)
print("\nOK: every save carries waypoints, bosses and completed quests.")
print(" A black map is the client-side LocalData.sav, not this.")
if __name__ == "__main__":
main()