Skip to content

Instantly share code, notes, and snippets.

@nimaid
Created September 30, 2026 20:32
Show Gist options
  • Select an option

  • Save nimaid/bae6a12cd24a551aae258a3bd7bee4c5 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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:])
#!/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