mirror of
https://github.com/forkless/NotAlterra.git
synced 2026-08-20 01:53:35 +02:00
add migration notification, dev scan_props tool
This commit is contained in:
@@ -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
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user