Jump to content

Outfit7 starlite engine (pre-2023) 3d models (My Talking Tom 2/My Talking Angela 2 etc)


Recommended Posts

Posted

Nice!

I haven't stopped work completely, but I'm focusing on different mysteries. I did find some files in the asset bundle that describe colorsets. So like you can find strings like 

wardrobe-bottom-basicShorts-turquoise
wardrobe-bottom-basicShorts-lightGreen
wardrobe-bottom-basicShorts-gray
wardrobe-bottom-basicShorts-yellow
wardrobe-bottom-basicShorts-pink

and there are consecutive float values in the same file such as:

FAF9793F 8F8E0E3F FAF9793F 0000803F ➡ 0.9764706, 0.5568628, 0.9764706, 1
F1F0703F ADAC2C3F F1F0703F 0000803F ➡ 0.9411765, 0.6745098, 0.9411765, 1
BDBC3C3F D5D4D43E D4D3533F 0000803F ➡ 0.7372549, 0.41568628, 0.827451, 1

Those specific values are shades of pink, and my guess is that those are the base color, the highlights and the shadows for the "pink" colorset listed above.

Posted (edited)
On 3/21/2025 at 7:00 PM, scratchcat579 said:

note: you forgot to add scale transformations in your blender importer. the format is float time, uint16 track, float16 sx, float 16 sy, float 16 sz.

What do you mean? blender-outfit7-skeleton-converter in the zip i attached has
 

        # scales
        sxs = struct.unpack_from('<ffff', ozz_skeleton, offset)
        sys = struct.unpack_from('<ffff', ozz_skeleton, offset + 16)
        szs = struct.unpack_from('<ffff', ozz_skeleton, offset + 32)
        for i in range(4):
            bind_pose_scales.append(Vector((sxs[i], sys[i], szs[i])))

and blender-outfit7-animation-converter has
 

    for i in range(scale_count):
        time = struct.unpack_from('<f', input_data[offset:offset + 4])[0]
        track = struct.unpack_from('<H', input_data[offset + 4:offset + 6])[0]
        sx = struct.unpack_from('<e', input_data[offset + 6:offset + 8])[0]
        sy = struct.unpack_from('<e', input_data[offset + 8:offset + 10])[0]
        sz = struct.unpack_from('<e', input_data[offset + 10:offset + 12])[0]

edit: And all the animations I have seen seem to work fine. Except for the fact that the animations are in absolute pose space and not relative to the rest pose. (Absolute pose space meaning that the bones must be at the origin of the skeleton, in their default orientation and scale. Or in other words: their transformation matrx must be the identity matrix)

Edited by yarcunham
  • 2 months later...
Posted (edited)

i think why these header and footer exist because of the way Starlite loads assets using a memcpy and pointer fix.

the values i found in the header are here (the vertex information in the header is at the end in the new format for whatever reason):

struct HeaderNew {
    Vec4f boundsPosition @ 0x30; Vec4f boundsSize @ 0x40; 
    u64 bindPosesPtr @ 0x50; u32 numBindPoses @ 0x58; u32 bindPosesExist @ 0x5C;
    u64 boneLengthsPtr @ 0x60; u32 numBoneLengths @ 0x68; u32 boneLengthsExist @ 0x6C;
    u64 boneIDsPtr @ 0x70; u32 numBoneIDs @ 0x78; u32 boneIDsExist @ 0x7C;
    u64 vertexDataPtr @ 0x110; u32 vertexDataLength @ 0x118; u32 vertexDataExist @ 0x11C;
    u64 blendShapesPtr @ 0x80; u32 numBlendShapes @ 0x88; u32 blendShapesExist @ 0x8C;
    u64 indicesPtr @ 0x90; u32 numIndices @ 0x98; u32 indicesExist @ 0x9C;
    u64 unknownDataPtr @ 0xB0; u32 unknownDataCount @ 0xB8; u32 unknownDataExist @ 0xBC;
    u32 vertexStride @ 0xC4;
};

struct Header {
    Vec4f boundsPosition @ 0x30; Vec4f boundsSize @ 0x40;
    u64 bindPosesPtr @ 0x50; u32 numBindPoses @ 0x58; u32 bindPosesExist @ 0x5C;
    u64 boneLengthsPtr @ 0x60; u32 numBoneLengths @ 0x68; u32 boneLengthsExist @ 0x6C;
    u64 boneIDsPtr @ 0x70; u32 numBoneIDs @ 0x78; u32 boneIDsExist @ 0x7C;
    u64 vertexDataPtr @ 0x80; u32 vertexDataLength @ 0x88; u32 vertexDataExist @ 0x8C;
    u64 blendShapesPtr @ 0x90; u32 numBlendShapes @ 0x98; u32 blendShapesExist @ 0x9C;
    u64 indicesPtr @ 0xA0; u32 numIndices @ 0xA8; u32 indicesExist @ 0xAC;
    u64 unknownDataPtr @ 0xB0; u32 unknownDataCount @ 0xB8; u32 unknownDataExist @ 0xBC;
    u32 vertexStride @ 0xC4;
};

i have also attached the imhex scripts to highlight values in a zip.

mesh hexpat.zip

Edited by scratchcat579
grammar fix
  • 3 months later...
  • 3 weeks later...
  • 3 weeks later...
Posted

This is not a complete solution yet, but maybe it will help someone get started. This describes the new structure of the project, bundle and assets files. They all seem to either use LZMA compression or none. This uses the pattern language from ImHex, but I'm sure it's "self documenting" enough that another programmer is able to decode it:

enum Compression: u32 {
    None,
    zStd,
    LZMA,
    Lz4
};

struct lzmaData<auto size> {
    u24 lzma_conf;
    be u24 lzma_dict_size;
    
    u8 data[size-6];
};

struct Chunk {
    u32 uncompressed_size;
    u32 compressed_size;
    u32 hash;
    Compression compression;
    u32 hash2;
    
    match(compression) {
        (Compression::None): u8 data[compressed_size];
        (Compression::LZMA): lzmaData<compressed_size>;
    }
};

struct ProjectHeader {
    char magic[0x04];
    u32 version;
    u32 uncompressed_size_of_file;
    u32 zeros;
};


struct ProjectFile {
    ProjectHeader header;
    Chunk data;
};

// for the .project file:
// ProjectFile project @ 0x00;

// for .bundle and .assets files:
// Chunk chunks[50] @ 0x00; // the number of entries varies

 

  • 2 weeks later...
Posted

So, the project and bundle files contain a bunch of hashes. The engine uses xxhash64 to store references to text strings and just by hashing strings found in the engine binary and also in the decompressed data files, I was able to make reverse mappings for a bunch of them. The algorithm itself is just xxhash64("string"), no seed value. The hex values I got from web pages that let you encode strings as xxhash, such as https://www.coderstool.com/xxh-hash-generator, give the result in the opposite endianness.

So for example the above site gives me 0735c614e52391c9 for the string "root" when the hex sequence in the files is c99123e514c63507

The attachment root.project.string_hashes.zip contains a mapping of strings to hashes I found in the decompressed root.project file.

root.bundle.string_hashes.zip contains the same for the root.bundle file.

You should be able to just copy-paste the hex strings into your favorite hex editor's "find hex value" functionality and find any offsets that refer to a specific string.

Unfortunately this does not completely blow open the file format, but I think it might help at least.

root.project.string_hashes.zip root.bundle.string_hashes.zip

  • 3 weeks later...
Posted (edited)

here are some common structures:

image.png.984875b2cad6ac35310f7e636f24a1d1.png array

the red value is a pointer, purple value is element count, brown value tells if its allocated

image.png.44b08f1787d0de6c3acf827d8a901c76.png  list

all values from array, and the pink value is the array length

image.png.f7ff844abd5ab16848512086fd4ce988.png  string

all values from list, yellow value is string length, and the cyan value is the string hash (xxh64)

image.png.698f85829a48d6dec812a2223685746c.png object

the red value is the type index. the purple value is the object type id. the brown value is the object flags. the pink and yellow values track strong and weak references.

Edited by scratchcat579
corrections
  • 3 weeks later...
Posted
On 1/6/2025 at 11:09 PM, yarcunham said:

TEAQM2H.png

The last part of the whole file is the footer of the meta file container. I don't know what any of these values mean, but so far it hasn't been a problem.

Okay, so looking at those 8 bytes there: C2 F9 75 FF B5 82 88 2F, that's the XXhash value for "Starlite::MeshResourceData". In other words, the type of the data is tagged right at the end of the data itself

Posted
On 12/21/2025 at 1:30 PM, scratchcat579 said:

image.png.f1fff4904d450284778345ae3e042fc4.png asset header

the blue value is the payload size, the orange value is the number of 16 byte sized values (idk), the green value is the count of pointers to classesin the payload, and the red value is the count of pointers to pointers in the payload.

I already mentioned part of the 16-byte values: the first 8 bytes are the xxHash of the type name,  like "Starlite::UiImage", "Starlite::Camera", "Starlite::Data". The next 4 bytes might be the type's size in bytes. Not sure about the rest

Posted (edited)

image.thumb.png.047dff90a14878ab26a7049105a6110e.png

This is a simple quad mesh I found in the assets. This is its structure:

header (16 bytes)

uint32: 40 01 00 00 (decimal value 320). Offset to type descriptors.

uint32: Number of type descriptos in the file (1)

uint32: Number of objects in the file (1)

uint32: Number of offsets (or pointers) in the file (2)

(skipping over the object section for now, returning to that later in the decoding process)

Continuing at offset 336 (0x150), or 16 + 320 (size of the header + offset to type descriptors):

Type descriptor (16 bytes):

uint64: 0xC2F975FFB582992F xxHash of the type name ("Starlite::MeshResourceData")

uint32 type size 0x00010000 (256 bytes)

uint16 type index 0x8500 (133). Just another way of referring to the same type. The hash and the id value always correlate in every file I've checked.

last 2 bytes: padding.

Immediately after the type descriptors (offset 160)

Object offsets (number of objects * 4 bytes):

the only object in this file is at offset 0, so immediately after the header, at absolute offset 0x10 (16 in decimal)

Offsets/pointers (number of offsets * 4 bytes):

there are 2 pointers in the object section, at offsets 0x70 (112) and at 0x90 (144). Adding the header size, you get the absolute offset in the file, 128 (0x80) and 160 (0xa0)

Jumping back up to the objects section, which starts at offset 16 (0x10):

The first 32 bytes is an index into the type descriptor array, in this case it's 0, meaning Starlite::MeshResourceData.

The next 12 bytes are padding.

The mesh data type has been reversed earlier in this thread, but I want to point out that the offsets/pointers listed point at the "start of vertex data" field and the "start of face data" fields. So, whatever values the offset list points at, those are offsets themselves. A pointer to a pointer in other words.

One final observation: The mesh data type itself is 256 bytes long, as the type descriptor says. It spans from offset 0x10 to 0x100. It is then followed by the vertex data (which can be located with the start of vertex data pointer and the vertex data size fields) and the vertices are followed by the face data (again, found by following the start of face data pointer and the "number of face entries" field). So the size of the object in the file is size of the type + size of any buffers that the type points to.

Edited by yarcunham
add details
Posted

One thing I learned about the LZMA decompression today: if you configure your LZMA decompressor to use raw LZMA, you must configure the maximum size of the returned data. The rax format does not include that information and will happily return more than 65536 bytes (which is still the chunk size with LZMA compressed assets). Those few extra bytes will cause corrupted data due to misalignments.

Posted

I have managed to dump the assets with a directory structure. Well, kind of. The asset names do look like a directory structure, but there are multiple assets with identical nimes (mostly "", i.e. empty string and "internal"). There are also assets which have an asset file with an identical name to a sub directory, as in a file whose path is "bundles/foo/bar/baz", but then another asset which is just "bundles/foo".

But for the most part, it does work. I will post the script later after I do some cleanup

  • 2 weeks later...
Posted

Well, it took a bit longer before I got to the cleanup part, but here it is. I'm not exactly sure how universal this extractor is. I have only tried it with My Talking Angela 2 and only with apks downloaded from one site, so it could be that this it actually completely broken on other games and other apks.

Anyway, the way this process works is: Extract the apk file (it's just a zip file, rename it to base.apk.zip if you need to). Inside the extracted directory there is a directory named "assets" and inside the assets directory there are 2 files: root.bundle and root.assets. 

Run the outfit7-asset-extractor-with-paths.py file with the arguments <path/to/root.bundle> and <path/to/output/directory>

For me, the extraction process only takes a few seconds. After that, you can post-process some of the known file types by running outfit7-asset-post-processor.py with <path/to/output/directory> and it will make copies of file types it recognizes with proper file extensions. 

The outfit7-asset-extractor-with-paths.py script requires python 3.14 because it uses the compression package for zstd

outfit7-asset-extractor-with-paths.zip

Posted (edited)

 

moonlite requires that you are running Windows and have the .NET Framework 4.7.2 installed.

look at the readme to get some Informationz Matey

 

edit: i forgot to say that this is a complete rewrite

Release 1.7z Release.7z

Edited by scratchcat579
the binaries refused to load the dll files
Posted

Okay, I did go and confirm that the code works on the only other starlite game I could find: My talking Tom 2. All the other games seem to use unity

Posted
On 2/18/2026 at 12:22 AM, scratchcat579 said:
    xx_hash_unknown: int
    xx_hash_unknown2: int

xx_hash_unknown is the asset id (hash of guid string) and xx_hash_unknown2 is the hash of the decompressed asset data

Excellent, thanks

  • 2 weeks later...
  • 4 weeks later...
  • 1 month later...
  • 3 weeks later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...