Files
ares/mia/medium/arcade.cpp
T
ZengGengSen 30131cd32c Fc/vs unisystem support (#2594)
## Overview

Adds Nintendo Vs. UniSystem support to the ares FC core. Games remain in
MAME
ZIP format and load through the existing Arcade path, without going
through
the iNES detection path used by regular Famicom software.

The implementation covers MAME metadata generation, parent ZIP lookup,
Vs. PPU
and palette behavior, cartridge boards, protection logic, cabinet I/O,
DIP
switches, controller wiring, Zapper input, and save states. A total of
41 Vs.
UniSystem sets have been validated. Vs. DualSystem, non-standard bootleg
hardware, and other configurations that have not yet been implemented
are
explicitly marked as unsupported instead of being started with the
UniSystem
model.

## Why `vsnes.py` is separate

MAME groups three substantially different hardware families in
`nintendo/vsnes.cpp`:

- Vs. UniSystem with one CPU and one PPU;
- Vs. DualSystem with two CPUs and two PPUs;
- non-standard bootleg hardware built around a Z80 and SN76489.

The generic MAME parser reads common data such as machine entries, ROM
regions,
and DIP switches. Those fields alone are not enough to determine which
Vs.
hardware model a game requires. The three families have different
runtime
structures and cannot share a single emulation path.

Configurations also differ between Vs. UniSystem games. The PPU,
palette,
cartridge board, protection logic, controller wiring, Zapper support,
and DIP
settings may all vary by set. The core needs explicit configuration data
rather than inferring hardware from a game title.

`vsnes.py` reuses the MAME XML and ROM-region information produced by
the
generic parser, then adds hardware classification and per-game
configuration.
It generates a deterministic `VsSystem.bml` database. At runtime,
supported
sets select the corresponding FC hardware model from this metadata,
while
DualSystem and bootleg sets are rejected with a clear explanation.

## Video

The combined video shows the attract modes of all 41 supported games.
Each
game is shown for approximately ten seconds, with its name in the
upper-right corner.



https://github.com/user-attachments/assets/46a59b2b-f64a-40ce-a870-1351d5df3948



https://github.com/user-attachments/assets/7927d1d0-4349-4f4b-9092-feeb7b0ac127



https://github.com/user-attachments/assets/d162c834-b227-497b-afe7-2f766f3ab42a



https://github.com/user-attachments/assets/12d7b58e-7cd5-4bbf-b040-9d615e654e0c



https://github.com/user-attachments/assets/a8708059-4369-48e1-be1f-42d15099d7a8


## Screenshots

The contact sheet contains one attract-mode screenshot for each game,
composed
at the original capture resolution.

<img width="3920" height="5328" alt="contact-sheet"
src="https://github.com/user-attachments/assets/e111cf05-4a49-4b0d-9719-082b2b925342"
/>

## Supported sets

The following 41 Vs. UniSystem MAME sets have been validated for loading
and
attract-mode operation:

| No. | MAME short name | Game |
| ---: | --- | --- |
| 1 | `btlecity` | Vs. Battle City |
| 2 | `cluclu` | Vs. Clu Clu Land |
| 3 | `cstlevna` | Vs. Castlevania |
| 4 | `drmario` | Vs. Dr. Mario |
| 5 | `duckhunt` | Vs. Duck Hunt (set DH3 E) |
| 6 | `excitebk` | Vs. Excitebike (set EB4-3 E) |
| 7 | `excitebkj` | Vs. Excitebike (set EB4-4 A) |
| 8 | `excitebko` | Vs. Excitebike (set EB4-3 ?) |
| 9 | `goonies` | Vs. The Goonies (set E) |
| 10 | `hogalley` | Vs. Hogan's Alley (set HA4-1 E-1) |
| 11 | `iceclimb` | Vs. Ice Climber (set IC4-4 B-1) |
| 12 | `iceclimba` | Vs. Ice Climber (set IC4-4 ?) |
| 13 | `jajamaru` | Vs. Ninja Jajamaru Kun (Japan) |
| 14 | `ladygolf` | Vs. Stroke & Match Golf (Ladies Version, set LG4 ?)
|
| 15 | `ladygolfe` | Vs. Stroke & Match Golf (Ladies Version, set LG4 E)
|
| 16 | `nvs_machrider` | Vs. Mach Rider (Endurance Course Version) |
| 17 | `nvs_machridera` | Vs. Mach Rider (Fighting Course Version, set
MR4-1 A) |
| 18 | `nvs_mightybj` | Vs. Mighty Bomb Jack (Japan) |
| 19 | `nvs_platoon` | Vs. Platoon |
| 20 | `rbibb` | Vs. Atari R.B.I. Baseball (set 1) |
| 21 | `rbibba` | Vs. Atari R.B.I. Baseball (set 2) |
| 22 | `skatekds` | Vs. Skate Kids. (Graphic hack of Super Mario Bros.)
|
| 23 | `smgolf` | Vs. Stroke & Match Golf (Men Version, set GF4-2 F) |
| 24 | `smgolfb` | Vs. Stroke & Match Golf (Men Version, set GF4-2 ?) |
| 25 | `smgolfj` | Vs. Stroke & Match Golf (Men Version) (Japan, set GF3
B) |
| 26 | `starlstr` | Vs. Star Luster |
| 27 | `suprmrio` | Vs. Super Mario Bros. (set SM4-4 E) |
| 28 | `suprmrioa` | Vs. Super Mario Bros. (set ?, harder) |
| 29 | `supxevs` | Vs. Super Xevious |
| 30 | `tkoboxng` | Vs. T.K.O. Boxing |
| 31 | `topgun` | Vs. Top Gun |
| 32 | `vsfdf` | Vs. Freedom Force |
| 33 | `vsgradus` | Vs. Gradius (US, set GR E) |
| 34 | `vsgshoe` | Vs. Gumshoe (set GM5) |
| 35 | `vspinbal` | Vs. Pinball (US, set PN4 E-1) |
| 36 | `vspinbalj` | Vs. Pinball (Japan, set PN3 B) |
| 37 | `vsskykid` | Vs. Super SkyKid |
| 38 | `vsslalom` | Vs. Slalom |
| 39 | `vssoccer` | Vs. Soccer (set SC4-2 A) |
| 40 | `vssoccera` | Vs. Soccer (set SC4-3 ?) |
| 41 | `vstetris` | Vs. Tetris |
2026-08-20 16:35:50 +01:00

236 lines
7.6 KiB
C++

struct VsSystemDatabase {
enum class Status : u32 {
Successful,
NotFound,
CouldNotParse,
};
auto load() -> Status;
auto database() -> Database;
auto manifest(string name) -> string;
auto document(string name) -> Markup::Node;
};
auto VsSystemDatabase::load() -> Status {
for(auto& database : Media::databases) {
if(database.name == "VsSystem") return Status::Successful;
}
auto databaseFile = locate("Database/VsSystem.bml");
if(!inode::exists(databaseFile)) return Status::NotFound;
Database database;
database.name = "VsSystem";
database.list = BML::unserialize(file::read(databaseFile));
if(!database.list) return Status::CouldNotParse;
Media::databases.push_back(std::move(database));
return Status::Successful;
}
auto VsSystemDatabase::database() -> Database {
if(load() != Status::Successful) return {};
for(auto& database : Media::databases) {
if(database.name == "VsSystem") return database;
}
return {};
}
auto VsSystemDatabase::manifest(string name) -> string {
auto source = database();
for(auto node : source.list) {
if(node["name"].string().iequals(name)) return BML::serialize(node);
}
return {};
}
auto VsSystemDatabase::document(string name) -> Markup::Node {
return BML::unserialize(manifest(name));
}
struct Arcade : Mame {
auto name() -> string override { return "Arcade"; }
auto extensions() -> std::vector<string> override { return {}; }
auto database() -> Database override;
auto load(string location) -> LoadResult override;
auto save(string location) -> bool override;
private:
auto loadGeneric(string location, Markup::Node& document) -> LoadResult;
auto loadVs(string location, Markup::Node& document) -> LoadResult;
};
auto Arcade::database() -> Database {
auto combined = Medium::database();
combined.name = "Arcade";
combined.list = combined.list.clone();
VsSystemDatabase vsDatabase;
auto vs = vsDatabase.database();
for(auto node : vs.list) {
if(node["hardware/topology"].string() != "unisystem") continue;
auto nodeName = node["name"].string();
bool duplicate = false;
for(auto existing : combined.list) {
if(existing["name"].string().iequals(nodeName)) {
duplicate = true;
break;
}
}
if(!duplicate) combined.list.append(node.clone());
}
return combined;
}
auto Arcade::load(string location) -> LoadResult {
manifest = {};
auto shortName = Medium::name(location);
auto genericAvailable = Medium::loadDatabase();
if(genericAvailable) manifest = manifestDatabaseArcade(shortName);
if(manifest) {
auto document = BML::unserialize(manifest);
if(!document) return couldNotParseManifest;
return loadGeneric(location, document);
}
VsSystemDatabase vsDatabase;
auto vsStatus = vsDatabase.load();
if(vsStatus == VsSystemDatabase::Status::CouldNotParse) {
return {couldNotParseManifest, "VsSystem.bml"};
}
if(vsStatus == VsSystemDatabase::Status::Successful) {
manifest = vsDatabase.manifest(shortName);
if(manifest) {
auto document = BML::unserialize(manifest);
if(!document) return couldNotParseManifest;
return loadVs(location, document);
}
}
if(!genericAvailable && vsStatus == VsSystemDatabase::Status::NotFound) {
return {databaseNotFound, "Arcade.bml and VsSystem.bml"};
}
return romNotFoundInDatabase;
}
auto Arcade::loadGeneric(string location, Markup::Node& document) -> LoadResult {
auto resolver = [&](string parent) -> Markup::Node {
return BML::unserialize(manifestDatabaseArcade(parent));
};
// Sega SG-1000 based arcade
if(document["game/board"].string() == "sega/sg1000a") {
auto rom = loadRoms(location, document, "maincpu", resolver);
if(!rom) return {invalidROM, assemblyError(rom)};
if(rom.data.empty()) {
return {invalidROM, "Ensure your ROM is in a MAME-compatible .zip format."};
}
this->location = location;
pak = std::make_shared<vfs::directory>();
pak->setAttribute("board", document["game/board" ].string());
pak->setAttribute("name", document["game/name" ].string());
pak->setAttribute("title", document["game/title"].string());
pak->setAttribute("region", document["game/region"].string());
pak->append("manifest.bml", manifest);
pak->append("program.rom", rom.data);
return successful;
}
// Aleck 64
if(document["game/board"].string() == "nintendo/aleck64") {
auto rom = loadRoms(location, document, "user2", resolver);
if(!rom) return {invalidROM, assemblyError(rom)};
if(rom.data.empty()) {
return {invalidROM, "Ensure your ROM is in a MAME-compatible .zip format."};
}
// MAME stores ROMs in byte-swapped (v64) format, but we need native big-endian (z64).
if(!endianSwap(rom.data)) {
return {invalidROM, "Aleck64 program ROM has an invalid byte-swap extent."};
}
auto pif = loadRoms(location, document, "user1", resolver);
if(!pif) return {invalidROM, assemblyError(pif)};
if(pif.data.empty()) return {
invalidROM,
"Ensure your ROM is in a MAME-compatible .zip format and that the Aleck64 pif ROM is available."
};
this->location = location;
pak = std::make_shared<vfs::directory>();
pak->setAttribute("board", document["game/board" ].string());
pak->setAttribute("name", document["game/name" ].string());
pak->setAttribute("title", document["game/title"].string());
pak->setAttribute("region", document["game/region"].string());
pak->setAttribute("cic", "CIC-NUS-5101");
pak->append("manifest.bml", manifest);
pak->append("program.rom", rom.data);
pak->append("pif.aleck64.rom", pif.data);
return successful;
}
return otherError;
}
auto Arcade::loadVs(string location, Markup::Node& document) -> LoadResult {
auto topology = document["game/hardware/topology"].string();
if(topology == "unsupported") {
auto reason = document["game/hardware/reason"].string();
if(reason != "dualsystem"
&& reason != "raid-protection"
&& reason != "bootleg-z80") {
return {invalidROM, "Invalid Vs. System metadata field: game/hardware/reason. "};
}
return {unsupportedMedia, reason};
}
VsSystemDatabase vsDatabase;
auto resolver = [&](string parent) -> Markup::Node {
return vsDatabase.document(parent);
};
auto program = loadRoms(location, document, "prg", resolver);
if(!program) return {invalidROM, assemblyError(program)};
auto mapper = document["game/hardware/mapper"].string();
AssemblyResult character;
bool hasCharacter = (bool)document["game/gfx1"];
if(hasCharacter) {
character = loadRoms(location, document, "gfx1", resolver);
if(!character) return {invalidROM, assemblyError(character)};
}
auto palette = loadRoms(location, document, "ppu1-palette", resolver);
if(!palette) return {invalidROM, assemblyError(palette)};
this->location = location;
pak = std::make_shared<vfs::directory>();
pak->setAttribute("board", "nintendo/vs");
pak->setAttribute("name", document["game/name"].string());
pak->setAttribute("title", document["game/title"].string());
pak->setAttribute("region", "NTSC");
pak->setAttribute("system", "Vs. UniSystem");
pak->setAttribute("ppu", document["game/hardware/ppu"].string());
pak->setAttribute("mapper", mapper);
pak->setAttribute("protection", document["game/hardware/protection"].string());
pak->setAttribute("input", document["game/hardware/input"].string());
pak->append("manifest.bml", manifest);
pak->append("program.rom", program.data);
if(hasCharacter) pak->append("character.rom", character.data);
pak->append("palette.rom", palette.data);
return successful;
}
auto Arcade::save(string location) -> bool {
return true;
}