ROMM

De Le Wiki de Lug
Aller à la navigation Aller à la recherche

Prérequis

  • Alpine LXC avec Nesting et keyctl activé (Docker).

Ressource confortable :

  • 2 CPU
  • 2Gb de RAM
  • 16GB de Disque

Il est recommandé d'organiser les fichiers de la façon suivante :

/roms/{platform}/
/bios/{platform}/

Il est également nécessaire d'utiliser des noms de dossier reconnu par ROMM, exemple :

roms/
  nes
  snes
  n64
  gb
  gbc
  gba
  genesis
  segacd
  sega32
  neogeomvs
  neo-geo-pocket
  tg16
  pcenginecd
  wonderswan
  wonderswan-color

Metadata

Afin d'obtenir les jaquettes etc il est recommandé de :

* créer un compte chez screenscraper
* créer une clef API chez steamgriddb
* créer une application "twitch developers" (nécessite un mobile) pour certaines focntions, suivre ce tutoriel.

Installation

# apk update && apk upgrade
# apk add docker docker-cli-compose
# rc-update add docker default
# rc-service docker start
# mkdir -p /opt/romm /opt/romm/config /opt/romm/data /opt/romm/assets
# cd /opt/romm

Fichier de configuration du conteneur :

# vi docker-compose.yml
version: "3.8"

volumes:
  mysql_data:
  romm_resources:
  romm_redis_data:

services:
  romm:
    image: ghcr.io/rommapp/romm:latest
    container_name: romm
    restart: unless-stopped
    environment:
      - TZ=Europe/Madrid

      # Database (MariaDB)
      - DB_HOST=romm-db
      - DB_NAME=romm
      - DB_USER=romm-user
      - DB_PASSWD=CHANGE_MOI_ROMM_MARIADB_PASSWORD # mot de passe de "romm-user", identique plus bas!!

      # Required auth secret (IMPORTANT)
      - ROMM_AUTH_SECRET_KEY=CHANGE_MOI_AUTH_SECRET_KEY

      # Metadata providers (optionnel mais recommandé)
      - SCREENSCRAPER_USER=ton_user
      - SCREENSCRAPER_PASSWORD=ton_mdp
      # - RETROACHIEVEMENTS_API_KEY=ta_cle
      - STEAMGRIDDB_API_KEY=ta_cle
      - HASHEOUS_API_ENABLED=true
      - IGDB_CLIENT_ID=idapplitwitch
      - IGDB_CLIENT_SECRET=motdepassapplitwitch

    volumes:
      # Cache / ressources / jobs
      #- romm_resources:/romm/resources
      - /mnt/Emulation/resources:/romm/resources:rw # stockage images, manuels etc..
      - romm_redis_data:/redis-data

      # Ta bibliothèque ROM (lecture seule)
      - /mnt/Emulation/roms:/romm/library/roms:rw # ro pour read-only
      - /mnt/Emulation/bios:/romm/library/bios:rw

      # Assets RomM (écriture)
      - /opt/romm/assets:/romm/assets

      # Config (config.yml)
      - /opt/romm/config:/romm/config:rw

    ports:
      - "8080:8080"

    depends_on:
      romm-db:
        condition: service_healthy

  romm-db:
    image: mariadb:11
    container_name: romm-db
    restart: unless-stopped
    environment:
      - MARIADB_ROOT_PASSWORD=CHANGE_MOI_ROOT_PASSWORD
      - MARIADB_DATABASE=romm
      - MARIADB_USER=romm-user
      - MARIADB_PASSWORD=CHANGE_MOI_ROMM_MARIADB_PASSWORD  # mot de passe de "romm-user", identique plus haut!!
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      start_period: 30s
      interval: 10s
      timeout: 5s
      retries: 10

Fichier de configuration de ROMM :

# vi /opt/romm/config/config.yml
filesystem:
  roms: /romm/library/roms
  bios: /romm/library/bios

scan:
  export_gamelist: true # optionnel
  priority:
    metadata:
      - "ss"
      - "hasheous"

    artwork:
      - "ss"
      - "steamgriddb"

    region:
      - "eu"
      - "us"
      - "jp"

    language:
      - "fr"
      - "en"
      - "es"

  media:
    - box2d
    - screenshot
    - marquee
    - fanart
    - manual
    - title_screen
    - bezel

emulatorjs:
  debug: false
  cache_limit: null
  disable_batch_bootup: false
  disable_auto_unload: false

  netplay:
    enabled: true
    ice_servers:
      - urls: "stun:stun.l.google.com:19302"
      - urls: "stun:stun1.l.google.com:19302"
      - urls: "stun:stun2.l.google.com:19302"
      - urls: "turn:openrelay.metered.ca:80"
        username: "openrelayproject"
        credential: "openrelayproject"
      - urls: "turn:openrelay.metered.ca:443"
        username: "openrelayproject"
        credential: "openrelayproject"
# docker compose up -d

vhost nginx

server {
    listen 80;
    listen [::]:80;
    server_name site.example.net;

    # Force HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name site.example.net;

    # Uploads (RomM peut être "gros")
    client_max_body_size 0;

    # TLS (Certbot)
    ssl_certificate     /etc/letsencrypt/live/site.example.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/site.example.net/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # HSTS (optionnel)
    add_header Strict-Transport-Security "max-age=31536000" always;

    # Sécurité / info
    server_tokens off;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;

    location / {
        # WebSocket + keep-alive
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Headers reverse-proxy
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;

        # Timeouts (scans/transferts longs)
        proxy_read_timeout 3600;
        proxy_send_timeout 3600;

        # Backend RomM
        proxy_pass http://IP_SERVEUR_WEB:8080;

        # Optionnel: si tu as des soucis de buffering
        # proxy_buffering off;
    }
}

convertir les gamelist.xml en metadata.txt

Pour passer de ROMM à Pegasus : lien

Mais le convertisseur officiel ne convertie pas les assets et il faut rajouter à la main les collections etc, on peut utiliser un script local plus optimisé :

# vi /mnt/Media/Media/Emulation/gamelist_to_pegasus.py
#!/usr/bin/env python3

import sys
import xml.etree.ElementTree as ET
from pathlib import Path


ROOT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/mnt/Media/Media/Emulation/roms")

# Mets True si tu veux ajouter automatiquement une commande de lancement par système
ADD_LAUNCH = True

RETROARCH = "retroarch --fullscreen"
CORE_DIR = "/home/retro/.config/retroarch/cores"


def clean(text: str | None) -> str:
    if text is None:
        return ""
    return text.replace("\r", " ").replace("\n", " ").strip()


def escape_metadata_value(text: str) -> str:
    # Pegasus n'aime pas les retours ligne bruts dans les champs
    return clean(text)


def collection_info_from_dir(dirname: str) -> tuple[str, str]:
    slug = dirname.strip().lower()

    mapping = {
        "3do": ("3DO Interactive Multiplayer", "3do"),
        "acpc": ("Amstrad CPC", "acpc"),
        "amiga": ("Amiga", "amiga"),
        "amiga-cd": ("Amiga CD", "amiga-cd"),
        "amiga-cd32": ("Amiga CD32", "amiga-cd32"),
        "apple": ("Apple I", "apple"),
        "appleii": ("Apple II", "appleii"),
        "apple-iigs": ("Apple IIGS", "apple-iigs"),
        "appleiii": ("Apple III", "appleiii"),
        "apple-lisa": ("Apple Lisa", "apple-lisa"),
        "apple-pippin": ("Apple Pippin", "apple-pippin"),
        "arcade": ("Arcade", "arcade"),
        "astrocade": ("Bally Astrocade", "astrocade"),
        "atari2600": ("Atari 2600", "atari2600"),
        "atari5200": ("Atari 5200", "atari5200"),
        "atari7800": ("Atari 7800", "atari7800"),
        "atari8bit": ("Atari 8-bit", "atari8bit"),
        "atari-st": ("Atari ST/STE", "atari-st"),
        "bbcmicro": ("BBC Microcomputer System", "bbcmicro"),
        "c128": ("Commodore 128", "c128"),
        "c16": ("Commodore 16", "c16"),
        "c64": ("Commodore C64/128/MAX", "c64"),
        "c-plus-4": ("Commodore Plus/4", "c-plus-4"),
        "colecoadam": ("Coleco Adam", "colecoadam"),
        "colecovision": ("ColecoVision", "colecovision"),
        "dc": ("Dreamcast", "dc"),
        "dos": ("DOS", "dos"),
        "dragon-32-slash-64": ("Dragon 32/64", "dragon-32-slash-64"),
        "fds": ("Family Computer Disk System", "fds"),
        "famicom": ("Family Computer", "famicom"),
        "fairchild-channel-f": ("Fairchild Channel F", "fairchild-channel-f"),
        "fm-towns": ("FM Towns", "fm-towns"),
        "fm-7": ("FM-7", "fm-7"),
        "gb": ("Game Boy", "gb"),
        "gba": ("Game Boy Advance", "gba"),
        "gbc": ("Game Boy Color", "gbc"),
        "game-dot-com": ("Game.com", "game-dot-com"),
        "gamegear": ("Sega Game Gear", "gamegear"),
        "genesis": ("Sega Mega Drive/Genesis", "genesis"),
        "jaguar": ("Atari Jaguar", "jaguar"),
        "laseractive": ("LaserActive", "laseractive"),
        "linux": ("Linux", "linux"),
        "lynx": ("Atari Lynx", "lynx"),
        "mac": ("Mac", "mac"),
        "mastersystem": ("Sega Master System/Mark III", "sms"),
        "megacd": ("Sega CD", "segacd"),
        "megadrive": ("Sega Mega Drive/Genesis", "genesis"),
        "microvision": ("Microvision", "microvision"),
        "msx": ("MSX", "msx"),
        "msx2": ("MSX2", "msx2"),
        "msx2plus": ("Microsoft MSX2+", "msx2plus"),
        "msx-turbo": ("MSX Turbo R", "msx-turbo"),
        "n64": ("Nintendo 64", "n64"),
        "64dd": ("Nintendo 64DD", "64dd"),
        "nds": ("Nintendo DS", "nds"),
        "neogeoaes": ("Neo Geo AES", "neogeoaes"),
        "neogeomvs": ("Neo Geo MVS", "neogeomvs"),
        "neogeo": ("Neo Geo AES", "neogeoaes"),
        "neo-geo-cd": ("Neo Geo CD", "neo-geo-cd"),
        "neo-geo-pocket": ("Neo Geo Pocket", "neo-geo-pocket"),
        "neo-geo-pocket-color": ("Neo Geo Pocket Color", "neo-geo-pocket-color"),
        "nes": ("Nintendo Entertainment System", "nes"),
        "new-nintendo-3ds": ("New Nintendo 3DS", "new-nintendo-3ds"),
        "ngage": ("N-Gage", "ngage"),
        "ngc": ("Nintendo GameCube", "ngc"),
        "odyssey": ("Magnavox Odyssey", "odyssey"),
        "odyssey-2": ("Odyssey 2", "odyssey-2"),
        "openbor": ("OpenBOR", "openbor"),
        "oric": ("Oric", "oric"),
        "palm-os": ("Palm OS", "palm-os"),
        "pc-fx": ("PC-FX", "pc-fx"),
        "pc-jr": ("IBM PCjr", "pc-jr"),
        "pc-booter": ("PC Booter", "pc-booter"),
        "pcengine": ("TurboGrafx-16/PC Engine", "tg16"),
        "pcenginecd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
        "pcecd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
        "philips-cd-i": ("Philips CD-i", "philips-cd-i"),
        "pico": ("PICO", "pico"),
        "pokemon-mini": ("Pokémon mini", "pokemon-mini"),
        "psx": ("PlayStation", "psx"),
        "ps1": ("PlayStation", "psx"),
        "ps2": ("PlayStation 2", "ps2"),
        "ps3": ("PlayStation 3", "ps3"),
        "ps4": ("PlayStation 4", "ps4"),
        "ps5": ("PlayStation 5", "ps5"),
        "psp": ("PlayStation Portable", "psp"),
        "psvita": ("PlayStation Vita", "psvita"),
        "sam-coupe": ("SAM Coupé", "sam-coupe"),
        "satellaview": ("Satellaview", "satellaview"),
        "saturn": ("Sega Saturn", "saturn"),
        "sc3000": ("Sega SC-3000", "sc3000"),
        "scummvm": ("ScummVM", "scummvm"),
        "sega-pico": ("Sega Pico", "sega-pico"),
        "sega32": ("Sega 32X", "sega32"),
        "segacd": ("Sega CD", "segacd"),
        "segacd32": ("Sega CD 32X", "segacd32"),
        "sg1000": ("SG-1000", "sg1000"),
        "sharp-x68000": ("Sharp X68000", "sharp-x68000"),
        "sms": ("Sega Master System/Mark III", "sms"),
        "snes": ("Super Nintendo Entertainment System", "snes"),
        "sfam": ("Super Famicom", "sfam"),
        "super-acan": ("Super A'Can", "super-acan"),
        "supergrafx": ("PC Engine SuperGrafx", "supergrafx"),
        "supervision": ("Watara/QuickShot Supervision", "supervision"),
        "switch": ("Nintendo Switch", "switch"),
        "switch-2": ("Nintendo Switch 2", "switch-2"),
        "tg16": ("TurboGrafx-16/PC Engine", "tg16"),
        "turbografx-cd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
        "ti-99": ("Texas Instruments TI-99", "ti-99"),
        "ti-994a": ("TI-99/4A", "ti-994a"),
        "trs-80": ("TRS-80", "trs-80"),
        "trs-80-color-computer": ("TRS-80 Color Computer", "trs-80-color-computer"),
        "vb": ("Virtual Boy", "virtualboy"),
        "vectrex": ("Vectrex", "vectrex"),
        "vic-20": ("Commodore VIC-20", "vic-20"),
        "virtualboy": ("Virtual Boy", "virtualboy"),
        "vmu": ("Sega Dreamcast VMU", "vmu"),
        "wii": ("Wii", "wii"),
        "wiiu": ("Wii U", "wiiu"),
        "win": ("Windows", "win"),
        "win3x": ("Windows 3.x", "win3x"),
        "windows-apps": ("Windows Apps", "windows-apps"),
        "wonderswan": ("WonderSwan", "wonderswan"),
        "wonderswan-color": ("WonderSwan Color", "wonderswan-color"),
        "x1": ("Sharp X1", "x1"),
        "xavixport": ("XaviXPORT", "xavixport"),
        "xbox": ("Xbox", "xbox"),
        "xbox360": ("Xbox 360", "xbox360"),
        "xboxone": ("Xbox One", "xboxone"),
        "series-x-s": ("Xbox Series X/S", "series-x-s"),
        "z-machine": ("Z-machine", "z-machine"),
        "z88": ("Cambridge Computer Z88", "z88"),
        "zinc": ("ZiNc", "zinc"),
        "zx80": ("ZX80", "zx80"),
        "zx81": ("Sinclair ZX81", "zx81"),
        "zxs": ("ZX Spectrum", "zxs"),
        "zx-spectrum-next": ("ZX Spectrum Next", "zx-spectrum-next"),
    }

    if slug in mapping:
        return mapping[slug]

    pretty = slug.replace("-", " ").replace("_", " ").title()
    return pretty, slug


def launch_for_shortname(shortname: str) -> str | None:
    launches = {
        "arcade": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "Modèle:File.path"',
        "neogeoaes": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "Modèle:File.path"',
        "neogeomvs": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "Modèle:File.path"',
        "neo-geo-cd": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "Modèle:File.path"',
        "genesis": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
        "segacd": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
        "segacd32": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "Modèle:File.path"',
        "sega32": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "Modèle:File.path"',
        "sms": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
        "sg1000": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
        "gamegear": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
        "tg16": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "Modèle:File.path"',
        "turbografx-cd": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "Modèle:File.path"',
        "supergrafx": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "Modèle:File.path"',
        "nes": f'{RETROARCH} -L {CORE_DIR}/nestopia_libretro.so "Modèle:File.path"',
        "snes": f'{RETROARCH} -L {CORE_DIR}/snes9x_libretro.so "Modèle:File.path"',
        "sfam": f'{RETROARCH} -L {CORE_DIR}/snes9x_libretro.so "Modèle:File.path"',
        "gb": f'{RETROARCH} -L {CORE_DIR}/gambatte_libretro.so "Modèle:File.path"',
        "gbc": f'{RETROARCH} -L {CORE_DIR}/gambatte_libretro.so "Modèle:File.path"',
        "gba": f'{RETROARCH} -L {CORE_DIR}/mgba_libretro.so "Modèle:File.path"',
        "psx": f'{RETROARCH} -L {CORE_DIR}/pcsx_rearmed_libretro.so "Modèle:File.path"',
        "saturn": f'{RETROARCH} -L {CORE_DIR}/beetle_saturn_libretro.so "Modèle:File.path"',
        "dc": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "Modèle:File.path"',
        "n64": f'{RETROARCH} -L {CORE_DIR}/mupen64plus_next_libretro.so "Modèle:File.path"',
        "nds": f'{RETROARCH} -L {CORE_DIR}/melonds_libretro.so "Modèle:File.path"',
        "ngc": f'{RETROARCH} -L {CORE_DIR}/dolphin_libretro.so "Modèle:File.path"',
        "wii": f'{RETROARCH} -L {CORE_DIR}/dolphin_libretro.so "Modèle:File.path"',
        "virtualboy": f'{RETROARCH} -L {CORE_DIR}/mednafen_vb_libretro.so "Modèle:File.path"',
        "lynx": f'{RETROARCH} -L {CORE_DIR}/handy_libretro.so "Modèle:File.path"',
        "jaguar": f'{RETROARCH} -L {CORE_DIR}/virtualjaguar_libretro.so "Modèle:File.path"',
        "wonderswan": f'{RETROARCH} -L {CORE_DIR}/mednafen_wswan_libretro.so "Modèle:File.path"',
        "wonderswan-color": f'{RETROARCH} -L {CORE_DIR}/mednafen_wswan_libretro.so "Modèle:File.path"',
        "msx": f'{RETROARCH} -L {CORE_DIR}/fmsx_libretro.so "Modèle:File.path"',
        "msx2": f'{RETROARCH} -L {CORE_DIR}/fmsx_libretro.so "Modèle:File.path"',
        "amiga": f'{RETROARCH} -L {CORE_DIR}/puae_libretro.so "Modèle:File.path"',
        "amiga-cd32": f'{RETROARCH} -L {CORE_DIR}/puae_libretro.so "Modèle:File.path"',
        "scummvm": f'{RETROARCH} -L {CORE_DIR}/scummvm_libretro.so "Modèle:File.path"',
    }
    return launches.get(shortname)


def detect_extensions(games: list[ET.Element]) -> list[str]:
    exts = set()

    for game in games:
        path = clean(game.findtext("path"))
        if not path:
            continue
        suffix = Path(path).suffix.lower().lstrip(".")
        if suffix:
            exts.add(suffix)

    return sorted(exts)


def write_line(out, key: str, value: str | None):
    value = escape_metadata_value(value or "")
    if value:
        out.write(f"{key}: {value}\n")


def convert_gamelist(gamelist_path: Path):
    romdir = gamelist_path.parent
    dirname = romdir.name
    out_path = romdir / "metadata.pegasus.txt"

    try:
        tree = ET.parse(gamelist_path)
        root = tree.getroot()
    except Exception as e:
        print(f"[ERR] {gamelist_path}: {e}")
        return

    games = root.findall("game")
    if not games:
        print(f"[WARN] {gamelist_path}: no <game> entries")
        return

    collection, shortname = collection_info_from_dir(dirname)
    exts = detect_extensions(games)

    with out_path.open("w", encoding="utf-8") as out:
        out.write(f"collection: {collection}\n")
        out.write(f"shortname: {shortname}\n")
        if exts:
            out.write(f"extensions: {' '.join(exts)}\n")

        if ADD_LAUNCH:
            launch = launch_for_shortname(shortname)
            if launch:
                out.write(f"launch: {launch}\n")

        out.write("\n")

        for game in games:
            name = clean(game.findtext("name"))
            path = clean(game.findtext("path"))
            if not path:
                continue

            if path.startswith("./"):
                path = path[2:]

            out.write(f"game: {name or Path(path).stem}\n")
            out.write(f"file: {path}\n")

            write_line(out, "description", game.findtext("desc"))
            write_line(out, "developer", game.findtext("developer"))
            write_line(out, "publisher", game.findtext("publisher"))
            write_line(out, "genre", game.findtext("genre"))

            releasedate = clean(game.findtext("releasedate"))
            if releasedate:
                if len(releasedate) >= 8 and releasedate[:8].isdigit():
                    rd = releasedate[:8]
                    out.write(f"release: {rd[:4]}-{rd[4:6]}-{rd[6:8]}\n")
                else:
                    out.write(f"release: {releasedate}\n")

            rating = clean(game.findtext("rating"))
            if rating:
                out.write(f"rating: {rating}\n")

            thumbnail = clean(game.findtext("thumbnail"))
            image = clean(game.findtext("image"))
            screenshot = clean(game.findtext("screenshot"))
            fanart = clean(game.findtext("fanart"))
            video = clean(game.findtext("video"))

            # jaquette
            if thumbnail:
                out.write(f"assets.box_front: {thumbnail}\n")
            elif image:
                out.write(f"assets.box_front: {image}\n")

            if screenshot:
                out.write(f"assets.screenshot: {screenshot}\n")
            if fanart:
                out.write(f"assets.background: {fanart}\n")
            if video:
                out.write(f"assets.video: {video}\n")

            out.write("\n")

    print(f"[OK] {gamelist_path} -> {out_path} ({len(games)} games)")


def main():
    gamelists = list(ROOT.rglob("gamelist.xml"))
    if not gamelists:
        print(f"[WARN] No gamelist.xml found under {ROOT}")
        return

    for gamelist in sorted(gamelists):
        convert_gamelist(gamelist)


if __name__ == "__main__":
    main()
# chmod +x /mnt/Media/Media/Emulation/gamelist_to_pegasus.py

Pour l'exécuter :

# python3 /mnt/Media/Media/Emulation/gamelist_to_pegasus.py /mnt/Media/Media/Emulation/roms