Hell Inspector Posted April 26 Posted April 26 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. 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 Rabatini Posted April 26 Engineers Posted April 26 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
Hell Inspector Posted April 26 Author Posted April 26 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.
Hell Inspector Posted May 11 Author Posted May 11 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"
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