add migration notification, dev scan_props tool

This commit is contained in:
2026-06-03 01:32:30 +02:00
parent 85b900dda7
commit 995344b153
2 changed files with 42 additions and 59 deletions
+3
View File
@@ -36,6 +36,9 @@ All notable changes to NotAlterra are documented in this file.
current user's default save locations (`%LOCALAPPDATA%` on Windows,
Proton + XDG data on Linux). No scanning of other profiles or system
drives. If nothing is found, use **Set save folder** as before.
- **Migration notification** — old backups in `NotAlterra_Backups/` are
migrated silently on first launch with a log entry. The user is informed
their original data remains untouched.
### Removed
- **`config.ini` eliminated entirely** — no save path, disclaimer flag, or
+39 -59
View File
@@ -1,77 +1,57 @@
#!/usr/bin/env python3
"""Dump specific property values from GVAS files with debug."""
import struct, sys, os
"""Extract GVAS property values using correct offsets."""
import struct, sys
data = open(sys.argv[1], 'rb').read()
def read_u32_at(off):
def u32(off):
if off + 4 > len(data): return None
return struct.unpack_from('<I', data, off)[0]
def read_fname_at(off):
slen = read_u32_at(off)
def read_fname(off):
slen = u32(off)
if slen is None: return (None, off)
if slen == 0: return (None, off + 4)
s = data[off+4:off+4+slen-1].decode('utf-8', errors='replace')
return (s, off + 4 + slen)
targets = [
('SlotName', 'StrProperty'),
('DisplayName', 'StrProperty'),
('HostAddress', 'StrProperty'),
('HostPort', 'IntProperty'),
('bIsMultiplayerSave', 'BoolProperty'),
('bWasMultiplayerSave', 'BoolProperty'),
('bOnlineSave', 'BoolProperty'),
('bIsUserCreated', 'BoolProperty'),
('LevelName', 'StrProperty'),
('ModeSwitchCount', 'IntProperty'),
('PlayTime', 'DoubleProperty'),
]
print(f"── {os.path.basename(sys.argv[1])} ──")
for prop_name, type_name in targets:
def get(prop_name, type_name):
target = prop_name.encode('utf-8')
offset = 0
found_val = None
attempts = 0
while offset < len(data) - 20 and attempts < 500:
pos = data.find(target, offset)
if pos < 0: break
if pos < 4:
offset = pos + 1; attempts += 1; continue
# Try len(target) + 1 or len(target) (with or without null)
name_len = read_u32_at(pos - 4)
if name_len is None or (name_len != len(target) + 1 and name_len != len(target)):
offset = pos + 1; attempts += 1; continue
if data[pos + len(target)] != 0:
offset = pos + 1; attempts += 1; continue
after_name = pos + len(target) + 1
(next_name, next_offset) = read_fname_at(after_name)
if next_name != type_name:
offset = pos + 1; attempts += 1; continue
# Extract value
off = 0
while off + 20 < len(data):
pos = data.find(target, off)
if pos < 0: return None
if pos < 4: off = pos + 1; continue
nl = u32(pos - 4)
if nl is None or nl != len(target) + 1: off = pos + 1; continue
if data[pos + len(target)] != 0: off = pos + 1; continue
after = pos + len(target) + 1
(tn, no) = read_fname(after)
if tn != type_name: off = pos + 1; continue
val_off = no + 9
if type_name == 'BoolProperty':
voff = next_offset + 9
if voff < len(data):
found_val = 'yes' if data[voff] != 0 else 'no'
return 'yes' if val_off < len(data) and data[val_off] != 0 else 'no'
elif type_name == 'IntProperty':
voff = next_offset + 9
if voff + 4 <= len(data):
found_val = struct.unpack_from('<i', data, voff)[0]
if val_off + 4 <= len(data):
return str(struct.unpack_from('<i', data, val_off)[0])
elif type_name == 'StrProperty':
slen = read_u32_at(next_offset + 5)
if slen is not None and slen > 0:
found_val = data[next_offset+9:next_offset+9+slen-1].decode('utf-8', errors='replace')
slen = u32(val_off)
if slen and slen > 1:
return data[val_off+4:val_off+4+slen-1].decode('utf-8', errors='replace')
elif slen == 1:
found_val = '(empty)'
return '(empty)'
elif type_name == 'DoubleProperty':
voff = next_offset + 5
if voff + 8 <= len(data):
v = struct.unpack_from('<d', data, voff)[0]
found_val = f'{v:.0f}s ({v/3600:.1f}h)'
break
if found_val is not None:
print(f" {prop_name:24} {found_val}")
if val_off + 8 <= len(data):
v = struct.unpack_from('<d', data, val_off)[0]
return f'{v:.0f}s'
return ''
return None
# Only what was asked for
for name, typ in [('HostAddress', 'StrProperty'), ('HostPort', 'IntProperty'),
('bIsUserCreated', 'BoolProperty')]:
v = get(name, typ)
if v is not None:
print(f" {name:24} {v}")
else:
print(f" {name:24} (not found)")