Agent Skills for Batocera

Agent Skills for Batocera

Over the past year I've been using coding agents (mostly Claude Code) to help setup, maintain, and customize my cabinet. Which is how I learned coding agents lack knowledge about Batocera...

So we packaged our learnings up as batocera-skills: a free, MIT-licensed collection of Agent Skills (the AGENTS.md/SKILL.md format) that teaches an agent how to actually operate a Batocera box.

Repo: https://github.com/t3chnaztea/batocera-skills

There are five skills, each covering one area:

  • batocera-ops: the foundation. The read-only filesystem model, config and log paths, a safety doctrine, and a remote verify loop
  • batocera-roms: why a game won't show up, hide-don't-delete curation, multi-disc cleanup, dump verification against No-Intro/Redump
  • batocera-display: setting shaders and bezel artwork
  • batocera-tuning: laggy frontend diagnosis, the governor-stuck-in-powersave bug, overclock scoping, input latency
  • batocera-maintenance: backups over SSH, surviving version upgrades

It's all original prose from actually running a cabinet (v41 through v43, x86_64), not a scrape of the wiki. The wiki stays the canonical manual; this covers the stuff the wiki doesn't tell you.

If you use Claude Code, it installs as a plugin in two commands:

/plugin marketplace add t3chnaztea/batocera-skills
/plugin install batocera@t3chnaztea

But the skills are plain Markdown with standard Agent Skills frontmatter, so they work with any harness that reads SKILL.md files. You can also just clone the repo and copy the folders in.

Fair warning before you install: these skills direct an agent to SSH into your cabinet as root. Read the skills first (they're just Markdown), and please change Batocera's default root/linux password if you haven't. Everything is designed non-destructive-first (hide don't delete, back up before edit, dry-run flags on anything that can remove files), but it's your cabinet and your risk.

If you've got your own hard-won Batocera lessons, there's a contribution template in the repo. PRs welcome.

Not affiliated with the Batocera project, just a fan.

u/itsk2049 — 1 day ago

Gamepad-friendly "Toolbox" for Batocera

A few chores kept dragging me back to a keyboard and an SSH session: backing up saves before an OS upgrade, figuring out why a game won't boot (usually BIOS), spotting systems with missing scrape data, applying the right CRT shader to the right systems... so I shoved all that onto the screen as a single Port you drive with a gamepad or joystick.

Multiple modules in one text UI app:

Backup/Restore is plain rsync over SSH to your NAS, push-only, never removes files. Restore previews with a dry-run by default.

ROM Audit is a read-only dashboard, per system ROM count, scraped %, missing artwork, gamelist orphans, and dupes.

BIOS Check tells you which BIOS files are missing or invalid per system, and it's version-aware, parsing batocera-systems (the tool that ships with the OS) instead of a hardcoded hash list.

Shaders is a per-system renderer-path picker, point the right CRT preset at each system without digging through menus.

Library (1G1R) compresses a No-Intro/Redump set into one preferred copy per game, hiding non-winners in gamelist.xml without moving or deleting files. Never touches Favorites, skips ambiguous ties, leaves arcade alone.

Performance gives you per-system run-ahead, overclock toggles, and the latency/speed knobs otherwise hidden in RetroArch menus.

Install script drops it into PORTS and walks you through an optional backup target.

Repo: https://github.com/t3chnaztea/batocera-toolbox

or if you want to live dangerously...

curl -fsSL https://raw.githubusercontent.com/t3chnaztea/batocera-toolbox/main/install.sh | bash

u/itsk2049 — 8 days ago

Vintage Dunhill Rollagas S-Type, dead, want to repair not display. What not to touch?

A friend's aunt passed away. She had several Dunhill lighters and he gave me this one to repair. It doesn't currently work... but I'd really like to repair it rather than leave it as a display piece. ITS SO COOL.

From the base markings and shape, I think it's an Alfred Dunhill S-Type Rollagas, circa 1970.

Markings I can see:

  • dunhill
  • 20 MICRONS
  • A66789 or similar
  • MADE IN ENGLAND

Fluted/engine-turned gold-tone case, hinged cap, S-Type/Rollagas-style mechanism. The base has a circular concentric adjustment disc.

What I'm trying to figure out:

  1. Can anyone confirm the exact model and era?
  2. What's the correct refill procedure for this S-Type?
  3. Are these reasonable to service at home, or should it go straight to a Dunhill/Rollagas repair specialist?

Current condition:

  • Appears complete
  • Mechanism is dirty, but moves freely
  • Not currently lighting
  • Trying not to force anything or damage the base/valve assembly
  • Don't know yet whether it just needs flint and fuel or a full seal/valve rebuild

Any advice from Dunhill/Rollagas collectors or repairers would be appreciated. I'd especially like to know what not to touch before I make it worse.

u/itsk2049 — 8 days ago

My Batocera arcade cabinet in our living room runs attract mode when unoccupied. To blend with our holiday decor, I created seasonal game lists that for it, similar to Plex’s seasonal collections.

I surface two seasonal pools into EmulationStation on the right dates and hide them when the season ended using a custom shell scripts.

Fall: The Halloween pool goes visible from Sep 15 to Oct 31, featuring 52 games across 13 systems. It includes lesser Castlevania ports, Friday the 13th NES, Splatterhouse, Bram Stoker’s Dracula, Frankenstein and Dracula shovelware, Zombies Ate My Neighbors, A Nightmare on Elm Street, Nosferatu SNES, Werewolf: The Last Warrior, and more.

Winter: The Christmas pool comes up from Nov 1 to Jan 1, featuring 23 games. It includes every Home Alone variant (NES, SNES, GB, MD, GG), Christmas NiGHTS into Dreams, Daze Before Christmas, Elf Bowling 1+2, and Santa Claus Saves The Earth.

Jan 2 through Sep 14, both pools are hidden, and the cabinet returns to its curated baseline.

The total install is a Python script, a shell script, two text files of ROM paths, and a 10-line append to custom.sh. Lists are easy to extend. Adding a 4th of July or Thanksgiving pool is cp halloween.txt thanksgiving.txt, edit, and add a row to the date map.

#!/bin/sh
# Output: off | halloween | christmas

MM=$(date +%m); MM=${MM#0}
DD=$(date +%d); DD=${DD#0}
KEY=$((MM * 100 + DD))

# Halloween: 9/15 (915) through 10/31 (1031)
if [ "$KEY" -ge 915 ] && [ "$KEY" -le 1031 ]; then
    echo halloween; exit 0
fi

# Christmas: 11/1 through 12/31, plus 1/1
if [ "$KEY" -ge 1101 ] || [ "$KEY" -eq 101 ]; then
    echo christmas; exit 0
fi

echo off

The flipper edits hidden flags in each affected system’s gamelist.xml, taking one of the three states. It’s idempotent, atomic, and only kills ES when actual diffs occur. Re-running with the same state is a no-op.

import os, re, subprocess
from pathlib import Path

ROMS = Path("/userdata/roms")
GAME_BLOCK_RE = re.compile(r"(<game\b[^>]*>)(.*?)(</game>)", re.S)
PATH_RE = re.compile(r"<path>([^<]+)</path>")
HIDDEN_RE = re.compile(r"<hidden>(true|false)</hidden>")

def patch_block(inner: str, want_hidden: bool) -> tuple[str, bool]:
    target = "true" if want_hidden else "false"
    m = HIDDEN_RE.search(inner)
    if m:
        if m.group(1) == target:
            return inner, False
        return inner[:m.start()] + f"<hidden>{target}</hidden>" + inner[m.end():], True
    if not want_hidden:
        return inner, False  # absent == visible (default)
    stripped = inner.rstrip()
    return stripped + f"\n\t\t<hidden>{target}</hidden>" + inner[len(stripped):], True

# active = paths in the target season's list
# inactive = paths in any OTHER season's list
# games not on either list are untouched (byte-identical)
def apply(system: str, target: str, lists: dict[str, set[str]]) -> int:
    gl = ROMS / system / "gamelist.xml"
    text = gl.read_text(encoding="utf-8")
    active = lists.get(target, set())
    inactive = {p for k, v in lists.items() for p in v if k != target}
    out, last, changes = [], 0, 0
    for m in GAME_BLOCK_RE.finditer(text):
        out.append(text[last:m.start()])
        open_t, inner, close_t = m.group(1), m.group(2), m.group(3)
        pm = PATH_RE.search(inner)
        if pm:
            rom = f"{system}/{pm.group(1).lstrip('./')}"
            if rom in active:
                inner, changed = patch_block(inner, want_hidden=False)
                changes += int(changed)
            elif rom in inactive:
                inner, changed = patch_block(inner, want_hidden=True)
                changes += int(changed)
        out.append(open_t + inner + close_t)
        last = m.end()
    out.append(text[last:])
    new_text = "".join(out)
    if new_text != text:
        tmp = gl.with_suffix(gl.suffix + ".tmp")
        with tmp.open("w") as f:
            f.write(new_text); f.flush(); os.fsync(f.fileno())
        tmp.replace(gl)
    return changes

# ...after editing every system's gamelist...
def sigkill_es():
    pids = subprocess.run(["pgrep", "-f", "^emulationstation "],
                          capture_output=True, text=True).stdout.split()
    for pid in pids:
        subprocess.run(["kill", "-9", pid])
    # emulationstation-standalone wrapper respawns ES

Loop in custom.sh calls the schedule script, compares to a state file, runs the flipper only when they diverge:

(
  while true; do
    DESIRED=$(/userdata/system/holiday-rotation/desired-state.sh)
    CURRENT=$(cat /userdata/system/holiday-rotation/state 2>/dev/null || echo "")
    if [ "$DESIRED" != "$CURRENT" ]; then
      /userdata/system/holiday-rotation/flip.py "$DESIRED" \
        >> /userdata/system/logs/holiday-rotation.log 2>&1
    fi
    sleep 43200
  done
) &

Today the cabinet is at off, looks like its normal curated self. On Sep 15 the loop will see desired=halloween, current=off, run the flipper, and 52 Halloween games quietly join the visible library. Nov 1 flips to Christmas. Jan 2 back to baseline.

reddit.com
u/itsk2049 — 2 months ago