Created
September 30, 2026 20:32
-
-
Save nimaid/bae6a12cd24a551aae258a3bd7bee4c5 to your computer and use it in GitHub Desktop.
Extracts minimal ScummVM game files from Windows ISO files of Humongous Entertainment games.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| import os | |
| import sys | |
| import shutil | |
| from pathlib import Path | |
| import argparse | |
| from fnmatch import fnmatch | |
| import tempfile | |
| import posixpath | |
| import pycdlib | |
| def enumerate_wildcards(strings, wildcard="?"): | |
| output = [] | |
| for s in strings: | |
| digit_count = s.count(wildcard) | |
| if digit_count > 0: | |
| digits_list = [] | |
| for n in range(pow(10, digit_count)): | |
| digits = f"{n:0{digit_count}d}" | |
| new_item = s | |
| for i in range(digit_count): | |
| new_item = new_item.replace(wildcard, digits[i], 1) | |
| output.append(new_item) | |
| else: | |
| output.append(s) | |
| return output | |
| def find_first_non_redundant_path_level(path_in): | |
| path_in = Path(path_in) | |
| while True: | |
| subdirs = [p for p in path_in.iterdir() if p.is_dir()] | |
| files = [p for p in path_in.iterdir() if p.is_file()] | |
| if len(subdirs) != 1 or len(files) > 0: | |
| return path_in | |
| path_in = subdirs[0] | |
| def extract_he_scumm_iso_to_tempdir(iso_path): | |
| iso_path = Path(iso_path) | |
| excluded_folders = [ | |
| "*demo", | |
| "*demos", | |
| "bc*dem", | |
| "catalog", | |
| "catalog?", | |
| "setup", | |
| "system", | |
| "dosgame", | |
| "drivers", | |
| ] | |
| excluded_folders = enumerate_wildcards(excluded_folders) | |
| excluded_files = [ | |
| "os.dat", | |
| "lang.dat", | |
| "h20-con2.dat", | |
| ] | |
| required_files = [ | |
| "*.(a)", | |
| "*.(b)", | |
| "*.(?)", | |
| "*.he?", | |
| "*.c??", | |
| "*.drv", | |
| "*.pal", | |
| "*.tlk", | |
| "*.tan", | |
| "*.sng", | |
| "*.brs", | |
| "*.oss", | |
| "*.map", | |
| "*.obj", | |
| "*.wiz", | |
| "*.da2", | |
| "*.cof", | |
| "*.thm", | |
| "*.map", | |
| "*.cmp", | |
| "*.ot?", | |
| "*.dat", | |
| "*.bre", | |
| "map.ini", | |
| "moonbase.u32", | |
| ] | |
| required_files = enumerate_wildcards(required_files) | |
| temp_dir = Path(tempfile.mkdtemp()) | |
| iso = pycdlib.PyCdlib() | |
| iso.open(iso_path) | |
| has_joliet = iso.has_joliet() | |
| try: | |
| for root, dirs, files in (iso.walk(joliet_path="/") if has_joliet else iso.walk(iso_path="/")): | |
| for file in files: | |
| folder_is_excluded = False | |
| for folder in root.split("/"): | |
| for excluded_folder in excluded_folders: | |
| if fnmatch(folder.lower(), excluded_folder): | |
| folder_is_excluded = True | |
| break | |
| if folder_is_excluded: | |
| break | |
| if folder_is_excluded: | |
| continue | |
| iso_file_path = posixpath.join(root, file) | |
| clean_name = file.split(";")[0] | |
| match_name = clean_name.lower() | |
| file_is_required = False | |
| for required_file in required_files: | |
| if fnmatch(match_name, required_file): | |
| file_is_required = True | |
| break | |
| for excluded_file in excluded_files: | |
| if fnmatch(match_name, excluded_file): | |
| file_is_required = False | |
| break | |
| if file_is_required == False: | |
| continue | |
| full_name = Path(os.path.join(root.lstrip("/"), clean_name)) | |
| local_file_path = Path(os.path.join(temp_dir, full_name)) | |
| print(f"{iso_path.name} -> \"{full_name}\"") | |
| os.makedirs(os.path.dirname(local_file_path), exist_ok=True) | |
| with open(local_file_path, "wb") as f: | |
| if has_joliet: | |
| iso.get_file_from_iso_fp(f, joliet_path=iso_file_path) | |
| else: | |
| iso.get_file_from_iso_fp(f, iso_path=iso_file_path) | |
| finally: | |
| iso.close() | |
| first_non_redundant_path = find_first_non_redundant_path_level(temp_dir) | |
| if not first_non_redundant_path.samefile(temp_dir): | |
| print("WARNING: Redundant directories detected. Moving files to main directory.") | |
| new_temp_dir = Path(tempfile.mkdtemp()) | |
| shutil.rmtree(new_temp_dir) | |
| first_non_redundant_path.rename(new_temp_dir) | |
| shutil.rmtree(temp_dir) | |
| temp_dir = new_temp_dir | |
| return temp_dir | |
| def zip_folder(target_folder_path, output_zip_path, output_zip_name=None): | |
| if output_zip_name == None: | |
| output_zip_name = Path(target_folder_path).name | |
| output_zip_pathname = os.path.join(output_zip_path, output_zip_name) | |
| full_path = Path(output_zip_pathname + os.path.extsep + "zip") | |
| if full_path.is_file(): | |
| print("WARNING: Overwriting existing file...") | |
| os.remove(full_path) | |
| shutil.make_archive(output_zip_pathname, "zip", target_folder_path) | |
| return full_path | |
| def strip_he_scumm_iso(iso_path, output_dir, zip_files=False): | |
| iso_path = Path(iso_path).resolve() | |
| output_dir = Path(output_dir).resolve() | |
| print(f"Extracting ScummVM resources from \"{iso_path.name}\"...") | |
| temp_dir = extract_he_scumm_iso_to_tempdir(iso_path) | |
| print("Done.") | |
| print() | |
| if zip_files: | |
| print("Zipping files...") | |
| output_zip_file = zip_folder(temp_dir, output_dir, iso_path.stem) | |
| print(f"Done. Files added to \"{output_zip_file}\".") | |
| print() | |
| print(f"Deleting temporary directory \"{temp_dir}\"...") | |
| shutil.rmtree(temp_dir) | |
| print("Done.") | |
| else: | |
| print("Moving files...") | |
| final_output_dir = Path(os.path.join(output_dir, iso_path.stem)) | |
| if final_output_dir.is_dir(): | |
| print("WARNING: Directory already exists, overwriting...") | |
| shutil.rmtree(final_output_dir) | |
| final_output_dir.parent.mkdir(parents=True, exist_ok=True) | |
| temp_dir.rename(final_output_dir) | |
| print(f"Done. Files moved to \"{final_output_dir}\".") | |
| def parse_args(args): | |
| parser = argparse.ArgumentParser( | |
| description=f"Strips all extraneous files from a Humungous Entertainment SCUMM-based game ISO (in preperation for use in ScummVM), and optionally add them to a ZIP file.", | |
| formatter_class=argparse.RawDescriptionHelpFormatter | |
| ) | |
| parser.add_argument("-i", "--iso", dest="iso", type=str, required=True, | |
| help=f"the Humungous Entertainment SCUMM-based game ISO" | |
| ) | |
| parser.add_argument("-o", "--output", dest="output_dir", type=str, required=True, | |
| help=f"the directory to save the output files to" | |
| ) | |
| parser.add_argument("-z", "--zip", dest="zip", action="store_true", | |
| help=f"add the files to a ZIP file after extraction" | |
| ) | |
| parsed_args = parser.parse_args(args) | |
| # Interpret string arguments | |
| iso = Path(parsed_args.iso).resolve() | |
| if not iso.is_file(): | |
| parser.error(f"the file \"{iso}\" does not exist") | |
| if iso.suffix.lower() != ".iso": | |
| parser.error(f"the file \"{iso}\" is not a valid ISO file") | |
| parsed_args.iso = iso | |
| output_dir = Path(parsed_args.output_dir).resolve() | |
| try: | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| except OSError: | |
| parser.error(f"\"{parsed_args.output_dir}\" is not a valid directory name") | |
| parsed_args.output_dir = output_dir | |
| return parsed_args | |
| def main(args): | |
| print() | |
| parsed_args = parse_args(args) | |
| strip_he_scumm_iso(parsed_args.iso, parsed_args.output_dir, zip_files=parsed_args.zip) | |
| if __name__ == "__main__": | |
| main(sys.argv[1:]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| import os | |
| import sys | |
| from pathlib import Path | |
| import argparse | |
| import strip_he_scumm_game | |
| def parse_args(args): | |
| parser = argparse.ArgumentParser( | |
| description=f"Recursively strips all extraneous files from Humungous Entertainment SCUMM-based game ISOs in a directory (in preperation for use in ScummVM), and optionally adds them to ZIP files.", | |
| formatter_class=argparse.RawDescriptionHelpFormatter | |
| ) | |
| parser.add_argument("-i", "--input", dest="input_dir", type=str, required=True, | |
| help=f"the folder with Humungous Entertainment SCUMM-based game ISOs" | |
| ) | |
| parser.add_argument("-o", "--output", dest="output_dir", type=str, required=True, | |
| help=f"the folder to save the output ZIP files to" | |
| ) | |
| parser.add_argument("-z", "--zip", dest="zip", action="store_true", | |
| help=f"add the files to a ZIP file after extraction" | |
| ) | |
| parsed_args = parser.parse_args(args) | |
| # Interpret string arguments | |
| input_dir = Path(parsed_args.input_dir).resolve() | |
| if not input_dir.is_dir(): | |
| parser.error(f"the directory \"{parsed_args.input_dir}\" does not exist") | |
| parsed_args.input_dir = input_dir | |
| output_dir = Path(parsed_args.output_dir).resolve() | |
| try: | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| except OSError: | |
| parser.error(f"\"{parsed_args.output_dir}\" is not a valid directory name") | |
| parsed_args.output_dir = output_dir | |
| return parsed_args | |
| def main(args): | |
| print() | |
| parsed_args = parse_args(args) | |
| iso_files = list(parsed_args.input_dir.rglob("*.iso")) | |
| print(f"Processing {len(iso_files)} ISO files...") | |
| for iso_path in iso_files: | |
| relative_path = iso_path.relative_to(parsed_args.input_dir) | |
| print() | |
| print(f"-------- {relative_path} --------") | |
| zip_path = Path(os.path.join(parsed_args.output_dir, relative_path.parent)) | |
| zip_path.mkdir(parents=True, exist_ok=True) | |
| strip_he_scumm_game.strip_he_scumm_iso(iso_path, zip_path, zip_files=parsed_args.zip) | |
| print() | |
| print("Finished processing all ISO files.") | |
| if __name__ == "__main__": | |
| main(sys.argv[1:]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment