Jump to content

Leisure Suit Larry: Magna Cum Laude .JAM Files


Angel333119

Recommended Posts

Hello, could someone help me understand the structure of the file for documentation purposes?

dNDD0I2.png

 

4 - magic (JAM2)
4 - Unknown
4 - offset of the first file data in the container
4 - magic 2 (none)
12 - null
2 - Number Of Filenames
2 - Number Of Extensions

// for each filename
8 - Filename

// for each extension
4 - Extension Name // the first extension is null

8 - Unknown
4 - offset of the first file size
12 - Unknown
4 - offset of the first ID

// for each file
2 - Filename ID
2 - File Extension ID
4 - Offset

//for each file
4 - Compressed File Size
4 - Decompressed File Size
24 - Unknown
XX - File Data
1 - FF
*next file

INTRFRAM.rar

Link to comment
Share on other sites

Here's the file format from xentax wiki backup:
 


char {4}     - Header (JAM2)
uint32 {4}   - Unknown
uint32 {4}   - First File Offset
char {4}     - Header 2 (none)
byte {12}    - null
uint16 {2}   - Number Of Filenames
uint16 {2}   - Number Of Extensions

// for each filename

    char {8}   - Filename (null)


// for each extension

    char {4}   - Extension Name (null) // the first extension is all nulls


uint32 {4}   - Unknown

// NOTE: Some files have invalid offsets - only allow offsets >= FirstFileOffset
// for each file

    uint16 {2}   - Filename ID
    uint16 {2}   - File Extension ID
    uint32 {4}   - Offset


// File Data

    // for each file

        uint32 {4}   - Compressed File Size
        uint32 {4}   - Decompressed File Size
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        byte {X}     - File Data
        byte {0-3}   - Junk padding to a multiple of 4 bytes

 

Link to comment
Share on other sites

1 hour ago, ikskoks said:

Here's the file format from xentax wiki backup:
 


char {4}     - Header (JAM2)
uint32 {4}   - Unknown
uint32 {4}   - First File Offset
char {4}     - Header 2 (none)
byte {12}    - null
uint16 {2}   - Number Of Filenames
uint16 {2}   - Number Of Extensions

// for each filename

    char {8}   - Filename (null)


// for each extension

    char {4}   - Extension Name (null) // the first extension is all nulls


uint32 {4}   - Unknown

// NOTE: Some files have invalid offsets - only allow offsets >= FirstFileOffset
// for each file

    uint16 {2}   - Filename ID
    uint16 {2}   - File Extension ID
    uint32 {4}   - Offset


// File Data

    // for each file

        uint32 {4}   - Compressed File Size
        uint32 {4}   - Decompressed File Size
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        uint32 {4}   - Unknown
        byte {X}     - File Data
        byte {0-3}   - Junk padding to a multiple of 4 bytes

 


This is exactly the same thing I published, but incomplete.

I need someone to help me understand the file in its entirety.

I found this schema that you say is from Xentax, and it's even on Zenhax at this link:

https://zenhax.com/viewtopic.php@t=17851.html

However, this didn’t help me much, as it’s incorrect in several points that have been corrected in my documentation.

Edited by Angel333119
Link to comment
Share on other sites

Some files have incorrect offset, so i am ignoring that.

here the code to extract your file.

 

import os
import struct
import tkinter as tk
from tkinter import filedialog

def read_string(file, length):
    return file.read(length).decode('utf-8').strip('\x00')

def extract_files(file_path, output_folder):
    with open(file_path, 'rb') as f:
        # Ler o cabeçalho
        magic = f.read(4)
        unknown = f.read(4)
        first_file_offset = struct.unpack('<I', f.read(4))[0]
        magic2 = f.read(4)
        null_bytes = f.read(12)
        num_filenames = struct.unpack('<H', f.read(2))[0]
        num_extensions = struct.unpack('<H', f.read(2))[0]

        # Ler os nomes dos arquivos
        filenames = []
        for _ in range(num_filenames):
            filenames.append(read_string(f, 8))

        # Ler as extensões dos arquivos
        extensions = []
        for _ in range(num_extensions):
            extensions.append(read_string(f, 4))

        # Ler informações adicionais
        unknown = f.read(8)
        offset_first_file_size = struct.unpack('<I', f.read(4))[0]
        unknown = f.read(12)
        offset_first_id = struct.unpack('<I', f.read(4))[0]

        # Ler os arquivos
        files = []
        for i in range(num_filenames):
            filename_id = struct.unpack('<H', f.read(2))[0]
            file_extension_id = struct.unpack('<H', f.read(2))[0]
            offset = struct.unpack('<I', f.read(4))[0]
            files.append((i, filename_id, file_extension_id, offset))

        # Ler os tamanhos e dados dos arquivos
        for position, filename_id, file_extension_id, offset in files:
            if offset < first_file_offset:
                print(f"Aviso: Offset inválido ({offset}) para o arquivo na posição {position}. Ignorando este arquivo.")
                continue

            f.seek(offset)
            compressed_size = struct.unpack('<I', f.read(4))[0]
            decompressed_size = struct.unpack('<I', f.read(4))[0]
            unknown = f.read(24)
            file_data = f.read(compressed_size)
            f.read(1)  # FF

            # Salvar o arquivo extraído
            filename = filenames[filename_id % num_filenames]
            extension = extensions[file_extension_id % num_extensions]
            output_path = os.path.join(output_folder, f"{filename}.{extension}")
            with open(output_path, 'wb') as out_file:
                out_file.write(file_data)

            print(f"Extraído: {filename}.{extension}")
            print(f"  Posição: {position}")
            print(f"  Offset: {offset}")
            print(f"  Tamanho comprimido: {compressed_size}")
            print(f"  Tamanho descomprimido: {decompressed_size}")
            print(f"  ID do nome do arquivo: {filename_id}")
            print(f"  ID da extensão: {file_extension_id}")
            print("--------------------")

if __name__ == "__main__":
    # Criar uma janela root (mas não a mostraremos)
    root = tk.Tk()
    root.withdraw()

    # Abrir o diálogo de seleção de arquivo
    file_path = filedialog.askopenfilename(
        title="Selecione o arquivo JAM",
        filetypes=[("Arquivos JAM", "*.JAM"), ("Todos os arquivos", "*.*")]
    )

    if file_path:
        # O usuário selecionou um arquivo
        output_folder = "extraidos"
        os.makedirs(output_folder, exist_ok=True)
        print(f"Extraindo arquivos de: {file_path}")
        extract_files(file_path, output_folder)
        print(f"Extração concluída. Os arquivos foram salvos em: {os.path.abspath(output_folder)}")
    else:
        print("Nenhum arquivo selecionado. Encerrando o programa.")

    # Fechar a janela root
    root.destroy()

 

leisure _unpack.py

Link to comment
Share on other sites

The problem is reconstructing the file. The truth is that I want to understand the file so I can extract and rebuild it, both by adding new files and using only the original ones. Some of the unknown bytes have some relevance because the same file on PC and PS2 has different values. Some others with similar values are probably just junk.

Edited by Angel333119
Link to comment
Share on other sites

Hi, I know this is probably useless, but the second 4 Byte is most probably a UNIX Hex timestamp.

4 - magic (JAM2)
4 - hexadecimal timestamp when packed
4 - offset of the first file data in the container
4 - magic 2 (none)

Link to comment
Share on other sites

This "1 - FF " also seems to be wrong, if the "Compressed File Size" / 4 has a remainder, it gets padded with 1 - FF, 2 - 00FF, or 3 - 0000FF  in order to start new file on a new word.

There are examples of this in the file:

File intro.adr starts on 10d334, is 20(32) header + 5DF(1503) data length, and ends on 10d932 so 10d933 gets padded with 1-FF. The next file starts on 10d934

File Larry.bmp starts on 3cc, is 20(32) header + 40436(263222) data length, and ends on 40821 so 40822 is FF, and 40823 is 00. The next file starts on 40824.

File assetspc.aua starts on 10d934, is 20(32) header + 205(517) data length, and ends on 10db58, so b59 is FF, b5a is 00, and b5b is 00. The next file starts on 10db5c.

I hope this makes sense and helps with the documentation. 

Rebuilding the file would probably be easier than trying to replace data, thinking you could probably dump unknown header information to a file and then add it back in when rebuilding.

Link to comment
Share on other sites

Here is a repack unpack that should work on all of the JAM files. Drop the JAM you want to unpack on the unpack.py, it will unpack the files to "extracted_files" and create a metadata.txt. This is required for the repack.py. When changes have been applied, double click the repack file, and rename the new repacked.jam file to the original name. Hope it helps someone. EDIT: This seemed to repack the files properly at glance, but the game did not want to run with the new files.

 

Repack.py Unpack.py

Edited by Anti6
Link to comment
Share on other sites

This is the closest i've gotten to a properly working unpack, repack script, for some reason the game gives a duplicate file found error. I'll keep checking to get it working. Maybe someone can fix it if I can't. Basically the issue is that in the header data, you get filenames and extensions, which then have to be linked to the id#, ext# and offset given right after that, for unpacking this is fine. These offsets are basically pointer values that the file contains and the game reads. When repacking you have to go and update these offsets while ensuring that the actual files are packed in the same order that they were unpacked, not sure if it is completely necessary but during my tests this yielded the best results. Unfortunately there are duplicates among the (id#, ext#) with different offsets meaning different files with the same name. I am pretty sure I am handling everything correctly as much as possible. The padding logic is a bit tricky and causes deviations when repacking, which should be fine and good as a test to see if files can be altered en-mass. But for some reason, not sure why, the game doesn't recognise the new pointers properly, which means (I hope not) that you would never be able to replace assets with something bigger. If anyone has some insights I might have overlooked, please share.

 

repack.py Unpack.py

Edited by Anti6
Link to comment
Share on other sites

Hi Rabatini, yehp I did take a look at yours, the language throws me off a bit, though what I want to do is be able to replace entire sets of files, and repack it in exactly the same order, with exactly the same/correct header information, file header information/ and padding, also you said "Some files have incorrect offset, so i am ignoring that.", which is true for some "files", but they are critical to repack the JAM files, as the game still uses them. Also, the information in the original post is incorrect to some extent, which I will be adding to the end of this post. Lastly the duplicate file found error I previously mentioned happened ingame, the scripts handled duplicate files albeit incorrectly.
There were also some incorrect assumptions from the original post, for instance, "4 - offset of the first file size" and "4 - offset of the first ID" is only true for this file, the others have other values there - see update at end.
With that said, there are some extra challenges. Though it seems I sorted it out, I am now able to unpack and repack a jam file to recreate the exact same file.
I had to adjust how I sort when unpacking, and duplicate handling when repacking, though I made a few other changes as well. Anyone can feel free to use it/change it/distribute it as they wish. These seem to work fine for all Magna Cum Laude Jam files. I repacked all of them, but I don't have a 100% file to test all stages, I tested all the initial ones.

I also made some changes to textures and tested, which work fine:image.png.65958a7862ad49f215d16836538294a2.png
Sadly it seems I will have to modify the EXE if I want to upscale textures due to an "out of memory error", unfortunately, I am already completely out of my depth. In any case, I have attached my current final versions to this, I also included a metadata generator for somewhat easier comparison. 

Updated information for JAM2 Files:

4 - magic (JAM2)
4 - Hex Timestamp
4 - offset of the first file data in the container
4 - magic 2 (none)
12 - null
2 - Number Of Filenames
2 - Number Of Extensions

// for each filename
8 - Filename

// for each extension
4 - Extension Name // the first extension is null

2 - Number of 'Unique' or file specific folders/offsets
2 - Number of 'Unique' or file specific files

// for each file - Starts with 'Unique folders/offsets', then 'Unique Files' then the rest of the offsets/folders/files which may or may not be shared.
2 - Filename ID
2 - File Extension ID
4 - Offset

//for each file
4 - Compressed File Size
4 - Decompressed File Size
4 - Unknown 1(Seems to always be 0x000000ff)
4 - Unknown 2(Seems to always be 0x0000002d)
4 - Unknown 3(Seems to always be 0x00000042)
4 - Unknown 4(Seems to always be 0x00000053)
4 - Unknown 5(Seems to always be 0x0000008b)
4 - Unknown 6(Seems to always be 0x000000a2)
XX - File Data
1 to 3 (Padding) - Pads first byte with FF then next with 00 until 4 bytes are filled at end of file - ""padding=(4-(Compressed File Size%4))%4 then (b'\xFF' + b'\x00' * (padding - 1))""
*next file

Generate Metadata.py Repack.py Unpack.py

  • Thanks 1
Link to comment
Share on other sites

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