Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Last active April 27, 2025 02:39
Show Gist options
  • Save ssokolow/3fd6744f6af428c477b0f1b11b35cdac to your computer and use it in GitHub Desktop.
Save ssokolow/3fd6744f6af428c477b0f1b11b35cdac to your computer and use it in GitHub Desktop.
Script to automate adding ScummVM games to EmulationStation Desktop Edition
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# prep_scummvm_games.py
# Copyright 2025 Stephan Sokolow
#
# Helper for batch-adding ScummVM games to ES-DE.
#
# License: MIT (https://opensource.org/license/mit)
#
# Usage:
# 0. If you set a non-default ROMs directory, edit ESDE_SCUMMVM_ROMS_PATH
# 1. Copy game folders of the form ScummVM expects into ESDE_SCUMMVM_ROMS_PATH
# 2. Run standalone ScummVM's "Mass Add..." on ESDE_SCUMMVM_ROMS_PATH to
# add the required metadata to scummvm.ini
# 3. Run this script to read the metadata out of scummvm.ini and fix up the
# folders so ES-DE can use them.
#
# Note: In order to avoid the need to manually prune away demos and spurious
# results that ScummVM's "Mass Add.." will find inside certain games,
# this will ignore game folders nested inside other game folders.
# However, GOG.com intentionally does that with some games, where the CD
# version is ../GameName/ and the floppy version is ../GameName/FDD/.
#
# If you want the GOG.com floppy versions included, just move them so
# they're directly in ESDE_SCUMMVM_ROMS_PATH before running this script.
# (i.e. Beside the CD versions instead of inside of them)
import io, os, sys # NOQA
from configparser import ConfigParser
from typing import Optional
# Default path for ES-DE folder for ScummVM games
ESDE_SCUMMVM_ROMS_PATH = os.path.join(
os.path.expanduser("~"), 'ROMs', 'scummvm')
def get_scummvm_ini_path() -> Optional[str]:
"""Find a valid path for a `scummvm.ini` file or return None
Raises:
- KeyError: Environment variable which should always be set is missing
- NotImplementedError: Not running under a supported OS
References used:
https://docs.scummvm.org/en/latest/advanced_topics/configuration_file.html
"""
if os.name == 'nt': # Windows
# NOTE: Intentionally not bothering to support the Win9x ScummVM port
path = os.path.join(
os.environ['APPDATA'], 'ScummVM', 'scummvm.ini')
elif sys.platform == 'darwin': # macOS
path = os.path.expanduser(
"~/Library/Preferences/ScummVM Preferences")
elif os.name == 'posix': # Linux/*BSD
for path in (
os.path.expanduser( # Flatpak
"~/.var/app/org.scummvm.ScummVM/config/scummvm/scummvm.ini"),
os.path.expanduser( # Snap
"~/snap/scummvm/current/.config/scummvm/scummvm.ini"),
os.path.join( # Other
os.environ.get('XDG_CONFIG_HOME',
os.path.expanduser('~/.config')),
'scummvm/scummvm.ini')
):
if os.path.exists(path):
return path
else:
raise NotImplementedError("Unspported platform")
if os.path.exists(path):
return path
return None
def fixup_game(game: str, config: ConfigParser, esde_scummvm_dir: str) -> bool:
"""Rename game folder if needed and augment with metadata from scummvm.ini
Will update ``config`` to point to the new path.
Returns a boolean indicating whether ``config`` now has unsaved changes.
"""
changed = False
# Skip ScummVM's global configuration settings
if game == "scummvm":
return False
# Get and normalize the game's path for reliable comparison
path = os.path.realpath(config[game]['path'])
if not os.path.exists(path):
print("Path does not exist. Skipping:", path)
return False
# Exclude games not directly in the ES-DE ROMs dir for ScummVM
if not os.path.realpath(os.path.dirname(path)) == esde_scummvm_dir:
print("Not in ES-DE ROM dir. Skipping:", path)
return False
# If the folder doesn't end in .scummvm, rename it and fix up the ScummVM
# config file so the '.scummvm' file and its containing folder will
# collapse together in ES-DE
scummvm_ext = ".scummvm"
game_dir_name = os.path.basename(path)
if not game_dir_name.lower().endswith(scummvm_ext):
new_path = path + scummvm_ext
# Don't overwrite a pre-existing target path
if os.path.exists(path) and not os.path.exists(new_path):
print("Renaming %r -> %r" % (path, new_path))
os.rename(path, new_path)
else:
print("WARNING: Not renaming %r -> %r" % (path, new_path))
game_dir_name += scummvm_ext
path = new_path
# Copy changed path into the in-memory copy of scummvm.ini
config[game]['path'] = path
changed = True
# Delete any existing .scummvm files inside the game folder
for fname in os.listdir(path):
if fname.lower().endswith('.scummvm'):
fpath = os.path.join(path, fname)
print("Removing %r" % fpath)
os.remove(os.path.join(path, fname))
# Create the new .scummvm file
fpath = os.path.join(path, game_dir_name)
print("Creating %r" % fpath)
with open(fpath, 'w') as fobj:
fobj.write(config[game]['gameid'])
return changed
def main() -> None:
# Normalize path to ES-DE ROMs folder for reliable comparison
esde_scummvm_dir = os.path.realpath(ESDE_SCUMMVM_ROMS_PATH)
if not os.path.isdir(esde_scummvm_dir):
print("Could not find %r or is not a directory. "
"Quitting." % esde_scummvm_dir)
sys.exit(1)
scummvm_ini_path = get_scummvm_ini_path()
if scummvm_ini_path and os.path.exists(scummvm_ini_path):
print("Found scummvm.ini:", scummvm_ini_path)
else:
print("Could not find %r. Quitting." % scummvm_ini_path)
sys.exit(1)
# Parse ScummVM's config file to extract scraped games
config = ConfigParser()
with io.open(scummvm_ini_path, mode='r', encoding="utf8") as fobj:
config.read_file(fobj)
# Do the actual fixups
changed = any(fixup_game(game, config, esde_scummvm_dir)
for game in config.sections())
# Write any changed paths to on-disk copy of scummvm.ini
if changed:
with io.open(scummvm_ini_path + ".tmp", mode='w', encoding="utf8") as fobj:
config.write(fobj)
# Almost-atomic replace since Windows doesn't allow os.rename to
# overwrite existing files
os.remove(scummvm_ini_path)
os.rename(scummvm_ini_path + ".tmp", scummvm_ini_path)
# Only run main() if not being import-ed (eg. by a unit test suite)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment