Files
Campus-25/server/ConvertBundles2021.py
T
2026-10-14 06:59:51 +02:00

105 lines
4.1 KiB
Python

"""
Re-stamp every Rec Room CDN asset bundle for the Unity 2021.3.41f1 client.
python ConvertBundles2021.py [SRC] [DST] default D:\AssetBundles -> D:\ConvertedAssetBundles
What it does to each .assetbundle:
- sets the engine version to 2021.3.41f1 on the UnityFS header and on every serialized
file inside (header format 22 and UnityFS v8 already match what the client builds)
- keeps the embedded type trees, so the player deserialises by field name
- re-saves LZ4, then re-opens the result and checks every object still reads
Nothing else is touched. The folder layout is mirrored, so DST/<a>/<b>/(Item)/<bodytype>/x.assetbundle.
Output next to the bundles:
DST\hashes.json { "<file name>.assetbundle": "<base64 sha256 of the converted file>" }
- the UnityAssetHash / UnityAsset2Hash the catalog must carry for it
DST\convert.log one line per file; "FAIL" lines are the ones to look at
Already-converted files (present in DST with the right size) are skipped, so a rerun resumes.
"""
import os, sys, json, hashlib, base64, time
from concurrent.futures import ProcessPoolExecutor, as_completed
TARGET = "2021.3.41f1"
SRC = sys.argv[1] if len(sys.argv) > 1 else r"D:\AssetBundles"
DST = sys.argv[2] if len(sys.argv) > 2 else r"D:\ConvertedAssetBundles"
def convert_one(rel):
import UnityPy
src = os.path.join(SRC, rel)
dst = os.path.join(DST, rel)
try:
env = UnityPy.load(src)
bundles = [f for f in env.files.values() if type(f).__name__ == "BundleFile"]
if not bundles:
return rel, None, "not a UnityFS bundle"
bf = bundles[0]
before = bf.version_engine
bf.version = 8
bf.version_engine = TARGET
n_sf = 0
for f in bf.files.values():
if type(f).__name__ == "SerializedFile":
f.unity_version = TARGET
n_sf += 1
data = bf.save(packer="lz4")
# Verify: re-open and read every object through its type tree.
env2 = UnityPy.load(data)
bad = 0
for o in env2.objects:
try:
o.read_typetree()
except Exception:
bad += 1
if bad:
return rel, None, f"{bad} object(s) unreadable after convert"
os.makedirs(os.path.dirname(dst), exist_ok=True)
with open(dst, "wb") as fh:
fh.write(data)
h = base64.b64encode(hashlib.sha256(data).digest()).decode()
return rel, h, f"ok {before} -> {TARGET} ({n_sf} file(s), {len(data)} B)"
except Exception as ex:
return rel, None, f"EXC {type(ex).__name__}: {ex}"
def main():
todo = []
for dp, _, fn in os.walk(SRC):
for f in fn:
if f.lower().endswith(".assetbundle"):
rel = os.path.relpath(os.path.join(dp, f), SRC)
out = os.path.join(DST, rel)
if os.path.exists(out) and os.path.getsize(out) > 0:
continue
todo.append(rel)
os.makedirs(DST, exist_ok=True)
hashes_path = os.path.join(DST, "hashes.json")
hashes = json.load(open(hashes_path)) if os.path.exists(hashes_path) else {}
print(f"{len(todo)} bundle(s) to convert ({len(hashes)} already hashed)")
ok = fail = 0
t0 = time.time()
with open(os.path.join(DST, "convert.log"), "a", encoding="utf-8") as log, \
ProcessPoolExecutor(max_workers=max(2, os.cpu_count() - 1)) as pool:
futs = [pool.submit(convert_one, rel) for rel in todo]
for i, fut in enumerate(as_completed(futs), 1):
rel, h, msg = fut.result()
if h:
ok += 1
hashes[os.path.basename(rel)] = h
else:
fail += 1
msg = "FAIL " + msg
log.write(f"{rel}\t{msg}\n")
if i % 500 == 0 or i == len(futs):
json.dump(hashes, open(hashes_path, "w"), indent=0)
print(f" {i}/{len(futs)} ok={ok} fail={fail} {time.time()-t0:.0f}s")
json.dump(hashes, open(hashes_path, "w"), indent=0)
print(f"done: {ok} converted, {fail} failed, hashes for {len(hashes)} file(s) in {hashes_path}")
if __name__ == "__main__":
main()