Jump to content

Recommended Posts

Posted

I've been trying to figure out how to correctly extract the files from one of these .zap archives. I know there's a bms script for them but every time I use it everything always comes out incorrectly where, for example, using it with ovldata.zap a character file will actually have data with an unrelated shape in it and the header (FGRK) wouldn't be in the correct spot at the start of the files so I had to manually go through the entire file archive and manually take them out with a hex editor... EVERY SINGLE FILE. 

Since I've got a semi-solution to ovldata, I'm now primarily working with auxdata.zap which houses all of the game's world models and I can't seem to find a good way to find out any of the specific files cause they don't seem to have explicit headers unlike ovldata.

And unfortunately the bms script seems to be extra broken with auxdata as it shares the same issue with data being in the wrong file, but it also has it's own bug where the extractor just seems to trip up and asks to rename the file in order for it to save, but you can try to rename the file as much as you want but it never actually goes through. 

How the BMS script looks when it trips up


I've attached both of the aforementioned .zap archives so hopefully someone can figure out a solution to this. 

ovldata.zap on Google Drive
auxdata.zap on Google Drive

  • Engineers
Posted
import os
import struct
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import threading
import re

class ZAPExtractor:
    def __init__(self):
        """Initialize the ZAP extractor"""
        self.path_stack = ["/"]
        self.output_callback = None

    def log(self, message):
        """Log messages if callback is provided"""
        if self.output_callback:
            self.output_callback(message)

    def sanitize_path(self, path):
        """Sanitize file/folder names to remove null bytes and invalid characters"""
        # Replace null bytes and control characters
        path = re.sub(r'[\x00-\x1F\x7F]', '', path)
        
        # Replace other potentially problematic characters
        path = path.replace(':', '_').replace('*', '_').replace('?', '_')
        path = path.replace('"', '_').replace('<', '_').replace('>', '_')
        path = path.replace('|', '_').replace('\\', '_')
        
        return path

    def extract_zap(self, zap_path, output_dir, output_callback=None):
        """Extract files from a ZAP archive"""
        self.output_callback = output_callback
        
        # Create output directory if it doesn't exist
        os.makedirs(output_dir, exist_ok=True)
        
        # Track used filenames to handle duplicates
        used_filenames = {}
        
        try:
            with open(zap_path, 'rb') as f:
                # Check signature "ZAP\0"
                signature = f.read(4)
                if signature != b'ZAP\0':
                    self.log(f"Invalid ZAP archive: Signature mismatch")
                    return False
                
                # Read header
                offset = struct.unpack("<I", f.read(4))[0]  # 32-bit integer (long)
                entries = struct.unpack("<I", f.read(4))[0]  # number of entries
                dummy = struct.unpack("<I", f.read(4))[0]    # dummy value
                
                self.log(f"Archive has {entries} entries, starting at offset {offset}")
                self.path_stack = ["/"]
                
                # Process each entry
                for i in range(entries):
                    # Read entry header
                    dummy = struct.unpack("<H", f.read(2))[0]  # 0x0a0d (short)
                    size = struct.unpack("<I", f.read(4))[0]   # file size
                    name_size = struct.unpack("<H", f.read(2))[0]  # name length
                    
                    # Read and sanitize name - handle possible null bytes
                    name_bytes = f.read(name_size)
                    try:
                        # First try to decode until the first null byte
                        null_pos = name_bytes.find(b'\0')
                        if null_pos != -1:
                            name = name_bytes[:null_pos].decode('utf-8', errors='replace')
                        else:
                            name = name_bytes.decode('utf-8', errors='replace')
                    except:
                        # Fall back to hexadecimal representation if decoding fails
                        name = f"unknown_file_{i}_0x{name_bytes.hex()}"
                    
                    # Sanitize name to remove any remaining problematic characters
                    name = self.sanitize_path(name)
                    
                    # Skip empty names
                    if not name:
                        name = f"unnamed_entry_{i}"
                    
                    if size == 0:  # It's a directory
                        if name == "..":
                            # Go up one directory
                            if len(self.path_stack) > 1:
                                self.path_stack.pop()
                        else:
                            # Enter directory
                            self.path_stack.append(name + "/")
                        self.log(f"Directory: {''.join(self.path_stack)}")
                    else:  # It's a file
                        current_path = ''.join(self.path_stack)
                        file_path = current_path + name
                        
                        # Save current position
                        current_pos = f.tell()
                        
                        # Create full path for output
                        rel_path = file_path[1:] if file_path.startswith('/') else file_path  # Remove leading /
                        
                        # Replace any path separators in the filename itself (shouldn't happen, but just in case)
                        rel_path = rel_path.replace('/', os.sep)
                        
                        # Make sure the final path is safe
                        output_file = os.path.normpath(os.path.join(output_dir, rel_path))
                        
                        # Safety check - make sure we're not trying to write outside output_dir
                        # (path traversal protection)
                        if not output_file.startswith(os.path.abspath(output_dir)):
                            self.log(f"Warning: Attempted path traversal detected: {rel_path}")
                            output_file = os.path.join(output_dir, f"unsafe_path_{i}_{os.path.basename(rel_path)}")
                        
                        output_dir_path = os.path.dirname(output_file)
                        
                        # Handle duplicate filenames
                        base_output_file = output_file
                        if output_file in used_filenames:
                            base_name, ext = os.path.splitext(output_file)
                            counter = used_filenames[output_file] + 1
                            used_filenames[output_file] = counter
                            output_file = f"{base_name}_{counter}{ext}"
                            self.log(f"Renamed duplicate: {rel_path} -> {os.path.basename(output_file)}")
                        else:
                            used_filenames[output_file] = 0
                        
                        # Create directory structure
                        try:
                            os.makedirs(output_dir_path, exist_ok=True)
                        except Exception as e:
                            self.log(f"Error creating directory {output_dir_path}: {str(e)}")
                            # Try with a simplified path
                            simple_dir = os.path.join(output_dir, f"recovered_dir_{i}")
                            os.makedirs(simple_dir, exist_ok=True)
                            output_file = os.path.join(simple_dir, f"file_{i}")
                        
                        # Go to file data and extract
                        try:
                            f.seek(offset)
                            data = f.read(size)
                            
                            with open(output_file, 'wb') as out_file:
                                out_file.write(data)
                            
                            self.log(f"Extracted: {rel_path} ({size} bytes)")
                        except Exception as e:
                            self.log(f"Error extracting {rel_path}: {str(e)}")
                        
                        # Adjust offset for next file - align to 0x1000
                        offset += size
                        offset = (offset + 0xFFF) & ~0xFFF  # Equivalent to math OFFSET x 0x1000
                        
                        # Return to entry list
                        f.seek(current_pos)
                
                self.log("Extraction completed successfully")
                return True
                
        except Exception as e:
            self.log(f"Error extracting archive: {str(e)}")
            return False


class ZAPExtractorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("ZAP Archive Extractor")
        self.root.geometry("650x500")
        self.root.resizable(True, True)
        
        # Create extractor
        self.extractor = ZAPExtractor()
        
        # Create GUI elements
        self.create_widgets()

    def create_widgets(self):
        # Frame for input controls
        input_frame = tk.Frame(self.root, padx=10, pady=10)
        input_frame.pack(fill=tk.X)
        
        # Input file selection
        tk.Label(input_frame, text="Input ZAP Archive:").grid(row=0, column=0, sticky=tk.W, pady=5)
        self.input_path = tk.StringVar()
        tk.Entry(input_frame, textvariable=self.input_path, width=50).grid(row=0, column=1, padx=5)
        tk.Button(input_frame, text="Browse", command=self.browse_input).grid(row=0, column=2)
        
        # Output directory selection
        tk.Label(input_frame, text="Output Directory:").grid(row=1, column=0, sticky=tk.W, pady=5)
        self.output_path = tk.StringVar()
        tk.Entry(input_frame, textvariable=self.output_path, width=50).grid(row=1, column=1, padx=5)
        tk.Button(input_frame, text="Browse", command=self.browse_output).grid(row=1, column=2)
        
        # Extract button
        extract_btn = tk.Button(input_frame, text="Extract Files", command=self.start_extraction, bg="#4CAF50", fg="white", padx=10, pady=5)
        extract_btn.grid(row=2, column=0, columnspan=3, pady=10)
        
        # Log frame
        log_frame = tk.Frame(self.root, padx=10, pady=5)
        log_frame.pack(fill=tk.BOTH, expand=True)
        
        tk.Label(log_frame, text="Extraction Log:").pack(anchor=tk.W)
        self.log_text = scrolledtext.ScrolledText(log_frame, height=15)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        
        # Status bar
        self.status_var = tk.StringVar()
        self.status_var.set("Ready")
        status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # Clear log button
        clear_btn = tk.Button(log_frame, text="Clear Log", command=self.clear_log)
        clear_btn.pack(anchor=tk.E, pady=5)

    def clear_log(self):
        self.log_text.delete(1.0, tk.END)

    def browse_input(self):
        path = filedialog.askopenfilename(title="Select ZAP Archive",
                                          filetypes=[("ZAP Archives", "*.zap"), ("All Files", "*.*")])
        if path:
            self.input_path.set(path)

    def browse_output(self):
        path = filedialog.askdirectory(title="Select Output Directory")
        if path:
            self.output_path.set(path)

    def log(self, message):
        self.log_text.insert(tk.END, message + "\n")
        self.log_text.see(tk.END)
        self.root.update_idletasks()

    def start_extraction(self):
        input_path = self.input_path.get()
        output_path = self.output_path.get()
        
        # Validate inputs
        if not input_path or not os.path.isfile(input_path):
            messagebox.showerror("Error", "Invalid input archive path")
            return
        
        if not output_path:
            messagebox.showerror("Error", "Please select an output directory")
            return
        
        # Update status
        self.status_var.set("Extracting files...")
        self.log(f"Starting extraction of {os.path.basename(input_path)}")
        
        # Run extraction in a separate thread to avoid UI freezing
        threading.Thread(target=self.run_extraction, 
                        args=(input_path, output_path), 
                        daemon=True).start()

    def run_extraction(self, input_path, output_path):
        # Run extraction
        success = self.extractor.extract_zap(input_path, output_path, self.log)
        
        # Update UI
        if success:
            self.status_var.set("Extraction completed")
            messagebox.showinfo("Success", "Files have been extracted successfully")
        else:
            self.status_var.set("Extraction failed")
            messagebox.showerror("Error", "Failed to extract files. See log for details.")


if __name__ == "__main__":
    root = tk.Tk()
    app = ZAPExtractorApp(root)
    root.mainloop()

A workaround is, rename the repeat folder to another name.

try this.

python

Posted
6 hours ago, Rabatini said:
import os
import struct
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import threading
import re

class ZAPExtractor:
    def __init__(self):
        """Initialize the ZAP extractor"""
        self.path_stack = ["/"]
        self.output_callback = None

    def log(self, message):
        """Log messages if callback is provided"""
        if self.output_callback:
            self.output_callback(message)

    def sanitize_path(self, path):
        """Sanitize file/folder names to remove null bytes and invalid characters"""
        # Replace null bytes and control characters
        path = re.sub(r'[\x00-\x1F\x7F]', '', path)
        
        # Replace other potentially problematic characters
        path = path.replace(':', '_').replace('*', '_').replace('?', '_')
        path = path.replace('"', '_').replace('<', '_').replace('>', '_')
        path = path.replace('|', '_').replace('\\', '_')
        
        return path

    def extract_zap(self, zap_path, output_dir, output_callback=None):
        """Extract files from a ZAP archive"""
        self.output_callback = output_callback
        
        # Create output directory if it doesn't exist
        os.makedirs(output_dir, exist_ok=True)
        
        # Track used filenames to handle duplicates
        used_filenames = {}
        
        try:
            with open(zap_path, 'rb') as f:
                # Check signature "ZAP\0"
                signature = f.read(4)
                if signature != b'ZAP\0':
                    self.log(f"Invalid ZAP archive: Signature mismatch")
                    return False
                
                # Read header
                offset = struct.unpack("<I", f.read(4))[0]  # 32-bit integer (long)
                entries = struct.unpack("<I", f.read(4))[0]  # number of entries
                dummy = struct.unpack("<I", f.read(4))[0]    # dummy value
                
                self.log(f"Archive has {entries} entries, starting at offset {offset}")
                self.path_stack = ["/"]
                
                # Process each entry
                for i in range(entries):
                    # Read entry header
                    dummy = struct.unpack("<H", f.read(2))[0]  # 0x0a0d (short)
                    size = struct.unpack("<I", f.read(4))[0]   # file size
                    name_size = struct.unpack("<H", f.read(2))[0]  # name length
                    
                    # Read and sanitize name - handle possible null bytes
                    name_bytes = f.read(name_size)
                    try:
                        # First try to decode until the first null byte
                        null_pos = name_bytes.find(b'\0')
                        if null_pos != -1:
                            name = name_bytes[:null_pos].decode('utf-8', errors='replace')
                        else:
                            name = name_bytes.decode('utf-8', errors='replace')
                    except:
                        # Fall back to hexadecimal representation if decoding fails
                        name = f"unknown_file_{i}_0x{name_bytes.hex()}"
                    
                    # Sanitize name to remove any remaining problematic characters
                    name = self.sanitize_path(name)
                    
                    # Skip empty names
                    if not name:
                        name = f"unnamed_entry_{i}"
                    
                    if size == 0:  # It's a directory
                        if name == "..":
                            # Go up one directory
                            if len(self.path_stack) > 1:
                                self.path_stack.pop()
                        else:
                            # Enter directory
                            self.path_stack.append(name + "/")
                        self.log(f"Directory: {''.join(self.path_stack)}")
                    else:  # It's a file
                        current_path = ''.join(self.path_stack)
                        file_path = current_path + name
                        
                        # Save current position
                        current_pos = f.tell()
                        
                        # Create full path for output
                        rel_path = file_path[1:] if file_path.startswith('/') else file_path  # Remove leading /
                        
                        # Replace any path separators in the filename itself (shouldn't happen, but just in case)
                        rel_path = rel_path.replace('/', os.sep)
                        
                        # Make sure the final path is safe
                        output_file = os.path.normpath(os.path.join(output_dir, rel_path))
                        
                        # Safety check - make sure we're not trying to write outside output_dir
                        # (path traversal protection)
                        if not output_file.startswith(os.path.abspath(output_dir)):
                            self.log(f"Warning: Attempted path traversal detected: {rel_path}")
                            output_file = os.path.join(output_dir, f"unsafe_path_{i}_{os.path.basename(rel_path)}")
                        
                        output_dir_path = os.path.dirname(output_file)
                        
                        # Handle duplicate filenames
                        base_output_file = output_file
                        if output_file in used_filenames:
                            base_name, ext = os.path.splitext(output_file)
                            counter = used_filenames[output_file] + 1
                            used_filenames[output_file] = counter
                            output_file = f"{base_name}_{counter}{ext}"
                            self.log(f"Renamed duplicate: {rel_path} -> {os.path.basename(output_file)}")
                        else:
                            used_filenames[output_file] = 0
                        
                        # Create directory structure
                        try:
                            os.makedirs(output_dir_path, exist_ok=True)
                        except Exception as e:
                            self.log(f"Error creating directory {output_dir_path}: {str(e)}")
                            # Try with a simplified path
                            simple_dir = os.path.join(output_dir, f"recovered_dir_{i}")
                            os.makedirs(simple_dir, exist_ok=True)
                            output_file = os.path.join(simple_dir, f"file_{i}")
                        
                        # Go to file data and extract
                        try:
                            f.seek(offset)
                            data = f.read(size)
                            
                            with open(output_file, 'wb') as out_file:
                                out_file.write(data)
                            
                            self.log(f"Extracted: {rel_path} ({size} bytes)")
                        except Exception as e:
                            self.log(f"Error extracting {rel_path}: {str(e)}")
                        
                        # Adjust offset for next file - align to 0x1000
                        offset += size
                        offset = (offset + 0xFFF) & ~0xFFF  # Equivalent to math OFFSET x 0x1000
                        
                        # Return to entry list
                        f.seek(current_pos)
                
                self.log("Extraction completed successfully")
                return True
                
        except Exception as e:
            self.log(f"Error extracting archive: {str(e)}")
            return False


class ZAPExtractorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("ZAP Archive Extractor")
        self.root.geometry("650x500")
        self.root.resizable(True, True)
        
        # Create extractor
        self.extractor = ZAPExtractor()
        
        # Create GUI elements
        self.create_widgets()

    def create_widgets(self):
        # Frame for input controls
        input_frame = tk.Frame(self.root, padx=10, pady=10)
        input_frame.pack(fill=tk.X)
        
        # Input file selection
        tk.Label(input_frame, text="Input ZAP Archive:").grid(row=0, column=0, sticky=tk.W, pady=5)
        self.input_path = tk.StringVar()
        tk.Entry(input_frame, textvariable=self.input_path, width=50).grid(row=0, column=1, padx=5)
        tk.Button(input_frame, text="Browse", command=self.browse_input).grid(row=0, column=2)
        
        # Output directory selection
        tk.Label(input_frame, text="Output Directory:").grid(row=1, column=0, sticky=tk.W, pady=5)
        self.output_path = tk.StringVar()
        tk.Entry(input_frame, textvariable=self.output_path, width=50).grid(row=1, column=1, padx=5)
        tk.Button(input_frame, text="Browse", command=self.browse_output).grid(row=1, column=2)
        
        # Extract button
        extract_btn = tk.Button(input_frame, text="Extract Files", command=self.start_extraction, bg="#4CAF50", fg="white", padx=10, pady=5)
        extract_btn.grid(row=2, column=0, columnspan=3, pady=10)
        
        # Log frame
        log_frame = tk.Frame(self.root, padx=10, pady=5)
        log_frame.pack(fill=tk.BOTH, expand=True)
        
        tk.Label(log_frame, text="Extraction Log:").pack(anchor=tk.W)
        self.log_text = scrolledtext.ScrolledText(log_frame, height=15)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        
        # Status bar
        self.status_var = tk.StringVar()
        self.status_var.set("Ready")
        status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # Clear log button
        clear_btn = tk.Button(log_frame, text="Clear Log", command=self.clear_log)
        clear_btn.pack(anchor=tk.E, pady=5)

    def clear_log(self):
        self.log_text.delete(1.0, tk.END)

    def browse_input(self):
        path = filedialog.askopenfilename(title="Select ZAP Archive",
                                          filetypes=[("ZAP Archives", "*.zap"), ("All Files", "*.*")])
        if path:
            self.input_path.set(path)

    def browse_output(self):
        path = filedialog.askdirectory(title="Select Output Directory")
        if path:
            self.output_path.set(path)

    def log(self, message):
        self.log_text.insert(tk.END, message + "\n")
        self.log_text.see(tk.END)
        self.root.update_idletasks()

    def start_extraction(self):
        input_path = self.input_path.get()
        output_path = self.output_path.get()
        
        # Validate inputs
        if not input_path or not os.path.isfile(input_path):
            messagebox.showerror("Error", "Invalid input archive path")
            return
        
        if not output_path:
            messagebox.showerror("Error", "Please select an output directory")
            return
        
        # Update status
        self.status_var.set("Extracting files...")
        self.log(f"Starting extraction of {os.path.basename(input_path)}")
        
        # Run extraction in a separate thread to avoid UI freezing
        threading.Thread(target=self.run_extraction, 
                        args=(input_path, output_path), 
                        daemon=True).start()

    def run_extraction(self, input_path, output_path):
        # Run extraction
        success = self.extractor.extract_zap(input_path, output_path, self.log)
        
        # Update UI
        if success:
            self.status_var.set("Extraction completed")
            messagebox.showinfo("Success", "Files have been extracted successfully")
        else:
            self.status_var.set("Extraction failed")
            messagebox.showerror("Error", "Failed to extract files. See log for details.")


if __name__ == "__main__":
    root = tk.Tk()
    app = ZAPExtractorApp(root)
    root.mainloop()

A workaround is, rename the repeat folder to another name.

try this.

python

This kinda works, it does get the rest of the files but it still has the issue of having the wrong data in the wrong file, this one having part of the file list that's originally at the start of the zap archive.

image.thumb.png.c4c9587d8d93c02b9d8d82d8f9b16478.png

  • 2 weeks later...
Posted
On 4/26/2025 at 4:20 AM, ikskoks said:

Did you try to use "Game Extractor"?

Finally got around to trying it and it doesn't work for either PS2 or Xbox ports, just says either "No files were found" or "Selected Plugin was not able to open the archive"

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