# Zaparoo Documentation > Universal loading system — NFC, QR codes, and discs for launching games and media across MiSTer FPGA, Steam Deck, Batocera, Windows, and more. This file contains all documentation content in a single document following the llmstxt.org standard. ## Zaparoo App The Zaparoo App is the main way to interact with [Zaparoo Core](../core/index.md) from your phone. Install it on iOS or Android and it connects to your Core service over the local network. The app can search for nearby Zaparoo devices, or you can enter your device's IP address manually. If you'd rather not use a phone at all, there's a browser-based version of the app bundled with every Core release. See the [Web UI](./web.md) page for that. ## What the app does The app has three main sections: **Zap** is the home screen. You can scan [NFC tags](../tokens/nfc/index.md), [QR codes](../tokens/qr-codes.md), or [barcodes](../tokens/barcodes.md), see what's currently playing on the connected device, stop playback, and check what was last scanned. Tapping the scan button puts the phone into active scanning mode; once you're done you can review your scan history too. On supported Core versions, the Controls button opens remote-style controls, screenshot capture, and a full keyboard input view. **Create** is where you make tokens. Search your connected device's media library and write results directly to an NFC tag, or write the currently playing media to a new tag. There's also a custom [ZapScript](../zapscript/index.md) editor for arbitrary commands, a set of NFC utilities for reading raw tag data, formatting, erasing, and making tags read-only, and a [mappings](../features/mappings.md) manager for assigning [NFC toys](../tokens/nfc-toys/index.md) and barcodes to media without writing to the tag itself. **Settings** covers everything else: entering your device address or searching for nearby Zaparoo devices automatically, managing saved devices and pairing credentials, updating and scraping the media database, configuring [play controls](../features/play-controls.md), configuring reader behavior (scan mode, audio feedback, auto-detecting [external readers](../readers/index.md)), viewing inbox notifications, accessibility options, language, and your [Zaparoo Online](../online/index.md) account for managing purchases and subscriptions. Pro features and purchase restoration are also in here. If a Core device requires encrypted connections, the app prompts for the pairing PIN shown by Core and remembers the paired device for future reconnects. The app is available in English, Chinese (Simplified), Dutch, French, German, Japanese, Korean, and Spanish. ## Zaparoo Pro Pro is defined by a single rule: any feature that lets the app act as a reader using the phone's own hardware sensors. Everything else is free. There are two current Pro features. Launch on scan forwards in-app scans to the connected device so the phone works as a wireless Zaparoo reader over Wi-Fi, the same as any physical reader would. Shake to launch uses the accelerometer: shake the phone to play something random from a chosen system, or trigger a custom [ZapScript](../zapscript/index.md). Pro is a one-time purchase through the App Store or Play Store. All future Pro features are included at no extra cost. If you'd rather support Zaparoo development another way, there are [other ways to help](/sponsor/). ## Getting the app The Zaparoo App is on the iOS App Store and Google Play. See the [Downloads](/downloads/) page for current links and version information. There's also the [Web UI](./web.md), a version of the app embedded in Zaparoo Core and accessible from any browser on your local network. NFC, camera scanning, network device discovery, and Pro aren't available in the browser. ## Open source The [app source code](https://github.com/ZaparooProject/zaparoo-app) is open source under the [Apache 2.0 license](https://github.com/ZaparooProject/zaparoo-app/blob/main/LICENSE), including the Pro features. You're free to build and modify it under the license terms. The project just asks in good faith that you don't redistribute compiled builds with Pro features enabled, since those purchases fund ongoing development. ## FAQ **Do I need the app to use Zaparoo?** No. The app is the most convenient way to manage things and write tags, but you can also use the built-in [Web UI](./web.md) from any browser, the [TUI](../core/tui.md), or the [CLI](../core/cli.md) without installing anything on your phone. **Does my phone need to be near my MiSTer or other device?** No. The app communicates with Zaparoo Core over Wi-Fi. As long as both are on the same network, distance doesn't matter. **Does the app work on iPhone?** Yes. The Zaparoo App is on iOS and iPhones can scan and write NFC tags. The one limitation is that iPhone NFC only supports [NTAG](../tokens/nfc/ntag.md) format tags, not [MIFARE Classic](../tokens/nfc/mifare.md), but NTAG is the tag type Zaparoo recommends anyway so this usually isn't an issue. --- ## Web UI Every [Zaparoo Core](../core/index.md) release includes a web version of the [Zaparoo App](./index.md). Open it from any browser on your network. Nothing to install. Most features work the same as the full app. The main gaps are things that need native device access: NFC (read, write, and format) and camera barcode scanning. Tag reads and writes go through whatever [NFC reader](../readers/nfc/index.md) is plugged into the Core host. ## Accessing the Web UI Open a browser and go to `http://:7497/app/`. The root URL (`http://:7497/`) redirects there too. For example, if your IP is `192.168.1.100`, go to `http://192.168.1.100:7497/app/`. When the Web UI is hosted by a Core instance, it automatically connects back to that same host. You don't need to enter the IP address again inside the app. [Zaparoo Online](../online/index.md) sign-in on the embedded Web UI is email and password only. Google and Apple sign-in are hidden there; use the native apps if you need social sign-in. ## Configuration ### Remote access By default, the Web UI is accessible from any device on your local network. Core automatically allows `localhost`, local IP addresses on the Core API port, and configured `.local` hostnames. To allow other browser origins, add them to your [configuration file](../core/config.md): ```toml [service] allowed_origins = ["https://example.com", "http://custom-domain.local"] ``` --- ## Contributors Zaparoo has been a community effort from day one. Everyone's contributions are appreciated and encouraged! - [Aitor Gomez Garcia](https://github.com/spark2k06) - [Andrea Bogazzi](https://github.com/asturur) - [Anime0t4ku](https://github.com/Anime0t4ku) - ArielAces - [batty](https://github.com/protogem2) - BedroomNinja - [BossRighteous](https://github.com/BossRighteous) - [Christoph Helms](https://github.com/mrchrisster) - [ewrt101](https://github.com/ewrt101) - [fayep](https://github.com/fayep) - [Gaz](https://github.com/symm) - [jmf866](https://github.com/jmf866) - [kamikazechaser](https://github.com/kamikazechaser) - [Minty Trebor](https://github.com/MintyTrebor) - [philips](https://github.com/philips) - [Phoenix](https://github.com/PhoenixFire61) - Pink Melon - [Ranny Snice](https://github.com/Ranny-Snice) - RetroCastle - Seexelas - [Sensorium](https://github.com/SensoriumEmbedded) - [Spice-King](https://github.com/Spice-King) - [sreuter](https://github.com/sreuter) - [Stat-Mat](https://github.com/Stat-Mat) - [Teimuraz Arobelidze](https://github.com/geocybrid) - [TheRealBenForce](https://github.com/TheRealBenForce) - [TheTrain](https://github.com/TheTrainGoes) - [theypsilon](https://github.com/theypsilon) - [Tim Wilsie](https://github.com/timwilsie) - [v1605](https://github.com/v1605) - [wizzo](https://github.com/wizzomafizzo) - Ze Conehead - [Ziggurat](https://github.com/sigboe) Special thanks to **Gaz** for starting the project originally, and to **[javiwwweb](https://github.com/javiwwweb/MisTerRFID)** and **[Conner](https://github.com/ElRojo/MiSTerRFID)** for their existing MiSTerRFID projects. --- ## Community The Zaparoo community is active on several platforms. ## Discord [Discord](https://discord.gg/zaparoo) is our main community hub for real-time discussion and support. It's the best place to: - Get quick help with technical issues - Chat with other community members - Share your projects and get feedback - Participate in community events - Stay up to date with the latest developments ## Reddit Our [Reddit community](https://www.reddit.com/r/zaparoo) is great for: - Announcements and updates - Sharing project showcases ## Lemmy For those who prefer federated services, we also have a presence on [Lemmy](https://lemmy.world/c/zaparoo). ## Code of Conduct Our [Code of Conduct](/conduct/) applies to all community platforms. --- ## Media Coverage of the Zaparoo project online. ## Press & Articles ### Major Tech Publications - **[How to modernize your retro game collection in a few convoluted steps](https://www.theverge.com/24072166/retro-classic-game-roms-nfc-mister-how-to-modern)** - *The Verge* (February 29, 2024) Comprehensive feature article on TapTo/NFC system for MiSTer FPGA - **[Exploring TapTo NFC Integration On The MiSTer](https://hackaday.com/2024/09/15/exploring-tapto-nfc-inegration-on-the-mister)** - *Hackaday* (September 15, 2024) Technical deep-dive into TapTo NFC cards and integration - **[TapTo NFC Loading System - Gives MiSTer FPGA A Vital Physical Connection](https://www.timeextension.com/reviews/tapto-nfc-loading-system-gives-mister-fpga-a-vital-physical-connection)** - *Time Extension* (July 14, 2024) Full review of the TapTo NFC loading system - **[This Amiibo-Like NFC System Is A Cute Way To Load Up Games On Your MiSTer FPGA](https://www.timeextension.com/news/2024/12/this-amiibo-like-nfc-system-is-a-cute-way-to-load-up-games-on-your-mister-fpga)** - *Time Extension* (December 19, 2024) Coverage of Pixel Memories NFC reader design - **[MiSTer FPGA's Next Trick? Launching Games From CD](https://www.timeextension.com/news/2024/10/mister-fpgas-next-trick-launching-games-from-cd)** - *Time Extension* (October 8, 2024) Coverage of CD-based game launching with TapTo - **[Retro Remake unveils $150 FPGA-based PlayStation clone, with built-in NFC reader](https://www.polygon.com/gaming/512589/superstation-one-pre-order-playstation-console/)** - *Polygon* (January 28, 2025) Mainstream tech coverage of the SuperStation ONE with Zaparoo NFC reader built in - **[SuperStation ONE: $150 FPGA-based PlayStation clone with NFC reader](https://www.cnx-software.com/2025/01/29/superstation-one-150-fpga-based-playstation-clone-with-nfc-reader/)** - *CNX Software* (January 29, 2025) Hardware write-up covering the SuperStation ONE and its Zaparoo NFC reader - **[SuperStation ONE: PlayStation-inspired FPGA retro console](https://www.electronics-lab.com/superstation-one-playstation-inspired-fpga-retro-console/)** - *electronics-lab.com* (February 24, 2025) Electronics-focused coverage of the SuperStation ONE with Zaparoo integration ### Retro Gaming News - **[Using NFC Tags to Run Games on the MiSTer FPGA](https://www.retrorgb.com/using-nfc-tags-to-run-games-on-the-mister-fpga.html)** - *RetroRGB* (September 8, 2023) Early coverage of the NFC system - **[Groovy MiSTer, MiSTerCast, TapTo, MiSTex & More](https://www.retrorgb.com/mister-fpga-news-groovy-mister-mistercast-tapto-mistex-more.html)** - *RetroRGB* (March 3, 2024) MiSTer FPGA news roundup including TapTo coverage - **[TapTo Mobile NFC MiSTer App](https://www.retrorgb.com/tapto-mobile-nfc-mister-app.html)** - *RetroRGB* (April 18, 2024) Coverage of mobile app launch - **[MiSTerFPGA News – Atari Jaguar, TapTo Rename & More](https://www.retrorgb.com/misterfpga-news-atari-jaguar-tapto-rename-more.html)** - *RetroRGB* (November 18, 2024) Coverage of TapTo rebranding to Zaparoo - **[PC88, Play Videos on MiSTer, SNES Save States & More](https://www.retrorgb.com/mister-fpga-news-pc88-play-videos-on-mister-snes-save-states-more.html)** - *RetroRGB* (November 26, 2024) Coverage of Zaparoo movie launching feature - **[MiSTer FPGA News – MultiSystem 2, ZX Spectrum Next, Gridiron Fight & More](https://www.retrorgb.com/mister-fpga-news-multisystem-2-zx-spectrum-next-gridiron-fight-more.html)** - *RetroRGB* (December 6, 2024) Coverage of Zaparoo QR codes and the 1001 Games book integration - **[The DIY MiSTer Handheld](https://www.retrorgb.com/the-diy-mister-handheld.html)** - *RetroRGB* (December 16, 2024) Features Zaparoo integration in handheld build - **[MiSTerFPGA News – Ninja Gaiden, Gradius 2, Atari Jaguar, Sega ST-V & More](https://retrorgb.com/misterfpga-news-ninja-gaiden-gradius-2-atari-jaguar-sega-st-v-more.html)** - *RetroRGB* (January 12, 2025) TapTo-to-Zaparoo transition coverage alongside Zaploader NT news - **[Zaparoo Online, New Arcade Cores, New Mini-ITX IO Board & More](https://www.retrorgb.com/mister-fpga-news-zaparoo-online-new-arcade-cores-new-mini-itx-io-board-more.html)** - *RetroRGB* (February 17, 2025) Coverage of Zaparoo Online features - **[MiSTer Pi Successor, New Arcade Cores, Downloader & More](https://www.retrorgb.com/mister-fpga-news-mister-pi-successor-new-arcade-cores-downloader-more.html)** - *RetroRGB* (March 20, 2025) Zaparoo integration coverage - **[MiSTer FPGA News – 3DO, Taito F2, New Arcade Cores, Zaparoo & More](https://retrorgb.com/mister-fpga-news-mister-fpga-news-3do-taito-f2-new-arcade-cores-zaparoo-more.html)** - *RetroRGB* (April 13, 2025) Zaparoo Core v2.2.0 release coverage - **[Multisystem² has landed](https://gamingretro.co.uk/multisystem-has-landed/)** - *Gaming Retro* (May 19, 2025) Hardware review of the Multisystem 2 covering its Zaparoo integration - **[MiSTer FPGA News – CD-i, Sega ST-V, Sega Saturn & More](https://retrorgb.com/mister-fpga-news-cd-i-sega-st-v-sega-saturn-more.html)** - *RetroRGB* (June 20, 2025) Preview of the Zaparoo TUI and related updates - **[Atari System 2, Quarter MiSter, Multisystem Cart Support & More](https://www.retrorgb.com/mister-fpga-news-atari-system-2-quarter-mister-multisystem-cart-support-more.html)** - *RetroRGB* (July 21, 2025) Zaparoo updates including NAS support - **[Zaparoo Releases Multisystem2 NFC Cards + Kit](https://metalgamesolid.com/misc/zaparoo-releases-multisystem2-nfc-cards-kit)** - *MetalGameSolid* (August 2, 2025) Product release coverage - **[Zaparoo Update: Seamless Integration with tty2oled – Coming Soon!](https://metalgamesolid.com/misc/zaparoo-update-seamless-integration-with-tty2oled-coming-soon/)** - *MetalGameSolid* (August 21, 2025) Feature announcement covering tty2oled integration - **[Zaparoo Will Soon Use Smart Game Matching](https://metalgamesolid.com/misc/zaparoo-will-soon-use-smart-game-matching)** - *MetalGameSolid* (October 19, 2025) Technical feature announcement - **[Zaparoo Adds Support for USB and SD Cards as Launch Tokens](https://metalgamesolid.com/misc/zaparoo-adds-support-for-usb-and-sd-cards-as-launch-tokens)** - *MetalGameSolid* (October 25, 2025) New reader driver announcement - **[RFIDisk: When Floppy Drives Go Contactless](https://hackaday.com/2025/10/16/rfidisk-when-floppy-drives-go-contactless/)** - *Hackaday* (October 16, 2025) NFC floppy drive builds, featuring Zaparoo community projects - **[Amstrad CPW & New Arcade Games, NES Mappers & More](https://retrorgb.com/mister-fpga-news-amstrad-cpw-new-arcade-games-nes-mappers-more.html)** - *RetroRGB* (November 3, 2025) MiSTer FPGA news with Zaparoo technical updates - **[MiSTer NFC Cards Bring Physical Fun to Digital Games](https://theoasisbbs.com/mister-nfc-cards-bring-physical-fun-to-digital-games)** - *The Oasis BBS* (April 8, 2025) Article covering Wolfgang's NFC card creation tutorial - **[Wizzo Teases Zaparoo Graphical Front-End For MiSTerFPGA](https://metalgamesolid.com/fpga/mister-fpga/wizzo-teases-zaparoo-graphical-front-end-for-misterfpga/)** - *MetalGameSolid* (December 2025) Early look at the Zaparoo graphical front-end for MiSTer - **[MiSTer SuperStation One Review](https://retrorgb.com/mister-superstation-one-review.html)** - *RetroRGB* (March 5, 2026) Hardware review of the SuperStation ONE featuring the built-in NFC reader used by Zaparoo - **[MiSTer Companion v2.2.0 Released](https://metalgamesolid.com/misc/mister-companion-v2-2-0-released/)** - *MetalGameSolid* (March 8, 2026) Coverage highlighting the built-in Zaparoo install flow in MiSTer Companion v2.2.0 ### Developer Articles & Community - **[Creating TapTo](https://timwilsie.com/creating-tapto)** - *Tim Wilsie* (March 26, 2024) Behind-the-scenes on creating TapTo by a developer - **[Putting TapTo in your pocket](https://timwilsie.com/portfolio/putting-tapto-in-your-pocket)** - *Tim Wilsie* Development story of the mobile app - **[Welcome Zaparoo!](https://timwilsie.com/welcome-zaparoo/)** - *Tim Wilsie* (April 27, 2025) Behind-the-scenes on the TapTo-to-Zaparoo rebrand from the designer - **[The first Zaparoo equipped consoles arrive!](https://timwilsie.com/the-first-zaparoo-equipped-consoles-arrive/)** - *Tim Wilsie* (May 9, 2025) First look at SuperStation ONE and Multisystem 2 shipping with Zaparoo - **[Launching retro games with NFC cards for fun and pleasure](https://forums.bit-tech.net/index.php?threads/launching-retro-games-with-nfc-cards-for-fun-and-pleasure.387972/)** - *Bit-Tech Forums* (September 28, 2025) User build log and discussion ## Video Content ### Tutorials & Demos - **[MiSTer FPGA - Running Games with a Physical Tap (NFC)](https://www.youtube.com/watch?v=YwQviLwWHYM)** - *RetroRGB* (September 8, 2023) Early video companion to the RetroRGB NFC tutorial article - **[MiSTer FPGA - Zaparoo Demo and Tutorial](https://www.youtube.com/watch?v=mMfIT-2wXrc)** - *Gentlemen's Pixel Club* (December 14, 2024) Comprehensive demo and tutorial of Zaparoo on MiSTer FPGA - **[Exploring TapTo on the MiSTer](https://www.youtube.com/watch?v=73Kx3jwyk0U)** - *What's Ken Making* (July 12, 2024) In-depth exploration of the TapTo project - **[Launch Games With Your Phones NFC | TapTo Life | MiSTer FPGA](https://www.youtube.com/watch?v=YzEqLl4ow0Q)** - *Retro Dodo Gaming* (April 18, 2024) Tutorial on using phone NFC with TapTo - **[Launch Games with NFC Cards: Zaparoo + Batocera!](https://www.youtube.com/watch?v=BnnAX9cNUIE)** - *Zaparoo Official* (April 26, 2025) Zaparoo with Batocera on Raspberry Pi 5 demo - **[Zaparoo Pt. 2 - Build in MISTER NFC reader + NFC cards](https://www.youtube.com/watch?v=mCrdyhnv24g)** - *Pixel Cherry Ninja* (July 27, 2025) PCB design and build tutorial - **[TapTo NFC and MiSTer FPGA! Have a Physical Retro Game Collection](https://www.youtube.com/watch?v=rk-qAij-uM8)** - *Video Game Esoterica* (October 3, 2024) Overview of physical media aspect - **[Wireless NFC Launching for MiSTER | ZAPAROO!](https://www.youtube.com/watch?v=W_m8dzvHQdM)** - *Pixel Cherry Ninja* (January 26, 2025) Wireless NFC functionality demo - **[Unbelievably cool! Make you own working gamecarts!](https://www.youtube.com/watch?v=63EH1tI0HWk)** - *Pixel Cherry Ninja* (April 6, 2025) DIY game cartridge creation - **[1,001 games to play instantly on MiSTer FPGA with TapTo/Zaparoo](https://www.youtube.com/watch?v=jSHnx0W8qR4)** - *Video Game Esoterica* (December 2, 2024) Showcase of 1001 games book integration - **[You Can Have This Too | MiSTer FPGA | SMES + TapTo](https://www.youtube.com/watch?v=M220qOaxymY)** - *Retro Dodo Gaming* (September 15, 2024) SMES integration showcase - **[GPC ARCADE LOTTERY - we play multiple games on the MiSTer](https://www.youtube.com/watch?v=Sxu6ydrxcio)** - *Gentlemen's Pixel Club* (December 22, 2024) Arcade lottery using Zaparoo - **[MiSTer NFC cards experiment!](https://www.youtube.com/watch?v=jsficcAmyMI)** - *Gentlemen's Pixel Club* (November 20, 2023) Early NFC card experiments - **[My Zaparoo Problem!](https://www.youtube.com/watch?v=axvgDzqV5UU)** - *Pezz82* (March 2025) Personal experience and troubleshooting with Zaparoo - **[Zaparoo with MiSTercade and SNK Candy 25](https://www.youtube.com/watch?v=01nVvWWgOyg)** - *Oisin McG* (June 2025) Arcade cabinet integration with MiSTercade - **[MiSTer FPGA and Zaparoo for NFC](https://www.youtube.com/watch?v=hPx3euYoK_Q)** - *Mister JBAM* (November 2025) French language Zaparoo tutorial - **[Give Your PC Gaming A Physical Feel With NFC Cards And Zaparoo!](https://www.youtube.com/watch?v=oxfJf-r5K60)** - *Archades Games* (November 19, 2025) Tutorial on using Zaparoo with Windows and Steam games - **[MiSTer Multisystem² & Zaparoo](https://www.youtube.com/watch?v=UnmkAn7KUR4)** - *Pixel Cherry Ninja* (December 2025) Multisystem 2 with Zaparoo integration demo - **[The MiSTer Multisystem 2 plus Zaparoo is a retro gamers dream](https://www.youtube.com/watch?v=D7M2uFchdSg)** - *Scott Hanselman* (January 2, 2026) Enthusiastic walkthrough of Multisystem 2 and Zaparoo setup - **[Zaparoo徹底解説 - Super Station One](https://www.youtube.com/watch?v=36mkcwrvqxQ)** - *ゆっくりレトロ図書館* (February 28, 2026) Japanese language Zaparoo tutorial for Super Station One and MiSTer - **[Revendo o projeto Zaparoo + Teste dos itens do Unboxing anterior](https://www.youtube.com/watch?v=8zjln0Ffmv8)** - *YouTube* (March 16, 2026) Portuguese language Zaparoo project revisit and hardware unboxing follow-up - **[Zaparoo agora no Batocera](https://www.youtube.com/watch?v=Pfb8CWQFVhg)** - *YouTube* (March 20, 2026) Portuguese language walkthrough of Zaparoo running on Batocera - **[La magia dei giochi fisici su Batocera! Scopriamo Zaparoo](https://www.youtube.com/watch?v=aZPFoFqYJuc)** - *80 voglia di arcade* (May 26, 2026) Italian language Batocera walkthrough showing NFC cards, arcade cabinet setup, and Zaparoo Designer ### News & Updates - **[MiSTer, Analogue Pocket, FPGA, Random & Retro Gaming News (ep129)](https://www.youtube.com/watch?v=ZNOjMzGHwHY)** - *Pixel Cherry Ninja* (December 14, 2024) News roundup including Zaparoo coverage - **[FPGA & Retro Gaming News Ep172](https://www.youtube.com/watch?v=3Lo-bGugn6k)** - *Metal Game Solid* (November 8, 2025) FPGA news including Zaparoo updates - **[Retro 168 #3 | RGB Pi2, Zaparoo, M64, PS1 MC, MiSTer NES & More](https://www.youtube.com/watch?v=ep5t8ScWLiI)** - *Metal Game Solid* (October 24, 2025) Weekly retro gaming news - **[FPGA & Retro Gaming News Ep176](https://www.youtube.com/watch?v=e2OiJZnFpRY)** - *Metal Game Solid* (December 11, 2025) FPGA news including Zaparoo coverage - **[Retro 168 #5 | Zaparoo, DSPico, iiSU, NES Core, Romhacks](https://www.youtube.com/watch?v=J4ZkqYUv2lg)** - *Metal Game Solid* (December 5, 2025) Weekly news roundup leading with Zaparoo coverage - **[New Frontend for MiSTer FPGA in the Works... and it Looks Awesome!](https://www.youtube.com/watch?v=5896iuzbrck)** - *Pixel Cherry Ninja* (December 19, 2025) First-look video on the upcoming Zaparoo graphical front-end ## Official Resources - **[Zaparoo YouTube Channel](https://www.youtube.com/@HeyZaparoo)** - Official project channel with tutorials and updates - **[Zaparoo Blog](/blog)** - Official project blog with announcements and technical articles --- ## TapTo TapTo was the original name for the Zaparoo project. While the project is now officially called Zaparoo, the original name and logo can still be seen in the community. You are free to continue using the name "TapTo" and the associated logo in your projects, just be aware it now has no official association with the Zaparoo project. The TapTo logo was contributed to the project by [Ranny Snice](https://github.com/Ranny-Snice) who is the creator and copyright holder. The logo is, generally, free to use by anyone in their own projects. Reference files (including SVG) for the logo and license are always available in the [Zaparoo Media](https://github.com/ZaparooProject/zaparoo-media/tree/main/tapto-logo) repository on GitHub. --- ## Vendors Although we encourage people to sell Zaparoo compatible hardware and make the project more accessible, vendors come and go, so we're not able to maintain an accurate list of everyone who sells stuff. Please [join the Discord server](https://zaparoo.org/discord) to see if there's someone local to you selling custom readers and labels or offering services to make them on demand. If you're looking for official Zaparoo cards and readers make sure to check out the Zaparoo Shop, which also helps support development of the project! If you just need some generic NFC cards or reader modules, we have some recommended suppliers listed on the [NFC Readers](../readers/nfc/index.md) and [NFC Tags](../tokens/nfc/index.md) pages. --- ## DIY Reader This guide explains how to create a low-cost NFC reader using easily available parts. The design is compatible with Zaparoo on MiSTer, and the total cost of a single reader is approximately $10 USD in a small batch of 5. ## Supplies ### Equipment - Soldering iron - Solder - Flux - 3D printer Some soldering is necessary to attach the header pins that connect the serial board to the NFC module. It requires 8 through-hole solder joints, so it's a good beginner soldering project. Access to a 3D printer is required for color switching to show the top markings and LED window, but it's also possible to use a 3D printing service and print in one color. ### Parts Feel free to shop around for components. - [PN532 NFC module](../readers/nfc/pn532-module.md) - any supplier will do but must be a "PN532" module. - 2.54mm right angle female 4-pin header - attaches to the serial board. - 2.54mm right angle male 4-pin header - LIKELY OPTIONAL - attaches to the PN532 module but most modules come with one. - 3D printing filament - if printing yourself, PLA or PETG is fine. A single case uses approximately 20g of filament. ## Serial PCB This is a custom PCB which allows the PN532 to communicate with the MiSTer over a serial USB connection. It functions the same as a standard USB to serial cable/adapter, but has a better form factor for use in a case and does not risk incorrect voltage issues that have been found in these adapters. This section will explain how to order the PCB from JLCPCB, but any PCB manufacturer will work. ### Upload gerber file From the JLCPCB home page, click the "Add gerber file" button and upload the [Gerber.zip file](https://github.com/ZaparooProject/zaparoo-hardware/tree/main/diy-reader/pcb) from this repo. You'll be forwarded to the order page. ### Beginning order Scroll down to the "PCB Assembly" section and enable this option with the side toggle. Otherwise, leave all options as default and click "NEXT" on the right sidebar. ### Board confirmation Click "NEXT" on this page. ### Upload BOM and CPL files Upload the [BOM-JLCPCB.xlsx file](https://github.com/ZaparooProject/zaparoo-hardware/tree/main/diy-reader/pcb) in the left file picker (BOM) and the [PickPlace-JLCPCB.xlsx file](https://github.com/ZaparooProject/zaparoo-hardware/tree/main/diy-reader/pcb) in the right file picker (CPL). Then click the "Process BOM & CPL" button. :::info You may receive an error about the "J2 designator" as seen below. Ignore this and press "Continue". This is referring to the female header pins which you will solder yourself later. ::: ### Bill of materials There should be nothing to do on this page. Click "NEXT". ### Component placements You may see an error about components being offset when this page loads. Click "Ok". You can make sure the components aren't obviously in the wrong place, but it should be OK to click "NEXT" here too. ### Quote & order Select a "Product Description" option. Something like "DIY" is fine. Then click "SAVE TO CART". ### Checkout From here, follow the prompts to specify shipping and payment for your boards. You can also change the quantity of boards here. And that's it. When your boards arrive you're ready for assembly. ## Case The case is 3D printed in two parts, the top and bottom. Each part snaps together, no other assembly or parts are required. Download STL files: - [Micro-USB version](https://github.com/ZaparooProject/zaparoo-hardware/tree/main/diy-reader/case) - [USB-C version](https://www.printables.com/model/970108-tapto-usb-c-style-shell) Print each part with a 0.2mm layer height. PLA and PETG have both tested working fine. It requires approximately 20g of filament. If you want to make the markings on the top piece more visible, you can print it in two colors. Print the first layer in one color, print layers 2 and 3 in a contrasting color, then print the rest in the original color. Another option to make the LED lights visible is to print the layers around the USB port in a translucent filament. It has not been tested, but it should also be no problem to use a 3D printing service to print the case. You may have to request multiple cases to reach the minimum size requirements of the service. ## Assembly Assembly does not take long, but does require a small amount of soldering. ### Soldering First, solder the female 4 pin header to the serial PCB, as shown in the image below. Make sure the plastic casing is flush with the PCB. Next, solder the male 4 pin header to the PN532 module. See the image below, it needs to be soldered into the part with only 4 holes, not the side with 8 holes. It's important that this one goes on straight too, and it can be a little tricky to get right. You can hold one edge straight with your nail or helping hands, then solder the opposite side. Kapton tape may also help to hold it in place. :::note If you touch the male pins at any point, make sure to give them a quick clean with alcohol. This can affect the connection to the module. ::: Lastly, check the DIP switches on the PN532 module, next to the connector you soldered. They should be set to "HSU". This is how they usually come by default. Now you can connect the 2 boards together like shown. ### Case Drop the assembled boards into the bottom half of the case, aligning the mounting holes with the indentations in the case and the micro USB slot. Then, place the top piece on top and push it in place into the bottom piece. They will snap shut together. It can take a little bit of force to make them snap together, but it should not be too difficult. Congratulations, you did it! Welcome to Zaparoo. This reader should be plug and play with Zaparoo on a [MiSTer FPGA](../platforms/mister/index.md) and [Commodore 64](../platforms/commodore64.md). Have fun! ## Contributors - RetroCastle - serial PCB design and testing. - [wizzo](https://www.patreon.com/wizzo) - case design and documentation. - [Retro Frog](https://www.printables.com/@RetroFrog) - case design advice. --- ## Projects An index of projects created by the Zaparoo community. We love seeing what people create. If you've made something cool with Zaparoo, please share it! You can submit your project by opening a pull request to add it to this list, or by sharing it on our Discord or Reddit communities. Projects can be anything from 3D printed cases to custom tokens, software integrations, or creative ways to use Zaparoo. Even if it feels small, others may find it useful or inspiring! ## Custom Cases - **[3.5" TFT Display NFC Enclosure for MiSTer de-10-nano](https://www.printables.com/model/1125804-35-tft-display-nfc-enclosure-for-mister-de-10-nano)** by *MiSTerConsoles* Integrated enclosure for a MiSTer de-10-nano that combines a PN532 NFC reader with a 3.5" TFT display driven by TTY2TFT. The display shows core and game artwork as cards are tapped. - **[Amiga 500 Disk tray replacement for Zaparoo NFC](https://www.thingiverse.com/thing:6861725)** by *indubioproretro* Disk tray replacement for Amiga 500 that houses PN532 and PCR532 NFC readers for use with Zaparoo. - **[Floppy Disk NFC Reader Case](https://makerworld.com/de/models/2384860-floppy-disk-nfc-reader-case-for-zaparoo)** by *amiibolover99* Uses a slim USB floppy drive as the base with an added card slot. Modular design lets you adjust card reader dimensions. - **[KMDT MiSTer NFC Case + HuCards](https://makerworld.com/models/1570997)** by *Kumodot* Case for the RetroRemake MiSTer Pi batch 3 NFC reader with dual slot for HuCards and mini NFC PCB cards. - **[Mini NFC Card Reader](https://makerworld.com/en/models/822814-mini-nfc-card-reader)** by *Ninja3DP* Compact case for the black USB-C NFC readers available on AliExpress. - **[Mister FPGA Case with integrated TapTo NFC Card Reader Slot](https://www.printables.com/model/979056-mister-fpga-case-with-integrated-tapto-nfc-card-re)** by *Raspberry1337* Compact MiSTer FPGA case with built-in NFC card slot. Features optional tactile blocker for satisfying click when card is inserted. - **[Mister FPGA Wedge Case with TapTo for Keychron K8](https://www.printables.com/model/964115-mister-fpga-wedge-case-with-tapto-for-keychron-k8)** by *Andrés del Campo Novales* A wedge case for the Mister FPGA with Zaparoo for the Keychron K8 keyboard. - **[MiSTer FPGA Case with TapTo Slot and Drive Tray](https://makerworld.com/en/models/846344-mister-fgpa-case-with-tapto-slot-and-drive-tray)** by *TBone_427120* MiSTer FPGA case variation with integrated TapTo slot and internal drive tray. - **[MiSTer PI Console Case with TapTo integration](https://www.printables.com/model/1058136-mister-pi-console-case-with-tapto-integration-for)** by *eljimador* Remix with power switch opening matching the original Retro Frog MiSTer Pi design. - **[NFC 1541 V2](https://makerworld.com/en/models/1670375-nfc-1541-v2)** by *Ninja3DP* Commodore 1541 floppy disk drive inspired case for PN532 NFC readers. Compatible with ZapPad and RetroRemake NFC reader. - **[Card Insert Stabilizer](https://www.printables.com/model/1306895-card-insert-stabilizer-for-tapto-nfc-1541)** by *The Opponent* Addon that replaces bottom joists to guide cards into the slot. - **[Tandy Remix](https://www.printables.com/model/804654-tandy-remix-of-tapto-nfc-1541)** by *RayneX* 1541 case with added Tandy logo to look like DOS-era floppy drive. - **[TapTo 1541 Sled](https://www.printables.com/model/970069-tapto-1541-sled)** by *BLuR42* Internal sled to mount TapTo USB-C Style Shell inside 1541 case. - **[NFC Enclosure for MiSTer de-10-nano](https://www.printables.com/model/1127790-nfc-enclosure-for-mister-de-10-nano)** by *MiSTerConsoles* Compact enclosure that mounts a PN532 reader with USB-C adapter PCB directly to a MiSTer de-10-nano. - **[NFC-DS V1 (ZDS)](https://makerworld.com/en/models/2174091-nfc-ds-v1-zds)** by *Ninja3DP* Famicom Disk System-inspired case for PN532 NFC readers. Compatible with ZapPad and the RetroRemake NFC reader. - **[NFC-Engine V2](https://makerworld.com/en/models/1542524-nfc-engine-v2)** by *Ninja3DP* PC-Engine inspired case for PN532 NFC readers. Compatible with ZapPad and RetroRemake NFC reader. - **[Retro NFC reader for NFC cards and Carts](https://makerworld.com/en/models/1963134-retro-nfc-reader-for-nfc-cards-and-carts)** by *SunieFairbreeze* All-in-one case for PN532 USB NFC Reader with retro video game-style carts (holds 25mm NFC tags) and cassette case insert. Includes label templates. - **[Retro Remake NFC Reader case](https://makerworld.com/en/models/1685590)** by *Ninja3DP* Sleek minimal case for the MiSTer Pi's NFC Reader PCB. - **[SHELFMODE: Physical Game Library](https://makerworld.com/en/collections/18600603-shelfmode-physical-game-library)** by *PullleMit3L* Custom NFC dock for the ACR122U reader and cassette-sized cartridges for PC games. Each cartridge has an embedded NTAG215 chip that launches a game via Zaparoo. Includes printable rack for shelf display. - **[Super Zap Boy](./super-zap-boy.md)** by *Phoenix* A custom [MiSTer FPGA](../platforms/mister/index.md) case project using an original Super Nintendo, Super GameBoy and reproduction GameBoy cartridges with custom made labels. - **[TapTo Card Reader With Spring Loaded Ejection Button](https://www.printables.com/model/999381-tapto-card-reader-with-spring-loaded-ejection-butt)** by *Raspberry1337* Floppy disk style with satisfying click and functional eject button. Originally designed by EntirelyTom. - **[TapTo NFC Consolized dock for Mister Pi case](https://www.printables.com/model/1007354-tapto-nfc-consolized-dock-for-mister-pi-case)** by *NightGecko* Dock attachment for RetroFrog-based MiSTer console. - **[TapTo PN532 V2 NFC Reader Case](https://www.printables.com/model/896333-tapto-pn532-v2-nfc-reader-case)** by *Blazewind* Remixed case for generic PN532 V2 NFC Reader. - **[TapTo Reader Case (PN532)](https://www.thingiverse.com/thing:6721541)** by *JPRanger18* A custom case for the PN532 module and an off the shelf USB to UART (TTL) adapter. - **[TapTo USB-C Style Shell](https://www.printables.com/model/970108-tapto-usb-c-style-shell)** by *BLuR42* A custom case for the TapTo DIY Reader, adjusted for the USB-C adapter variant. - **[Zaploader V2](https://makerworld.com/en/models/1695030-zaploader-v2)** by *Ninja3DP* NFC cart reader for SNES (PAL/JAP) carts and NFC cards. Supports ZapPad and USB-C NFC readers. - **[Famicom/Mega Drive/Master System Adapter](https://makerworld.com/en/models/1700855-zaploader-fam-md-ms-adapter)** Adapter for Famicom, Mega Drive, and Master System cartridges - **[Game Boy Adapter](https://makerworld.com/en/models/1700887-zaploader-game-boy-adapter)** Adapter for Game Boy cartridges - **[Game Gear Adapter](https://makerworld.com/en/models/1700896-zaploader-game-gear-adapter)** Adapter for Game Gear cartridges - **[N64 Adapter](https://makerworld.com/en/models/1700838-zaploader-n64-adapter)** Adapter for N64 cartridges - **[NES Adapter](https://makerworld.com/en/models/1700876-zaploader-nes-adapter)** Adapter for NES cartridges - **[Zaparoo (TapTo) NFC Cart/Card Reader](https://makerworld.com/en/models/1417537-zaparoo-tapto-nfc-cart-card-reader-mister-fpga)** by *psybertech* Simple reader that holds USB NFC reader, reads both cartridges and standard cards. Includes optional riser to hide USB cable. - **[Zaparoo NFC Reader - Atari-homage version v20](https://makerworld.com/en/models/1536546-zaparoo-nfc-reader-atari-homage-version-v20)** by *psybertech* Atari 2600-inspired design. Supports both carts and standard NFC cards with optional sled for USB NFC reader. - **[Zaparoo/TapTo for MiSTer FPGA add-on enclosure](https://www.printables.com/model/1140054-zaparootapto-for-mister-fpga-add-on-enclosure)** by *Retro Frog* Mounts to bottom of MiSTer stack with M3 screws. For PN532 NFC board. ## Software Tools - **[ZaparooSkraper](https://github.com/AchillesPDX/ZaparooSkraper)** by *AchillesPDX* A custom Mix for Skraper to generate Zaparoo NFC labels. Features controller illustrations from the RetroPie es-theme-carbon project and professionally redrawn console logos from the Console Logos archive. - **[zaparoo-helpers](https://github.com/thwonp/zaparoo-helpers)** by *Thwonp* Bash scripts for composing and printing custom Zaparoo cards. Includes `imagemagick_composer.sh` for batch image composition (combining cover art and marquees with templates) and `print_composer.sh` for arranging cards into print-ready 8.5x11" pages at 300 DPI. - **[NFC Card Generator](https://github.com/Anime0t4ku/NFC-Card-Generator)** by *Anime0t4ku* Desktop app for generating printable NFC card labels. Pulls cover art from SteamGrid and TMDB, with custom template support. - **[NFC Cassette Cover Generator](https://github.com/Anime0t4ku/NFC-Cassette-Cover-Generator)** by *Anime0t4ku* Desktop app for creating print-ready cassette wrap covers with live preview, customizable colours, SteamGridDB/TMDB artwork integration, and PDF export for batch printing. - **[zaparoo-custom-launcher-builder](https://github.com/Anime0t4ku/zaparoo-custom-launcher-builder)** by *Anime0t4ku* Python GUI for generating custom launcher TOML files for Zaparoo Core. - **[SuperStationOne_CoverArt](https://github.com/Anime0t4ku/SuperStationOne_CoverArt)** by *Anime0t4ku* Printable 9mm half-height DVD case cover art designed for holding NFC cards in Super Station One-style cases. - **[zaparoo-label-automator](https://github.com/byronlcollier/zaparoo-label-automator)** by *byronlcollier* Python CLI that automates Zaparoo card label creation using the IGDB API for metadata and artwork. - **[Main_MiSTer (Zaparoo fork)](https://github.com/spark2k06/Main_MiSTer)** by *spark2k06* Fork of the MiSTerFPGA main binary that adds optional Zaparoo-aware features: a standby screen while waiting for a card tap, and per-core loading screens with cover art. ## Tokens ### Storage - **[NFC card box](https://www.printables.com/model/1272025-nfc-card-box)** by *EtNSiD* Simple blocky NFC card box that holds up to 80 standard-sized cards. Includes GIMP template for printing stickers. - **[NFC Card Case - Full back](https://makerworld.com/en/models/598032)** by *TheTrain* This is a card case for NFC cards that are 3.375" x 2.125". - **[NFC Card Case with TapTo Logo](https://www.printables.com/model/1045987-nfc-card-case-with-tapto-logo)** by *BLuR42* Remix of TheTrain's card case with embossed/colored TapTo logo. - **[PlayStation games case for NFC Cards](https://makerworld.com/en/models/1546786-playstation-games-case-box-mini-for-nfc-cards)** by *LoVeMaKeRz* Sony PlayStation-styled mini case for standard-sized NFC cards with print-in-place hinge. - **[Sega Saturn mini case for NFC Cards](https://makerworld.com/en/models/1484053-sega-saturn-mini-case-for-nfc-cards)** by *LoVeMaKeRz* Sega Saturn-styled mini case designed to hold standard-sized NFC cards (54mm x 85.6mm). - **[TapTo Card Stand/Holder](https://www.thingiverse.com/thing:6723128)** by *JPRanger18* Simple stand for some credit card sized TapTo NFC Game Cards. Fits 20-25 cards. - **[TapTo NFC Card Storage Box & Separator Cards](https://makerworld.com/en/models/584450)** by *Raspberry1337* Storage box for larger number of standard size NFC cards, plus printable divider cards with example logos. ### Custom Tokens - **[54mm Floppy Disks](https://www.printables.com/model/805885-54mm-floppy-disk-for-tapto-nfc-1541)** by *iZero* 3D printed floppy disks for use with the TapTo NFC 1541 case. - **[SNES Game Cart Tap-to-Play Mini NFC Keychains](https://makerworld.com/en/models/2387775-snes-game-cart-tap-to-play-mini-nfc-retro-keychain)** by *kertgartner* Mini SNES cartridge keychains with embedded NFC. Works with Zaparoo and Delta (iOS). Includes video tutorial. - **[Zaparoo(TapTo) Retro Game Cartridges/Carts NFC](https://makerworld.com/en/models/1417529-zaparoo-tapto-retro-game-cartridges-carts-nfc)** by *psybertech* Multiple cart styles: NES, SNES, N64, GB, Atari 2600, 3.5" floppy, CD/DVD, custom cart. Uses 1" (25mm) circular NFC tags. Includes organizers and label size guide. --- ## Super Zap Boy Super Zap Boy is a custom case project created by Phoenix that uses an SNES shell with an SBC inside, designed to work with a Super Game Boy for reading games. The Super Game Boy contains a PN532 NFC reader connected to an SBC (either a DE-10 Nano or a clone board), which is housed inside the SNES shell. These components are connected internally via a wired USB connection. The games' paths are written onto an NFC tag using the Zaparoo App. This tag is then placed inside a Game Boy cartridge, where the NFC reader inside the Super Game Boy can scan it and launch the game. For the current writing flow, see the [Zaparoo App docs](../app/index.md). Since the original purpose of the items are being changed in this build (playing GameBoy and SNES games), from here on out I will be referring to the Super Game Boy as the Super Zap Boy and the SNES (Super Nintendo Entertainment System) as the SMES (Super MiSTer Entertainment System). ## Introduction This project started its life after contemplating on how best to use physical media together with software or hardware emulation. Since the MiSTer Project and the multitude of Linux-based Emulation operating systems have no physical media for games, a lot of people resort to modding and using their own classic consoles for the most nostalgic looking and feeling experience. Currently software and hardware emulation have come a long way and are in some cases (almost) interchangeable with original hardware accuracy-wise. The absence of physical media keeps people from viewing this route as a 'complete' and authentic experience. The Zaparoo project introduces a new way for people to fill this gap with physical NFC cards. Super Zap Boy tries to improve upon this concept with physical cartridges to replicate the feeling of inserting games and having a classic console to look at while keeping the cost relatively low compared to modding original hardware for the same performance. ## The build :::warning[Before You Begin] This process requires some soldering skills and involves cutting or removing parts of your existing hardware. I would rank this process as intermediate. If you enjoy tinkering, read on. Otherwise, consider seeking help from others before attempting any irreversible modifications. ::: With that out of the way, below is a list of the requirements for this build. Try to acquire as many of these items as possible before you begin. I divided it into two sections, so you can choose what parts to get or omit. Firstly, you will need either a European Super Nintendo or a Japanese Super Famicom. The American Super Nintendo might work as well, but due to its different shape and size, and since I do not own one, I cannot guarantee that all steps will work perfectly, so your experience may vary. Secondly, you will need a Super Game Boy and some Game Boy cartridges. The latter can be purchased online in the form of empty shells or you can use your own cartridges that you want to put an NFC chip into (more on that later). Preferably, you will also need a 3D printer to print some parts for the build, although these are cosmetic and not mandatory. Additionally, you will need some cables and other items. Keep in mind that we have limited space in the case, so the smaller or shorter the items are, the better: ### SMES **Mandatory:** - A small USB hub for inside the case to hook your devices to. Don't make this too big or it won't fit. Also don't hook up too many (power hungry) devices to it as all of it needs to be powered by the board - DuPont cables (female to female) - General tools like a plier and screwdrivers (including a 4.5 mm Gamebit screwdrivers for the Super Nintendo) - Double sided tape (I used 3M heavy duty tape) **Optional:** - Female USB-A and C ports - A Bluetooth dongle - A Wi-Fi dongle - Some heat shrink tubes - An Arduino board (Arduino Pro Micro ATMega32U4) for the front controller ports - Soldering iron and wire ### Super Zap Boy **Mandatory:** - A USB-C to USB-A cable for the NFC reader - Round NFC tags (NTAG 215) for the cartridges - A PN532 NFC reader with USB-C (black one) - General tools like a plier and screwdrivers (including a 3.8 mm for the Super Game Boy and Game Boy cartridges) - Double sided tape (I used 3M heavy duty tape) - Gameboy cartridges. These can be empty or populated ones, it doesn't matter **Optional:** - A 90 degrees angled USB-C to USB-A cable would be better for positioning - A sticker to apply on the front of the case. The logo on the top of this wiki can be used for this as it's made to scale. ## Readying the Super Zap Boy **Steps:** 1. **Open the Super Game Boy:** Open up the Super Game Boy and remove everything, including the PCB board. Take the back of the shell and cut some of the support bars to make room for the NFC reader. Cut everything in the green area. 2. **Prepare the Shell:** After you're done, it should look like this. Ensure the cut areas are flat and won't scratch the back of your NFC reader. You can sand these off, but be careful not to overdo it. 3. **Position the NFC Reader:** Position the NFC reader to the back of the Super Game Boy case as shown. Ensure the NFC reader is positioned so that the USB-C cable can still be plugged into it. :::tip[Positioning Tip] Test the position with both the cable and the NFC reader to get it just right before applying the double-sided tape. I made the mistake of placing the NFC reader too flat in the center, which caused my USB-C cable not to fit anymore. ::: 4. **Adhere the NFC Reader:** After finding a good position where both fit, adhere the reader to the back. Note that having the NFC reader on the shell doesn't guarantee it will read the NFC tags in your cartridges because the cartridge slot remains in a fixed position. You can adjust by applying the tags slightly more to the right in your Game Boy cartridges to compensate for any misalignment. 5. **Align the NFC Tag:** Depending on the position of your NFC reader, you may need to adjust the position of the NFC tag in your Game Boy cartridges to align as much as possible. This will give you the highest chance of a successful scan and game read. It doesn't matter if the NFC reader is off-center, as long as the NFC tag is in a similar position. 6. **Cut an Opening for the USB Cable:** The final step for the Super Game Boy is to cut a small opening for the USB cable to fit through. I initially tried routing it through the bottom, but that didn't work well as the cartridge slot of the Super Nintendo didn't accommodate it. If you cut at a low position as shown in the picture of step 6, this cut won't be visible once the Super Game Boy is inserted into the case. I deemed this a necessary shell modification for the project. 7. **Fit the Super Game Boy into the SMES Case:** This is where the USB cable should pass through to get inside the SMES case. It requires some wiggling and fidgeting to get it in, so ensure it fits without forcing it too much. ## Checkpoint! At this point, you've reached a **checkpoint**. You can either: 1. Continue with the main build to complete the project. 2. Use the Super Zap Boy as a standalone reader via USB. If you decide to stop at this stage and use the Super Zap Boy as a standalone reader, the only thing left to do is: - **Create** custom labels for your games. - Start **playing** your games through the reader! Now that you have your Super Zap Boy, I invite everyone to get creative with how you display it. Here are some ideas: - Design and 3D print a stand for your Super Zap Boy, allowing it to be neatly displayed or slotted into something cool. - Get inventive with how you display your cases and games. The sky's the limit when it comes to **customization** and **presentation!** ## The Main Dish: Readying the Super MiSTer Entertainment System If you've made it this far, **congratulations** on building your own **Super Zap Boy**! Now, let's take it to the next level by integrating it with the **Super MiSTer** and completing the project. In this final section, we will house a Single-Board Computer (SBC), such as the **Raspyberry Pi,** the **DE-10 Nano** or one of its clones, inside the **SMES** case. :::note As of writing this tutorial, Zaparoo is **not yet officially supported** on the Raspberry Pi. Support will be added in the future. For now, you'll need to use a **Terasic DE-10 Nano** or a clone board like the **QMTECH** or **MiSTer Pi.** ::: ### Step 1: Disassembly To begin, you'll need to disassemble the SMES case. 1. **Tools Required:** - Regular screwdriver - Gamebit screwdriver 2. **Unscrew & Open:** - Start by unscrewing all visible screws and open up the case. Be sure to save the screws for later use. 3. **Remove Internal Components:** - Take out **everything** inside the case, **except** for the power switch. - You can also choose to keep the **cartridge slot** intact if you plan to use the Super Zap Boy. Keeping it will prevent the Super Zap Boy from moving around when inserting and removing Gameboy cartridges. - **Remove** the **eject mechanism** as it takes up too much space and will no longer be needed since the Super GameBoy will be **permanently slotted**. - For now, **remove** the **cartridge slot** (it can be re-installed later if desired). ### Step 2: Trimming Next, we need to make room inside the SMES case for the board and cables. 1. **Grab a Plier:** - Use the plier to **trim** away the **excess** plastic from the inside of the case. - Remove as much unnecessary plastic as possible to make room for the DE-10 Nano (or its clones) and the cables. 2. **Check the Fit:** - Position everything carefully to ensure the board and components fit **snugly** without obstructing anything. - Trim as needed until it matches the reference pictures (see images below). ### Step 3: Securing the Components Once everything is positioned correctly: 1. **Use Electrical or Double-Sided Tape:** - Secure the components in place with **electrical tape** and/or **double-sided tape**. - Ensure everything is firmly adhered and that no parts are loose or shifting inside the case. You're almost done! After these steps, your Zap Boy combined with Super MiSTer should be **good to go**. Keep tinkering with the positioning and trimming until everything is in its place and functioning as expected! ## Optional: Power Switch for SMES You can repurpose the **original** SNES power switch to power the MiSTer setup using a **Male DC Barrel Jack** and a **Female USB-C Breakout Board**. This setup allows you to turn your MiSTer on and off with the SNES switch. ### Required Parts (Available on AliExpress) - DC Power Pigtail Male Cable - USB Type-C Connector Board ### Wiring Instructions 1. **Snip off** the connector from the end of the **SNES power switch**. 2. **Cut** the **black cable** of the barrel jack in half. 3. **Solder** the black cable (just cut) to the **GND** through-hole of the female USB-C breakout board. 4. **Solder** the red cable of the barrel jack to the **VBUS** through-hole of the USB-C breakout board. 5. **Solder one end** of the SNES power switch (either end) to the black cable coming from the **USB-C breakout board**. 6. **Solder the other end** of the SNES power switch to the **black cable** of the barrel jack. ### Final Assembly 1. **Plug the male DC barrel jack** into the female DC barrel jack on your DE-10 Nano or QMTECH board. - If you are using a MiSTER Pi, you will need a **Female Barrel Jack to USB-C converter**. 2. You now have the **female USB-C breakout board** acting as a **DC input** for your MiSTer, and the SNES power switch will control its power. ### Power Supply Recommendation For powering the SMES I recommend using a **Raspberry Pi USB-C power supply** to ensure consistent and safe power delivery. The recommended power for the MiSTer is 5V 3A. ## Optional: Reset Button You can also get the **reset button** working on your SMES. For this you need some **glue,** a 3D-printed **mount** from the [SMES hardware files](https://github.com/ZaparooProject/zaparoo-hardware/tree/main/smes), some wires and an actuation button (I bought one of these). You need an **actuation button** and some wires for soldering. I used small clicky buttons found on AliExpress. The button will need to be attached to the mount and then placed beneath the reset button's mechanism so that when pressed down it activates your button that is on the mount. **3D print** the mount and place the switch on it. Then apply them on the stem of the reset button. Make sure it's properly seated all the way. It's advised to use (super) glue to make sure it stays in place. If you press on the reset button too hard, it could be pushed out from underneath! So be careful when pressing. The wires will need to go on **pin 12** and **pin 17** on the **GPIO1 header**. This will activate the **RESET** function of the board. You can also have it set to **OSD** or **USER** if you want. In this case the cable that goes to **pin 17** should go to either **pin 13** (OSD) or **pin 15** (USER) instead. I recommend using **bare wires** on one end and **female dupont** headers on the other end (see picture below). This way you can easily remove them should you want to in the future. :::note[QMTECH Board Users] If you are using the **QMTECH** board, you will need to bridge **pin 10** together with **pin 30**, otherwise the reset button will not work. Use a female Dupont to female Dupont cable for this. ::: ## Optional: Customizing the SNES Case Logo If you'd like, you can **remove the original logo** on the top front of the SNES case and replace it with a **custom-made logo** for this project. ### Materials Needed - **100% transparent sticker paper** (for applying the custom logo) - **Magic Eraser sponge** (acts like sanding paper but is gentler on the case) ### Step-by-Step Instructions 1. **Prepare the Sponge:** - Wet the **Magic Eraser sponge**. It will be used to gently remove the original logo. 2. **Wet Sanding Process:** - Begin rubbing the sponge on the logo in a **circular motion**. - Apply some pressure—the sponge is soft enough to not damage the shell. 3. **Be Patient:** - The wet sanding process can take about **15-20 minutes**. - Gradually, the original logo will **fade away**. Keep sanding until you're satisfied with the result. Once the logo is removed, you can then apply your custom logo using the **transparent sticker paper** for a sleek, professional look. ## Optional: 3D-Printed Backplate You can choose to make your own backplate, or opt to **3D print** one specifically designed for this project. - **3D Printable File**: Download it from the [SMES hardware files](https://github.com/ZaparooProject/zaparoo-hardware/tree/main/smes). :::note[Backplate Compatibility] The backplate is designed to fit **specific parts** (as mentioned earlier in the guide). If you choose to use different parts, there's a chance that the backplate may **not fit perfectly**. ::: ## Optional: Front Controller Ports & System LED You can use the front controller ports of the SMES, which offer **extremely low latency** due to being wired and support a **1000Hz polling rate**. Another neat feature of this mod is that the power to the front controller ports is the **same** power that lights up the **LED** on the SMES. So, you get two mods for the price of one! ### What You'll Need To set this up, you'll need the following components: - **Arduino Pro Micro (ATMega32U4)** board (available for around $5 on sites like AliExpress) - You only need **one board** to support **two controllers**. This guide assumes you want to use **both controller ports** since the same board supports both. 1. **Remove the front controller ports** carefully. 2. **Detach the ribbon cable**—you won't need it for this mod. ### Soldering Guide To complete this mod, you'll need to do some soldering. Follow the instructions below carefully. Check the diagram of the Arduino board and solder to the following points: - **LATCH**: 2x - **CLOCK**: 2x - **GROUND**: 2x - **VCC**: 2x - **DATA1**: 1x (for Controller 1) - **DATA2**: 1x (for Controller 2) :::info[Software Setup Required] The Arduino board requires specific software to work with the original SNES controllers. 1. Go to the following link for the necessary software: [DaemonBite-Retro-Controllers-USB Software](https://github.com/MickGyver/DaemonBite-Retro-Controllers-USB) 2. Follow the instructions provided on the page. ::: :::danger[Controller 2 Pin Orientation] When soldering the controller ports, **the pins for Controller 2 are inverted**. If you start from the left on Controller 1, start from the **opposite side** on Controller 2, or vice versa. ::: ### Final Check If you've soldered everything correctly, your setup should look something like this: ## Final Result This is it guys, the finish line! If you did everything correctly so far it should look a little something like this. Now if you don't have it exactly like this, that's completely okay! You can mix and match different items, it doesn't really matter. The important part is that you have a functional Super Zap Boy and a SMES. As long as that's working and you can enjoy games with it, that's all that matters! If somehow this wiki was not clear enough on a certain subject and you have questions still, then hit me up @Phoenix on the Zaparoo discord ([wizzo.dev](https://wizzo.dev/)). ## Optional: Cassette Cases and other cosmetics Wallpapers for this project are available in the [SMES wallpaper files](https://github.com/ZaparooProject/zaparoo-hardware/tree/main/smes/Wallpapers). The folder includes multiple **16:9 1920x1080** versions and one **4:3 640x480** version for CRTs. Use the one that you like best. If you plan on using these, I recommend turning off the MiSTer logo in the menu so it does not cover the wallpaper. You can do this in the `MiSTer.ini` file on the root of your SD card by changing `logo=1` to `logo=0`. And if you want one of those beautiful cases with the custom cover to go along with your cartridges, check out Anime0t4ku's [cassette case guide](../tokens/storage/cassette-cases.md). ## Credits _Project created, coordinated and wiki entry written, by Phoenix_ _BedroomNinja: Rewired the power switch, 3D designed and printed the backplate and the mount for the reset button_ _Anime0t4ku: Designed the SMES logo, SMES wallpapers and the final version of the Super Zap Boy logo_ _ArielAces: Made cartridge label templates, printed labels_ _Tim Wilsie: Created design of the Super Zap Boy logo_ _Wizzo: Supported and sent positive vibes :D_ _MickGyver: Created software for the Daemonbite Retro USB adapter which we used for the controller port_ Thank you so much to everyone who helped out! The complete SMES wouldn't be possible without BedroomNinja and his skills and a special thanks to Anime0t4ku, ArielAces and Tim Wilsie for helping out and Wizzo for making this all possible in the first place! Happy building everyone! --- ## API Encryption Zaparoo encrypts WebSocket traffic using PAKE-based pairing and AES-256-GCM with per-session HKDF-derived keys. This is the wire protocol reference for client implementors. ## Overview - **Pairing**: One-time PAKE2 (P-256) handshake. User enters a 6-digit PIN from the Zaparoo device into the client app. Both sides derive a shared 32-byte pairing key. The PIN is never transmitted. - **Encryption**: AES-256-GCM with counter-derived nonces on every WebSocket frame. - **Per-session keys**: Each WebSocket connection generates a random 16-byte session salt. Both sides derive ephemeral keys via HKDF-SHA256 (pairing key as IKM, session salt as HKDF salt). - **Scope**: WebSocket only. HTTP POST, SSE, and REST GET are localhost-restricted by default. ## Configuration Set `encryption` in `[service]` of `config.toml`: | Value | Behavior | |---|---| | `false` (default) | No encryption. All WebSocket connections accepted as plaintext. | | `true` | Remote WebSocket connections must send an encrypted first frame from a paired client. Localhost plaintext connections still work without pairing. | ## Pairing flow Two HTTP round trips establish a shared 32-byte pairing key. ```mermaid sequenceDiagram participant Z as Zaparoo participant C as Client Note over Z: User triggers "Pair device" Note over Z: Generate + display 6-digit PIN (5 min expiry) Note over C: User types PIN into client Note over C: A = pake.InitCurve(pin, 0, "p256") C->>Z: POST /api/pair/start{ pake: base64(wireFormat(A)), name: "MyApp" } Note over Z: B = pake.InitCurve(pin, 1, "p256")B.Update(wireFormat(A)) Z->>C: 200 { session: uuid, pake: base64(wireFormat(B)) } Note over C: A.Update(wireFormat(B))sessionKey = A.SessionKey()salt = msgA || msgBprk = HKDF-Extract(sessionKey, salt)confirmKeyA = HKDF-Expand(prk, "zaparoo-confirm-A", 32)confirmKeyB = HKDF-Expand(prk, "zaparoo-confirm-B", 32)pairingKey = HKDF-Expand(prk, "zaparoo-pairing-v1", 32) C->>Z: POST /api/pair/finish{ session: uuid, confirm: base64(HMAC) } Note over Z: Verify client HMAC, persist client Z->>C: 200 { authToken: uuid, clientId: uuid, confirm: base64(HMAC) } Note over C: Verify server HMACStore authToken + pairingKey Note over Z: Clear PIN ``` ### HMAC transcript Confirm HMACs use length-prefixed encoding: ```text LP(field) = 4-byte big-endian uint32 length || field bytes ``` Full transcript: ```text LP("zaparoo-v1") || LP("p256") || LP(role) || LP(clientName) || LP(MsgA) || LP(MsgB) ``` Where `role` is `"client"` or `"server"`, and `MsgA`/`MsgB` are the **raw bytes sent over the wire** (not the result of calling `pake.Bytes()` again after `Update()`). ### PAKE message format The `pake` field in `/pair/start` request and response carries a base64-encoded JSON object. The PAKE protocol is based on [schollz/pake](https://github.com/schollz/pake) v3 using the P-256 curve. Coordinates are string-encoded for cross-language compatibility. | Field | Type | Description | |---|---|---| | `role` | number | `0` (initiator/client) or `1` (responder/server) | | `ux` | string | U-point x-coordinate, decimal integer | | `uy` | string | U-point y-coordinate, decimal integer | | `vx` | string | V-point x-coordinate, decimal integer | | `vy` | string | V-point y-coordinate, decimal integer | | `xx` | string | X-point x-coordinate, decimal integer | | `xy` | string | X-point y-coordinate, decimal integer | | `yx` | string | Y-point x-coordinate, decimal integer | | `yy` | string | Y-point y-coordinate, decimal integer | All coordinate values are arbitrary-precision integers encoded as **quoted decimal strings** (not bare JSON numbers). This avoids precision loss in JSON parsers that use IEEE 754 doubles. Fields for points not yet computed in the current protocol step are `"0"`. Example client message (role 0, message A, Y not yet computed): ```json { "role": 0, "ux": "793136080485469241208656611513609866400481671852", "uy": "59748757929350367369315811184980635230185250460108398961713395032485227207304", "vx": "1086685267857089638167386722555472967068468061489", "vy": "9157340230202296554417312816309453883742349874205386245733062928888341584123", "xx": "48439561293906451759052585252797914202762949526041747995844080717082404635286", "xy": "36134250956749795798585127919587881956611106672985015071877198253568414405109", "yx": "0", "yy": "0" } ``` ### Pairing limits - **PIN**: 6 decimal digits, `crypto/rand`. ~20 bits of entropy. - **Expiry**: 5 minutes from PIN generation. - **Attempts**: 3 failed HMAC verifications per PIN before invalidation. - **Sessions**: `/pair/start` session expires after 2 minutes. - **Client name**: max 128 bytes. - **Max paired clients**: 50 per device. - **Rate limit**: 1 req/sec per IP on `/api/pair/*`. ## Message format Every encrypted WebSocket connection starts with a first frame that establishes the session. ### First frame (client → server) ```json { "v": 1, "e": "", "t": "", "s": "" } ``` | Field | Description | |---|---| | `v` | Protocol version. Currently `1`. Server returns a plaintext error if unsupported (see [Errors](#errors)). | | `e` | AES-256-GCM ciphertext of the JSON-RPC request, base64-encoded. | | `t` | Auth token (UUID) identifying the paired client. Not a secret; used for key lookup only. | | `s` | 16-byte random session salt, base64-encoded. Must be exactly 16 bytes. | ### Subsequent frames (both directions) ```json { "e": "" } ``` Counters are implicit: both sides start at 0 and increment per frame. WebSocket guarantees ordering. If decryption fails, the connection is closed. Server notifications (e.g. `media.started`) use this same format, encrypted with the session keys. ### Decrypted payload Standard JSON-RPC 2.0: ```json { "jsonrpc": "2.0", "method": "version", "params": {}, "id": 1 } ``` ## Session cryptography Each WebSocket connection derives its own ephemeral keys from a fresh random session salt. ### Key derivation ```text prk = HKDF-Extract(SHA-256, ikm=pairingKey, salt=sessionSalt) c2sKey = HKDF-Expand(SHA-256, prk, info="zaparoo-c2s-v1", length=32) s2cKey = HKDF-Expand(SHA-256, prk, info="zaparoo-s2c-v1", length=32) c2sBase = HKDF-Expand(SHA-256, prk, info="zaparoo-c2s-nonce-v1", length=12) s2cBase = HKDF-Expand(SHA-256, prk, info="zaparoo-s2c-nonce-v1", length=12) ``` ### Nonces 12-byte AES-GCM nonce per frame: XOR a big-endian counter into the last 8 bytes of the nonce base: ```text nonce[0:4] = base[0:4] nonce[4:12] = base[4:12] XOR (counter as 8 bytes big-endian) ``` Counters don't wrap. Disconnect and reconnect with a fresh salt to start over. ### AAD All encrypt/decrypt operations bind ciphertext to the session: ```text aad = authToken + ":ws" ``` ## Security limits - **Salt reuse**: The server rejects duplicate session salts per client (200-entry / 10-minute sliding window). Always use a CSPRNG for session salts. - **Failed frames**: 10 consecutive first-frame decryption failures per (authToken, IP) triggers a 30-second block with exponential backoff, capped at 30 minutes. ## Client dependencies **Pairing** needs raw P-256 elliptic curve point arithmetic: | Platform | Library | |---|---| | JavaScript | [`@noble/curves`](https://github.com/paulmillr/noble-curves) | | Python | `ecdsa` or `cryptography` | | Swift | [Swift Crypto](https://github.com/apple/swift-crypto) | | Kotlin/Android | Bouncy Castle `ECPoint` | | C#/.NET | BouncyCastle NuGet | | Rust | `p256` crate | **Per-connection encryption** is just HKDF-SHA256 + AES-256-GCM + HMAC-SHA256, all in platform stdlibs (Web Crypto, CryptoKit, JCE, .NET, RustCrypto). ## Client key storage | Platform | Recommended storage | |---|---| | iOS | Keychain | | Android | EncryptedSharedPreferences / Keystore | | Web/Electron | OS keychain via `keytar` or similar | | CLI tools | File mode `0600` in user config directory | Store both the pairing key (32 bytes) and auth token (UUID). The auth token isn't secret, but it's bound to the key. Lose either and you need to re-pair. ## Errors HTTP errors on pairing endpoints: | Status | Endpoint | Meaning | |---|---|---| | 400 | `/pair/*` | Malformed request body or PAKE message | | 401 | `/pair/finish` | HMAC mismatch (wrong PIN) | | 403 | `/pair/start` | Max paired clients reached, or attempts exhausted | | 404 | `/pair/finish` | Unknown session ID | | 410 | `/pair/*` | Pairing PIN expired | | 429 | `/pair/*` | Rate limit exceeded | WebSocket errors (plaintext JSON-RPC error, then connection closed): | Code | Meaning | |---|---| | -32001 | Unsupported encryption version | | -32002 | Encryption required. Remote clients must send an encrypted first frame. | ## Connection lifecycle If the client doesn't know whether encryption is on: 1. Try connecting plaintext. 2. If the server returns `-32002`, you need to pair first. 3. Prompt the user to start pairing from the Zaparoo device. 4. Run the PAKE handshake (`/pair/start` → `/pair/finish`). Derive `pairingKey` locally via HKDF (see [Pairing flow](#pairing-flow)). Store `authToken` and `pairingKey`. 5. On future connections: fresh session salt, derive session keys (see [Session cryptography](#session-cryptography)), send the encrypted first frame. ## Non-WebSocket transports HTTP POST, SSE, and REST GET (`/api`, `/api/events`, `/r/*`, `/run/*`) are localhost-only by default. Add IPs or CIDR ranges to `allowed_ips` in config for remote access. No encryption on these transports, they're for simple integrations on trusted networks. API key auth still applies. ## Pseudocode example Encrypted session lifecycle, assuming pairing is already done: ```text function connect(url, authToken, pairingKey): sessionSalt = random_bytes(16) // Derive per-session keys via HKDF-SHA256 prk = hkdf_extract(sha256, ikm=pairingKey, salt=sessionSalt) c2sKey = hkdf_expand(prk, info="zaparoo-c2s-v1", len=32) s2cKey = hkdf_expand(prk, info="zaparoo-s2c-v1", len=32) c2sBase = hkdf_expand(prk, info="zaparoo-c2s-nonce-v1", len=12) s2cBase = hkdf_expand(prk, info="zaparoo-s2c-nonce-v1", len=12) aad = encode(authToken + ":ws") sendCounter = 0 recvCounter = 0 ws = websocket_connect(url) function send(method, params): payload = json_encode({ jsonrpc: "2.0", method, params, id: next_id() }) nonce = xor_counter(c2sBase, sendCounter) ct = aes_gcm_encrypt(c2sKey, nonce, aad, payload) frame = { e: base64(ct) } if sendCounter == 0: frame.v = 1 frame.t = authToken frame.s = base64(sessionSalt) sendCounter++ ws.send(json_encode(frame)) function on_receive(raw): frame = json_decode(raw) nonce = xor_counter(s2cBase, recvCounter) pt = aes_gcm_decrypt(s2cKey, nonce, aad, base64_decode(frame.e)) recvCounter++ return json_decode(pt) return { send, on_receive } ``` --- ## Core API :::warning This API is not finalized. A version 1 release will be announced when it is ready for production. You're welcome to use the API as is, but be aware that breaking changes may still happen. ::: The **Core API** is available on and published by every device running the [Zaparoo Core](../../core) software. This API allows management of all Zaparoo features locally and remotely. The [Zaparoo](https://zaparoo.app/) app uses this API for all communication with Zaparoo devices, as do most of the flags when Zaparoo is run via the command line. This page documents the protocol used to communicate with the API and how to interact with it. _It is currently the source of truth when developing applications that work with Zaparoo._ ## Communication Protocol The API uses a standard [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) connection to exchange [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) payloads using the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) protocol. Communication follows a loose client-server relationship. Clients, by default, are not expected to implement the API beyond what that particular client needs to function. ### Connection Connections to the API can be established with any standard WebSocket client, using the versioned endpoint of the HTTP server published by the Zaparoo service. By default, the HTTP server is accessible on port `7497`. This port is configurable by the user. These endpoints are currently available: - `/api/v0.1` - `/api/v0` - `/api/` It's highly recommended to target your application at the specific minor version of the endpoint you're using, which will maintain compatibility during API updates. An example address for connecting to the API: `ws://10.0.0.123:7497/api/v0.1` The connection requires no special configuration or authentication to initiate. When [encryption](./encryption) is enabled, remote clients must complete a one-time pairing and encrypt all WebSocket traffic. ### HTTP POST All API methods can also be called via HTTP POST requests to the same versioned endpoints used for WebSocket connections. This provides an alternative for applications that prefer REST-style communication over persistent WebSocket connections. These HTTP endpoints are available: - `/api/v0.1` - `/api/v0` - `/api/` HTTP requests must use the `POST` method with `Content-Type: application/json` and send the same JSON-RPC 2.0 formatted payloads as WebSocket connections. The server will respond with a single JSON-RPC 2.0 response object. An example HTTP request to get the server version: ```bash curl -X POST http://10.0.0.123:7497/api/v0.1 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": "550e8400-e29b-41d4-a716-446655440000", "method": "version" }' ``` This would return a response like: ```json { "jsonrpc": "2.0", "id": "550e8400-e29b-41d4-a716-446655440000", "result": { "version": "0.7.0", "platform": "linux" } } ``` Unlike WebSocket connections, HTTP requests are stateless and do not support notifications. Each request requires a complete JSON-RPC 2.0 payload and will receive a single response. ### Server-Sent Events (SSE) The API provides a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) endpoint for receiving notifications over a standard HTTP connection. This is useful for clients that only need to receive notifications without the full bidirectional communication of WebSocket, and works with any HTTP client without additional libraries. These SSE endpoints are available: - `/api/v0.1/events` - `/api/v0/events` - `/api/events` An example using `curl`: ```bash curl -N http://10.0.0.123:7497/api/v0.1/events ``` Each notification is sent as an SSE `data` field containing the same JSON-RPC 2.0 notification object used by WebSocket: ``` data: {"jsonrpc":"2.0","method":"tokens.added","params":{"type":"nfc","uid":"04E1234567890","text":"**launch:game.rom"}} data: {"jsonrpc":"2.0","method":"media.started","params":{"systemId":"NES","systemName":"NES","name":"game.rom"}} ``` SSE connections are long-lived and will continue receiving events until the client disconnects. To call methods, use HTTP POST to the standard API endpoint alongside the SSE connection. ### JSON Payloads Server and clients communicate back and forth using JSON payloads, following the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) protocol. Because a WebSocket connection is asynchronous, request payloads are tagged with a unique ID. The client must keep track of IDs sent to another client and wait for a matching response object. A client can continue sending requests while waiting for responses to previous requests. #### Requests A request object asks the connected server to run a predefined [method](#methods), and report back when it's completed with a response object. An example request: ```json { "jsonrpc": "2.0", "id": "4b5da056-a5d4-436b-b4e6-b96231e99969", "method": "media.search", "params": { "query": "240p" } } ``` This request would query Zaparoo's media database for a filename containing the word "240p" and return a response with the search results. ##### Request object | Key | Type | Required | Description | | :------ | :----- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | jsonrpc | string | Yes | As per the JSON-RPC 2.0 spec, this key's value must be the string `2.0` for a payload to be accepted. | | id | string | Yes\* | A UUID generated by the requesting client, used to match requests back to responses. A request missing this key is valid but would be treated as a notification and not receive any response. | | method | string | Yes | A string corresponding to a method to be run by the receiving server. | | params | any | No | Arguments supplied for the method. The value of this key depends on the method used and is omitted for some methods. | All available request methods and their parameters are [documented below](#methods). #### Notifications Notifications are requests which do not contain an ID. Otherwise, they are identical to a standard request object. Notifications can be sent by either server or client and do not receive a response. Like standard requests, notifications may or may not have parameters and its value will depend on the method. Types of notifications are [documented below](#notifications). #### Responses Every request sent must have a matching response. An example response to the `media.search` request shown above: ```json { "jsonrpc": "2.0", "id": "4b5da056-a5d4-436b-b4e6-b96231e99969", "result": { "results": [ { "system": { "id": "Gameboy", "name": "Gameboy" }, "name": "240p Test Suite (PD) v0.03 tepples", "path": "Gameboy/240p Test Suite (PD) v0.03 tepples.gb", "zapScript": "@Gameboy/240p Test Suite (PD) v0.03 tepples", "tags": [] } ], "total": 1, "pagination": { "hasNextPage": false, "pageSize": 100 } } } ``` ##### Response object | Key | Type | Required | Description | | :------ | :----- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | | jsonrpc | string | Yes | Same as a [request](#requests). | | id | string | Yes | Same as a [request](#requests). The same ID sent by the original request. | | result | any | No\* | Return value of the method. May be `null` depending on the method, will be missing if there was an error. See [methods](#methods) for possible values. | | error | Error | No\* | If a method failed, this key will be populated with the error details and the result key will be empty. See [below](#response-errors) for details about errors. | ##### Response errors If a method fails, it will populate the `error` key in the response object with details about the failure. An example of a failed request: ```json { "jsonrpc": "2.0", "id": "4b5da056-a5d4-436b-b4e6-b96231e99969", "error": { "code": 1, "message": "invalid cursor: invalid base64" } } ``` ##### Error object | Key | Type | Required | Description | | :------ | :----- | :------- | :---------------------------------------------------------------------------------------- | | code | number | Yes | An integer specifying the general error category. **Error codes are not yet formalised.** | | message | string | Yes | Short human readable message explaining the error cause, if possible. | #### Protocol Errors If a low-level error occurs before a request context can be established, a protocol error will be sent back. This can happen, for example, if a JSON payload is malformed or a payload could not be decrypted. They're identical to an error response except they will have no ID. Protocol errors may be sent cleartext if a secure context couldn't be established, but will not contain any sensitive data. ### Anonymous Access Anonymous cleartext access is, generally, allowed when an API connection is made from a loopback address (i.e. from the same device Zaparoo is running). This access depends on the platform and whether the service is running with elevated privileges. Check the page for the specific platform you're using to make sure it's available to you. This access is also allowed when a connection is made over a WebSocket Secure (wss) connection. ### Heartbeat If sent the bytes `ping`, the API will immediately respond with the bytes `pong`. This feature can be used by heartbeat functions in WebSocket libraries. ## Launch Endpoint The HTTP server has additional endpoints which allow restricted access to launch ZapScript using a GET request. These endpoints are specifically meant to support uses such as QR codes scanned by a phone's camera app or simple launch testing. The following endpoints are available: - `/run/` - Preferred endpoint for launching ZapScript. - `/r/` - Alias for `/run/`. - `/l/` - **Deprecated.** Use `/run/` instead. An example request: `GET http://10.0.0.123:7497/run/**launch.system:snes` This would act as though a token with the text `**launch.system:snes` had been scanned. Requests from the local device are allowed without restriction. Remote requests must be explicitly allowed using the `allow_launch` config file setting. ## Methods Methods are used to execute actions and request data back from the API. These API docs cover **62 methods** across core functionality areas, including deprecated aliases. See the [API Methods](./methods) page for detailed definitions and examples of each method. | ID | Description | | :------------------------------ | :------------------------------------------------------------------------------------ | | launch | **Deprecated.** Alias for `run`. | | run | Run supplied ZapScript. | | stop | Kill any active launcher, if possible. | | confirm | Confirm and launch the currently staged token. | | tokens | List active tokens. | | tokens.history | Return a list of the latest token launches. | | media | Return status and statistics about media database. | | media.active | Return the currently active (now playing) media. | | media.active.update | Update the currently active media information. | | media.search | Query the media database and return all matching indexed media. | | media.tags | Query available tags for filtering media search results. | | media.tags.update | Add or remove user tags for indexed media. | | media.generate | Start a new media database index. | | media.generate.cancel | Cancel any currently running media database indexing operation. | | media.generate.resume | Resume paused media database indexing. | | media.index | **Deprecated.** Alias for `media.generate`. | | media.browse | Browse indexed media in a directory-style hierarchy. | | media.browse.index | Return first-character jump-to-letter buckets and seek cursors for a browse scope. | | media.lookup | Resolve a game name and system to a media database match. | | media.meta | Return metadata for a specific indexed media row. | | media.meta.update | Update writable metadata fields for a specific indexed media row. | | media.image | Return the best matching image for a specific indexed media row. | | media.clean.orphans | Remove orphaned media database rows. | | scrapers | List available metadata scrapers. | | media.scrape | Start metadata scraping for indexed media. | | media.scrape.status | Return the latest metadata scraping status. | | media.scrape.cancel | Cancel any currently running metadata scrape. | | media.scrape.resume | Resume paused metadata scraping. | | media.history | Return paginated media play history. | | media.control | Send a control action to the active media's launcher. | | media.history.top | Return most-played media ranked by total play time. | | playtime | Query current playtime session status and usage statistics. | | systems | List all currently indexed systems. | | settings | List current configuration settings. | | settings.update | Update one or more settings in-memory and save changes to disk. | | settings.reload | Reload settings from disk. | | settings.auth.claim | Claim API credentials from a remote server. | | settings.logs.download | Download the current log file as base64-encoded content. | | settings.playtime.limits | Get current playtime limit configuration. | | settings.playtime.limits.update | Update playtime limit settings. | | mappings | List all mappings. | | mappings.new | Create a new mapping. | | mappings.update | Change an existing mapping. | | mappings.delete | Delete an existing mapping. | | mappings.reload | Reload mappings from disk. | | readers | List all currently connected readers and their capabilities. | | readers.write | Attempt to write given text to the first available write-capable reader, if possible. | | readers.write.cancel | Cancel any active write operation. | | launchers | List all launchers known to the running service. | | launchers.refresh | Refresh the internal launcher cache, forcing a reload of launcher configurations. | | version | Return server's current version and platform. | | health | Simple health check to verify the server is running and responding. | | inbox | List all inbox messages. | | inbox.delete | Delete a specific inbox message by ID. | | inbox.clear | Delete all inbox messages. | | clients | List paired API clients. | | clients.delete | Delete a paired API client. | | input.keyboard | Send a keyboard input sequence. | | input.gamepad | Send a gamepad input sequence. | | screenshot | Capture a screenshot of the current platform display. | | update.check | Check if a newer version is available. | | update.apply | Download and apply the latest update with graceful restart. | ## Notifications Notifications let a server or client know an event has occurred. See the [API Notifications](./notifications) page for detailed definitions and examples of each notification. | ID | Description | | :--------------------- | :-------------------------------------------------------------------------------- | | readers.added | A new reader was connected to the server. | | readers.removed | A connected reader was disconnected from the server. | | tokens.added | A new token detected by a reader. | | tokens.removed | A token was removed. | | tokens.staged | A token was staged by launch guard and is awaiting confirmation. | | tokens.staged.ready | A staged token's delay period has expired and is ready for confirmation. | | media.started | New media was started on server. | | media.stopped | Media has stopped on server. | | media.indexing | The state of the indexing or optimization process has changed. | | media.scraping | Progress updates emitted during media scraping (includes progress/status details). | | playtime.limit.reached | A playtime limit (session or daily) has been reached and enforced. | | playtime.limit.warning | A playtime warning notification sent at configured intervals before limit reached. | | inbox.added | A new inbox message was added to the server. | --- ## Methods Methods are used to execute actions and request data back from the API. ## Launching ### run Emulate the scanning of a token. #### Parameters Accepts two types of parameters: - A string, in which case the string will be treated as the token text with all other options set as default. - An object: | Key | Type | Required | Description | | :----- | :------ | :------- | :------------------------------------------------------------------------------------------------------------- | | type | string | No | An internal category of the type of token being scanned. _Not currently in use outside of logging._ | | uid | string | No\* | The UID of the token being scanned. For example, the UID of an NFC tag. Used for matching mappings. | | text | string | No\* | The main text to be processed from a scan, should contain [ZapScript](../../zapscript/index.md). | | data | string | No\* | The raw data read from a token, converted to a hexadecimal string. Used in mappings and detection of NFC toys. | | unsafe | boolean | No | Allow unsafe operations. Default is false. | These parameters allow emulating a token exactly as it would be read directly from an attached reader on the server. A request's parameters must contain at least a populated `uid`, `text` or `data` value. #### Result Returns `null` on success. Currently, it is not reported if the launched ZapScript encountered an error during launching, and the method will return before execution of ZapScript is complete. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "52f6242e-7a5a-11ef-bf93-020304050607", "method": "run", "params": { "text": "**launch.system:snes" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "52f6242e-7a5a-11ef-bf93-020304050607", "result": null } ``` ### stop Kill any active launcher, if possible. This method is highly dependant on the platform and specific launcher used. It's not guaranteed that a launcher is capable of killing the playing process. #### Parameters None. #### Result Returns `null` on success. Currently, it is not reported if a process was killed or not. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "176b4558-7a5b-11ef-b318-020304050607", "method": "stop" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "176b4558-7a5b-11ef-b318-020304050607", "result": null } ``` ### confirm Confirm and launch a staged token from the launch guard. When launch guard is enabled and media is playing, scanned tokens are staged instead of launched immediately. This method confirms the currently staged token and launches it. #### Parameters None. #### Result Returns `null` on success. Returns an error if no token is currently staged. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5b-11ef-b318-020304050607", "method": "confirm" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5b-11ef-b318-020304050607", "result": null } ``` ## Tokens ### tokens Returns information about active and last scanned tokens. #### Parameters None. #### Result | Key | Type | Required | Description | | :----- | :------------------------------ | :------- | :------------------------------------------------------------- | | active | [TokenResponse](#token-object)[] | Yes | A list of currently active tokens. | | last | [TokenResponse](#token-object) | No | The last scanned token. Null if no token has been scanned yet. | ##### Token object | Key | Type | Required | Description | | :------- | :------ | :------- | :----------------------------------------------- | | type | string | Yes | Type of token. | | uid | string | Yes | UID of the token. | | text | string | Yes | Text content of the token. | | data | string | Yes | Raw data of the token as hexadecimal string. | | scanTime | string | Yes | Timestamp of when the token was scanned in RFC3339 format. | | readerId | string | No | ID of the reader that scanned the token. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "5e9f3a0e-7a5b-11ef-8084-020304050607", "method": "tokens" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "5e9f3a0e-7a5b-11ef-8084-020304050607", "result": { "active": [], "last": { "type": "", "uid": "", "text": "**launch.system:snes", "data": "", "scanTime": "2024-09-24T17:49:42.938167429+08:00" } } } ``` ### tokens.history Returns a list of the last recorded token launches. #### Parameters None. #### Result | Key | Type | Required | Description | | :------ | :------------------------------------ | :------- | :--------------------------------- | | entries | [LaunchEntry](#launch-entry-object)[] | Yes | A list of recorded token launches. | ##### Launch entry object | Key | Type | Required | Description | | :------ | :------ | :------- | :---------------------------------------------- | | data | string | Yes | Raw data of the token as hexadecimal string. | | success | boolean | Yes | True if the launch was successful. | | text | string | Yes | Text content of the token. | | time | string | Yes | Timestamp of the launch time in RFC3339 format. | | type | string | Yes | Type of token. | | uid | string | Yes | UID of the token. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "5e9f3a0e-7a5b-11ef-8084-020304050607", "method": "tokens.history" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "5e9f3a0e-7a5b-11ef-8084-020304050607", "result": { "entries": [ { "data": "", "success": true, "text": "**launch.system:snes", "time": "2024-09-24T17:49:42.938167429+08:00", "type": "", "uid": "" } ] } } ``` ## Media ### media Returns the current media database status and active media. The database status includes both indexing and optimization information: - **Indexing** takes priority over optimization in the response (if both are running, only indexing status is shown) - **Optimization** status and progress are shown when no indexing is in progress #### Parameters None. #### Result | Key | Type | Required | Description | | :-------- | :---------------------------------------- | :------- | :------------------------------------- | | database | [IndexingStatus](#indexing-status-object) | Yes | Status of the media database. | | active | [ActiveMedia](#active-media-object)[] | Yes | List of currently active media. | | playlists | [PlaylistState](#playlist-state-object)[] | No | Currently active playlist slots. | ##### Indexing status object | Key | Type | Required | Description | | :----------------- | :----- | :------- | :----------------------------------------------- | | exists | boolean| Yes | True if the database exists. | | indexing | boolean| Yes | True if indexing is currently in progress. | | optimizing | boolean| Yes | True if database optimization is currently in progress. | | totalSteps | number | No | Total number of indexing steps. | | currentStep | number | No | Current indexing step. | | currentStepDisplay | string | No | Display name of the current indexing step or optimization step. | | totalFiles | number | No | Total number of files to index. | | totalMedia | number | No | Total number of media entries in the database. Only included when database exists and is not indexing. | ##### Active media object | Key | Type | Required | Description | | :--------------- | :------- | :------- | :----------------------------------------- | | mediaId | number | No | Opaque media database row ID for efficient follow-up `media.meta` and `media.image` requests. Omitted when the active path cannot be resolved in the current media database. | | launcherId | string | Yes | ID of the launcher. | | systemId | string | Yes | ID of the system. | | systemName | string | Yes | Display name of the system. | | mediaPath | string | Yes | Path to the media file. | | relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. | | positionMs | number | No | Current playback position in milliseconds for native audio media. | | durationMs | number | No | Total playback duration in milliseconds for native audio media. | | mediaName | string | Yes | Display name of the media. | | slot | string | No | Media slot for the item. Omitted or `primary` is foreground media; `background` is background audio. | | started | string | Yes | Timestamp when media started in RFC3339 format. | | zapScript | string | Yes | ZapScript command to launch this media item. | | launcherControls | string[] | No | List of control action names supported by the active launcher. Only present if the launcher supports controls. See [media.control](#mediacontrol). | ##### Playlist state object | Key | Type | Required | Description | | :------ | :----- | :------- | :---------- | | id | string | Yes | Playlist ID. | | name | string | Yes | Playlist display name. | | slot | string | Yes | Playlist slot, `primary` or `background`. | | repeat | string | Yes | Repeat mode: `none`, `all`, or `one`. | | items | object[] | Yes | Playlist items. | | index | number | Yes | Zero-based current item index. | | total | number | Yes | Total item count. | | playing | boolean | Yes | Whether playlist slot is playing. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "method": "media" } ``` ##### Response (database ready) ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "result": { "database": { "exists": true, "indexing": false, "optimizing": false, "totalMedia": 1337 }, "active": [] } } ``` ##### Response (optimization in progress) ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "result": { "database": { "exists": true, "indexing": false, "optimizing": true, "currentStepDisplay": "vacuum", "totalMedia": 1337 }, "active": [] } } ``` ### media.search Query the media database and return all matching indexed media. **Note:** This API now uses cursor-based pagination for all requests. The `total` field is deprecated and always returns -1. Use the `pagination` object to navigate through results. For subsequent pages, include the `nextCursor` value in the `cursor` parameter of your next request. #### Parameters An object: | Key | Type | Required | Description | | :--------- | :------- | :------- | :----------------------------------------------------------------------------------------------------------------------------- | | query | string | No | Case-insensitive search by filename. By default, query is split by white space and results are found which contain every word. If omitted, all media is returned. | | systems | string[] | No | Case-sensitive list of system IDs to restrict search to. A missing key or empty list will search all systems. | | maxResults | number | No | Max number of results to return. Default is 100. | | cursor | string | No | Cursor for pagination. Omit for first page, use `nextCursor` from previous response for subsequent pages. | | tags | string[] | No | Filter results by tags. Maximum 50 tags, each up to 128 characters. Tags are case-sensitive and results must match all provided tags. Can be used without query or systems for tag-only searches. | | letter | string | No | Filter results by first character of game name. Supports: A-Z (single letters), "0-9" (numbers), "#" (symbols). Case-insensitive. | | fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in the `systems` array (e.g., `"snes"` matches `"SNES"`). | #### Result | Key | Type | Required | Description | | :--------- | :--------------------------------- | :------- | :------------------------------------------------------------------------------ | | results | Media[] | Yes | A list of all search results from the given query. | | total | number | Yes | **Deprecated:** Returns the count of results in the current response page. Use pagination info for navigation. | | pagination | [Pagination](#pagination-object) | Yes | Pagination information for cursor-based navigation. | ##### Media object | Key | Type | Required | Description | | :-------- | :----------------------- | :------- | :---------------------------------------------------------------------------------------------------------- | | mediaId | number | No | Opaque media database row ID for efficient follow-up `media.meta` and `media.image` requests. | | system | [System](#system-object) | Yes | System which the media has been indexed under. | | name | string | Yes | A human-readable version of the result's filename without a file extension. | | path | string | Yes | Canonical indexed media path. Use with `system.id` for `media.meta` and `media.image`. | | relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. | | zapScript | string | Yes | ZapScript command to launch this media item. Includes the disambiguating tags inline (e.g. `@Arcade/X-Men Vs. Street Fighter (region:eu) (builddate:1996-10-04)`) so the written command resolves back to this specific variant. | | tags | [TagInfo](#taginfo-object)[] | Yes | Array of tags associated with this media item. | | disambiguatingTags | [TagInfo](#taginfo-object)[] | No | Subset of `tags` whose values differ across same-named siblings of this title, ordered by display importance. Omitted when the title has nothing to disambiguate. Clients can render these to tell variants apart. | ##### System object | Key | Type | Required | Description | | :----------- | :----- | :------- | :----------------------------------------------------------------------- | | id | string | No | Internal system ID for this system. | | name | string | No | Display name of the system. | | category | string | No | Category of system (e.g., "Console", "Computer"). Not yet formalised. | | releaseDate | string | No | Release date of the system in ISO 8601 format (YYYY-MM-DD). | | manufacturer | string | No | Manufacturer of the system (e.g., "Nintendo", "Sega"). | ##### Pagination object | Key | Type | Required | Description | | :---------- | :------ | :------- | :-------------------------------------------------------------------------- | | nextCursor | string | No | Cursor for the next page of results. Omitted if no more pages available. | | hasNextPage | boolean | Yes | Whether there are more results available after the current page. | | pageSize | number | Yes | Number of results requested for this page (matches `maxResults` parameter). | ##### TagInfo object | Key | Type | Required | Description | | :--- | :----- | :------- | :---------------------------------------------------- | | tag | string | Yes | The tag name. | | type | string | Yes | The type/category of the tag (e.g., "genre", "year"). | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "method": "media.search", "params": { "query": "240p" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "result": { "results": [ { "mediaId": 123, "name": "240p Test Suite (PD) v0.03 tepples", "path": "/media/fat/games/Gameboy/240p Test Suite (PD) v0.03 tepples.gb", "relativePath": "Gameboy/240p Test Suite (PD) v0.03 tepples.gb", "zapScript": "@Gameboy/240p Test Suite (PD) v0.03 tepples", "system": { "category": "Handheld", "id": "Gameboy", "name": "Gameboy" }, "tags": [ { "tag": "test", "type": "category" }, { "tag": "homebrew", "type": "category" } ] } ], "total": 1, "pagination": { "hasNextPage": false, "pageSize": 100 } } } ``` ##### Example with tag filtering ###### Request ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607", "method": "media.search", "params": { "query": "mario", "tags": ["platformer", "nintendo"], "maxResults": 10 } } ``` ###### Response ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607", "result": { "results": [ { "mediaId": 456, "name": "Super Mario Bros.", "path": "/media/fat/games/NES/Super Mario Bros.nes", "relativePath": "NES/Super Mario Bros.nes", "zapScript": "@NES/Super Mario Bros. (year:1985)", "system": { "category": "Console", "id": "NES", "name": "Nintendo Entertainment System" }, "tags": [ { "tag": "platformer", "type": "genre" }, { "tag": "nintendo", "type": "publisher" }, { "tag": "1985", "type": "year" } ] } ], "total": 1, "pagination": { "hasNextPage": false, "pageSize": 10 } } } ``` ### media.browse Browse indexed media content by directory, similar to navigating a file manager. Supports filesystem paths, virtual URI schemes (e.g. `mame-arcade://`), and paginated results. When called without a `path` parameter (or with an empty path), returns top-level root entries including filesystem roots and virtual scheme roots. When `systems` is provided without `path`, returns populated launcher routes for those systems only. Pass the same `systems` filter when browsing a returned route to keep shared paths scoped to the selected systems. #### Parameters All parameters are optional. When called with no parameters, returns root entries. | Key | Type | Required | Description | | :--------- | :----- | :------- | :--------------------------------------------------------------------------------------------------------- | | path | string | No | Directory path to browse. Omit or set empty to list root entries. Supports filesystem paths and virtual URI schemes (e.g. `mame-arcade://`). | | systems | string[] | No | Case-sensitive list of system IDs to restrict route discovery and browse results to. A missing key or empty list preserves unfiltered behavior. | | fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in the `systems` array (e.g., `"snes"` matches `"SNES"`). | | maxResults | number | No | Maximum results per page. Default is 100, maximum is 1000. | | cursor | string | No | Opaque pagination cursor from a previous response's `nextCursor`. Omit for first page. Cursors are valid only with the same path, systems, letter, and sort parameters. | | letter | string | No | Filter results to entries starting with this letter. | | sort | string | No | Sort order. One of: `name-asc` (default), `name-desc`, `filename-asc`, `filename-desc`. Name sorting is prefix-aware for detected ranked/date collection folders. The `filename` variants sort by full file path. | #### Result | Key | Type | Required | Description | | :--------- | :------------------------------------ | :------- | :----------------------------------------------------------------------- | | path | string | Yes | The browsed directory path. Empty string when listing roots. | | entries | [BrowseEntry](#browse-entry-object)[] | Yes | Array of entries in the current path. | | totalFiles | number | Yes | Total count of media files in the current directory (respects `letter` filter). | | pagination | [Pagination](#browse-pagination-object) | No | Pagination info. Omitted when there are no file results. | ##### Browse entry object | Key | Type | Required | Description | | :----------- | :------- | :------- | :----------------------------------------------------------------------------------------------- | | mediaId | number | No | Opaque media database row ID. Present on `media` entries, and on zip-as-directory platform `directory` entries whose direct contents collapse to one logical launch target, for efficient follow-up `media.meta` and `media.image` requests. | | name | string | Yes | Display name of the entry. | | path | string | Yes | Full path to the entry. | | type | string | Yes | Entry type: `root`, `directory`, or `media`. | | fileCount | number | No | Number of files in this directory. Present on `root` and `directory` entries. | | group | string | No | Launcher group name. Present on virtual scheme `root` entries. | | systemId | string | No | System ID for the media or single-system filtered route (e.g. `SNES`). Present on `media` entries and filtered `root` entries when exactly one system applies. | | systemIds | string[] | No | System IDs represented by a filtered `root` or `directory` entry. | | zapScript | string | No | ZapScript command to launch this media. Present on `media` entries and logical single-game container `directory` entries on zip-as-directory platforms. | | relativePath | string | No | Relative path from root directory. Present on `media` entries and logical single-game container `directory` entries on zip-as-directory platforms. | | tags | object[] | No | Tags attached to the media. Each object has `tag` (string) and `type` (string). Present on `media` entries and logical single-game container `directory` entries on zip-as-directory platforms. | | disambiguatingTags | object[] | No | Subset of `tags` whose values differ across same-named siblings of this title, ordered by display importance. Same object shape as `tags`. Omitted when the title has nothing to disambiguate. | ##### Browse pagination object | Key | Type | Required | Description | | :---------- | :----- | :------- | :------------------------------------------------------- | | hasNextPage | bool | Yes | Whether more results exist beyond the current page. | | pageSize | number | Yes | The requested page size. | | nextCursor | string | No | Opaque cursor for the next page. Absent on the last page. | #### System route example ##### Request ```json { "jsonrpc": "2.0", "id": 1, "method": "media.browse", "params": { "systems": ["SNES"] } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "path": "", "entries": [ { "name": "SNES", "path": "/roms/SNES", "type": "root", "fileCount": 150, "systemId": "SNES", "systemIds": ["SNES"] } ], "totalFiles": 0 } } ``` #### Browse path example ##### Request ```json { "jsonrpc": "2.0", "id": 1, "method": "media.browse", "params": { "path": "/roms/SNES", "maxResults": 3 } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "path": "/roms/SNES", "entries": [ { "name": "RPGs", "path": "/roms/SNES/RPGs", "type": "directory", "fileCount": 42 }, { "mediaId": 42, "name": "Super Mario World", "path": "/roms/SNES/Super Mario World.sfc", "type": "media", "systemId": "SNES", "zapScript": "@SNES/Super Mario World", "relativePath": "Super Mario World.sfc", "tags": [ {"tag": "1990", "type": "year"}, {"tag": "2", "type": "players"} ] }, { "mediaId": 43, "name": "The Legend of Zelda - A Link to the Past", "path": "/roms/SNES/The Legend of Zelda - A Link to the Past.sfc", "type": "media", "systemId": "SNES", "zapScript": "@SNES/The Legend of Zelda - A Link to the Past", "relativePath": "The Legend of Zelda - A Link to the Past.sfc", "tags": [ {"tag": "1991", "type": "year"}, {"tag": "1", "type": "players"} ] } ], "totalFiles": 150, "pagination": { "hasNextPage": true, "pageSize": 3, "nextCursor": "eyJzb3J0VmFsdWUiOiJUaGUgTGVnZW5kIG9mIFplbGRhIC0gQSBMaW5rIHRvIHRoZSBQYXN0IiwibGFzdElkIjo0Mn0=" } } } ``` ### media.browse.index Return the ordered first-character "jump to letter" buckets for a browse scope. Each bucket carries a count and a ready-to-use cursor that seeks `media.browse` to the start of that bucket, so a single round trip gives a client everything it needs to draw a section rail _and_ jump into the full ordered list. This avoids paging from the top to reach a distant section, which matters on constrained clients (e.g. MiSTer). The scope parameters mirror `media.browse` so the index describes the exact list `media.browse` would return for the same scope. The per-bucket `cursor` is an ordinary browse cursor: pass it to `media.browse` with the same `path`/`systems`/`sort` to get a normal page that begins at the bucket and continues into the next bucket as the user scrolls. #### Parameters All parameters are optional. | Key | Type | Required | Description | | :---------- | :------- | :------- | :------------------------------------------------------------------------------------------------ | | path | string | No | Directory or virtual scheme to index, same as `media.browse`. Omit or set empty for a root listing (no rail applies). | | systems | string[] | No | Case-sensitive system IDs to scope the index to, same as `media.browse`. | | fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in `systems`. | | sort | string | No | Sort order, must match the `media.browse` sort the rail is for. One of `name-asc` (default), `name-desc`, `filename-asc`, `filename-desc`. | #### Result | Key | Type | Required | Description | | :--------- | :-------------------------------------------- | :------- | :-------------------------------------------------------------------------- | | scheme | string | Yes | Collation used to derive the buckets. `latin` for first-character bucketing; `none` when no rail applies (a root listing, or a directory whose effective sort is not alphabetical, e.g. a ranked/date-prefixed collection folder), in which case `groups` is empty. | | totalFiles | number | Yes | Total media files in the scope. | | groups | [BrowseIndexGroup](#browse-index-group-object)[] | Yes | Only non-empty buckets, ordered to match `sort`. | ##### Browse index group object | Key | Type | Required | Description | | :----- | :----- | :------- | :------------------------------------------------------------------------------------------------ | | key | string | Yes | Stable bucket identifier (`A`–`Z`, `0-9`, `#`). Treat as opaque. | | label | string | Yes | Display text for the bucket. Equal to `key` for the `latin` scheme. | | count | number | Yes | Number of media files in the bucket. | | cursor | string | Yes | Opaque `media.browse` cursor positioned just before the bucket's first row. Empty string for the bucket that begins the list (call `media.browse` with no cursor for the first page). | | offset | number | Yes | 0-based position of the bucket's first item among the scope's media files, taken from its row number in the same ordered listing `media.browse` pages through (so it cannot drift from the browse order). Excludes any directory entries the listing shows before files; a client that jumps to a position in the full list adds its own leading-directory count. Use this to jump to the bucket's position rather than reloading from `cursor`. | Clients should render `groups` exactly as received, in order, without assuming a particular alphabet: `scheme` and `key` are opaque so a future locale-aware scheme (e.g. pinyin/kana/hangul buckets) requires no client change. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": 1, "method": "media.browse.index", "params": { "path": "/roms/SNES", "sort": "name-asc" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "scheme": "latin", "totalFiles": 150, "groups": [ { "key": "#", "label": "#", "count": 3, "cursor": "", "offset": 0 }, { "key": "0-9", "label": "0-9", "count": 7, "cursor": "eyJzb3J0VmFsdWUiOiIjV29sZiIsImxhc3RJZCI6MTAyfQ==", "offset": 3 }, { "key": "A", "label": "A", "count": 12, "cursor": "eyJzb3J0VmFsdWUiOiI5IExpdmVzIiwibGFzdElkIjoxMTV9", "offset": 10 } ] } } ``` To jump to "A", the client calls `media.browse` with that group's `cursor` and the same `path`/`sort`; the returned page begins at the first "A" title and continues into "B" as the user keeps scrolling. ### media.tags Query the media database and return available tags for filtering. This method returns all available tags (with their types) for the specified systems. Use this to build dynamic filter UIs showing available tag options. #### Parameters | Key | Type | Required | Description | | :------ | :------- | :------- | :-------------------------------------------------------------------------------------------------- | | systems | string[] | No | Case-sensitive list of system IDs to restrict tags to. A missing key or empty list will get all systems. | | fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in the `systems` array (e.g., `"snes"` matches `"SNES"`). | #### Result | Key | Type | Required | Description | | :--- | :----------------------- | :------- | :----------------------------- | | tags | [TagInfo](#taginfo-object)[] | Yes | Array of available tags. | **Tag Capping:** To prevent large responses, long-tail tag types are capped at 100 entries per type. Tags within each type are sorted by usage count (most popular first), then alphabetically. The following types are capped: `credit`, `developer`, `mameparent`, `publisher`, `search`. Taxonomy types (e.g., `region`, `year`, `lang`, `gamegenre`, `gamefamily`) have finite vocabularies per system and are always returned in full without truncation. ##### TagInfo object | Key | Type | Required | Description | | :--- | :----- | :------- | :------------------------------------ | | tag | string | Yes | The tag value. | | type | string | Yes | The tag type (e.g., "genre", "year"). | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607", "method": "media.tags", "params": { "systems": ["NES", "SNES"] } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607", "result": { "tags": [ { "type": "genre", "tag": "action" }, { "type": "genre", "tag": "platformer" }, { "type": "gamefamily", "tag": "Mario Bros" }, { "type": "gamefamily", "tag": "Super Mario" } ] } } ``` ### media.tags.update Add or remove user tags for an indexed media item. The initial mutable tag is `user:favorite`. It appears in normal media tag results and can be queried with `media.search` tag filters such as `user:favorite`, `-user:favorite`, and `~user:favorite`. #### Parameters | Key | Type | Required | Description | | :------ | :------- | :------- | :-------------------------------------------------------- | | mediaId | number | No | Media DBID to update. Cannot be mixed with system/path. | | system | string | No | System ID for path-based lookup. Required when using path. | | path | string | No | Media path for path-based lookup. Required with system. | | add | string[] | No | Tags to add. Currently only `user:favorite` is mutable. | | remove | string[] | No | Tags to remove. Currently only `user:favorite` is mutable. | Either `mediaId` or `system` plus `path` is required. At least one of `add` or `remove` is required. Search operators (`+`, `-`, `~`) are not valid in mutation requests. #### Result | Key | Type | Required | Description | | :--- | :--------------------------- | :------- | :----------------------------------- | | tags | [TagInfo](#taginfo-object)[] | Yes | Effective tags for the media item. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607", "method": "media.tags.update", "params": { "mediaId": 42, "add": ["user:favorite"] } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607", "result": { "tags": [ { "type": "user", "tag": "favorite" } ] } } ``` ### media.generate Create a new media database index. During indexing, the server will emit [media.indexing](./notifications.md) notifications showing progress of the index. #### Parameters Optionally, an object: | Key | Type | Required | Description | | :------ | :------- | :------- | :---------------------------------------------------------------------------------- | | systems | string[] | No | List of system IDs to restrict indexing to. Other system indexes will remain as is. | | fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in the `systems` array (e.g., `"snes"` matches `"SNES"`). | An omitted or `null` value parameters key is also valid and will index every system. **Selective Indexing Behavior:** - When `systems` is provided with specific system IDs, only those systems will be reindexed - The server will validate all provided system IDs and return an error if any are invalid - If all systems are specified (equivalent to no restriction), a full database rebuild will be performed for optimal performance - Selective indexing cannot be performed while database optimization is running - Resume functionality will validate that the system configuration hasn't changed between indexing sessions #### Result Returns `null` on success. Indexing runs in the background after the response is sent. Track progress using [media.indexing](./notifications.md) notifications. #### Examples ##### Full index request ```json { "jsonrpc": "2.0", "id": "6f20e07c-7a5e-11ef-84bb-020304050607", "method": "media.generate" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "6f20e07c-7a5e-11ef-84bb-020304050607", "result": null } ``` ##### Selective index request ```json { "jsonrpc": "2.0", "id": "7f30e17d-7a5e-11ef-85cc-020304050607", "method": "media.generate", "params": { "systems": ["NES", "SNES", "Genesis"] } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "7f30e17d-7a5e-11ef-85cc-020304050607", "result": null } ``` ### media.generate.cancel Cancel any currently running media database indexing operation. #### Parameters None. #### Result | Key | Type | Required | Description | | :------ | :----- | :------- | :------------------------------------ | | message | string | Yes | Status message about the cancellation. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "8f40e28e-7a5e-11ef-86dd-020304050607", "method": "media.generate.cancel" } ``` ##### Response (indexing was running) ```json { "jsonrpc": "2.0", "id": "8f40e28e-7a5e-11ef-86dd-020304050607", "result": { "message": "Media indexing cancelled successfully" } } ``` ##### Response (no indexing running) ```json { "jsonrpc": "2.0", "id": "8f40e28e-7a5e-11ef-86dd-020304050607", "result": { "message": "No media indexing operation is currently running" } } ``` ### media.active Returns the currently active media. #### Parameters | Key | Type | Required | Description | | :--- | :----- | :------- | :---------- | | slot | string | No | Media slot to query. Use `primary` or `background`. Defaults to `primary`. | #### Result Returns an [ActiveMedia](#active-media-object) object if media is currently active, or `null` if no media is active. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "method": "media.active" } ``` ##### Response (no active media) ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "result": null } ``` ##### Response (media active) ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "result": { "mediaId": 42, "started": "2024-09-24T17:49:42.938167429+08:00", "launcherId": "SNES", "systemId": "SNES", "systemName": "Super Nintendo Entertainment System", "mediaPath": "/roms/snes/Super Mario World (USA).sfc", "relativePath": "snes/Super Mario World (USA).sfc", "mediaName": "Super Mario World", "zapScript": "@SNES/Super Mario World", "launcherControls": ["load_state", "save_state", "toggle_menu"] } } ``` ### media.active.update Update the currently active media information. #### Parameters An object: | Key | Type | Required | Description | | :-------- | :----- | :------- | :-------------------------- | | systemId | string | Yes | ID of the system. | | mediaPath | string | Yes | Path to the media file. | | mediaName | string | Yes | Display name of the media. | #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "method": "media.active.update", "params": { "systemId": "SNES", "mediaPath": "/roms/snes/game.sfc", "mediaName": "Game" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "47f80537-7a5d-11ef-9c7b-020304050607", "result": null } ``` ### media.history.latest Return the most recent played media entry from the user database only. This is intended for startup paths that need the last played game as quickly as possible, without media database enrichment. This method does not return tags, metadata, media IDs, relative paths, pagination, end time, or play time. #### Parameters None. Empty params may be omitted or sent as `{}`. #### Result | Key | Type | Required | Description | | :---- | :----------------------------------------------------------- | :------- | :------------------------------------------------------------ | | entry | [MediaHistoryLatestEntry](#media-history-latest-entry-object) | Yes | Most recent media play history entry, or `null` when none exists. | ##### Media history latest entry object | Key | Type | Required | Description | | :--------- | :----- | :------- | :---------------------------------------------- | | systemId | string | Yes | ID of the system. | | systemName | string | Yes | Display name of the system from the history row. | | mediaName | string | Yes | Display name of the media from the history row. | | mediaPath | string | Yes | Path to the media file from the history row. | | launcherId | string | Yes | ID of the launcher used. | | startedAt | string | Yes | Timestamp when media started in RFC3339 format. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "9f2c6a52-7a5d-11ef-9c7b-020304050607", "method": "media.history.latest" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "9f2c6a52-7a5d-11ef-9c7b-020304050607", "result": { "entry": { "systemId": "SNES", "systemName": "Super Nintendo Entertainment System", "mediaName": "Super Mario World", "mediaPath": "/roms/snes/Super Mario World (USA).sfc", "launcherId": "SNES", "startedAt": "2025-01-22T14:30:00Z" } } } ``` ### media.history Return paginated media play history. #### Parameters Optionally, an object: | Key | Type | Required | Description | | :---------- | :------- | :------- | :---------------------------------------------------------------------------------------------- | | limit | number | No | Maximum number of entries to return. Default is 25, maximum is 100. | | cursor | string | No | Cursor for pagination. Omit for first page, use `nextCursor` from previous response for subsequent pages. | | systems | string[] | No | Filter to one or more system IDs (e.g., `["SNES", "NES"]`). | | fuzzySystem | boolean | No | Enable fuzzy matching for system IDs. | #### Result | Key | Type | Required | Description | | :--------- | :--------------------------------------------------- | :------- | :--------------------------------------- | | entries | [MediaHistoryEntry](#media-history-entry-object)[] | Yes | A list of media play history entries. | | pagination | [Pagination](#pagination-object) | No | Pagination information for cursor-based navigation. Only present when entries are returned. | ##### Media history entry object | Key | Type | Required | Description | | :--------- | :----- | :------- | :----------------------------------------------------- | | mediaId | number | No | Opaque media database row ID for efficient follow-up `media.meta` and `media.image` requests. Omitted when the history path cannot be resolved in the current media database. | | systemId | string | Yes | ID of the system. | | systemName | string | Yes | Display name of the system. | | mediaName | string | Yes | Display name of the media. | | mediaPath | string | Yes | Path to the media file. | | relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. | | launcherId | string | Yes | ID of the launcher used. | | startedAt | string | Yes | Timestamp when media started in RFC3339 format. | | endedAt | string | No | Timestamp when media stopped in RFC3339 format. Omitted if media is still active. | | playTime | number | Yes | Duration of the play session in seconds. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607", "method": "media.history", "params": { "limit": 10 } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607", "result": { "entries": [ { "mediaId": 42, "systemId": "SNES", "systemName": "Super Nintendo Entertainment System", "mediaName": "Super Mario World", "mediaPath": "/roms/snes/Super Mario World (USA).sfc", "relativePath": "snes/Super Mario World (USA).sfc", "launcherId": "SNES", "startedAt": "2025-01-22T14:30:00Z", "endedAt": "2025-01-22T15:15:30Z", "playTime": 2730 } ], "pagination": { "hasNextPage": false, "pageSize": 10 } } } ``` ### media.history.top Return aggregated media play history grouped by game, sorted by total play time descending. Useful for "most played" displays. #### Parameters Optionally, an object: | Key | Type | Required | Description | | :---------- | :------- | :------- | :---------------------------------------------------------------------------------------------- | | limit | number | No | Maximum number of entries to return. Default is 25, maximum is 100. | | systems | string[] | No | Filter to one or more system IDs (e.g., `["SNES", "NES"]`). | | fuzzySystem | boolean | No | Enable fuzzy matching for system IDs. | | since | string | No | Only count sessions starting after this RFC3339 timestamp. | #### Result | Key | Type | Required | Description | | :------ | :----------------------------------------------------------- | :------- | :--------------------------------------- | | entries | [MediaHistoryTopEntry](#media-history-top-entry-object)[] | Yes | A ranked list of games by total play time. | ##### Media history top entry object | Key | Type | Required | Description | | :------------ | :----- | :------- | :----------------------------------------------------- | | mediaId | number | No | Opaque media database row ID for efficient follow-up `media.meta` and `media.image` requests. Omitted when the history path cannot be resolved in the current media database. | | systemId | string | Yes | ID of the system. | | systemName | string | Yes | Display name of the system. | | mediaName | string | Yes | Display name of the media. | | mediaPath | string | Yes | Path to the media file (from most recent session). | | relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. | | totalPlayTime | number | Yes | Total play time across all sessions in seconds. | | sessionCount | number | Yes | Number of play sessions. | | lastPlayedAt | string | Yes | Timestamp of the most recent session in RFC3339 format. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-8b6e-12f0-ad8c-030405060708", "method": "media.history.top", "params": { "limit": 5, "systems": ["SNES"] } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-8b6e-12f0-ad8c-030405060708", "result": { "entries": [ { "mediaId": 42, "systemId": "SNES", "systemName": "Super Nintendo Entertainment System", "mediaName": "Super Mario World", "mediaPath": "/roms/snes/Super Mario World (USA).sfc", "relativePath": "snes/Super Mario World (USA).sfc", "totalPlayTime": 7200, "sessionCount": 12, "lastPlayedAt": "2026-02-14T20:30:00Z" } ] } } ``` ### media.lookup Resolve a game name and system to a media database match. Given a system ID and game name, searches the media database for the best matching title. Uses fuzzy matching to handle minor differences in naming. Returns `null` for the match when no title is found or confidence is too low. #### Parameters An object: | Key | Type | Required | Description | | :---------- | :------ | :------- | :------------------------------------------------------------------------------------- | | system | string | Yes | System ID to search within (e.g., `"SNES"`, `"Genesis"`). | | name | string | Yes | Game name to look up. | | fuzzySystem | boolean | No | Enable fuzzy matching for the system ID (e.g., `"snes"` matches `"SNES"`). | #### Result | Key | Type | Required | Description | | :---- | :------------------------------------------- | :------- | :----------------------------------------------------- | | match | [MediaLookupMatch](#media-lookup-match-object) | No | The best matching media entry, or `null` if no match found. | ##### Media lookup match object | Key | Type | Required | Description | | :--------- | :--------------------------- | :------- | :------------------------------------------------------------------------------------------ | | mediaId | number | No | Opaque media database row ID for efficient follow-up `media.meta` and `media.image` requests. | | system | [System](#system-object) | Yes | System the media was found in. | | name | string | Yes | Display name of the matched media. | | path | string | Yes | Path to the media file. | | relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. | | zapScript | string | Yes | ZapScript command to launch this media item. | | tags | [TagInfo](#taginfo-object)[] | Yes | Array of tags associated with this media item. | | confidence | number | Yes | Match confidence score from 0.0 to 1.0. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607", "method": "media.lookup", "params": { "system": "SNES", "name": "Super Mario World" } } ``` ##### Response (match found) ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607", "result": { "match": { "mediaId": 42, "system": { "id": "SNES", "name": "Super Nintendo Entertainment System", "category": "Console", "releaseDate": "1990-11-21", "manufacturer": "Nintendo" }, "name": "Super Mario World", "path": "/roms/snes/Super Mario World (USA).sfc", "relativePath": "SNES/Super Mario World (USA).sfc", "zapScript": "@SNES/Super Mario World", "tags": [ { "tag": "platformer", "type": "genre" }, { "tag": "1990", "type": "year" } ], "confidence": 0.95 } } } ``` ##### Response (no match) ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607", "result": { "match": null } } ``` ### media.meta Return the full metadata graph for one indexed media row, including its title, system, tags, and scraped properties. Use this when a client has a search, browse, or lookup result and needs all metadata attached to that row. Identify media by the result's `mediaId` when available, or by `system.id` and canonical `path`. Launcher-relative paths in the `system/path` shape are accepted as a compatibility fallback when they resolve to exactly one indexed media row. Properties are separated by scope: `media.properties` applies to the specific ROM/file row, and `media.title.properties` applies to the shared title. #### Parameters An object: | Key | Type | Required | Description | | :----- | :----- | :------- | :---------------------------------- | | mediaId | number | No | Opaque media database row ID from search, browse, or lookup. Cannot be mixed with `system`/`path`. | | system | string | No | System ID for the media row. Required when `mediaId` is omitted. | | path | string | No | Canonical indexed media path. Required when `mediaId` is omitted. | | items | object[] | No | Batch request items. Each item uses either `mediaId` or `system`/`path`. Maximum 100 items. Cannot be mixed with top-level media ref fields. | Single requests return the existing single `media` response shape. Batch requests return `{ "items": [...] }` in input order. Each batch item contains either `media` or `error`, so one missing media row does not fail the whole batch. #### Result | Key | Type | Required | Description | | :---- | :------------------------------------ | :------- | :--------------------------- | | media | [MediaMeta](#media-meta-object) | Yes | Metadata for the media row. | ##### Media meta object | Key | Type | Required | Description | | :--------- | :-------------------------------------- | :------- | :---------------------------------------------------- | | path | string | Yes | Media file path. | | parentDir | string | Yes | Parent directory stored for the media row. | | isMissing | boolean | Yes | Whether the indexed file is currently missing. | | tags | [TagInfo](#taginfo-object)[] | Yes | ROM-level tags for this media row. | | properties | object | Yes | ROM-level properties keyed by canonical type tag. | | launcherOverride | string | No | Launcher ID stored for this media row, mirrored from `property:launcher-override` in `properties`. When present, Core uses it for title, search, path, random, and history launches unless ZapScript includes an explicit `launcher` argument. | | title | [MediaMetaTitle](#media-meta-title-object) | Yes | Shared title metadata for this media row. | ##### Media meta title object | Key | Type | Required | Description | | :------------ | :--------------------------- | :------- | :----------------------------------------------- | | slug | string | Yes | Primary normalized title slug. | | secondarySlug | string | No | Secondary title slug, when available. | | name | string | Yes | Display title. | | slugLength | number | Yes | Character length of the primary slug. | | slugWordCount | number | Yes | Word count of the primary slug. | | system | object | Yes | Stored system object with `id` and `name`. | | tags | [TagInfo](#taginfo-object)[] | Yes | Title-level tags shared by matching media rows. | | properties | object | Yes | Title-level properties keyed by canonical type tag. | ##### Media meta property object | Key | Type | Required | Description | | :---------- | :----- | :------- | :--------------------------------------------------------------------- | | text | string | Yes | Text value or source path for the property. | | contentType | string | Yes | MIME type for binary-backed properties, empty for text-only values. | | extension | string | No | File extension without a dot, derived from MIME type or source path. | | blobSize | number | No | Size in bytes for binary-backed properties. | Binary property data is not returned by `media.meta`. Use `media.image` to fetch image bytes. Property keys are canonical type tags such as `property:description`, `property:image-image`, or `property:manual`. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "d4e5f6a7-7a5d-11ef-9c7b-020304050607", "method": "media.meta", "params": { "system": "SNES", "path": "/roms/snes/Super Mario World.sfc" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "d4e5f6a7-7a5d-11ef-9c7b-020304050607", "result": { "media": { "path": "/roms/snes/Super Mario World.sfc", "parentDir": "/roms/snes", "isMissing": false, "tags": [ {"type": "region", "tag": "usa"} ], "properties": { "property:launcher-override": { "text": "RetroArch", "contentType": "" } }, "launcherOverride": "RetroArch", "title": { "slug": "super mario world", "name": "Super Mario World", "slugLength": 17, "slugWordCount": 3, "system": { "id": "SNES", "name": "Super Nintendo Entertainment System" }, "tags": [ {"type": "developer", "tag": "Nintendo"}, {"type": "gamegenre", "tag": "platformer"} ], "properties": { "property:description": { "text": "Mario's dinosaur friend Yoshi makes his debut.", "contentType": "" } } } } } } ``` ##### Batch Request ```json { "jsonrpc": "2.0", "id": "d4e5f6a7-7a5d-11ef-9c7b-020304050608", "method": "media.meta", "params": { "items": [ {"mediaId": 42}, {"system": "SNES", "path": "/roms/snes/Super Metroid.sfc"} ] } } ``` ### media.meta.update Update writable metadata fields for one indexed media row, then return the same response shape as [`media.meta`](#mediameta). Use this to store a per-media launcher override. Core validates the launcher exists and supports the media row's system before saving it. Set `launcherOverride` to `null` to clear the override. Launcher selection order is: 1. Explicit `launcher` advanced argument in ZapScript. 2. Per-media `launcherOverride` stored with `media.meta.update`. 3. System default launcher from configuration. 4. Normal launcher matching. #### Parameters An object identifying the media row by `mediaId` or by `system` and canonical `path`. | Key | Type | Required | Description | | :------ | :----- | :------- | :---------- | | mediaId | number | No | Opaque media database row ID from search, browse, or lookup. Cannot be mixed with `system`/`path`. | | system | string | No | System ID for the media row. Required when `mediaId` is omitted. | | path | string | No | Canonical indexed media path. Required when `mediaId` is omitted. | | media | object | Yes | Patch object. Currently supports only `launcherOverride`. | ##### Media patch object | Key | Type | Required | Description | | :--------------- | :---------- | :------- | :---------- | | launcherOverride | string\|null | Yes | Launcher ID to use for this media row, matched case-insensitively and stored with canonical casing. Use `null` to clear it. Empty strings are rejected. | #### Result | Key | Type | Required | Description | | :---- | :------------------------------ | :------- | :-------------------------- | | media | [MediaMeta](#media-meta-object) | Yes | Updated metadata for row. | #### Example ##### Set override ```json { "jsonrpc": "2.0", "id": 1, "method": "media.meta.update", "params": { "mediaId": 42, "media": { "launcherOverride": "RetroArch" } } } ``` ##### Clear override ```json { "jsonrpc": "2.0", "id": 2, "method": "media.meta.update", "params": { "system": "SNES", "path": "/roms/snes/Super Mario World.sfc", "media": { "launcherOverride": null } } } ``` ### media.image Return the best matching image for one indexed media row as base64-encoded data. `media.image` checks the requested image types in order. For each type it tries media-level properties first, then title-level properties. If a stored file path no longer exists, the stale property is removed and lookup continues. #### Parameters An object identifying the media row by `mediaId` or `(system, path)`. Canonical indexed paths are preferred. Launcher-relative paths in the `system/path` shape are accepted as a compatibility fallback when they resolve to exactly one indexed media row. | Key | Type | Required | Description | | :--------- | :------- | :------- | :-------------------------------------------------------------------------- | | mediaId | number | No | Opaque media database row ID from search, browse, or lookup. Cannot be mixed with `system`/`path`. | | system | string | No | System ID. Required when `mediaId` is omitted. | | path | string | No | Canonical indexed media path. Required when `mediaId` is omitted. | | imageTypes | string[] | No | Image type preference order. Defaults to `image`, `boxart`, `screenshot`, `wheel`, `titleshot`, `map`, `marquee`, `fanart`. | Supported image type values are `image`, `boxart`, `screenshot`, `wheel`, `titleshot`, `map`, `marquee`, and `fanart`. They resolve to canonical property tags such as `property:image-image` and `property:image-boxart`. #### Result | Key | Type | Required | Description | | :---------- | :----- | :------- | :------------------------------------------- | | contentType | string | Yes | MIME type of the returned image data. | | extension | string | No | File extension without a dot, derived from MIME type or source path. | | data | string | Yes | Base64-encoded image bytes. | | typeTag | string | Yes | Canonical property tag that matched. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "e5f6a7b8-7a5d-11ef-9c7b-020304050607", "method": "media.image", "params": { "system": "SNES", "path": "/roms/snes/Super Mario World.sfc", "imageTypes": ["boxart", "image"] } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "e5f6a7b8-7a5d-11ef-9c7b-020304050607", "result": { "contentType": "image/png", "extension": "png", "data": "iVBORw0KGgoAAAANSUhEUgAA...", "typeTag": "property:image-boxart" } } ``` ### scrapers List all registered metadata scrapers. #### Parameters None. #### Result | Key | Type | Required | Description | | :------- | :----------------------------------- | :------- | :---------------------------------- | | scrapers | [ScraperInfo](#scraper-info-object)[] | Yes | Registered scraper implementations. | ##### Scraper info object | Key | Type | Required | Description | | :--------------- | :------- | :------- | :----------------------------------------------------------------------- | | id | string | Yes | Stable scraper ID used by `media.scrape`. | | name | string | Yes | Human-readable scraper name. | | supportedSystems | string[] | Yes | Supported system IDs. Empty means the scraper can run against all systems. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "f6a7b8c9-7a5d-11ef-9c7b-020304050607", "method": "scrapers" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "f6a7b8c9-7a5d-11ef-9c7b-020304050607", "result": { "scrapers": [ { "id": "gamelist.xml", "name": "gamelist.xml", "supportedSystems": [] } ] } } ``` ### media.scrape Start a metadata scraper run in the background. Scraping enriches existing MediaDB records only. It does not create media rows; run `media.generate` first so the filesystem scanner has indexed the library. Scraping and media indexing are mutually exclusive, and only one scraper can run at a time. Progress is reported with [media.scraping](./notifications.md#mediascraping) notifications and can be queried with `media.scrape.status`. Scraping pauses while media is running and resumes automatically when playback stops. #### Parameters An object: | Key | Type | Required | Description | | :-------- | :------- | :------- | :-------------------------------------------------------------------------- | | scraperId | string | Yes | Scraper ID from the `scrapers` method, for example `gamelist.xml`. | | systems | string[] | No | System IDs to scrape. Omit or pass an empty array to scrape all eligible systems. | | force | boolean | No | Re-scrape records that already have this scraper's sentinel tag. Default is false. | #### Result Returns `null` on success. The scraper continues after the response is sent. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a7b8c9d0-7a5d-11ef-9c7b-020304050607", "method": "media.scrape", "params": { "scraperId": "gamelist.xml", "systems": ["SNES", "NES"], "force": false } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a7b8c9d0-7a5d-11ef-9c7b-020304050607", "result": null } ``` ### media.scrape.status Return the latest known metadata scraper status. This method behaves like `media` does for indexing status: clients can query the current scrape snapshot after opening a UI, then continue listening for `media.scraping` notifications. If no scrape has run since startup, the result is idle with `scraping: false`, `done: false`, and `state: "idle"`. Existing flat counter fields remain for compatibility; new UIs should prefer `currentSystem` for per-system progress and `totalSteps`/`currentStep`/`currentStepDisplay` for whole-run progress. #### Parameters None. #### Result | Key | Type | Required | Description | | :-------- | :------ | :------- | :--------------------------------------------------------- | | scraperId | string | No | Scraper ID for the latest or active run. | | systemId | string | No | System currently being processed, when known. | | processed | integer | Yes | Number of records processed. | | total | integer | Yes | Total records expected for the current scrape, when known. | | matched | integer | Yes | Number of records matched and enriched. | | skipped | integer | Yes | Number of records skipped. | | totalScraped | integer | Yes | Number of media records already marked scraped. | | scraping | boolean | Yes | Whether a scrape is currently running. | | done | boolean | Yes | Whether the latest scrape reached a terminal state. | | paused | boolean | Yes | Whether the active scrape is paused because media is running or until resumed. | | state | string | No | Explicit lifecycle state: `idle`, `running`, `paused`, `completed`, `cancelled`, or `failed`. | | error | string | No | Fatal scrape error on failed terminal updates. | | totalSteps | integer | No | Total systems in the scrape run, when known. | | currentStep | integer | No | 1-based current system step, when known. | | currentStepDisplay | string | No | Display name for the current system step, falling back to system ID. | | currentSystem | object | No | Per-system progress object with `systemId`, `systemName`, `processed`, `total`, `matched`, and `skipped`. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "b8c9d0e1-7a5d-11ef-9c7b-020304050607", "method": "media.scrape.status" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "b8c9d0e1-7a5d-11ef-9c7b-020304050607", "result": { "scraperId": "gamelist.xml", "systemId": "snes", "processed": 42, "total": 100, "matched": 38, "skipped": 4, "totalScraped": 1200, "scraping": true, "done": false, "paused": false, "state": "running", "totalSteps": 2, "currentStep": 1, "currentStepDisplay": "Super Nintendo Entertainment System", "currentSystem": { "systemId": "snes", "systemName": "Super Nintendo Entertainment System", "processed": 42, "total": 100, "matched": 38, "skipped": 4 } } } ``` ### media.scrape.cancel Cancel the currently running metadata scraper operation. #### Parameters None. #### Result | Key | Type | Required | Description | | :------ | :----- | :------- | :------------------------------------ | | message | string | Yes | Status message about the cancellation. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "b8c9d0e1-7a5d-11ef-9c7b-020304050607", "method": "media.scrape.cancel" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "b8c9d0e1-7a5d-11ef-9c7b-020304050607", "result": { "message": "scraping cancelled" } } ``` ### media.scrape.resume Resume a paused metadata scraper operation. Scraping normally resumes automatically when playback stops. This method mirrors `media.generate.resume` and lets a local client force the active scrape to continue while the pauser is currently paused. #### Parameters None. #### Result | Key | Type | Required | Description | | :------ | :----- | :------- | :----------------------------- | | message | string | Yes | Status message about resuming. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "c9d0e1f2-7a5d-11ef-9c7b-020304050607", "method": "media.scrape.resume" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "c9d0e1f2-7a5d-11ef-9c7b-020304050607", "result": { "message": "Media scraping resumed" } } ``` ### media.clean.orphans Delete media rows marked missing and remove orphaned related data. This is intended for cleanup after files have been removed from disk and the media database has been refreshed. It removes missing `Media` rows, their tags and properties, and any titles that no longer have media rows. It does not run `VACUUM`; SQLite will reuse freed pages. #### Parameters None. #### Result | Key | Type | Required | Description | | :------ | :----- | :------- | :-------------------------------- | | deleted | number | Yes | Number of missing media rows removed. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "c9d0e1f2-7a5d-11ef-9c7b-020304050607", "method": "media.clean.orphans" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "c9d0e1f2-7a5d-11ef-9c7b-020304050607", "result": { "deleted": 12 } } ``` ### media.control Send a control action to the active media's launcher. Requires active media with a launcher that supports control capabilities. The available control actions depend on the launcher. Use the `launcherControls` field from `media.active` or `media` to discover supported actions. Control actions run in a restricted runtime that blocks media-launching and playlist commands. Utility commands like `input.keyboard`, `execute`, `delay` and `echo` are allowed. The `execute` command bypasses the `allow_execute` allowlist for control scripts defined in launcher configuration. #### Parameters An object: | Key | Type | Required | Description | | :----- | :----- | :------- | :---------------------------------------------------------------- | | action | string | Yes | The control action to execute (e.g., `"save_state"`, `"toggle_pause"`). | | slot | string | No | Target media slot. Omit for primary media; use `"background"` to control background audio. | | args | object | No | Optional key-value arguments for the control action. Values are strings. | #### Result Returns an empty object `{}` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "c3d4e5f6-7a5d-11ef-9c7b-020304050607", "method": "media.control", "params": { "action": "save_state" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "c3d4e5f6-7a5d-11ef-9c7b-020304050607", "result": {} } ``` ##### Background audio example Native audio supports `toggle_pause`, `pause`, `resume`, `stop`, `fast_forward`, and `rewind` controls on the `background` slot. `fast_forward` and `rewind` accept an optional `seconds` argument; default is 10 seconds. ```json { "jsonrpc": "2.0", "id": "d4e5f6a7-7a5d-11ef-9c7b-020304050607", "method": "media.control", "params": { "action": "fast_forward", "slot": "background", "args": { "seconds": "30" } } } ``` ### systems List all currently indexed systems. #### Parameters None. #### Result | Key | Type | Required | Description | | :------ | :------------------------- | :------- | :----------------------------- | | systems | [System](#system-object)[] | Yes | A list of all indexed systems. | See [System object](#system-object). #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "dbd312f3-7a5f-11ef-8f29-020304050607", "method": "systems" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "dbd312f3-7a5f-11ef-8f29-020304050607", "result": { "systems": [ { "id": "GameboyColor", "name": "Gameboy Color", "category": "Handheld", "releaseDate": "1998-10-21", "manufacturer": "Nintendo" }, { "id": "EDSAC", "name": "EDSAC", "category": "Computer", "releaseDate": "1949-05-06", "manufacturer": "University of Cambridge" } ] } } ``` ## Settings ### settings List currently set configuration settings. This method will list values set in the [Config File](../../core/config.md). Some config file options may be omitted which are not appropriate to be read or written remotely. #### Parameters None. #### Result | Key | Type | Required | Description | | :------------------------ | :---------------------------------------- | :------- | :-------------------------------------------------------------- | | runZapScript | boolean | Yes | Whether ZapScript execution is enabled. | | debugLogging | boolean | Yes | Whether debug logging is enabled. | | audioScanFeedback | boolean | Yes | Whether audio feedback on scan is enabled. | | readersAutoDetect | boolean | Yes | Whether automatic reader detection is enabled. | | readersScanMode | string | Yes | Current scan mode setting. | | readersScanExitDelay | number | Yes | Delay before exiting scan mode in seconds. | | readersScanIgnoreSystems | string[] | Yes | List of system IDs to ignore during scanning. | | errorReporting | boolean | Yes | Whether error reporting is enabled. | | readersConnect | [ReaderConnection](#reader-connection-object)[] | Yes | List of manually configured reader connections. | | systemDefaults | [SystemDefault](#system-default-object)[] | Yes | Per-system overrides for default launcher and exit ZapScript. | ##### Reader connection object | Key | Type | Required | Description | | :------- | :----- | :------- | :----------------------------------------------- | | driver | string | Yes | Reader driver type (e.g., "pn532uart", "acr122pcsc"). | | path | string | Yes | Path or address for the reader connection. | | idSource | string | No | Source for the reader ID. | | enabled | bool | No | Whether the connection is enabled. Defaults to true if omitted. | ##### System default object | Key | Type | Required | Description | | :--------- | :----- | :------- | :------------------------------------------------------------------------------------------------ | | system | string | Yes | System ID this default applies to. Accepts canonical IDs and aliases. | | launcher | string | No | Launcher ID or group name to use for this system. Empty means no override. | | beforeExit | string | No | ZapScript to run when a media instance for this system is exiting (before the new launch starts). | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "f208d996-7ae6-11ef-960e-020304050607", "method": "settings" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "f208d996-7ae6-11ef-960e-020304050607", "result": { "runZapScript": true, "debugLogging": false, "audioScanFeedback": true, "readersAutoDetect": true, "readersScanMode": "tap", "readersScanExitDelay": 0.0, "readersScanIgnoreSystems": ["DOS"], "errorReporting": true, "readersConnect": [], "systemDefaults": [ { "system": "Genesis", "launcher": "retroarch" } ] } } ``` ### settings.update Update one or more settings in-memory and save changes to disk. This method will only write values which are supplied. Existing values will not be modified. #### Parameters An object containing any of the following optional keys: | Key | Type | Required | Description | | :------------------------ | :---------------------------------------- | :------- | :-------------------------------------------------------------- | | runZapScript | boolean | No | Whether ZapScript execution is enabled. | | debugLogging | boolean | No | Whether debug logging is enabled. | | audioScanFeedback | boolean | No | Whether audio feedback on scan is enabled. | | readersAutoDetect | boolean | No | Whether automatic reader detection is enabled. | | readersScanMode | string | No | Current scan mode setting. | | readersScanExitDelay | number | No | Delay before exiting scan mode in seconds. | | readersScanIgnoreSystems | string[] | No | List of system IDs to ignore during scanning. | | errorReporting | boolean | No | Whether error reporting is enabled. | | readersConnect | [ReaderConnection](#reader-connection-object)[] | No | List of manually configured reader connections. | | systemDefaults | [SystemDefault](#system-default-object)[] | No | Replace the full list of per-system launcher/exit-script overrides. Each `launcher` value, if non-empty, must match a known launcher ID or group (case-insensitive). | #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "method": "settings.update", "params": { "debugLogging": false } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "result": null } ``` ### settings.reload Reload settings from the configuration file. #### Parameters None. #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "method": "settings.reload" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "result": null } ``` ### settings.auth.claim Redeem a claim token against a remote auth server and store the resulting credentials in `auth.toml`. This method performs trust discovery using the `.well-known/zaparoo` protocol. It first verifies that the claim URL's root domain supports auth (`auth: 1` in the well-known response), then redeems the claim token to obtain a bearer credential. If the root domain's well-known response includes a `trusted` list, each related domain is checked for bidirectional trust confirmation before extending the credential. #### Parameters An object: | Key | Type | Required | Description | | :------- | :----- | :------- | :------------------------------------------------------------- | | claimUrl | string | Yes | HTTPS URL of the claim endpoint to redeem the token against. | | token | string | Yes | The one-time claim token to redeem. | #### Result | Key | Type | Required | Description | | :------ | :------- | :------- | :----------------------------------------------------------------- | | domains | string[] | Yes | List of domains the credential was stored for (root + any trusted). | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-auth-claim-example", "method": "settings.auth.claim", "params": { "claimUrl": "https://api.example.com/auth/claim", "token": "claim-token-abc123" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-auth-claim-example", "result": { "domains": [ "https://api.example.com", "https://cdn.example.com" ] } } ``` ### settings.logs.download Download the current log file as base64-encoded content. #### Parameters None. #### Result | Key | Type | Required | Description | | :------- | :----- | :------- | :----------------------------------------------- | | filename | string | Yes | Name of the log file. | | size | number | Yes | Size of the log file in bytes. | | content | string | Yes | Base64-encoded content of the log file. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "9f50e39f-7a5e-11ef-87ee-020304050607", "method": "settings.logs.download" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "9f50e39f-7a5e-11ef-87ee-020304050607", "result": { "filename": "zaparoo.log", "size": 1024, "content": "MjAyNC0wOS0yNFQxNzowMDowMC4wMDBaIElORk8gU3RhcnRpbmcgWmFwYXJvby4uLg==" } } ``` ## Playtime ### playtime Query current playtime session status and usage statistics. This method returns comprehensive information about the current playtime session, including active game time, cumulative session time, cooldown state, daily usage, and remaining time before limits are reached. **Session States:** - `reset` - No active session, ready to start new session - `active` - Game currently running, time being tracked - `cooldown` - Game stopped but session persists (within session reset timeout) #### Parameters None. #### Result | Key | Type | Required | Description | | :-------------------- | :------ | :------- | :---------------------------------------------------------------------------------------------------- | | state | string | Yes | Current session state: `"reset"`, `"active"`, or `"cooldown"`. | | sessionActive | boolean | Yes | Whether a game is currently running. | | limitsEnabled | boolean | Yes | Whether playtime limits are currently enabled for enforcement. | | sessionStarted | string | No | ISO 8601 timestamp when current game started. Only present during `"active"` state. | | sessionDuration | string | No | Total time in current session (Go duration format). Present during `"active"` and `"cooldown"` states. | | sessionCumulativeTime | string | No | Cumulative time from previous games in session. Present during `"active"` and `"cooldown"` states. | | sessionRemaining | string | No | Time remaining before session limit reached. Only present if session limit is configured. | | cooldownRemaining | string | No | Time until session auto-resets. Only present during `"cooldown"` state. | | dailyUsageToday | string | No | Total playtime accumulated today. Available in all states when data is available. | | dailyRemaining | string | No | Time remaining before daily limit reached. Available in all states if daily limit is configured. | **Note:** All duration fields use Go's duration format (e.g., `"1h30m45s"`, `"45m"`, `"2h"`). #### Examples ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607", "method": "playtime" } ``` ##### Response (reset state) ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607", "result": { "state": "reset", "sessionActive": false, "limitsEnabled": true, "dailyUsageToday": "1h30m0s", "dailyRemaining": "2h30m0s" } } ``` ##### Response (active game with limits) ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607", "result": { "state": "active", "sessionActive": true, "limitsEnabled": true, "sessionStarted": "2025-01-22T14:30:00Z", "sessionDuration": "45m30s", "sessionCumulativeTime": "15m", "sessionRemaining": "14m30s", "dailyUsageToday": "2h15m30s", "dailyRemaining": "1h44m30s" } } ``` ##### Response (cooldown state) ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607", "result": { "state": "cooldown", "sessionActive": false, "limitsEnabled": true, "sessionDuration": "45m30s", "sessionCumulativeTime": "45m30s", "sessionRemaining": "14m30s", "cooldownRemaining": "12m30s", "dailyUsageToday": "2h15m30s", "dailyRemaining": "1h44m30s" } } ``` ### settings.playtime.limits Get current playtime limit configuration. Returns all configured playtime limits including daily limits, session limits, session reset timeout, warning intervals, and retention settings. #### Parameters None. #### Result | Key | Type | Required | Description | | :----------- | :------- | :------- | :--------------------------------------------------------------------------------------------------------------------- | | enabled | boolean | Yes | Whether playtime limits are enabled for enforcement. | | daily | string | No | Daily playtime limit in Go duration format (e.g., `"4h"`). Omitted if not configured. | | session | string | No | Per-session playtime limit in Go duration format (e.g., `"1h"`). Omitted if not configured. | | sessionReset | string | No | Idle timeout before session auto-resets in Go duration format (e.g., `"20m"`). `"0s"` means session never resets. | | warnings | string[] | Yes | List of time intervals when warnings are sent before limits reached (e.g., `["5m", "2m", "1m"]`). Empty array if none. | | retention | number | No | Number of days to retain playtime history. Omitted if not configured. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-7a5e-11ef-9c7b-020304050607", "method": "settings.playtime.limits" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-7a5e-11ef-9c7b-020304050607", "result": { "enabled": true, "daily": "4h", "session": "1h", "sessionReset": "20m", "warnings": ["5m", "2m", "1m"], "retention": 30 } } ``` ### settings.playtime.limits.update Update playtime limit settings. This method updates one or more playtime limit configuration values in-memory and saves changes to disk. Only provided fields will be updated; omitted fields remain unchanged. #### Parameters An object containing any of the following optional keys: | Key | Type | Required | Description | | :----------- | :------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------- | | enabled | boolean | No | Enable or disable playtime limit enforcement. | | daily | string | No | Daily playtime limit in Go duration format (e.g., `"4h"`, `"2h30m"`). Use `"0"` or `"0s"` to disable daily limit. | | session | string | No | Per-session playtime limit in Go duration format (e.g., `"1h"`, `"45m"`). Use `"0"` or `"0s"` to disable session limit. | | sessionReset | string | No | Idle timeout before session auto-resets in Go duration format (e.g., `"20m"`). Use `"0"` or `"0s"` for sessions that never reset. | | warnings | string[] | No | List of time intervals for warnings in Go duration format (e.g., `["10m", "5m", "1m"]`). Empty array disables warnings. | | retention | number | No | Number of days to retain playtime history. Use `0` for no retention limit. | **Important:** Duration strings must use Go duration format: combinations of hours (`h`), minutes (`m`), and seconds (`s`). Examples: `"1h"`, `"30m"`, `"1h30m"`, `"2h15m30s"`. #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "c3d4e5f6-7a5e-11ef-9c7b-020304050607", "method": "settings.playtime.limits.update", "params": { "enabled": true, "session": "1h", "warnings": ["10m", "5m", "2m"] } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "c3d4e5f6-7a5e-11ef-9c7b-020304050607", "result": null } ``` ## Mappings Mappings are used to modify the contents of tokens before they're launched, based on different types of matching parameters. Stored mappings are queried before every launch and applied to the token if there's a match. This allows, for example, adding ZapScript to a read-only NFC tag based on its UID. ### mappings List all mappings. Returns a list of all active and inactive mappings entries stored on server. #### Parameters None. #### Result | Key | Type | Required | Description | | :------- | :--------------------------- | :------- | :------------------------------------------------------------------ | | mappings | [Mapping](#mapping-object)[] | Yes | List of all stored mappings. See [mapping object](#mapping-object). | ##### Mapping object | Key | Type | Required | Description | | :------- | :------ | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id | string | Yes | Internal database ID of mapping entry. Used to reference mapping for updates and deletions. | | added | string | Yes | Timestamp of the time mapping was created in RFC3339 format. | | label | string | Yes | An optional display name shown to the user. | | enabled | boolean | Yes | True if the mapping will be used when looking up matching mappings. | | type | string | Yes | The field which will be matched against:_ `uid`: match on UID, if available. UIDs are normalized before matching to remove spaces, colons and convert to lowercase._ `text`: match on the stored text on token.\* `data`: match on the raw token data, if available. This is converted from bytes to a hexadecimal string and should be matched as this. | | match | string | Yes | The method used to match a mapping pattern:_ `exact`: match the entire string exactly to the field._ `partial`: match part of the string to the field.\* `regex`: use a regular expression to match the field. | | pattern | string | Yes | Pattern that will be matched against the token, using the above settings. | | override | string | Yes | Final text that will completely replace the existing token text if a match was successful. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "1a8bee28-7aef-11ef-8427-020304050607", "method": "mappings" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "1a8bee28-7aef-11ef-8427-020304050607", "result": { "mappings": [ { "id": "1", "added": "1970-01-21T06:08:18+08:00", "label": "barcode pokemon", "enabled": true, "type": "text", "match": "partial", "pattern": "9780307468031", "override": "**launch.search:gbc/*pokemon*gold*" } ] } } ``` ### mappings.new Create a new mapping. #### Parameters An object: | Key | Type | Required | Description | | :------- | :------ | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | label | string | Yes | An optional display name shown to the user. | | enabled | boolean | Yes | True if the mapping will be used when looking up matching mappings. | | type | string | Yes | The field which will be matched against:_ `uid`: match on UID, if available. UIDs are normalized before matching to remove spaces, colons and convert to lowercase._ `text`: match on the stored text on token.\* `data`: match on the raw token data, if available. This is converted from bytes to a hexadecimal string and should be matched as this. | | match | string | Yes | The method used to match a mapping pattern:_ `exact`: match the entire string exactly to the field._ `partial`: match part of the string to the field.\* `regex`: use a regular expression to match the field. | | pattern | string | Yes | Pattern that will be matched against the token, using the above settings. | | override | string | Yes | Final text that will completely replace the existing token text if a match was successful. | #### Result Returns an empty object `{}` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "method": "mappings.new", "params": { "label": "Test Mapping", "enabled": true, "type": "text", "match": "exact", "pattern": "test", "override": "**launch.system:snes" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "result": {} } ``` ### mappings.delete Delete an existing mapping. #### Parameters An object: | Key | Type | Required | Description | | :-- | :----- | :------- | :---------------------- | | id | number | Yes | Database ID of mapping. | #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "method": "mappings.delete", "params": { "id": 1 } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "result": null } ``` ### mappings.update Change an existing mapping. #### Parameters An object: | Key | Type | Required | Description | | :------- | :------ | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id | number | Yes | Internal database ID of mapping entry. | | label | string | No | An optional display name shown to the user. | | enabled | boolean | No | True if the mapping will be used when looking up matching mappings. | | type | string | No | The field which will be matched against:_ `uid`: match on UID, if available. UIDs are normalized before matching to remove spaces, colons and convert to lowercase._ `text`: match on the stored text on token.\* `data`: match on the raw token data, if available. This is converted from bytes to a hexadecimal string and should be matched as this. | | match | string | No | The method used to match a mapping pattern:_ `exact`: match the entire string exactly to the field._ `partial`: match part of the string to the field.\* `regex`: use a regular expression to match the field. | | pattern | string | No | Pattern that will be matched against the token, using the above settings. | | override | string | No | Final text that will completely replace the existing token text if a match was successful. | Only keys which are provided in the object will be updated in the database. #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "e98fd686-7e62-11ef-8f8c-020304050607", "method": "mappings.update", "params": { "id": 1, "enabled": false } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "e98fd686-7e62-11ef-8f8c-020304050607", "result": null } ``` ### mappings.reload Reload mappings from the configuration file. #### Parameters None. #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "method": "mappings.reload" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "result": null } ``` ## Readers ### readers List all currently connected readers and their capabilities. #### Parameters None. #### Result | Key | Type | Required | Description | | :------ | :------------------------- | :------- | :---------------------------------- | | readers | [ReaderInfo](#reader-info-object)[] | Yes | A list of all connected readers. | ##### Reader info object | Key | Type | Required | Description | | :----------- | :------- | :------- | :-------------------------------------------- | | id | string | Yes | Device path or system identifier of the reader. Legacy field, prefer `readerId` for stable identification. | | readerId | string | Yes | Stable reader ID, deterministic across restarts. Format: `{driver}-{hash}`. | | driver | string | Yes | Driver type for the reader (e.g., `"pn532"`, `"acr122pcsc"`, `"file"`). | | info | string | Yes | Human-readable information about the reader. | | connected | boolean | Yes | Whether the reader is currently connected. | | capabilities | string[] | Yes | List of capabilities supported by the reader. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "method": "readers" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "result": { "readers": [ { "id": "/dev/ttyUSB0", "readerId": "pn532-ujqixjv6", "driver": "pn532", "info": "PN532 (1-2.3.1)", "capabilities": ["read", "write"], "connected": true } ] } } ``` ### readers.write Attempt to write given text to the first available write-capable reader, if possible. #### Parameters An object: | Key | Type | Required | Description | | :------- | :----- | :------- | :--------------------------------------------------------------------------- | | text | string | Yes | ZapScript to be written to the token. | | readerId | string | No | ID of a specific reader to write to. If omitted, uses the first available write-capable reader. | #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "method": "readers.write", "params": { "text": "**launch.system:snes" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "result": null } ``` ### readers.write.cancel Cancel any ongoing write operation. #### Parameters Optionally, an object: | Key | Type | Required | Description | | :------- | :----- | :------- | :----------------------------------------------------------------------------- | | readerId | string | No | ID of a specific reader to cancel write on. If omitted, cancels on all readers. | #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "method": "readers.write.cancel" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "562c0b60-7ae8-11ef-87d7-020304050607", "result": null } ``` ## Launchers ### launchers List all launchers known to the running service. Suitable for populating a UI launcher picker (for example, when assigning a per-system default via [settings.update](#settingsupdate)). #### Parameters None. #### Result | Key | Type | Required | Description | | :-------- | :------------------------------------ | :------- | :--------------------------- | | launchers | [Launcher](#launcher-object)[] | Yes | All cached launchers, sorted by `systemId` then `id`. | ##### Launcher object | Key | Type | Required | Description | | :--------- | :------- | :------- | :----------------------------------------------------------------------------------------------------- | | id | string | Yes | Unique launcher identifier. | | systemId | string | No | The system this launcher targets. Omitted for generic launchers without a fixed system. | | systemName | string | No | Human-readable system name resolved from system metadata. Omitted when no metadata is available. | | groups | string[] | No | Group names this launcher belongs to. Group names are valid values for `systemDefaults.launcher`. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "5b8c3a40-7a5e-11ef-88ff-020304050607", "method": "launchers" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "5b8c3a40-7a5e-11ef-88ff-020304050607", "result": { "launchers": [ { "id": "retroarch", "systemId": "Genesis", "systemName": "Genesis", "groups": ["libretro"] }, { "id": "snes9x", "systemId": "SNES", "systemName": "Super Nintendo", "groups": ["libretro"] } ] } } ``` ### launchers.refresh Refresh the internal launcher cache, forcing a reload of launcher configurations. #### Parameters None. #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "af60e4a0-7a5e-11ef-88ff-020304050607", "method": "launchers.refresh" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "af60e4a0-7a5e-11ef-88ff-020304050607", "result": null } ``` ## Service ### version Return server's current version and platform. #### Parameters None. #### Result | Key | Type | Required | Description | | :------- | :----- | :------- | :-------------------------------------------------- | | platform | string | Yes | ID of platform the service is currently running on. | | version | string | Yes | Current version of the running Zaparoo service. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "ca47f646-7e47-11ef-971a-020304050607", "method": "version" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "ca47f646-7e47-11ef-971a-020304050607", "result": { "platform": "mister", "version": "2.0.0-dev" } } ``` ### health Simple health check to verify the server is running and responding. #### Parameters None. #### Result | Key | Type | Required | Description | | :----- | :----- | :------- | :----------------------------------------------- | | status | string | Yes | Health status. Returns `"ok"` when server is healthy. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "db58f757-7e47-11ef-982b-020304050607", "method": "health" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "db58f757-7e47-11ef-982b-020304050607", "result": { "status": "ok" } } ``` ## Inbox Inbox messages are system notifications stored on the server, typically used to inform the user of events like update availability, errors, or other important information. ### inbox List all inbox messages. #### Parameters None. #### Result | Key | Type | Required | Description | | :------- | :--------------------------------- | :------- | :------------------------ | | messages | [InboxMessage](#inbox-message-object)[] | Yes | List of inbox messages. | ##### Inbox message object | Key | Type | Required | Description | | :-------- | :----- | :------- | :----------------------------------------------- | | id | number | Yes | Unique identifier of the message. | | title | string | Yes | Title of the message. | | body | string | No | Body text of the message. | | severity | number | Yes | Severity level (0=info, 1=warning, 2=error). | | category | string | No | Category of the message. | | profileId | number | No | Associated profile ID, if applicable. | | createdAt | string | Yes | Timestamp when message was created in RFC3339 format. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "ec69f868-7e47-11ef-993c-020304050607", "method": "inbox" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "ec69f868-7e47-11ef-993c-020304050607", "result": { "messages": [ { "id": 1, "title": "Update Available", "body": "A new version of Zaparoo is available.", "severity": 0, "category": "update", "createdAt": "2024-09-24T17:49:42.938167429+08:00" } ] } } ``` ### inbox.delete Delete a specific inbox message by ID. #### Parameters An object: | Key | Type | Required | Description | | :-- | :----- | :------- | :---------------------------- | | id | number | Yes | ID of the message to delete. | #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "fd7a0979-7e47-11ef-9a4d-020304050607", "method": "inbox.delete", "params": { "id": 1 } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "fd7a0979-7e47-11ef-9a4d-020304050607", "result": null } ``` ### inbox.clear Delete all inbox messages. #### Parameters None. #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "0e8b1a8a-7e48-11ef-9b5e-020304050607", "method": "inbox.clear" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "0e8b1a8a-7e48-11ef-9b5e-020304050607", "result": null } ``` ## Input Direct platform input control for remote control use cases. These methods bypass the token pipeline entirely: no hooks, history, or sound effects are triggered. The input macro format is identical to what goes after the `:` in a ZapScript `input.keyboard` or `input.gamepad` command on a token. Each character is a separate keypress, `{...}` groups are special keys/combos, and `\` is the escape character. ### input.keyboard Press keyboard keys using the ZapScript input macro format. #### Parameters An object: | Key | Type | Required | Description | | :--- | :----- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | | keys | string | Yes | Input macro string. Each character is a keypress, `{...}` for special keys (e.g. `{enter}`, `{f9}`, `{ctrl+q}`). Same format as ZapScript on a token. | #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-1234-5678-9abc-def012345678", "method": "input.keyboard", "params": { "keys": "abc{enter}" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-1234-5678-9abc-def012345678", "result": null } ``` ### input.gamepad Press gamepad buttons using the ZapScript input macro format. #### Parameters An object: | Key | Type | Required | Description | | :------ | :----- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | buttons | string | Yes | Input macro string. Each character is a button press, `{...}` for named buttons (e.g. `{up}`, `{start}`, `{l1}`). Same format as ZapScript on a token. | #### Result Returns `null` on success. #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-2345-6789-abcd-ef0123456789", "method": "input.gamepad", "params": { "buttons": "^^vv<><>BA{start}" } } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "b2c3d4e5-2345-6789-abcd-ef0123456789", "result": null } ``` ## Screenshot ### screenshot Capture a screenshot of the current platform display. Returns the image as base64-encoded data and the path where it was saved on disk. Currently supported on MiSTer only. Other platforms will return an error. #### Parameters None. #### Result | Key | Type | Required | Description | | :--- | :----- | :------- | :------------------------------------------- | | path | string | Yes | Path where the screenshot was saved on disk. | | data | string | Yes | Base64-encoded image data. | | size | number | Yes | Size of the image data in bytes. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-1234-5678-9abc-def012345678", "method": "screenshot" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-1234-5678-9abc-def012345678", "result": { "path": "/media/fat/screenshots/MiSTer_20260329_181500.png", "data": "iVBORw0KGgo...", "size": 245760 } } ``` ## Updates ### update.check Check if a newer version of Zaparoo Core is available. Returns version information and release notes. On development builds, always returns `updateAvailable: false`. #### Parameters None. #### Result | Key | Type | Required | Description | | :-------------- | :------ | :------- | :------------------------------------------------- | | currentVersion | string | Yes | The currently running version. | | latestVersion | string | No | The latest available version (if check succeeded). | | updateAvailable | boolean | Yes | Whether a newer version is available. | | releaseNotes | string | No | Release notes for the latest version. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-1234-5678-9abc-def012345678", "method": "update.check" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-1234-5678-9abc-def012345678", "result": { "currentVersion": "2.9.1", "latestVersion": "2.10.0", "updateAvailable": true, "releaseNotes": "..." } } ``` ### update.apply Download and apply the latest available update, then gracefully restart the service. The response is sent to the client before the restart occurs. Returns an error if media indexing is in progress or if running a development build. #### Parameters None. #### Result | Key | Type | Required | Description | | :-------------- | :----- | :------- | :----------------------------- | | previousVersion | string | Yes | The version before the update. | | newVersion | string | Yes | The version after the update. | #### Example ##### Request ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-1234-5678-9abc-def012345678", "method": "update.apply" } ``` ##### Response ```json { "jsonrpc": "2.0", "id": "a1b2c3d4-1234-5678-9abc-def012345678", "result": { "previousVersion": "2.9.1", "newVersion": "2.10.0" } } ``` --- ## Notifications Notifications are sent from the server to connected clients to inform them of events. ## Readers ### readers.added A new reader was connected to the server. #### Response | Key | Type | Required | Description | | :-------- | :------ | :------- | :----------------------------------- | | connected | boolean | Yes | Whether the reader is connected. | | driver | string | Yes | Driver type for the reader. | | path | string | Yes | System path or identifier of reader. | ### readers.removed A connected reader was disconnected from the server. #### Response | Key | Type | Required | Description | | :-------- | :------ | :------- | :----------------------------------- | | connected | boolean | Yes | Whether the reader is connected. | | driver | string | Yes | Driver type for the reader. | | path | string | Yes | System path or identifier of reader. | ## Tokens ### tokens.added A token was detected by a connected reader. #### Response | Key | Type | Required | Description | | :------- | :----- | :------- | :--------------------------------------------- | | type | string | Yes | Type of token (e.g., "nfc", "barcode"). | | uid | string | Yes | Unique identifier of the token. | | text | string | No | Text data associated with the token. | | data | string | No | Raw binary data of the token (base64 encoded). | | scanTime | string | Yes | ISO 8601 timestamp when token was scanned. | | readerId | string | No | ID of the reader that scanned the token. | ### tokens.staged A token was staged by the launch guard and is waiting for confirmation. Sent when launch guard is enabled and media is currently playing. #### Response | Key | Type | Required | Description | | :------- | :----- | :------- | :--------------------------------------------- | | type | string | Yes | Type of token (e.g., "nfc", "barcode"). | | uid | string | Yes | Unique identifier of the token. | | text | string | No | Text data associated with the token. | | data | string | No | Raw binary data of the token (base64 encoded). | | scanTime | string | Yes | ISO 8601 timestamp when token was scanned. | ### tokens.staged.ready A staged token's delay period has expired and is now ready for confirmation. Sent when launch guard delay is configured and the mandatory waiting period completes. #### Response | Key | Type | Required | Description | | :------- | :----- | :------- | :--------------------------------------------- | | type | string | Yes | Type of token (e.g., "nfc", "barcode"). | | uid | string | Yes | Unique identifier of the token. | | text | string | No | Text data associated with the token. | | data | string | No | Raw binary data of the token (base64 encoded). | | scanTime | string | Yes | ISO 8601 timestamp when token was scanned. | ### tokens.removed A token was removed from a connected reader. #### Response Returns `null`. ## Media ### media.started New media was started on server. #### Response | Key | Type | Required | Description | |:-----------|:-------|:---------|:---------------------------------------------| | systemId | string | Yes | Internal ID of system associated with media. | | systemName | string | Yes | Display name of system. | | mediaPath | string | Yes | Path to media file on server. | | mediaName | string | Yes | Cleaned display title of media. | #### Example ```json { "systemId": "SNES", "systemName": "Super Nintendo Entertainment System", "mediaPath": "/roms/snes/Super_Mario_World_(USA)_[!].sfc", "mediaName": "Super Mario World" } ``` ### media.stopped Media has stopped on server. #### Response | Key | Type | Required | Description | | :--------- | :----- | :------- | :--------------------------------------------- | | systemId | string | Yes | ID of the system. | | systemName | string | Yes | Display name of the system. | | mediaName | string | Yes | Display name of the media. | | mediaPath | string | Yes | Path to media file on server. | | launcherId | string | Yes | ID of the launcher. | | elapsed | number | Yes | Duration of the media session in seconds. | #### Example ```json { "jsonrpc": "2.0", "method": "media.stopped", "params": { "systemId": "SNES", "systemName": "Super Nintendo Entertainment System", "mediaName": "Super Mario World", "mediaPath": "/roms/snes/Super Mario World (USA).sfc", "launcherId": "SNES", "elapsed": 2730 } } ``` ### media.indexing Sent during media database generation to indicate indexing progress and completion status, as well as database optimization progress. **Priority:** Indexing takes priority over optimization in notifications. If both are running, only indexing status will be shown. Optimization status is shown when no indexing is in progress. #### Parameters | Key | Type | Required | Description | | :----------------- | :------ | :------- | :----------------------------------------------------------------------------------------------- | | exists | boolean | Yes | True if media database exists. | | indexing | boolean | Yes | True if indexing is currently in progress. | | optimizing | boolean | Yes | True if database optimization is currently in progress. | | totalSteps | number | No | Total number of systems to process during indexing. | | currentStep | number | No | Current system being processed during indexing (1-based). | | currentStepDisplay | string | No | Display name of current system being indexed, or optimization step name (e.g., `"vacuum"`). | | totalFiles | number | No | Total number of media files discovered during indexing. | | totalMedia | number | No | Total number of media entries in the database. Only included when database exists and is ready. | **Indexing Progress:** Track using `currentStep` out of `totalSteps` systems processed. **Optimization Progress:** When `optimizing` is true and `indexing` is false, `currentStepDisplay` shows the optimization operation name (e.g., `"vacuum"`, `"analyze"`). #### Examples ##### Indexing in progress ```json { "jsonrpc": "2.0", "method": "media.indexing", "params": { "exists": true, "indexing": true, "optimizing": false, "totalSteps": 50, "currentStep": 25, "currentStepDisplay": "SNES", "totalFiles": 1523 } } ``` ##### Optimization in progress ```json { "jsonrpc": "2.0", "method": "media.indexing", "params": { "exists": true, "indexing": false, "optimizing": true, "currentStepDisplay": "vacuum", "totalMedia": 5432 } } ``` ##### Database ready ```json { "jsonrpc": "2.0", "method": "media.indexing", "params": { "exists": true, "indexing": false, "optimizing": false, "totalMedia": 5432 } } ``` ### media.scraping Sent while a metadata scraper run is active and when it completes. The first notification for a scraper run identifies the scraper and sets `scraping` to true. Progress notifications include the current system, per-system counters, whole-run system-step progress, pause state, and completion state. A final notification has `scraping` set to false and `done` set to true. Existing flat counter fields remain for compatibility; new UIs should prefer `currentSystem` for per-system progress and `totalSteps`/`currentStep`/`currentStepDisplay` for whole-run progress. #### Parameters | Key | Type | Required | Description | | :-------- | :------ | :------- | :---------------------------------------------------------------- | | scraperId | string | No | Scraper ID, for example `gamelist.xml`. | | systemId | string | No | System currently being scraped. | | processed | number | Yes | Number of source records processed so far. | | total | number | Yes | Total source records for the current system, or 0 before known. | | matched | number | Yes | Number of records matched to existing media rows. | | skipped | number | Yes | Number of records skipped because they were unmatched, already scraped, or failed per-record processing. | | totalScraped | number | Yes | Number of media records already marked scraped. | | scraping | boolean | Yes | True while scraping is active. | | done | boolean | Yes | True on the terminal update for the scraper run. | | paused | boolean | Yes | True when the active scrape is paused. | | state | string | No | Explicit lifecycle state: `idle`, `running`, `paused`, `completed`, `cancelled`, or `failed`. | | error | string | No | Fatal scrape error on failed terminal updates. | | totalSteps | number | No | Total systems in the scrape run, when known. | | currentStep | number | No | 1-based current system step, when known. | | currentStepDisplay | string | No | Display name for the current system step, falling back to system ID. | | currentSystem | object | No | Per-system progress object with `systemId`, `systemName`, `processed`, `total`, `matched`, and `skipped`. | #### Examples ##### Scraping in progress ```json { "jsonrpc": "2.0", "method": "media.scraping", "params": { "scraperId": "gamelist.xml", "systemId": "SNES", "processed": 42, "total": 100, "matched": 38, "skipped": 4, "totalScraped": 1200, "scraping": true, "done": false, "paused": false, "state": "running", "totalSteps": 12, "currentStep": 3, "currentStepDisplay": "Super Nintendo Entertainment System", "currentSystem": { "systemId": "SNES", "systemName": "Super Nintendo Entertainment System", "processed": 42, "total": 100, "matched": 38, "skipped": 4 } } } ``` ##### Scraping complete ```json { "jsonrpc": "2.0", "method": "media.scraping", "params": { "scraperId": "gamelist.xml", "systemId": "SNES", "processed": 100, "total": 100, "matched": 92, "skipped": 8, "totalScraped": 1250, "scraping": false, "done": true, "paused": false, "state": "completed", "totalSteps": 12, "currentStep": 12, "currentStepDisplay": "Super Nintendo Entertainment System", "currentSystem": { "systemId": "SNES", "systemName": "Super Nintendo Entertainment System", "processed": 100, "total": 100, "matched": 92, "skipped": 8 } } } ``` ## Playtime ### playtime.limit.reached Sent when a playtime limit (session or daily) has been reached and enforced by the system. The currently playing game will be stopped when this notification is sent. #### Parameters | Key | Type | Required | Description | | :----- | :----- | :------- | :---------------------------------------------------------------- | | reason | string | Yes | The type of limit that was reached: `"session"` or `"daily"`. | #### Examples ##### Session limit reached ```json { "jsonrpc": "2.0", "method": "playtime.limit.reached", "params": { "reason": "session" } } ``` ##### Daily limit reached ```json { "jsonrpc": "2.0", "method": "playtime.limit.reached", "params": { "reason": "daily" } } ``` ### playtime.limit.warning Sent at configured time intervals to warn that a playtime limit is approaching. These notifications are sent based on the `warnings` configuration (e.g., at 5 minutes, 2 minutes, and 1 minute before limit). The warning applies to whichever limit will be reached first (session or daily). #### Parameters | Key | Type | Required | Description | | :-------- | :----- | :------- | :---------------------------------------------------------------------------------------- | | interval | string | Yes | The configured warning interval that triggered this notification (Go duration format). | | remaining | string | Yes | The actual time remaining before the limit is reached (Go duration format). | **Note:** `remaining` may differ slightly from `interval` due to timing precision. #### Example ```json { "jsonrpc": "2.0", "method": "playtime.limit.warning", "params": { "interval": "5m", "remaining": "4m58s" } } ``` ## Inbox ### inbox.added Sent when a new inbox message is added to the server. #### Parameters | Key | Type | Required | Description | | :-------- | :----- | :------- | :----------------------------------------------- | | id | number | Yes | Unique identifier of the message. | | title | string | Yes | Title of the message. | | body | string | No | Body text of the message. | | severity | number | Yes | Severity level (0=info, 1=warning, 2=error). | | category | string | No | Category of the message. | | profileId | number | No | Associated profile ID, if applicable. | | createdAt | string | Yes | Timestamp when message was created in RFC3339 format. | #### Example ```json { "jsonrpc": "2.0", "method": "inbox.added", "params": { "id": 1, "title": "Update Available", "body": "A new version of Zaparoo is available.", "severity": 0, "category": "update", "createdAt": "2024-09-24T17:49:42.938167429+08:00" } } ``` --- ## Command Line Zaparoo Core includes a command line interface for local scripting, quick tests, and platform service management. Most action flags call the local [Core API](./api/index.md), so Core needs to be running and reachable on the same device unless the flag starts or manages the service itself. The executable name depends on the platform. [MiSTer FPGA](../platforms/mister/index.md) uses `zaparoo.sh`, while [Linux](../platforms/linux/index.md) usually uses `zaparoo`. Replace the executable name in the examples with the one from your platform. ```bash ./zaparoo -version ./zaparoo -run "**launch.system:menu" ``` Run `-help` on your installed binary for the exact flags supported by that build. ## Common flags These flags are defined by the shared Core CLI and are available in current command-line builds. | Flag | Argument | Description | | ---- | -------- | ----------- | | `-help` | None | Prints the available flags for the current binary. | | `-version` | None | Prints the binary version and platform ID, then exits. | | `-run` | ZapScript string | Runs text as [ZapScript](../zapscript/index.md), like a scanned token. | | `-launch` | ZapScript string | Deprecated alias of `-run`. | | `-api` | `method:params` | Calls one [Core API method](./api/index.md) and prints the response. | | `-read` | None | Prints the next scanned token without running its actions. | | `-write` | Text string | Writes text to the next token found by a write-capable reader. | | `-reload` | None | Reloads `config.toml` and mapping files from disk. | | `-pair` | None | Starts app/client pairing and prints the pairing result when complete. | | `-backup` | None | Creates a manual backup of the user database. | | `-backups` | None | Lists available user database backups. | | `-restore` | Backup name | Restores the user database from a named backup. | The `-config` flag may appear in `-help` output because it is still defined by the shared parser. In current Core source it does not run a separate action; start the [TUI](tui.md) or use the [Web UI](../app/web.md) for configuration instead. ## Run ZapScript Use `-run` when you want the CLI to behave like a scanned token. The argument is ZapScript, so it can be a media path, launcher command, input command, or several commands joined with `||`. ```bash ./zaparoo -run "**launch.system:menu" ./zaparoo -run "SNES/Super Metroid.sfc" ``` `-launch` still works as an alias for older scripts, but new scripts should use `-run`. ## Call the API Use `-api` to send one raw API call to the local Core service. The format is the method name, a colon, then the JSON parameters for that method. ```bash ./zaparoo -api 'run:{"text":"**launch.system:menu"}' ./zaparoo -api 'settings.reload' ``` If the method does not need parameters, omit the colon and JSON body. The CLI prints the response body returned by Core. For method names and parameter shapes, see the [API methods reference](./api/methods.md). ## Read or write tokens Use `-read` to wait for the next token scan and print its stored text without running the token. ```bash ./zaparoo -read ``` Use `-write` to write text to the next token detected by a write-capable reader. ```bash ./zaparoo -write "SNES/Super Metroid.sfc" ./zaparoo -write "**launch.system:menu" ``` While reading or writing, Core temporarily disables normal ZapScript execution so the scanned token is handled by the CLI action instead of launching media. ## Reload settings Use `-reload` after editing the [config file](config.md) or [mapping files](../features/mappings.md) while Core is running. ```bash ./zaparoo -reload ``` This calls the `settings.reload` API method. Settings that require a full service restart still need the service to be restarted. ## Pair a client Use `-pair` to start the same pairing flow used by clients such as the [Zaparoo App](../app/index.md). ```bash ./zaparoo -pair ``` Core prints a PIN to the terminal. Enter that PIN in the client app. When pairing succeeds, the CLI prints the pairing response as a single line. ## Back up and restore user data Core keeps your favorites, per-media [launcher overrides](../features/launchers.md#default-launchers), history, and token mappings in a user database that is separate from the rebuildable media database. Favorites and overrides are not lost if Core has to rebuild the media database after corruption. Core takes automatic backups of the whole user database and prunes old ones, keeping the most recent few. It can also recover automatically if the database is found to be corrupt on startup. Use `-backup` to create a manual backup. Manual backups are kept until you remove them yourself. ```bash ./zaparoo -backup ``` Use `-backups` to list the backups Core currently has, including their names. ```bash ./zaparoo -backups ``` Use `-restore` with a backup name to restore the user database from that backup. Core takes a safety backup of the current database before restoring. ```bash ./zaparoo -restore backup-20260624-130454-000000000-manual.db ``` These flags call the `settings.backup`, `settings.backup.list`, and `settings.backup.restore` [API methods](./api/methods.md). ## Platform flags Platform builds add their own service, UI, and installer flags. These are the user-facing flags in current Core source. ### Linux, SteamOS, Bazzite, and ChimeraOS These Linux desktop-style builds share the same extra flags. | Flag | Argument | Description | | ---- | -------- | ----------- | | `-install` | `application`, `desktop`, `service`, or `hardware` | Installs one component. | | `-uninstall` | `application`, `desktop`, `service`, or `hardware` | Uninstalls one component. | | `-daemon` | None | Runs the service in the foreground with no TUI. | | `-start` | None | Starts the user service if needed and opens the Web UI in the browser. | Examples: ```bash ./zaparoo -install service ./zaparoo -install hardware ./zaparoo -start ``` Outside install and uninstall commands, do not run these builds as root. The Linux service is a user service, and Core exits if normal service or UI mode starts with root privileges. ### MiSTer FPGA and MiSTeX | Flag | Argument | Description | | ---- | -------- | ----------- | | `-service` | `start`, `stop`, `restart`, or `status` | Manages the Zaparoo service. | | `-add-startup` | None | Adds Zaparoo to MiSTer startup if it is not already there. | | `-show-loader` | JSON file path | Shows a MiSTer loading widget. | | `-show-notice` | JSON file path | Shows a MiSTer notice widget. | | `-show-picker` | JSON file path | Shows a MiSTer picker widget. | Examples: ```bash /media/fat/Scripts/zaparoo.sh -service restart /media/fat/Scripts/zaparoo.sh -add-startup ``` The widget flags are mainly used by Core and MiSTer integrations. They expect a JSON configuration file path. ### Batocera | Flag | Argument | Description | | ---- | -------- | ----------- | | `-service` | `start`, `stop`, `restart`, or `status` | Manages the Zaparoo service. | | `-install` | None | Installs the Batocera service file. | | `-uninstall` | None | Removes the Batocera service file. | Examples: ```bash /userdata/system/zaparoo -service restart /userdata/system/zaparoo -install ``` ### LibreELEC | Flag | Argument | Description | | ---- | -------- | ----------- | | `-service` | `start`, `stop`, `restart`, or `status` | Manages the Zaparoo service. | Example: ```bash /storage/zaparoo -service restart ``` ### macOS | Flag | Argument | Description | | ---- | -------- | ----------- | | `-daemon` | None | Runs the service without the TUI or system tray UI. | | `-gui` | None | Runs the service with the system tray UI. | Without either flag, the macOS build starts the local [TUI](tui.md) after starting or connecting to the Core service. ### Windows Windows does not support CLI arguments. The Windows build is compiled as a GUI application and starts Core in the system tray. Windows also refuses to run with elevated administrator rights. Start it as your normal user. --- ## Config File The config file is the main configuration file of the [Zaparoo Core](./index.md) software service. Its location depends on the [platform](../platforms/index.mdx) where the service is running. On [MiSTer](../platforms/mister/index.md), it's located in the `/media/fat/zaparoo` folder (i.e. `zaparoo` folder in the root of the SD card). The file is always called `config.toml` on every platform. The config file is written in [TOML](https://toml.io/en/). :::warning Although comments are supported in TOML, _they will be lost if Core updates this file_ (e.g. when adjusting settings via the Zaparoo App). Manual edits to values are preserved on save. ::: Any changes made to the config file while the Core service is running require the service to be restarted before changes will take effect, or the `-reload` [CLI command](./cli.md) to be run. :::tip Portable Mode Optionally, Zaparoo Core can run in portable mode, where the config and all other data are stored in a single folder alongside the executable. To enable this, create an empty folder called `user` in the same folder as the Core executable, then start Core normally. ::: ## Options Options in the config file are grouped by sections which start with a header. For example, the [audio](#audio) section begins with `[audio]` and continues until the next header is encountered. ### Global Settings The global settings section does not start with a section header and is the only section that behaves this way. It's reserved for certain options that affect all parts of Core or the config file itself. ```toml config_schema = 1 debug_logging = true auto_update = false update_channel = "stable" ``` #### config_schema | Key | Type | Default | | ------------- | ------- | ------- | | config_schema | integer | 1 | **This option should not be changed or removed.** `config_schema` is used internally by Core to track what version of itself last wrote to the file. This makes it possible to perform migrations between versions if the layout of the config file must be changed. #### debug_logging | Key | Type | Default | | ------------- | ------- | ------- | | debug_logging | boolean | false | `debug_logging` enables or disables logging debug messages to Core log files. It's useful for troubleshooting issues but can make log files noisy. This option should be enabled when attempting to reproduce issues for reporting. #### error_reporting | Key | Type | Default | | --------------- | ------- | ------- | | error_reporting | boolean | false | `error_reporting` enables or disables opt-in error reporting. When enabled, anonymous error reports are sent to help improve Zaparoo. See the [Privacy Policy](/privacy) for details on what data is collected. #### auto_update | Key | Type | Default | | ----------- | ------- | -------------------- | | auto_update | boolean | _varies by platform_ | `auto_update` controls whether Zaparoo checks for and notifies about available updates. ```toml auto_update = false ``` Platform defaults: - **Most platforms**: Enabled by default - **MiSTer (Downloader)**: Disabled by default (updates managed by MiSTer Downloader) - **Batocera (pacman)**: Disabled by default (updates managed by Batocera's package manager) When disabled, Zaparoo will not check for new versions or display update notifications. #### update_channel | Key | Type | Default | | -------------- | ---------------------------- | ---------- | | update_channel | string (`"stable"`, `"beta"`) | `"stable"` | `update_channel` controls which release channel Core uses when checking for updates. ```toml update_channel = "stable" ``` Use `"stable"` for normal releases. Use `"beta"` only if you want Core to check for beta releases. ### Audio ```toml [audio] scan_feedback = true volume = 100 success_sound = "custom_success.wav" fail_sound = "custom_fail.wav" limit_sound = "custom_limit.wav" pending_sound = "custom_pending.ogg" ready_sound = "custom_ready.ogg" ``` #### scan_feedback | Key | Type | Default | | ------------- | ------- | ------- | | scan_feedback | boolean | true | `scan_feedback` enables or disables playing a sound from the host device when a scan is successful or results in an error. #### volume | Key | Type | Default | | ------ | ----------------- | ------- | | volume | integer (0–200) | 100 | `volume` sets the playback volume for audio feedback sounds and [native audio playback](../features/audio.md). At the default `100`, sounds play at their original recorded level. Lower values quiet them down; higher values amplify, up to double at `200`. ```toml [audio] volume = 150 # 50% louder than the original sound ``` #### success_sound | Key | Type | Default | | ------------- | ------ | ------------------ | | success_sound | string | (embedded default) | `success_sound` specifies a custom audio file to play when a token scan is successful. Supports WAV, MP3, OGG, and FLAC formats. ```toml [audio] success_sound = "custom_success.wav" ``` - **Omit or comment out**: Use embedded default success sound - **Empty string `""`**: Disable success sound completely - **Relative path**: Resolved to `/assets/` (e.g., `"success.mp3"` → `/media/fat/zaparoo/assets/success.mp3`) - **Absolute path**: Used as-is (e.g., `"/path/to/sound.wav"`) #### fail_sound | Key | Type | Default | | ---------- | ------ | ------------------ | | fail_sound | string | (embedded default) | `fail_sound` specifies a custom audio file to play when a token scan fails or ZapScript execution errors occur. Supports WAV, MP3, OGG, and FLAC formats. ```toml [audio] fail_sound = "custom_fail.wav" ``` Configuration works the same as [`success_sound`](#success_sound): - **Omit or comment out**: Use embedded default fail sound - **Empty string `""`**: Disable fail sound completely - **Relative path**: Resolved to `/assets/` - **Absolute path**: Used as-is #### limit_sound | Key | Type | Default | | ----------- | ------ | ------------------ | | limit_sound | string | (embedded default) | `limit_sound` specifies a custom audio file to play when playtime limit warnings occur. Supports WAV, MP3, OGG, and FLAC formats. ```toml [audio] limit_sound = "custom_limit.wav" ``` Configuration works the same as [`success_sound`](#success_sound): - **Omit or comment out**: Use embedded default limit sound - **Empty string `""`**: Disable limit sound completely - **Relative path**: Resolved to `/assets/` - **Absolute path**: Used as-is #### pending_sound | Key | Type | Default | | ------------- | ------ | ------------------ | | pending_sound | string | (embedded default) | `pending_sound` specifies a custom audio file to play when a token is staged by [launch guard](../features/play-controls.md#launch-guard). Supports WAV, MP3, OGG, and FLAC formats. ```toml [audio] pending_sound = "custom_pending.ogg" ``` Configuration works the same as [`success_sound`](#success_sound): - **Omit or comment out**: Use embedded default pending sound - **Empty string `""`**: Disable pending sound completely - **Relative path**: Resolved to `/assets/` - **Absolute path**: Used as-is #### ready_sound | Key | Type | Default | | ----------- | ------ | ------------------ | | ready_sound | string | (embedded default) | `ready_sound` specifies a custom audio file to play when the launch guard delay period expires and a staged token is ready for confirmation. Supports WAV, MP3, OGG, and FLAC formats. ```toml [audio] ready_sound = "custom_ready.ogg" ``` Configuration works the same as [`success_sound`](#success_sound): - **Omit or comment out**: Use embedded default ready sound - **Empty string `""`**: Disable ready sound completely - **Relative path**: Resolved to `/assets/` - **Absolute path**: Used as-is ### Input ```toml [input] gamepad_enabled = true ``` The `input` section configures input device emulation features. #### gamepad_enabled | Key | Type | Default | | --------------- | ------- | -------------------- | | gamepad_enabled | boolean | _varies by platform_ | `gamepad_enabled` enables or disables the virtual gamepad device used by the [`**input.gamepad`](../zapscript/input.md#inputgamepad) ZapScript command. ```toml [input] gamepad_enabled = true ``` Platform defaults: - **MiSTer/MiSTeX**: Enabled by default - **Batocera**: Disabled by default (may conflict with some emulators) - **Other platforms**: Enabled by default When disabled, the `**input.gamepad` command will return an error. ### Media ```toml [media] filename_tags = true default_regions = ["us", "world"] default_langs = ["en"] ``` The `media` section configures how Core processes and matches media files, including tag parsing and region/language preferences. #### filename_tags | Key | Type | Default | | ------------- | ------- | ------- | | filename_tags | boolean | true | `filename_tags` enables or disables automatic parsing of tags from media filenames (e.g., region codes, languages, revision numbers). ```toml [media] filename_tags = true ``` When enabled, Core extracts metadata tags from filenames like `(USA)`, `(En)`, `(Rev 1)` to help with conflict resolution and media matching. See the [Tags documentation](../features/tags.md) for detailed information about tag parsing and usage. #### default_regions | Key | Type | Default | | --------------- | -------- | ------------------ | | default_regions | string[] | `["us", "world"]` | `default_regions` specifies which region tags to prefer when multiple versions of the same game exist. ```toml [media] default_regions = ["us", "eu", "world"] ``` Regions are checked in order. If a game has both `(USA)` and `(Europe)` versions, the USA version will be preferred with the default settings. Common region codes: `us`, `eu`, `jp`, `world`, `uk`, `de`, `fr`, `es`, `it` #### default_langs | Key | Type | Default | | ------------- | -------- | -------- | | default_langs | string[] | `["en"]` | `default_langs` specifies which language tags to prefer when multiple language versions exist. ```toml [media] default_langs = ["en", "es"] ``` Languages are checked in order. Common language codes: `en`, `es`, `fr`, `de`, `it`, `ja`, `pt` ### Readers ```toml [readers] auto_detect = true scan_history = 30 ``` #### auto_detect {#readers-auto-detect} | Key | Type | Default | | ----------- | ------- | ------- | | auto_detect | boolean | true | `auto_detect` enables or disables automatically searching for and probing possible connected readers on the host device. It may be required to disable this option if auto-detection is causing problems with unrelated connected devices. #### scan_history | Key | Type | Default | | ------------ | ------- | ------- | | scan_history | integer | 30 | `scan_history` specifies how many days of scan history to keep for the recent scans list. Old scan records are automatically cleaned up. ```toml [readers] scan_history = 30 ``` Set to `0` to keep all scan history forever (disables cleanup). #### readers.scan `readers.scan` is a sub-section of `readers` and must be defined with the header: `[readers.scan]` ```toml [readers.scan] mode = 'hold' exit_delay = 3.0 ignore_system = [ 'PC', 'MSX' ] on_scan = '**echo:card was scanned' on_remove = '**echo:card was removed' ignore_on_connect = true ``` ##### mode {#scan-mode} | Key | Type | Default | | ---- | ------------------------ | ------- | | mode | string ("tap" \| "hold") | tap | `mode` defines the behavior of scans. It has two options: - `tap` is the default mode and means when a token is used with a reader it can be removed again without affecting the playing media. If a token is tapped, removed and then tapped again it will relaunch the already playing media. - `hold` mode makes it so a token must be held to the reader for as long as any launched media will play. That is, after a token is removed from the reader, it will exit the media. This makes a token act more like real physical media. **Core does not currently make any attempt to save before exiting media.** See [`exit_delay`](#exit_delay), [`ignore_system`](#ignore_system), and [`on_remove`](#on_remove) for related options. ##### exit_delay | Key | Type | Default | | ---------- | ------------ | ------- | | exit_delay | float (≥0.0) | 0.0 | `exit_delay` adds a delay, in seconds, before media is exited after a token is removed from a reader. It's only active if `hold` [mode](#scan-mode) is also active. For example, if `exit_delay` was set to `2.3`, it would mean when a token is removed from a reader, instead of immediately exiting the media, a timer is started for 2.3 seconds first. If the same token is placed back on the reader before the timer is complete, the timer will be cleared and the media won't exit. This feature can be useful if you want to, using a single reader, scan other tokens such as adding credit without exiting the current game. ##### ignore_system | Key | Type | Default | | ------------- | -------- | ------- | | ignore_system | string[] | [] | `ignore_system` is a list of systems which will not exit playing media on token removal. It's only active in `hold` [`mode`](#scan-mode). ##### on_scan | Key | Type | Default | | ------- | ------ | ------- | | on_scan | string | | `on_scan` is a [hook](../features/hooks.md) containing a snippet of [ZapScript](../zapscript/index.md). It runs immediately after a token is scanned but before ZapScript on the token itself (or a mapping) is run. It is always active if enabled. This hook can block the scan by returning an error. If the ZapScript command fails or a script executed via `**execute:` returns a non-zero exit code, the token processing is blocked. Scripts executed via `**execute:` receive a `ZAPAROO_ENVIRONMENT` environment variable containing the [expression environment](../zapscript/syntax.md#expression-environment) as JSON. ##### on_remove | Key | Type | Default | | --------- | ------ | ------- | | on_remove | string | | `on_remove` is a [hook](../features/hooks.md) containing a snippet of [ZapScript](../zapscript/index.md). It runs immediately after a token is removed from the reader. It's only active in `hold` [`mode`](#scan-mode). Note that this will _always_ run in `hold` mode when a token is removed from the reader, no matter if any media was launched or is active. It also does not respect the [`exit_delay`](#exit_delay) setting and runs before any media exit logic happens. This hook can block the remove action by returning an error. If the ZapScript command fails or a script executed via `**execute:` returns a non-zero exit code, the remove processing is blocked. Scripts executed via `**execute:` receive a `ZAPAROO_ENVIRONMENT` environment variable containing the [expression environment](../zapscript/syntax.md#expression-environment) as JSON. ##### ignore_on_connect | Key | Type | Default | | ------------------ | ------- | ------- | | ignore_on_connect | boolean | false | `ignore_on_connect` suppresses the first token scan from each newly-connected reader, preventing accidental launches from cards left on readers at startup. ```toml [readers.scan] ignore_on_connect = true ``` When enabled, if a token is already present on a reader when it connects (e.g., a card left on the reader when Zaparoo starts), that initial scan will be silently ignored. Subsequent scans from the same reader will work normally. #### readers.scan.launch_guard {#launch-guard-config} `readers.scan.launch_guard` is a sub-section of `readers.scan` and must be defined with the header: `[readers.scan.launch_guard]` ```toml [readers.scan.launch_guard] enabled = true timeout = 15 delay = 0 require_confirm = false ``` ##### enabled {#launch-guard-enabled} | Key | Type | Default | | ------- | ------- | ------- | | enabled | boolean | false | `enabled` turns launch guard on or off. When enabled, tokens scanned while media is playing are staged rather than launched immediately. All scans are unaffected when nothing is playing. ```toml [readers.scan.launch_guard] enabled = true ``` ##### timeout {#launch-guard-timeout} | Key | Type | Default | | ------- | ------------- | ------- | | timeout | float (≥-1.0) | 15.0 | `timeout` sets how long, in seconds, a staged token waits for confirmation before being silently dropped. Setting `timeout` to `0` uses the default of 15 seconds. Setting it to a negative value (e.g., `-1`) disables the timeout entirely — the staged token persists until it's confirmed, replaced by another scan, or cleared when media stops. ```toml [readers.scan.launch_guard] timeout = 30 # wait up to 30 seconds timeout = -1 # wait indefinitely ``` ##### delay {#launch-guard-delay} | Key | Type | Default | | ----- | ------------ | ------- | | delay | float (≥0.0) | 0.0 | `delay` sets a mandatory cool-down period, in seconds, before re-tap confirmation is accepted. During this window, re-tapping the same card resets both the delay and the timeout — confirmation is not accepted until the full delay has elapsed. When the delay expires, a ready sound plays and a `tokens.staged.ready` notification is sent. `delay` is clamped to half of `timeout` if it would equal or exceed it. It is forced to `0` when `timeout` is negative. ```toml [readers.scan.launch_guard] timeout = 30 delay = 5 # block re-tap for the first 5 seconds ``` Setting `delay` to `0` (the default) disables the cool-down and re-tap confirmation is accepted immediately after staging. ##### require_confirm {#launch-guard-require-confirm} | Key | Type | Default | | --------------- | ------- | ------- | | require_confirm | boolean | false | `require_confirm` disables re-tap confirmation. When set to `true`, re-tapping the staged card does nothing — the only way to launch a staged token is via the `confirm` API method. ```toml [readers.scan.launch_guard] enabled = true require_confirm = true ``` This is useful when you want an external device (a physical button, a companion app, or an automation script) to be the sole confirmation path. #### readers.connect `readers.connect` manually defines a reader which is physically connected to the host device and is not auto-detected. It's a sub-section that can be defined multiple times, and must have this header: `[[readers.connect]]` Pay attention to the double pairs of square brackets. Each defined `readers.connect` section must have its own header. ```toml [[readers.connect]] driver = 'pn532uart' path = '/dev/ttyUSB0' [[readers.connect]] driver = 'file' path = '/tmp/some_file' ``` ##### driver | Key | Type | Default | | ------ | ------ | ------- | | driver | string | | `driver` specifies which reader driver should be used to attempt connection to the reader device. See [reader drivers](../readers/drivers.md) for a list of available drivers. ##### path | Key | Type | Default | | ---- | ------ | ------- | | path | string | | `path` is an argument for the specified reader driver for how the device should be found. See the documentation for your specific reader hardware for configuration examples. ##### id_source | Key | Type | Default | | --------- | ------ | ------- | | id_source | string | | `id_source` specifies which identifier source to use for token identification. This is only supported by certain reader drivers: - `opticaldrive`: Can use `uuid` (disc UUID) or `label` (disc label) Other reader drivers ignore this setting. ##### enabled {#readers-connect-enabled} | Key | Type | Default | | ------- | ------- | ------- | | enabled | boolean | | `enabled` temporarily disables a reader connection without removing it from the config. When not set, the connection is enabled by default. ```toml [[readers.connect]] driver = 'pn532uart' path = '/dev/ttyUSB0' enabled = false ``` This is useful for keeping a connection definition in the config while not actively using it, without having to delete and re-add it later. #### readers.drivers `readers.drivers` configures driver-specific settings. It's a sub-section that uses driver IDs as keys, and must have this header format: `[readers.drivers.DRIVER_ID]` ```toml [readers.drivers.acr122pcsc] auto_detect = false [readers.drivers.simpleserial] enabled = false ``` ##### enabled {#readers-drivers-enabled} | Key | Type | Default | | ------- | ------- | ------------------ | | enabled | boolean | _varies by driver_ | `enabled` allows you to explicitly enable or disable a specific reader driver. When not specified, the driver uses its default enabled state. ##### auto_detect {#readers-drivers-auto-detect} | Key | Type | Default | | ----------- | ------- | ------- | | auto_detect | boolean | true | `auto_detect` controls whether this specific driver should participate in automatic reader detection, overriding the global `auto_detect` setting for this driver only. ### Systems #### systems.default `systems.default` overrides the default behavior of the specified system. It's a sub-section that can be defined multiple times, and must have this header: `[[systems.default]]`. See also [`launchers.default`](#launchersdefault) for launcher-specific settings. Pay attention to the double pairs of square brackets. Each defined `systems.default` section must have its own header. ```toml [[systems.default]] system = 'SNES' launcher = 'SindenSNES' before_exit = '**input.keyboard:{f12}||**delay:2000' ``` ##### system | Key | Type | Default | | ------ | ------ | ------- | | system | string | | ID of the [system](../features/systems.md) this default override entry applies to. ##### launcher {#systems-default-launcher} | Key | Type | Default | | -------- | ------ | ------- | | launcher | string | | ID of the [launcher](../features/launchers.md) that should be used by default when media in this system is launched. This applies to title/search launches and direct path launches when Core can infer the system. Explicit ZapScript `?launcher=` arguments and per-media launcher overrides saved through [`media.meta.update`](./api/methods.md#mediametaupdate) take priority over this default. ##### before_exit | Key | Type | Default | | ----------- | ------ | ------- | | before_exit | string | | A [hook](../features/hooks.md) containing a snippet of [ZapScript](../zapscript/index.md) to run before media exits if [hold mode](#scan-mode) is enabled. Core waits for it before continuing exit handling, so commands like [`delay`](../zapscript/utilities.md#delay) can be used. ##### pause_on_launch | Key | Type | Default | | --------------- | ------- | ------- | | pause_on_launch | boolean | true | `pause_on_launch` controls whether [background audio](../features/audio.md) for this system pauses when a game launches in the primary slot, then resumes when the game quits. It currently applies to the `Audio` system. Set it to `false` to keep background music playing through game launches. ```toml [[systems.default]] system = "Audio" pause_on_launch = false ``` ### Launchers ```toml [launchers] index_root = [ '/media/alt_mount/games' ] allow_file = [ '^/media/fat/something.mgl$' ] on_media_start = '**echo:media started' ``` #### index_root | Key | Type | Default | | ---------- | -------- | ------- | | index_root | string[] | [] | `index_root` is a list of paths on the host device that should _also_ be searched when indexing media during a media database update. For example, if `index_root` was set to `[ '/media/fat/other_place' ]`, a database update will search all standard locations like normal but then also attempt to search _/media/fat/other_place/SNES_, _/media/fat/other_place/Genesis_, etc. for potential media. To exclude specific directories from being scanned, create an empty file named `.zaparooignore` in that directory. The directory and all subdirectories will be skipped during media database updates. #### allow_file | Key | Type | Default | | ---------- | ------------------------- | ------- | | allow_file | string[] (regex patterns) | [] | `allow_file` allows certain files to be launched if their assigned launcher requires it. This is used on platforms like [Windows](../platforms/windows/index.md) to allow executable files to be launched with tokens, where this ability is useful but would be a security issue if allowed globally. Each entry in this option is a [Regular Expression](https://github.com/google/re2/wiki/Syntax). Notes on usage here: - Patterns are automatically anchored and must match the full file path. Use `.*pattern.*` for substring matching. - On Windows, file path separators must be escaped: `C:\\Test\\Thing.exe` - On Windows, patterns are automatically made case-insensitive. On other platforms, add `(?i)` at the beginning of a pattern for case-insensitive matching. #### media_dir | Key | Type | Default | | --------- | ------ | ------------------ | | media_dir | string | \/media | `media_dir` overrides the default location on disk where remote media downloads will be stored. By default, it will use the `media` directory in the Core data folder. #### on_media_start | Key | Type | Default | | -------------- | ------ | ------- | | on_media_start | string | | `on_media_start` is a [hook](../features/hooks.md#media-launch-hooks) containing a snippet of [ZapScript](../zapscript/index.md). It runs after Core sets active media, regardless of the scan mode. See also [`on_scan`](#on_scan) and [`on_remove`](#on_remove) for related scan events. #### before_media_start | Key | Type | Default | | ------------------ | ------ | ------- | | before_media_start | string | | `before_media_start` is a [hook](../features/hooks.md#media-launch-hooks) containing a snippet of [ZapScript](../zapscript/index.md). It runs immediately before media launches. ```toml [launchers] before_media_start = "**execute:/path/to/script.sh" ``` This hook can block the launch by returning an error. If the ZapScript command fails or a script executed via `**execute:` returns a non-zero exit code, the media launch is blocked and an error is shown. Scripts executed via `**execute:` receive a `ZAPAROO_ENVIRONMENT` environment variable containing the [expression environment](../zapscript/syntax.md#expression-environment) as JSON. The `launching` object contains information about the media that is about to launch, which is only available in this hook. See [Hooks](../features/hooks.md) for examples. #### launchers.default `launchers.default` overrides default settings for specific launchers. It's a sub-section that can be defined multiple times, and must have this header: `[[launchers.default]]`. See also [`systems.default`](#systemsdefault) for system-specific settings. Pay attention to the double pairs of square brackets. Each defined `launchers.default` section must have its own header. ```toml [[launchers.default]] launcher = 'KodiTV' server_url = 'http://localhost:5678' ``` ##### launcher {#launchers-default-launcher} | Key | Type | Default | | -------- | ------ | ------- | | launcher | string | | ID of the [launcher](../features/launchers.md) this default override entry applies to. ##### install_dir | Key | Type | Default | | ----------- | ------ | ------- | | install_dir | string | | Override the default installation directory for this launcher. Only supported by some launchers and usually refers to the parent directory of the executable which does the launching. ##### server_url | Key | Type | Default | | ---------- | ------ | ------- | | server_url | string | | Override the default server URL for this launcher. Only supported by some launchers and refers to an API base address. ##### action | Key | Type | Default | | ------ | ------ | ------- | | action | string | | Set the default action for this launcher. Currently only supported by the Steam launcher. ```toml [[launchers.default]] launcher = "Steam" action = "details" ``` Available values for Steam: - `run` (default): Launch the game - `details`: Open the game's details page in the Steam library This can be overridden per-token using the `?action=` advanced argument in ZapScript. ##### load_path | Key | Type | Default | | --------- | ------ | ------- | | load_path | string | | Override the implementation file the launcher loads. Only supported by some launchers. For MiSTer, this is an MGL-form RBF path relative to `/media/fat`, without extension. This is useful when multiple versions of a core share the same short name and you want to set a specific one as the default. ```toml [[launchers.default]] launcher = "Nintendo64" load_path = "_LLAPI/N64_LLAPI" ``` ### ZapScript ```toml [zapscript] allow_execute = [ 'touch /tmp/tap_time', '/media/fat/linux/mplayer .+' ] allow_http = [ 'https://example\.com/.*' ] block_commands = [ 'execute' ] [zapscript.input] mode = 'combos' block = ['{alt+f4}'] ``` #### allow_execute | Key | Type | Default | | ------------- | ------------------------- | ------- | | allow_execute | string[] (regex patterns) | [] | :::danger Security Warning `allow_execute` allows specific executables and arguments to be run using the `**execute` [ZapScript](../zapscript/index.md) command. By default, the command does not allow anything to be run. Be extremely careful with this setting as it can execute arbitrary commands on your system. ::: Each entry in this option is a [Regular Expression](https://github.com/google/re2/wiki/Syntax). Notes on usage here: - Patterns are automatically anchored and must match the full command string. `echo` matches only `echo`, not `echo && rm -rf /`. Use `echo.*` or `.*pattern.*` for broader matching. - On Windows, file path separators must be escaped: `C:\\Test\\Thing.exe` #### allow_http | Key | Type | Default | | ---------- | ------------------------- | ------- | | allow_http | string[] (regex patterns) | [] | `allow_http` restricts which URLs the [`**http.get`](../zapscript/http.md#httpget) and [`**http.post`](../zapscript/http.md#httppost) ZapScript commands can access. When empty (the default), all URLs are allowed. When configured, only matching URLs are permitted. Each entry is a [Regular Expression](https://github.com/google/re2/wiki/Syntax). Patterns are automatically anchored to match the full URL. ```toml [zapscript] allow_http = [ 'https://example\.com/.*', 'http://localhost:8080/.*' ] ``` #### block_commands | Key | Type | Default | | -------------- | -------- | ------- | | block_commands | string[] | [] | `block_commands` disables specific ZapScript commands by name. Any listed command will always fail, regardless of any other allowlists. ```toml [zapscript] block_commands = [ 'execute', 'http.post' ] ``` Command names match the ZapScript command identifier (e.g., `execute`, `http.get`, `http.post`, `input.keyboard`). #### zapscript.input `zapscript.input` is a sub-section of `zapscript` that controls which keys the [`**input.keyboard`](../zapscript/input.md#inputkeyboard) and [`**input.gamepad`](../zapscript/input.md#inputgamepad) commands can send. ```toml [zapscript.input] mode = 'combos' allow = ['{f1}', '{f2}', '{enter}', '{esc}'] block = ['{alt+f4}'] ``` ##### mode {#zapscript-input-mode} | Key | Type | Default | | ---- | ----------------------------------- | -------------------- | | mode | string (`"combos"`, `"unrestricted"`) | _varies by platform_ | Controls how input keys are filtered when no `allow` list is configured. - `combos` — only key combos and named special keys (e.g., `{f1}`, `{ctrl+q}`) are allowed. Single characters (e.g., `a`, `5`) are blocked. This is the default on desktop platforms. - `unrestricted` — all keys are allowed (subject to the `block` list). This is the default on embedded platforms like MiSTer. Platform defaults: - **Desktop** (Linux, Windows, macOS, SteamOS, ChimeraOS, Bazzite): `combos` - **Embedded** (MiSTer, MiSTex, Batocera, Recalbox, LibreELEC, RetroPie): `unrestricted` ##### allow {#zapscript-input-allow} | Key | Type | Default | | ----- | -------- | ------- | | allow | string[] | [] | When set, only the listed keys are permitted. All others are blocked regardless of `mode` or `block`. Matching is case-insensitive. ```toml [zapscript.input] allow = ['{f1}', '{f2}', '{enter}', '{esc}'] ``` ##### block {#zapscript-input-block} | Key | Type | Default | | ----- | -------- | ---------------------------- | | block | string[] | _platform default (desktop)_ | A list of keys to always block. On desktop platforms, a built-in block list applies by default; setting `block` to any value, even an empty list, replaces it entirely. The default desktop block list covers TTY switching (`{ctrl+alt+f1}`–`{ctrl+alt+f7}`), `{ctrl+alt+t}`, `{ctrl+alt+delete}`, `{super}`, `{meta}`, `{alt+f4}`, and `{cmd+space}`. ```toml [zapscript.input] block = ['{alt+f4}', '{ctrl+alt+t}'] # custom block list, replaces defaults ``` The `block` list is ignored when `allow` is configured. ### Service ```toml [service] api_port = 7497 api_listen = "0.0.0.0" on_boot = "**execute:/media/fat/zaparoo/scripts/on-boot.sh" on_ready = "**execute:/media/fat/zaparoo/scripts/on-ready.sh" allowed_ips = [ "192.168.1.100", "192.168.1.0/24" ] encryption = false device_id = '4d01c19f-09ba-4871-a58a-82fb49f5b518' allowed_origins = [ 'https://app.zaparoo.org' ] allow_run = [ '\*\*launch\.random:.+' ] [[service.publishers.mqtt]] enabled = true broker = "mqtt://localhost:1883" topic = "zaparoo/events" filter = [ "media.started", "media.stopped", "tokens.added" ] ``` #### api_port | Key | Type | Default | | -------- | -------------------- | ------- | | api_port | integer (1024-65535) | 7497 | `api_port` specifies which port the [API](./api/index.md) of Core should be accessible from. **Don't change this unless you know what you're doing. It will currently break external tools that rely on it being the default value.** #### on_boot | Key | Type | Default | | ------- | ------ | ------- | | on_boot | string | | `on_boot` is a [hook](../features/hooks.md#service-startup-hooks) containing a snippet of [ZapScript](../zapscript/index.md). It runs after Core initializes, only on the first Core start for the current operating system boot. ```toml [service] on_boot = "**execute:/media/fat/zaparoo/scripts/on-boot.sh" ``` If Core cannot detect boot state, `on_boot` is skipped and a warning is logged. #### on_ready | Key | Type | Default | | -------- | ------ | ------- | | on_ready | string | | `on_ready` is a [hook](../features/hooks.md#service-startup-hooks) containing a snippet of [ZapScript](../zapscript/index.md). It runs after Core initializes on every service start. On platforms that report service readiness, Core waits for that readiness signal first. ```toml [service] on_ready = "**execute:/media/fat/zaparoo/scripts/on-ready.sh" ``` #### api_listen | Key | Type | Default | | ---------- | ------ | ----------- | | api_listen | string | `"0.0.0.0"` | `api_listen` specifies which network interface the API server should bind to. ```toml [service] api_listen = "127.0.0.1" # Localhost only ``` Common values: - `"0.0.0.0"` - Listen on all network interfaces (default, allows remote connections) - `"127.0.0.1"` - Localhost only (blocks all remote connections) - Specific IP address - Bind to a specific network interface Use `"127.0.0.1"` if you only want local access to the API and Web UI. #### allowed_ips | Key | Type | Default | | ----------- | -------- | ------- | | allowed_ips | string[] | [] | `allowed_ips` creates an IP allowlist for remote HTTP API access. Localhost is always allowed. When this list is empty, remote HTTP and REST API requests are blocked by default. WebSocket API routes are not controlled by `allowed_ips`. They use API key authentication when API keys are configured, or [paired-client encryption](./api/encryption.md) when [`encryption`](#encryption) is enabled. ```toml [service] allowed_ips = [ "192.168.1.100", # Single IP "192.168.1.0/24", # CIDR range (entire subnet) "10.0.0.0/8", # Large CIDR range "2001:db8::1", # IPv6 address "2001:db8::/32" # IPv6 CIDR range ] ``` Supports: - Individual IPv4 addresses (e.g., `"192.168.1.100"`) - Individual IPv6 addresses (e.g., `"2001:db8::1"`) - CIDR ranges for IPv4 (e.g., `"192.168.1.0/24"`) - CIDR ranges for IPv6 (e.g., `"2001:db8::/32"`) **Empty list (default)**: Localhost only for HTTP and REST API requests Port numbers in IP addresses are automatically stripped during matching. #### device_id | Key | Type | Default | | --------- | ------------- | -------------------------- | | device_id | string (UUID) | _generated at first start_ | `device_id` is a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) that is used to uniquely identify the instance of the Core service running on a host device. It's currently reserved for future use when devices can communicate with each other and external services. **It should not be changed once set.** #### allowed_origins | Key | Type | Default | | --------------- | -------- | ------- | | allowed_origins | string[] | [] | `allowed_origins` specifies which origins are allowed to access the Core API via CORS (Cross-Origin Resource Sharing). By default, localhost and the active device IP address are allowed. #### encryption | Key | Type | Default | | ---------- | ------- | ------- | | encryption | boolean | false | `encryption` requires remote WebSocket API clients to use the [paired-client encryption flow](./api/encryption.md). Localhost connections are always allowed without encryption. ```toml [service] encryption = true ``` When disabled, remote WebSocket clients use API key authentication if API keys are configured. #### allow_run | Key | Type | Default | | --------- | ------------------------- | ------- | | allow_run | string[] (regex patterns) | [] | `allow_run` explicitly allows [ZapScript](../zapscript/index.md) to be run using the [run endpoint](./api/methods.md) of the [Core API](./api/index.md). By default, nothing is allowed. Each entry is a [Regular Expression](https://github.com/google/re2/wiki/Syntax). Notes on usage here: - Patterns are automatically anchored and must match the full command string. Characters `*` and `.` common in ZapScript must be escaped (e.g., `\*\*launch\.random:.*`). - The input is parsed as ZapScript and each command is checked individually. All commands in a chained script must match, so a pattern like `\*\*launch\.random:.*` covers both `**launch.random:SNES` alone and `**launch.random:SNES||**launch.random:NES`. - Plain file paths are normalized to a launch command before checking. - When `allow_run` is configured, remote IPs can access run endpoints regardless of `allowed_ips`. The `allow_run` patterns still restrict which ZapScript can run. #### service.discovery `service.discovery` is a sub-section of `service` that configures mDNS network discovery. ```toml [service.discovery] enabled = true instance_name = "Living Room MiSTer" ``` When enabled, Zaparoo advertises itself on your local network using mDNS/DNS-SD. Mobile apps and other clients can automatically find and connect to your Zaparoo instance without needing to enter IP addresses manually. Access Zaparoo using friendly `.local` addresses like `http://mister.local:7497`. ##### enabled {#service-discovery-enabled} | Key | Type | Default | | ------- | ------- | ------- | | enabled | boolean | true | `enabled` enables or disables mDNS network discovery. When enabled, Zaparoo advertises a `_zaparoo._tcp` service with TXT records containing the device ID, version, and platform. ##### instance_name | Key | Type | Default | | ------------- | ------ | -------------------- | | instance_name | string | _system hostname_ | `instance_name` specifies a custom display name for this Zaparoo instance on the network. If not set, defaults to the system hostname. #### service.publishers `service.publishers` is a sub-section of `service` that configures publishing Core events to external services. ##### service.publishers.mqtt `service.publishers.mqtt` configures MQTT publishers for broadcasting Core events. See the [Publishers](../features/publishers.md#mqtt) feature page for an overview. It's a sub-section that can be defined multiple times, and must have this header: `[[service.publishers.mqtt]]` Pay attention to the double pairs of square brackets. Each defined MQTT publisher section must have its own header. ```toml [[service.publishers.mqtt]] enabled = true broker = "mqtt://localhost:1883" topic = "zaparoo/events" filter = [ "media.started", "media.stopped", "tokens.added" ] ``` ###### enabled {#mqtt-publisher-enabled} | Key | Type | Default | | ------- | ------- | ------- | | enabled | boolean | true | `enabled` turns this MQTT publisher on or off. ```toml [[service.publishers.mqtt]] enabled = false ``` ###### broker {#mqtt-publisher-broker} | Key | Type | Default | | ------ | ------ | ------- | | broker | string | | `broker` specifies the MQTT broker connection URL. ```toml [[service.publishers.mqtt]] broker = "mqtt://192.168.1.100:1883" ``` Supported URL schemes: - `mqtt://` - Standard MQTT connection - `mqtts://` or `ssl://` - MQTT over TLS/SSL For TLS connections and authentication, see the MQTT reader's [auth.toml configuration](../readers/mqtt.md#add-broker-credentials). ###### topic {#mqtt-publisher-topic} | Key | Type | Default | | ----- | ------ | ------- | | topic | string | | `topic` specifies the MQTT topic to publish events to. ```toml [[service.publishers.mqtt]] topic = "home/zaparoo/events" ``` ###### filter {#mqtt-publisher-filter} | Key | Type | Default | | ------ | -------- | ------------------------ | | filter | string[] | [] (publish all events) | `filter` limits which event types are published. When empty, all events are published. ```toml [[service.publishers.mqtt]] filter = [ "media.started", "media.stopped", "tokens.added", "readers.added" ] ``` Available event types match the [Core API notification types](./api/notifications.md). ##### service.publishers.pixelcade `service.publishers.pixelcade` configures [PixelCade](https://pixelcade.org) publishers that display game marquee artwork on PixelCade LED displays. See the [Publishers](../features/publishers.md#pixelcade) feature page for an overview. It can be defined multiple times and must use this header: `[[service.publishers.pixelcade]]` On `media.started`, the publisher sends a GET request to the PixelCade arcade endpoint, mapping the Zaparoo system ID to the matching PixelCade console folder. ```toml [[service.publishers.pixelcade]] enabled = true host = "192.168.1.50" port = 8080 mode = "stream" filter = [ "media.started" ] ``` ###### enabled {#pixelcade-publisher-enabled} | Key | Type | Default | | ------- | ------- | ------- | | enabled | boolean | true | `enabled` turns this PixelCade publisher on or off. ```toml [[service.publishers.pixelcade]] enabled = false ``` ###### host {#pixelcade-publisher-host} | Key | Type | Default | | ---- | ------ | ------- | | host | string | | `host` is the hostname or IP address of the PixelCade device. This field is required. ```toml [[service.publishers.pixelcade]] host = "192.168.1.50" ``` ###### port {#pixelcade-publisher-port} | Key | Type | Default | | ---- | ------- | ------- | | port | integer | 8080 | `port` is the PixelCade HTTP API port. ###### mode {#pixelcade-publisher-mode} | Key | Type | Default | | ---- | ------ | ---------- | | mode | string | `"stream"` | `mode` controls which PixelCade arcade endpoint is used when displaying marquee art on `media.started`. Accepted values: - `"stream"` — uses the streaming endpoint (default) - `"write"` — uses the write endpoint ###### filter {#pixelcade-publisher-filter} | Key | Type | Default | | ------ | -------- | ------------------------ | | filter | string[] | [] (publish all events) | `filter` limits which event types trigger requests to PixelCade. When empty, all events are forwarded to the publisher. Only `media.started` produces PixelCade requests; all other notification types are ignored. ```toml [[service.publishers.pixelcade]] filter = [ "media.started" ] ``` Available event types match the [Core API notification types](./api/notifications.md). ### Groovy ```toml [groovy] gmc_proxy_enabled = true gmc_proxy_port = 32106 gmc_proxy_beacon_interval = '2s' ``` #### gmc_proxy_enabled | Key | Type | Default | | ----------------- | ------- | ------- | | gmc_proxy_enabled | boolean | false | `gmc_proxy_enabled` enables or disables the GMC proxy service for Groovy MiSTer Control integration. #### gmc_proxy_port | Key | Type | Default | | -------------- | ----------------- | ------- | | gmc_proxy_port | integer (1-65535) | 32106 | `gmc_proxy_port` specifies which port the GMC proxy service should listen on. #### gmc_proxy_beacon_interval | Key | Type | Default | | ------------------------- | ------ | ------- | | gmc_proxy_beacon_interval | string | 2s | `gmc_proxy_beacon_interval` sets the interval for GMC proxy beacon broadcasts. ### Playtime ```toml [playtime] retention = 365 [playtime.limits] enabled = true daily = "2h" session = "45m" session_reset = "20m" warnings = ["10m", "5m", "2m", "1m"] ``` The `playtime` section configures playtime tracking, limits, and parental controls. See the [Play Controls documentation](../features/play-controls.md#playtime-limits) for detailed information and examples. #### retention | Key | Type | Default | | --------- | ------- | ------- | | retention | integer | 365 | `retention` specifies how many days of playtime history to keep. Old records are automatically cleaned up. ```toml [playtime] retention = 90 # Keep 90 days ``` Set to `0` to keep all history forever (disables cleanup). #### playtime.limits `playtime.limits` is a sub-section of `playtime` and must be defined with the header: `[playtime.limits]` ##### enabled {#playtime-limits-enabled} | Key | Type | Default | | ------- | ------- | ------- | | enabled | boolean | false | `enabled` turns playtime limit enforcement on or off. ```toml [playtime.limits] enabled = true ``` ##### daily | Key | Type | Default | | ----- | -------- | ------- | | daily | duration | none | `daily` sets the maximum total playtime per calendar day (resets at midnight). ```toml [playtime.limits] daily = "2h" # 2 hours per day daily = "1h30m" # 1 hour 30 minutes ``` Omit or leave empty to disable daily limits. ##### session | Key | Type | Default | | ------- | -------- | ------- | | session | duration | none | `session` sets the maximum playtime per gaming session. ```toml [playtime.limits] session = "45m" # 45 minutes per session session = "1h" # 1 hour per session ``` Omit or leave empty to disable session limits. ##### session_reset | Key | Type | Default | | ------------- | -------- | ------- | | session_reset | duration | `"20m"` | `session_reset` sets the enforced break time between sessions. After a game stops, cumulative playtime is preserved during this period. If another game launches within this timeout and the session limit hasn't been reached, the session continues. However, if the session limit was reached, new launches are blocked until this timeout expires and the session fully resets. ```toml [playtime.limits] session_reset = "20m" # 20-minute break before new session (default) session_reset = "1h" # 1-hour break before new session session_reset = "0" # Never reset sessions automatically ``` This creates a mandatory cooldown period between sessions when limits are enforced. ##### warnings | Key | Type | Default | | -------- | ---------- | -------------------- | | warnings | duration[] | `["5m", "2m", "1m"]` | `warnings` sets the time-remaining intervals when warnings should be triggered. ```toml [playtime.limits] warnings = ["10m", "5m", "2m", "1m"] ``` Warnings are sent as notifications to the Zaparoo App and played as audio feedback. ## Auth File A separate TOML file called `auth.toml`, alongside the config file, can be created which defines credentials for remote connections. ### API Keys :::warning API key authentication is a basic access control measure, not a security feature. Connections are unencrypted and keys can be intercepted by anyone on your network. ::: API key authentication restricts remote access to the Zaparoo API: ```toml api_keys = ["your-secret-key-here"] ``` Authenticate using either a Bearer token header or query parameter: ``` Authorization: Bearer your-secret-key-here ``` ``` http://zaparoo:7497/api/v1/launch?key=your-secret-key-here ``` Localhost connections are always allowed without authentication, so the CLI and TUI continue to work without configuration. ### Remote Credentials Define credentials used when Core connects to remote endpoints: ```toml ["smb://10.0.0.123/Games"] username = "myaccount" password = "Password123" ``` ### URL Matching When a remote endpoint matches against the URL key, the credential set will be attached to the request. Matching uses a 3-step fallback: 1. **Exact scheme match** - scheme, host, and path prefix must match exactly 2. **Canonical scheme match** - equivalent schemes match (e.g., `tcp://` matches `mqtt://`) 3. **Schemeless match** - entries without a scheme match any connection to that host:port ```toml # Matches only mqtt:// connections ["mqtt://192.168.1.100:1883"] username = "mqtt_only" password = "pass" # Matches any connection to this host:port (mqtt, mqtts, tcp, etc.) ["192.168.1.100:1883"] username = "any_scheme" password = "pass" ``` ### Scheme Aliases These scheme aliases are recognized: | Alias | Canonical | |-------|-----------| | `tcp://` | `mqtt://` | | `ssl://` | `mqtts://` | | `ws://` | `http://` | | `wss://` | `https://` | Multiple credentials may be defined for the same server but on different paths. ## TUI Config File A separate TOML file called `tui.toml`, alongside the config file, stores settings for the [terminal UI](tui.md). ```toml theme = "default" write_format = "zapscript" mouse = true crt_mode = false on_screen_keyboard = false ``` ### theme Visual theme for the TUI. Available themes can be cycled through in the TUI settings menu. ### write_format Format used when writing tokens from the search media screen. Options are `zapscript` (default) or `path`. ### mouse Enable mouse input in the TUI. Defaults to `true`. ### crt_mode Fixed 75x15 window size for better display on CRT monitors. Requires a restart to take effect. Defaults to `false`, but `true` on MiSTer and MiSTeX. ### on_screen_keyboard Show a virtual keyboard when pressing Enter on text fields, for controller/gamepad input. Defaults to `false`, but `true` on MiSTer and MiSTeX. ## Example File
Click to view a complete example config.toml file An example `config.toml` file with all fields filled, using the example sections shown above. ```toml title="config.toml" config_schema = 1 debug_logging = true error_reporting = false auto_update = false update_channel = "stable" [audio] scan_feedback = true volume = 100 success_sound = "custom_success.wav" fail_sound = "custom_fail.wav" limit_sound = "custom_limit.wav" pending_sound = "custom_pending.ogg" ready_sound = "custom_ready.ogg" [input] gamepad_enabled = true [media] filename_tags = true default_regions = ["us", "world"] default_langs = ["en"] [readers] auto_detect = true scan_history = 30 [readers.scan] mode = 'hold' exit_delay = 3.0 ignore_system = [ 'PC', 'MSX' ] on_scan = '**echo:card was scanned' on_remove = '**echo:card was removed' ignore_on_connect = true [readers.scan.launch_guard] enabled = true timeout = 30 delay = 5 require_confirm = false [[readers.connect]] driver = 'acr122pcsc' path = '/dev/ttyUSB0' [[readers.connect]] driver = 'opticaldrive' path = '/dev/sr0' id_source = 'uuid' [readers.drivers.simpleserial] enabled = false [[systems.default]] system = 'SNES' launcher = 'SindenSNES' before_exit = '**input.keyboard:{f12}||**delay:2000' [launchers] index_root = [ '/media/alt_mount/games' ] allow_file = [ '^/media/fat/something.mgl$' ] on_media_start = '**echo:media started' [[launchers.default]] launcher = 'KodiTV' server_url = 'http://localhost:5678' [zapscript] allow_execute = [ 'touch /tmp/tap_time', '/media/fat/linux/mplayer .+' ] allow_http = [ 'https://example\.com/.*' ] block_commands = [] [zapscript.input] mode = 'combos' block = [] [playtime] retention = 365 [playtime.limits] enabled = true daily = "2h" session = "45m" session_reset = "20m" warnings = ["10m", "5m", "2m", "1m"] [service] api_port = 7497 api_listen = "0.0.0.0" allowed_ips = [ "192.168.1.100", "192.168.1.0/24" ] encryption = false device_id = '4d01c19f-09ba-4871-a58a-82fb49f5b518' allowed_origins = [ 'https://app.zaparoo.org' ] allow_run = [ '\*\*launch\.random:.+' ] [service.discovery] enabled = true instance_name = "Living Room MiSTer" [[service.publishers.mqtt]] enabled = true broker = "mqtt://localhost:1883" topic = "zaparoo/events" filter = [ "media.started", "media.stopped", "tokens.added" ] [groovy] gmc_proxy_enabled = true gmc_proxy_port = 32106 gmc_proxy_beacon_interval = '2s' ```
--- ## Developer Guide ## Environment Zaparoo Core is written in Go, uses Task for build scripts, and Docker for building all platforms. Build scripts work on Linux, Mac and Windows (natively or WSL). Just make sure all dependencies are installed with Go, Task and Docker binaries available in your path. ### Dependencies - [Go](https://go.dev/) Version 1.26 or newer. The build script assumes your Go path is in the default location, for caching between Docker build environments: `$HOME/go` - [Task](https://taskfile.dev/) - [Docker](https://www.docker.com/) On Linux, enable cross-platform builds with something like: `apt install qemu binfmt-support qemu-user-static` On Mac and Windows, Docker Desktop comes with everything you need already. If you're using WSL, make sure it's using Docker from the host machine. ## Building To start, you can run `go mod download` from the root of the project folder. This will download all dependencies used by the project. Builds automatically do this, but running it now will stop your editor from complaining about missing modules. All build steps are done with the `task` command run from the root of the project folder. Run `task --list-all` by itself to see a list of available commands. Built binaries will be created in the `_build` directory under its appropriate platform and architecture subdirectory. These are the important commands: - `task :build-` Complete a full build for the given platform and architecture. This will also automatically create any necessary Docker images. - `task :deploy-` Some platforms also have a helper command to automatically make a new build, transfer it to a remote device and remotely restart the service running. For example, to enable this for MiSTer, add `MISTER_IP=1.2.3.4` to a `.env` file in the root of the project and then run `task mister:deploy-arm`. ### Direct Builds Core can be built directly on the host using the `task build` command, but will require some extra dependencies in the environment depending on the platform, to satisfy the Cgo dependency. Docker is not required. #### Linux Linux is the most complex because it uses a custom build of libnfc. Check the Dockerfile at `scripts/zigcc/Dockerfile` for full details of setting up the environment. You will need: - Standard build tools (GCC) - libnfc build dependencies (check Dockerfile) - Latest commit of [libnfc repo](https://github.com/nfc-tools/libnfc) Once these are installed and set up, you can just run `task build` for a generic Linux desktop build or `PLATFORM= task build` for one of the variant platforms. #### Windows Windows doesn't use libnfc so doesn't have as many dependencies, but does need a compiler available for sqlite. You will need: - [TDM-GCC](https://jmeubank.github.io/tdm-gcc/) Then you can build with `NO_LIBNFC=true task build`. The `CC` and `CXX` environment variables can also be set if you need to specify the path to TDM-GCC. #### Mac Mac also doesn't use libnfc but does need Xcode for sqlite and some native UI elements. It also cannot produce static builds which is the default. You will need: - Install Xcode from the App Store Then build with `NO_LIBNFC=true NO_STATIC=true task build`. This will produce a binary for your host's architecture, not a universal binary or .app folder. ## Testing When changing the application behavior, in particular the reader loop, some testing is required. The [Scan Behavior checklist](./scan-behavior) contains a list of expected behavior for the application under certain conditions. It is useful to test them and ensure we didn't break any flows. --- ## Title normalization and matching Zaparoo matches games by title, not filename. Write a game name like "The Legend of Zelda: Ocarina of Time" and it gets fuzzy-matched against indexed ROM filenames. Both the query and the filenames go through the same slug pipeline, so naming differences wash out. Slugs are not IDs. They're a normalized form used for matching. The original titles and filenames stick around for display and fallback matching. Offline-first, no hashing (too slow on MiSTer FPGA / older Pis). English heuristics only for now. ## How it works ### Indexing When scanning media, Zaparoo: 1. Cleans path and extracts filename (strips extension and path) 2. Parses filename to extract a clean display title (see [Filename parser](#filename-parser)) 3. Extracts tags from brackets/parentheses (see [Tag extraction](#tag-extraction)) 4. Determines media type from the system (Game, TVShow, Movie, Music, Image, Audio, Video, Application) 5. Slugifies the title using media-type-aware normalization (see [Slug normalization](#slug-normalization)) 6. Stores path, title, slug, tags, and metadata in the database ### Resolution When launching by title (e.g. `launch.title` with arg `NES/Super Mario Bros`): 1. Parses `SystemID/GameName` format 2. Extracts and merges tags from the query (three sources — see [Tags in queries](#tags-in-queries)) 3. Slugifies the game name using the same normalization as indexing 4. Tries matching strategies in order until finding a result (see [Matching strategies](#matching-strategies)) 5. Scores and filters results to pick the best match 6. Caches the resolution for future queries --- ## Slug normalization Two-phase pipeline that converts titles into a canonical form. Indexing and resolution run the same normalization. `pkg/database/slugs/slugify.go` → `Slugify(mediaType, input)` ### Phase 1: Media-specific parsing Format-specific normalization runs before the universal pass. **Games** (`pkg/database/slugs/media_parsing_game.go`): Width normalization first (fullwidth → ASCII), then: 1. Split on `:` and strip leading articles: "The Zelda: Link's Awakening" → "Zelda Link's Awakening" 2. Strip trailing articles: "Legend, The" → "Legend" 3. Strip metadata brackets: `(USA)`, `[!]`, `{Europe}` → removed 4. Strip edition/version suffixes: "Edition", "Version", "v1.0" → removed 5. Normalize symbols/separators (preserve commas for trailing article detection) 6. Expand abbreviations: "Bros" → "brothers", "vs" → "versus", "Dr" → "doctor" 7. Expand number words: "one" → "1", "two" → "2" (1-20) 8. Normalize ordinals: "2nd" → "2", "3rd" → "3" 9. Convert roman numerals: "VII" → "7", "II" → "2" (preserves "X" in "Mega Man X") **TV Shows** (`pkg/database/slugs/media_parsing_tv.go`): Width normalization first, then: 1. Strip scene tags: quality, codec, source (1080p, x264, BluRay, etc.) 2. Dots → spaces (scene release convention) 3. Strip metadata brackets 4. Normalize date episodes: various date formats → `YYYY-MM-DD` 5. Normalize season-based: `S01E02`, `1x02`, `S01.E02` → `s01e02` 6. Normalize absolute numbering: `Episode 001`, `Ep 42`, `#001` → `e001` 7. Reorder components: episode marker placed after show name 8. Split titles and strip articles 9. Strip trailing articles **Movies** (`pkg/database/slugs/media_parsing_movie.go`): 1. Width normalization 2. Strip scene tags (preserves edition qualifiers like "Extended", "Director's Cut") 3. Dots → spaces 4. Strip edition suffixes: "Edition", "Version", "Cut", "Release" 5. Strip brackets (years extracted as tags) 6. Split titles and strip articles 7. Strip trailing articles **Music** (`pkg/database/slugs/media_parsing_music.go`): 1. Width normalization 2. Strip scene tags: format (FLAC, MP3), quality (V0, 320), source (CD, Vinyl, WEB) 3. Separators → spaces (dots, underscores, dashes) 4. Strip brackets (years extracted as tags) 5. Strip disc numbers: CD1, Disc 1 → removed 6. Strip leading article: "The Beatles" → "Beatles" 7. Strip trailing articles 8. Collapse whitespace **Image/Audio/Video/Application**: Pass through to Phase 2 only. ### Phase 2: Universal normalization `pkg/database/slugs/slugify.go` → `normalizeInternal()` Applied after media-specific parsing: 1. Width normalization — fullwidth → halfwidth (ASCII), halfwidth → fullwidth (CJK) 2. Punctuation normalization — curly quotes, fancy dashes → ASCII equivalents 3. Unicode normalization — remove symbols (™©®), strip diacritics (Pokémon → Pokemon) 4. Symbols and separators — `&` → `and`, separators → spaces 5. Periods → spaces (safe after abbreviation expansion) 6. Lowercase Final: strip all non-alphanumeric characters (script-aware for non-Latin text). ### Example ``` Input: "The Legend of Zelda: Ocarina of Time (USA) [!]" Phase 1 (Game): Split & strip articles → "Legend of Zelda Ocarina of Time (USA) [!]" Strip brackets → "Legend of Zelda Ocarina of Time" Phase 2: Lowercase → "legend of zelda ocarina of time" Final: Strip non-alphanumeric → "legendofzeldaocarinaoftime" ``` ### Multi-script support `pkg/database/slugs/scripts.go` Non-Latin scripts (CJK, Cyrillic, Arabic, etc.) are kept intact while Latin text gets full normalization. Mixed titles just concatenate both parts: ``` "Street Fighter ストリートファイター" → "streetfighterストリートファイター" ``` You can search by either portion. Script-specific rules handle diacritics differently per writing system (Arabic vowel marks get stripped, Indic vowel marks are preserved, etc.). --- ## Filename parser Filenames are parsed during indexing to pull out clean titles and metadata tags. `pkg/database/mediascanner/indexing_pipeline.go` → `GetPathFragments()` `pkg/database/tags/filename_parser.go` ### Title extraction `tags.ParseTitleFromFilename(filename, stripLeadingNumbers)` — 8-step pipeline: 1. **Remove extension** — strips `.zip`, `.nes`, `.mkv` etc. (2-4 char extensions only) 2. **Strip release group** — `-YIFY`, `-SPARKS` at end (uppercase, 3+ chars) 3. **Normalize separators** — if filename has no spaces and 2+ dots/underscores/dashes, convert all to spaces. Detects scene releases and ROM naming conventions. 4. **Strip scene artifacts** — only after a year if found. Removes resolution, source, codec, audio, HDR, status tags. Protects titles like "Cam (2018)" where "Cam" is the actual title. 5. **Strip episode markers** — `S01E02`, `s1e2` removed from display title (kept for tag extraction) 6. **Strip leading numbers** — optional, only when directory context shows list-style numbering (`01 - Game Name` → `Game Name`) 7. **Remove bracket content** — all `()`, `[]`, `{}`, `<>` and their contents 8. **Normalize whitespace** — collapse multiple spaces, trim Examples: ``` "Super Mario Bros. III (USA) (Rev A) [!].nes" → remove ext → strip brackets → "Super Mario Bros. III" "The.Dark.Knight.2008.1080p.BluRay.x264-YIFY.mkv" → remove ext → strip group → normalize seps → strip scene → "The Dark Knight 2008" "Breaking.Bad.S01E02.Gray.Matter.720p.mkv" → normalize seps → strip scene → strip episode → "Breaking Bad Gray Matter" ``` ### Tag extraction `tags.ParseFilenameToCanonicalTags(filename)` — 4-step pipeline: **Step 1: Special patterns** — extracts patterns outside brackets: disc numbers (`Disc 1 of 2` → `disc:1`), revisions (`Rev A` → `rev:a`), versions (`v1.2` → `rev:1-2`), years (`1997` → `year:1997`), episodes (`S01E02` → `season:1`, `episode:2`), issues, tracks, translations. **Step 2: Bracket content** — state machine parser extracts `()`, `{}`, `<>` tags separately from `[]` tags. **Step 3: Parentheses tags** — context-aware disambiguation: - First paren tag → region if it matches the known region list (USA, Europe, Japan, etc.) - Subsequent tags → language, version, dev status - Multi-value: `(En,Fr,De)` → `lang:en`, `lang:fr`, `lang:de` - MiSTer Arcade `(Region YYMMDD)` packs a region and romset build date in one group: `(World 931005)` → `region:world`, `builddate:1993-10-05`. The date is also accepted as `YYYYMMDD` or `YYYY-MM-DD`, and the region/date may be comma-separated (`(EU, 961004)`). **Step 4: Square bracket tags** — always dump info or modifications: - `[!]` → `dump:verified`, `[b]` → `dump:bad`, `[h]` → `hack:yes`, `[T+En]` → `translation:en` Example: ``` "Super Mario Bros. 3 (USA) (Rev A) [!].nes" → [rev:a, region:us, dump:verified] "Zelda (Europe) (En,Fr,De,Es,It).gba" → [region:eu, lang:en, lang:fr, lang:de, lang:es, lang:it] "X-Men Vs. Street Fighter (Europe 961004).mra" → [region:eu, builddate:1996-10-04] ``` --- ## Matching strategies Resolution tries strategies in order, each more lenient than the last, until something matches. `pkg/zapscript/titles/resolve.go` ``` 1. Cache lookup ↓ miss 2. Exact slug match (with tag filters) ↓ no results or low confidence 3. Exact slug match (without tags) ↓ no results 4. Secondary title match ↓ no results 5. Fuzzy matching ↓ no results 6. Main title only ↓ no results 7. Progressive trim ``` **Cache** — keyed by SystemID + Slug + Tags. Returns immediately on hit. **Exact match with tags** — direct slug lookup with tags as filters. Early exit if confidence >= 0.95. **Exact match without tags** — same lookup, tags ignored. Tags become soft preferences during result selection. **Secondary title** — handles mismatched subtitles. Bidirectional: "Zelda: Ocarina of Time" matches "Ocarina of Time" and vice versa. **Fuzzy matching** — pre-filter (±2 chars length diff), then: token signature (word-order-independent), Jaro-Winkler (typo tolerance, 0.85+ similarity), Damerau-Levenshtein for tie-breaking top candidates. **Main title only** — uses just the part before the first delimiter. "Zelda: Ocarina" matches "Zelda" and "Zelda: Ocarina of Time". **Progressive trim** — removes words from the end of the query, max 3 iterations. "Legend of Zelda Link's Awakening DX" tries progressively shorter slugs. ### Tags in queries Tags come from three sources (highest priority wins): 1. **Advanced args**: `NES/Zelda?tags=region:us,-unfinished:beta` — explicit, overrides everything 2. **Inline canonical**: `NES/Zelda (+region:us) (-unfinished:beta)` — `+` for AND, `-` for NOT 3. **Filename-style**: `NES/Zelda (USA) (1986)` — auto-extracted, lowest priority ### Result selection When multiple results match, they get filtered and scored: **Confidence** is a base score from the strategy (0.85–1.0), adjusted by tag matching: - >= 0.95: launch immediately - >= 0.70: launch with info - >= 0.60: launch with warning - < 0.60: error **Filtering** (in order): 1. User-specified tag filters 2. Exclude variants: unfinished (demo, beta, proto), unlicensed (hack, translation, bootleg), bad dumps 3. Exclude re-releases 4. Preferred regions from user config 5. Preferred languages from user config 6. File type priority based on launcher extension order 7. Quality tie-breaking: numeric suffix penalty, path depth, character density, filename length --- ## Scan Behavior This is the expected behavior of scanning tokens. --- ## Default Behavior: `readers.scan.mode='tap'` When `readers.scan.mode='tap'` (default setting) and a game is launched using a card: - [ ] Removing the card from the reader won't close the game. - [ ] Leaving the card on the reader will have no effect. - [ ] Tapping another card to launch a game will start the new game without returning to the core menu. - [ ] Tapping the same card will reload the game from the beginning. - [ ] Tapping a command like `input.coin` will execute the command without interrupting the game. - [ ] Exiting the game manually through the internal menu will reset the state, allowing you to tap any card to launch a different game. - [ ] Exiting the game manually while the card remains on the reader will not cause the game to relaunch once in the menu. --- ## Behavior: `readers.scan.mode='hold'` and `readers.scan.exit_delay=0.0` When `readers.scan.mode='hold'` with `readers.scan.exit_delay=0.0` and a game is launched using a card: - [ ] Removing the card from the reader will immediately close the game. - [ ] Exiting the game manually while the card is still on the reader will not cause the game to relaunch when returning to the menu. - [ ] Exiting the game manually via the internal menu and then removing the card won't trigger a core menu reload. --- ## Behavior: `readers.scan.mode='hold'` and `readers.scan.exit_delay=N` When `readers.scan.mode='hold'` with `readers.scan.exit_delay=N` and a game is launched using a card: - [ ] Removing the card from the reader will close the game after **N seconds**. - [ ] Removing the card and reinserting it before the N-second countdown ends will not interrupt the ongoing game. - [ ] Removing the card and tapping a different game card will immediately launch the other game. - [ ] Removing the card and tapping a command card will execute the command and reset the countdown timer. You can repeatedly tap various command cards without changing the game, resetting the timer each time, and then reinsert the original game card to continue the session. - [ ] Exiting the game manually while the card is still on the reader will not cause the game to relaunch when returning to the menu. - [ ] Exiting the game manually via the internal menu and then removing the card won't trigger a core menu reload. - [ ] Exiting the game manually during the N-second countdown cancels the countdown and returns to the menu. --- ## Scraper Subsystem The scraper subsystem enriches existing MediaDB records with metadata from external sources. The filesystem scanner owns record creation; scrapers update records that already exist. Current scraper implementations: - `gamelist.xml` imports EmulationStation metadata such as developer, publisher, genre, rating, player count, descriptions, artwork paths, videos, manuals, and ScreenScraper game IDs. - `media-folder` imports image paths from EmulationStation-style `media/` folders under each system folder. It does not read `gamelist.xml`, download assets, or write non-image metadata. A force run (re-scrape) also deletes stale image properties whose paths match the same local media-folder convention and whose replacement file is no longer found. ## Code Layout | Path | Purpose | |---|---| | `pkg/database/scraper/` | Shared scrape types (`ScrapeOptions`, `ScrapeUpdate`), sentinel helper, and small channel startup helper | | `pkg/database/scraper/gamelistxml/` | EmulationStation `gamelist.xml` scraper loop, matcher, mapper, and companion-entry handling | | `pkg/database/scraper/localmedia/` | EmulationStation `media/` folder image-path importer | | `pkg/platforms/shared/esmedia/` | Shared EmulationStation media-folder path resolver | | `pkg/platforms/*` | Platform scraper registration through `Platform.Scrapers` | | `pkg/database/mediadb/sql_scraper.go` | MediaDB scraper read/write helpers, property/blob helpers, and metadata graph queries | | `pkg/api/methods/media_scrape.go` | JSON-RPC scrape start/status/cancel/resume handlers and scraper listing | | `pkg/api/methods/media_meta.go` | JSON-RPC metadata graph lookup for media rows | | `pkg/api/methods/media_image.go` | JSON-RPC image lookup from scraped properties | ## Registration And API Lifecycle Platforms expose available scrapers with: ```go Scrapers(*config.Instance) map[string]platforms.Scraper ``` `platforms.Scraper` carries `ID`, `Name`, `SupportedSystemIDs`, optional `CustomOpts`, and a `Scrape` callback. The callback receives context, config, platform, filesystem, database, shared scrape options, custom options, and an update channel. `media.scrape` looks up the requested `scraperId` from `env.Platform.Scrapers(env.Config)`, rejects the request if media indexing or another scrape is active, creates an app-scoped cancelable context, starts the scraper in the background, tracks it as a MediaDB background operation, and publishes `media.scraping` notifications. `media.scrape.status` returns the latest in-memory status snapshot plus a fresh scraped-count query. `media.scrape.cancel` cancels the active scrape context. `media.scrape.resume` resumes the shared scrape pauser. Scraping and indexing are mutually exclusive. ## Run Loop There is no generic source-record scrape loop. `pkg/database/scraper/run.go` only provides a small helper for wrapping callback/channel startup. The `gamelist.xml` implementation owns its loop in `GamelistXMLScraper.scrapeLoop`. For each system, the normal loop: 1. Resolves target systems from indexed MediaDB systems and platform launcher paths. 2. Runs ZaparooCompanion processing first. This is a special path; see [ZaparooCompanion Entries](#zaparoocompanion-entries). 3. Loads eligible indexed titles for slug matching (`force=true` loads all titles; otherwise titles without the scraper sentinel are loaded). 4. Loads indexed media rows for the system. 5. With `force=false`, removes media rows that already have sentinel tag `scraper.gamelist.xml:scraped` from path fallback candidates. 6. Loads `gamelist.xml` from each ROM root. 7. Resolves each `` path under its ROM root. 8. Computes the same display-name slug used by the original scraper and prefers that title match. 9. Uses the resolved path to select the concrete Media row for the slug-matched title when possible; otherwise falls back to the first Media row for that title. 10. If slug matching fails, falls back to case-insensitive path matching so otherwise missed records can still scrape. 11. Maps XML fields to media-level tags/properties plus title-level shared tags/properties. 12. Writes metadata through `MediaDB.ApplyScrapeResult`. 13. Writes the scraper sentinel tag to the selected Media row last inside the same transaction. 14. Emits progress updates and a final done update. The sentinel tag format is `scraper.:scraped`, for example `scraper.gamelist.xml:scraped`. Writing it last is intentional: if a normal record write fails, the transaction rolls back and the missing sentinel leaves that media row eligible for retry. Force scrapes also persist a run ID and write `scraper-run.:` to each media row completed in that operation. If Core restarts mid-force-scrape, resume reuses that run ID and skips rows already marked for the same run while still refreshing older rows that only had the normal sentinel. Run markers are removed when the operation reaches a terminal state. Per-record write failures are non-fatal: they increment `Skipped`, emit `Err`, and continue. Fatal setup/load/database errors end the run with a terminal update unless caused by context cancellation. ## Tags And Properties The DB supports tags/properties at both media and title scope. Normal `gamelist.xml` scraping writes per-ROM `region`/`lang` tags and shared title metadata. | Storage | Scope | Current normal `gamelist.xml` use | |---|---|---| | `MediaTags` | ROM-level variant metadata | region, lang, scraper sentinel | | `MediaTitleTags` | Title-level shared metadata | developer, publisher, year, rating, genre, players, arcadeboard, gamefamily | | `MediaTitleProperties` | Title-level shared static content | description, XML game ID | | `MediaProperties` | ROM-level static content | artwork paths, video path, manual path for normal `gamelist.xml` entries | Tag exclusivity is controlled by `TagTypes.IsExclusive`. Exclusive types replace existing values for that type; additive types accumulate distinct values. The scraper write path groups tags by type and applies that behavior in `upsertTags`. Property rows are keyed by entity and property type tag. Re-scraping the same property type updates the row in place and preserves row DBID. Path-backed properties persist their text path and optional `BlobDBID`; the property tables do not persist the `ContentType` computed by the mapper for path values. Blob-backed properties expose content type from `MediaBlobs`. API responses infer path-backed content type and extension from the stored path when DB content type is empty. Normal gamelist artwork is media-level so regional or language variants can carry different image paths while sharing one title. `media.image` checks media-level properties before title-level properties. If an older scrape left title-level artwork behind, run a force scrape to refresh media-level artwork; no migration removes old title properties. ## Media-level Sentinel Invariant Normal `gamelist.xml` entries prefer slug/title matching, then use path matching to select the concrete Media row when possible. If no slug match exists, path-only fallback can still select a Media row. The sentinel is written to the same Media row that receives ROM-level tags such as `region` and `lang` plus ROM-level file properties such as artwork. Title metadata remains shared by `MediaTitleDBID`, so multiple ROM variants can write the same title-level tags/properties. Rewrites are idempotent: exclusive title tags replace same-type values, additive tags are inserted-or-ignored, and properties upsert by type. Media-level properties upsert per concrete Media row, preventing regional artwork variants from overwriting each other. ## gamelist.xml Behavior `GamelistXMLScraper` scans each system ROM root for `gamelist.xml`. Regular `` entries are resolved to absolute paths under the system ROM root. The scraper first matches the entry to an existing title by the original display-name slug behavior, then uses the resolved path to choose the concrete Media row for that title when possible. If slug matching finds a title but the path does not identify a Media row for that title, only title-level metadata is written. If no known title slug exists, it falls back to case-insensitive path matching. Scrapers do not create `Media` or `MediaTitle` rows. Path handling for `` stays strict: | Input | Behavior | |---|---| | `./relative` or `relative` | Resolved under the system ROM root and rejected if it escapes that root | | `~/...` | Resolved under the current user's home directory, then rejected unless still under the system ROM root | | Absolute path | Cleaned and rejected unless under the system ROM root | Asset path handling for artwork/video/manual uses the same root-bound behavior by default. On MiSTer and MiSTeX only, absolute or `~/...` asset paths may also resolve under platform root directories from `RootDirs(cfg)`, covering SD, USB, CIFS, network, and configured index roots. This applies only to file-backed asset fields; game paths remain bound to the ROM root. Path traversal outside the ROM root or approved platform roots is rejected. Zip-as-directory paths are supported for matching XML entries such as `./Japan/Game.zip` to indexed media stored under that zip path, while nested artwork paths such as `./media/images/Japan/Game.png` remain resolved as asset paths. Source fields are cleaned before mapping: HTML entities are unescaped, tab/newline/carriage-return characters become spaces, and surrounding whitespace is trimmed. ### Field Mapping | ES field | Destination | Notes | |---|---|---| | `lang` | `MediaTags: lang` | CSV split, trimmed, lowercased, additive | | `region` | `MediaTags: region` | CSV split, trimmed, lowercased, additive | | `developer` | `MediaTitleTags: developer` | Exclusive | | `publisher` | `MediaTitleTags: publisher` | Exclusive | | `releasedate` | `MediaTitleTags: year` | First four characters when present | | `rating` | `MediaTitleTags: rating` | Normalized from `0..1` style ratings to `0..100` text | | `genre` | `MediaTitleTags: genre` | Additive | | `players` | `MediaTitleTags: players` | Highest player count from ranges/lists | | `arcadesystemname` | `MediaTitleTags: arcadeboard` | Exclusive | | `family` | `MediaTitleTags: gamefamily` | Additive | | `desc` | `MediaTitleProperties: property:description` | Plain text | | ScreenScraper game ID | `MediaTitleProperties: property:xml-game-id` | From XML attribute or element value | | `image` | `MediaProperties: property:image-image` | XML path or filesystem fallback | | `thumbnail` | `MediaProperties: property:image-thumbnail` | Cover/thumbnail path in most ES forks | | `boxart2d` | `MediaProperties: property:image-boxart` | XML path or filesystem fallback | | `boxart3d` | `MediaProperties: property:image-boxart3d` | XML path or filesystem fallback | | `screenshot` | `MediaProperties: property:image-screenshot` | XML path or filesystem fallback | | `video` | `MediaProperties: property:video` | Filesystem path | | `marquee` | `MediaProperties: property:image-marquee` | XML path or filesystem fallback | | `logo` / `wheel` | `MediaProperties: property:image-wheel` | `logo` takes priority over `wheel`; XML path or filesystem fallback | | `fanart` | `MediaProperties: property:image-fanart` | XML path or filesystem fallback | | `titlescreen` / `titleshot` | `MediaProperties: property:image-titleshot` | `titlescreen` takes priority over `titleshot`; XML path or filesystem fallback | | `map` | `MediaProperties: property:image-map` | XML path or filesystem fallback | | `manual` | `MediaProperties: property:manual` | PDF path | Filesystem fallback searches known subdirectories under `/media/` when an XML path is absent. For games in subfolders, it searches the mirrored ROM-relative path before the flat filename; for example `./Japan/Game.nes` checks `media/images/Japan/Game.png` before `media/images/Game.png`. Side/back box art are filesystem-fallback only. Only `/gamelist.xml` files are loaded. Nested files such as `/Japan/gamelist.xml` are not read by the current scraper. For systems that index virtual or non-file-backed entries (where the stored media path does not correspond to a real file), `` must match the exact path the indexer stored for that media row. `gamelist.xml` deliberately does not scrape user-state fields such as favorite, hidden, or kidgame. It also does not overwrite filename-parser-owned fields such as disc and track. ## ZaparooCompanion Entries `gamelist.xml` has a special path for entries marked with `source="ZaparooCompanion"` as either a `source` attribute or `` element. Companion records are split into: - Parent entries: have an ID attribute and no path. They carry shared title metadata. - Child entries: have `parentid` and path. They reference parent metadata. Child matching: - Paths ending in `.slug` match an existing title by slug, then use the first Media row for that title as the write target. - Other child paths first try an exact case-insensitive media path lookup. - If exact lookup fails, the scraper falls back to filename suffix matching with `FindMediaBySystemAndPathSuffix`. - Ambiguous suffix matches are skipped instead of updating multiple same-basename media rows. For matched children, parent metadata is written onto the child title, child `region` and `lang` are written to the child Media row as media-level tags, and the scraper sentinel is written to that child Media row. These writes use `ApplyScrapeResult`, so title metadata, child tags, and the sentinel are committed together. Current caveats: - Companion processing still runs before normal title filtering. - With `force=false`, child media rows that already have the `scraper.gamelist.xml:scraped` sentinel are skipped. - Companion processed/matched/skipped counts contribute to run counters, but companion entries do not have a separate total in status updates. These caveats document current behavior, not necessarily desired long-term behavior. ## API Surface JSON-RPC methods: | Method | Purpose | |---|---| | `scrapers` | Lists registered scrapers with ID, name, and supported systems | | `media.scrape` | Starts a scraper run as a background operation | | `media.scrape.status` | Returns latest in-memory scraper status plus current DB scraped count | | `media.scrape.cancel` | Cancels the active scraper run | | `media.scrape.resume` | Resumes a paused scraper run | | `media.meta` | Returns tags and metadata-only properties for one or more media rows and their titles | | `media.image` | Returns the best matching image property as base64 data for one media row, including thumbnail art | | `media.clean.orphans` | Removes missing media rows and orphaned related data | `media.scrape` params: ```json { "scraperId": "gamelist.xml", "systems": ["snes", "nes"], "force": false } ``` Progress is queryable with `media.scrape.status` and broadcast as `media.scraping` notifications: ```json { "scraperId": "gamelist.xml", "systemId": "snes", "processed": 42, "total": 100, "matched": 38, "skipped": 4, "totalScraped": 1000, "scraping": true, "done": false, "paused": false, "state": "running", "totalSteps": 2, "currentStep": 1, "currentStepDisplay": "Super Nintendo Entertainment System", "currentSystem": { "systemId": "snes", "systemName": "Super Nintendo Entertainment System", "processed": 42, "total": 100, "matched": 38, "skipped": 4 } } ``` `totalScraped` is derived from scraper sentinel tags in the database, not from the current run's `matched` count. Existing flat fields stay for compatibility; new UIs should use `currentSystem` for current-system progress and `totalSteps`/`currentStep`/`currentStepDisplay` for whole-run system-step progress. Only one scraper can run at a time, and scraping is mutually exclusive with media indexing. `media.meta` returns the metadata graph for media rows: media-level tags and properties, title-level tags and properties, and stored system identity. Single requests accept `mediaId` or `system`/`path` and keep the single-response shape; batch requests use `items` and return per-item results. Binary property bytes are not included; clients should use `media.image` for image data. On platforms that treat zips as directories, a `system`/`path` request for a folder or zip-as-directory container resolves only when its direct contents collapse to one logical launch target. `media.image` accepts one media ref plus image type preferences such as `image`, `boxart`, `boxart3d`, `screenshot`, `wheel`, `titleshot`, `map`, `marquee`, and `fanart`. These resolve to canonical image property tags; for example `boxart` becomes `property:image-boxart` and `image` becomes `property:image-image`. Media-level properties are preferred over title-level properties for the same type. On zip-as-directory platforms, logical container aliases are checked as media-level fallbacks, so artwork attached to a direct single-game target or its container can be found from either path. For stale image properties in these canonical tags, such as missing file paths for `property:image-boxart` or `property:image-image`, `media.image` logs the stale property in memory only and does not delete DB rows; lookup falls through to the next available source. ## Useful Focused Tests ```bash go test ./pkg/database/scraper/... go test ./pkg/database/mediadb/ -run 'Scrape|Property|Blob|Sentinel|MediaImage' go test ./pkg/api/methods/ -run 'Scrape|MediaImage|MediaMeta' ``` --- ## Zaparoo Core Zaparoo Core is the service that runs on your MiSTer, PC, handheld, or media device. It sits in the background, watches for scans from [readers](../readers/index.md), reads the [ZapScript](../zapscript/index.md) from matching [tokens](../tokens/index.md), and launches the right game, app, script, or media file for that platform. Core is the part of Zaparoo you install on the device that has access to your media library and launchers. The [Zaparoo App](../app/index.md), [Web UI](../app/web.md), command line tools, readers, and custom integrations all connect to or are managed by Core. ## What Core does Core coordinates the main Zaparoo system: - Manages reader connections for NFC readers, barcode scanners, optical drives, MQTT, file readers, and other supported reader types. - Reads token data from NFC tags, NFC toys, QR codes, barcodes, optical discs, and mapped sources. - Applies mappings and runs ZapScript for launches, input actions, playlists, and other commands. - Builds and searches the local media database so scans can match games, apps, videos, scripts, and other launchable files. - Starts, monitors, and controls media through built-in or custom [launchers](../features/launchers.md). - Publishes the [Core API](./api/index.md), a local JSON-RPC API used by the app, web UI, CLI, and integrations. ## Installing and controlling Core Start with the [platform guides](../platforms/index.mdx) for installation steps. Each platform has its own paths, startup behavior, launchers, and reader notes. After Core is installed, you can control it through: - The [Zaparoo App](../app/index.md) on iOS or Android. - The embedded [Web UI](../app/web.md) at `http://:7497/app/`. - The [terminal UI](./tui.md), when available for your platform. - The [command line](./cli.md), when supported by your platform. - The [Core API](./api/index.md) for custom tools and integrations. Most Core settings live in `config.toml`. See the [configuration reference](./config.md) for file locations, option names, and examples. ## Features and customization Core includes user-configurable features for larger setups and custom workflows. The [features section](../features/index.md) covers mappings, playlists, play controls, hooks, publishers, launcher controls, custom launchers, systems, and tags. If you are setting up hardware, start with the [readers](../readers/index.md) and [tokens](../tokens/index.md) sections first. If you already have a reader working and want to control what a scan does, start with [ZapScript](../zapscript/index.md). ## Source and license Zaparoo Core is free and open source under the GPLv3 license. The source code and releases are hosted in the [Zaparoo Core GitHub repository](https://github.com/ZaparooProject/zaparoo-core/). --- ## Terminal UI The Zaparoo Core terminal UI, or TUI, is Core's local menu interface. Use it when you are on the device running Core and want to check status, write a token, manage media, or change settings without opening the [Zaparoo App](../app/index.md), [Web UI](../app/web.md), or [command line](cli.md). Windows does not use the TUI. It runs Core from the system tray instead. ## Opening the TUI Start Core from the launcher, script, or executable documented for your platform. For example, MiSTer users open Zaparoo from the Scripts menu, and Batocera users open it from the Ports system in EmulationStation. Use the relevant platform guide for install paths and startup behavior: - [MiSTer FPGA](../platforms/mister/index.md) - [Batocera](../platforms/batocera/index.md) - [Linux](../platforms/linux/index.md) - [RePlayOS](../platforms/replayos.md) Service or daemon modes skip the TUI. See the [command line reference](cli.md) for platform flags. ## Main screen The main screen shows the local Core service status, device IP address, Web UI address, connected reader status, and the most recent scanned token. If the Core service is not running, most actions are disabled and the TUI tells you to check the logs. Exiting the TUI may leave the Core service running in the background, depending on how Core was started on that platform. ## Main actions | Action | What it does | | ------ | ------------ | | **Search media** | Searches indexed media and writes the selected result to a token. | | **Custom write** | Writes custom [ZapScript](../zapscript/index.md) to a token. | | **Manage media** | Updates the media index and runs metadata scraping. | | **Settings** | Opens Core settings, TUI settings, reader settings, audio settings, and logs. | | **Exit** | Closes the TUI. | ## Managing media Open **Manage media** to update the media index, scrape metadata, or resume/cancel media jobs. ## Writing tokens The TUI can write tokens from **Search media** or **Custom write**. Search results use the TUI `write_format` setting: - `zapscript` writes the ZapScript generated by Core. This is the default. - `path` writes the media path directly. Custom writes parse the ZapScript before writing and show an error for unknown command names. ## TUI settings TUI-specific settings live in `tui.toml` next to `config.toml`. You can change them from **Settings** > **TUI**, or edit the file manually. See the [TUI config reference](config.md#tui-config-file) for the full list. The TUI settings menu includes: - `theme` - `write_format` - `mouse` - `crt_mode` - `on_screen_keyboard` `crt_mode` and `on_screen_keyboard` default to `true` on MiSTer and MiSTeX, and `false` elsewhere. ## Logs Open **Settings** > **Logs** to view recent Core log output, refresh the view, or upload the log for support. Some platforms also show a **Copy** action that copies the log to a platform-specific location. --- ## Zaparoo Designer Zaparoo Designer is a web app for creating custom [labels](../labels/index.md) for your [Zaparoo NFC cards](../tokens/nfc/index.md). Use it to build one label or a full set from SVG templates, then export files for printing at home, through a print shop, or with a plotter cutter. ## Features - No login or account required - Local image files stay in your browser; built-in artwork search uses external services - Create custom labels from SVG-based templates - Choose from a variety of community-contributed templates with different layouts and styles - Customize colors, layouts, and media types - Search for game artwork from [IGDB](https://igdb.com) - Export print-ready files in PDF or PNG formats with printer/plotter configuration options - Use media sizes for standard NFC cards, slim NFC cards, mini cards, Zap Trading Cards, cassette tape cases, foldable sleeves, and supported PCB card designs ## Getting started 1. Navigate to [Zaparoo Designer](https://design.zaparoo.org/) 2. Select **Add files** to upload a local image or **Search image** to find game artwork from IGDB 3. Choose a template that matches your media type (horizontal or vertical layouts available) 4. Customize colors, layout, and other template options 5. Repeat steps 2-4 for each label you want to create in this session 6. Select **Print** and configure the output that works best for your printer or plotter 7. Press **Download** to get your PDF file or ZIP of PNG files 8. Print the output files on your printer ## Template system The Designer uses SVG templates with image placeholders and media definitions: - Community templates can include their own license information - Templates can use horizontal or vertical layouts - Image placeholders control where artwork is placed and whether it fits or covers the placeholder area - Media definitions set the target size, such as a standard NFC card, mini card, cassette tape case, or foldable sleeve ## Contributing templates Interested in creating your own templates? Templates use SVG format with special `zaparoo-placeholder` attributes for image positioning. Contributed templates must include license information and cannot contain copyrighted material or infringe trademarks. See the [template creation guide](https://github.com/ZaparooProject/zaparoo-designer/blob/main/TEMPLATES.md) in the repository for detailed instructions. Once you have your designs ready, see the [printing guide](../labels/printing-guide.md) for tips on printing and applying your labels to NFC cards and [PCB cards](../tokens/pcb-cards). --- ## Audio Playback Zaparoo Core can play audio files on their own, without an emulator or separate media player. This is handled by a built-in native audio launcher that plays files through the same audio output Core uses for [scan feedback sounds](../core/config.md#audio). Use it to play a single track, a folder of music as a [playlist](./playlists.md), or background music that keeps playing while you launch and play games. ## Supported files The native audio launcher handles the `Audio` [system](./systems.md) and plays these formats: - `.wav` - `.mp3` - `.ogg` - `.flac` Audio files indexed under an `Audio` or `Music` folder are launched through it automatically. You can also launch any audio file directly by its path: ```zapscript **launch:/media/fat/music/chiptunes/intro.mp3 ``` ## Media slots Playback is split into two slots: | Slot | Use | | ---- | --- | | `primary` | The default slot. Games and other media launch here. | | `background` | A separate slot for audio that plays alongside whatever is in the primary slot. | Commands act on the primary slot unless you set a slot. Add `?slot=background` (or the short form `?slot=bg`) to target the background slot instead. The background slot is what makes background music possible: a game runs in the primary slot while music plays in the background slot at the same time. ## Background music Start background music from a single track: ```zapscript **launch:/media/fat/music/album/track01.flac?slot=background ``` Or play a whole folder as a background [playlist](./playlists.md): ```zapscript **playlist.play:/media/fat/music/album?slot=background ``` While background music plays you can keep scanning game tokens as usual. They launch in the primary slot without clearing the background slot, so the music stays loaded. ### Pause on launch By default, background music automatically pauses when a game launches in the primary slot and resumes when the game quits. This keeps the music from competing with a game's own audio. To keep background music playing through game launches, turn this off for the `Audio` system in the [config file](../core/config.md#systemsdefault): ```toml [[systems.default]] system = "Audio" pause_on_launch = false ``` If you manually start background music while a game is already running, Core treats that as intentional and does not pause it when the game quits. ### Looping Background playlists can repeat. Add a [`repeat`](../zapscript/playlist.md#repeat-modes) argument when you load the playlist: ```zapscript **playlist.play:/media/fat/music/album?slot=background&repeat=all ``` Use `repeat=all` to loop the whole playlist or `repeat=one` to loop the current track. The default is `repeat=off`. ## Controlling playback Use the [`control`](../zapscript/utilities.md#control) command, or the [`media.control`](../core/api/methods.md#mediacontrol) API method, to control audio. The native audio launcher supports: | Action | Effect | | ------ | ------ | | `toggle_pause` | Pause or resume | | `pause` | Pause | | `resume` | Resume | | `stop` | Stop playback | | `fast_forward` | Seek forward | | `rewind` | Seek backward | To control background audio, add the slot: ```zapscript **control:toggle_pause?slot=background ``` `fast_forward` and `rewind` seek by 10 seconds by default. Set a custom amount with the `seconds` advanced argument: ```zapscript **control:fast_forward?slot=background&seconds=30 ``` ## Volume Native audio playback uses the same volume setting as scan feedback sounds, set with [`volume`](../core/config.md#volume) in the config file. --- ## Hooks Hooks run [ZapScript](../zapscript/index.md) at specific points in Core's lifecycle. Use them for local automation, webhooks, save workflows, startup actions, lighting, buttons, or scripts that need context from Core. Hooks are configured in `config.toml`. Manual config edits require a Core restart or config reload where supported. ## Hook reference | Hook | Config location | When it runs | Can block? | Extra context | | --- | --- | --- | --- | --- | | `on_scan` | `[readers.scan]` | After a token is scanned, before the token or mapping script runs | Yes, blocks token processing | `scanned` | | `on_remove` | `[readers.scan]` | After a token is removed in hold mode | Yes, blocks remove processing and keeps media running | none | | `before_exit` | `[[systems.default]]` | Before hold mode exits media for the matching system | Waits for script; errors are logged | none | | `before_media_start` | `[launchers]` | Before a media-launching command runs | Yes, blocks launch | `launching` | | `on_media_start` | `[launchers]` | After active media is set | No, errors are logged | active media | | `on_boot` | `[service]` | First Core start after the device boots | No, errors are logged | `hook` | | `on_ready` | `[service]` | Every time Core starts and is ready | No, errors are logged | `hook` | ## Reader hooks Reader hooks live under `[readers.scan]`. ```toml [readers.scan] on_scan = "**http.post:https://example.com/scan,text/plain,Token scanned" on_remove = "**echo:card was removed" ``` `on_scan` runs before the scanned token is processed. If the hook fails, the scan is blocked. `on_remove` only runs in hold mode. If the hook fails, remove processing is blocked, so media keeps running. ## Media launch hooks Media launch hooks live under `[launchers]`. ```toml [launchers] before_media_start = "**http.get:https://example.com/before-launch" on_media_start = "**http.get:https://example.com/media-started" ``` `before_media_start` runs before launch commands such as `**launch`, `**launch.random`, `**launch.title`, and MiSTer MGL launches. If it fails, the launch is blocked. `on_media_start` runs after Core starts media. It is useful for notifications or follow-up actions. If it fails, the launched media keeps running. ## Exit hooks Use `before_exit` on a system default when hold mode should run a script before exiting media. ```toml [[systems.default]] system = "SNES" before_exit = "**input.keyboard:{f12}||**delay:2000" ``` This can be useful for opening an emulator menu, saving, or giving the platform time to settle before hold mode exits media. ## Service startup hooks Service hooks live under `[service]`. ```toml [service] on_boot = "**execute:/media/fat/zaparoo/scripts/on-boot.sh" on_ready = "**execute:/media/fat/zaparoo/scripts/on-ready.sh" ``` `on_boot` runs once after the device boots. If Core restarts without rebooting the device, `on_boot` is skipped. `on_ready` runs every time Core starts and is ready. ## Expression environment Hook scripts can use the normal [ZapScript expression environment](../zapscript/syntax.md#expression-environment). Hook-related fields include: - `hook.name`: current hook name, such as `on_boot` or `on_ready`. - `hook.first_boot_start`: `true` when this Core start is the first start for the current OS boot. - `scanned`: available in `on_scan`. - `launching`: available in `before_media_start`. The normal environment also includes `media_ready`, which is `true` when active media is considered ready. Example: ```toml [service] on_ready = "**echo:Hook [[hook.name]] started, first boot: [[hook.first_boot_start]]" ``` ## External scripts The `**execute` command receives a `ZAPAROO_ENVIRONMENT` environment variable containing the expression environment as JSON. ```toml [zapscript] allow_execute = ["/media/fat/zaparoo/scripts/.*"] [service] on_ready = "**execute:/media/fat/zaparoo/scripts/on-ready.sh" ``` `execute` still requires a matching [`allow_execute`](../core/config.md#allow_execute) entry. Input commands still follow the configured [`[zapscript.input]`](../core/config.md#zapscriptinput) rules. ## Examples Send a webhook when a token is scanned: ```toml [readers.scan] on_scan = "**http.post:https://hooks.example.com/zaparoo,text/plain,[[scanned.id]]" ``` Block launches when an external script returns an error: ```toml [launchers] before_media_start = "**execute:/media/fat/zaparoo/scripts/check-launch.sh" ``` --- ## Features Zaparoo Core includes several features that expand what you can do with your tokens and media library. These are user-configurable capabilities that you can enable and customize to suit your setup. - [Launchers](./launchers.md) - define custom launchers and send launcher control actions to active media - [Hooks](./hooks.md) - run ZapScript when tokens are scanned, media starts, media exits, or Core becomes ready - [Mappings](./mappings.md) - assign ZapScript to tokens that can't be written to directly, like Amiibos and barcodes - [Play Controls](./play-controls.md) - stage disruptive scans and enforce playtime limits - [Playlists](./playlists.md) - group media or ZapScript actions and move through them with playlist commands - [Audio Playback](./audio.md) - play music and audio files through Core, including background music while you game - [Publishers](./publishers.md) - broadcast Core events to MQTT brokers or PixelCade LED displays - [Systems](./systems.md) - reference of all system IDs used in configuration and ZapScript commands - [Tags](./tags.md) - metadata labels extracted from filenames used to filter and match games - [Scraping](./scraping.md) - import local metadata and artwork into your media library with the gamelist.xml and media-folder scrapers --- ## Launchers A launcher tells Zaparoo Core how to open a game, video, app, or other media file. Each [platform](../platforms/index.mdx) has its own launchers for matching a [system](./systems.md) and file to the right program. ## Launchables Some things can be launched but don't map cleanly to a file on disk or a normal [system](./systems.md), like a ROM-less FPGA core. Launchables are how Core handles these. Core defines them in code and exposes them as virtual versions of the things you already use: a virtual system or a virtual media entry, whichever fits. You don't deal with launchables as a separate concept. They show up in the [Zaparoo App](../app/index.md) as ordinary systems or media that you can browse, pick, and write to a token like anything else. The only difference is that there is no real file or system behind them. MiSTer's ROM-less [Other cores](../platforms/mister/launchers.md#other-cores), like Chess or Flappy Bird, are the first example. They have no game file and no system of their own, so Core exposes each one as a virtual system and indexes the ones you have installed. The one place this shows through is the token value. A launchable is identified by a compact `zaparoo://` URI instead of a file path, so a token that launches one holds a value like `zaparoo://gezdgnbvgy3tqojqgezdgnbvgy` rather than a normal path. You don't write this by hand; the App fills it in when you save a launchable. ## Launcher controls Launcher controls send actions to the launcher handling the currently active media. Use them for actions like pause, stop, save state, load state, fast forward, rewind, or next and previous track. Control support depends on the active launcher. If no media is active, or the launcher does not support the requested action, the command returns an error. :::note Control support varies Each launcher supports its own set of actions. Check the active media response to see which controls are available. ::: ### How control actions work Zaparoo Core asks the active launcher to run a named control action. Official launchers can implement controls directly. [Custom launchers](#controls) can define controls as ZapScript snippets in their launcher configuration. When Core can tell that media is still starting, the `control` command waits before sending the action. Built-in action names include: | Action | Typical use | | ------ | ----------- | | `toggle_pause` | Pause or unpause active media | | `pause` | Pause active media | | `resume` | Resume active media | | `save_state` | Save emulator state | | `load_state` | Load emulator state | | `save_ram` | Save RAM data | | `toggle_menu` | Open or close an in-game menu | | `reset` | Reset active media | | `stop` | Stop active media | | `fast_forward` | Fast forward | | `rewind` | Rewind | | `next` | Move to the next item | | `previous` | Move to the previous item | Not every launcher supports every action. Use the [`media`](../core/api/methods.md#media) or [`media.active`](../core/api/methods.md#mediaactive) API response to check the `launcherControls` available for the current media. ### Current support Built-in launcher support currently includes: | Launcher | Supported actions | | -------- | ----------------- | | Kodi launchers: `KodiLocalVideo`, `KodiMovie`, `KodiTVEpisode`, `KodiLocalAudio`, `KodiAlbum`, `KodiArtist`, `KodiTVShow`, `KodiSong` | `toggle_pause`, `stop`, `fast_forward`, `rewind`, `next`, `previous` | | EmuDeck RetroArch-based launchers on SteamOS | `save_state`, `load_state`, `toggle_menu`, `toggle_pause`, `reset`, `fast_forward`, `stop` | | Native audio launcher (`Audio` system, see [Audio Playback](./audio.md)) | `toggle_pause`, `pause`, `resume`, `stop`, `fast_forward`, `rewind` | | Custom launchers | Whatever is defined in the launcher's `controls` table | EmuDeck standalone emulator launchers and RetroDECK launchers do not currently define built-in launcher controls. To control [background audio](./audio.md) instead of the active game, add `?slot=background` to the control action. ### ZapScript control Use the `control` command to send a control action from a token: ```zapscript **control:toggle_pause ``` Save state for the active media, if the launcher supports it: ```zapscript **control:save_state ``` See the [ZapScript utility command reference](../zapscript/utilities.md#control) for exact syntax and examples. ### API control Apps and integrations can use the [`media.control`](../core/api/methods.md#mediacontrol) API method to send launcher control actions. The active media response includes `launcherControls` when controls are available. ## Custom launchers Custom launchers are user-defined launchers configured with [TOML](https://toml.io/) files, similar to [mapping files](./mappings.md#mapping-files). Use them when your [platform](../platforms/index.mdx) does not include a launcher for an emulator or media app you want to use. Custom launchers are less configurable than official launchers. They work best when the app can launch media from a file path or URL. :::note Media tracking limitations Custom launcher tracking depends on how the launcher starts the app: - With the default `blocking` lifecycle, Core tracks the direct process started by the launcher - With `background`, Core starts the command and does not track the process afterward - If the command hands off to another app and exits, Core may lose track of the media even with `blocking` When Core cannot track the launched process, features that depend on active media state, such as [hold mode](../core/config.md#scan-mode) exit handling or API stop commands, may not work for that launcher. ::: ### Creating a custom launcher To start, open the `launchers` directory in the Core data folder. Check the page for your [platform](../platforms/index.mdx) if you're not sure where that folder is. Create a new file ending in `.toml`. This example uses `OpenEmuGB.toml`. The filename is not important, but launcher IDs are, so use a unique ID unless you have a specific reason not to. Open the file and add the launcher definition: ```toml [[launchers.custom]] id = "OpenEmuGB" system = "Gameboy" media_dirs = ["/Volumes/games/Gameboy"] file_exts = [".gb"] execute = "osascript -e 'tell application \"OpenEmu\" to open POSIX file \"[[media_path]]\"'" ``` The first line, `[[launchers.custom]]`, tells Core this is a custom launcher definition. It's required. Make sure to include the double square brackets. The `id` line defines the internal ID of the launcher. Generally this won't matter, but you can reference it with the `?launcher=` advanced argument in [ZapScript](../zapscript/index.md). The `system` line specifies which system this launcher will belong to. The `media_dirs` line specifies a list of directories where Core should search for media. Relative paths resolve from the Core executable directory. This definition is also used to match a file to the launcher during scans. The `file_exts` line is a list of file extensions which will match to this launcher. Extensions are automatically normalized - you can write `".gb"` or `"gb"` and both will work (a `.` prefix is added if missing and extensions are converted to lowercase). The `execute` line is the command which will be run when a token is scanned which matches to this launcher. Expression variables are replaced by their values, then the resulting string is split into a program and arguments (respecting double and single quoted strings) and executed directly without a shell interpreter. If you need shell features like pipes or redirection, create a wrapper script and reference it here instead. :::tip Avoid nesting another shell Core runs your command directly, without a shell. If your `execute` command starts PowerShell or `cmd`, that shell parses your arguments again and can split quoted paths on their spaces. Run the target program directly when you can. See [Windows custom launchers](../platforms/windows/launchers.md#quoting-paths-and-powershell) for a worked example. ::: #### Available expression variables You can use the following variables in your execute command: | Variable | Description | |----------|-------------| | `[[media_path]]` | Resolved absolute path to the media file | | `[[platform]]` | Platform ID (e.g., "linux", "windows", "mister") | | `[[version]]` | Zaparoo Core version | | `[[device.hostname]]` | Device hostname | | `[[device.os]]` | Operating system ("linux", "windows", "darwin") | | `[[device.arch]]` | System architecture ("amd64", "arm64", etc.) | | `[[action]]` | Launch action from [launcher defaults](../core/config.md#launchersdefault) or ZapScript advanced args | | `[[install_dir]]` | Install directory from [launcher defaults](../core/config.md#launchersdefault) | | `[[server_url]]` | Server URL from [launcher defaults](../core/config.md#launchersdefault) | | `[[system_id]]` | System ID the launcher belongs to | | `[[launcher_id]]` | The launcher's ID | #### Environment variable Custom launcher commands also receive a `ZAPAROO_ENVIRONMENT` environment variable containing a JSON object with the same data as the expression variables. This allows shell scripts to access the full context. Save the file, restart Zaparoo Core, and run a media database update. Core should detect matching media for the launcher and make it launchable from scans. ### Advanced options #### groups Associate the launcher with one or more groups. Groups let you set defaults for multiple launchers at once using `[[launchers.default]]` in `config.toml`. ```toml [[launchers.custom]] id = "MyKodiVideo" system = "Video" groups = ["Kodi"] # ... ``` ```toml title="config.toml" [[launchers.default]] launcher = "Kodi" server_url = "http://kodi:8080" ``` The defaults lookup checks both the launcher ID and any groups it belongs to. #### schemes Define URL schemes the launcher handles, without the trailing `://`. This allows the launcher to be matched when launching URLs with that scheme. ```toml [[launchers.custom]] id = "MyPlayer" schemes = ["myplayer"] # ... ``` #### lifecycle Control how the launcher process is managed: - `"blocking"` (default) - Core waits for the process and tracks it - `"background"` - Fire and forget, Core doesn't track the process ```toml [[launchers.custom]] id = "BackgroundPlayer" lifecycle = "background" # ... ``` #### controls Define control actions that can be triggered on active media via [launcher controls](#launcher-controls). Values are [ZapScript](../zapscript/index.md) strings that run in a restricted control runtime. Media-launching, playlist, and nested `control` commands are blocked, but utility commands like `input.keyboard`, `execute`, `delay`, and `echo` are allowed. The `execute` command in control scripts still requires a matching [`allow_execute`](../core/config.md#allow_execute) entry. ```toml [[launchers.custom]] id = "RetroArchSNES" system = "SNES" execute = "retroarch -L snes \"[[media_path]]\"" media_dirs = ["/games/snes"] file_exts = [".sfc", ".smc"] [launchers.custom.controls] save_state = "**input.keyboard:{f2}" load_state = "**input.keyboard:{f4}" toggle_menu = "**input.keyboard:{f1}" toggle_pause = "**input.keyboard:{p}" ``` Available control actions are reported in the `launcherControls` field of the active media object. Scripts are validated when the launcher loads; entries with invalid ZapScript syntax are skipped with a warning in the logs. #### restricted When set to `true`, only files matching the [`allow_file`](../core/config.md#allow_file) patterns in `config.toml` can be launched. Use this for security-sensitive launchers. ```toml [[launchers.custom]] id = "ScriptRunner" restricted = true # ... ``` ### More examples #### Windows emulator ```toml [[launchers.custom]] id = "WindowsEmulator" system = "PS2" media_dirs = ["C:/Games/PS2"] file_exts = [".iso", ".chd"] execute = "C:/Emulators/PCSX2/pcsx2.exe \"[[media_path]]\"" ``` #### Platform-aware launcher This example uses expression variables to change the command by platform: ```toml [[launchers.custom]] id = "PlatformAware" system = "Multi" media_dirs = ["games"] file_exts = [".rom"] execute = "/opt/launchers/[[platform]]/run.sh \"[[media_path]]\" --host [[device.hostname]]" ``` ## Default launchers Use [`[[systems.default]]`](../core/config.md#systemsdefault) to choose the default launcher for a system. Core applies this to title/search launches and direct path launches when it can infer the system from the path. API clients can also save a per-media launcher override through [`media.meta.update`](../core/api/methods.md#mediametaupdate). Use this when one game should always use a different launcher from the rest of its system. Explicit ZapScript `?launcher=` arguments still win for one-off launches, then Core checks the per-media override, then system defaults. Per-media overrides are stored alongside favorites in Core's user database, separate from the rebuildable media database, so they are kept even if Core has to rebuild the media database after corruption. This user data is included in Core's [database backups](../core/cli.md#back-up-and-restore-user-data). Use [`[[launchers.default]]`](../core/config.md#launchersdefault) to set launcher-specific defaults such as `action` or `load_path`. ## Troubleshooting ### Verifying your launcher loaded Check the Zaparoo Core logs when it starts up. Look for messages about custom launchers, such as: - `parsed custom launcher from TOML` - `registered custom launcher` - `loaded custom launchers` If your launcher isn't loading, check for TOML syntax errors in the logs. ### Testing commands Before adding a command to your launcher config, test it manually in your terminal or command prompt. Replace `[[media_path]]` with an actual file path to verify it works. ### Common issues - **Paths with spaces**: Quote the program path and `[[media_path]]` separately in your `execute` command, especially on Windows. If you wrap the command in another shell like PowerShell, that shell can split the path on its spaces. Launch the program directly instead when you can. See [Windows custom launchers](../platforms/windows/launchers.md#quoting-paths-and-powershell) - **Launcher selection**: If several launchers match the same file, Core prefers more specific matches. Duplicate IDs or equally specific matches can be order-dependent - **File not found**: Ensure your `media_dirs` paths are absolute or correctly relative to the Core executable directory - **Command not found**: Verify the programs you're calling in `execute` are installed and in your system's PATH --- ## Mappings Mappings let Zaparoo Core run custom [ZapScript](../zapscript/index.md) when a scanned token matches a UID, barcode, stored text, or raw data pattern. They are useful for read-only tokens, including [barcodes](../tokens/qr-codes.md) and NFC toys like [Amiibos](../tokens/nfc-toys/amiibo.md), where you cannot write ZapScript directly to the token. Core can load mappings from local TOML files or from mappings stored in its database through the API. Database mappings are checked first. If a database mapping matches, Core uses that ZapScript and does not check file mappings or legacy platform mappings. The [Zaparoo App](../app/index.md) can manage database mappings from **Create > Mappings**. Use the app or API for mappings you want to create, edit, enable, disable, and delete interactively. Use mapping files for bulk lists, hand-authored mappings, or mappings you want to keep outside the Core database. ## Mapping files Create mapping files in the `mappings` folder inside the Core data folder. Check the page for your [platform](../platforms/index.mdx) to find that folder. You can organize mappings in subfolders, and each mapping file must use the `.toml` extension. Each file can contain any number of entries. Every entry starts with `[[mappings.entry]]` and needs at least `match_pattern` and `zapscript`. ```toml [[mappings.entry]] match_pattern = '044ed8daed7281' zapscript = '**launch.random:snes' ``` This example matches the NFC tag UID `044ed8daed7281` and runs `**launch.random:snes` instead of any ZapScript stored on the token. When adding or changing mapping files, restart Core so the changes are loaded. API clients can also call `mappings.reload` when they need to reload file mappings directly. ## Matching fields Use `token_key` to choose which token field Core should match. If `token_key` is omitted, Core matches against `id`. | `token_key` | Matches | | --- | --- | | `id` | The token's identifier, such as an NFC UID or barcode contents. | | `value` | The text stored on the token. | | `data` | The raw token data as a hexadecimal string. | For API-managed mappings, the stored type names are also `id`, `value`, and `data`. The API still accepts the older `uid` and `text` names for compatibility, and API responses may return those older names. ## Match patterns The `match_pattern` value controls how the selected token field is matched. | Pattern format | Behavior | Example | | --- | --- | --- | | Plain text | Exact match. For `id` matches, Core normalizes both values by trimming outer whitespace, converting to lowercase, and removing colons. | `044ed8daed7281` | | Contains `*` | Partial match. Core removes the `*` characters and checks whether the remaining text appears anywhere in the field. | `978*` matches any ID containing `978`. | | Wrapped in `/` | Regular expression. Core removes the surrounding slashes before compiling the expression. | `/^GAME-\d{4}$/` | For `value` and `data` matches, exact and partial matching use the text exactly as stored. Regex patterns are not normalized. ## Examples ### Match an NFC UID ```toml [[mappings.entry]] token_key = 'id' match_pattern = '04:4E:D8:DA:ED:72:81' zapscript = '**launch.random:snes' ``` UID matching is normalized, so this also matches `044ed8daed7281`. ### Match a barcode ```toml [[mappings.entry]] token_key = 'id' match_pattern = '978*' zapscript = '**launch.search:books' ``` Because `*` creates a partial match, this matches any barcode ID containing `978`. ### Match stored token text ```toml [[mappings.entry]] token_key = 'value' match_pattern = '*pokemon*' zapscript = '**launch.random:gbc/*pokemon*' ``` ### Match with a regular expression ```toml [[mappings.entry]] token_key = 'value' match_pattern = '/^GAME-\d{4}$/' zapscript = '**launch.favorites' ``` ### Match raw token data ```toml [[mappings.entry]] token_key = 'data' match_pattern = '*0327*' zapscript = '**launch.system:switch/amiibo' ``` Raw data is matched as a hexadecimal string. ## Multiple mappings Core checks database mappings first, then file mappings, then legacy platform mappings such as MiSTer CSV mappings. The first match wins. Within mapping files, entries are loaded from `.toml` files in the `mappings` folder and its subfolders. File ordering can matter when two mappings could match the same token, so keep more specific mappings before broader ones when they are in the same file. ```toml # NFC tag for SNES games [[mappings.entry]] match_pattern = '044ed8daed7281' zapscript = '**launch.random:snes' # Barcode for retro gaming [[mappings.entry]] token_key = 'id' match_pattern = '/^[0-9]{13}$/' zapscript = '**launch.search:retro' # Text value override [[mappings.entry]] token_key = 'value' match_pattern = 'old-script' zapscript = '**launch.system:genesis' ``` ## API-managed mappings Core also exposes API methods for creating, updating, deleting, listing, and reloading mappings. API-managed mappings are stored in the Core database, can be enabled or disabled without restarting Core, and take precedence over file mappings. | Method | Purpose | | --- | --- | | `mappings` | List stored mappings. | | `mappings.new` | Create a mapping. | | `mappings.update` | Update a mapping by ID. | | `mappings.delete` | Delete a mapping by ID. | | `mappings.reload` | Reload file mappings from disk. | See the [Core API methods](../core/api/methods.md#mappings) documentation for request parameters and examples. --- ## Play Controls Play controls help manage what happens while media is active. Use them to prevent accidental game switches, require confirmation before changing games, or limit how long games can be played. Use these for parental limits, shared arcade cabinets, events, kiosks, or any setup where scans should be more deliberate. ## Setup in the Zaparoo App Open the [Zaparoo App](../app/index.md) or [Web UI](../app/web.md), then go to **Settings > Play Controls**. From there you can configure: - **Launch guard**: stage disruptive scans instead of launching them immediately. - **Playtime limits**: set daily and per-session play limits. Changes made through the App apply without restarting Core. ## Launch guard Launch guard protects the active game session from accidental token scans. When enabled, tokens scanned while media is playing are staged first. A second deliberate action is required before Zaparoo switches games. Launch guard only activates when media is currently playing. If nothing is playing, tokens launch as normal. Not every token is staged. Launch guard checks the token's [ZapScript](../zapscript/index.md), including any mapped script, and only stages scans that would disrupt the active session. Media launches, `playlist.*`, and `stop` are staged. Utility commands like `input.keyboard`, `http.get`, and `input.coinp1` pass through immediately. If Core can't parse the script, it stages the scan instead of passing it through. ### Staging flow When a disrupting token is staged: 1. A pending sound plays and a [`tokens.staged`](../core/api/notifications.md#tokensstaged) notification is sent with the token details. 2. The timeout timer starts. If no confirmation arrives before it expires, the staged token is dropped. 3. If a delay is configured, re-tap confirmation is blocked until the delay expires. When the delay expires, a ready sound plays and a [`tokens.staged.ready`](../core/api/notifications.md#tokensstagedready) notification is sent. If a different disrupting token is scanned while one is already staged, it replaces the staged token and the timers restart. ### Confirming a staged token Once the delay has passed, re-tap the same card to confirm and launch it. Tapping a different card replaces the staged token instead. The API [`confirm`](../core/api/methods.md#confirm) method also confirms and launches the staged token. It bypasses the delay for physical buttons and automation. Setting `require_confirm = true` disables re-tap confirmation. The staged token can only be launched through the API `confirm` method. ## Playtime limits Playtime limits track how long games are played and can enforce a daily limit, a per-session limit, or both. Core sends warnings before time runs out, stops active media when a limit is reached, and blocks new launches until the limit resets. ### Sessions A session is a continuous period of gaming. Playtime accumulates across multiple games within the same session. A session has three states: 1. **Active**: A game is running and time is being tracked. 2. **Cooldown**: The game stopped, but cumulative time is preserved for the `session_reset` duration. Starting another game continues the same session. 3. **Reset**: The cooldown expired and cumulative time is cleared. The next game starts a new session. Example session under the limit: ```text 12:00 - Launch Mario Kart (session starts, Active) 12:30 - Stop Mario Kart (Cooldown begins, cumulative: 30m) 12:35 - Launch Sonic (session continues, cumulative: 30m) 13:00 - Stop Sonic (Cooldown begins, cumulative: 55m) 13:25 - Cooldown expires (session resets to 0) 13:30 - Launch Zelda (new session starts) ``` When a session limit is reached, the cooldown becomes the enforced break before the next session. ```text 12:00 - Launch game (session starts) 13:00 - Session limit reached, game stopped (cumulative: 1h) 13:05 - Try to launch another game: blocked 13:20 - Cooldown expires (session resets to 0) 13:21 - Launch game: allowed ``` Daily limits reset at midnight in the local timezone. The total includes all sessions from the current calendar day. If less than 1 minute remains, the launch is blocked entirely. ## Manual configuration ### Launch guard ```toml [readers.scan.launch_guard] enabled = true timeout = 15 delay = 0 require_confirm = false ``` Common examples: ```toml # Re-tap to confirm, using default timeout [readers.scan.launch_guard] enabled = true ``` ```toml # Wait up to 30 seconds and require a 5-second cool-down before re-tap [readers.scan.launch_guard] enabled = true timeout = 30 delay = 5 ``` ```toml # Only confirm through the API [readers.scan.launch_guard] enabled = true require_confirm = true ``` ```toml # Stage indefinitely until confirmed, replaced, or media stops [readers.scan.launch_guard] enabled = true timeout = -1 ``` See the [launch guard config reference](../core/config.md#launch-guard-config) for all options. ### Playtime limits ```toml [playtime.limits] enabled = true daily = "2h" session = "45m" session_reset = "20m" warnings = ["10m", "5m", "2m", "1m"] ``` Limit play to 1 hour per day: ```toml [playtime.limits] enabled = true daily = "1h" ``` Allow 45-minute gaming sessions with 30-minute breaks: ```toml [playtime.limits] enabled = true session = "45m" session_reset = "30m" ``` See the [playtime config reference](../core/config.md#playtime) for all options. ## API and notifications - [`confirm`](../core/api/methods.md#confirm) launches the currently staged token and bypasses launch guard delay. - [`settings.playtime.limits.update`](../core/api/methods.md#settingsplaytimelimitsupdate) changes playtime limits at runtime. - [`tokens.staged`](../core/api/notifications.md#tokensstaged) reports a staged token. - [`tokens.staged.ready`](../core/api/notifications.md#tokensstagedready) reports that re-tap confirmation is ready. Disabling playtime limits resets the current session and clears cooldown timers. Daily usage history is kept. Re-enabling starts a fresh session, but daily usage from history still counts toward the daily limit. Playtime history is kept for 365 days by default. Change [`playtime.retention`](../core/config.md#retention) to keep fewer days or set it to `0` to keep all history. ## Platform support Playtime tracking works on supported platforms, but accuracy depends on how well Core can detect when media starts and stops. MiSTer and Batocera currently have the most accurate game detection. Other platforms are still improving. On MiSTer, game tracking requires `recents=1` in `MiSTer.ini`. Without it, games launched from the MiSTer menu are not detected. See [MiSTer game tracking](../platforms/mister/index.md#game-tracking). ## Troubleshooting ### Launch guard does not stage scans 1. Check that `[readers.scan.launch_guard] enabled = true` is set. 2. Check that media is already playing. Launch guard does not stage scans from the menu. 3. Check whether the scanned token only runs utility commands. Utility commands pass through by design. ### Playtime limits not enforcing 1. Check that `[playtime.limits] enabled = true` is set. 2. Check that daily/session values are valid duration strings. 3. Restart Core after manual config edits, or use the App to apply changes immediately. 4. Enable debug logging to see limit check messages. ### Warnings not appearing 1. Check the [`warnings`](../core/config.md#warnings) array. 2. Make sure [audio feedback](../core/config.md#scan_feedback) is enabled. 3. Check the Zaparoo App is connected to receive notifications. ### Session not resetting Check [`session_reset`](../core/config.md#session_reset). If it is set to `"0"`, sessions never reset automatically. --- ## Playlists Playlists let Zaparoo Core keep a temporary list of media or [ZapScript](../zapscript/index.md) actions in memory. A playlist can start playing immediately, wait for you to choose an item, or stay loaded so another token can move forward, go back, or jump to a specific position. Use playlists when one token should represent a set of games, a music or video queue, a shuffled folder, or a menu of related launches. ## How playlists work A playlist has an ordered list of items and a current position. Each item contains ZapScript, so an item can be a media path, a [title ID](../zapscript/launch.md#launchtitle), or another supported command. Playlists are loaded at runtime. They are not a permanent library feature, and the active playlist is cleared when you stop it or when Core restarts. ## Playlist source formats The `playlist.play`, `playlist.load`, and `playlist.open` commands all take the same kind of source. A source can be a folder path, a `.pls` file path, or an inline JSON playlist. | Source | Use it when | | ------ | ----------- | | Folder | You want immediate files in a folder to become playlist items. Subfolders, hidden files, files without extensions, and zip contents are not included. | | `.pls` file | You want a reusable playlist file with numbered entries, optional display titles, and entries that can run ZapScript. | | Inline JSON | You want the whole playlist stored inside one ZapScript command. | ### Folder A folder source turns each immediate file in that folder into a playlist item. The item name comes from the filename without its extension, and the item ZapScript is the file path. ```zapscript **playlist.play:/media/fat/games/Genesis ``` Folder playlists do not scan subfolders. They also skip hidden files, entries without file extensions, and files inside zip archives. Add `?mode=shuffle` to shuffle the loaded items: ```zapscript **playlist.play:/media/fat/games/Genesis?mode=shuffle ``` ### `.pls` file A `.pls` source uses the standard playlist-style header and numbered `File` entries. In Zaparoo, each `File` value is treated as ZapScript. It can be a media path, a title ID, or a command such as `**launch.random:Genesis`. The field is still named `File` because that is the `.pls` format. ```text [playlist] File1=/media/fat/games/Genesis/Sonic The Hedgehog.md Title1=Sonic The Hedgehog File2=/media/fat/games/Genesis/Streets of Rage.md Title2=Streets of Rage File3=**launch.random:Genesis Title3=Random Genesis Game ``` `Title` lines are optional. If a title is missing, Core generates a display name from the item ZapScript where it can. Items are sorted by their number, not by the order they appear in the file. `File2` plays before `File10`, even if `File10` appears first. Numbering does not need to be consecutive, but each playable item needs a matching `File` line. Relative media filenames are resolved from the same folder as the `.pls` file when Core can find a matching file there. For portable playlists, absolute paths or normal ZapScript entries are clearer: ```text [playlist] File1=Some Game (USA).md Title1=Some Game File2=/media/fat/games/SNES/Another Game.sfc?launcher=LLAPISNES Title2=Another Game ``` Load a `.pls` file the same way you would load a folder: ```zapscript **playlist.play:/media/fat/playlists/favorites.pls ``` ### Inline JSON Inline JSON stores the playlist object directly in the ZapScript command. Use this when you want a complete playlist on one token without creating a separate `.pls` file. Use these top-level fields: | Field | Description | | ----- | ----------- | | `id` | Playlist ID. Core uses this to recognize the same playlist when opening a source that is already active. | | `name` | Display name for the playlist. The picker uses this as its title. | | `items` | Array of playlist items. Without items, there is nothing useful to play. | Each item uses these fields: | Field | Description | | ----- | ----------- | | `name` | Display name for the item. If this is missing, Core generates a display name from `zapscript` where it can. | | `zapscript` | ZapScript to run when the item is selected or played. Each useful item needs this field. | This is the JSON object by itself: ```json { "id": "party-list", "name": "Party List", "items": [ { "name": "Sonic", "zapscript": "/media/fat/games/Genesis/Sonic The Hedgehog.md" }, { "name": "Random SNES Game", "zapscript": "**launch.random:SNES" } ] } ``` To use it on a token, put the JSON after the playlist command: ```zapscript **playlist.play:{"id":"party-list","name":"Party List","items":[{"name":"Sonic","zapscript":"/media/fat/games/Genesis/Sonic The Hedgehog.md"},{"name":"Random SNES Game","zapscript":"**launch.random:SNES"}]} ``` Inline JSON is parsed as a single [JSON argument](../zapscript/syntax.md#json-arguments), so the command can still contain ZapScript separators such as `||` after the closing `}` if you need to chain another command. Because this is JSON, strings must be quoted and any quotes inside item ZapScript need to be escaped. ## Using a playlist Load a playlist source and launch the first item: ```zapscript **playlist.play:/media/fat/playlists/favorites.pls ``` Load a playlist source without launching anything yet: ```zapscript **playlist.load:/media/fat/playlists/favorites.pls ``` Open a picker for a playlist source: ```zapscript **playlist.open:/media/fat/playlists/favorites.pls ``` Move through an active playlist from separate tokens: ```zapscript **playlist.next ``` ```zapscript **playlist.previous ``` Pause and resume the active playlist: ```zapscript **playlist.pause ``` ```zapscript **playlist.play ``` Stop the current media and clear the active playlist: ```zapscript **playlist.stop ``` ## Picker support `playlist.open` opens an interactive picker for the active playlist or for a playlist source you provide. :::note Platform Support The interactive picker is currently only supported on [MiSTer](../platforms/mister/index.md). ::: ## Command reference See the [ZapScript playlist command reference](../zapscript/playlist.md) for exact syntax, supported formats, and all playlist commands. --- ## Publishers Publishers send [Zaparoo Core](../core/index.md) notifications to other services in real time. MQTT publishers forward the notification stream as JSON. PixelCade publishers turn media launch notifications into marquee display requests. Multiple publishers can run at the same time, including more than one of the same type. Each one receives the same event stream. Publishers are configured in `config.toml`. There is no Web UI for this feature. ## MQTT The MQTT publisher sends Core notifications as JSON messages to an MQTT broker. Use it with home automation setups like [Home Assistant](https://www.home-assistant.io/) and Node-RED, or any other system that can subscribe to MQTT topics. Events are published to the configured topic as they fire. ```toml [[service.publishers.mqtt]] broker = "mqtt://192.168.1.100:1883" topic = "zaparoo/events" ``` See the [Config File Reference](../core/config.md#servicepublishersmqtt) for all options: - [`enabled`](../core/config.md#mqtt-publisher-enabled) — turn this publisher on or off - [`broker`](../core/config.md#mqtt-publisher-broker) — MQTT broker URL (`mqtt://`, `mqtts://`, or `ssl://`) - [`topic`](../core/config.md#mqtt-publisher-topic) — MQTT topic to publish to - [`filter`](../core/config.md#mqtt-publisher-filter) — limit which event types are published ## PixelCade The PixelCade publisher displays game marquee art on a [PixelCade](https://pixelcade.org) LED display. When media starts, it maps the Zaparoo system ID to a PixelCade console folder and requests the matching marquee for the launched file. ```toml [[service.publishers.pixelcade]] host = "192.168.1.50" ``` See the [Config File Reference](../core/config.md#servicepublisherspixelcade) for all options: - [`enabled`](../core/config.md#pixelcade-publisher-enabled) — turn this publisher on or off - [`host`](../core/config.md#pixelcade-publisher-host) — hostname or IP address of the PixelCade device (required) - [`port`](../core/config.md#pixelcade-publisher-port) — HTTP API port (default: `8080`) - [`mode`](../core/config.md#pixelcade-publisher-mode) — `"stream"` or `"write"` endpoint (default: `"stream"`) - [`filter`](../core/config.md#pixelcade-publisher-filter) — limit which event types trigger requests PixelCade currently only sends requests for `media.started` notifications. Other notification types are ignored even if they pass the filter. ## Filtering events Every publisher has a `filter` option that limits which [notification types](../core/api/notifications.md) it receives. Leave it empty to receive all events. ```toml [[service.publishers.mqtt]] broker = "mqtt://localhost:1883" topic = "zaparoo/events" filter = [ "media.started", "media.stopped" ] ``` If you only care about media events and not every token scan, filter down to the event types you need. ## Multiple publishers You can define multiple publishers of the same type, each with different settings: ```toml [[service.publishers.mqtt]] broker = "mqtt://localhost:1883" topic = "zaparoo/all" [[service.publishers.mqtt]] broker = "mqtt://192.168.1.200:1883" topic = "home/arcade/events" filter = ["media.started", "media.stopped"] [[service.publishers.pixelcade]] host = "192.168.1.50" ``` --- ## Scraping Scraping enriches your indexed media with extra metadata and artwork. Media indexing finds your files and creates the records; scraping fills those records with details like cover art, descriptions, developers, and genres. Scrapers only update media that already exists in your library. They never create, move, or delete your games, and a scrape never changes the files on disk. :::info Local sources only Core's built-in scrapers read metadata and artwork that already exist on your device. They do not download anything from the internet. To fetch artwork, scrape it first with a tool like MiSTer Companion or Skraper, then run a Zaparoo scrape to import the results. ::: ## Running a scrape Start a scrape from the [Zaparoo App](../app/index.md) under **Settings**, or from the [TUI](../core/tui.md) under **Manage media**. You can scrape your whole library or pick specific [systems](./systems.md). Only one scrape runs at a time, and scraping cannot run while a media database update is in progress. A running scrape can be paused, resumed, or cancelled. By default a scrape skips media that has already been scraped, so repeat runs are quick. A **force** (full re-scrape) processes everything again, refreshes existing metadata, and cleans up references to artwork files that have since been removed. ## Scrapers Core currently includes two scrapers, both based on the [EmulationStation](https://emulationstation.org/) folder conventions used by distributions like [Batocera](../platforms/batocera/index.md), RetroBat, ES-DE, RetroDECK, and RetroPie. Both run on all [platforms](../platforms/index.mdx) wherever the matching files are present. ### gamelist.xml The `gamelist.xml` scraper imports EmulationStation metadata from a `gamelist.xml` file in each system's games folder. This is the richer of the two scrapers and brings in both text metadata and artwork. It imports: - **Tags**: developer, publisher, year, genre, rating, player count, plus per-file region and language. These feed into the [tag system](./tags.md#scraped-tags-and-labels) for matching and filtering. - **Descriptions** and the game's ScreenScraper ID. - **Artwork and media paths**: box art (2D, 3D, side, back), screenshots, title screens, marquees, wheels and logos, fan art, maps, plus videos and PDF manuals. When a `gamelist.xml` entry does not list an image directly, the scraper falls back to looking in the system's `media/` folder, the same place the media-folder scraper reads. ### media-folder The `media-folder` scraper imports artwork from EmulationStation-style `media/` folders without needing a `gamelist.xml`. Use it when you have media folders but no gamelist, or to pick up artwork a gamelist did not list. It only imports images. It does not read descriptions, tags, videos, or manuals. For each game, it looks under `/media/` in convention-named subfolders and matches files by the game's filename. For example, for `SNES/Super Mario World.sfc` it looks for files like: ```text SNES/media/images/Super Mario World.png SNES/media/boxart/Super Mario World.jpg SNES/media/screenshot/Super Mario World.png ``` Common subfolders include `images`, `boxart` (and `cover`, `box2dfront`), `boxart3d`, `screenshot`, `thumbnail`, `marquee`, `wheel` (and `logo`), `fanart`, `titleshot`, and `map`. Supported image types are PNG, JPG, JPEG, and WEBP. Games in subfolders are matched against the mirrored path first, then the flat filename. When a system exists in more than one indexed root, such as the normal games folder plus an [`index_root`](../core/config.md#index_root), Core checks each root's `media/` folder in root order and uses the first matching file. The same lookup applies when `gamelist.xml` falls back to `media/`. This lets setups with ROMs on one root and artwork on another use the same EmulationStation `media/` folder layout. A force re-scrape also removes image references that follow this naming convention when their file is no longer on disk. ## What scraping produces Scraped text values become [tags](./tags.md), which Core uses to choose between similar media and which you can filter on when launching by title. Scraped artwork is shown in the Zaparoo App when you browse your library. For the full metadata field mapping and the API methods used to start scrapes and read scraped data, see the [scraper subsystem reference](../core/contributing/scraper.md). --- ## Systems A system is a category [Zaparoo Core](../core/index.md) uses to group scanned media. Systems help Core match media to [launchers](./launchers.md), and you can use them in [ZapScript](../zapscript/index.md) commands. Most systems refer to a console, handheld, computer, or arcade platform, but systems can also describe other media types like movies, music, and images. Each [platform](../platforms/index.mdx) supports the systems its launchers can handle. System IDs are case-insensitive in [configuration files](../core/config.md) and ZapScript commands. This page lists the user-facing system IDs, explicit aliases, and fallback systems built into Core. ## Game Consoles | System ID | Name | Aliases | Fallbacks | |-----------|------|---------|-----------| | `3DO` | 3DO | | | | `3DS` | Nintendo 3DS | | | | `AdventureVision` | Entex Adventure Vision | `AVision` | | | `Arcadia` | Emerson Arcadia 2001 | | | | `Astrocade` | Bally Astrocade | | | | `Atari2600` | Atari 2600 | | `Atari7800` | | `Atari5200` | Atari 5200 | | | | `Atari7800` | Atari 7800 | | `Atari2600` | | `AtariXEGS` | Atari XEGS | | | | `CasioPV1000` | Casio PV-1000 | `Casio_PV-1000` | | | `ChannelF` | Fairchild Channel F | | | | `ColecoVision` | ColecoVision | `Coleco` | `SG1000` | | `CreatiVision` | CreatiVision | | | | `Dreamcast` | Sega Dreamcast | | | | `FDS` | Famicom Disk System | `FamicomDiskSystem` | | | `GameCube` | Nintendo GameCube | | | | `Genesis` | Sega Genesis/Mega Drive | `MegaDrive` | | | `GenesisMSU` | Genesis/Mega Drive MSU-MD | `MegaDriveMSU`, `MSU-MD` | `Genesis` | | `Intellivision` | Intellivision | | | | `Jaguar` | Atari Jaguar | | | | `JaguarCD` | Atari Jaguar CD | | `Jaguar` | | `MasterSystem` | Sega Master System | `SMS` | | | `MegaCD` | Sega CD/Mega CD | `SegaCD` | `Genesis` | | `Multivision` | Magnavox/Philips Odyssey² Multivision | | | | `NES` | Nintendo NES | | | | `NESMusic` | NES Music | | `NES` | | `NGage` | Nokia N-Gage | `N-Gage` | | | `Nintendo64` | Nintendo 64 | `N64` | | | `Odyssey2` | Magnavox Odyssey² | | | | `Ouya` | Ouya | | | | `PCFX` | PC-FX | | | | `PS2` | PlayStation 2 | `Playstation2` | | | `PS3` | PlayStation 3 | `Playstation3` | | | `PS4` | PlayStation 4 | `Playstation4` | | | `PS5` | PlayStation 5 | `Playstation5` | | | `PSX` | Sony PlayStation | `Playstation`, `PS1` | | | `Saturn` | Sega Saturn | | | | `Sega32X` | Sega 32X | `S32X`, `32X` | | | `SeriesXS` | Xbox Series X/S | `SeriesX`, `SeriesS` | | | `SG1000` | Sega SG-1000 | | `ColecoVision` | | `SNES` | Super Nintendo | `SuperNintendo` | | | `SNESMSU1` | SNES MSU-1 | `MSU1`, `MSU-1` | `SNES` | | `SNESMusic` | SNES Music | | `SNES` | | `Socrates` | VTech Socrates | | | | `Sufami` | Sufami Turbo | | | | `SuperACan` | Funtech Super A'Can | | | | `SuperGrafx` | SuperGrafx | | `TurboGrafx16` | | `Switch` | Nintendo Switch | `NintendoSwitch` | | | `TurboGrafx16` | TurboGrafx-16/PC Engine | `TGFX16`, `PCEngine` | `SuperGrafx` | | `TurboGrafx16CD` | TurboGrafx-16 CD/PC Engine CD | `TGFX16-CD`, `PCEngineCD` | `TurboGrafx16` | | `VC4000` | Interton VC 4000 | | | | `Vectrex` | Vectrex | | | | `VideopacPlus` | Philips Videopac Plus G7400 | | | | `VirtualBoy` | Virtual Boy | | | | `VSmile` | VTech V.Smile | | | | `Wii` | Nintendo Wii | `NintendoWii` | | | `WiiU` | Nintendo Wii U | `NintendoWiiU` | | | `Xbox` | Xbox | | | | `Xbox360` | Xbox 360 | | | | `XboxOne` | Xbox One | | | ## Handheld Systems | System ID | Name | Aliases | Fallbacks | |-----------|------|---------|-----------| | `AtariLynx` | Atari Lynx | | | | `Gamate` | Gamate | | | | `Gameboy` | Nintendo Game Boy | `GB` | | | `Gameboy2P` | Game Boy (2 Player) | | | | `GameboyColor` | Game Boy Color | `GBC` | `Gameboy` | | `GameCom` | Tiger Game.com | | | | `GameGear` | Sega Game Gear | `GG` | | | `GameGear2P` | Game Gear (2 Player) | | | | `GameNWatch` | Game & Watch | | | | `GBA` | Game Boy Advance | `GameboyAdvance` | | | `GBA2P` | Game Boy Advance (2 Player) | | | | `MegaDuck` | Mega Duck | | | | `NDS` | Nintendo DS | `NintendoDS` | | | `NeoGeoPocket` | Neo Geo Pocket | | | | `NeoGeoPocketColor` | Neo Geo Pocket Color | | `NeoGeoPocket` | | `PocketChallengeV2` | WonderSwan Pocket Challenge V2 | | | | `PokemonMini` | Pokémon Mini | | | | `PSP` | PlayStation Portable | `PlaystationPortable` | | | `SGBMSU1` | Super Game Boy MSU-1 | | `SuperGameboy` | | `SuperGameboy` | Super Game Boy | `SGB` | `Gameboy` | | `SuperVision` | Watara SuperVision | | | | `Vita` | PlayStation Vita | `PSVita` | | | `WonderSwan` | WonderSwan | | | | `WonderSwanColor` | WonderSwan Color | | `WonderSwan` | ## Computer Systems | System ID | Name | Aliases | Fallbacks | |-----------|------|---------|-----------| | `AcornAtom` | Acorn Atom | | | | `AcornElectron` | Acorn Electron | | | | `AliceMC10` | Alice MC-10 | | | | `Amiga` | Commodore Amiga | `Minimig` | `Amiga500`, `Amiga1200` | | `Amiga500` | Amiga 500 | `A500` | `Amiga` | | `Amiga1200` | Amiga 1200 | `A1200` | `Amiga` | | `AmigaCD32` | Amiga CD32 | | `Amiga` | | `Amstrad` | Amstrad CPC | | | | `AmstradPCW` | Amstrad PCW | `Amstrad-PCW` | | | `Apogee` | Apogee BK-01 | | | | `AppleI` | Apple I | `Apple-I` | | | `AppleII` | Apple II | `Apple-II` | | | `Aquarius` | Mattel Aquarius | | | | `Archimedes` | Acorn Archimedes | | | | `Atari800` | Atari 8-bit computers | | | | `AtariST` | Atari ST | | | | `BBCMicro` | BBC Micro | | | | `BK0011M` | Elektronika BK-0011M | | | | `C16` | Commodore 16 | | | | `C64` | Commodore 64 | | | | `CasioPV2000` | Casio PV-2000 | `Casio_PV-2000` | | | `CDI` | Philips CD-i | `CD-i` | | | `CoCo2` | TRS-80 Color Computer 2 | | | | `ColecoAdam` | Coleco Adam | | | | `CommanderX16` | Commander X16 | | | | `DOS` | MS-DOS | `ao486`, `MS-DOS` | `PC` | | `EDSAC` | EDSAC | | | | `FM7` | Fujitsu FM-7 | | | | `FMTowns` | FM Towns | | | | `Galaksija` | Galaksija | | | | `Interact` | Interact Home Computer | | | | `Jupiter` | Jupiter Ace | | | | `Laser` | Laser 310 | `Laser310` | | | `Lynx48` | Camputers Lynx | | | | `MacOS` | macOS | | | | `MacPlus` | Macintosh Plus | | | | `MSX` | MSX | | `MSX1`, `MSX2` | | `MSX1` | MSX1 | | `MSX` | | `MSX2` | MSX2 | | `MSX` | | `MSX2Plus` | MSX2+ | | `MSX2`, `MSX` | | `MultiComp` | MultiComp | | | | `Orao` | Orao | | | | `Oric` | Oric | | | | `PC` | PC | | `DOS`, `Windows` | | `PC88` | NEC PC-8801 | | | | `PC98` | NEC PC-9801 | | | | `PCXT` | IBM PC XT | | | | `PDP1` | PDP-1 | | | | `PET2001` | Commodore PET | | | | `PMD85` | PMD 85 | | | | `QL` | Sinclair QL | | | | `RX78` | Bandai RX-78 | | | | `SAMCoupe` | SAM Coupé | | | | `ScummVM` | ScummVM | | | | `SordM5` | Sord M5 | `Sord M5` | | | `Specialist` | Specialist MX | `SPMX` | | | `Spectravideo` | Spectravideo SVI-318/328 | | | | `SVI328` | Spectravideo SVI-328 | | | | `TatungEinstein` | Tatung Einstein | | | | `Thomson` | Thomson computers | | | | `TI994A` | TI-99/4A | `TI-99_4A` | | | `TomyTutor` | Tomy Tutor | | | | `TRS80` | TRS-80 | | | | `TSConf` | TS-Configuration | | | | `UK101` | UK101 | | | | `Vector06C` | Vector-06C | `Vector06` | | | `VIC20` | Commodore VIC-20 | | | | `Windows` | Windows | `Win32`, `Win16` | `PC` | | `X1` | Sharp X1 | | | | `X68000` | Sharp X68000 | | | | `ZX81` | Sinclair ZX81 | | | | `ZXNext` | ZX Spectrum Next | | | | `ZXSpectrum` | ZX Spectrum | `Spectrum` | | ## Arcade Systems | System ID | Name | Aliases | Fallbacks | |-----------|------|---------|-----------| | `Arcade` | Arcade/MAME | `MAME` | | | `Atomiswave` | Atomiswave | | | | `Chihiro` | Sega Chihiro | | | | `CPS1` | Capcom Play System 1 | | | | `CPS2` | Capcom Play System 2 | | | | `CPS3` | Capcom Play System 3 | | | | `DAPHNE` | DAPHNE (Laserdisc) | `LaserDisc` | | | `DICE` | Sega DICE | | | | `Gaelco` | Gaelco arcade boards | | | | `Hikaru` | Sega Hikaru | | | | `Lindbergh` | Sega Lindbergh | | | | `Model1` | Sega Model 1 | | | | `Model2` | Sega Model 2 | | | | `Model3` | Sega Model 3 | | | | `Namco22` | Namco System 22 | | | | `Namco2X6` | Namco System 2 | | | | `NAOMI` | Sega NAOMI | | | | `NAOMI2` | Sega NAOMI 2 | | | | `NeoGeo` | SNK Neo Geo | | `NeoGeoAES`, `NeoGeoMVS` | | `NeoGeoAES` | Neo Geo AES (home) | | `NeoGeo`, `NeoGeoMVS` | | `NeoGeoCD` | Neo Geo CD | | `NeoGeo` | | `NeoGeoMVS` | Neo Geo MVS (arcade) | | `NeoGeo`, `NeoGeoAES` | | `Singe` | Singe (Laserdisc) | | | | `Triforce` | Triforce | | | ## Other Systems | System ID | Name | Aliases | Fallbacks | |-----------|------|---------|-----------| | `Android` | Android | | | | `Arduboy` | Arduboy | | | | `Audio` | Audio Files | | | | `Chip8` | CHIP-8 | | | | `GameMaster` | Game Master | | | | `GamePocket` | Game Pocket | | | | `GP32` | GP32 | | | | `Groovy` | Groovy | | | | `Image` | Image Files | | | | `iOS` | iOS | | | | `J2ME` | Java ME | | | | `Movie` | Movie Files | | `Video` | | `MusicAlbum` | Music Albums | | `Audio` | | `MusicArtist` | Music Artists | | `Audio` | | `MusicTrack` | Music Tracks | `Music` | `Audio` | | `Pico8` | PICO-8 | | | | `PlugNPlay` | Plug & Play TV Games | | | | `TIC80` | TIC-80 | | | | `TVEpisode` | TV Episodes | `TV` | `Video` | | `TVShow` | TV Shows | | `Video` | | `Video` | Video Files | | | ## Usage Notes - **System IDs are case-insensitive** when used in configuration files and ZapScript commands - **Aliases** are explicit alternative names that Core treats as the same system, such as `MegaDrive` for `Genesis`, `GB` for `Gameboy`, or `N64` for `Nintendo64` - **Fallbacks** let Core try related systems when looking for a launcher. For example, `GameboyColor` can fall back to `Gameboy`, and `MegaCD` can fall back to `Genesis` - Not all systems are supported on every platform - check your [platform's documentation](../platforms/index.mdx) for launcher-specific support - System defaults can be configured in your [configuration file](../core/config.md#systems) - Systems are used in [ZapScript](../zapscript/index.md) commands for launching specific system types ### Examples ```toml # Using system IDs in configuration - case doesn't matter [[systems.default]] system = "SNES" # Same as "snes" or "Snes" launcher = "retroarch" [[systems.default]] system = "genesis" # Can also use alias "megadrive" launcher = "retroarch" ``` --- ## Tags Tags are metadata labels that Zaparoo Core extracts from media filenames. They help Core choose between multiple matches for the same title, especially when your library has several regions, languages, revisions, demos, or bad dumps. You will usually see tags when using [title IDs](../zapscript/launch.md#launchtitle). For example, this launches the US version of `Super Mario World` when more than one version is available: ```zapscript @SNES/Super Mario World (region:us) ``` ## Tag format Tags use `type:value` format. The type is the category, and the value is the specific label. | Part | Example | | --- | --- | | Tag type | `region` | | Tag value | `us` | | Full tag | `region:us` | When a tag is part of a title ID, wrap it in parentheses: ```zapscript @SNES/Game Title (region:eu) (lang:de) ``` ## Filename tags During media database updates, Core can parse known filename markers from ROM sets such as [No-Intro](https://no-intro.org/) and [TOSEC](https://www.tosec.org/). This behavior is controlled by [`filename_tags`](../core/config.md#filename_tags), which is enabled by default. This filename: ```text Sonic the Hedgehog (USA, Europe) (En,Fr,De) (Rev A).md ``` can produce tags like: - `region:us` - `region:eu` - `lang:en` - `lang:fr` - `lang:de` - `rev:a` Core also extracts tags from other recognized markers, including years, disc numbers, versions, dumps, prototypes, demos, translations, unofficial hacks, set numbers, and re-releases. MiSTer Arcade filenames can include a region and build date in one parenthetical group. For example, `(World 931005)` becomes `region:world` and `builddate:1993-10-05`. Core also accepts `YYYYMMDD`, `YYYY-MM-DD`, and comma-separated forms such as `(EU, 961004)`. ## Using tags in title IDs Use tags when a title ID needs to be more specific than the game name alone. ```zapscript @SNES/Super Mario World @SNES/Super Mario World (region:us) @SNES/Super Mario World (region:eu) (lang:de) @Arcade/X-Men Vs. Street Fighter (region:eu) (builddate:1996-10-04) @Genesis/Sonic (+unfinished:demo) @SNES/Game Title (~lang:en) (~lang:fr) ``` The first example lets Core choose the best match. The others require or exclude specific tagged matches. ### Operators Tags support three operators in title IDs and `launch.title` tag arguments. | Syntax | Meaning | | --- | --- | | `(tag:value)` or `(+tag:value)` | Match must have this tag. | | `(-tag:value)` | Match must not have this tag. | | `(~tag:value)` | At least one `~` tag must match. | Examples: ```zapscript @Genesis/Sonic (region:us) @Genesis/Sonic (-unfinished:demo) @SNES/Game Title (lang:en) (-unfinished:beta) @SNES/Game Title (~lang:en) (~lang:fr) ``` Multiple tags can share one parenthetical group when separated by commas: ```zapscript @SNES/Game Title (region:us, lang:en) ``` The `tags` argument uses the same operators without parentheses: ```zapscript **launch.title:Genesis/Sonic?tags=region:us,lang:en **launch.title:Genesis/Sonic?tags=region:us,-unfinished:demo ``` ## Matching behavior When you launch by title ID, Core matches the title first, then uses tags to narrow or rank the results. Core also filters out some variants by default, including demos, betas, prototypes, hacks, translations, bootlegs, re-releases, and bad dumps. If you want one of those variants, request it with a positive tag: ```zapscript @Genesis/Sonic (+unfinished:demo) @SNES/Game Title (+unfinished:beta) @SNES/Game Title (+unlicensed:translation) ``` If several good matches remain, Core applies your configured region and language preferences, then other scoring rules such as launcher file type priority and filename quality. For same-named variants, browse, search, and lookup responses include `disambiguatingTags`: the tags that distinguish one variant from another. Clients can show those labels to help you pick the right entry, and the generated ZapScript includes the same tags so the token resolves back to that specific variant. ## Default regions and languages Set default preferences in the [`media`](../core/config.md#media) config section when you want Core to prefer certain regions or languages without writing tags into every title ID. ```toml [media] default_regions = ["us", "eu", "world"] default_langs = ["en", "de"] ``` With those preferences, `@SNES/Super Mario World` will prefer matching US, European, or World releases, and English or German language tags, when those tags are present. ## Useful tag types These are the tag types most likely to matter when writing title IDs by hand. | Tag type | Used for | Examples | | --- | --- | --- | | `region` | Release region | `region:us`, `region:eu`, `region:jp`, `region:world` | | `lang` | Language | `lang:en`, `lang:fr`, `lang:de`, `lang:ja` | | `rev` | Revision or version | `rev:a`, `rev:b`, `rev:1`, `rev:prg0` | | `year` | Release year | `year:1991`, `year:1996`, `year:19xx` | | `builddate` | ROM set build date, often from arcade filenames | `builddate:1996-10-04` | | `unfinished` | Pre-release or incomplete builds | `unfinished:alpha`, `unfinished:beta`, `unfinished:demo`, `unfinished:proto` | | `dump` | Dump quality or dump status | `dump:verified`, `dump:bad`, `dump:overdump`, `dump:underdump` | | `unlicensed` | Unofficial releases and modifications | `unlicensed:bootleg`, `unlicensed:hack`, `unlicensed:translation` | | `rerelease` | Digital re-releases and collections | `rerelease:virtualconsole:wii`, `rerelease:mdmini:1` | | `reboxed` | Re-releases and packaging variants | `reboxed:playerschoice`, `reboxed:satakore` | | `disc` and `disctotal` | Multi-disc sets | `disc:1`, `disc:2`, `disctotal:3` | | `media` | Media type or side | `media:disc`, `media:tape`, `media:side-a` | | `set` | ROM set number or variant group | `set:1`, `set:2` | | `video` | Video standard | `video:ntsc`, `video:pal`, `video:pal-60` | Core defines many more tag types than this, including hardware add-ons, embedded cartridge chips, compatibility tags, publishers, developers, genres, and player counts. Most users do not need to write those by hand. ## Scraped tags and labels [Metadata scrapers](./scraping.md) can add tags for publishers, developers, genres, arcade boards, game families, regions, and languages. Core stores these tag values as normalized slugs so matching stays consistent. For example, `T&E Soft` is stored as `t-and-e-soft`. When scraped metadata includes a human-readable label, API responses can preserve that label alongside the normalized tag value. Use the normalized `type:value` form when writing title IDs or tag filters by hand. ## Credit Zaparoo's tag taxonomy is inspired by [GameDataBase](https://github.com/PigSaint/GameDataBase), a game metadata project by [PigSaint](https://github.com/PigSaint). GameDataBase uses a deeper hierarchical tag system; Zaparoo uses simpler `type:value` tags for filename parsing and title matching. --- ## Frontend Customization Zaparoo Frontend can load your own system artwork, Hub icons, and system display names without rebuilding Frontend. Customization is read at startup. Restart Frontend after adding, changing, or removing custom files. ## Custom folder On MiSTer, the default customization folder is: ```text /media/fat/zaparoo/custom/ ``` Create one subfolder for each kind of image override: ```text /media/fat/zaparoo/custom/ systems/ # system artwork in the systems grid hub/ # Hub category and action icons ``` To use another folder, edit `/media/fat/zaparoo/frontend.toml`: ```toml title="frontend.toml" [custom] dir = "/media/usb/zaparoo-art" ``` ## Image filenames Name each image after the ID it overrides. Matching is case-insensitive, so `snes.png` and `SNES.png` match the same system. Supported image types are PNG, JPG, JPEG, WEBP, BMP, and SVG. ### System artwork System artwork in `systems/` uses [Zaparoo system IDs](../features/systems.md): ```text systems/snes.png systems/genesis.svg systems/turbografx16.webp ``` ### Hub icons Hub icons in `hub/` use category or action IDs: | ID | Hub item | | --- | --- | | `arcade` | Arcade category | | `computer` | Computer category | | `console` | Console category | | `handheld` | Handheld category | | `other` | Other category | | `resume` | Resume Game | | `favorites` | Favorites | | `recents` | Recently Played | | `settings` | Settings & Utilities | ```text hub/arcade.png hub/favorites.svg hub/settings.svg ``` Custom images are shown as-is. Built-in icons can be tinted by Frontend's theme, but your override images are not recolored. ## System names To rename systems in Frontend, add a `[custom.system_names]` table to `/media/fat/zaparoo/frontend.toml`: ```toml title="frontend.toml" [custom.system_names] snes = "Super Nintendo" psx = "PlayStation" genesis = "Mega Drive" ``` The key is the system ID. Matching is forgiving about case and punctuation, so `snes`, `SNES`, and `S.N.E.S` resolve to the same system. Custom names take priority over Frontend's built-in regional names and names reported by Core. ## Regional system names Frontend also includes built-in regional system names and logo variants. Open **Settings**, then **Language**, then **System names** to choose **Automatic**, **Americas**, **Europe**, or **Japan**. Use custom system names when you want a specific name regardless of the regional setting. ## System logo style Frontend ships its built-in system logos in two styles. The **System logos** setting switches between tintable logos, which follow the active theme, and full-color logos. Tintable is the default. This setting only affects the built-in logos, not your own [system artwork](#system-artwork). --- ## Zaparoo Frontend Zaparoo Frontend is a TV and controller-friendly interface for browsing and launching your [Zaparoo Core](../core/index.md) media library on [MiSTer FPGA](../platforms/mister/index.md). It runs on the MiSTer display and uses Core as its backend, so media scanning, launching, metadata, favorites, and token writing stay in Zaparoo Core. Frontend does not replace physical tokens, the [Zaparoo App](../app/index.md), the [Web UI](../app/web.md), or the [TUI](../core/tui.md). It gives Zaparoo an on-screen browser for users who want to browse artwork or launch games from the TV. ## Quick start 1. [Install or update Frontend](./setup.mdx) on MiSTer. 2. Start Frontend. It connects to the local Core service. 3. If your media database is empty, run the first-time media scan. 4. Optional: [add artwork and metadata](./scraping.mdx) for game covers, descriptions, and variant names. 5. Optional: [customize artwork, Hub icons, and system names](./customization.mdx). On MiSTer, Frontend launches through Core. For platform-specific launcher details, see the [MiSTer launcher documentation](../platforms/mister/launchers.md). ## Using Frontend Frontend starts at your library hub. From there you can browse categories, systems, games, favorites, recents, and folders. Games launch through Core, so launcher configuration and media paths follow your normal Zaparoo setup. Use the options menu to manage the item you are viewing. Depending on context, it can launch a system core, change a system launcher, update the media database, scrape metadata, hide or unhide a category or system, write an NFC token, or discover alternate Arcade versions. While browsing games, open the **View** menu and choose **Go to...** to jump by letter or bucket instead of paging through long lists. The **Other** category holds [launchable items](../features/launchers.md#launchables) that have no normal game file behind them, such as ROM-less MiSTer cores. You can launch them like any other game or write them to a token. The Settings screen is split into **Display**, **Browsing**, **Language**, **Controls**, **Library**, and **Support** pages. Common browsing preferences include grid or list layout, preferred artwork type, system logo style, hidden item visibility, original filename display, regional system names, clock format, mouse support, and reduced motion. ## Controls The default keyboard controls are: | Action | Default keys | | ------ | ------------ | | Move | Arrow keys | | Select | `Enter` | | Back | `Escape` | | Options menu | `Tab` | | View menu | `Space` | | Previous page | `PageUp` | | Next page | `PageDown` | MiSTer will automatically map your controller buttons to these keys. ## Support logs If you need help troubleshooting Frontend, open **Settings**, then **Support**, then **Upload log file**. Frontend uploads a support bundle and shows a link you can share in Discord or a GitHub issue. The upload includes a support summary, the Frontend log tail, and the Core log tail when Core is reachable. ## Credits A big thank you to [Andrea Bogazzi](https://github.com/asturur), [BossRighteous](https://github.com/BossRighteous), and [Tim Wilsie](https://github.com/timwilsie) for creating Zaparoo Frontend with [Wizzo](https://github.com/wizzomafizzo). ## Source and license The [Frontend source code](https://github.com/ZaparooProject/zaparoo-frontend) is available under the [PolyForm Noncommercial License 1.0.0](https://github.com/ZaparooProject/zaparoo-frontend/blob/main/COPYING). Non-commercial use is allowed under that license. For commercial licensing, contact [legal@zaparoo.org](mailto:legal@zaparoo.org). --- ## Scraping Artwork and Metadata Frontend shows artwork and metadata after Core imports compatible `gamelist.xml` files. There are currently three supported ways to get metadata onto your MiSTer. ## Boss's Basic Batch [Boss's Basic Batch](https://github.com/BossRighteous/MiSTer-Media-DB/releases/tag/basic-batch-1.0) is a metadata and artwork pack from [BossRighteous](https://github.com/BossRighteous). It includes image dumps and `gamelist.xml` files mapped to Zaparoo title slugs for known ROM sets. To use it, download the ZIP and extract it into your MiSTer `games` directory. The release notes also mention that Arcade contents need to be moved to `/media/fat/_Arcade`, where MiSTer's `.mra` files live. This option works well if your ROM names match the pack and you want a ready-made metadata set. It may not match custom naming, ROM hacks, or unusual library layouts. ## ZapScraper in MiSTer Companion [MiSTer Companion v4.4.0](https://github.com/Anime0t4ku/mister-companion/releases/tag/v4.4.0) adds ZapScraper in Offline Mode. ZapScraper can scrape artwork for Zaparoo Frontend directly to a selected MiSTer SD card, or to a custom local or network location on your PC. ZapScraper has two output modes: - **Recalbox Mode** creates a standard Recalbox-style `gamelist.xml` structure. - **Zaparoo Companion Mode** creates output for Zaparoo Frontend and requires Zaparoo Core v2.13.0 or newer. ZapScraper supports console, handheld, and Arcade folders. PC cores are planned for a later ZapScraper update. ## Skraper You can also use [Skraper](https://www.skraper.net/) with a [ScreenScraper](https://www.screenscraper.fr/) account. Before you run a scrape, set Skraper to **RecalBox mode** so that it outputs the `gamelist.xml` files Core expects. Set the default image style to whatever you prefer. Skraper supports the game folder names that MiSTer uses, so you can point it directly at your SD card or a NAS share. ## Import into Frontend After using one of the methods above, open **Settings**, then **Library**, then run **Scrape metadata**. Frontend processes the metadata in the background and reconciles it against Core's media database. When it finishes, artwork appears in the game browser. Core pauses metadata scraping while media is running and resumes when playback stops. Frontend shows scrape progress by system when Core provides it. Use **Re-scrape existing** when you want Core to refresh media that already has scraper metadata. To update or scrape one system at a time, open a system's options menu and choose the media database or metadata scrape action for that system. ## Browse metadata options Open **Settings**, then **Browsing** to change how scraped media appears: - **Preferred artwork** chooses which scraped image type Frontend asks Core for first. - **Show original filenames** uses each game's original filename without the extension instead of Core's cleaned title. When multiple games have the same display name, Frontend can show variant tags inline to help distinguish regions, versions, and other variants. ## Custom artwork and names Frontend can also use your own system artwork, Hub icons, and system display names. See [Frontend customization](./customization.mdx) for the custom folder layout and `frontend.toml` examples. --- ## Frontend Setup Zaparoo Frontend runs on MiSTer FPGA and connects to the local [Zaparoo Core](../core/index.md) service. ## Requirements Frontend requires Zaparoo Core v2.15.0 or newer. If Core is too old for your installed Frontend, Frontend shows a warning on startup. On MiSTer, install or update Core from [Update All](../platforms/mister/index.md#install). Update All can automatically configure Frontend in your `MiSTer.ini` file. You can also install Frontend through [MiSTer Companion](https://github.com/Anime0t4ku/mister-companion), or install it manually from the release ZIP. :::tip SuperStation ONE users If you are using a SuperStation ONE or another MiSTer image that already includes Zaparoo, update Zaparoo Core before starting Frontend. Some images include old Core builds, such as v2.6.2, that do not support Frontend. If first-time setup stays on `Preparing...`, update Core, then reboot MiSTer or restart Frontend. ::: ## Manual install For manual installation, download the release ZIP from [GitHub Releases](https://github.com/ZaparooProject/zaparoo-frontend/releases). The ZIP contains a `zaparoo` folder with these files: - `MiSTer_Zaparoo` - `frontend` - `menu_zaparoo.rbf` Install Frontend: 1. Copy the `zaparoo` folder to the root of your MiSTer SD card. 2. Edit your `MiSTer.ini` file and add this line under the `[MiSTer]` or `[Menu]` section: ```ini main=zaparoo/MiSTer_Zaparoo ``` 3. Start or reboot your MiSTer. Frontend will start automatically. To disable or uninstall Frontend, remove the `main=zaparoo/MiSTer_Zaparoo` line from `MiSTer.ini` to return to the stock MiSTer menu. ## Updating The standard MiSTer build of Frontend includes a built-in updater. From the Hub, select **Update** to run the MiSTer Downloader, the same tool behind [Update All](../platforms/mister/index.md#install), without leaving Frontend. It updates your MiSTer firmware, cores, and Zaparoo, and shows progress on a controller-friendly screen. This is separate from **Update media database**, which only rescans your game library. See [First run](#first-run) for the library scan. You can still update manually by downloading a new release ZIP and replacing the files from the [Manual install](#manual-install) steps. ## First run On first run, Frontend connects to Core locally. If your media database is empty, it prompts you to scan your games before browsing. You can update the media database later from **Settings**, then **Library**. You can also open a system's options menu and update that system on its own. ## Display setup Frontend has display options for HDMI, CRT, and vertical setups. ### Enable CRT mode If you use Frontend on a CRT, open **Settings**, then **Display**, then **Analog video**. - **CRT mode** turns native analog output on or off. Confirming this setting restarts Frontend so MiSTer can relaunch it in the new video mode. - **Video standard** chooses NTSC or PAL when CRT mode is active. Changing it also requires a Frontend restart. - **Screen position** opens a calibration screen. Use the directional controls to move the image, then save when the picture is centered on your display. You can also toggle CRT mode from the MiSTer OSD, which is useful when you need to turn CRT mode on before you have access to an HDMI display. Press Menu on a controller or `F12` on a keyboard, as you would in a normal MiSTer core, then open **System Settings**. **CRT mode** is the last setting in the list, directly above **Reboot**. CRT mode enables the pixel fonts and tighter layouts designed for 240p output. It also uses a safe area for menus while backgrounds and screensaver art can still fill the full CRT frame. ### Enable TATE mode TATE mode rotates Frontend for vertical display setups, such as arcade cabinets or MiSTer displays mounted for vertical games. Open **Settings**, then set **Orientation** to the clockwise or counter-clockwise option that matches your display. ### Change MiSTer resolution On MiSTer, open **Settings**, then **Display**, then change **Resolution** when you need a fixed HDMI output mode instead of the default Frontend video settings. Frontend includes HD options plus larger modes such as `1920x1200`, `1920x1440`, `2048x1536`, and `2560x1440`. Resolution changes apply after Frontend restarts. --- ## Introduction Monitor, ScanLine, CreditCard, Cpu, Smartphone, Code2, Puzzle, Wand2, Palette, Hammer, Users, } from "lucide-react"; # Introduction First time here? The [setup wizard](/start/) asks 3 questions and builds a guide for your specific hardware. The docs are open for contributions, so if you see something missing or want to add something, please [create a pull request](https://github.com/ZaparooProject/zaparoo.org/pulls). --- ## Labels Create custom artwork for your Zaparoo NFC cards, then print it at home or through a print shop. :::danger[Important legal restrictions] - You are responsible for making sure you have the legal right to print any artwork you use, including game art, movie posters, album covers, and other copyrighted images. - Zaparoo trademarks, including the Zaparoo logo, must not appear on printed labels without permission. See the [Terms of Use](/terms) for details. - The Zaparoo project cannot offer pre-printed labels due to intellectual property concerns. See the [Zaparoo Media](/projects/) project for marks that are free to use for displaying token compatibility. ::: ## Design your labels The [Zaparoo Designer](../designer/index.md) web app is built for creating card labels. It runs in your browser, includes community templates, and lets you search [IGDB](https://www.igdb.com/) for game artwork. Export to PDF or PNG when you're done. You can also design labels in Photoshop, GIMP, or any graphics program using the template dimensions below. ## Print your labels See the **[Printing Guide](./printing-guide.md)** for materials, printers, cutting tools, lamination, and application techniques recommended by the community. ## Template specifications ### Standard dimensions - **Credit card size:** 3.375" x 2.125" (85.725mm x 53.975mm) - **Zaparoo label size:** 3.313" x 2.063" (84.15mm x 52.4mm) - **Print resolution:** about 994px x 619px at 300 DPI Labels include a 1/32" (0.79mm) margin from the card edge. This gives you tolerance for alignment errors and prevents edge peeling. ### Artwork margins (from label edge) - **Horizontal layout:** 3.1mm margin on top/right/bottom, 26.2mm margin on left - **Vertical layout:** 3.1mm margin on top/right/left, 12.2mm margin on bottom ### Upgrade stickers Upgrade stickers are smaller labels designed to fit on existing printed cards. They let larger designs be printed in bulk while still allowing customization later. They're easier to apply than full-size labels and are designed to look good without a corner rounder. All upgrade stickers are 36.75mm (1.45in) high with variable width: - **Zap Trading Cards:** 63x36.75mm (single area) or 63x75mm (double area with 1.5mm gap) - **Standard labels:** 47.4x36.75mm (single area) - **Frame style:** leave a 1mm border for the background frame effect --- ## Printing Guide This guide covers materials, tools, and techniques used by the Zaparoo community for printing labels at home. :::danger[Copyright notice] You are responsible for making sure you have the legal right to print any artwork you use, including game artwork, movie posters, music album covers, and other copyrighted images. The Zaparoo project cannot offer pre-printed labels due to intellectual property concerns. ::: ## Materials ### Sticker paper **Matte vinyl sticker paper** is a popular choice in the community. It produces clean results, handles well in most inkjet printers, and takes lamination well. Search for "matte vinyl sticker paper A4 inkjet" on Amazon or AliExpress. **Glossy vinyl** looks sharper but can bleed or fade within days without a lamination layer on top. If you go glossy, plan to laminate. **Holographic rainbow vinyl** is a popular option for special or favorite cards. It gives a foil/shimmer effect, but the metallic layer can slightly weaken the NFC signal when applied to a card. Search for "holographic vinyl sticker paper A4" on Amazon. **Matte sticker paper** (non-vinyl, like Avery) is a cheaper option that works fine for testing or large batches where cost matters. ### Inkjet-printable PVC cards If you want to skip labels entirely, you can print directly onto blank NFC cards using inkjet-printable PVC cards with an aftermarket PVC card tray. Only certain Canon and Epson inkjet models support this. Check the [Brainstorm ID printer compatibility list](https://support.brainstormidsupply.com/article/187-which-current-printers-can-use-an-inkjet-pvc-card-tray) before buying a printer or tray. One community member got set up with a Canon TS702a and aftermarket tray for about $130 total. ## Protection Unprotected inkjet prints will smear when touched. You have a few options: **Self-adhesive cold lamination sheets** are the community favorite. Apply a lamination sheet over your printed sticker paper before cutting. This protects against scratches, fingerprints, UV, and water. Matte vinyl with glossy lamination is a good combination for appearance and durability. **Clear coat spray** is a quicker alternative. Spray your printed sheets and let them dry before cutting. Less durable than lamination but much faster for large batches. **Holographic lamination sheets** can be applied over matte prints for a shimmer effect without needing holographic sticker paper. :::warning Avoid heat lamination. The heat can damage the NFC chip in your cards. Always use cold/self-adhesive lamination. ::: ## Printers Most consumer inkjet printers can work for sticker sheets, but print quality and paper handling vary a lot. Some models the community has had good results with: - **Epson EcoTank L8050** and **ET-4800**: Popular for high volume. EcoTanks use refillable ink bottles instead of cartridges, which keeps costs down. - **Canon TS702a**: Budget option that supports PVC card trays for direct-to-card printing. - **HP Envy 2820e**: Decent results on matte sticker paper using the "glossy photo" print setting. - **Brother J4335-DW**: Good print quality, but it can struggle to pick up heavier sticker paper because it has no manual feed. **Laser printers** can work with compatible paper, but they usually produce less vibrant colors than inkjet. Be careful with vinyl sticker paper in a laser printer because the heat can melt it. Laser is better suited to regular paper or paper-based sticker sheets. **Local print shops** are a solid option if you don't want to buy a printer. Design your labels at home, export as PDF, and send them to a shop. You probably won't be able to use specialty vinyl paper this way, but it avoids printer setup and maintenance. **Dedicated card printers** like the Badgy line print directly onto PVC cards with full bleed. They can produce excellent results, but expect a much higher cost per card once consumables are included. ## Cutting ### By hand A basic setup is an X-Acto knife, metal ruler, and cutting mat. Many community members lay out labels in a 2x5 grid on A4 paper using a Photoshop template with guides, then print and cut. A **corner rounder** helps match the rounded corners of NFC cards. A **3mm (R3) corner rounder** is the closest fit. Cheap 3-in-1 rounders from Amazon or AliExpress work fine for many people. Clean the adhesive buildup out of the punch every few cuts or it can jam. A **paper trimmer/guillotine** speeds up straight cuts significantly compared to a knife. ### Plotter cutters For precision and batch work, a plotter cutter with print-and-cut capability can save a lot of time. **Silhouette Portrait 4** is the current community recommendation. It uses Silhouette Studio, has no subscription requirement, and handles the credit card label size well. Multiple community members use this with good results. **Siser cutters** are another option with good cut consistency and third-party software support. **Cricut** has not been a good fit for this workflow. Multiple community members, including the Zaparoo creator, have had problems with the hardware or software workflow. Community testing has not produced a reliable auto-cut workflow with Zaparoo Designer. ### Die cutting For high-volume production, die cutting produces the cleanest results. [ArielAces](https://github.com/ArielAces) in the community uses a roll-based system that prints and die-cuts labels in one pass, producing thousands of consistently sized labels. ## Applying labels to cards Alignment is the hardest part of the whole process. Here are techniques that work: **Manual application:** Cut the label from the sheet first. Align the top edge of the label with the top edge of the card. Using a credit card or similar flat edge, slowly press the label down from top to bottom, pushing air out as you go. Vinyl sticker paper is slightly elastic, so you can steer it a little if it starts going off-center. **3D printed alignment jigs** are being developed by community members to hold cards in place during label application. Check [Printables](https://www.printables.com/) for "NFC card label alignment" or similar. **Double-sided approach:** If your binder pages or sleeves are double-sided, consider only labeling the front of the card. Getting both sides aligned cleanly is significantly harder. ## Print settings - Set quality to "High" or "Photo" mode - Select the correct paper type in your printer settings (glossy photo, matte, etc.) - Print a test page on regular paper first to check alignment and colors - Let ink dry completely before handling or laminating ## Commercial printing Professional printing is worth considering for 50+ identical labels or when you want premium finishes (metallic, spot gloss). Export labels as PDF from [Zaparoo Designer](../designer/index.md). Ask the printer what they need before ordering. Common requirements include: - 300+ DPI resolution - CMYK color mode instead of RGB, if the printer requires it - 1-2mm bleed if required - Fonts embedded or converted to outlines --- ## Zaparoo Online [Zaparoo Online](https://online.zaparoo.com) is a companion web service for managing virtual cards and decks. Create cards with ZapScript on them, organize them into shareable decks, and write them to physical NFC tags using the [Zaparoo App](../app/index.md). Cards live online. Change a card's script on the website, and every NFC tag linked to that card updates instantly without rewriting. :::info Not Open Source Zaparoo Online is a proprietary service operated by [Wizzo Pty Ltd](https://wizzo.au/). It adds cloud features that complement the open source project without replacing any offline functionality. Using Zaparoo Online helps support the ongoing development of the Zaparoo open source project, the same way purchasing from the Zaparoo Shop does. ::: ## Cards A card is a virtual container for one or more [ZapScript](../zapscript/index.md) commands. Each card gets a unique short URL that can be written to a physical NFC tag. You can create cards from the dashboard or by redeeming a code, then add ZapScripts to define what happens when the card is scanned. Because the tag points to the card (not the script), changes you make apply instantly to all NFC tags linked to that card. ## Decks Decks are collections of cards, like playlists. You can organize cards into themed sets, reorder them, and share the deck via a public URL. Decks can also be locked to prevent changes after sharing. ## Writing to NFC tags Cards and decks are written to physical NFC tags using the [Zaparoo App](../app/index.md). The tag stores a link to the card, not the script itself, so the tag never needs to be rewritten when you change what it does. ## Warp Warp is the premium tier for Zaparoo Online, available through [Patreon](https://www.patreon.com/wizzo). It unlocks higher card and deck limits and additional features like hiding zap counts on shared cards. --- ## Batocera [Batocera](https://batocera.org/) is a software emulation distribution based around EmulationStation, with support for a huge number of systems and devices. ## File Paths | Item | Path | | ------------------ | ----------------------------------------------------- | | Config file | `/userdata/system/.config/zaparoo/config.toml` | | Data directory | `/userdata/system/.local/share/zaparoo` | | Log file | `/userdata/system/.local/share/zaparoo/logs/core.log` | | Mappings directory | `/userdata/system/.local/share/zaparoo/mappings` | Network share paths use `\\BATOCERA\share\system\` prefix instead of `/userdata/system/`. ## Install ### Content Downloader The easiest way to install is directly from Batocera's Content Downloader: 1. Press `Start` to open the main menu 2. Navigate to `Updates & Downloads` > `Content Downloader` 3. Go to the `SYS` section and find `Zaparoo Core` 4. Press `A` to install After installation, launch Zaparoo from the Ports system in EmulationStation to access the TUI, or use the [Zaparoo App](/docs/app/) on your phone to manage your setup and write tokens. ### Terminal Alternatively, install via SSH or in the Batocera terminal (press `F1` to open the file manager, then `F4` to open a terminal): ```bash curl -fsSL https://zaparoo.org/install.sh | bash ``` This installs Zaparoo via the pacman package manager, sets up the startup service, and adds a Zaparoo entry to the Ports system. For manual installation methods (Network Share, USB, or SSH), see [Manual Install](./install.md). ## Readers ## Supported Launchers | Launcher | Notes | | ---------------- | ------------------------------------- | | EmulationStation | 150+ systems via Batocera's emulators | | Kodi | Movies, TV, Music (Kodi mode only) | | Shell Scripts | Custom `.sh` file execution | Kodi integration only works when Batocera is in Kodi mode with the API enabled. See [LibreELEC](../libreelec.md) for API configuration details. See [Launchers](./launchers.md) for the full list of supported systems and launcher details. --- ## Manual Install Alternative installation methods if you prefer not to use the automated install script. Download Zaparoo Core for Batocera from the [Downloads page](/downloads/) and unzip it. It comes with two files: - `zaparoo` - The main Zaparoo Core executable - `zaparoo_service` - A service script that runs Zaparoo Core on startup ## Network Share 1. Copy the `zaparoo` file into the `system` directory of your Batocera share (`\\BATOCERA\share\system` on Windows) 2. Create a `services` directory inside the `system` directory if it doesn't exist 3. Copy the `zaparoo_service` file into the `services` directory 4. Enable the service: - Press `Start` to open the main menu - Navigate to `System Settings` > `Services` (in the Advanced section) - Find `zaparoo_service` and enable it 5. Restart Batocera ## USB 1. Copy both files to a USB drive and plug it into your Batocera device 2. Press `F1` to open the file manager and navigate to the USB drive 3. Copy the `zaparoo` file into `/userdata/system` 4. Create a `services` directory inside `/userdata/system` if it doesn't exist 5. Copy the `zaparoo_service` file into the `services` directory 6. Enable the service: - Press `Start` to open the main menu - Navigate to `System Settings` > `Services` (in the Advanced section) - Find `zaparoo_service` and enable it 7. Restart Batocera ## SSH 1. Copy the `zaparoo` file to `/userdata/system` using `scp` or [WinSCP](https://winscp.net/) 2. SSH into your device and run: ```bash cd /userdata/system chmod +x ./zaparoo ./zaparoo -install batocera-services enable zaparoo_service batocera-services start zaparoo_service ``` :::tip Not familiar with SSH? See Batocera's [SSH guide](https://wiki.batocera.org/access_the_batocera_via_ssh). Default credentials are `root` / `linux`. ::: --- ## Launchers(Batocera) Batocera uses EmulationStation as its primary launcher. Games are stored in `/userdata/roms//` and automatically detected during media indexing. ## EmulationStation The built-in EmulationStation launcher supports all systems listed below. Each system maps to one or more folders in `/userdata/roms/`. ### Supported Systems | System ID | Folders | Extensions | |-----------|---------|------------| | `3DO` | `3do` | `.iso`, `.chd`, `.cue` | | `3DS` | `3ds` | `.3ds`, `.cci`, `.cxi`, `.squashfs`, `.zcci` | | `Amiga` | `amigacdtv` | `.bin`, `.cue`, `.iso`, `.chd`, `.m3u` | | `Amiga500` | `amiga500` | `.adf`, `.uae`, `.ipf`, `.dms`, `.dmz`, `.adz`, `.lha`, `.hdf`, `.exe`, `.m3u`, `.zip` | | `Amiga1200` | `amiga1200` | `.adf`, `.uae`, `.ipf`, `.dms`, `.dmz`, `.adz`, `.lha`, `.hdf`, `.exe`, `.m3u`, `.zip` | | `AmigaCD32` | `amigacd32` | `.bin`, `.cue`, `.iso`, `.chd` | | `Amstrad` | `amstradcpc`, `gx4000` | `.dsk`, `.sna`, `.tap`, `.cdt`, `.voc`, `.m3u`, `.zip`, `.7z`, `.cpr` | | `AcornAtom` | `atom` | `.wav`, `.tap`, `.csw`, `.uef`, `.mfi`, `.dfi`, `.hfe`, `.mfm`, `.td0`, `.imd`, `.d77`, `.d88`, `.1dd`, `.cqm`, `.cqi`, `.dsk`, `.40t`, `.atm`, `.bin`, `.rom`, `.zip`, `.7z` | | `AcornElectron` | `electron` | `.wav`, `.csw`, `.uef`, `.mfi`, `.dfi`, `.hfe`, `.mfm`, `.td0`, `.imd`, `.d77`, `.d88`, `.1dd`, `.cqm`, `.cqi`, `.dsk`, `.ssd`, `.bbc`, `.img`, `.dsd`, `.adf`, `.ads`, `.adm`, `.adl`, `.rom`, `.bin`, `.zip`, `.7z` | | `AdventureVision` | `advision` | `.bin`, `.zip`, `.7z` | | `AppleII` | `apple2`, `apple2gs` | `.nib`, `.do`, `.po`, `.dsk`, `.mfi`, `.dfi`, `.rti`, `.edd`, `.woz`, `.wav`, `.zip`, `.7z` | | `Arcade` | `arcade`, `mame`, `fbneo`, `apfm1000`, `cannonball`, `cave3rd`, `gong`, `systemsp` | `.zip`, `.7z`, `.cannonball`, `.game` | | `Arcadia` | `arcadia` | `.bin`, `.zip`, `.7z` | | `Archimedes` | `archimedes` | `.mfi`, `.dfi`, `.hfe`, `.mfm`, `.td0`, `.imd`, `.d77`, `.d88`, `.1dd`, `.cqm`, `.cqi`, `.dsk`, `.ima`, `.img`, `.ufi`, `.360`, `.ipf`, `.adf`, `.apd`, `.jfd`, `.ads`, `.adm`, `.adl`, `.ssd`, `.bbc`, `.dsd`, `.st`, `.msa`, `.chd`, `.zip`, `.7z` | | `Arduboy` | `arduboy` | `.hex`, `.zip`, `.7z` | | `Astrocade` | `astrocde` | `.bin`, `.zip`, `.7z` | | `Atari2600` | `atari2600` | `.a26`, `.bin`, `.zip`, `.7z` | | `Atari5200` | `atari5200` | `.rom`, `.xfd`, `.atr`, `.atx`, `.cdm`, `.cas`, `.car`, `.bin`, `.a52`, `.xex`, `.zip`, `.7z` | | `Atari7800` | `atari7800` | `.a78`, `.bin`, `.zip`, `.7z` | | `Atari800` | `atari800` | `.rom`, `.xfd`, `.atr`, `.atx`, `.cdm`, `.cas`, `.car`, `.bin`, `.a52`, `.xex`, `.zip`, `.7z` | | `AtariLynx` | `lynx` | `.lnx`, `.zip`, `.7z` | | `AtariST` | `atarist` | `.st`, `.msa`, `.stx`, `.dim`, `.ipf`, `.m3u`, `.zip`, `.7z` | | `AtariXEGS` | `xegs` | `.atr`, `.dsk`, `.xfd`, `.bin`, `.rom`, `.car`, `.zip`, `.7z` | | `Atomiswave` | `atomiswave` | `.lst`, `.bin`, `.dat`, `.zip`, `.7z` | | `Audio` | `vgmplay` | `.vgm`, `.vgz` | | `BBCMicro` | `bbc` | `.mfi`, `.dfi`, `.hfe`, `.mfm`, `.td0`, `.imd`, `.d77`, `.d88`, `.1dd`, `.cqm`, `.cqi`, `.dsk`, `.ima`, `.img`, `.ufi`, `.360`, `.ipf`, `.ssd`, `.bbc`, `.dsd`, `.adf`, `.ads`, `.adm`, `.adl`, `.fsd`, `.wav`, `.tap`, `.bin`, `.zip`, `.7z` | | `C16` | `cplus4` | `.d64`, `.prg`, `.tap`, `.m3u`, `.zip`, `.7z` | | `C64` | `c64`, `c128` | `.d64`, `.d81`, `.crt`, `.prg`, `.tap`, `.t64`, `.lnx`, `.m3u`, `.zip`, `.7z` | | `CasioPV1000` | `pv1000` | `.bin`, `.zip`, `.7z` | | `CDI` | `cdi` | `.chd`, `.cue`, `.toc`, `.nrg`, `.gdi`, `.iso`, `.cdr` | | `ChannelF` | `channelf` | `.zip`, `.rom`, `.bin`, `.chf` | | `Chihiro` | `chihiro` | `.chd` | | `CoCo2` | `coco` | `.wav`, `.cas`, `.ccc`, `.rom`, `.zip`, `.7z` | | `ColecoAdam` | `adam` | `.wav`, `.ddp`, `.mfi`, `.dfi`, `.hfe`, `.mfm`, `.td0`, `.imd`, `.d77`, `.d88`, `.1dd`, `.cqm`, `.cqi`, `.dsk`, `.rom`, `.col`, `.bin`, `.zip`, `.7z` | | `ColecoVision` | `colecovision` | `.bin`, `.col`, `.rom`, `.zip`, `.7z` | | `CommanderX16` | `commanderx16` | `.prg`, `.crt`, `.bin`, `.zip` | | `CreatiVision` | `crvision` | `.bin`, `.rom`, `.zip`, `.7z` | | `DAPHNE` | `daphne` | `.daphne`, `.squashfs` | | `DICE` | `dice` | `.zip`, `.dmy` | | `DOS` | `dos`, `abuse`, `bstone`, `catacomb`, `cdogs`, `cgenius`, `dxx-rebirth`, `ecwolf`, `eduke32`, `fury`, `gzdoom`, `prboom`, `raze`, `rott`, `tyrian` | `.pc`, `.dos`, `.zip`, `.squashfs`, `.dosz`, `.m3u`, `.iso`, `.cue`, `.bstone`, `.game`, `.d1x`, `.d2x`, `.ecwolf`, `.pk3`, `.eduke32`, `.grp`, `.wad`, `.iwad`, `.pwad`, `.gzdoom`, `.raze`, `.rott` | | `Dreamcast` | `dreamcast` | `.cdi`, `.cue`, `.gdi`, `.chd`, `.m3u` | | `FDS` | `fds` | `.fds`, `.zip`, `.7z` | | `FM7` | `fm7` | `.wav`, `.t77`, `.mfi`, `.dfi`, `.hfe`, `.mfm`, `.td0`, `.imd`, `.d77`, `.d88`, `.1dd`, `.cqm`, `.cqi`, `.dsk`, `.zip`, `.7z` | | `FMTowns` | `fmtowns` | `.bin`, `.m3u`, `.cue`, `.d88`, `.d77`, `.xdf`, `.iso`, `.chd`, `.toc`, `.nrg`, `.gdi`, `.cdr`, `.mfi`, `.dfi`, `.hfe`, `.mfm`, `.td0`, `.imd`, `.1dd`, `.cqm`, `.cqi`, `.dsk`, `.zip`, `.7z` | | `Gaelco` | `gaelco` | `.zip` | | `Gamate` | `gamate` | `.bin`, `.zip`, `.7z` | | `GameCom` | `gamecom` | `.bin`, `.tgc`, `.zip`, `.7z` | | `GameCube` | `gamecube` | `.gcm`, `.iso`, `.gcz`, `.ciso`, `.wbfs`, `.rvz`, `.elf`, `.dol`, `.m3u` | | `GameGear` | `gamegear` | `.bin`, `.gg`, `.zip`, `.7z` | | `GameMaster` | `gmaster` | `.bin`, `.zip`, `.7z` | | `GameNWatch` | `gameandwatch`, `lcdgames` | `.mgw`, `.zip`, `.7z` | | `GamePocket` | `gamepock` | `.bin`, `.zip`, `.7z` | | `Gameboy` | `gb` | `.gb`, `.zip`, `.7z` | | `Gameboy2P` | `gb2players`, `gbc2players` | `.gb`, `.gb2`, `.gbc2`, `.gbc`, `.zip`, `.7z` | | `GameboyColor` | `gbc` | `.gbc`, `.zip`, `.7z` | | `GBA` | `gba` | `.gba`, `.zip`, `.7z` | | `Genesis` | `megadrive`, `pico`, `sonic3-air`, `sonicretro` | `.bin`, `.gen`, `.md`, `.sg`, `.smd`, `.zip`, `.7z`, `.sonic3air`, `.sonicretro` | | `GenesisMSU` | `msu-md` | `.msu`, `.md` | | `GP32` | `gp32` | `.smc`, `.zip`, `.7z` | | `Hikaru` | `hikaru` | `.chd`, `.zip` | | `Image` | `imageviewer` | `.jpg`, `.png`, `.gif`, `.bmp` | | `Intellivision` | `intellivision` | `.int`, `.bin`, `.rom`, `.zip`, `.7z` | | `J2ME` | `j2me` | `.jar` | | `Jaguar` | `jaguar` | `.cue`, `.j64`, `.jag`, `.cof`, `.abs`, `.cdi`, `.rom`, `.zip`, `.7z` | | `JaguarCD` | `jaguarcd` | `.cue`, `.chd` | | `Laser` | `laser310` | `.vz`, `.wav`, `.cas`, `.zip`, `.7z` | | `Lindbergh` | `lindbergh` | `.zip` | | `Lynx48` | `camplynx` | `.wav`, `.tap`, `.zip`, `.7z` | | `MacOS` | `macintosh` | `.dsk`, `.zip`, `.7z`, `.mfi`, `.dfi`, `.hfe`, `.mfm`, `.td0`, `.imd`, `.d77`, `.d88`, `.1dd`, `.cqm`, `.cqi`, `.ima`, `.img`, `.ufi`, `.ipf`, `.dc42`, `.woz`, `.2mg`, `.360`, `.chd`, `.cue`, `.toc`, `.nrg`, `.gdi`, `.iso`, `.cdr`, `.hd`, `.hdv`, `.hdi` | | `MasterSystem` | `mastersystem` | `.bin`, `.sms`, `.zip`, `.7z` | | `MegaCD` | `megacd` | `.cue`, `.iso`, `.chd`, `.m3u` | | `MegaDuck` | `megaduck` | `.bin`, `.zip`, `.7z` | | `Model1` | `model1` | `.zip` | | `Model2` | `model2` | `.zip` | | `Model3` | `model3` | `.zip` | | `MSX` | `msx1`, `msx2`, `msxturbor` | `.dsk`, `.mx1`, `.mx2`, `.rom`, `.zip`, `.7z`, `.cas`, `.m3u` | | `MSX2Plus` | `msx2+` | `.dsk`, `.mx2`, `.rom`, `.zip`, `.7z`, `.cas`, `.m3u` | | `Multivision` | `multivision` | `.bin`, `.gg`, `.rom`, `.sg`, `.sms`, `.zip` | | `NAOMI` | `naomi` | `.lst`, `.bin`, `.dat`, `.zip`, `.7z` | | `NAOMI2` | `naomi2` | `.zip`, `.7z` | | `Namco22` | `namco22` | `.zip` | | `Namco2X6` | `namco2x6` | `.zip` | | `NDS` | `nds` | `.nds`, `.bin`, `.zip`, `.7z` | | `NeoGeo` | `neogeo` | `.7z`, `.zip` | | `NeoGeoCD` | `neogeocd` | `.cue`, `.iso`, `.chd` | | `NeoGeoPocket` | `ngp` | `.ngp`, `.zip`, `.7z` | | `NeoGeoPocketColor` | `ngpc` | `.ngc`, `.zip`, `.7z` | | `NES` | `nes` | `.nes`, `.unif`, `.unf`, `.zip`, `.7z` | | `NGage` | `ngage` | `.ngage`, `.jar` | | `Nintendo64` | `n64`, `n64dd` | `.z64`, `.n64`, `.v64`, `.zip`, `.7z`, `.z64.ndd` | | `Odyssey2` | `o2em` | `.bin`, `.zip`, `.7z` | | `Oric` | `oricatmos` | `.dsk`, `.tap` | | `PC` | `bennugd`, `cavestory`, `corsixth`, `devilutionx`, `doom3`, `easyrpg`, `etlegacy`, `flash`, `hcl`, `hurrican`, `iortcw`, `jazz2`, `jkdf2`, `jknight`, `library`, `lowresnx`, `lutro`, `mohaa`, `moonlight`, `mrboom`, `odcommander`, `openjazz`, `ports`, `pygame`, `quake`, `quake2`, `quake3`, `reminiscence`, `sdlpop`, `solarus`, `sonic-mania`, `superbroswar`, `theforceengine`, `thextech`, `traider1`, `traider2`, `tyrquake`, `uqm`, `uzebox`, `vemulator`, `vircon32`, `vis`, `wasm4`, `flatpak`, `steam`, `xash3d_fwgs`, `xrick`, `zc210`, `pyxel` | Various | | `PC88` | `pc88` | `.d88`, `.u88`, `.m3u` | | `PC98` | `pc98` | `.d98`, `.zip`, `.98d`, `.fdi`, `.fdd`, `.2hd`, `.tfd`, `.d88`, `.88d`, `.hdm`, `.xdf`, `.dup`, `.cmd`, `.hdi`, `.thd`, `.nhd`, `.hdd`, `.hdn`, `.m3u` | | `PCFX` | `pcfx` | `.cue`, `.ccd`, `.toc`, `.chd`, `.zip`, `.7z` | | `PDP1` | `pdp1` | `.zip`, `.7z`, `.tap`, `.rim`, `.drm` | | `PET2001` | `pet` | `.a0`, `.b0`, `.crt`, `.d64`, `.d81`, `.prg`, `.tap`, `.t64`, `.m3u`, `.zip`, `.7z` | | `Pico8` | `pico8` | `.p8`, `.png`, `.m3u` | | `PlugNPlay` | `plugnplay` | `.game` | | `PokemonMini` | `pokemini` | `.min`, `.zip`, `.7z` | | `PS2` | `ps2` | `.iso`, `.mdf`, `.nrg`, `.bin`, `.img`, `.dump`, `.gz`, `.cso`, `.chd`, `.m3u` | | `PS3` | `ps3` | `.ps3`, `.psn`, `.squashfs` | | `PS4` | `ps4` | `.ps4` | | `PSP` | `psp` | `.iso`, `.cso`, `.pbp`, `.chd` | | `PSX` | `psx` | `.cue`, `.img`, `.mdf`, `.pbp`, `.toc`, `.cbn`, `.m3u`, `.ccd`, `.chd`, `.iso` | | `SAMCoupe` | `samcoupe` | `.cpm`, `.dsk`, `.sad`, `.mgt`, `.sdf`, `.td0`, `.sbt`, `.zip` | | `Saturn` | `saturn` | `.cue`, `.ccd`, `.m3u`, `.chd`, `.iso`, `.zip` | | `ScummVM` | `scummvm` | `.scummvm`, `.squashfs` | | `Sega32X` | `sega32x` | `.32x`, `.chd`, `.smd`, `.bin`, `.md`, `.zip`, `.7z` | | `SG1000` | `sg1000`, `scv` | `.bin`, `.sg`, `.zip`, `.7z`, `.0` | | `SGBMSU1` | `sgb-msu1` | `.gb`, `.gbc`, `.zip`, `.7z` | | `Singe` | `singe` | `.singe` | | `SNES` | `snes`, `satellaview` | `.smc`, `.fig`, `.sfc`, `.gd3`, `.gd7`, `.dx2`, `.bsx`, `.swc`, `.zip`, `.7z`, `.bs` | | `SNESMSU1` | `snes-msu1` | `.smc`, `.fig`, `.sfc`, `.gd3`, `.gd7`, `.dx2`, `.bsx`, `.swc`, `.zip`, `.7z`, `.squashfs` | | `Socrates` | `socrates` | `.bin`, `.zip` | | `Spectravideo` | `spectravideo` | `.cas`, `.rom`, `.ri`, `.mx1`, `.mx2`, `.dsk`, `.zip` | | `Sufami` | `sufami` | `.st`, `.zip` | | `SuperACan` | `supracan` | `.bin`, `.zip` | | `SuperGameboy` | `sgb` | `.gb`, `.gbc`, `.zip`, `.7z` | | `SuperGrafx` | `supergrafx` | `.pce`, `.sgx`, `.cue`, `.ccd`, `.chd`, `.zip`, `.7z` | | `SuperVision` | `supervision` | `.sv`, `.zip`, `.7z` | | `Switch` | `switch` | `.xci`, `.nsp` | | `Thomson` | `thomson` | `.fd`, `.sap`, `.k7`, `.m7`, `.m5`, `.rom`, `.zip` | | `TI994A` | `ti99` | `.rpk`, `.wav`, `.zip`, `.7z` | | `TIC80` | `tic80` | `.tic` | | `TomyTutor` | `tutor` | `.bin`, `.wav`, `.zip`, `.7z` | | `Triforce` | `triforce` | `.iso`, `.gcz` | | `TurboGrafx16` | `pcengine` | `.pce`, `.bin`, `.zip`, `.7z` | | `TurboGrafx16CD` | `pcenginecd` | `.pce`, `.cue`, `.ccd`, `.iso`, `.img`, `.chd` | | `VC4000` | `vc4000` | `.bin`, `.rom`, `.pgm`, `.tvc`, `.zip`, `.7z` | | `Vectrex` | `vectrex` | `.bin`, `.gam`, `.vec`, `.zip`, `.7z` | | `VIC20` | `c20` | `.a0`, `.b0`, `.crt`, `.d64`, `.d81`, `.prg`, `.tap`, `.t64`, `.m3u`, `.zip`, `.7z` | | `Video` | `recordings` | `.mp4`, `.avi`, `.mkv` | | `VideopacPlus` | `videopacplus` | `.bin`, `.zip` | | `VirtualBoy` | `virtualboy` | `.vb`, `.zip`, `.7z` | | `Vita` | `psvita` | `.zip`, `.psvita` | | `VSmile` | `vsmile` | `.zip`, `.7z` | | `Wii` | `wii` | `.gcm`, `.iso`, `.gcz`, `.ciso`, `.wbfs`, `.wad`, `.rvz`, `.elf`, `.dol`, `.m3u`, `.json` | | `WiiU` | `wiiu` | `.wua`, `.wup`, `.wud`, `.wux`, `.rpx`, `.squashfs`, `.wuhb` | | `Windows` | `wine`, `windows`, `windows_installers`, `fallout1-ce`, `fallout2-ce` | `.wine`, `.exe`, `.pc`, `.wsquashfs`, `.wtgz`, `.iso`, `.msi`, `.f1ce`, `.f2ce` | | `WonderSwan` | `wswan` | `.ws`, `.zip`, `.7z` | | `WonderSwanColor` | `wswanc` | `.wsc`, `.zip`, `.7z` | | `X1` | `x1` | `.dx1`, `.zip`, `.2d`, `.2hd`, `.tfd`, `.d88`, `.88d`, `.hdm`, `.xdf`, `.dup`, `.cmd`, `.7z` | | `X68000` | `x68000` | `.dim`, `.img`, `.d88`, `.88d`, `.hdm`, `.dup`, `.2hd`, `.xdf`, `.hdf`, `.cmd`, `.m3u`, `.zip`, `.7z` | | `Xbox` | `xbox` | `.iso`, `.squashfs` | | `Xbox360` | `xbox360` | `.iso`, `.xex`, `.xbox360`, `.zar` | | `ZX81` | `zx81` | `.tzx`, `.p`, `.zip`, `.7z` | | `ZXSpectrum` | `zxspectrum` | `.tzx`, `.tap`, `.z80`, `.rzx`, `.scl`, `.trd`, `.dsk`, `.zip`, `.7z` | ### Additional Game Engines These folders run games through standalone game engines: | Folder | Description | Extensions | |--------|-------------|------------| | `ikemen` | Ikemen GO fighting game engine | `.ikemen` | | `mugen` | M.U.G.E.N fighting game engine | `.mugen` | | `openbor` | OpenBOR beat 'em up engine | `.pak` | | `vpinball` | Visual Pinball | `.vpx`, `.vpt` | ## Kodi Media playback through Kodi is available when the Kodi JSON-RPC API is enabled. Kodi must be running for these launchers to work. ### Local Files These launchers play files directly from disk: | System ID | Folders | Extensions | |-----------|---------|------------| | `Video` | `videos`, `tvshows` | `.avi`, `.mp4`, `.mkv`, `.iso`, `.bdmv`, `.ifo`, `.mpeg`, `.mpg`, `.mov`, `.wmv`, `.flv`, `.webm`, `.m4v`, `.3gp`, `.ts`, `.m2ts`, `.mts`, `.m3u`, `.m3u8` | | `MusicTrack` | `music` | `.mp3`, `.flac`, `.ogg`, `.m4a`, `.wav`, `.wma`, `.aac`, `.opus` | ### Library Media These launchers play media from Kodi's indexed library. They don't scan filesystem folders - instead they query Kodi's database during media indexing. | System ID | Description | |-----------|-------------| | `Movie` | Movies from Kodi library | | `TVEpisode` | Individual TV episodes | | `TVShow` | Entire TV show (plays next unwatched episode) | | `MusicTrack` | Songs from Kodi music library | | `MusicAlbum` | Albums from Kodi music library | | `MusicArtist` | Artists from Kodi music library (plays all songs) | ### Configuration Configure the Kodi server URL in `config.toml`: ```toml [[launchers.default]] launcher = "Kodi" server_url = "http://localhost:8080" ``` If Kodi requires authentication, add credentials to `auth.toml`: ```toml ["http://localhost:8080"] username = "kodi" password = "your_password" ``` See [LibreELEC](../libreelec.md) for detailed Kodi API setup instructions. ## Shell Scripts Custom shell scripts (`.sh` files) can be launched directly. Scripts must be added to the `allow_file` list in `config.toml`. ```toml [launchers] allow_file = [ "^/userdata/roms/.*\\.sh$" ] ``` Restart Zaparoo after modifying the config for changes to take effect. --- ## Bazzite :::warning[Beta] Bazzite support is currently in beta. Some features may not work as expected. ::: Zaparoo Core on [Bazzite](https://bazzite.gg/) supports launching Steam games and shell scripts. Release builds are available for both AMD64 and ARM64 architectures. ## File Paths | Item | Path | | ------------------ | -------------------------------------- | | Config file | `~/.config/zaparoo/config.toml` | | Data directory | `~/.local/share/zaparoo` | | Log file | `/tmp/-zaparoo/core.log` | | Mappings directory | `~/.local/share/zaparoo/mappings` | Where `~` is the home directory of the current user. ## Install Open a terminal and run: ```bash curl -fsSL https://zaparoo.org/install.sh | bash ``` This installs all necessary components and sets up the service to run on startup. Since Bazzite has an immutable root filesystem, the installation is user-local (installed to `~/.local/bin`). To uninstall: ```bash ~/.local/bin/zaparoo -uninstall application ~/.local/bin/zaparoo -uninstall service ~/.local/bin/zaparoo -uninstall desktop sudo ~/.local/bin/zaparoo -uninstall hardware ``` ## Readers ## Launchers | Launcher | Systems | Notes | | ------------- | ------- | -------------------------------------- | | Steam | PC | Native and Flatpak Steam installations | | Kodi | Media | Videos, movies, TV shows, music | | Web Browser | N/A | Opens URLs in default browser | | Shell Scripts | Any | Custom `.sh` file execution | ### Steam Launches games from your Steam library. Both native (pre-installed) and Flatpak Steam installations are automatically detected. Games are launched using `xdg-open`, which works in both desktop and gaming modes. Games are indexed from: - `~/.steam/steam/steamapps/` - `~/.local/share/Steam/` To manually launch a Steam game, write `steam://` to a token. ### Kodi Launch media content from a [Kodi](https://kodi.tv/) instance running on your network. Supported content: - Video and audio files - Movies and TV episodes - Music (songs, albums, artists) ```toml title="config.toml" [[launchers.default]] launcher = "Kodi" server_url = "http://kodi:8080" ``` Replace `kodi:8080` with your Kodi instance's address and web interface port. ### Shell Scripts Execute arbitrary shell scripts. Requires explicit allow list configuration for security. ```toml title="config.toml" [launchers] allow_file = [ "^/home/.*/scripts/.*\\.sh$" ] ``` --- ## ChimeraOS :::warning[Beta] ChimeraOS support is currently in beta. Some features may not work as expected. ::: Zaparoo Core on [ChimeraOS](https://chimeraos.org/) supports launching Steam games, GOG games via the Chimera web app, and shell scripts. Release builds are available for both AMD64 and ARM64 architectures. ## File Paths | Item | Path | | ------------------ | -------------------------------------- | | Config file | `~/.config/zaparoo/config.toml` | | Data directory | `~/.local/share/zaparoo` | | Log file | `/tmp/-zaparoo/core.log` | | Mappings directory | `~/.local/share/zaparoo/mappings` | Where `~` is the home directory of the `gamer` user. ## Install Access the terminal with `Ctrl+Alt+F2` (default credentials: `gamer` / `gamer`) and run: ```bash curl -fsSL https://zaparoo.org/install.sh | bash ``` This installs all necessary components and sets up the service to run on startup. Since ChimeraOS has an immutable root filesystem, the installation is user-local (installed to `~/.local/bin`). To uninstall: ```bash ~/.local/bin/zaparoo -uninstall application ~/.local/bin/zaparoo -uninstall service ~/.local/bin/zaparoo -uninstall desktop sudo ~/.local/bin/zaparoo -uninstall hardware ``` ## Readers ## Launchers | Launcher | Systems | Notes | | ------------- | ------- | ----------------------------------- | | Steam | PC | Games and non-Steam shortcuts | | ChimeraGOG | PC | GOG games from Chimera web app | | Kodi | Media | Videos, movies, TV shows, music | | Shell Scripts | Any | Custom `.sh` file execution | ### Steam Launches games from your Steam library. Both official Steam games and non-Steam shortcuts added to your library are detected. Games are indexed from: - `~/.steam/steam/steamapps/` - `~/.local/share/Steam/` To manually launch a Steam game, write `steam://` to a token. ### ChimeraGOG Zaparoo automatically indexes GOG games installed through the Chimera web app. Games are indexed from: - `~/.local/share/chimera/content/gog/` Games are launched using the `gog://` scheme. ### Kodi Launch media content from a [Kodi](https://kodi.tv/) instance running on your network. Supported content: - Video and audio files - Movies and TV episodes - Music (songs, albums, artists) ```toml title="config.toml" [[launchers.default]] launcher = "Kodi" server_url = "http://kodi:8080" ``` Replace `kodi:8080` with your Kodi instance's address and web interface port. ### Shell Scripts Execute arbitrary shell scripts. Requires explicit allow list configuration for security. ```toml title="config.toml" [launchers] allow_file = [ "^/home/gamer/scripts/.*\\.sh$" ] ``` --- ## Commodore 64 The Commodore 64 is not a directly supported platform for Zaparoo Core, but Zaparoo tokens can be used with a real Commodore 64 via the [TeensyROM project](https://github.com/SensoriumEmbedded/TeensyROM/). TeensyROM is a custom cartridge for the Commodore 64 that supports the [PN532 NFC reader](../readers/nfc/pn532-usb.md). See the [NFC guide](https://github.com/SensoriumEmbedded/TeensyROM/blob/main/docs/NFC_Loader.md) on the TeensyROM GitHub for setup instructions. Video by [What's Ken Making](https://www.youtube.com/@whatskenmaking). --- ## Platforms Zaparoo is available on multiple platforms. If you're not sure where to start, the [getting started guide](/start/) can walk you through picking the right platform and reader for your setup. ## Base OS ## FPGA Gaming ## Retro Gaming ## Gaming Handhelds ## Media Centers ## Retro Hardware --- *Trademarks shown on this page are property of their respective owners. The Zaparoo project uses logos on this page in good faith and/or within published usage guidelines, to show compatibility with the respective operating system they represent. The Zaparoo project is not affiliated with nor receives endorsement from any of the projects or companies shown above.* --- ## LibreELEC :::warning LibreELEC support is in beta. Launching and readers are supported, but requires manual setup. ::: Zaparoo Core on LibreELEC provides Kodi integration for movies, TV shows, and music. Also works on [CoreELEC](https://coreelec.org/) and other Kodi-based systems. ## File Paths | Item | Path | | ------------------ | ---------------------------------------- | | Config file | `/storage/.config/zaparoo/config.toml` | | Data directory | `/storage/.local/share/zaparoo` | | Log file | `/tmp/zaparoo/core.log` | | Mappings directory | `/storage/.local/share/zaparoo/mappings` | The config file can be accessed through the SMB share in the `Configfiles` folder. ## Install Download Zaparoo Core for LibreELEC from the [Downloads page](/downloads/), unzip it and copy the `zaparoo` file to `/storage`. Enable SSH in LibreELEC settings (Services > SSH Server). Default credentials: `root` / `libreelec`. ```bash cd /storage ./zaparoo -service start ``` ### Kodi API Setup Enable Kodi remote control for Zaparoo to work: 1. Open Settings > Services 2. Change view setting to at least "Standard" 3. Open the Control page 4. Enable "Allow remote control via HTTP" 5. Set a blank password 6. Enable "Allow remote control from applications on this system" ### Adding to Startup Add to `/storage/.config/autostart.sh`: ```bash /storage/zaparoo -service start ``` ## Readers ## Launchers LibreELEC uses Kodi as its primary launcher. All media is played through the Kodi JSON-RPC API. ### Local Files These launchers play files directly from disk in `/storage/`: | System ID | Folders | Extensions | |-----------|---------|------------| | `Video` | `videos`, `tvshows` | `.avi`, `.mp4`, `.mkv`, `.iso`, `.bdmv`, `.ifo`, `.mpeg`, `.mpg`, `.mov`, `.wmv`, `.flv`, `.webm`, `.m4v`, `.3gp`, `.ts`, `.m2ts`, `.mts`, `.m3u`, `.m3u8` | | `MusicTrack` | `music` | `.mp3`, `.flac`, `.ogg`, `.m4a`, `.wav`, `.wma`, `.aac`, `.opus` | ### Library Media These launchers play media from Kodi's indexed library. They query Kodi's database during media indexing rather than scanning the filesystem. | System ID | Description | |-----------|-------------| | `Movie` | Movies from Kodi library | | `TVEpisode` | Individual TV episodes | | `TVShow` | Entire TV show (plays next unwatched episode) | | `MusicTrack` | Songs from Kodi music library | | `MusicAlbum` | Albums from Kodi music library | | `MusicArtist` | Artists from Kodi music library (plays all songs) | ### Shell Scripts Custom shell scripts (`.sh` files) can be launched directly. Scripts must be added to the `allow_file` list in `config.toml`. ```toml [launchers] allow_file = [ "^/storage/.*\\.sh$" ] ``` Restart Zaparoo after modifying the config for changes to take effect. --- ## Linux Zaparoo Core on Linux provides desktop integration with support for Steam game launching. This platform serves as the foundation for other Linux-based platforms. ## File Paths | Item | Path | | ------------------ | -------------------------------------- | | Config file | `~/.config/zaparoo/config.toml` | | Data directory | `~/.local/share/zaparoo` | | Log file | `~/.local/share/zaparoo/logs/core.log` | | Mappings directory | `~/.local/share/zaparoo/mappings` | Where `~` is the home directory of the current user. ## Install Open a terminal and run: ```bash curl -fsSL https://zaparoo.org/install.sh | bash ``` This installs all necessary components and sets up the service to run on startup. Once running, use the [Zaparoo App](/docs/app/) on your phone or the built-in web UI to manage your setup and write tokens. For manual component-based installation, see [Manual Install](./install.md). ## Readers ## Launchers | Launcher | Description | |----------|-------------| | Steam | Steam games and non-Steam shortcuts | | Kodi | Movies, TV, Music (requires Kodi API) | | Web Browser | Opens URLs in default browser | | Shell Scripts | Custom `.sh` execution (allowlist required) | See [Launchers](./launchers.md) for full details and configuration. --- ## Manual Install(Linux) Component-based manual installation for Linux. Download Zaparoo Core for Linux from the [Downloads page](/downloads/), unzip it and copy the `zaparoo` file somewhere like your home directory or `/usr/local/bin`. ## Component Installation Zaparoo supports installing individual components: ```bash ./zaparoo -install application # Installs the application binary ./zaparoo -install desktop # Installs desktop integration ./zaparoo -install service # Installs systemd service ./zaparoo -install hardware # Installs udev rules and hardware support ``` For a complete installation, run all four commands. The `hardware` component creates: - A udev rule allowing users to read NFC reader serial devices - A modprobe blacklist entry to fix ACR122U reader issues ## Starting the Service ```bash systemctl --user enable zaparoo.service systemctl --user start zaparoo.service ``` ## Uninstalling To uninstall components: ```bash ./zaparoo -uninstall application ./zaparoo -uninstall desktop ./zaparoo -uninstall service ./zaparoo -uninstall hardware ``` --- ## Launchers(Linux) Linux supports multiple game launchers, Kodi media playback, web browser URLs, and shell scripts. ## Steam Scans Steam library for installed games and non-Steam shortcuts. Works with native Steam, Flatpak, and Snap installations. | System ID | Scheme | |-----------|--------| | `PC` | `steam://` | Auto-detected paths: - `~/.steam/steam` - `~/.local/share/Steam` - `~/.var/app/com.valvesoftware.Steam/.steam/steam` (Flatpak) - `~/snap/steam/common/.steam/steam` (Snap) To specify a custom Steam installation directory: ```toml [[launchers.default]] launcher = "Steam" install_dir = "/path/to/steam" ``` ## Kodi Media playback through Kodi is available when the Kodi JSON-RPC API is enabled. ### Local Files These launchers play files directly from disk: | System ID | Folders | Extensions | |-----------|---------|------------| | `Video` | `videos`, `tvshows` | `.avi`, `.mp4`, `.mkv`, `.iso`, `.bdmv`, `.ifo`, `.mpeg`, `.mpg`, `.mov`, `.wmv`, `.flv`, `.webm`, `.m4v`, `.3gp`, `.ts`, `.m2ts`, `.mts`, `.m3u`, `.m3u8` | | `MusicTrack` | `music` | `.mp3`, `.flac`, `.ogg`, `.m4a`, `.wav`, `.wma`, `.aac`, `.opus` | ### Library Media These launchers play media from Kodi's indexed library: | System ID | Description | |-----------|-------------| | `Movie` | Movies from Kodi library | | `TVEpisode` | Individual TV episodes | | `TVShow` | Entire TV show (plays next unwatched episode) | | `MusicTrack` | Songs from Kodi music library | | `MusicAlbum` | Albums from Kodi music library | | `MusicArtist` | Artists from Kodi music library (plays all songs) | ### Configuration Configure the Kodi server URL in `config.toml`: ```toml [[launchers.default]] launcher = "Kodi" server_url = "http://localhost:8080" ``` If Kodi requires authentication, add credentials to `auth.toml`: ```toml ["http://localhost:8080"] username = "kodi" password = "your_password" ``` See [LibreELEC](../libreelec.md) for detailed Kodi API setup instructions. ## Web Browser Opens URLs in the default system browser using `xdg-open`. | Scheme | Description | |--------|-------------| | `http://` | HTTP URLs | | `https://` | HTTPS URLs | ## Shell Scripts Custom shell scripts (`.sh` files) can be launched directly. Scripts must be added to the `allow_file` list in `config.toml`. ```toml [launchers] allow_file = [ "^/home/user/scripts/.*\\.sh$" ] ``` ### Example Script Create a launcher script for an emulator: ```bash #!/bin/bash # ~/scripts/launch_nes.sh EMULATOR="/usr/bin/fceux" GAME="$HOME/games/nes/SuperMarioBros.nes" "$EMULATOR" "$GAME" ``` Make executable and add to allowlist: ```bash chmod +x ~/scripts/launch_nes.sh ``` ```toml [launchers] allow_file = [ "^/home/user/scripts/launch_nes\\.sh$" ] ``` Restart Zaparoo after modifying the config for changes to take effect. --- ## macOS :::danger This platform is in beta and not ready for production use. ::: Zaparoo Core on macOS supports shell script launching. Full launcher support is coming in a future release. ## File Paths | Item | Path | | ------------------ | ------------------------------------------------ | | Config file | `~/Library/Preferences/zaparoo/config.toml` | | Data directory | `~/Library/Application Support/zaparoo` | | Log file | `~/Library/Logs/zaparoo/core.log` | | Mappings directory | `~/Library/Application Support/zaparoo/mappings` | Where `~` is the home directory of the current user. ## Install Download Zaparoo Core for macOS from the [Downloads page](/downloads/), unzip it and copy the `zaparoo` file to `/usr/local/bin`. ```bash cd /usr/local/bin ./zaparoo -install launchctl load ~/Library/LaunchAgents/org.zaparoo.core.plist ``` To uninstall: ```bash ./zaparoo -uninstall ``` ## Readers ## Supported Launchers | Launcher | Notes | | ------------- | ---------------------------- | | Shell Scripts | Custom `.sh` file execution | Create shell scripts to launch games, applications, or media. Add scripts to the `allow_file` list in config.toml and restart Zaparoo. --- ## MiSTer FPGA MiSTer is fully supported by Zaparoo and is where the project originally started. Zaparoo has several MiSTer-exclusive features because of this, including [MiSTer-specific ZapScript commands](../../zapscript/mister.md). ## File Paths | Item | Path | | ------------------ | -------------------------------- | | Config file | `/media/fat/zaparoo/config.toml` | | Data directory | `/media/fat/zaparoo` | | Log file | `/tmp/zaparoo/core.log` | | Mappings directory | `/media/fat/zaparoo/mappings` | The `/media/fat` directory is the top level of the SD card. The `/tmp` directory is not accessible from the SD card and is deleted when MiSTer is powered off. ## Install Zaparoo is available in [Update All](https://github.com/theypsilon/Update_All_MiSTer) as a dedicated item in **Tools & Scripts**. Update All can install Zaparoo Core and can also enable [Zaparoo Frontend](../../frontend/) in your `MiSTer.ini` file. Once Core is installed, run `zaparoo` from the MiSTer `Scripts` menu. A prompt will offer to enable Zaparoo as a startup service. For manual Core installation, download from the [Downloads page](/downloads/) and copy `zaparoo.sh` to the `Scripts` folder on your SD card. :::info Upgrading from TapTo? If you previously had TapTo installed, make sure the old `tapto.sh` is removed from your Scripts folder and that `linux/user-startup.sh` no longer references it. Having both services running simultaneously can cause double-launches and detection conflicts. Zaparoo is a direct replacement. Your existing NFC cards will continue to work. ::: ### Game Tracking Zaparoo can detect games launched outside of Zaparoo, like games started directly from the MiSTer menu. This is needed for [playtime tracking](../../features/play-controls.md#playtime-limits) to work correctly. It requires the `recents` setting in MiSTer's configuration. To enable it: 1. Open `MiSTer.ini` on your SD card (located at `/media/fat/MiSTer.ini`) 2. Look for a `recents=` line. If it exists, change it to `recents=1`. If there's no `recents` line, add `recents=1` to the file. 3. Save the file and reboot MiSTer This setting makes MiSTer write recent game data to the SD card each time a game is loaded. MiSTer.ini warns about the extra SD card writes, but it's not a real concern with modern SD cards. ## Readers ## Launchers MiSTer supports 100+ systems including consoles, computers, and arcade games. Games launch automatically using the official core folder structure from Downloader. See [Launchers](./launchers.md) for the full list of supported systems, file extensions, special features, and alternate cores. ## Main Forks Some MiSTer Main forks are available with Zaparoo integration or features that work well with Zaparoo. ### spark2k06 An alternative version of MiSTer Main by [spark2k06](https://aitorgomez.net/) adds many Zaparoo-related features: - Show status of connected reader as icon in top bar - Zaparoo standby screen - Box art on game load - Many additional MGL features See [spark2k06's repository](https://github.com/spark2k06/Main_MiSTer) for more details. ### Insert-Coin An alternative version of MiSTer Main by [funkycochise](https://github.com/funkycochise) as part of the [Insert-Coin project](https://github.com/funkycochise/Insert-Coin). This version hides the loading screen before cores start games, which works great with Zaparoo. ## Known Issues - Zaparoo can have conflicts with other devices that use serial USB connections such as the tty2oled project and anything else using an Arduino board. The workaround is to disable auto_detect in the config.toml file and manually set the reader path. ## FAQ **Which NFC reader should I use with MiSTer?** The [PN532 USB](../../readers/nfc/pn532-usb.md) reader is the recommended choice. It works out of the box with no configuration required. The [ACR122U](../../readers/nfc/acr122u.md) is also supported on MiSTer but is more prone to clone compatibility issues. **Do I need the app to use Zaparoo on MiSTer?** No. Once Zaparoo Core is installed, you can write tokens using the built-in TUI from the Scripts menu or the Web UI. The Zaparoo App makes things easier but is not required. **Will Zaparoo work with SNAC or through a USB hub?** Do not plug NFC readers into the SNAC/USER port on MiSTer. It looks like USB but it's not. Use a standard USB port or hub instead. USB hubs are fine! **Do my existing TapTo NFC cards work after upgrading to Zaparoo?** Yes. Zaparoo is a direct replacement for TapTo and all existing cards continue to work without being rewritten. --- ## Launchers(Mister) MiSTer FPGA uses a built-in launcher that automatically detects games from your games folders. Games launch via MGL files generated by Zaparoo, following the official folder structure set by Downloader. Zaparoo also indexes `.mgl` files placed directly in your games folders. Drop a custom MGL shortcut into a games folder and it gets picked up by the media database, so it shows up in browse and search and can be launched by title like any other game. ## Supported Systems ### Console Systems | System ID | Folders | Extensions | |-----------|---------|------------| | `3DO` | 3DO | `.iso`, `.cue`, `.chd` | | `AdventureVision` | AVision | `.bin` | | `Arcadia` | Arcadia | `.bin` | | `AmigaCD32` | AmigaCD32 | `.cue`, `.chd`, `.iso` | | `Astrocade` | Astrocade | `.bin` | | `Atari2600` | ATARI7800, Atari2600 | `.a26`, `.bin` | | `Atari5200` | ATARI5200 | `.car`, `.a52`, `.bin`, `.rom` | | `Atari7800` | ATARI7800 | `.a78`, `.bin` | | `AtariLynx` | AtariLynx | `.lnx` | | `CasioPV1000` | Casio_PV-1000 | `.bin` | | `CDI` | CD-i | `.cue`, `.chd` | | `ChannelF` | ChannelF | `.rom`, `.bin` | | `ColecoVision` | Coleco | `.col`, `.bin`, `.rom` | | `CreatiVision` | CreatiVision | `.rom`, `.bin`, `.bas` | | `FDS` | NES, FDS | `.fds` | | `Gamate` | Gamate | `.bin` | | `Gameboy` | GAMEBOY | `.gb` | | `GameboyColor` | GAMEBOY, GBC | `.gbc` | | `Gameboy2P` | GAMEBOY2P | `.gb`, `.gbc` | | `GameGear` | SMS, GameGear | `.gg` | | `GameGear2P` | GameGear2P | `.gg` | | `GameNWatch` | GameNWatch | `.bin` | | `GBA` | GBA | `.gba` | | `GBA2P` | GBA2P | `.gba` | | `Genesis` | MegaDrive, Genesis | `.gen`, `.bin`, `.md` | | `Intellivision` | Intellivision | `.int`, `.bin` | | `Jaguar` | Jaguar | `.jag`, `.j64`, `.rom`, `.bin` | | `JaguarCD` | Jaguar | `.cdi` | | `MasterSystem` | SMS | `.sms` | | `MegaCD` | MegaCD | `.cue`, `.chd` | | `MegaDuck` | GAMEBOY, MegaDuck | `.bin` | | `NeoGeo` | NEOGEO | `.neo`, `.zip` (folders) | | `NeoGeoCD` | NeoGeo-CD, NEOGEO | `.cue`, `.chd` | | `NeoGeoPocket` | NGP | `.ngp` | | `NeoGeoPocketColor` | NGPC | `.ngc` | | `NES` | NES | `.nes` | | `NESMusic` | NES | `.nsf` | | `Nintendo64` | N64 | `.n64`, `.z64` | | `Odyssey2` | ODYSSEY2 | `.bin` | | `PocketChallengeV2` | WonderSwan, PocketChallengeV2 | `.pc2` | | `PokemonMini` | PokemonMini | `.min` | | `PSX` | PSX | `.cue`, `.chd`, `.exe` | | `Saturn` | Saturn | `.cue`, `.chd` | | `Sega32X` | S32X | `.32x` | | `SG1000` | SG1000, Coleco, SMS | `.sg` | | `SNES` | SNES | `.sfc`, `.smc`, `.bin`, `.bs` | | `SNESMusic` | SNES | `.spc` | | `SuperGameboy` | SGB | `.sgb`, `.gb`, `.gbc` | | `SuperGrafx` | TGFX16 | `.sgx` | | `SuperVision` | SuperVision | `.bin`, `.sv` | | `TurboGrafx16` | TGFX16 | `.pce`, `.bin` | | `TurboGrafx16CD` | TGFX16-CD | `.cue`, `.chd` | | `VC4000` | VC4000 | `.bin` | | `Vectrex` | VECTREX | `.vec`, `.bin`, `.rom` | | `VirtualBoy` | VirtualBoy | `.vb` | | `WonderSwan` | WonderSwan | `.ws` | | `WonderSwanColor` | WonderSwan, WonderSwanColor | `.wsc` | ### Computer Systems | System ID | Folders | Extensions | |-----------|---------|------------| | `AcornAtom` | AcornAtom | `.vhd` | | `AcornElectron` | AcornElectron | `.vhd` | | `AliceMC10` | AliceMC10 | `.c10` | | `Amiga` | Amiga | `.adf` | | `Amstrad` | Amstrad | `.dsk`, `.cdt`, `.sna` | | `AmstradPCW` | Amstrad PCW | `.dsk` | | `Apogee` | APOGEE | `.rka`, `.rkr`, `.gam` | | `AppleI` | Apple-I | `.txt` | | `AppleII` | Apple-II | `.dsk`, `.do`, `.po`, `.nib`, `.hdv` | | `Aquarius` | AQUARIUS | `.bin`, `.caq` | | `Atari800` | ATARI800 | `.atr`, `.xex`, `.xfd`, `.atx`, `.car`, `.rom`, `.bin` | | `BBCMicro` | BBCMicro | `.ssd`, `.dsd`, `.vhd` | | `BK0011M` | BK0011M | `.bin`, `.dsk`, `.vhd` | | `C16` | C16 | `.d64`, `.g64`, `.prg`, `.tap`, `.bin` | | `C64` | C64 | `.d64`, `.g64`, `.t64`, `.d81`, `.prg`, `.crt`, `.reu`, `.tap` | | `CasioPV2000` | Casio_PV-2000 | `.bin` | | `CoCo2` | CoCo2 | `.dsk`, `.cas`, `.ccc`, `.rom` | | `DOS` | AO486, /media/fat/_DOS Games | `.mgl`, `.vhd`, `.img`, `.ima`, `.vfd`, `.iso`, `.cue`, `.chd` | | `EDSAC` | EDSAC | `.tap` | | `Galaksija` | Galaksija | `.tap` | | `Interact` | Interact | `.cin`, `.k7` | | `Jupiter` | Jupiter | `.ace` | | `Laser` | Laser | `.vz` | | `Lynx48` | Lynx48 | `.tap` | | `MacPlus` | MACPLUS | `.dsk`, `.img`, `.vhd` | | `MSX` | MSX | `.vhd` | | `MSX1` | MSX1 | `.dsk`, `.rom` | | `MultiComp` | MultiComp | `.img` | | `Orao` | ORAO | `.tap` | | `Oric` | Oric | `.dsk` | | `PCXT` | PCXT | `.img`, `.vhd`, `.ima`, `.vfd` | | `PDP1` | PDP1 | `.bin`, `.rim`, `.pdp` | | `PET2001` | PET2001 | `.prg`, `.tap` | | `PMD85` | PMD85 | `.rmm` | | `QL` | QL | `.mdv`, `.win` | | `RX78` | RX78 | `.bin` | | `SAMCoupe` | SAMCOUPE | `.dsk`, `.mgt`, `.img` | | `SordM5` | Sord M5 | `.bin`, `.rom`, `.cas` | | `Specialist` | SPMX | `.rks`, `.odi` | | `SVI328` | SVI328 | `.cas`, `.bin`, `.rom` | | `TatungEinstein` | TatungEinstein | `.dsk` | | `TI994A` | TI-99_4A | `.bin`, `.m99` | | `TomyTutor` | TomyTutor | `.bin`, `.cas` | | `TRS80` | TRS-80 | `.dsk`, `.jvi`, `.cmd`, `.cas` | | `TSConf` | TSConf | `.vhd` | | `UK101` | UK101 | `.txt`, `.bas`, `.lod` | | `Vector06C` | VECTOR06 | `.rom`, `.com`, `.c00`, `.edd`, `.fdd` | | `VIC20` | VIC20 | `.d64`, `.g64`, `.prg`, `.tap`, `.crt` | | `X68000` | X68000 | `.d88`, `.hdf`, `.mgl` | | `ZX81` | ZX81 | `.p`, `.0` | | `ZXSpectrum` | Spectrum | `.tap`, `.csw`, `.tzx`, `.sna`, `.z80`, `.trd`, `.img`, `.dsk`, `.mgt`, `.vhd` | | `ZXNext` | ZXNext | `.vhd`, `.tzx`, `.csw` | ### Other Systems | System ID | Folders | Extensions | |-----------|---------|------------| | `Arcade` | _Arcade | `.mra` | | `Arduboy` | Arduboy | `.hex`, `.bin` | | `Chip8` | Chip8 | `.ch8` | | `Groovy` | Groovy | `.gmc` | | `ScummVM` | ScummVM | (special) | | `Video` | Video, Movies, TV | `.mp4`, `.mkv`, `.avi`, `.mov`, `.webm` | ## Special Features ### Other Cores MiSTer's `_Other` folder holds standalone FPGA cores that run without a ROM file, like Chess, Flappy Bird, and Game of Life. Zaparoo exposes the supported ones as virtual systems ([launchables](../../features/launchers.md#launchables)) and indexes them when their core is installed, so they show up in browse and search and can be written to a token like any other game. Supported cores: Chess, Donut, Epoch Galaxy II, Flappy Bird, Game of Life, GBMidi, GenMidi, Slug Cross, Tomy Scramble. Only cores you have installed are indexed. Update the media database after adding new `_Other` cores. The 3S-ARM port of Street Fighter III: 3rd Strike also ships as an `_Other` core, but Zaparoo exposes it as virtual media under the `Arcade` system instead of as an Other core entry. ### ao486 If a `.vhd` file is launched via Zaparoo and is in its own folder with an `.iso` or `.chd` file, that disc image will be automatically mounted alongside the hard drive. ### AmigaVision (Amiga) [AmigaVision](https://amiga.vision/) games and demos on the Amiga core are indexed under virtual `Games` and `Demos` folders, matching how they appear in AmigaVision itself. After a media database update they show up in browse and search, and can be launched or written to a token like any other game. For example, to launch Beneath a Steel Sky: ``` Amiga/Games/Beneath a Steel Sky (OCS)[en] ``` Or a demo, using its name from the `demos.txt` listing: ``` Amiga/Demos/ ``` The game and demo names come from the `games.txt` and `demos.txt` listing files that AmigaVision generates in the `Amiga/listings` folder on your SD card. The older `Amiga/listings/games.txt/` path still works for existing tokens. ### Game & Watch Two cores are supported: | Folder | Extensions | Core | |--------|------------|------| | GameNWatch | `.bin` | GnW | | Game and Watch | `.gnw` | GameAndWatch | ### NeoGeo NeoGeo supports launching `.zip` files and folders directly with Zaparoo, including romset folders in subfolders, as supported by the MiSTer core itself. For example, a `.zip` file: ``` NeoGeo/mslug.zip ``` Or a folder: ``` NeoGeo/mslug2 ``` ### ScummVM ScummVM support uses the [bbond007 script](https://github.com/bbond007/MiSTer_ScummVM). Install the script, add your games to the ScummVM folder, and update the media database. Games are automatically detected from the ScummVM configuration file. ### Video Playback Video playback uses [nextfvp](https://github.com/kyx0r/nextfvp), a framebuffer video player. To set up video playback: 1. Download [fvp-mister_arm-15ea265.zip](https://zaparoo.org/files/fvp-mister_arm-15ea265.zip) 2. Extract the `fvp` binary to `/media/fat/linux/` 3. Create a `Video` folder in your games directory (e.g., `/media/fat/games/Video/`) 4. Add your video files to the Video folder 5. Update the media database Supported formats: MP4, MKV, AVI, MOV, WEBM ## Alternate Launchers Some alternate or unofficial versions of cores are supported and can be used by explicitly setting a launcher in the ZapScript on a token. Zaparoo assumes they're installed in either the default location from Update All or in the appropriate menu folder. To use them, add the following to the end of the file path or launch command: `?launcher=`. For example: `N64/some/game.n64?launcher=80MHzNintendo64` MiSTer launch commands also support `set_name` and `set_name_same_dir` advanced arguments. These map to MiSTer's MGL `` tag and `same_dir` attribute. Use `set_name_same_dir=1` when you want a separate config name while keeping the normal games folder. Without `set_name_same_dir=1`, MiSTer also uses the set name as the games folder. ```zapscript NES/Mega Man 2.nes?set_name=RA_NES&set_name_same_dir=1 ``` To set an alternate core as the default for all tokens of a launcher (instead of per-token), use the [`load_path`](../../core/config.md#launchersdefault) field in `config.toml`: ```toml [[launchers.default]] launcher = "Nintendo64" load_path = "_LLAPI/N64_LLAPI" ``` `load_path` is an MGL-form RBF path relative to `/media/fat`, without extension. A config reload is sufficient after changing this — no service restart required. ### RetroAchievements RetroAchievements cores from [Odelot's MiSTer FPGA RetroAchievements Cores](https://github.com/odelot/mister-cores) are supported when installed in `_RA_Cores/Cores`. These launchers automatically use an `RA_*` set name with `same_dir` enabled. This gives each RetroAchievements core its own MiSTer config while still loading games from the normal system folder. | Launcher ID | System | |-------------|--------| | `RAAtari2600` | Atari 2600 | | `RAGameboy` | Game Boy | | `RAGameboyColor` | Game Boy Color | | `RAGBA` | Game Boy Advance | | `RAMegaDrive` | Genesis/Mega Drive | | `RAMegaCD` | Mega CD | | `RASMS` | Master System | | `RANeoGeo` | Neo Geo | | `RANES` | NES | | `RANintendo64` | Nintendo 64 | | `RAPSX` | PlayStation | | `RAS32X` | Sega 32X | | `RASNES` | SNES | | `RASuperGameboy` | Super Game Boy | | `RATurboGrafx16` | TurboGrafx-16 | ### DB9 DB9 cores are alternate MiSTer cores for DB9 controller adapters. Install them in the matching `_Console` core locations, then select them with `?launcher=` or a default launcher override. | Launcher ID | System | |-------------|--------| | `DB9AdventureVision` | Adventure Vision | | `DB9Astrocade` | Astrocade | | `DB9Atari5200` | Atari 5200 | | `DB9Atari7800` | Atari 7800 | | `DB9AtariLynx` | Atari Lynx | | `DB9CasioPV1000` | Casio PV-1000 | | `DB9CDI` | CD-i | | `DB9ColecoVision` | ColecoVision | | `DB9CreatiVision` | CreatiVision | | `DB9Gameboy` | Game Boy | | `DB9Gameboy2P` | Game Boy 2P | | `DB9GameNWatch` | Game & Watch | | `DB9GBA` | Game Boy Advance | | `DB9GBAAccuracy` | Game Boy Advance (accuracy) | | `DB9GBA2P` | Game Boy Advance 2P | | `DB9MegaDrive` | Mega Drive | | `DB9Genesis` | Genesis | | `DB9SMS` | Master System | | `DB9MegaCD` | Mega CD | | `DB9NeoGeo` | Neo Geo | | `DB9NeoGeo24MHz` | Neo Geo (24 MHz) | | `DB9NES` | NES | | `DB9Odyssey2` | Odyssey 2 | | `DB9PSX` | PlayStation | | `DB9DualRAMPSX` | PlayStation (DualRAM) | | `DB9Sega32X` | Sega 32X | | `DB9SuperGameboy` | Super Game Boy | | `DB9Saturn` | Saturn | | `DB9DualRAMSaturn` | Saturn (DualRAM) | | `DB9SNES` | SNES | | `DB9TurboGrafx16` | TurboGrafx-16 | | `DB9Vectrex` | Vectrex | ### LLAPI Bliss-Box LLAPI cores. Alternate Arcade cores can be referenced directly with their `.mra` files. | Launcher ID | System | |-------------|--------| | `LLAPIAtari2600` | Atari 2600 | | `LLAPIAtari7800` | Atari 7800 | | `LLAPIGameboy` | Game Boy | | `LLAPIGBA` | Game Boy Advance | | `LLAPIMegaDrive` | Genesis/Mega Drive | | `LLAPISMS` | Master System | | `LLAPIMegaCD` | Mega CD | | `LLAPINeoGeo` | Neo Geo | | `LLAPINES` | NES | | `LLAPINintendo64` | Nintendo 64 | | `LLAPI80MHzNintendo64` | Nintendo 64 (80MHz) | | `LLAPIPSX` | PlayStation | | `LLAPIS32X` | Sega 32X | | `LLAPISuperGameboy` | Super Game Boy | | `LLAPISaturn` | Saturn | | `LLAPISNES` | SNES | | `LLAPITurboGrafx16` | TurboGrafx-16 | | `LLAPISuperGrafx` | SuperGrafx | ### PWM 24-bit video PWM cores. | Launcher ID | System | |-------------|--------| | `PWMNintendo64` | Nintendo 64 | | `PWM80MHzNintendo64` | Nintendo 64 (80MHz) | | `PWMPSX` | PlayStation | | `PWM2XPSX` | PlayStation (2X CPU) | | `PWMSaturn` | Saturn | ### Overclock Robert Piep's experimental overclock cores. | Launcher ID | System | |-------------|--------| | `80MHzNintendo64` | Nintendo 64 | | `2XPSX` | PlayStation | ### DualRAM Dual-SDRAM cores from [TheJesusFish/Dual-Ram-Console-Cores](https://github.com/TheJesusFish/Dual-Ram-Console-Cores). | Launcher ID | System | |-------------|--------| | `DualRAM3DO` | 3DO | | `DualRAMJaguar` | Jaguar | | `DualRAMPSX` | PlayStation | | `DualRAMSaturn` | Saturn | ### Sinden Lightgun Sinden Lightgun cores. :::info Zaparoo looks for Sinden cores in `Light Gun/-Sinden.rbf` first, then the legacy `_Sinden/_Sinden.rbf` path. If both paths exist, Zaparoo uses the `Light Gun` version. ::: | Launcher ID | System | |-------------|--------| | `SindenGenesis` | Genesis | | `SindenMegaDrive` | Mega Drive | | `SindenSMS` | Master System | | `SindenMegaCD` | Mega CD | | `SindenNES` | NES | | `SindenPSX` | PlayStation | | `SindenSNES` | SNES | --- ## MiSTeX MiSTeX is fully supported by Zaparoo and closely follows [MiSTer](./mister/index.md). The main differences are installation and the current reader set. ## File Paths | Item | Path | | ------------------ | -------------------------------- | | Config file | `/media/fat/zaparoo/config.toml` | | Data directory | `/media/fat/zaparoo` | | Log file | `/tmp/zaparoo/core.log` | | Mappings directory | `/media/fat/zaparoo/mappings` | The `/media/fat` directory is the top level of the SD card. The `/tmp` directory is deleted when MiSTeX is powered off. ## Install Download Zaparoo Core for MiSTeX from the [Downloads page](/downloads/) and copy the `zaparoo.sh` file to the `Scripts` folder on your SD card. Run `zaparoo` from the MiSTeX `Scripts` menu. A prompt will offer to enable Zaparoo as a startup service. :::warning Using FileZilla? Enable binary transfer mode per [these steps](https://oryon.net/knowledge-base/article/how-to-change-filezilla-ftp-program-to-binary-transfer/). FileZilla incorrectly detects `zaparoo.sh` as text and will corrupt it. ::: ## Readers ## Supported Launchers All launchers supported by MiSTer are also supported by MiSTeX (as far as MiSTeX itself supports them): | Launcher | Notes | | ------------- | --------------------------------------- | | FPGA Cores | 100+ arcade, console, and computer systems | | MGL Files | MiSTer Game Loader format | | Shell Scripts | Custom `.sh` file execution | See [MiSTer Launchers](./mister/launchers.md) for the full list of supported systems. --- ## Recalbox :::danger This platform is in beta and not ready for production use. ::: Zaparoo Core on Recalbox supports shell script launching. Full launcher support is coming in a future release. ## File Paths | Item | Path | | ------------------ | ---------------------------------------- | | Config file | `~/.config/zaparoo/config.toml` | | Data directory | `~/.local/share/zaparoo` | | Log file | `/tmp/-zaparoo/core.log` | | Mappings directory | `~/.local/share/zaparoo/mappings` | Where `~` is the home directory of the current user. ## Install Download Zaparoo Core for Linux from the [Downloads page](/downloads/), unzip it and copy the `zaparoo` file to `/recalbox/share/system/bin`. Enable SSH in Recalbox settings (Network > SSH Server). Default credentials: `root` / `recalboxroot`. ```bash cd /recalbox/share/system/bin ./zaparoo -install systemctl --user enable zaparoo.service systemctl --user start zaparoo.service ``` To uninstall: ```bash ./zaparoo -uninstall ``` ## Readers ## Supported Launchers | Launcher | Notes | | ------------- | ---------------------------- | | Shell Scripts | Custom `.sh` file execution | Create shell scripts to launch games, applications, or media. Add scripts to the `allow_file` list in config.toml and restart Zaparoo. --- ## RePlayOS Zaparoo Core on [RePlayOS](https://www.replayos.com/) launches games and media through the RePlayOS libretro frontend. ## File Paths | Item | Path | | ------------------ | ------------------------------------- | | Config file | `/media/sd/zaparoo/config.toml` | | Data directory | `/media/sd/zaparoo` | | Log file | `/media/sd/zaparoo/logs/core.log` | | Mappings directory | `/media/sd/zaparoo/mappings` | `/media/sd` is the persistent SD card partition and survives OS updates. ## Install SSH into your RePlayOS device and run: ```bash curl -fsSL https://zaparoo.org/install.sh | bash ``` This installs and enables the `zaparoo.service` systemd unit to run on startup. Once running, use the [Zaparoo App](/docs/app/) on your phone or the built-in web UI to manage your setup and write tokens. To uninstall: ```bash /media/sd/zaparoo/zaparoo -uninstall ``` ## Readers ## Launchers | System ID | Folder | Extensions | | ------------------------- | -------------------- | ------------------------------------------------------------------------------------------------- | | `NES` | `nintendo_nes` | `.nes`, `.unif`, `.unf`, `.fds`, `.zip`, `.7z` | | `SNES` | `nintendo_snes` | `.smc`, `.sfc`, `.swc`, `.fig`, `.bs`, `.st`, `.zip`, `.7z` | | `Gameboy` | `nintendo_gb` | `.gb`, `.zip`, `.7z` | | `GameboyColor` | `nintendo_gbc` | `.gbc`, `.zip`, `.7z` | | `GBA` | `nintendo_gba` | `.gba`, `.zip`, `.7z` | | `Nintendo64` | `nintendo_n64` | `.z64`, `.n64`, `.v64`, `.bin`, `.u1`, `.zip`, `.7z` | | `NDS` | `nintendo_ds` | `.nds`, `.bin`, `.zip`, `.7z` | | `Genesis` | `sega_smd` | `.bin`, `.gen`, `.md`, `.sg`, `.smd`, `.zip`, `.7z` | | `MasterSystem` | `sega_sms` | `.bin`, `.sms`, `.zip`, `.7z` | | `GameGear` | `sega_gg` | `.bin`, `.gg`, `.zip`, `.7z` | | `Sega32X` | `sega_32x` | `.32x`, `.chd`, `.smd`, `.bin`, `.md`, `.zip`, `.7z` | | `MegaCD` | `sega_cd` | `.cue`, `.iso`, `.chd`, `.m3u` | | `Dreamcast` | `sega_dc` | `.cdi`, `.cue`, `.gdi`, `.chd`, `.m3u` | | `Saturn` | `sega_saturn` | `.cue`, `.ccd`, `.m3u`, `.chd`, `.iso`, `.zip` | | `SG1000` | `sega_sg1000` | `.bin`, `.sg`, `.zip`, `.7z` | | `Arcade` (FBNeo) | `arcade_fbneo` | `.zip`, `.7z` | | `Arcade` (MAME) | `arcade_mame` | `.zip`, `.7z` | | `Arcade` (MAME 2003 Plus) | `arcade_mame_2k3p` | `.zip`, `.7z` | | `Atomiswave` | `arcade_dc` | `.zip`, `.chd`, `.lst`, `.bin`, `.dat`, `.7z` | | `Atari2600` | `atari_2600` | `.a26`, `.bin`, `.zip`, `.7z` | | `Atari5200` | `atari_5200` | `.rom`, `.xfd`, `.atr`, `.atx`, `.cdm`, `.cas`, `.car`, `.bin`, `.a52`, `.xex`, `.zip`, `.7z` | | `Atari7800` | `atari_7800` | `.a78`, `.bin`, `.zip`, `.7z` | | `Jaguar` | `atari_jaguar` | `.cue`, `.j64`, `.jag`, `.cof`, `.abs`, `.cdi`, `.rom`, `.zip`, `.7z` | | `AtariLynx` | `atari_lynx` | `.lnx`, `.zip`, `.7z` | | `PSX` | `sony_psx` | `.cue`, `.img`, `.mdf`, `.pbp`, `.toc`, `.cbn`, `.m3u`, `.ccd`, `.chd`, `.iso` | | `TurboGrafx16` | `nec_pce` | `.pce`, `.bin`, `.zip`, `.7z` | | `TurboGrafx16CD` | `nec_pcecd` | `.pce`, `.cue`, `.ccd`, `.iso`, `.img`, `.chd` | | `NeoGeo` | `snk_neogeo` | `.7z`, `.zip` | | `NeoGeoCD` | `snk_neocd` | `.cue`, `.iso`, `.chd` | | `NeoGeoPocket` | `snk_ngp` | `.ngp`, `.zip`, `.7z` | | `C64` | `commodore_c64` | `.d64`, `.d81`, `.crt`, `.prg`, `.tap`, `.t64`, `.m3u`, `.zip`, `.7z` | | `Amiga` | `commodore_ami` | `.adf`, `.uae`, `.ipf`, `.dms`, `.dmz`, `.adz`, `.lha`, `.hdf`, `.exe`, `.m3u`, `.zip` | | `AmigaCD32` | `commodore_amicd` | `.bin`, `.cue`, `.iso`, `.chd` | | `MSX1` | `msx_msx` | `.dsk`, `.mx1`, `.rom`, `.zip`, `.7z`, `.cas`, `.m3u` | | `MSX2` | `msx_msx2` | `.dsk`, `.mx2`, `.rom`, `.zip`, `.7z`, `.cas`, `.m3u` | | `ZXSpectrum` | `sinclair_zxs` | `.tzx`, `.tap`, `.z80`, `.rzx`, `.scl`, `.trd`, `.dsk`, `.zip`, `.7z` | | `X68000` | `sharp_x68k` | `.dim`, `.img`, `.d88`, `.88d`, `.hdm`, `.dup`, `.2hd`, `.xdf`, `.hdf`, `.cmd`, `.m3u`, `.zip`, `.7z` | | `3DO` | `panasonic_3do` | `.iso`, `.chd`, `.cue` | | `CDI` | `philips_cdi` | `.chd`, `.cue`, `.toc`, `.nrg`, `.gdi`, `.iso`, `.cdr` | | `DOS` | `pc_dos` | `.pc`, `.dos`, `.zip`, `.squashfs`, `.dosz`, `.m3u`, `.iso`, `.cue` | | `ScummVM` | `pc_scummvm` | `.scummvm`, `.squashfs` | | `Amstrad` | `amstrad_cpc` | `.dsk`, `.sna`, `.tap`, `.cdt`, `.voc`, `.cpr`, `.zip`, `.7z` | | `Video` | `alpha_player` | `.mkv`, `.avi`, `.mp4`, `.flac`, `.ogg`, `.nsf`, `.vgm` | ### Shell Scripts Run `.sh` scripts. Scripts must be allow-listed via the [`allow_file`](../core/config.md#allow_file) setting in `config.toml`. --- ## SteamOS Zaparoo Core on SteamOS supports launching Steam games, EmuDeck, and RetroDECK from your Steam Deck. ## File Paths | Item | Path | | ------------------ | ------------------------------------------ | | Config file | `/home/deck/.config/zaparoo/config.toml` | | Data directory | `/home/deck/.local/share/zaparoo` | | Log file | `/tmp/zaparoo/core.log` | | Mappings directory | `/home/deck/.local/share/zaparoo/mappings` | Assuming the default `deck` account. ## Install In Desktop Mode, open Konsole and run: ```bash curl -fsSL https://zaparoo.org/install.sh | bash ``` This installs all necessary components and sets up the service to run on startup. Since SteamOS has an immutable root filesystem, the installation is user-local (installed to `~/.local/bin`). Once running, use the [Zaparoo App](/docs/app/) or the built-in web UI to manage your setup and write tokens. To uninstall: ```bash ~/.local/bin/zaparoo -uninstall application ~/.local/bin/zaparoo -uninstall service ~/.local/bin/zaparoo -uninstall desktop sudo ~/.local/bin/zaparoo -uninstall hardware ``` ## Readers ## Launchers | Launcher | Systems | Notes | | ---------- | ------- | ------------------------------------- | | Steam | PC | Games and non-Steam shortcuts | | EmuDeck | 56+ | RetroArch and standalone emulators | | RetroDECK | 200+ | Unified emulator frontend | | Kodi | Media | Videos, movies, TV shows, music | | Shell Scripts | Any | Custom `.sh` file execution | ### Steam Launches games from your Steam library via the `steam://` URL scheme. Both official Steam games and non-Steam shortcuts added to your library are detected. Zaparoo also tracks when you start Steam games externally (from Big Picture or the desktop client), showing the currently running game in ActiveMedia. Games are indexed from: - `~/.steam/steam/steamapps/` - `~/.local/share/Steam/` To manually launch a Steam game, write `steam://` to a token. For example: `steam://1145360` for Hades. ```toml title="config.toml" [[launchers.default]] launcher = "Steam" install_dir = "/custom/steam/path" # Optional custom Steam install directory ``` ### EmuDeck Zaparoo automatically detects [EmuDeck](https://www.emudeck.com/) installations and creates launchers for each system. Games are launched through Flatpak to individual emulators (RetroArch cores or standalone emulators like Dolphin, PCSX2, etc.). EmuDeck is detected when `~/Emulation/roms/` exists. **Default paths:** - ROMs: `~/Emulation/roms/` - Gamelists: `~/ES-DE/gamelists/` Supported systems include: NES, SNES, Game Boy, GBA, N64, NDS, GameCube, Wii, Wii U, Switch, 3DS, Genesis, Saturn, Dreamcast, PSX, PS2, PS3, PSP, Neo Geo, Arcade, and many more. Games are discovered using ES-DE's `gamelist.xml` files for proper display names. ### RetroDECK Zaparoo automatically detects [RetroDECK](https://retrodeck.net/) and creates launchers for each system. All games are launched through RetroDECK's unified CLI (`flatpak run net.retrodeck.retrodeck `), which handles emulator selection internally. RetroDECK is detected when the `net.retrodeck.retrodeck` Flatpak is installed. **Default paths:** - ROMs: `~/retrodeck/roms/` - Gamelists: `~/retrodeck/ES-DE/gamelists/` RetroDECK supports any system folder that matches an ES-DE system definition. ### Kodi Launch media content from a [Kodi](https://kodi.tv/) instance running on your network. Supported content: - Video and audio files - Movies and TV episodes - Music (songs, albums, artists) ```toml title="config.toml" [[launchers.default]] launcher = "Kodi" server_url = "http://kodi:8080" ``` Replace `kodi:8080` with your Kodi instance's address and web interface port. ### Gaming Mode Both EmuDeck and RetroDECK work in Steam's Gaming Mode. Zaparoo manages window focus through gamescope to ensure the emulator window is properly displayed when launching games. ### Shell Scripts Execute arbitrary shell scripts. Requires explicit allow list configuration for security. | System ID | Extensions | |-----------|------------| | Any | `.sh` | ```toml title="config.toml" [launchers] allow_file = [ "^/home/deck/scripts/.*\\.sh$" ] ``` ## Known Issues - Updates require stopping the service first: `sudo systemctl stop zaparoo.service` ## FAQ **What NFC reader should I use with a Steam Deck?** The [PN532 USB](../readers/nfc/pn532-usb.md) reader is the recommended choice. Plug it into the Steam Deck or a connected USB hub and it will be auto-detected by Zaparoo. **Can Zaparoo launch non-Steam games on the Steam Deck?** Zaparoo can launch any game that EmuDeck or RetroDECK has configured. It launches through Steam shortcuts, so anything added to Steam library is launchable. **Does Zaparoo work in Game Mode?** Yes. The service runs in the background and launches games from Game Mode. You do not need to switch to Desktop Mode for normal scanning to work. **Does Zaparoo survive SteamOS updates?** SteamOS updates can reset the system root, but Zaparoo is installed user-locally and the service should survive updates without needing reinstallation. --- ## Windows Zaparoo Core on Windows runs in the system tray with support for Steam, LaunchBox, and custom launcher configurations. ## File Paths | Item | Path | | ------------------ | -------------------------------------- | | Config file | `%localappdata%\zaparoo\config.toml` | | Data directory | `%localappdata%\zaparoo` | | Log file | `%localappdata%\zaparoo\logs\core.log` | | Mappings directory | `%localappdata%\zaparoo\mappings` | | Launcher directory | `%localappdata%\zaparoo\launchers` | Access these paths by pasting them in Explorer's address bar or in a Win+R dialog. ## Install Download Zaparoo Core for Windows from the [Downloads page](/downloads/). **Installer**: Run the setup executable and follow the wizard. Options include running on startup and creating a desktop icon. **Manual**: Extract `Zaparoo.exe` from the zip and run it. It starts in the system tray. Once running, use the [Zaparoo App](/docs/app/) on your phone or the built-in web UI to manage your setup and write tokens. ## System Tray Right-click the Zaparoo icon in the system tray to access the following options: | Menu Item | Description | |-----------|-------------| | Open | Opens the Zaparoo web UI in your browser | | Address | Shows the local IP address and copies it to clipboard | | Edit Config | Opens `config.toml` in your default text editor | | Mappings | Opens the mappings directory in Explorer | | Launchers | Opens the custom launchers directory in Explorer | | Reload | Reloads Core settings and files without restarting | | View Log | Opens the log file for troubleshooting | | Quit | Stops the Zaparoo service and exits | ## Readers ## Launchers | Launcher | Systems | Notes | |----------|---------|-------| | Steam | PC | Auto-detected from registry | | LaunchBox | 100+ | Requires plugin installation | | RetroBat | 80+ | Auto-detected, requires running with web API enabled | | Flashpoint | PC | Manual token creation | | Kodi | Video, Music | Local files and library media | | Web Browser | Any | Opens URLs in default browser | | Executables | Any | `.exe` files (requires allow list) | | Scripts | Any | `.bat`, `.cmd`, `.lnk`, `.a3x`, `.ahk` (requires allow list) | Executables and Scripts require an `allow_file` configuration in your `config.toml` before they can be launched. See [Launchers](./launchers.md) for setup instructions and configuration. --- ## Launchers(Windows) Detailed launcher configuration for Windows. ## Steam Launches games from your Steam library via the `steam://` URL scheme. Both official Steam games and non-Steam shortcuts are detected. Steam is auto-detected via Windows registry. Games are indexed from your Steam library folders. To manually launch a Steam game, write `steam://` to a token. For example: `steam://1145360` for Hades. ```toml title="config.toml" [[launchers.default]] launcher = "Steam" install_dir = "D:\\Steam" # Optional custom Steam install directory ``` ## LaunchBox :::tip Required Plugin LaunchBox integration requires the Zaparoo plugin. Download [Zaparoo LaunchBox Integration v1.0.0.zip](https://zaparoo.org/files/Zaparoo%20LaunchBox%20Integration%20v1.0.0.zip) and follow the installation steps below. ::: ### Plugin Installation 1. Download the [Zaparoo LaunchBox Integration v1.0.0.zip](https://zaparoo.org/files/Zaparoo%20LaunchBox%20Integration%20v1.0.0.zip) file 2. Extract the zip file 3. Copy the `Zaparoo LaunchBox Integration` folder to your LaunchBox `Plugins` directory (usually `/Plugins/`) 4. Restart LaunchBox To verify the plugin loaded correctly, right-click any game in LaunchBox - you should see "Write to tag" in the context menu. Once installed, Zaparoo will automatically detect LaunchBox and add all games to the media database. To manually create a LaunchBox game token, write `launchbox://` to a token. ```toml title="config.toml" [[launchers.default]] launcher = "LaunchBox" install_dir = "D:\\LaunchBox" # Optional custom install directory ``` ## RetroBat RetroBat integration uses the EmulationStation API. RetroBat must be running for games to launch. Games are automatically detected from your RetroBat `roms` folder based on `gamelist.xml` files. ```toml title="config.toml" [[launchers.default]] launcher = "RetroBat" install_dir = "D:\\RetroBat" # Optional custom install directory ``` ### Web API access Core launches games and detects the running game through RetroBat's EmulationStation web API, which it reaches at `http://localhost:1234` on the same PC. If RetroBat is running but games won't launch, two things on the RetroBat side usually need attention. First, enable EmulationStation's web access. In RetroBat's EmulationStation menu this sits under the system settings, typically **Main Menu** > **System Settings** > **Frontend Developer Options** > **Enable public web access**. Restart RetroBat after changing it. If you can't find the option, the RetroBat Discord and wiki are the best places to check, since the menu layout changes between versions. Second, make sure `localhost` resolves to `127.0.0.1`. Core connects to `localhost`, and on some Windows setups that name resolves only to IPv6 (`::1`) or fails to resolve, which stops Core from reaching the web server. Editing the Windows hosts file at `C:\Windows\System32\drivers\etc\hosts` as administrator to include this line forces an IPv4 match: ```text 127.0.0.1 localhost ``` ### Supported Systems | System ID | RetroBat Folder | |-----------|-----------------| | `Arcade` | `mame`, `fbneo` | | `Atomiswave` | `atomiswave` | | `NAOMI` | `naomi` | | `NAOMI2` | `naomi2` | | `Model2` | `model2` | | `Model3` | `model3` | | `Triforce` | `triforce` | | `Chihiro` | `chihiro` | | `Hikaru` | `hikaru` | | `CPS1` | `cps1` | | `CPS2` | `cps2` | | `CPS3` | `cps3` | | `DAPHNE` | `daphne` | | `Singe` | `singe` | | `SG1000` | `sg1000` | | `MasterSystem` | `mastersystem` | | `Genesis` | `megadrive` | | `MegaCD` | `megacd` | | `Sega32X` | `sega32x` | | `Saturn` | `saturn` | | `Dreamcast` | `dreamcast` | | `GameGear` | `gamegear` | | `NES` | `nes` | | `FDS` | `fds` | | `SNES` | `snes` | | `SNESMSU1` | `snes-msu1` | | `Sufami` | `sufami` | | `Nintendo64` | `n64` | | `GameCube` | `gamecube` | | `Wii` | `wii` | | `WiiU` | `wiiu` | | `Switch` | `switch` | | `VirtualBoy` | `virtualboy` | | `Gameboy` | `gb` | | `Gameboy2P` | `gb2players` | | `SGBMSU1` | `gb-msu` | | `GameboyColor` | `gbc` | | `GBA` | `gba` | | `GBA2P` | `gba2players` | | `NDS` | `nds` | | `PokemonMini` | `pokemini` | | `GameNWatch` | `gw` | | `PSX` | `psx` | | `PS2` | `ps2` | | `PS3` | `ps3` | | `PS4` | `ps4` | | `Xbox` | `xbox` | | `Xbox360` | `xbox360` | | `TurboGrafx16` | `pcengine` | | `TurboGrafx16CD` | `pcenginecd` | | `SuperGrafx` | `supergrafx` | | `PCFX` | `pcfx` | | `NeoGeo` | `neogeo` | | `NeoGeoCD` | `neogeocd` | | `Atari2600` | `atari2600` | | `Atari5200` | `atari5200` | | `Atari7800` | `atari7800` | | `AtariLynx` | `lynx` | | `Jaguar` | `jaguar` | | `JaguarCD` | `jaguarcd` | | `3DO` | `3do` | | `ColecoVision` | `colecovision` | | `Intellivision` | `intellivision` | | `ChannelF` | `channelf` | | `Vectrex` | `vectrex` | | `Odyssey2` | `odyssey2` | | `Amiga500` | `amiga500` | | `Amiga1200` | `amiga1200` | | `AmigaCD32` | `amigacd32` | | `Amstrad` | `amstradcpc` | | `Atari800` | `atari800` | | `AtariST` | `atarist` | | `AtariXEGS` | `xegs` | | `C64` | `c64` | | `MSX1` | `msx1` | | `MSX2` | `msx2` | | `MSX2Plus` | `msx2+` | | `ZXSpectrum` | `zxspectrum` | | `ZX81` | `zx81` | | `X68000` | `x68000` | | `X1` | `x1` | | `PC88` | `pc88` | | `PC98` | `pc98` | | `Aquarius` | `aquarius` | | `SAMCoupe` | `samcoupe` | | `Thomson` | `thomson` | | `Spectravideo` | `spectravideo` | | `Oric` | `oricatmos` | ## Flashpoint Launches games from [Flashpoint Archive](https://flashpointarchive.org/). Games won't appear in the media database, but you can manually create tokens by writing `flashpoint://` or copying the URL from the Flashpoint launcher. ## Kodi Plays media via Kodi's JSON-RPC API. Kodi must be running with remote control enabled. ### Local Files | System ID | Extensions | |-----------|------------| | `Video` | `.mp4`, `.mkv`, `.avi`, `.mov`, `.webm`, `.m4v`, `.wmv`, `.flv` | | `MusicTrack` | `.mp3`, `.flac`, `.ogg`, `.wav`, `.m4a`, `.wma`, `.aac` | ### Library Media | System ID | Description | |-----------|-------------| | `Movie` | Movies from Kodi library | | `TVEpisode` | TV episodes from Kodi library | | `TVShow` | TV shows (plays next unwatched episode) | | `MusicTrack` | Songs from Kodi library | | `MusicAlbum` | Albums from Kodi library | | `MusicArtist` | Artists from Kodi library (plays all songs) | ### Configuration Configure the Kodi server URL in `config.toml`: ```toml title="config.toml" [[launchers.default]] launcher = "Kodi" server_url = "http://192.168.1.100:8080" ``` If Kodi requires authentication, add credentials to `auth.toml`: ```toml title="auth.toml" ["http://192.168.1.100:8080"] username = "kodi" password = "your_password" ``` ## Web Browser Opens URLs in the default web browser. Supports `http://` and `https://` schemes. ## Executables :::danger Security Risk Be extremely careful! Executables can do anything, including deleting files and installing malware. Only use this feature if you trust the executable. ::: | System ID | Extensions | |-----------|------------| | Any | `.exe` | Executables must be explicitly allowed in `config.toml`: ```toml title="config.toml" [launchers] allow_file = [ '^C:\\Games\\.*\\.exe$', '^D:\\Emulators\\.*\\.exe$' ] ``` ## Scripts :::danger Security Risk Scripts can execute arbitrary code. Only allow scripts you trust. ::: | System ID | Extensions | |-----------|------------| | Any | `.bat`, `.cmd`, `.lnk`, `.a3x`, `.ahk` | Scripts must be explicitly allowed in `config.toml`: ```toml title="config.toml" [launchers] allow_file = [ '^C:\\Scripts\\.*\\.bat$', '^C:\\Scripts\\.*\\.ahk$' ] ``` ## Custom Launchers Custom launchers allow you to start ROMs with a specified emulator. They are TOML files placed in the launcher directory (`%localappdata%\zaparoo\launchers`). Example for PCSX2: ```toml title="PCSX2PS2_zaparoo_launcher.toml" [[launchers.custom]] id = "PCSX2PS2" system = "PS2" media_dirs = ["D:\\Emulation\\Roms\\PS2"] file_exts = [".iso", ".bin", ".img", ".nrg", ".mdf", ".chd"] execute = "\"D:\\Emulation\\Emulators\\PCSX2-Nightly\\pcsx2-qt.exe\" \"[[media_path]]\"" ``` ### Quoting paths and PowerShell Core runs the `execute` command directly, without a shell. It splits the string into a program and arguments itself (respecting single and double quotes), then starts the program. This stops characters in a ROM path from being treated as shell commands. Launch the emulator executable directly whenever you can, as in the example above. Quote the program path and `[[media_path]]` separately so paths with spaces (like `D:\Roms\Sony - PlayStation 2\`) stay intact. Running the executable directly also lets Core track the process for [media tracking](../../features/launchers.md#custom-launchers). :::warning Avoid wrapping the command in PowerShell Wrapping the launch in PowerShell with `Start-Process -ArgumentList '"[[media_path]]"'` makes PowerShell parse the arguments a second time. It can strip your quotes and split the path on its spaces. A folder name with a space or hyphen like `Sony - PlayStation 2` then breaks, giving errors such as `Unknown parameter: '-'` and launching the emulator with no game. Use a PowerShell wrapper only when you need PowerShell-specific behavior. ::: If you do need PowerShell, such as to set an environment variable before launching, put the whole script in one `-Command "..."` argument and use the call operator `&` with single-quoted paths: ```toml execute = "powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -Command \"& 'D:\\Emulation\\Emulators\\PCSX2-Nightly\\pcsx2-qt.exe' '[[media_path]]'\"" ``` The whole `& '...' '...'` script is a single argument to `-Command`, so Core passes it to PowerShell untouched, and PowerShell's single quotes keep the paths intact.
PowerShell script for automatic launcher creation ```powershell title="launcher-generator.ps1" Write-Host "=== Zaparoo Launcher TOML Generator ===" $system = Read-Host "Enter system name (e.g., CPS2, PS1, NES)" $rompath = Read-Host "Enter full path to ROM folder" $emupath = Read-Host "Enter full path to emulator executable" $coreMap = @{ "PS1" = "mednafen_psx_hw_libretro.dll" "PS2" = "" "NES" = "mesen_libretro.dll" "SNES" = "snes9x_libretro.dll" "N64" = "mupen64plus_next_libretro.dll" "GBA" = "mgba_libretro.dll" "CPS1" = "fbalpha2012_cps1_libretro.dll" "CPS2" = "fbalpha2012_cps2_libretro.dll" "CPS3" = "fbalpha2012_cps3_libretro.dll" "NeoGeo" = "fbalpha2012_neogeo_libretro.dll" "Arcade" = "fbneo_libretro.dll" "Genesis" = "genesis_plus_gx_libretro.dll" "TG16" = "mednafen_pce_fast_libretro.dll" "GB" = "gambatte_libretro.dll" "GBC" = "gambatte_libretro.dll" "DS" = "melonds_libretro.dll" "PSP" = "ppsspp_libretro.dll" "Saturn" = "mednafen_saturn_libretro.dll" "Dreamcast" = "flycast_libretro.dll" "3DO" = "opera_libretro.dll" "MSX" = "bluemsx_libretro.dll" "DOS" = "dosbox_pure_libretro.dll" "ScummVM" = "scummvm_libretro.dll" "Amiga" = "puae_libretro.dll" } $extMap = @{ "PS1" = '".cue", ".bin", ".iso", ".chd"' "NES" = '".nes"' "SNES" = '".sfc", ".smc"' "N64" = '".z64", ".n64", ".v64"' "GBA" = '".gba"' "CPS1" = '".zip"' "CPS2" = '".zip"' "CPS3" = '".zip"' "NeoGeo" = '".zip"' "Arcade" = '".zip"' "Genesis" = '".md", ".bin"' "TG16" = '".pce"' "GB" = '".gb"' "GBC" = '".gbc"' "DS" = '".nds"' "PSP" = '".iso", ".cso"' "Saturn" = '".cue", ".bin"' "Dreamcast" = '".cdi", ".gdi"' "3DO" = '".iso"' "MSX" = '".rom", ".dsk"' "DOS" = '".zip"' "ScummVM" = '".svm"' "Amiga" = '".adf"' } $corepath = $null if ($coreMap.ContainsKey($system)) { $core = $coreMap[$system] if ($core -ne "") { $corepath = Join-Path (Split-Path $emupath) "cores\$core" } } $rompath = $rompath -replace '\\', '\\' $emupath = $emupath -replace '\\', '\\' if ($corepath) { $corepath = $corepath -replace '\\', '\\' } # Launch the executable directly. Single quotes keep paths with spaces intact # through Core's command split, and avoid a second round of PowerShell parsing. if ($corepath) { $exec = "'$emupath' -L '$corepath' '[[media_path]]'" } else { $exec = "'$emupath' '[[media_path]]'" } $exts = if ($extMap.ContainsKey($system)) { $extMap[$system] } else { '".zip"' } $outfile = "${system}_zaparoo_launcher.toml" @" [[launchers.custom]] id = "$system" system = "$system" media_dirs = ["$rompath"] file_exts = [$exts] execute = "$exec" "@ | Set-Content -Encoding UTF8 $outfile Write-Host "`nTOML launcher created: $outfile" Read-Host -Prompt "`nPress Enter to exit" ```
--- ## Barcode Scanners Zaparoo supports reading [barcodes](../../tokens/barcodes.md) and [QR codes](../../tokens/qr-codes.md) through the Zaparoo App or a configured serial scanner. ## Platforms ## App-Based Scanning Use your phone's camera to scan barcodes and QR codes with the native [Zaparoo App](../../app/index.md). Scans are sent to Zaparoo Core through the app, so you don't need a reader driver on the device running Core. - No additional hardware required - Supports barcode and QR code formats handled by the app's camera scanner See the [barcodes token page](../../tokens/barcodes.md) for supported formats and how to set up mappings. ## Hardware Scanners Connect a physical barcode scanner through an RS-232 serial port or USB-COM adapter for a dedicated scanner setup. The scanner must output serial data, not keyboard-emulation keystrokes. - Requires RS-232 or USB-COM barcode scanner hardware - Hands-free operation - Works with barcodes and QR codes supported by your scanner See [RS-232 Scanner](./rs232.md) for setup and usage. --- ## RS-232 Scanner Connect a physical barcode or QR code scanner through an RS-232 serial port or USB-to-serial adapter. The scanner must be configured for RS-232 or USB-COM output and send each scan as a line of serial text. ## Platforms ## Setup The RS-232 barcode reader is not auto-detected. Add it manually to your [`config.toml`](../../core/config.md) with the serial port path for your scanner. ### Configuration On Linux or macOS: ```toml [[readers.connect]] driver = "rs232barcode" path = "/dev/ttyUSB0" # Linux/macOS ``` On Windows: ```toml [[readers.connect]] driver = "rs232barcode" path = "COM3" # Windows ``` ### Finding your serial port **Linux:** ```bash ls /dev/ttyUSB* /dev/ttyACM* ``` **macOS:** ```bash ls /dev/tty.usb* /dev/cu.usb* ``` **Windows:** - Check Device Manager, then Ports (COM & LPT) - Look for "USB Serial Port (COMX)" ## Connection settings Zaparoo Core uses these fixed serial settings for the `rs232barcode` driver: - Baud rate: `9600` - Data bits: `8` - Parity: none - Stop bits: `1` These settings are not configurable in Core. If your scanner uses different settings, reconfigure the scanner with its manual or programming barcodes. :::warning Keyboard Emulation Mode Many barcode scanners ship in **keyboard emulation mode** by default, which is not supported by this reader driver. Switch your scanner to **RS-232 mode** or **USB-COM mode** using either: - A physical switch on the scanner - Programming barcodes from the scanner's manual Look for settings labeled "Serial Mode", "RS-232", or "USB-COM" in your scanner documentation. ::: ## Usage ### QR codes with embedded ZapScript QR codes can hold enough text to embed ZapScript directly. Use the [QR code generator](../../tokens/qr-codes.md#qr-code-generator) or another QR code tool to create a code with your ZapScript: ```zapscript **launch.random:snes ``` Scan the QR code and Zaparoo runs the ZapScript. ### Linear barcodes with mappings Linear barcode capacity depends on the format, scanner, and label size. For UPCs, EAN codes, and short custom IDs, [mappings](../../features/mappings.md) are usually the practical choice: the barcode stores an ID, and Zaparoo maps that ID to ZapScript. **Example workflow:** 1. Print a barcode with a short identifier, such as `SONIC001`. 2. Create a mapping in `mappings/barcodes.toml`: ```toml [[mappings.entry]] token_key = "id" match_pattern = "SONIC001" zapscript = "@Genesis/Sonic the Hedgehog" ``` 3. Scan the barcode. Zaparoo matches `SONIC001` and executes the mapped ZapScript. See the [mappings documentation](../../features/mappings.md) for more details on creating and managing mappings. ## Troubleshooting ### Scanner not working 1. Verify your scanner is in RS-232 or USB-COM mode, not keyboard emulation mode 2. Check the correct serial port path using the commands above 3. Check permissions (Linux/macOS): ```bash sudo usermod -a -G dialout $USER # Log out and back in for changes to take effect ``` 4. Enable debug logging (`debug_logging = true`) and check for "opened RS232 barcode reader" messages ### Nothing happens when scanning 1. For QR codes that contain ZapScript directly, verify the QR code text is valid ZapScript. 2. For mapped barcodes, verify the mapping uses `token_key = "id"` and that `match_pattern` matches the scanned value. 3. Check your scanner isn't adding unwanted prefixes or suffixes. Core strips common STX/ETX framing, but other extra characters can break exact mappings. 4. If your scanner uses different serial settings, reconfigure it to 9600 8N1 using programming barcodes from the manual. --- ## Reader Drivers Reader drivers let [Zaparoo Core](../core/index.md) communicate with different types of [reader hardware](./index.md). Each driver handles the protocol and connection method for one reader type. ## How drivers work When Zaparoo Core starts, it: 1. Loads all enabled reader drivers 2. Attempts to auto-detect connected readers when auto-detect is enabled 3. Establishes connections to detected or manually configured readers 4. Begins listening for token scans Most PN532 and ACR122U setups are auto-detected. Serial readers, MQTT, display devices, external drives, and fallback NFC drivers usually need manual configuration or explicit enabling. ## Available drivers ### NFC readers NFC readers are the most common type, supporting NFC tags, cards, and compatible toys (Amiibo, Lego Dimensions, etc.). | Driver ID | Hardware | Platforms | Documentation | | -------------------------- | ------------------ | -------------- | ---------------------------------------------- | | `pn532`, `pn532uart` | PN532 USB modules | Current Core platforms | [PN532 USB](./nfc/pn532-usb.md) | | `pn532i2c`, `pn532spi` | PN532 bare modules | Current Core platforms | [PN532 Module](./nfc/pn532-module.md) | | `libnfcacr122` | ACR122U reader | Linux-based platforms | [ACR122U](./nfc/acr122u.md) | | `acr122pcsc` | ACR122U reader | Windows | [ACR122U](./nfc/acr122u.md) | | `legacypn532uart` (legacy) | PN532 USB fallback | Linux-based platforms | See [legacy NFC drivers](#legacy-nfc-drivers) | | `legacypn532i2c` (legacy) | PN532 I2C fallback | Linux-based platforms | See [legacy NFC drivers](#legacy-nfc-drivers) | :::note Backward compatibility Driver IDs with underscores, such as `pn532_uart` and `simple_serial`, are still supported. Core strips underscores internally, but new configurations should use the non-underscore driver IDs shown here. ::: ### Optical readers | Driver ID | Hardware | Platforms | Documentation | | -------------- | --------------------- | --------------------- | -------------------------------------------- | | `opticaldrive` | CD/DVD/Blu-ray drives | Linux-based platforms | [Optical Drive](./optical-drive.md) | ### Display devices | Driver ID | Hardware | Platforms | Documentation | | ---------- | ------------------------ | ------------- | ---------------------------------- | | `tty2oled` | TTY2OLED serial displays | Current Core platforms | [TTY2OLED](./tty2oled.md) | `tty2oled` is disabled by default. Enable it before use. ### Barcode readers | Driver ID | Hardware | Platforms | Documentation | | -------------- | ------------------------- | ------------- | -------------------------------------------- | | `rs232barcode` | RS-232 barcode/QR scanners | Current Core platforms | [RS-232 Scanner](./barcode/rs232.md) | :::info App Barcode Scanning The Zaparoo App can also scan barcodes and QR codes using your device's camera. Scanned codes are sent to Core via the API and don't require a dedicated reader driver. ::: ### Protocol and virtual readers These drivers support custom hardware, automation, and non-reader token sources: | Driver ID | Purpose | Platforms | Documentation | | --------------- | ------------------------------ | ------------- | ---------------------------------------------- | | `mqtt` | MQTT broker integration | Current Core platforms | [MQTT Reader](./mqtt.md) | | `externaldrive` | USB/SD cards as tokens | Current Core platforms | [External Drive](./external-drive.md) | | `simpleserial` | Custom microcontroller readers | Current Core platforms | [Simple Serial](./simple-serial.md) | | `file` | File-based virtual reader | Current Core platforms | [File Reader](./file.md) | `externaldrive` is disabled by default. Enable it directly or add a manual connection for the drive you want Core to watch. ## Configuration ### Auto-detection By default, Zaparoo Core automatically detects readers whose drivers support auto-detection: ```toml [readers] auto_detect = true ``` Some drivers are enabled but not auto-detected by default. For those readers, add a manual connection or enable auto-detect for the specific driver. ### Manual reader configuration To manually specify a reader, add a `readers.connect` section to your [`config.toml`](../core/config.md): ```toml [[readers.connect]] driver = "pn532uart" path = "/dev/ttyUSB0" ``` A `[[readers.connect]]` entry also enables that driver unless the driver is explicitly disabled with `[readers.drivers.DRIVER_ID]`. See the individual reader page for driver-specific paths and options. ### Driver-specific settings You can control individual drivers with `readers.drivers` sections: ```toml [readers.drivers.tty2oled] enabled = true [readers.drivers.simpleserial] enabled = false auto_detect = false ``` Use `[readers.drivers.DRIVER_ID]` for driver settings. `[[readers.drivers]]` is not valid config syntax. ## Troubleshooting ### Reader not detected 1. Check auto-detect is enabled with `auto_detect = true` in `config.toml`. 2. Enable debug logging with `debug_logging = true` in `config.toml`. 3. Check the hardware connection. Make sure the USB cable is a data cable, not power-only. 4. Review the hardware-specific reader page for setup and troubleshooting notes. 5. Try manual configuration with `[[readers.connect]]`. ### Multiple readers You can connect multiple readers simultaneously: ```toml [[readers.connect]] driver = "pn532uart" path = "/dev/ttyUSB0" [[readers.connect]] driver = "pn532uart" path = "/dev/ttyUSB1" [[readers.connect]] driver = "opticaldrive" path = "/dev/sr0" ``` Each reader operates independently and can scan tokens. ## Legacy NFC drivers Core v2.6.0 switched PN532 readers to the newer `pn532` driver. The old libnfc-based PN532 drivers are still available as fallback options if a reader worked before v2.6.0 but has trouble with the newer driver. ### When to use legacy drivers Use the legacy drivers if you experience: - Connection issues with PN532 readers that worked before v2.6.0 - Problems reading or writing specific tag types - I2C communication issues on embedded platforms ### Configuration Manually configure your reader to use the legacy driver: For PN532 UART/USB readers: ```toml [[readers.connect]] driver = "legacypn532uart" path = "/dev/ttyUSB0" ``` For PN532 I2C readers: ```toml [[readers.connect]] driver = "legacypn532i2c" path = "/dev/i2c-1" ``` ### Limitations The legacy drivers have the following limitations compared to the new driver: - Higher CPU usage - MIFARE tags require pre-formatting before use - No FeLiCa tag support - Less responsive feel when scanning - Windows is not supported ### Reporting issues If you need to use the legacy drivers, please report the issue you're experiencing with the new driver on [GitHub](https://github.com/ZaparooProject/zaparoo-core/issues) or [Discord](https://zaparoo.org/discord). Reports help improve the current PN532 driver. --- ## External Drive Reader A USB stick or SD card can work as a [Zaparoo token](../tokens/index.md). Put a `zaparoo.txt` file on the drive, plug it into the device running [Core](../core/index.md), and Zaparoo scans the file contents. This is useful if you want physical tokens without buying [NFC cards](../tokens/nfc/index.md) or a separate reader. ## Platforms ## Enable the reader The reader is disabled by default. Add this to your [`config.toml`](../core/config.md): ```toml [[readers.connect]] driver = "externaldrive" enabled = true ``` Restart Core after changing the config. A `readers.connect` entry makes this reader active unless you have explicitly disabled the driver elsewhere. You can also enable the driver directly: ```toml [readers.drivers.externaldrive] enabled = true ``` ## Make a drive token 1. Format a USB stick or SD card using a filesystem your device can mount. 2. Create a plain text file named `zaparoo.txt` at the top level of the drive. 3. Put the [ZapScript](../zapscript/index.md) in the file. 4. Eject the drive, then insert it into the device running Core. Example `zaparoo.txt`: ```zapscript **launch.random:SNES ``` The filename is matched case-insensitively, so `zaparoo.txt`, `ZAPAROO.TXT`, and similar casing work. ## What happens when you insert it When the drive mounts, Core looks for `zaparoo.txt`. If it finds one, it trims surrounding whitespace and scans the remaining text as an `externaldrive` token. On Linux, Core can also find the file on sibling partitions mounted under `/media` or `/mnt`, which helps with multi-partition USB drives. Removing or ejecting the drive clears the active token, similar to removing an NFC card from a reader. Core ignores the file if: - It is empty or only contains whitespace - It is inside a folder instead of at the top level of the drive - It is a folder named `zaparoo.txt` rather than a file - It is a symlink - It is larger than 1 MiB The reader does not write anything back to the drive. You create and edit `zaparoo.txt` yourself. ## Detected drives Core watches for removable drive mount events. It does not scan every mounted disk on the system, and that distinction matters for USB hard drives. - Windows detects drives reported by Windows as removable, such as many USB sticks and SD card readers. - Linux uses UDisks2 when available. Without UDisks2, Core falls back to `/proc/mounts` and only watches removable mounts under `/media` or `/mnt`. - MiSTer, Batocera, SteamOS, and similar Linux-based systems depend on the fallback behavior if UDisks2 is not available. - macOS detects removable volumes mounted under `/Volumes`. Some USB hard drives and SSDs are reported by the operating system as fixed or internal disks. Those may not be picked up by this reader. ## Troubleshooting ### Drive not detected Check your config first: ```toml [[readers.connect]] driver = "externaldrive" enabled = true ``` If you use driver settings, make sure the driver has not been disabled: ```toml [readers.drivers.externaldrive] enabled = true ``` Then check the basics: 1. Restart Core after changing `config.toml`. 2. Confirm the operating system mounted the drive. 3. On Linux-based systems, check whether the drive mounted under `/media` or `/mnt`. 4. Enable `debug_logging = true` in `config.toml` and check the Core logs. ### File ignored Make sure `zaparoo.txt` is a normal text file at the top level of the drive. The name can use any case. The file must not be empty, whitespace-only, a symlink, or larger than 1 MiB. ### ZapScript not running Test the same text with another reader or through the app. If it still does not work, the ZapScript probably needs fixing. Check the [ZapScript docs](../zapscript/index.md) for syntax. --- ## File Reader The File Reader treats a text file as a [Zaparoo reader](./index.md). When the file contains text, Core reads that text as a [ZapScript](../zapscript/index.md) token. When the file is empty, the token is removed. This is useful for scripts, local tools, and automation that need to trigger Zaparoo without physical reader hardware. ## Platforms ## Configure the reader Add a `file` reader to your [`config.toml`](../core/config.md): ```toml [[readers.connect]] driver = 'file' path = '/tmp/zaparoo-token.txt' ``` On Windows, use an absolute path to a writable location: ```toml [[readers.connect]] driver = 'file' path = 'C:/zaparoo/input.txt' ``` The path must be absolute. The parent directory must already exist, but Core creates the watched file if it is missing when the reader starts. ## Use the file Write token text or ZapScript into the file to scan it: ```bash echo "**launch.random:SNES" > /tmp/zaparoo-token.txt ``` Clear the file to remove the active token: ```bash echo "" > /tmp/zaparoo-token.txt ``` On Windows PowerShell: ```powershell Set-Content -Path C:/zaparoo/input.txt -Value '**launch.random:SNES' Clear-Content -Path C:/zaparoo/input.txt ``` ## Behavior notes - Core checks the file about every 100 ms. - Surrounding whitespace is trimmed, including trailing newlines. - Empty or whitespace-only file contents mean there is no active token. - Writing different text scans a new token. - Writing the same trimmed text again is ignored as a duplicate. - You can control the token text only. UID is not configurable through this reader. - Writing physical tokens through the File Reader is not supported. ## Troubleshooting If changes are not detected, check these first: - The `path` in `config.toml` is absolute. - The parent directory exists. - Zaparoo Core can read and write the file. - Core was restarted after changing `config.toml`. - The file contains non-whitespace text when you expect a scan. Clear the file contents to remove the token. Do not delete the watched file while Core is running. If the file was deleted and scans stop, recreate the file and restart Core. --- ## Readers Readers are the physical hardware that scan [tokens](../tokens/index.md) and trigger actions in Zaparoo. Choose from ready-to-use USB readers, DIY modules, optical drives, or even virtual software-based readers. :::caution Hardware Selection Buying generic hardware? Pay attention to the recommendations in the docs. The hardware can be complicated without guidance, and it's easy to buy stuff that won't work or is low quality. ::: :::tip The Zaparoo Shop stocks official NFC readers and cards that are tested and ready to use out of the box. ::: ## Comparison | Reader | Token Types | Setup | Platforms | | ------ | ----------- | ----- | --------- | | [PN532 USB](./nfc/pn532-usb.md) | NFC/RFID | Plug and play | All | | [PN532 Module](./nfc/pn532-module.md) | NFC/RFID | DIY wiring required | All | | [ACR122U](./nfc/acr122u.md) | NFC/RFID | Plug and play, limited on PCSC | Linux-based platforms; Windows scanning only | | [RC522](./nfc/rc522.md) | NFC/RFID | Via Simple Serial | All | | [App/Camera Scanner](./barcode/index.md) | Barcodes, QR codes | Via Zaparoo App | All | | [RS232 Scanner](./barcode/rs232.md) | Barcodes, QR codes | Manual config | All | | [Optical Drive](./optical-drive.md) | CDs, DVDs, Blu-rays | Manual config | Linux-based platforms except MiSTeX | | [MQTT Reader](./mqtt.md) | Virtual/any | Network configuration | All | | [File Reader](./file.md) | Text files | No hardware needed | All | ## Reader Types ### NFC Readers - **[PN532 USB](./nfc/pn532-usb.md)** (Recommended) - **[PN532 Module](./nfc/pn532-module.md)** - **[ACR122U](./nfc/acr122u.md)** - **[RC522](./nfc/rc522.md)** ### Optical Readers - **[Optical Drive](./optical-drive.md)** ### Display Devices - **[TTY2OLED](./tty2oled.md)** ### Barcode Readers - **[Barcode Scanners](./barcode/index.md)** - App-based and hardware scanners ### Custom & Virtual Readers - **[MQTT Reader](./mqtt.md)** - **[External Drive Reader](./external-drive.md)** - **[Simple Serial Protocol](./simple-serial.md)** - **[File Reader](./file.md)** ## Platform compatibility Each reader page includes a platform support card with current platform notes. Check those cards before buying hardware, especially for [ACR122U](./nfc/acr122u.md), [RC522](./nfc/rc522.md), and [Optical Drive](./optical-drive.md) setups. ## NFC Readers NFC readers are the most popular option, working with NFC tags, cards, and compatible toys. - **[NFC Overview](./nfc/index.md)** - General information about NFC readers - **[PN532 USB](./nfc/pn532-usb.md)** - Ready-to-use USB NFC readers - **[PN532 Module](./nfc/pn532-module.md)** - Bare NFC modules for DIY projects - **[ACR122U](./nfc/acr122u.md)** - Common commercial NFC reader - **[RC522](./nfc/rc522.md)** - Basic RFID module for microcontrollers ### Supported Tokens All NFC readers can scan: - [NFC tags](../tokens/nfc/index.md) (NTAG, MIFARE) - [NFC toys](../tokens/nfc-toys/index.md) (Amiibo, Lego Dimensions) - [PCB cards](../tokens/pcb-cards.md) - Custom NFC-enabled items ## Getting Started 1. **Choose your reader** - See recommendations above 2. **Get tokens** - [NFC tags](../tokens/nfc/index.md), [QR codes](../tokens/qr-codes.md), or [optical discs](./optical-drive.md) 3. **Install Zaparoo Core** - [Platform guides](../platforms/index.mdx) 4. **Configure your reader** - Most work automatically! 5. **Start scanning** - Write [ZapScript](../zapscript/index.md) to your tokens ## Troubleshooting **Reader not detected?** - Check [Reader Drivers](./drivers.md) for configuration help - Enable `debug_logging = true` in your [config.toml](../core/config.md) - Review hardware-specific troubleshooting in reader docs **Which reader should I buy?** - Start with the [PN532 USB](./nfc/pn532-usb.md) if you want a ready-to-use NFC reader - It has broad platform support and needs the least setup **Can I use multiple readers?** - Yes! Connect as many readers as you want - Each reader operates independently --- --- ## MQTT Reader The MQTT reader lets [Zaparoo Core](../core/index.md) listen to an MQTT topic and treat each non-empty message as [ZapScript](../zapscript/index.md). Use it when another system already speaks MQTT, such as [Home Assistant](https://www.home-assistant.io/) or a custom button, script, or device that publishes launch commands. ## Platforms ## Configure the reader Add an MQTT reader to your [`config.toml`](../core/config.md): ```toml [[readers.connect]] driver = "mqtt" path = "localhost:1883/zaparoo/tokens" ``` The `path` is the broker address followed by the topic Core should subscribe to: `broker:port/topic`. The example above connects to `localhost:1883` and listens on `zaparoo/tokens`. Restart Core after changing the config. MQTT readers are not auto-detected, so the `[[readers.connect]]` entry is what enables this reader. For an encrypted broker, use `mqtts://` in the reader path: ```toml [[readers.connect]] driver = "mqtt" path = "mqtts://broker.example.com:8883/zaparoo/tokens" ``` ## Add broker credentials If your broker needs a username and password, add them to [`auth.toml`](../core/config.md#auth-file). Match the broker address, not the full topic path: ```toml ["mqtt://broker.example.com:1883"] username = "your_username" password = "your_password" ``` If you want the same credentials to work with either `mqtt://` or `mqtts://`, use only the host and port: ```toml ["192.168.1.100:1883"] username = "your_username" password = "your_password" ``` For encrypted brokers, use an `mqtts://` credential key. Core also treats `ssl://` auth entries as `mqtts://`. ## Send a message Publish ZapScript to the topic configured in `config.toml`: ```bash mosquitto_pub -h localhost -t zaparoo/tokens -m "@Genesis/Sonic the Hedgehog" ``` The `@` prefix is shorthand for a [title launch](../zapscript/launch.md#launchtitle). You can send any valid ZapScript, including commands such as `**launch.random:snes`. Choose whatever topic name fits your setup. For example, you could use `zaparoo/tokens`, `zaparoo/devices/living_room/tokens`, or `homeassistant/zaparoo/commands`. The important part is that the published topic matches the topic in your reader `path`. The MQTT reader is read-only. It can receive ZapScript, but it cannot write anything back to NFC tags, cards, or other tokens. ## Home Assistant integration Set up Home Assistant's MQTT integration to use the same broker as Zaparoo Core, then publish ZapScript from an automation. This example runs when an input button changes: ```yaml automation: - alias: "Launch Random SNES Game" triggers: - trigger: state entity_id: input_button.random_game actions: - action: mqtt.publish data: topic: "zaparoo/tokens" payload: "**launch.random:snes" ``` Change `topic` to match your reader path, and change `payload` to the ZapScript you want to run. ## Troubleshooting ### No connection Enable debug logging in `config.toml`, restart Core, then check the Core logs: ```toml debug_logging = true ``` For a working connection, you should see messages like: ```text mqtt reader: connected to localhost:1883 mqtt reader: subscribed to topic zaparoo/tokens ``` If Core does not connect, check the broker host, port, network access, and whether encrypted brokers use `mqtts://` in the reader path. ### Authentication fails Check that your `auth.toml` entry matches the broker address and port. Do not include the MQTT topic in the auth key. For example, if the reader path is `mqtts://broker.example.com:8883/zaparoo/tokens`, the auth key should be `mqtts://broker.example.com:8883`. ### Messages do nothing Check these first: 1. The publisher is using the same topic as the reader path. 2. The payload is not empty. 3. The payload is valid ZapScript. 4. Core logs show the MQTT message being received. Test the same ZapScript with another reader or through the app. If it still does not work, the ZapScript probably needs fixing. --- ## ACR122U The ACR122U, also sold as ACR122, is a USB NFC reader in a finished case with a built-in cable, status LED, and beeper. It can scan supported Zaparoo [NFC tags](../../tokens/nfc/index.md) and cards, but it has more compatibility caveats than the [PN532 USB](./pn532-usb.md). Internally it uses a PN532 NFC chip, but the host device does not talk to that chip the same way it talks to PN532 USB modules. Zaparoo uses different ACR122U drivers depending on the platform. :::warning Before you buy If you're buying your first reader, a [PN532 USB](./pn532-usb.md) is a safer choice. It avoids the ACR122U clone compatibility issues described below. The Zaparoo Shop stocks official PN532 readers that support the project. ::: :::info The ACR122U is no longer produced by its original designer [ACS](https://www.acs.com.hk/en/), so new listings are usually clone hardware. Internal differences between clones can affect Zaparoo compatibility, and they are not always visible from the listing. Some clones only work through [PC/SC](https://pcscworkgroup.com/specifications/), which means they may work on Windows but fail on Linux-based Zaparoo platforms where [Zaparoo Core](../../core/index.md) uses [libnfc](https://github.com/nfc-tools/libnfc) for this reader. ::: ## Platforms :::warning Linux-based platforms Zaparoo uses the libnfc ACR122U driver on Linux-based platforms. [NTAG](../../tokens/nfc/ntag.md) read/write support works, but [MIFARE Classic](../../tokens/nfc/mifare.md) write support is limited and some MIFARE cards may need manual formatting or may not work. The reader LED and beeper also do not work with this driver, and some clone variants are incompatible. ::: ## Configure the reader Zaparoo Core auto-detects supported ACR122U readers by default. On Linux-based platforms, Core uses the `libnfcacr122` driver. This does not need a PC/SC daemon, but some clone variants are incompatible. With the libnfc driver, it is normal for the reader LED and beeper not to react when scanning a card. On Windows, Core uses the `acr122pcsc` driver through PC/SC. This can scan tags, but writing tags through Zaparoo is not supported with this driver. ### Manual configuration Auto-detection should work on supported platforms, but if auto-detection fails or you need to specify a particular reader, you can manually configure it. On Windows, the `path` must match the PC/SC reader name: ```toml [[readers.connect]] driver = "acr122pcsc" path = "ACS ACR122 0" ``` To find the PC/SC reader name, check [Device Manager](https://www.lifewire.com/device-manager-2625860) under **Smart card readers**. ## Linux-based platforms An ACR122U on Linux-based platforms should be detected automatically unless `auto_detect` is disabled in [`config.toml`](../../core/config.md). If the reader is detected but the LED and beeper do not react, that is expected with the libnfc driver. If the reader lights up or beeps but Core does not detect scans, it may be a clone that only works through PC/SC. ## Windows On Windows, the reader should appear under **Smart card readers** in Device Manager. The LED usually lights red and flashes green when a card is scanned. The reader may also beep on scan. The Windows PC/SC driver can scan tags, but it cannot write tags through Zaparoo. If you want to write tags from Zaparoo, use a [PN532 USB](./pn532-usb.md) or the [Zaparoo App](../../app/index.md) instead. ### Troubleshooting If Windows does not detect the reader, check these items: Enable the Smart Card Plug and Play service via regedit: | Description | Value | | -------------- | ----------------------------------------- | | Registry Hive | HKEY_LOCAL_MACHINE | | Registry Path | SOFTWARE\Policies\Microsoft\Windows\ScPnP | | Value Name | EnableScPnP | | Value Type | REG_DWORD | | Enabled Value | 1 | | Disabled Value | 0 | Install the [official ACS ACR122U driver](https://www.acs.com.hk/en/driver/3/acr122u-usb-nfc-reader/). You can confirm it is working when **ACR122 Smart Card Reader** appears in the **Smart card readers** section of Device Manager. Make sure **Smart Card** and **Smart Card Device Enumeration** are running in the [Services Manager](https://www.thewindowsclub.com/open-windows-services). Their startup types should be **Automatic** and **Automatic (Trigger Start)**. Reboot Windows after changing drivers, registry values, or services. ## Where to buy The ACR122U can be found by searching for "ACR122U" on sites like Amazon, eBay and AliExpress. :::warning Please be aware there's still a risk of receiving an incompatible variant even from these listings. ::: These are some known working listings submitted by users: - Amazon (US) - Yosoo Store - Amazon (US) - ACS Store - Kogan (Australia) - [Everything ID (Australia)](https://www.everythingid.com.au/rfid-equipment-c-13/acr122u-usb-nfc-rfid-card-reader-writer-mifare-nfc-p-324) - AliExpress (China) - 5YOA Official Store --- ## NFC Readers NFC readers let Zaparoo scan [NFC tags](../../tokens/nfc/index.md), NFC cards, and compatible NFC toys. The token stores ZapScript or other token text, not the game itself. If you are buying your first reader, start with the [PN532 USB](./pn532-usb.md). It is the ready-to-use option in this section, and it is the reader stocked in the Zaparoo Shop. ## Which reader to choose | Reader | Use it when | Main caveat | | --- | --- | --- | | [PN532 USB](./pn532-usb.md) | You want a USB reader that works without wiring. | Some versions of Windows may need a third-party USB serial driver. | | [PN532 Module](./pn532-module.md) | You are building a custom reader or embedding NFC in another project. | Requires wiring, soldering, and a USB-to-serial adapter or direct bus connection. | | [ACR122U](./acr122u.md) | You already have one, or you specifically want this reader style. | Clone hardware support varies, and Windows scanning is read-only in Zaparoo. | | [RC522](./rc522.md) | You have a microcontroller project that can send scans to Core. | Not a direct Zaparoo NFC reader; it needs the [Simple Serial protocol](../simple-serial.md). | ## Reading and writing PN532 readers can read and write supported NFC tags through Zaparoo. That includes [NTAG](../../tokens/nfc/ntag.md) tags and [MIFARE Classic](../../tokens/nfc/mifare.md), though NTAG is the better choice when buying new tags. ACR122U support depends on the platform and driver. It can scan on Windows through PC/SC, but writing tags through Zaparoo is not supported there. On Linux-based platforms, Core uses [libnfc](https://github.com/nfc-tools/libnfc) for ACR122U support. RC522 setups are read-only from Zaparoo's point of view because the microcontroller sends scan text over Simple Serial. ## Before you buy Check the reader-specific page before ordering hardware. NFC modules and ACR122U-style readers are often sold as clones, and the same listing can change over time. If you are unsure, use the [PN532 USB](./pn532-usb.md) page first. The other readers are still useful, but they are better fits for existing hardware, custom builds, or experiments. --- ## PN532 Module The PN532 Module is a bare NFC reader board for custom builds. It can scan and write supported [NFC tags](../../tokens/nfc/index.md) through Zaparoo. Use it when you want to mount a reader inside a case, wire it into another device, or connect it directly to a board that exposes UART or I2C. If you only want a reader you can plug in and use, choose the [PN532 USB](./pn532-usb.md) instead. ## Platforms :::warning DIY hardware This is not a finished USB reader. Expect wiring, soldering, and some troubleshooting. ::: For a complete photographed build with a case and custom serial PCB, see the [DIY Reader](../../community-projects/diy-reader.md) community project. That guide shows the soldering, board assembly, and case fitment, and the custom serial PCB avoids the form factor and voltage issues found with some generic USB-to-serial adapters. :::caution Clone quality Many PN532 modules sold online are clones. They can work with Zaparoo, but read range and board quality vary by seller and revision. ::: ## What you need For the usual USB UART setup, you need: 1. A PN532 module. 2. A USB-to-serial adapter, such as a CH340, CP2102, or FT232RL board. 3. Wires or pin headers. 4. Basic soldering tools. ## Wire the module UART is the most practical connection for a Zaparoo host because it appears as a normal serial port. Most modules ship with the DIP switches already set to HSU, which is the UART mode used for USB-to-serial adapters. Check the switch labels before wiring the module. For HSU/UART wiring, use the labels on your PN532 board and USB-to-serial adapter: 1. Connect `GND` on the PN532 module to `GND` on the adapter. 2. Connect `TX` or `TXD` on the PN532 module to `RX` or `RXD` on the adapter. 3. Connect `RX` or `RXD` on the PN532 module to `TX` or `TXD` on the adapter. 4. Connect `VCC` to a voltage your PN532 module supports. Check the markings on your module before connecting power. Some boards accept both 3.3V and 5V; others are stricter. ## Configure the reader [Zaparoo Core](../../core/index.md) can auto-detect PN532 UART readers by default. If auto-detection does not find the module, add it to your [`config.toml`](../../core/config.md) with the `pn532uart` driver. Linux-based platforms usually expose the USB-to-serial adapter as `/dev/ttyUSB0` or `/dev/ttyACM0`: ```toml [[readers.connect]] driver = "pn532uart" path = "/dev/ttyUSB0" ``` On Windows, use the COM port shown in [Device Manager](https://www.lifewire.com/device-manager-2625860): ```toml [[readers.connect]] driver = "pn532uart" path = "COM3" ``` On macOS, use the matching `/dev/cu.*` device: ```toml [[readers.connect]] driver = "pn532uart" path = "/dev/cu.usbserial-1234" ``` ## I2C Direct I2C connections are for embedded setups, such as a Raspberry Pi-style board with an exposed I2C bus. I2C is supported, but it does not auto-detect. You must add the reader to `config.toml`. Set the module to I2C mode and configure the bus device: ```toml [[readers.connect]] driver = "pn532i2c" path = "/dev/i2c-1" ``` ## Troubleshooting ### Module not detected Check these first: 1. `TX` on the module goes to `RX` on the adapter, and `RX` goes to `TX`. 2. The DIP switches match the configured transport: HSU/UART or I2C. 3. The module has stable power. If you are using a generic USB-to-serial adapter and the reader is unreliable, compare it with the DIY Reader build that uses a custom serial PCB. 4. For UART, the USB-to-serial adapter appears as a device path or COM port. 5. For I2C, the `path` in `config.toml` matches the I2C device you connected the module to. ### Scans are unreliable Move the module away from metal, dense wiring, and other electronics. If the tag only reads at very close range, the module may be a low-quality clone. ### Writing fails Keep the tag still on the reader while writing. If writing [MIFARE Classic](../../tokens/nfc/mifare.md) tags fails repeatedly, test with an [NTAG](../../tokens/nfc/ntag.md) tag before replacing the reader. ## Where to buy For a ready-to-use reader, use the PN532 USB page instead. Original hardware: - Elechouse - Official PN532 module Other modules are usually listed as `PN532 Module`: - Amazon - eBay - AliExpress - Local electronics shops Known listings submitted by users: - AliExpress (China) - JIAQISHENG JQS Official Store - AliExpress (China) - TENSTAR ROBOT Store --- ## PN532 USB The PN532 USB is the ready-to-use NFC reader option for Zaparoo. It connects over USB serial, scans supported [NFC tags](../../tokens/nfc/index.md) and cards, and can write tokens through Zaparoo. :::tip This is the reader stocked in the Zaparoo Shop. Official hardware purchases support development. ::: ## Platforms ## Configure the reader [Zaparoo Core](../../core/index.md) auto-detects PN532 USB readers by default. In most setups, connect the reader to a normal USB port, start Core, and scan a tag. If auto-detection does not find it, add the serial port to your [`config.toml`](../../core/config.md). Use the `pn532uart` driver for USB serial modules. Use the serial device or COM port assigned by your operating system. Linux-based platforms usually expose the reader as `/dev/ttyUSB0` or `/dev/ttyACM0`: ```toml [[readers.connect]] driver = "pn532uart" path = "/dev/ttyUSB0" ``` On Windows, use the COM port shown in [Device Manager](https://www.lifewire.com/device-manager-2625860): ```toml [[readers.connect]] driver = "pn532uart" path = "COM3" ``` On macOS, use the matching `/dev/cu.*` device: ```toml [[readers.connect]] driver = "pn532uart" path = "/dev/cu.usbserial-1234" ``` ## Platform notes ### MiSTer On [MiSTer](../../platforms/mister/index.md), use one of the normal USB ports. The SNAC/USER port looks like USB, but it is not a standard USB port and will not work for NFC readers. ### Windows PN532 USB readers use a USB serial driver on Windows. If Windows does not create a COM port for the reader, install or reinstall the driver. :::info CH340 Driver Installation You can download the [CH340 driver (ZIP)](https://zaparoo.org/drivers/Windows-CH340-Driver.zip) or [CH340 driver (EXE)](https://zaparoo.org/drivers/CH341SER.EXE). The [SparkFun CH340 installation guide](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all) has driver install steps if you need them. ::: ### Linux-based platforms Desktop Linux users may need to add their account to the `dialout` group before Core can open the serial port: ```bash sudo usermod -a -G dialout $USER ``` Log out and back in after changing groups. ### macOS The PN532 USB reader is supported on macOS. If auto-detection fails, configure the `/dev/cu.*` serial device manually. ## Troubleshooting ### Reader not detected Check these first: 1. Use a USB cable that supports data, not a charge-only cable. 2. Try a different normal USB port. On MiSTer, do not use the SNAC/USER port. 3. Check that the reader appears as a serial device or COM port. 4. On Linux-based platforms, check serial port permissions. 5. On Windows, install or reinstall the [CH340 driver](#windows) if no COM port appears. 6. Enable `debug_logging = true` in `config.toml`, restart Core, and check the logs for PN532 detection attempts. ### Slow or inconsistent scanning Keep the reader away from metal surfaces and other electronics. If it is connected through a hub, test it directly on the host device. ## Where to buy :::tip Official Support Buying from the Zaparoo Shop directly supports the project and includes a case. ::: Original hardware: - Elechouse - Official PN532 USB module Other modules are usually listed as `PN532 Type C` or `PCR532`: - AliExpress (China) - Allinbest Store - AliExpress (China) - MI YU KOUNG Official Store --- ## RC522 The RC522 is an RFID module based on the [MFRC522](https://www.nxp.com/products/rfid-nfc/nfc-hf/nfc-readers/standard-performance-mifare-and-ntag-frontend:MFRC52202HN1) chip from NXP Semiconductors. It operates at 13.56 MHz and is usually used in microcontroller projects. [Zaparoo Core](../../core/index.md) does not talk to RC522 modules directly. To use one, connect it to a microcontroller and have that microcontroller send scans to Core using the [Simple Serial protocol](../simple-serial.md). :::warning The RC522 is not a substitute for a PN532 reader. The chip supports ISO/IEC 14443 A/MIFARE and NTAG, but Zaparoo cannot use it through the PN532 reader driver and cannot write tags through Simple Serial. ::: ## Platforms :::note Simple Serial required The RC522 is not a direct USB reader. Every supported platform requires a microcontroller that sends scans to Zaparoo using Simple Serial. ::: ## Configure the reader Configure the microcontroller as a Simple Serial reader. Simple Serial uses `115200` baud. Core receives the text your microcontroller sends; it does not manage the RC522 hardware or write data back to tags. For example, a microcontroller can send this line when it scans a tag. The `\t` and `\n` sequences represent tab and newline characters: ```text SCAN\ttext=Genesis/Sonic The Hedgehog\n ``` ## Limitations compared to PN532 - Requires microcontroller firmware; it is not a direct USB reader for Zaparoo Core. - Zaparoo cannot write tags through Simple Serial. - Tag compatibility depends on your RC522 hardware and firmware. ## Where to buy Search for `RC522 RFID module` or `MFRC522 module` at: - Local electronics stores and hobby shops - Amazon, eBay, AliExpress - Arduino/Raspberry Pi suppliers :::tip For a direct USB reader, use the [PN532 USB](./pn532-usb.md) instead. ::: --- ## Optical Drive Reader An optical disc can work as a [Zaparoo token](../tokens/index.md) on Linux-based systems. Connect a CD, DVD, or Blu-ray drive to the device running [Core](../core/index.md), point the reader at the drive device, and Core uses the disc ID reported by Linux. This reader does not read games or media from the disc. It uses the disc as a physical trigger for a [mapping](../features/mappings.md) or [ZapScript](../zapscript/index.md) command. ## Platforms ## Enable the reader The optical drive reader is available on supported Linux-based platforms, but you usually need to tell Core which drive device to watch. Linux optical drives usually appear as `/dev/sr0`, `/dev/sr1`, and so on. Add a `readers.connect` entry to your [`config.toml`](../core/config.md): ```toml [[readers.connect]] driver = "opticaldrive" path = "/dev/sr0" ``` Restart Core after changing the config. Use `lsblk` or check `/dev/sr*` if you are not sure which path your drive uses. ## Choose the scanned ID Core checks the configured drive about once a second. It asks Linux `blkid` for the disc `UUID` and `LABEL`, then scans the result as a `disc` token. The `id_source` option controls which value is used for matching: ```toml [[readers.connect]] driver = "opticaldrive" path = "/dev/sr0" id_source = "merged" ``` The available modes are: - `merged` combines UUID and label as `/