tools: SAND .wbt + game-data extraction scripts
Python tooling for decoding walker saves and mining game data: sand.py / build_wbt.py / walker_hashes.py / harvest_hashes.py (.wbt codec + hashes), extract_*/loot_probe/odin_read/unitybundle (asset parsing), make_*_wiki + render_wiki (wiki generation), recover_key. Paths point at the local extracted/, wiki/, and Walkers symlink.
This commit is contained in:
101
extract_i2.py
Normal file
101
extract_i2.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract the I2 Localization term table (English) from SAND's I2Languages asset.
|
||||
|
||||
The typetree read trips on an I2 alignment quirk, but each TermData is
|
||||
self-describing, so we parse the MonoBehaviour body directly. Layout (Unity
|
||||
serialization, little-endian, 4-byte aligned strings/arrays), from dump.cs:
|
||||
|
||||
engine header: m_GameObject(12) m_Enabled(1,align4) m_Script(12) m_Name(string)
|
||||
LanguageSourceData mSource:
|
||||
UInt8 x3 flags (align4)
|
||||
TermData[] mTerms: int count, then per term:
|
||||
string Term
|
||||
int32 TermType
|
||||
string Description
|
||||
string[] Languages (the translations, one per language)
|
||||
byte[] Flags
|
||||
string[] Languages_Touch
|
||||
UInt8 CaseInsensitiveTerms (align4)
|
||||
int32 OnMissingTranslation
|
||||
string mTerm_AppName
|
||||
LanguageData[] mLanguages: int count, then per language:
|
||||
string Name; string Code; byte Flags; byte Compressed (align4)
|
||||
"""
|
||||
import os, struct, json
|
||||
|
||||
GAME = "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest"
|
||||
DU = os.path.join(GAME, "Sand_Data/data.unity3d")
|
||||
OUT = "/home/downloadpizza/sand_tools/i2_terms_en.json"
|
||||
|
||||
class R:
|
||||
def __init__(self, b, off=0): self.b=b; self.o=off
|
||||
def align(self):
|
||||
self.o = (self.o + 3) & ~3
|
||||
def u8(self):
|
||||
v=self.b[self.o]; self.o+=1; return v
|
||||
def i32(self):
|
||||
v=struct.unpack_from('<i',self.b,self.o)[0]; self.o+=4; return v
|
||||
def i64(self):
|
||||
v=struct.unpack_from('<q',self.b,self.o)[0]; self.o+=8; return v
|
||||
def s(self):
|
||||
n=self.i32()
|
||||
if n<0 or n>10_000_000: raise ValueError(f"bad str len {n} @ {self.o}")
|
||||
v=self.b[self.o:self.o+n].decode('utf-8','replace'); self.o+=n; self.align(); return v
|
||||
def bytes(self):
|
||||
n=self.i32(); v=self.b[self.o:self.o+n]; self.o+=n; self.align(); return v
|
||||
def str_array(self):
|
||||
n=self.i32(); return [self.s() for _ in range(n)]
|
||||
|
||||
def parse(raw):
|
||||
r=R(raw)
|
||||
# engine header
|
||||
r.i32(); r.i64() # m_GameObject
|
||||
r.u8(); r.align() # m_Enabled
|
||||
r.i32(); r.i64() # m_Script
|
||||
name=r.s() # m_Name
|
||||
# mSource — each bool is individually 4-byte aligned
|
||||
r.u8(); r.align(); r.u8(); r.align(); r.u8(); r.align()
|
||||
nterms=r.i32()
|
||||
terms=[]
|
||||
for _ in range(nterms):
|
||||
term=r.s()
|
||||
desc=r.s() # TermType is not serialized; Description (usually empty)
|
||||
langs=r.str_array() # the translations, one per language
|
||||
flags=r.bytes() # one flag byte per language
|
||||
touch=r.str_array() # Languages_Touch
|
||||
terms.append((term, langs))
|
||||
languages=[]
|
||||
try: # trailing block is best-effort (English is index 0 regardless)
|
||||
r.u8(); r.align() # CaseInsensitiveTerms
|
||||
r.i32() # OnMissingTranslation
|
||||
r.s() # mTerm_AppName
|
||||
nlang=r.i32()
|
||||
for _ in range(nlang):
|
||||
lname=r.s(); lcode=r.s(); r.u8(); r.align(); r.u8(); r.align()
|
||||
languages.append((lname,lcode))
|
||||
except Exception:
|
||||
pass
|
||||
return name, languages, terms
|
||||
|
||||
def main():
|
||||
import UnityPy
|
||||
env=UnityPy.load(DU)
|
||||
obj=next(o for o in env.objects if o.type.name=='MonoBehaviour'
|
||||
and len(o.get_raw_data())==3816792)
|
||||
name, languages, terms = parse(obj.get_raw_data())
|
||||
print("asset:",name,"| languages:",languages)
|
||||
eng=next((i for i,(n,c) in enumerate(languages)
|
||||
if c.lower().startswith('en') or n.lower().startswith('english')),0)
|
||||
print("english index:",eng,"| terms:",len(terms))
|
||||
table={t:(tr[eng] if eng<len(tr) else (tr[0] if tr else '')) for t,tr in terms}
|
||||
json.dump({'_source':'I2 LanguageSourceAsset (I2Languages), data.unity3d',
|
||||
'languages':languages,'english_index':eng,'count':len(table),
|
||||
'terms':table}, open(OUT,'w'), indent=2, ensure_ascii=False)
|
||||
from collections import Counter
|
||||
ns=Counter(k.split('/')[0] for k in table)
|
||||
print("\ntop term-key prefixes:")
|
||||
for k,c in ns.most_common(30): print(f" {c:4d} {k}")
|
||||
print("\nwrote",OUT)
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user