Skip to content

Instantly share code, notes, and snippets.

@sgtlaggy
Last active June 9, 2026 06:24
Show Gist options
  • Select an option

  • Save sgtlaggy/d209703236f96831bf3c7fda570849a4 to your computer and use it in GitHub Desktop.

Select an option

Save sgtlaggy/d209703236f96831bf3c7fda570849a4 to your computer and use it in GitHub Desktop.
Unpack SPT mods that are packed with explicit backslash paths, resulting in a flat list of files.
#!/usr/bin/env python3
import argparse
import subprocess
import sys
import zipfile
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
SCRIPT_IN_SPT = (SCRIPT_DIR / "EscapeFromTarkov.exe").exists()
PARSER = argparse.ArgumentParser(description="Unpack bad-behaving SPT mods.")
PARSER.add_argument(
"-o",
"--output-dir",
default=SCRIPT_DIR if SCRIPT_IN_SPT else None,
type=Path,
help="Path to folder that contains EscapeFromTarkov.exe.",
)
PARSER.add_argument(
"-M",
"--no-move",
action="store_true",
help="Leave extracted files in `output-dir/tmp`.",
)
PARSER.add_argument("zip", nargs="+")
ARGS = PARSER.parse_args()
def main(): # noqa: C901
for arg in ARGS.zip:
fp = Path(arg)
if not fp.exists():
print(f"Error: {arg} does not exist.", file=sys.stderr)
continue
out: Path | None = ARGS.output_dir
if out is None:
out = fp.with_suffix("")
n = 0
while out.exists():
n += 1
out = fp.with_suffix(f".{n}")
tmp = out / "tmp"
match fp.suffix.casefold():
case ".zip":
try:
unzip(fp, tmp)
except Exception as e:
print(e, file=sys.stderr)
case ".7z" | ".rar":
try:
subprocess.run( # noqa: S603
["7z", "x", f"-o{tmp}", fp], # noqa: S607
stdout=subprocess.DEVNULL,
)
except Exception as e:
print(e, file=sys.stderr)
if not ARGS.output_dir:
cleanup_tmp(tmp)
if ARGS.output_dir:
cleanup_tmp(tmp)
def unzip(fp: Path, out: Path):
with zipfile.ZipFile(fp) as zf:
for member in zf.namelist():
# ignore directories
if member.endswith("\\"):
continue
zf.extract(member, out)
# clean mod, should have extracted normally
if "\\" not in member:
continue
def cleanup_tmp(tmp: Path):
fix_backslashes_in(tmp)
fix_casing(tmp)
if ARGS.no_move:
return
unpack_tmp(tmp)
def fix_backslashes_in(tmp: Path):
for fp in tmp.iterdir():
if "\\" not in fp.name:
continue
if fp.is_dir():
try_rmdir(fp)
continue
elif fp.name.endswith("\\"):
fp.unlink()
continue
fixed = tmp / fp.name.replace("\\", "/")
fixed.parent.mkdir(parents=True, exist_ok=True)
fp.move(fixed)
def fix_casing(tmp: Path):
_fix_casing(tmp, ["BepInEx", "SPT"])
bepinex = tmp / "BepInEx"
if bepinex.exists():
_fix_casing(bepinex, ["patchers", "plugins"])
spt = tmp / "SPT"
if not spt.exists():
return
_fix_casing(spt, ["user", "SPT_Data"])
user = spt / "user"
if user.exists():
_fix_casing(user, ["mods"])
# never seen a mod use this, but supposedly it’s a valid case
data = spt / "SPT_Data"
if data.exists():
_fix_casing(data, ["Launcher"])
def _fix_casing(root: Path, dir_names: list[str]):
for fp in root.iterdir():
for name in dir_names:
if is_wrong_case(fp.name, name):
movetree(fp, root / name)
def is_wrong_case(string: str, reference: str) -> bool:
return string != reference and string.casefold() == reference.casefold()
def unpack_tmp(tmp: Path):
known_dirs = {"BepInEx", "SPT", "EscapeFromTarkov_Data"}
for fp in tmp.iterdir():
if fp.is_dir() and fp.name in known_dirs:
try:
movetree(fp, tmp.parent / fp.name)
except Exception as e:
print(e, file=sys.stderr)
elif fp.suffix.casefold() == ".exe":
fp.move_into(tmp.parent)
try_rmdir(tmp)
def movetree(src: Path, dst: Path):
if not dst.exists():
src.move(dst)
return
for fp in src.iterdir():
new_fp = dst / fp.relative_to(src)
if fp.is_dir():
movetree(fp, new_fp)
try_rmdir(fp)
else:
new_fp.parent.mkdir(parents=True, exist_ok=True)
fp.move(new_fp)
try_rmdir(src)
def try_rmdir(fp: Path):
try:
fp.rmdir()
except FileNotFoundError:
pass
except Exception as e:
print(e, file=sys.stderr)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment