mirror of
https://github.com/ares-emulator/ares.git
synced 2026-10-12 02:11:04 +00:00
## 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 |
236 lines
7.6 KiB
C++
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;
|
|
}
|