CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-unitypy

A Unity asset extractor for Python based on AssetStudio that supports extraction, editing, and manipulation of Unity game assets.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

binary-data.mddocs/

Binary Data Handling

Low-level binary data reading and writing with endianness support, enabling direct manipulation of Unity's binary asset formats and custom data structures.

Capabilities

Binary Reader

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
        """

Binary Writer

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
        """

Usage Examples

Reading Binary Asset 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']}")

Creating Binary Data

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)

Parsing Custom Unity Data Structures

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}")

Endianness Handling

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}")

Working with Alignment

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}")

Custom Data Format Implementation

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

docs

asset-classes.md

asset-export.md

asset-loading.md

binary-data.md

enumerations.md

file-formats.md

index.md

tile.json