A Unity asset extractor for Python based on AssetStudio that supports extraction, editing, and manipulation of Unity game assets.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Low-level binary data reading and writing with endianness support, enabling direct manipulation of Unity's binary asset formats and custom data structures.
Endian-aware binary data reader for parsing Unity's binary formats.
class EndianBinaryReader:
"""
Endian-aware binary data reader for Unity asset files.
"""
def __init__(self, data, endian="<"):
"""
Initialize binary reader.
Parameters:
- data: bytes or file-like object to read from
- endian: Byte order ("<" for little-endian, ">" for big-endian)
"""
@property
def endian(self):
"""
Get current endianness.
Returns:
str: Endian string ("<" or ">")
"""
@endian.setter
def endian(self, value):
"""
Set endianness.
Parameters:
- value: Endian string ("<" or ">")
"""
def read_byte(self):
"""
Read a single byte.
Returns:
int: Byte value (0-255)
"""
def read_sbyte(self):
"""
Read a signed byte.
Returns:
int: Signed byte value (-128 to 127)
"""
def read_boolean(self):
"""
Read a boolean value.
Returns:
bool: Boolean value (True if non-zero)
"""
def read_int16(self):
"""
Read a 16-bit signed integer.
Returns:
int: 16-bit signed integer
"""
def read_uint16(self):
"""
Read a 16-bit unsigned integer.
Returns:
int: 16-bit unsigned integer
"""
def read_int32(self):
"""
Read a 32-bit signed integer.
Returns:
int: 32-bit signed integer
"""
def read_uint32(self):
"""
Read a 32-bit unsigned integer.
Returns:
int: 32-bit unsigned integer
"""
def read_int64(self):
"""
Read a 64-bit signed integer.
Returns:
int: 64-bit signed integer
"""
def read_uint64(self):
"""
Read a 64-bit unsigned integer.
Returns:
int: 64-bit unsigned integer
"""
def read_float(self):
"""
Read a 32-bit float.
Returns:
float: 32-bit floating point value
"""
def read_double(self):
"""
Read a 64-bit double.
Returns:
float: 64-bit floating point value
"""
def read_string(self, length=None, encoding="utf-8"):
"""
Read a string with length prefix or fixed length.
Parameters:
- length: Optional fixed length, otherwise reads length prefix
- encoding: Text encoding (default "utf-8")
Returns:
str: Decoded string
"""
def read_cstring(self, encoding="utf-8"):
"""
Read a null-terminated C-style string.
Parameters:
- encoding: Text encoding (default "utf-8")
Returns:
str: Decoded string
"""
def read_bytes(self, count):
"""
Read a specific number of bytes.
Parameters:
- count: Number of bytes to read
Returns:
bytes: Raw byte data
"""
def align_stream(self, alignment=4):
"""
Align stream position to boundary.
Parameters:
- alignment: Alignment boundary in bytes
"""
@property
def position(self):
"""
Get current stream position.
Returns:
int: Current position in bytes
"""
@position.setter
def position(self, value):
"""
Set stream position.
Parameters:
- value: New position in bytes
"""
def seek(self, position):
"""
Seek to absolute position.
Parameters:
- position: Absolute position in bytes
"""
def tell(self):
"""
Get current position.
Returns:
int: Current position in bytes
"""Endian-aware binary data writer for creating Unity-compatible binary formats.
class EndianBinaryWriter:
"""
Endian-aware binary data writer for Unity asset files.
"""
def __init__(self, endian="<"):
"""
Initialize binary writer.
Parameters:
- endian: Byte order ("<" for little-endian, ">" for big-endian)
"""
@property
def endian(self):
"""
Get current endianness.
Returns:
str: Endian string ("<" or ">")
"""
@endian.setter
def endian(self, value):
"""
Set endianness.
Parameters:
- value: Endian string ("<" or ">")
"""
def write_byte(self, value):
"""
Write a single byte.
Parameters:
- value: Byte value (0-255)
"""
def write_sbyte(self, value):
"""
Write a signed byte.
Parameters:
- value: Signed byte value (-128 to 127)
"""
def write_boolean(self, value):
"""
Write a boolean value.
Parameters:
- value: Boolean value
"""
def write_int16(self, value):
"""
Write a 16-bit signed integer.
Parameters:
- value: 16-bit signed integer
"""
def write_uint16(self, value):
"""
Write a 16-bit unsigned integer.
Parameters:
- value: 16-bit unsigned integer
"""
def write_int32(self, value):
"""
Write a 32-bit signed integer.
Parameters:
- value: 32-bit signed integer
"""
def write_uint32(self, value):
"""
Write a 32-bit unsigned integer.
Parameters:
- value: 32-bit unsigned integer
"""
def write_int64(self, value):
"""
Write a 64-bit signed integer.
Parameters:
- value: 64-bit signed integer
"""
def write_uint64(self, value):
"""
Write a 64-bit unsigned integer.
Parameters:
- value: 64-bit unsigned integer
"""
def write_float(self, value):
"""
Write a 32-bit float.
Parameters:
- value: 32-bit floating point value
"""
def write_double(self, value):
"""
Write a 64-bit double.
Parameters:
- value: 64-bit floating point value
"""
def write_string(self, value, encoding="utf-8"):
"""
Write a string with length prefix.
Parameters:
- value: String to write
- encoding: Text encoding (default "utf-8")
"""
def write_cstring(self, value, encoding="utf-8"):
"""
Write a null-terminated C-style string.
Parameters:
- value: String to write
- encoding: Text encoding (default "utf-8")
"""
def write_bytes(self, data):
"""
Write raw byte data.
Parameters:
- data: bytes or bytearray to write
"""
def align_stream(self, alignment=4):
"""
Pad stream to alignment boundary.
Parameters:
- alignment: Alignment boundary in bytes
"""
@property
def position(self):
"""
Get current stream position.
Returns:
int: Current position in bytes
"""
def to_bytes(self):
"""
Get written data as bytes.
Returns:
bytes: All written data
"""from UnityPy.streams import EndianBinaryReader
import UnityPy
# Read binary data from a Unity asset file
env = UnityPy.load("binary_asset.dat")
for obj in env.objects:
if obj.type.name == "TextAsset":
text_asset = obj.read()
# If the text asset contains binary data
if hasattr(text_asset, 'm_Script') and text_asset.m_Script:
# Create reader from binary data
reader = EndianBinaryReader(text_asset.m_Script, endian="<")
# Read structured data
magic = reader.read_uint32()
version = reader.read_uint16()
count = reader.read_uint32()
print(f"Magic: 0x{magic:08X}")
print(f"Version: {version}")
print(f"Count: {count}")
# Read array of structures
entries = []
for i in range(count):
entry = {
'id': reader.read_uint32(),
'name': reader.read_string(),
'value': reader.read_float(),
'active': reader.read_boolean()
}
entries.append(entry)
# Align to 4-byte boundary
reader.align_stream(4)
print(f"Read {len(entries)} entries")
for entry in entries[:5]: # Show first 5
print(f" ID: {entry['id']}, Name: {entry['name']}, Value: {entry['value']}")from UnityPy.streams import EndianBinaryWriter
# Create binary data structure
writer = EndianBinaryWriter(endian="<")
# Write header
writer.write_uint32(0x12345678) # Magic number
writer.write_uint16(1) # Version
writer.write_uint32(3) # Entry count
# Write entries
entries = [
{"id": 1, "name": "First", "value": 1.5, "active": True},
{"id": 2, "name": "Second", "value": 2.5, "active": False},
{"id": 3, "name": "Third", "value": 3.5, "active": True}
]
for entry in entries:
writer.write_uint32(entry["id"])
writer.write_string(entry["name"])
writer.write_float(entry["value"])
writer.write_boolean(entry["active"])
writer.align_stream(4)
# Get the binary data
binary_data = writer.to_bytes()
print(f"Created {len(binary_data)} bytes of binary data")
# Write to file
with open("custom_data.bin", "wb") as f:
f.write(binary_data)from UnityPy.streams import EndianBinaryReader
import UnityPy
def parse_custom_mesh_data(data):
"""Parse a custom mesh format."""
reader = EndianBinaryReader(data, endian="<")
# Read header
signature = reader.read_bytes(4)
if signature != b"MESH":
raise ValueError("Invalid mesh signature")
version = reader.read_uint32()
vertex_count = reader.read_uint32()
triangle_count = reader.read_uint32()
print(f"Mesh version: {version}")
print(f"Vertices: {vertex_count}")
print(f"Triangles: {triangle_count}")
# Read vertices
vertices = []
for i in range(vertex_count):
x = reader.read_float()
y = reader.read_float()
z = reader.read_float()
vertices.append((x, y, z))
# Read triangles
triangles = []
for i in range(triangle_count):
a = reader.read_uint16()
b = reader.read_uint16()
c = reader.read_uint16()
triangles.append((a, b, c))
reader.align_stream(4) # Align to 4 bytes
return {
'version': version,
'vertices': vertices,
'triangles': triangles
}
# Usage with Unity asset
env = UnityPy.load("custom_mesh_assets/")
for obj in env.objects:
if obj.type.name == "TextAsset":
text_asset = obj.read()
if text_asset.name.endswith("_mesh"):
try:
mesh_data = parse_custom_mesh_data(text_asset.m_Script)
print(f"Parsed mesh: {text_asset.name}")
print(f" Vertices: {len(mesh_data['vertices'])}")
print(f" Triangles: {len(mesh_data['triangles'])}")
except Exception as e:
print(f"Failed to parse {text_asset.name}: {e}")from UnityPy.streams import EndianBinaryReader, EndianBinaryWriter
# Create data with different endianness
def create_test_data(endian):
writer = EndianBinaryWriter(endian=endian)
writer.write_uint32(0x12345678)
writer.write_float(3.14159)
writer.write_string("Test String")
return writer.to_bytes()
# Create little-endian and big-endian data
little_endian_data = create_test_data("<")
big_endian_data = create_test_data(">")
print(f"Little-endian data: {little_endian_data.hex()}")
print(f"Big-endian data: {big_endian_data.hex()}")
# Read with appropriate endianness
def read_test_data(data, endian):
reader = EndianBinaryReader(data, endian=endian)
magic = reader.read_uint32()
pi = reader.read_float()
text = reader.read_string()
return magic, pi, text
# Read little-endian data
magic, pi, text = read_test_data(little_endian_data, "<")
print(f"Little-endian: Magic=0x{magic:08X}, Pi={pi:.5f}, Text='{text}'")
# Read big-endian data
magic, pi, text = read_test_data(big_endian_data, ">")
print(f"Big-endian: Magic=0x{magic:08X}, Pi={pi:.5f}, Text='{text}'")
# Demonstrate endian switching
reader = EndianBinaryReader(little_endian_data, endian="<")
magic1 = reader.read_uint32()
reader.endian = ">" # Switch to big-endian
magic2 = reader.read_uint32() # This will read incorrectly
print(f"Same data, different endian: 0x{magic1:08X} vs 0x{magic2:08X}")from UnityPy.streams import EndianBinaryReader, EndianBinaryWriter
# Create data with alignment requirements
writer = EndianBinaryWriter()
# Write header (8 bytes)
writer.write_uint32(0xDEADBEEF)
writer.write_uint32(42)
# Write variable-length string
writer.write_string("Variable length string")
# Align to 16-byte boundary before next structure
writer.align_stream(16)
start_pos = writer.position
# Write aligned structure
writer.write_float(1.0)
writer.write_float(2.0)
writer.write_float(3.0)
writer.write_float(4.0) # 16 bytes total
data = writer.to_bytes()
print(f"Total size: {len(data)} bytes")
print(f"Aligned structure starts at: {start_pos}")
# Read back with alignment
reader = EndianBinaryReader(data)
# Read header
magic = reader.read_uint32()
value = reader.read_uint32()
text = reader.read_string()
print(f"Magic: 0x{magic:08X}")
print(f"Value: {value}")
print(f"Text: '{text}'")
print(f"Position after string: {reader.position}")
# Align to same boundary
reader.align_stream(16)
print(f"Position after alignment: {reader.position}")
# Read aligned structure
floats = [reader.read_float() for _ in range(4)]
print(f"Float values: {floats}")from UnityPy.streams import EndianBinaryReader, EndianBinaryWriter
import struct
class CustomDataFormat:
"""Example custom binary data format handler."""
def __init__(self):
self.magic = 0x43555354 # "CUST"
self.version = 1
self.entries = []
def add_entry(self, name, data_type, value):
"""Add a data entry."""
self.entries.append({
'name': name,
'type': data_type,
'value': value
})
def serialize(self):
"""Serialize to binary format."""
writer = EndianBinaryWriter(endian="<")
# Write header
writer.write_uint32(self.magic)
writer.write_uint16(self.version)
writer.write_uint16(len(self.entries))
# Write entries
for entry in self.entries:
writer.write_string(entry['name'])
writer.write_byte(entry['type'])
if entry['type'] == 0: # Integer
writer.write_int32(entry['value'])
elif entry['type'] == 1: # Float
writer.write_float(entry['value'])
elif entry['type'] == 2: # String
writer.write_string(entry['value'])
elif entry['type'] == 3: # Boolean
writer.write_boolean(entry['value'])
writer.align_stream(4)
return writer.to_bytes()
@classmethod
def deserialize(cls, data):
"""Deserialize from binary format."""
reader = EndianBinaryReader(data, endian="<")
# Read header
magic = reader.read_uint32()
if magic != 0x43555354:
raise ValueError(f"Invalid magic: 0x{magic:08X}")
version = reader.read_uint16()
count = reader.read_uint16()
# Create instance
instance = cls()
instance.version = version
# Read entries
for _ in range(count):
name = reader.read_string()
data_type = reader.read_byte()
if data_type == 0: # Integer
value = reader.read_int32()
elif data_type == 1: # Float
value = reader.read_float()
elif data_type == 2: # String
value = reader.read_string()
elif data_type == 3: # Boolean
value = reader.read_boolean()
else:
raise ValueError(f"Unknown data type: {data_type}")
instance.add_entry(name, data_type, value)
reader.align_stream(4)
return instance
# Usage example
custom_data = CustomDataFormat()
custom_data.add_entry("player_level", 0, 42)
custom_data.add_entry("player_health", 1, 75.5)
custom_data.add_entry("player_name", 2, "Hero")
custom_data.add_entry("is_alive", 3, True)
# Serialize
binary_data = custom_data.serialize()
print(f"Serialized {len(binary_data)} bytes")
# Deserialize
loaded_data = CustomDataFormat.deserialize(binary_data)
print(f"Loaded {len(loaded_data.entries)} entries")
for entry in loaded_data.entries:
print(f" {entry['name']}: {entry['value']} (type {entry['type']})")Install with Tessl CLI
npx tessl i tessl/pypi-unitypy