This repository has been archived on 2025-04-28. You can view files and clone it, but cannot push or open issues or pull requests.
Files
Blue-Archive---Asset-Downlo…/AssetBatchConverter.py
2021-11-08 10:47:15 +01:00

164 lines
4.8 KiB
Python

import os
import UnityPy
from collections import Counter
import json
TYPES = [
# Images
'Sprite',
'Texture2D',
# Text (filish)
'TextAsset',
'Shader',
'MonoBehaviour',
'Mesh'
# Font
'Font',
# Audio
'AudioClip',
]
ROOT = os.path.dirname(os.path.realpath(__file__))
DST = os.path.join(ROOT, "extracted")
IGNOR_DIR_COUNT=0
def extract_assets(src):
# load source
env = UnityPy.load(src)
# iterate over assets
for asset in env.assets:
# assets without container / internal path will be ignored for now
if not asset.container:
continue
# filter objects and put Texture2Ds at the end of the list
objs = sorted(
(
obj
for obj in asset.get_objects()
if obj.type.name in TYPES
),
key=lambda x: 1 if x.type == "Texture2D" else 0
)
cobjs = sorted(
(
(key, obj)
for key, obj in asset.container.items()
if obj.type.name in TYPES
),
key=lambda x: 1 if x[1].type == "Texture2D" else 0
)
# check which mode we will have to use
num_cont = len(cobjs)
num_objs = len(objs)
# check if container contains all important assets, if yes, just ignore the container
if num_objs <= num_cont * 2:
for asset_path, obj in cobjs:
fp = os.path.join(DST, *asset_path.split('/')
[IGNOR_DIR_COUNT:])
export_obj(obj, fp)
# otherwise use the container to generate a path for the normal objects
else:
extracted = []
# find the most common path
occurence_count = Counter(os.path.splitext(asset_path)[
0] for asset_path in asset.container.keys())
local_path = os.path.join(
DST, *occurence_count.most_common(1)[0][0].split('/')[IGNOR_DIR_COUNT:])
for obj in objs:
if obj.path_id not in extracted:
try:
extracted.extend(export_obj(
obj, local_path, append_name=True))
except Exception as e:
print(e, obj.path_id)
def export_obj(obj, fp: str, append_name: bool = False) -> list:
if obj.type not in TYPES:
return []
data = obj.read()
if append_name:
fp = os.path.join(fp, data.name)
fp, extension = os.path.splitext(fp)
os.makedirs(os.path.dirname(fp), exist_ok=True)
# streamlineable types
export = None
if obj.type == 'TextAsset':
if not extension:
extension = '.txt'
export = data.script
elif obj.type == "Font":
if data.m_FontData:
extension = ".ttf"
if data.m_FontData[0:4] == b"OTTO":
extension = ".otf"
export = data.m_FontData
else:
return [obj.path_id]
elif obj.type == "Mesh":
extension = ".obf"
export = data.export().encode("utf8")
elif obj.type == "Shader":
extension = ".txt"
export = data.export().encode("utf8")
elif obj.type == "MonoBehaviour":
# The data structure of MonoBehaviours is custom
# and is stored as nodes
# If this structure doesn't exist,
# it might still help to at least save the binary data,
# which can then be inspected in detail.
if obj.serialized_type.nodes:
extension = ".json"
export = json.dumps(
obj.read_typetree(),
indent=4,
ensure_ascii=False
).encode("utf8")
else:
extension = ".bin"
export = data.raw_data
if export:
with open(f"{fp}{extension}", "wb") as f:
f.write(export)
# non-streamlineable types
if obj.type == "Sprite":
data.image.save(f"{fp}.png")
return [obj.path_id, data.m_RD.texture.path_id, getattr(data.m_RD.alphaTexture, 'path_id', None)]
elif obj.type == "Texture2D":
if not os.path.exists(fp) and data.m_Width:
# textures can have size 0.....
data.image.save(f"{fp}.png")
elif obj.type == "AudioClip":
samples = data.samples
if len(samples) == 0:
pass
elif len(samples) == 1:
with open(f"{fp}.wav", "wb") as f:
f.write(list(data.samples.values())[0])
else:
os.makedirs(fp, exist_ok=True)
for name, clip_data in samples.items():
with open(os.path.join(fp, f"{name}.wav"), "wb") as f:
f.write(clip_data)
return [obj.path_id]
if __name__ == '__main__':
main()