Angel333119 Posted September 1 Share Posted September 1 Hello, could someone help me understand the structure of the file for documentation purposes? 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 More sharing options...
ikskoks Posted September 1 Share Posted September 1 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 More sharing options...
Angel333119 Posted September 1 Author Share Posted September 1 (edited) 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 September 1 by Angel333119 Link to comment Share on other sites More sharing options...
Engineer Rabatini Posted September 1 Engineer Share Posted September 1 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 More sharing options...
Angel333119 Posted September 2 Author Share Posted September 2 (edited) 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 September 2 by Angel333119 Link to comment Share on other sites More sharing options...
Engineer Rabatini Posted September 2 Engineer Share Posted September 2 (edited) Try this, if works in the game, need make some ajustments, but maybe the game accepts OLD_leisure_pack.py OLD_leisure_pack.py Edited September 3 by Rabatini Link to comment Share on other sites More sharing options...
Anti6 Posted September 10 Share Posted September 10 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 More sharing options...
Anti6 Posted September 10 Share Posted September 10 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 More sharing options...
Anti6 Posted September 12 Share Posted September 12 (edited) 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 September 13 by Anti6 Link to comment Share on other sites More sharing options...
Anti6 Posted September 13 Share Posted September 13 (edited) 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 September 13 by Anti6 Link to comment Share on other sites More sharing options...
Engineer Rabatini Posted September 13 Engineer Share Posted September 13 Did you take a look in my unpack, in previous messages? Link to comment Share on other sites More sharing options...
Anti6 Posted September 17 Share Posted September 17 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: 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 1 Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now