#!/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('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