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 |
This commit is contained in:
ZengGengSen
2026-08-20 16:35:50 +01:00
committed by GitHub
parent b80f67d383
commit 30131cd32c
56 changed files with 9241 additions and 270 deletions
+2
View File
@@ -5,6 +5,8 @@ namespace name=Resource
binary name=Logo1x file=logo.png
binary name=Logo2x [email protected]
namespace name=Sprite
namespace name=Famicom
binary name=Crosshair file=sprite/fc/crosshair.png
namespace name=SuperFamicom
binary name=CrosshairBlue file=sprite/sfc/crosshair-blue.png
binary name=CrosshairGreen file=sprite/sfc/crosshair-green.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

+31 -1
View File
@@ -14,6 +14,7 @@ ares_add_sources(
UNITY #
fc.cpp
PRIMARY #
vs/unisystem.cpp
system/system.cpp
controller/controller.cpp
expansion/expansion.cpp
@@ -26,6 +27,18 @@ ares_add_sources(
ares_add_sources(CORE fc INCLUDED fc.hpp CMakeLists.txt)
ares_add_sources(
CORE #
fc
INCLUDED #
vs/unisystem.hpp
vs/controls.cpp
vs/dip-switches.cpp
vs/io.cpp
vs/standard-input.cpp
vs/zapper-input.cpp
)
ares_add_sources(
CORE #
fc
@@ -121,6 +134,22 @@ ares_add_sources(
cartridge/board/unl-sc127.cpp
cartridge/board/unl-txc.cpp
cartridge/board/unrom-512.cpp
cartridge/board/vs-unisystem.cpp
)
ares_add_sources(
CORE #
fc
INCLUDED #
cartridge/board/vs/board.cpp
cartridge/board/vs/board.hpp
cartridge/board/vs/mmc1.cpp
cartridge/board/vs/namco-108.cpp
cartridge/board/vs/protection.hpp
cartridge/board/vs/standard.cpp
cartridge/board/vs/sunsoft-3.cpp
cartridge/board/vs/unrom.cpp
cartridge/board/vs/vrc1.cpp
)
ares_add_sources(
@@ -212,9 +241,10 @@ ares_add_sources(
CORE #
fc
INCLUDED #
ppu/color.cpp
ppu/debugger.cpp
ppu/memory.cpp
ppu/model.cpp
ppu/model.hpp
ppu/ppu.hpp
ppu/render.cpp
ppu/scroll.cpp
+4
View File
@@ -57,6 +57,8 @@ namespace Board {
#include "taito-x1-017.cpp"
#include "tengen-80003x.cpp"
#include "unrom-512.cpp"
#include "vs/board.cpp"
#include "vs-unisystem.cpp"
#include "unl-bmc.cpp"
#include "unl-jy.cpp"
@@ -132,6 +134,8 @@ auto Interface::create(string board) -> Interface* {
if(!p) p = Sc127::create(board);
if(!p) p = TXC::create(board);
if(!p) p = VsUniSystem::create(board);
if(!p) p = new Interface;
return p;
}
+78
View File
@@ -0,0 +1,78 @@
struct VsUniSystem : Interface {
static auto create(string id) -> Interface* {
if(id == "nintendo/vs") return new VsUniSystem;
return nullptr;
}
private:
Memory::Readable<n8> programROM;
Memory::Readable<n8> characterROM;
Memory::Writable<n8> characterRAM;
Memory::Writable<n8> workRAM;
Memory::Writable<n8> nametableRAM;
std::unique_ptr<Vs::Interface> board;
std::unique_ptr<Vs::Protection> protection;
n8 coinLatch;
public:
auto load() -> void override {
Interface::load(programROM, "program.rom");
Interface::load(characterROM, "character.rom");
if(!characterROM) characterRAM.allocate(0x2000);
workRAM.allocate(0x0800, 0x00);
nametableRAM.allocate(0x1000);
auto characterSize = characterROM ? characterROM.size() : characterRAM.size();
board.reset(Vs::Interface::create(pak->attribute("mapper"), programROM.size(), characterSize));
protection.reset(Vs::Protection::create(pak->attribute("protection")));
}
auto readPRG(n32 address, n8 data) -> n8 override {
if(address >= 0x4020 && address <= 0x5fff) return protection->readPRG(address, coinLatch);
if(address >= 0x6000 && address <= 0x7fff) return workRAM.read(address & 0x07ff);
if(address >= 0x8000) {
if(auto mapped = board->addressPRG(address)) return programROM.read(*mapped);
}
return data;
}
auto writePRG(n32 address, n8 data) -> void override {
if(address == 0x4016) return board->writePRG(address, data);
if(address >= 0x4020 && address <= 0x5fff) {
coinLatch = data;
return;
}
if(address >= 0x6000 && address <= 0x7fff) return workRAM.write(address & 0x07ff, data);
if(address >= 0x8000) return board->writePRG(address, data);
}
auto readCHR(n32 address, n8 data) -> n8 override {
if(address & 0x2000) return nametableRAM.read(address & 0x0fff);
if(auto mapped = board->addressCHR(address)) {
if(characterROM) return characterROM.read(*mapped);
if(characterRAM) return characterRAM.read(*mapped);
}
return data;
}
auto writeCHR(n32 address, n8 data) -> void override {
if(address & 0x2000) return nametableRAM.write(address & 0x0fff, data);
if(!characterRAM) return;
if(auto mapped = board->addressCHR(address)) return characterRAM.write(*mapped, data);
}
auto power() -> void override {
coinLatch = 0;
board->power();
protection->power();
}
auto serialize(serializer& s) -> void override {
s(characterRAM);
s(workRAM);
s(nametableRAM);
s(coinLatch);
board->serialize(s);
protection->serialize(s);
}
};
+31
View File
@@ -0,0 +1,31 @@
namespace Vs {
#include "board.hpp"
#include "standard.cpp"
#include "vrc1.cpp"
#include "unrom.cpp"
#include "mmc1.cpp"
#include "namco-108.cpp"
#include "sunsoft-3.cpp"
#include "protection.hpp"
auto Interface::create(string id, u32 programSize, u32 characterSize) -> Interface* {
if(id == "standard" && (programSize == 0x8000 || programSize == 0xa000)) {
return new Standard(programSize, characterSize);
}
if(id == "vrc1") return new VRC1(programSize, characterSize);
if(id == "unrom") return new UNROM(programSize, characterSize);
if(id == "mmc1") return new MMC1(programSize, characterSize);
if(id == "namco-108") return new Namco108(programSize, characterSize);
if(id == "sunsoft-3") return new Sunsoft3(programSize, characterSize);
return new Interface(programSize, characterSize);
}
auto Protection::create(string id) -> Protection* {
if(id == "rbi-baseball") return new RBIBaseball;
if(id == "tko-boxing") return new TKOBoxing;
if(id == "super-xevious") return new SuperXevious;
return new Protection;
}
}
+62
View File
@@ -0,0 +1,62 @@
struct Interface {
Interface(u32 programSize, u32 characterSize)
: programBanks(programSize / 0x2000), characterBanks(characterSize / 0x0400) {}
virtual ~Interface() = default;
static auto create(string id, u32 programSize, u32 characterSize) -> Interface*;
virtual auto addressPRG(n32 address) -> maybe<n32> { return {}; }
virtual auto writePRG(n32 address, n8 data) -> void {}
virtual auto addressCHR(n32 address) -> maybe<n32> { return {}; }
virtual auto power() -> void {}
virtual auto serialize(serializer& s) -> void {}
protected:
auto mapPRG(n32 bank, n32 address) -> maybe<n32> {
if(!programBanks) return {};
return {(bank % programBanks) * 0x2000 | (address & 0x1fff)};
}
auto mapCHR(n32 bank, n32 address) -> maybe<n32> {
if(!characterBanks) return {};
return {(bank % characterBanks) * 0x0400 | (address & 0x03ff)};
}
auto initializePRG(n8 banks[4]) -> void {
for(auto slot : range(4)) banks[slot] = 0;
if(programBanks < 4) return;
for(auto slot : range(4)) banks[slot] = programBanks - 4 + slot;
}
auto initializeCHR(n8 banks[8]) -> void {
for(auto slot : range(8)) banks[slot] = slot;
}
auto selectPRG8(n8 banks[4], u32 slot, n32 bank) -> void {
if(!programBanks) return;
banks[slot & 3] = bank & (programBanks - 1);
}
auto selectPRG16(n8 banks[4], u32 slot, n32 bank) -> void {
if(!programBanks) return;
bank = bank * 2 & (programBanks - 1);
slot = (slot & 1) * 2;
banks[slot + 0] = bank + 0;
banks[slot + 1] = bank + 1;
}
auto selectPRG32(n8 banks[4], n32 bank) -> void {
if(!programBanks) return;
bank = bank * 4 & (programBanks - 1);
for(auto slot : range(4)) banks[slot] = bank + slot;
}
auto selectCHR(n8 banks[8], u32 start, u32 count, n32 bank) -> void {
if(!characterBanks) return;
bank &= characterBanks - 1;
for(auto slot : range(count)) banks[start + slot] = bank + slot;
}
u32 programBanks;
u32 characterBanks;
};
+73
View File
@@ -0,0 +1,73 @@
struct MMC1 : Interface {
using Interface::Interface;
auto addressPRG(n32 address) -> maybe<n32> override {
if(address < 0x8000) return {};
return mapPRG(programBank[address >> 13 & 3], address);
}
auto writePRG(n32 address, n8 data) -> void override {
if(address < 0x8000) return;
if(data.bit(7)) {
shiftCount = 0;
program16K = 1;
switchLow = 1;
return;
}
shiftRegister = shiftRegister >> 1 | data.bit(0) << 4;
shiftCount = (shiftCount + 1) % 5;
if(shiftCount) return;
switch(address >> 13 & 3) {
case 0:
character4K = shiftRegister.bit(4);
program16K = shiftRegister.bit(3);
switchLow = shiftRegister.bit(2);
break;
case 1:
if(character4K) selectCHR(characterBank, 0, 4, shiftRegister * 4);
else selectCHR(characterBank, 0, 8, (shiftRegister & ~1) * 4);
break;
case 2:
if(character4K) selectCHR(characterBank, 4, 4, shiftRegister * 4);
break;
case 3:
if(program16K) selectPRG16(programBank, !switchLow, shiftRegister);
else selectPRG32(programBank, shiftRegister >> 1);
break;
}
}
auto addressCHR(n32 address) -> maybe<n32> override {
return mapCHR(characterBank[address >> 10 & 7], address);
}
auto power() -> void override {
initializePRG(programBank);
initializeCHR(characterBank);
shiftRegister = 0;
shiftCount = 0;
program16K = 0;
switchLow = 0;
character4K = 0;
}
auto serialize(serializer& s) -> void override {
s(programBank);
s(characterBank);
s(shiftRegister);
s(shiftCount);
s(program16K);
s(switchLow);
s(character4K);
}
n8 programBank[4];
n8 characterBank[8];
n5 shiftRegister;
n3 shiftCount;
n1 program16K;
n1 switchLow;
n1 character4K;
};
+42
View File
@@ -0,0 +1,42 @@
struct Namco108 : Interface {
using Interface::Interface;
auto addressPRG(n32 address) -> maybe<n32> override {
if(address < 0x8000) return {};
return mapPRG(programBank[address >> 13 & 3], address);
}
auto writePRG(n32 address, n8 data) -> void override {
if(address < 0x8000) return;
switch(address & 0x6001) {
case 0x0000:
bankSelect = data.bit(0, 2);
break;
case 0x0001:
if(bankSelect <= 1) selectCHR(characterBank, bankSelect * 2, 2, data);
if(bankSelect >= 2 && bankSelect <= 5) selectCHR(characterBank, bankSelect + 2, 1, data);
if(bankSelect >= 6) selectPRG8(programBank, bankSelect - 6, data);
break;
}
}
auto addressCHR(n32 address) -> maybe<n32> override {
return mapCHR(characterBank[address >> 10 & 7], address);
}
auto power() -> void override {
initializePRG(programBank);
initializeCHR(characterBank);
bankSelect = 0;
}
auto serialize(serializer& s) -> void override {
s(programBank);
s(characterBank);
s(bankSelect);
}
n8 programBank[4];
n8 characterBank[8];
n3 bankSelect;
};
+88
View File
@@ -0,0 +1,88 @@
struct Protection {
virtual ~Protection() = default;
static auto create(string id) -> Protection*;
virtual auto readPRG(n32 address, n8 data) -> n8 { return data; }
virtual auto power() -> void {}
virtual auto serialize(serializer& s) -> void {}
};
struct RBIBaseball : Protection {
auto readPRG(n32 address, n8 data) -> n8 override {
if(address != 0x5600 && address != 0x5601 && address != 0x5e00 && address != 0x5e01) return data;
if(!(address & 1)) {
index = 0;
return 0;
}
return sequence[index++ & 0x1f];
}
auto power() -> void override {
index = 0;
}
auto serialize(serializer& s) -> void override {
s(index);
}
static constexpr u8 sequence[32] = {
0xff, 0xfd, 0xf5, 0xf4, 0xb4, 0xb4, 0xa6, 0x2e,
0x2f, 0x6f, 0x6f, 0x7d, 0xd5, 0xd4, 0x94, 0x94,
0x86, 0x2e, 0x2f, 0x6f, 0x6b, 0x79, 0xd1, 0xd0,
0x92, 0x92, 0x8d, 0x65, 0x64, 0x34, 0xb0, 0xa2,
};
n8 index;
};
struct TKOBoxing : Protection {
auto readPRG(n32 address, n8 data) -> n8 override {
if(address != 0x5e00 && address != 0x5e01) return data;
if(!(address & 1)) {
index = 0;
return 0;
}
return sequence[index++ & 0x1f];
}
auto power() -> void override {
index = 0;
}
auto serialize(serializer& s) -> void override {
s(index);
}
static constexpr u8 sequence[32] = {
0xff, 0xbf, 0xb7, 0x97, 0x97, 0x17, 0x57, 0x4f,
0x6f, 0x6b, 0xeb, 0xa9, 0xb1, 0x90, 0x94, 0x14,
0x56, 0x4e, 0x6f, 0x6b, 0xeb, 0xa9, 0xb1, 0x90,
0xd4, 0x5c, 0x3e, 0x26, 0x87, 0x83, 0x13, 0x00,
};
n8 index;
};
struct SuperXevious : Protection {
auto readPRG(n32 address, n8 data) -> n8 override {
if(address == 0x54ff) {
index ^= 1;
return 0x05;
}
if(address == 0x5678) return index ? 0x01 : 0x00;
if(address == 0x578f) return index ? 0x89 : 0xd1;
if(address == 0x5567) return index ? 0x37 : 0x3e;
return data;
}
auto power() -> void override {
index = 0;
}
auto serialize(serializer& s) -> void override {
s(index);
}
n1 index;
};
+35
View File
@@ -0,0 +1,35 @@
struct Standard : Interface {
using Interface::Interface;
auto addressPRG(n32 address) -> maybe<n32> override {
if(address < 0x8000) return {};
n32 slot = (address - 0x8000) >> 13;
n32 bank = slot;
if(programBanks == 5) {
bank = programBank;
if(slot) bank = slot + 1;
}
return mapPRG(bank, address);
}
auto writePRG(n32 address, n8 data) -> void override {
if(address == 0x4016) programBank = characterBank = data.bit(2);
}
auto addressCHR(n32 address) -> maybe<n32> override {
return mapCHR(characterBank * 8 + (address >> 10 & 7), address);
}
auto power() -> void override {
programBank = programBanks == 5;
characterBank = 0;
}
auto serialize(serializer& s) -> void override {
s(programBank);
s(characterBank);
}
n1 programBank;
n1 characterBank;
};
+40
View File
@@ -0,0 +1,40 @@
struct Sunsoft3 : Interface {
using Interface::Interface;
auto addressPRG(n32 address) -> maybe<n32> override {
if(address < 0x8000) return {};
return mapPRG(programBank[address >> 13 & 3], address);
}
auto writePRG(n32 address, n8 data) -> void override {
if(address < 0x8000) return;
switch(address & 0x7800) {
case 0x0800:
case 0x1800:
case 0x2800:
case 0x3800:
selectCHR(characterBank, address >> 11 & 6, 2, data * 2);
break;
case 0x7800:
selectPRG16(programBank, 0, data);
break;
}
}
auto addressCHR(n32 address) -> maybe<n32> override {
return mapCHR(characterBank[address >> 10 & 7], address);
}
auto power() -> void override {
initializePRG(programBank);
initializeCHR(characterBank);
}
auto serialize(serializer& s) -> void override {
s(programBank);
s(characterBank);
}
n8 programBank[4];
n8 characterBank[8];
};
+27
View File
@@ -0,0 +1,27 @@
struct UNROM : Interface {
using Interface::Interface;
auto addressPRG(n32 address) -> maybe<n32> override {
if(address < 0x8000) return {};
return mapPRG(programBank[address >> 13 & 3], address);
}
auto writePRG(n32 address, n8 data) -> void override {
if(address >= 0x8000) selectPRG16(programBank, 0, data);
}
auto addressCHR(n32 address) -> maybe<n32> override {
return mapCHR(address >> 10 & 7, address);
}
auto power() -> void override {
initializePRG(programBank);
selectPRG16(programBank, 0, 0);
}
auto serialize(serializer& s) -> void override {
s(programBank);
}
n8 programBank[4];
};
+33
View File
@@ -0,0 +1,33 @@
struct VRC1 : Interface {
using Interface::Interface;
auto addressPRG(n32 address) -> maybe<n32> override {
if(address < 0x8000) return {};
return mapPRG(programBank[address >> 13 & 3], address);
}
auto writePRG(n32 address, n8 data) -> void override {
if(address < 0x8000) return;
auto index = address >> 12 & 7;
if(index == 0 || index == 2 || index == 4) selectPRG8(programBank, index >> 1, data);
if(index == 6) selectCHR(characterBank, 0, 4, data * 4);
if(index == 7) selectCHR(characterBank, 4, 4, data * 4);
}
auto addressCHR(n32 address) -> maybe<n32> override {
return mapCHR(characterBank[address >> 10 & 7], address);
}
auto power() -> void override {
initializePRG(programBank);
initializeCHR(characterBank);
}
auto serialize(serializer& s) -> void override {
s(programBank);
s(characterBank);
}
n8 programBank[4];
n8 characterBank[8];
};
+1
View File
@@ -38,6 +38,7 @@ auto Cartridge::disconnect() -> void {
board->pak.reset();
board.reset();
pak.reset();
information = {};
node.reset();
}
+1
View File
@@ -24,6 +24,7 @@ struct Controller {
virtual ~Controller() = default;
virtual auto frame() -> void {}
virtual auto data() -> n3 { return 0b000; }
virtual auto latch(n1 data) -> void {}
virtual auto serialize(serializer&) -> void {}
+1
View File
@@ -7,6 +7,7 @@ struct ControllerPort {
auto unload() -> void;
auto allocate(string name) -> Node::Peripheral;
auto frame() -> void { if(device) device->frame(); }
auto data() -> n3 { if(device) return device->data(); return 0b000; }
auto latch(n1 data) -> void { if(device) return device->latch(data); }
+89 -44
View File
@@ -1,81 +1,126 @@
Zapper::Zapper(Node::Port port) {
node = port->append<Node::Peripheral>("Zapper");
node = port->append<Node::Peripheral>("Zapper");
x = node->append<Node::Input::Axis >("X");
y = node->append<Node::Input::Axis >("Y");
x = node->append<Node::Input::Axis>("X");
y = node->append<Node::Input::Axis>("Y");
trigger = node->append<Node::Input::Button>("Trigger");
sprite = node->append<Node::Video::Sprite>("Crosshair");
sprite->setImage(Resource::Sprite::SuperFamicom::CrosshairGreen);
ppu.screen->attach(sprite);
lightGun.load(node, x, y, trigger);
}
Zapper::~Zapper() {
if(ppu.screen) ppu.screen->detach(sprite);
lightGun.unload();
}
auto Zapper::data() -> n3 {
u32 next = ppu.io.ly * 283 + ppu.io.lx;
auto LightGun::load(Node::Object parent, Node::Input::Axis x, Node::Input::Axis y,
Node::Input::Button trigger) -> void {
unload();
this->parent = parent;
this->x = x;
this->y = y;
this->trigger = trigger;
cx = 256 / 2;
cy = 240 / 2;
px = 0;
py = 0;
nx = 256 / 2;
ny = 240 / 2;
sprite = parent->append<Node::Video::Sprite>("Crosshair");
sprite->setImage(Resource::Sprite::Famicom::Crosshair);
ppu.screen->attach(sprite);
}
auto LightGun::unload() -> void {
if(ppu.screen && sprite) ppu.screen->detach(sprite);
if(parent && sprite) parent->remove(sprite);
parent.reset();
x.reset();
y.reset();
trigger.reset();
sprite.reset();
}
auto LightGun::frame() -> void {
platform->input(x); //-n = left, 0 = center, +n = right
platform->input(y); //-n = up, 0 = center, +n = down
px = x->value();
py = y->value();
if(!px && !py) return;
cx = max(-8, min(256 + 8, px + cx));
cy = max(-8, min(240 + 8, py + cy));
sprite->setPosition(cx, cy);
sprite->setVisible(true);
nx = cx + 8;
ny = cy + 8;
}
auto LightGun::data() -> n3 {
n3 result = 0b000;
platform->input(trigger);
if (trigger->value()) result.bit(2) = 1;
if (next < previous) {
platform->input(x); //-n = left, 0 = center, +n = right
platform->input(y); //-n = up, 0 = center, +n = down
if (x->value() != px || y->value() != py) {
px = x->value();
py = y->value();
cx = max(-8, min(256 + 8, px + cx));
cy = max(-8, min(240 + 8, py + cy));
sprite->setPosition(cx, cy);
sprite->setVisible(true);
nx = cx + 8;
ny = cy + 8;
}
}
previous = next;
if(trigger->value()) result.bit(2) = 1;
bool offscreen = nx < 0 || ny < 0 || nx >= 256 || ny >= 240;
if (offscreen) {
if(offscreen) {
result.bit(1) = 1;
return result;
}
u32 *target = ppu.screen->pixels().data();
u32* target = ppu.screen->pixels().data();
u32 count = 0;
for(int y = ny - 8; y <= ny + 8; y++) {
if (y < 0) continue;
if (y <= ppu.io.ly - 18) continue;
if (y > ppu.io.ly) break;
if(y < 0) continue;
if(y <= ppu.io.ly - 18) continue;
if(y > ppu.io.ly) break;
for(int x = nx - 8; x <= nx + 8; x++) {
if (x < 0) continue;
if (x >= 256) break;
if(x < 0) continue;
if(x >= 256) break;
n32 offset = y * 283 + x;// y * 283 + x + Region::PAL() ? 18 : 16;
n32 offset = y * 283 + x; //y * 283 + x + Region::PAL() ? 18 : 16;
n64 color = ppu.color(target[offset]);
f64 brightness = 0;
brightness += ((color >> 32) & 0xffff) * 0.299;
brightness += ((color >> 16) & 0xffff) * 0.587;
brightness += ((color >> 0) & 0xffff) * 0.114;
if (brightness > 0x80)
count++;
if(brightness > 0x80) count++;
}
}
if (count < 0x40)
result.bit(1) = 1;
if(count < 0x40) result.bit(1) = 1;
return result;
}
auto LightGun::serialize(serializer& s) -> void {
s(cx);
s(cy);
s(px);
s(py);
s(nx);
s(ny);
u32 legacyRaster = 0;
s(legacyRaster);
if(s.reading() && sprite) {
sprite->setPosition(cx, cy);
sprite->setVisible(true);
}
}
auto Zapper::frame() -> void {
lightGun.frame();
}
auto Zapper::data() -> n3 {
return lightGun.data();
}
auto Zapper::latch(n1 data) -> void {
}
+24 -11
View File
@@ -1,23 +1,36 @@
struct Zapper : Controller {
struct LightGun {
Node::Object parent;
Node::Input::Axis x;
Node::Input::Axis y;
Node::Input::Button trigger;
Node::Video::Sprite sprite;
Zapper(Node::Port);
~Zapper();
auto main() -> void;
auto data() -> n3 override;
auto latch(n1 data) -> void override;
auto serialize(serializer&) -> void override;
auto load(Node::Object parent, Node::Input::Axis x, Node::Input::Axis y, Node::Input::Button trigger) -> void;
auto unload() -> void;
auto frame() -> void;
auto data() -> n3;
auto serialize(serializer&) -> void;
private:
i32 cx = 256 / 2; //x-coordinate
i32 cy = 240 / 2; //y-coordinate
i32 cx = 256 / 2;
i32 cy = 240 / 2;
i32 px = 0;
i32 py = 0;
u32 nx = 256 / 2;
u32 ny = 240 / 2;
u32 previous = 0;
};
struct Zapper : Controller {
Node::Input::Axis x;
Node::Input::Axis y;
Node::Input::Button trigger;
LightGun lightGun;
Zapper(Node::Port);
~Zapper();
auto frame() -> void override;
auto data() -> n3 override;
auto latch(n1 data) -> void override;
auto serialize(serializer&) -> void override;
};
+3
View File
@@ -33,6 +33,7 @@ auto CPU::readIO(n16 address) -> n8 {
switch(address) {
case 0x4016: {
if(system.model() == System::Model::VsUniSystem) return vsUniSystem.io.read(address, data);
auto port1 = controllerPort1.data();
auto port3 = expansionPort.read1();
if(system.controls.microphone) platform->input(system.controls.microphone);
@@ -45,6 +46,7 @@ auto CPU::readIO(n16 address) -> n8 {
}
case 0x4017: {
if(system.model() == System::Model::VsUniSystem) return vsUniSystem.io.read(address, data);
auto port2 = controllerPort2.data();
auto port3 = expansionPort.read2();
data.bit(0) = port3.bit(0) | port2.bit(0);
@@ -70,6 +72,7 @@ auto CPU::writeIO(n16 address, n8 data) -> void {
}
case 0x4016: {
if(system.model() == System::Model::VsUniSystem) return vsUniSystem.io.write(data);
controllerPort1.latch(data.bit(0));
controllerPort2.latch(data.bit(0));
expansionPort.write(data);
+1
View File
@@ -1,5 +1,6 @@
//started: 2011-09-05
#include <fc/vs/unisystem.cpp>
#include <fc/system/system.cpp>
#include <fc/controller/controller.cpp>
#include <fc/expansion/expansion.cpp>
+1
View File
@@ -25,6 +25,7 @@ namespace ares::Famicom {
#include <fc/controller/controller.hpp>
#include <fc/expansion/expansion.hpp>
#include <fc/vs/unisystem.hpp>
#include <fc/system/system.hpp>
#include <fc/cartridge/cartridge.hpp>
#include <fc/cpu/cpu.hpp>
-50
View File
@@ -1,50 +0,0 @@
auto PPU::color(n32 n) -> n64 {
f64 saturation = 1.5;
f64 hue = 0.0;
f64 contrast = 1.0;
f64 brightness = 1.0;
f64 gamma = 2.2;
i32 color = (n & 0x0f), level = color < 0xe ? int(n >> 4 & 3) : 1;
static constexpr f64 black = 0.518, white = 1.962, attenuation = 0.746;
static constexpr f64 levels[8] = {
0.350, 0.518, 0.962, 1.550,
1.094, 1.506, 1.962, 1.962,
};
f64 lo_and_hi[2] = {
levels[level + 4 * (color == 0x0)],
levels[level + 4 * (color < 0xd)],
};
f64 y = 0.0, i = 0.0, q = 0.0;
auto wave = [](i32 p, i32 color) { return (color + p + 8) % 12 < 6; };
for(int p : range(12)) {
f64 spot = lo_and_hi[wave(p, color)];
if(((n & 0x040) && wave(p, 12))
|| ((n & 0x080) && wave(p, 4))
|| ((n & 0x100) && wave(p, 8))
) spot *= attenuation;
f64 v = (spot - black) / (white - black);
v = (v - 0.5) * contrast + 0.5;
v *= brightness / 12.0;
y += v;
i += v * cos((Math::Pi / 6.0) * (p + hue));
q += v * sin((Math::Pi / 6.0) * (p + hue));
}
i *= saturation;
q *= saturation;
auto gammaAdjust = [=](f64 f) { return f < 0.0 ? 0.0 : pow(f, 2.2 / gamma); };
n64 r = uclamp<16>(65535.0 * gammaAdjust(y + 0.946882 * i + 0.623557 * q));
n64 g = uclamp<16>(65535.0 * gammaAdjust(y + -0.274788 * i + -0.635691 * q));
n64 b = uclamp<16>(65535.0 * gammaAdjust(y + -1.108545 * i + 1.709007 * q));
return r << 32 | g << 16 | b << 0;
}
+4 -5
View File
@@ -30,6 +30,7 @@ auto PPU::readIO(n16 address) -> n8 {
result.bit(5) = sprite.spriteOverflow;
result.bit(6) = io.spriteZeroHit;
result.bit(7) = io.nmiFlag;
result = model->status(result);
scroll.latch = 0;
io.nmiHold = 0;
cpu.nmiLine(io.nmiFlag = 0);
@@ -39,8 +40,7 @@ auto PPU::readIO(n16 address) -> n8 {
case 4:
result = oam[sprite.oamAddress];
if (io.ly < 240 || io.ly == vlines() - 1 ||
(Region::PAL() && io.ly >= 264 && io.ly <= vlines() - 2))
if(model->raster.restrictsOAM(io.ly))
if (enable())
result = sprite.oamData;
break;
@@ -79,7 +79,7 @@ auto PPU::readIO(n16 address) -> n8 {
auto PPU::writeIO(n16 address, n8 data) -> void {
io.mdr = data;
switch(address.bit(0,2)) {
switch(model->mapWriteRegister(address.bit(0,2))) {
//PPUCTRL
case 0:
@@ -122,8 +122,7 @@ auto PPU::writeIO(n16 address, n8 data) -> void {
// sprite or background rendering is enabled) do not
// modify values in OAM, but do perform a glitchy
// increment of OAMADDR, bumping only the high 6 bits
if (io.ly < 240 || io.ly == vlines() - 1 ||
(Region::PAL() && io.ly >= 264 && io.ly <= vlines() - 2)) {
if(model->raster.restrictsOAM(io.ly)) {
if (enable()) {
++sprite.oamMainCounterIndex;
sprite.oamMainCounterTiming = 0;
+209
View File
@@ -0,0 +1,209 @@
auto PPU::Model::Raster::evaluatesSprites(u32 y) const -> bool {
return y < 240 || (y >= spriteEvaluationStart && y <= spriteEvaluationEnd);
}
auto PPU::Model::Raster::restrictsOAM(u32 y) const -> bool {
return evaluatesSprites(y) || y == scanlines - 1;
}
auto PPU::Model::Raster::pixelVisible(u32 x, u32 y) const -> bool {
return y >= visibleTop && x >= visibleLeft && x <= visibleRight;
}
auto PPU::Model::Raster::backdrop(n9 color) const -> n9 {
return fixedBackdrop ? backdropColor : color;
}
PPU::Model::Model(const Raster& raster) : raster(raster) {
}
auto PPU::Model::color(n9 input) const -> n64 {
f64 saturation = 1.5;
f64 hue = 0.0;
f64 contrast = 1.0;
f64 brightness = 1.0;
f64 gamma = 2.2;
i32 color = (input & 0x0f), level = color < 0xe ? int(input >> 4 & 3) : 1;
static constexpr f64 black = 0.518, white = 1.962, attenuation = 0.746;
static constexpr f64 levels[8] = {
0.350, 0.518, 0.962, 1.550,
1.094, 1.506, 1.962, 1.962,
};
f64 loAndHi[2] = {
levels[level + 4 * (color == 0x0)],
levels[level + 4 * (color < 0xd)],
};
f64 y = 0.0, i = 0.0, q = 0.0;
auto wave = [](i32 phase, i32 color) { return (color + phase + 8) % 12 < 6; };
for(auto phase : range(12)) {
f64 spot = loAndHi[wave(phase, color)];
if(((input & 0x040) && wave(phase, 12))
|| ((input & 0x080) && wave(phase, 4))
|| ((input & 0x100) && wave(phase, 8))
) spot *= attenuation;
f64 value = (spot - black) / (white - black);
value = (value - 0.5) * contrast + 0.5;
value *= brightness / 12.0;
y += value;
i += value * cos((Math::Pi / 6.0) * (phase + hue));
q += value * sin((Math::Pi / 6.0) * (phase + hue));
}
i *= saturation;
q *= saturation;
auto gammaAdjust = [=](f64 value) { return value < 0.0 ? 0.0 : pow(value, 2.2 / gamma); };
n64 red = uclamp<16>(65535.0 * gammaAdjust(y + 0.946882 * i + 0.623557 * q));
n64 green = uclamp<16>(65535.0 * gammaAdjust(y + -0.274788 * i + -0.635691 * q));
n64 blue = uclamp<16>(65535.0 * gammaAdjust(y + -1.108545 * i + 1.709007 * q));
return red << 32 | green << 16 | blue;
}
auto PPU::Model::mapWriteRegister(n3 address) const -> n3 {
return address;
}
auto PPU::Model::status(n8 data) const -> n8 {
return data;
}
namespace {
auto rp2c02Raster() -> PPU::Model::Raster {
return {
4, 262, 241, 242,
16, 0, 256, 240, 16,
1, 0,
0, 0, 255,
8.0, 7.0,
true, false, 0
};
}
auto rp2c07Raster() -> PPU::Model::Raster {
return {
5, 312, 241, 288,
18, 0, 256, 240, 18,
264, 310,
1, 2, 253,
55.0, 43.0,
false, true, 0x3f
};
}
auto ua6538Raster() -> PPU::Model::Raster {
return {
5, 312, 291, 242,
16, 0, 256, 240, 18,
1, 0,
0, 0, 255,
55.0, 43.0,
false, false, 0
};
}
struct RP2C02 final : PPU::Model {
RP2C02() : Model(rp2c02Raster()) {}
};
struct RP2C07 final : PPU::Model {
RP2C07() : Model(rp2c07Raster()) {}
};
struct UA6538 final : PPU::Model {
UA6538() : Model(ua6538Raster()) {}
};
struct RGBModel : PPU::Model {
RGBModel(const Palette& palette) : Model(rp2c02Raster()), palette(palette) {}
auto color(n9 color) const -> n64 override {
u32 address = (color & 0x3f) * 3;
n64 red = color.bit(6) ? 7 : palette[address + 0];
n64 green = color.bit(7) ? 7 : palette[address + 1];
n64 blue = color.bit(8) ? 7 : palette[address + 2];
return red * 0xffff / 7 << 32 | green * 0xffff / 7 << 16 | blue * 0xffff / 7;
}
const Palette palette;
};
struct RP2C04 final : RGBModel {
RP2C04(const Palette& palette) : RGBModel(palette) {}
};
struct RC2C05 : RGBModel {
RC2C05(const Palette& palette, n6 securityCode) : RGBModel(palette), securityCode(securityCode) {}
auto mapWriteRegister(n3 address) const -> n3 override {
if(address == 0) return 1;
if(address == 1) return 0;
return address;
}
auto status(n8 data) const -> n8 override {
return data & 0xc0 | securityCode;
}
const n6 securityCode;
};
struct RC2C05_01 final : RC2C05 {
RC2C05_01(const Palette& palette) : RC2C05(palette, 0x1b) {}
};
struct RC2C05_02 final : RC2C05 {
RC2C05_02(const Palette& palette) : RC2C05(palette, 0x3d) {}
};
struct RC2C05_03 final : RC2C05 {
RC2C05_03(const Palette& palette) : RC2C05(palette, 0x1c) {}
};
struct RC2C05_04 final : RC2C05 {
RC2C05_04(const Palette& palette) : RC2C05(palette, 0x1b) {}
};
}
auto PPU::createBaseModel() -> std::unique_ptr<Model> {
switch(system.region()) {
case System::Region::NTSCJ: return std::make_unique<RP2C02>();
case System::Region::NTSCU: return std::make_unique<RP2C02>();
case System::Region::PAL: return std::make_unique<RP2C07>();
case System::Region::Dendy: return std::make_unique<UA6538>();
}
return std::make_unique<RP2C02>();
}
auto PPU::createVsModel(string identifier, const Model::Palette& palette) -> std::unique_ptr<Model> {
if(identifier == "ppu2c04") return std::make_unique<RP2C04>(palette);
if(identifier == "ppu2c05_01") return std::make_unique<RC2C05_01>(palette);
if(identifier == "ppu2c05_02") return std::make_unique<RC2C05_02>(palette);
if(identifier == "ppu2c05_03") return std::make_unique<RC2C05_03>(palette);
if(identifier == "ppu2c05_04") return std::make_unique<RC2C05_04>(palette);
return {};
}
auto PPU::createVsModel() -> std::unique_ptr<Model> {
if(!cartridge.pak) return {};
auto paletteFile = cartridge.pak->read("palette.rom");
if(!paletteFile || paletteFile->size() != Model::PaletteSize) return {};
Model::Palette palette;
paletteFile->read(palette.data(), palette.size());
for(auto channel : palette) {
if(channel > 7) return {};
}
return createVsModel(cartridge.pak->attribute("ppu"), palette);
}
+40
View File
@@ -0,0 +1,40 @@
struct Model {
struct Raster {
u32 clockDivider;
u32 scanlines;
u32 vblankScanline;
u32 canvasHeight;
u32 viewportX;
u32 viewportY;
u32 viewportWidth;
u32 viewportHeight;
u32 outputOffset;
u32 spriteEvaluationStart;
u32 spriteEvaluationEnd;
u32 visibleTop;
u32 visibleLeft;
u32 visibleRight;
f64 aspectX;
f64 aspectY;
bool oddFrameCycleSkip;
bool fixedBackdrop;
n9 backdropColor;
auto evaluatesSprites(u32 y) const -> bool;
auto restrictsOAM(u32 y) const -> bool;
auto pixelVisible(u32 x, u32 y) const -> bool;
auto backdrop(n9 color) const -> n9;
};
static constexpr u32 PaletteSize = 192;
using Palette = std::array<u8, PaletteSize>;
Model(const Raster& raster);
virtual ~Model() = default;
virtual auto color(n9 color) const -> n64;
virtual auto mapWriteRegister(n3 address) const -> n3;
virtual auto status(n8 data) const -> n8;
const Raster raster;
};
+16 -15
View File
@@ -3,15 +3,17 @@
namespace ares::Famicom {
PPU ppu;
#include "model.cpp"
#include "memory.cpp"
#include "render.cpp"
#include "color.cpp"
#include "debugger.cpp"
#include "sprite.cpp"
#include "scroll.cpp"
#include "serialization.cpp"
auto PPU::load(Node::Object parent) -> void {
model = createBaseModel();
ciram.allocate(2048);
cgram.allocate(32);
oam.allocate(256);
@@ -23,7 +25,7 @@ auto PPU::load(Node::Object parent) -> void {
screen->colors(1 << 9, std::bind_front(&PPU::color, this));
screen->setSize(283, displayHeight());
screen->setScale(1.0, 1.0);
(Region::PAL() || Region::Dendy()) ? screen->setAspect(55.0, 43.0) : screen->setAspect(8.0, 7.0);
screen->setAspect(model->raster.aspectX, model->raster.aspectY);
screen->refreshRateHint(system.frequency() / rate(), 341, vlines());
debugger.load(node);
@@ -39,6 +41,7 @@ auto PPU::unload() -> void {
cgram.reset();
oam.reset();
soam.reset();
model.reset();
}
auto PPU::main() -> void {
@@ -55,7 +58,7 @@ auto PPU::step(u32 clocks) -> void {
if (enable() && io.ly == L - 1)
cyclePrepareSpriteEvaluation();
if (enable() && (io.ly < 240 || (Region::PAL() && io.ly >= 264 && io.ly <= L - 2)))
if(enable() && model->raster.evaluatesSprites(io.ly))
cycleSpriteEvaluation();
if (enable() && (io.ly < 240 || io.ly == L - 1))
@@ -97,18 +100,9 @@ auto PPU::frame() -> void {
screen->setSize(283, displayHeight());
screen->setViewport(0, 0, 283, displayHeight());
} else {
int x = 16;
int y = 0;
int width = 283 - 27;
int height = displayHeight() - 2;
if(Region::PAL()) {
x += 2;
height -= 46;
}
screen->setSize(width, height);
screen->setViewport(x, y, width, height);
auto& raster = model->raster;
screen->setSize(raster.viewportWidth, raster.viewportHeight);
screen->setViewport(raster.viewportX, raster.viewportY, raster.viewportWidth, raster.viewportHeight);
}
screen->frame();
@@ -116,6 +110,13 @@ auto PPU::frame() -> void {
}
auto PPU::power(bool reset) -> void {
if(!reset && system.model() == System::Model::VsUniSystem) {
auto candidate = createVsModel();
if(!candidate) return;
model = std::move(candidate);
screen->resetPalette();
}
Thread::create(system.frequency(), std::bind_front(&PPU::main, this));
screen->power();
+15 -6
View File
@@ -1,4 +1,6 @@
struct PPU : Thread {
#include "model.hpp"
Node::Object node;
Node::Video::Screen screen;
Memory::Writable<n8> ciram;
@@ -19,10 +21,10 @@ struct PPU : Thread {
} memory;
} debugger;
auto rate() const -> u32 { return Region::PAL() || Region::Dendy() ? 5 : 4; }
auto vlines() const -> u32 { return Region::PAL() || Region::Dendy() ? 312 : 262; }
auto displayHeight() const -> u32 { return Region::PAL() ? 288 : 242; }
auto vblankScanline() const -> u32 { return Region::Dendy() ? 291 : 241; }
auto rate() const -> u32 { return model->raster.clockDivider; }
auto vlines() const -> u32 { return model->raster.scanlines; }
auto displayHeight() const -> u32 { return model->raster.canvasHeight; }
auto vblankScanline() const -> u32 { return model->raster.vblankScanline; }
//ppu.cpp
auto load(Node::Object) -> void;
@@ -54,8 +56,8 @@ struct PPU : Thread {
auto renderPixel() -> void;
auto renderScanline() -> void;
//color.cpp
auto color(n32) -> n64;
//model.cpp
auto color(n32 c) -> n64 { return model->color(c); }
//serialization.cpp
auto serialize(serializer&) -> void;
@@ -207,6 +209,13 @@ struct PPU : Thread {
0x09, 0x01, 0x34, 0x03, 0x00, 0x04, 0x00, 0x14,
0x08, 0x3A, 0x00, 0x02, 0x00, 0x20, 0x2C, 0x08,
};
private:
auto createBaseModel() -> std::unique_ptr<Model>;
auto createVsModel() -> std::unique_ptr<Model>;
auto createVsModel(string identifier, const Model::Palette& palette) -> std::unique_ptr<Model>;
std::unique_ptr<Model> model;
};
extern PPU ppu;
+5 -9
View File
@@ -25,8 +25,7 @@ auto PPU::renderPixel() -> void {
u32 objectPalette = 0;
bool objectPriority = 0;
//PAL systems technically blank the topmost scanline and a 2px column on each side of active display.
if(Region::PAL()) if(io.ly == 0 || x < 2 || x > 253) return;
if(!model->raster.pixelVisible(x, io.ly)) return;
palette |= latch.tiledataLo & mask ? 1 : 0;
palette |= latch.tiledataHi & mask ? 2 : 0;
@@ -72,17 +71,14 @@ auto PPU::renderPixel() -> void {
color = io.emphasis << 6 | readCGRAM((n5)var.address);
}
if(Region::PAL() || Region::Dendy())
output[(x + 18) % 283] = color;
else
output[(x + 16) % 283] = color;
output[(x + model->raster.outputOffset) % 283] = color;
}
auto PPU::renderScanline() -> void {
if(io.ly < screen->canvasHeight()) {
output = screen->pixels().data() + io.ly * 283;
for (auto n : range(283))
output[n] = Region::PAL() ? 0x3f : io.emphasis << 6 | readCGRAM(0);
auto backdrop = model->raster.backdrop(io.emphasis << 6 | readCGRAM(0));
for(auto n : range(283)) output[n] = backdrop;
}
//Vblank
@@ -188,7 +184,7 @@ auto PPU::renderScanline() -> void {
//337-338
loadCHR(0x2000 | (n12)var.address);
bool skip = !Region::PAL() && !Region::Dendy() && enable() && io.field == 1 && io.ly == vlines() - 1;
bool skip = model->raster.oddFrameCycleSkip && enable() && io.field == 1 && io.ly == vlines() - 1;
step(2);
//339
+9 -5
View File
@@ -1,4 +1,4 @@
static const string SerializerVersion = "v153";
static const string SerializerVersion = "v154";
auto System::serialize(bool synchronize) -> serializer {
if(synchronize) scheduler.enter(Scheduler::Mode::Synchronize);
@@ -43,8 +43,12 @@ auto System::serialize(serializer& s, bool synchronize) -> void {
s(cpu);
s(apu);
s(ppu);
if(fds.present) s(fds);
s(controllerPort1);
s(controllerPort2);
expansionPort.serialize(s);
if(model() == Model::VsUniSystem) {
s(vsUniSystem);
} else {
if(fds.present) s(fds);
s(controllerPort1);
s(controllerPort2);
expansionPort.serialize(s);
}
}
+30 -9
View File
@@ -8,6 +8,7 @@ auto enumerate() -> std::vector<string> {
"[Nintendo] Famicom (NTSC-J)",
"[Nintendo] Famicom (NTSC-U)",
"[Nintendo] Famicom (PAL)",
"[Nintendo] Vs. UniSystem",
"[Dendy] Dendy",
};
}
@@ -38,6 +39,9 @@ auto System::game() -> string {
auto System::run() -> void {
scheduler.enter();
if(model() == Model::VsUniSystem) vsUniSystem.frame();
controllerPort1.frame();
controllerPort2.frame();
auto reset = controls.reset->value();
platform->input(controls.reset);
if(!reset && controls.reset->value()) power(true);
@@ -71,6 +75,13 @@ auto System::load(Node::System& root, string name) -> bool {
information.frequency = Constants::Colorburst::PAL * 6.0;
information.cpuDivider = 15;
}
if(name == "[Nintendo] Vs. UniSystem") {
information.model = Model::VsUniSystem;
information.name = "Famicom";
information.region = Region::NTSCU;
information.frequency = Constants::Colorburst::NTSC * 6.0;
information.cpuDivider = 12;
}
node = std::make_shared<Core::System>(information.name);
node->setAttribute("configuration", name);
@@ -89,9 +100,13 @@ auto System::load(Node::System& root, string name) -> bool {
apu.load(node);
ppu.load(node);
cartridgeSlot.load(node);
controllerPort1.load(node);
controllerPort2.load(node);
expansionPort.load(node);
if(model() == Model::VsUniSystem) {
vsUniSystem.load(node);
} else {
controllerPort1.load(node);
controllerPort2.load(node);
expansionPort.load(node);
}
return true;
}
@@ -107,17 +122,23 @@ auto System::unload() -> void {
controls.unload();
cpu.unload();
apu.unload();
ppu.unload();
cartridgeSlot.unload();
controllerPort1.unload();
controllerPort2.unload();
expansionPort.unload();
if(model() == Model::VsUniSystem) {
cartridgeSlot.unload();
vsUniSystem.unload();
ppu.unload();
} else {
ppu.unload();
cartridgeSlot.unload();
controllerPort1.unload();
controllerPort2.unload();
expansionPort.unload();
}
node = {};
}
auto System::power(bool reset) -> void {
if(model() == Model::VsUniSystem) vsUniSystem.power(reset);
for(auto& setting : node->find<Node::Setting::Setting>()) setting->setLatch();
random.entropy(Random::Entropy::Low);
// The apu should run before the cpu
apu.power(reset);
+3
View File
@@ -14,8 +14,10 @@ struct System {
auto poll() -> void;
} controls;
enum class Model : u32 { Famicom, VsUniSystem };
enum class Region : u32 { NTSCJ, NTSCU, PAL, Dendy };
auto model() const -> Model { return information.model; }
auto name() const -> string { return information.name; }
auto region() const -> Region { return information.region; }
auto frequency() const -> f64 { return information.frequency; }
@@ -36,6 +38,7 @@ struct System {
private:
struct Information {
Model model = Model::Famicom;
string name = "Famicom";
Region region = Region::NTSCJ;
f64 frequency = Constants::Colorburst::NTSC * 6.0;
+42
View File
@@ -0,0 +1,42 @@
auto VsUniSystem::Controls::load(Node::Object parent) -> bool {
unload();
if(!cartridge.pak) return false;
auto mode = cartridge.pak->attribute("input");
if(mode != "standard" && mode != "swapped" && mode != "swap-ab" && mode != "zapper") return false;
node = parent->append<Node::Object>("Controls");
service = node->append<Node::Input::Button>("Service");
coins[0] = node->append<Node::Input::Button>("Player 1 Coin");
coins[1] = node->append<Node::Input::Button>("Player 2 Coin");
n8 fixedBits = cartridge.pak->attribute("protection") == "ice-climber" ? 0x08 : 0x00;
if(mode == "standard") input = std::make_unique<StandardInput>(node, StandardInput::Wiring::Standard, fixedBits);
if(mode == "swapped") input = std::make_unique<StandardInput>(node, StandardInput::Wiring::Swapped, fixedBits);
if(mode == "swap-ab") input = std::make_unique<StandardInput>(node, StandardInput::Wiring::SwapAB, fixedBits);
if(mode == "zapper") input = std::make_unique<ZapperInput>(node);
return true;
}
auto VsUniSystem::Controls::unload() -> void {
input.reset();
service.reset();
coins[0].reset();
coins[1].reset();
if(auto parent = Node::parent(node)) parent->remove(node);
node.reset();
}
auto VsUniSystem::Controls::frame() -> void {
if(input) input->frame();
}
auto VsUniSystem::Controls::data(u32 stream) -> n1 {
if(input) return input->data(stream);
return 0;
}
auto VsUniSystem::Controls::latch() -> std::array<n8, 2> {
if(input) return input->latch();
return {};
}
+116
View File
@@ -0,0 +1,116 @@
static auto parseDipByte(string text, u8& value) -> bool {
if(!text) return false;
u32 base = 10;
u32 offset = 0;
if(text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
base = 16;
offset = 2;
}
if(offset == text.size()) return false;
u32 result = 0;
for(auto index : range(offset, text.size())) {
u32 digit;
auto character = text[(u32)index];
if(character >= '0' && character <= '9') digit = character - '0';
else if(character >= 'a' && character <= 'f') digit = character - 'a' + 10;
else if(character >= 'A' && character <= 'F') digit = character - 'A' + 10;
else return false;
if(digit >= base || result > (255 - digit) / base) return false;
result = result * base + digit;
}
value = result;
return true;
}
auto VsUniSystem::DIPSwitches::load(Node::Object parent) -> bool {
unload();
if(!cartridge.pak) return false;
auto manifestFile = cartridge.pak->read("manifest.bml");
if(!manifestFile) return false;
auto document = BML::unserialize(manifestFile->reads());
auto games = document.find("game");
if(games.size() != 1) return false;
u8 usedMasks = 0;
std::vector<Definition> definitions;
for(auto node : games.front()) {
if(node.name() != "dip") continue;
Definition definition;
definition.name = node["name"].string();
if(!definition.name) return false;
if(node["port"].string() != "DSW0") return false;
if(!parseDipByte(node["mask"].string(), definition.mask) || !definition.mask) return false;
if(usedMasks & definition.mask) return false;
if(!parseDipByte(node["default"].string(), definition.defaultValue)) return false;
if(definition.defaultValue & ~definition.mask) return false;
bool hasDefault = false;
for(auto optionNode : node) {
if(optionNode.name() != "option") continue;
Option option;
option.name = optionNode["name"].string();
if(!option.name) return false;
if(!parseDipByte(optionNode["value"].string(), option.value)) return false;
if(option.value & ~definition.mask) return false;
for(auto& existing : definition.options) {
if(existing.name == option.name) return false;
}
if(option.value == definition.defaultValue) hasDefault = true;
definition.options.push_back(std::move(option));
}
if(definition.options.empty() || !hasDefault) return false;
usedMasks |= definition.mask;
definitions.push_back(std::move(definition));
}
create(parent, std::move(definitions));
return true;
}
auto VsUniSystem::DIPSwitches::create(Node::Object parent, std::vector<Definition> definitions) -> void {
node = parent->append<Node::Object>("DIP Switches");
settings.reserve(definitions.size());
for(auto& definition : definitions) {
value |= definition.defaultValue;
auto index = settings.size();
std::vector<string> allowedValues;
for(auto& option : definition.options) allowedValues.push_back(option.name);
string defaultValue;
for(auto& option : definition.options) {
if(option.value == definition.defaultValue) defaultValue = option.name;
}
settings.push_back({{}, definition.mask, std::move(definition.options)});
auto& setting = settings.back();
setting.node = node->append<Node::Setting::String>(definition.name, defaultValue, [index](auto value) {
vsUniSystem.dipSwitches.update(index, value);
});
setting.node->setAllowedValues(std::move(allowedValues));
}
}
auto VsUniSystem::DIPSwitches::unload() -> void {
if(auto parent = Node::parent(node)) parent->remove(node);
node.reset();
settings.clear();
value = 0;
}
auto VsUniSystem::DIPSwitches::update(u32 index, string value) -> void {
if(index >= settings.size()) return;
auto& setting = settings[index];
for(auto& option : setting.options) {
if(option.name != value) continue;
this->value = (this->value & ~setting.mask) | (option.value & setting.mask);
return;
}
}
+60
View File
@@ -0,0 +1,60 @@
auto VsUniSystem::IO::readStream(u32 stream) -> n1 {
if(strobe) return vsUniSystem.controls.data(stream);
if(positions[stream] >= 8) return 1;
return streams[stream].bit(positions[stream]++);
}
auto VsUniSystem::IO::read(n16 address, n8 data) -> n8 {
data.bit(0) = readStream(address == 0x4017);
data.bit(1) = 0;
if(address == 0x4016) {
data.bit(2) = servicePulse.frames != 0;
data.bit(3, 4) = vsUniSystem.dipSwitches.value.bit(0, 1);
data.bit(5) = coinPulses[0].frames != 0;
data.bit(6) = coinPulses[1].frames != 0;
data.bit(7) = 0;
} else {
data.bit(2, 7) = vsUniSystem.dipSwitches.value.bit(2, 7);
}
return data;
}
auto VsUniSystem::IO::write(n8 data) -> void {
n1 nextStrobe = data.bit(0);
if(strobe && !nextStrobe) {
auto latched = vsUniSystem.controls.latch();
streams[0] = latched[0];
streams[1] = latched[1];
positions[0] = 0;
positions[1] = 0;
}
strobe = nextStrobe;
}
auto VsUniSystem::IO::pollPulse(Node::Input::Button input, Pulse& pulse) -> void {
if(pulse.frames) pulse.frames--;
platform->input(input);
bool pressed = input->value();
if(pressed && !pulse.held) pulse.frames = 3;
pulse.held = pressed;
}
auto VsUniSystem::IO::frame() -> void {
auto& controls = vsUniSystem.controls;
pollPulse(controls.service, servicePulse);
pollPulse(controls.coins[0], coinPulses[0]);
pollPulse(controls.coins[1], coinPulses[1]);
}
auto VsUniSystem::IO::power() -> void {
streams[0] = 0;
streams[1] = 0;
positions[0] = 0;
positions[1] = 0;
strobe = 0;
servicePulse = {};
coinPulses[0] = {};
coinPulses[1] = {};
}
+29
View File
@@ -0,0 +1,29 @@
auto VsUniSystem::Controls::ZapperInput::serialize(serializer& s) -> void {
lightGun.serialize(s);
}
auto VsUniSystem::Controls::serialize(serializer& s) -> void {
if(input) input->serialize(s);
}
auto VsUniSystem::DIPSwitches::serialize(serializer& s) -> void {
s(value);
}
auto VsUniSystem::IO::serialize(serializer& s) -> void {
s(streams);
s(positions);
s(strobe);
s(servicePulse.held);
s(servicePulse.frames);
s(coinPulses[0].held);
s(coinPulses[0].frames);
s(coinPulses[1].held);
s(coinPulses[1].frames);
}
auto VsUniSystem::serialize(serializer& s) -> void {
s(controls);
s(dipSwitches);
s(io);
}
+65
View File
@@ -0,0 +1,65 @@
VsUniSystem::Controls::StandardInput::StandardInput(Node::Object parent, Wiring wiring, n8 fixedBits)
: wiring(wiring), fixedBits(fixedBits) {
loadPlayer(parent, players[0], 1);
loadPlayer(parent, players[1], 2);
if(wiring == Wiring::Standard) live[0] = players[1].a, live[1] = players[0].a;
if(wiring == Wiring::Swapped) live[0] = players[0].a, live[1] = players[1].a;
if(wiring == Wiring::SwapAB) live[0] = players[1].b, live[1] = players[0].b;
}
auto VsUniSystem::Controls::StandardInput::loadPlayer(Node::Object parent, Player& player, u32 id) -> void {
auto prefix = string{"Player ", id, " "};
player.up = parent->append<Node::Input::Button>(string{prefix, "Up" });
player.down = parent->append<Node::Input::Button>(string{prefix, "Down" });
player.left = parent->append<Node::Input::Button>(string{prefix, "Left" });
player.right = parent->append<Node::Input::Button>(string{prefix, "Right" });
player.a = parent->append<Node::Input::Button>(string{prefix, "Button 1"});
player.b = parent->append<Node::Input::Button>(string{prefix, "Button 2"});
player.start = parent->append<Node::Input::Button>(string{prefix, "Start" });
}
auto VsUniSystem::Controls::StandardInput::pollPlayer(u32 player) -> n8 {
auto& controls = players[player];
platform->input(controls.a);
platform->input(controls.b);
platform->input(controls.start);
platform->input(controls.up);
platform->input(controls.down);
platform->input(controls.left);
platform->input(controls.right);
n8 data = fixedBits;
data.bit(0) = controls.a->value();
data.bit(1) = controls.b->value();
data.bit(2) = controls.start->value();
data.bit(4) = controls.up->value();
data.bit(5) = controls.down->value();
data.bit(6) = controls.left->value();
data.bit(7) = controls.right->value();
return data;
}
auto VsUniSystem::Controls::StandardInput::data(u32 stream) -> n1 {
platform->input(live[stream]);
return live[stream]->value();
}
auto VsUniSystem::Controls::StandardInput::latch() -> std::array<n8, 2> {
auto player1 = pollPlayer(0);
auto player2 = pollPlayer(1);
if(wiring == Wiring::Swapped) return {player1, player2};
player2.bit(2) = players[0].start->value();
player1.bit(2) = players[1].start->value();
if(wiring == Wiring::SwapAB) {
n1 player1A = player1.bit(0);
n1 player2A = player2.bit(0);
player1.bit(0) = player1.bit(1);
player1.bit(1) = player1A;
player2.bit(0) = player2.bit(1);
player2.bit(1) = player2A;
}
return {player2, player1};
}
+43
View File
@@ -0,0 +1,43 @@
#include <fc/fc.hpp>
namespace ares::Famicom {
VsUniSystem vsUniSystem;
#include "standard-input.cpp"
#include "zapper-input.cpp"
#include "controls.cpp"
#include "dip-switches.cpp"
#include "io.cpp"
#include "serialization.cpp"
auto VsUniSystem::load(Node::Object parent) -> void {
this->parent = parent;
node = parent->append<Node::Object>("Arcade");
}
auto VsUniSystem::unload() -> void {
dipSwitches.unload();
controls.unload();
io.power();
node.reset();
parent.reset();
}
auto VsUniSystem::power(bool reset) -> void {
io.power();
if(reset) return;
if(!controls.load(node)) return;
if(!dipSwitches.load(parent)) {
controls.unload();
return;
}
}
auto VsUniSystem::frame() -> void {
controls.frame();
io.frame();
}
}
+135
View File
@@ -0,0 +1,135 @@
struct VsUniSystem {
struct Controls {
Node::Object node;
Node::Input::Button service;
Node::Input::Button coins[2];
auto load(Node::Object parent) -> bool;
auto unload() -> void;
auto frame() -> void;
auto data(u32 stream) -> n1;
auto latch() -> std::array<n8, 2>;
auto serialize(serializer&) -> void;
private:
struct Input {
virtual ~Input() = default;
virtual auto frame() -> void {}
virtual auto data(u32 stream) -> n1 = 0;
virtual auto latch() -> std::array<n8, 2> = 0;
virtual auto serialize(serializer&) -> void {}
};
struct StandardInput : Input {
enum class Wiring : u32 {
Standard,
Swapped,
SwapAB,
} wiring;
n8 fixedBits;
struct Player {
Node::Input::Button up;
Node::Input::Button down;
Node::Input::Button left;
Node::Input::Button right;
Node::Input::Button a;
Node::Input::Button b;
Node::Input::Button start;
} players[2];
Node::Input::Button live[2];
StandardInput(Node::Object parent, Wiring wiring, n8 fixedBits);
auto data(u32 stream) -> n1 override;
auto latch() -> std::array<n8, 2> override;
private:
auto loadPlayer(Node::Object parent, Player& player, u32 id) -> void;
auto pollPlayer(u32 player) -> n8;
};
struct ZapperInput : Input {
Node::Input::Axis x;
Node::Input::Axis y;
Node::Input::Button trigger;
LightGun lightGun;
ZapperInput(Node::Object parent);
~ZapperInput();
auto frame() -> void override;
auto data(u32 stream) -> n1 override;
auto latch() -> std::array<n8, 2> override;
auto serialize(serializer&) -> void override;
};
std::unique_ptr<Input> input;
} controls;
struct DIPSwitches {
struct Option {
string name;
u8 value = 0;
};
struct Setting {
Node::Setting::String node;
u8 mask = 0;
std::vector<Option> options;
};
Node::Object node;
std::vector<Setting> settings;
n8 value = 0;
auto load(Node::Object parent) -> bool;
auto unload() -> void;
auto update(u32 index, string value) -> void;
auto serialize(serializer&) -> void;
private:
struct Definition {
string name;
u8 mask = 0;
u8 defaultValue = 0;
std::vector<Option> options;
};
auto create(Node::Object parent, std::vector<Definition> definitions) -> void;
} dipSwitches;
struct IO {
struct Pulse {
bool held = false;
u8 frames = 0;
};
auto power() -> void;
auto frame() -> void;
auto read(n16 address, n8 data) -> n8;
auto write(n8 data) -> void;
auto serialize(serializer&) -> void;
private:
auto readStream(u32 stream) -> n1;
auto pollPulse(Node::Input::Button input, Pulse& pulse) -> void;
n8 streams[2] = {};
u8 positions[2] = {};
n1 strobe = 0;
Pulse servicePulse;
Pulse coinPulses[2];
} io;
Node::Object parent;
Node::Object node;
auto load(Node::Object parent) -> void;
auto unload() -> void;
auto power(bool reset) -> void;
auto frame() -> void;
auto serialize(serializer&) -> void;
};
extern VsUniSystem vsUniSystem;
+27
View File
@@ -0,0 +1,27 @@
VsUniSystem::Controls::ZapperInput::ZapperInput(Node::Object parent) {
x = parent->append<Node::Input::Axis> ("X-Axis");
y = parent->append<Node::Input::Axis> ("Y-Axis");
trigger = parent->append<Node::Input::Button>("Trigger");
lightGun.load(parent, x, y, trigger);
}
VsUniSystem::Controls::ZapperInput::~ZapperInput() {
lightGun.unload();
}
auto VsUniSystem::Controls::ZapperInput::frame() -> void {
lightGun.frame();
}
auto VsUniSystem::Controls::ZapperInput::data(u32) -> n1 {
return 0;
}
auto VsUniSystem::Controls::ZapperInput::latch() -> std::array<n8, 2> {
auto gun = lightGun.data();
n8 stream = 0;
stream.bit(4) = 1; //Gun alarm wire connected
stream.bit(6) = !gun.bit(1);
stream.bit(7) = gun.bit(2);
return {stream, 0};
}
+38 -1
View File
@@ -5,6 +5,7 @@ struct Arcade : Emulator {
auto pak(ares::Node::Object) -> std::shared_ptr<vfs::directory> override;
auto group() -> string override { return "Arcade"; }
auto arcade() -> bool override { return true; }
auto gameBrowserCategories() -> std::vector<GameBrowserCategory> override;
auto input(ares::Node::Input::Input) -> void override;
string systemPakName = "Arcade";
string gamePakName = "Arcade";
@@ -45,6 +46,7 @@ Arcade::Arcade() {
{ InputDevice device{"Controls"} ;
device.digital("Service", virtualPorts[0].pad.lstick_click);
device.digital("Test", virtualPorts[0].pad.rstick_click);
for(auto n : range(2)) {
device.digital({"Player ", n + 1, " Up" }, virtualPorts[n].pad.up);
device.digital({"Player ", n + 1, " Down" }, virtualPorts[n].pad.down);
@@ -67,6 +69,11 @@ Arcade::Arcade() {
device.analog ({"Player ", n + 1, " X-Axis" }, virtualPorts[n].pad.lstick_left, virtualPorts[n].pad.lstick_right);
device.analog ({"Player ", n + 1, " Y-Axis" }, virtualPorts[n].pad.lstick_up, virtualPorts[n].pad.lstick_down);
}
device.relative("X-Axis", virtualPorts[0].mouse.x);
device.relative("Y-Axis", virtualPorts[0].mouse.y);
device.digital ("Trigger", virtualPorts[0].mouse.left);
port.append(device); }
{ InputDevice device{"Mahjong"};
@@ -97,14 +104,29 @@ Arcade::Arcade() {
}
auto Arcade::available() -> bool {
#if defined(CORE_SG) || defined(CORE_N64)
#if defined(CORE_SG) || defined(CORE_N64) || defined(CORE_FC)
return true;
#endif
return false;
}
auto Arcade::gameBrowserCategories() -> std::vector<GameBrowserCategory> {
std::vector<GameBrowserCategory> categories;
#ifdef CORE_FC
categories.push_back({"Nintendo Vs. System", "nintendo/vs"});
#endif
#ifdef CORE_N64
categories.push_back({"Nintendo Aleck 64", "nintendo/aleck64"});
#endif
#ifdef CORE_SG
categories.push_back({"Sega SG-1000 Arcade", "sega/sg1000a"});
#endif
return categories;
}
auto Arcade::load() -> LoadResult {
systemPakName = "Arcade";
gamePakName = "Arcade";
game = mia::Medium::create("Arcade");
string location = Emulator::load(game, configuration.game);
if(!location) return noFileSelected;
@@ -116,6 +138,21 @@ auto Arcade::load() -> LoadResult {
if(result != successful) return result;
//Determine from the game manifest which core to use for the given arcade rom
#ifdef CORE_FC
if(game->pak->attribute("board") == "nintendo/vs") {
if(game->pak->attribute("system") != "Vs. UniSystem") return otherError;
if(!ares::Famicom::load(root, {"[Nintendo] Vs. UniSystem"})) return otherError;
systemPakName = "Famicom";
gamePakName = "Famicom Cartridge";
if(auto port = root->find<ares::Node::Port>("Cartridge Slot")) {
port->allocate();
port->connect();
}
return successful;
}
#endif
#ifdef CORE_SG
if(game->pak->attribute("board") == "sega/sg1000a") {
if(!ares::SG1000::load(root, {"[Sega] SG-1000A"})) return otherError;
+4
View File
@@ -72,6 +72,10 @@ auto Emulator::handleLoadResult(LoadResult result) -> void {
errorText = { "There was an error trying to parse the selected ROM. \n",
"Your ROM may be corrupt or contain a bad dump. " };
break;
case unsupportedMedia:
errorText = { "This title was recognized, but its hardware configuration \n",
"is not currently supported by ares. " };
break;
case wrongMediaType:
errorText = { "The selected ROM media type is ", result.mediaType, " \n",
"which is not supported by this system. Please select \n",
+6
View File
@@ -3,6 +3,11 @@ struct Emulator {
static auto construct() -> void;
struct Firmware;
struct GameBrowserCategory {
string name;
string board;
};
virtual ~Emulator() = default;
//emulator.cpp
@@ -33,6 +38,7 @@ struct Emulator {
virtual auto notify(const string& message) -> void {}
virtual auto arcade() -> bool { return false; }
virtual auto group() -> string { return manufacturer; }
virtual auto gameBrowserCategories() -> std::vector<GameBrowserCategory> { return {}; }
virtual auto portMenu(Menu& portMenu, ares::Node::Port port) -> void {}
struct Firmware {
+45 -12
View File
@@ -10,18 +10,26 @@ GameBrowserWindow::GameBrowserWindow() {
setMinimumSize({480_sx, 320_sy});
searchLayout.setPadding(5, 0);
searchLabel.setText("Search:");
categoryList->setUsesSidebarStyle();
gameList.setHeadered();
gameList.onActivate([&](auto cell) {
auto name = cell.parent().attribute("name");
for(auto game : games) {
for(auto& game : games) {
if(game.name != name) continue;
if(program.load(emulator, game.path)) {
setVisible(false);
if(!game.available) {
program.error({"ROM archive not found: ", game.path});
return;
}
if(program.load(emulator, game.path)) setVisible(false);
return;
}
});
categoryList.onChange([&] {
refresh();
});
searchInput.onChange([&] {
refresh();
});
@@ -29,8 +37,9 @@ GameBrowserWindow::GameBrowserWindow() {
auto GameBrowserWindow::show(std::shared_ptr<Emulator> emulator) -> void {
this->emulator = emulator;
searchInput.setText();
games.clear();
categoryList.reset();
searchInput.setText();
auto tmp = std::dynamic_pointer_cast<mia::Medium>(mia::Medium::create(emulator->medium));
if(!tmp) {
@@ -39,23 +48,40 @@ auto GameBrowserWindow::show(std::shared_ptr<Emulator> emulator) -> void {
return;
}
auto categories = emulator->gameBrowserCategories();
ListViewItem all{&categoryList};
all.setText("All");
all.setAttribute("board", "");
for(auto& category : categories) {
ListViewItem item{&categoryList};
item.setText(category.name);
item.setAttribute("board", category.board);
}
auto romRoot = settings.paths.arcadeRoms;
if(!romRoot) romRoot = {mia::homeLocation(), "Arcade"};
auto db = tmp->database();
for(auto node : db.list) {
if(node["type"].string().size() && node["type"].string() != "game") continue;
auto path = settings.paths.arcadeRoms;
if(!path) path = {mia::homeLocation(), "Arcade"};
path = {path, "/", node["name"].string(), ".zip"};
auto board = node["board"].string();
auto supported = std::ranges::find_if(categories, [&](const auto& category) {
return category.board == board;
});
if(supported == categories.end()) continue;
if(inode::exists(path)) {
games.push_back({node["title"].string(), node["name"].string(), node["board"].string(), path});
}
auto path = string{romRoot, "/", node["name"].string(), ".zip"};
games.push_back({node["title"].string(), node["name"].string(), board, path, inode::exists(path)});
}
std::ranges::sort(games, [](auto x, auto y) {
std::ranges::sort(games, [](const auto& x, const auto& y) {
return string::icompare(x.title, y.title) < 0;
});
all.setSelected();
categoryList.resizeColumn();
setVisible();
setFocused();
setTitle({"Select ", emulator->medium, " Game"});
@@ -72,8 +98,12 @@ auto GameBrowserWindow::refresh() -> void {
gameList.append(TableViewColumn().setText("Board").setExpandable());
gameList.append(TableViewColumn().setText("MAME Name"));
auto board = string{};
if(auto category = categoryList.selected()) board = category.attribute("board");
auto searchText = searchInput.text();
for(auto& game : games) {
auto searchText = searchInput.text();
if(board && game.board != board) continue;
if(searchText.size()) {
if(!game.title.ifind(searchText) &&
!game.board.ifind(searchText) &&
@@ -85,6 +115,9 @@ auto GameBrowserWindow::refresh() -> void {
item.append(TableViewCell().setText(game.title));
item.append(TableViewCell().setText(game.board));
item.append(TableViewCell().setText(game.name));
if(!game.available) {
for(auto cell : item.cells()) cell.setForegroundColor(Color{255, 0, 0});
}
}
gameList.resizeColumns();
+4 -1
View File
@@ -3,6 +3,7 @@ struct GameBrowserEntry {
string name;
string board;
string path;
bool available = false;
};
struct GameBrowserWindow : Window {
@@ -14,7 +15,9 @@ struct GameBrowserWindow : Window {
HorizontalLayout searchLayout{&layout, Size{~0, 0}, 5};
Label searchLabel{&searchLayout, Size{100, 0}};
LineEdit searchInput{&searchLayout, Size{~0, 0}};
TableView gameList{&layout, Size{~0, ~0}};
HorizontalLayout browserLayout{&layout, Size{~0, ~0}, 5};
ListView categoryList{&browserLayout, Size{150_sx, ~0}};
TableView gameList{&browserLayout, Size{~0, ~0}};
std::vector<GameBrowserEntry> games;
std::shared_ptr<Emulator> emulator;
File diff suppressed because it is too large Load Diff
+185 -19
View File
@@ -1,23 +1,134 @@
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 {
auto foundDatabase = Medium::loadDatabase();
if(!foundDatabase) return { databaseNotFound, "Arcade.bml" };
manifest = manifestDatabaseArcade(Medium::name(location));
if(!manifest) return romNotFoundInDatabase;
manifest = {};
auto shortName = Medium::name(location);
auto genericAvailable = Medium::loadDatabase();
if(genericAvailable) manifest = manifestDatabaseArcade(shortName);
auto document = BML::unserialize(manifest);
if(!document) return couldNotParseManifest;
if(manifest) {
auto document = BML::unserialize(manifest);
if(!document) return couldNotParseManifest;
return loadGeneric(location, document);
}
//Sega SG-1000 based arcade
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") {
std::vector<u8> rom = loadRoms(location, document, "maincpu");
if(rom.empty()) return { invalidROM, "Ensure your ROM is in a MAME-compatible .zip format." };
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;
@@ -27,21 +138,27 @@ auto Arcade::load(string location) -> LoadResult {
pak->setAttribute("title", document["game/title"].string());
pak->setAttribute("region", document["game/region"].string());
pak->append("manifest.bml", manifest);
pak->append("program.rom", rom);
pak->append("program.rom", rom.data);
return successful;
}
//Aleck 64
// Aleck 64
if(document["game/board"].string() == "nintendo/aleck64") {
std::vector<u8> rom = loadRoms(location, document, "user2");
if(rom.empty()) return { invalidROM, "Ensure your ROM is in a MAME-compatible .zip format." };
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 them in their native Big-Endian (z64)
endianSwap(rom);
// 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."};
}
std::vector<u8> pif = loadRoms(location, document, "user1");
if(pif.empty()) return {
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."
};
@@ -55,8 +172,8 @@ auto Arcade::load(string location) -> LoadResult {
pak->setAttribute("region", document["game/region"].string());
pak->setAttribute("cic", "CIC-NUS-5101");
pak->append("manifest.bml", manifest);
pak->append("program.rom", rom);
pak->append("pif.aleck64.rom", pif);
pak->append("program.rom", rom.data);
pak->append("pif.aleck64.rom", pif.data);
return successful;
}
@@ -64,6 +181,55 @@ auto Arcade::load(string location) -> LoadResult {
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;
}
+318 -65
View File
@@ -1,91 +1,344 @@
// Helper/Utility for importing roms for systems that are using MAME romsets
// Helper/Utility for importing ROMs for systems that use MAME ROM sets.
struct Mame : Medium {
auto loadRoms(string location, Markup::Node& info, string sectionName) -> std::vector<u8>;
auto loadRomFile(string location, string filename, Markup::Node& currentInfo) -> std::vector<u8>;
auto endianSwap(std::vector<u8>& memory, u32 address = 0, int size = -1) -> void;
enum class AssemblyStatus : u32 {
Successful,
InvalidArchive,
ExtractionFailed,
MissingMember,
TruncatedMember,
MalformedMetadata,
MalformedParent,
ParentCycle,
};
struct AssemblyResult {
AssemblyStatus status;
std::vector<u8> data;
string region;
string member;
string archive;
string detail;
std::vector<string> archiveChain;
AssemblyResult() : status(AssemblyStatus::Successful) {}
AssemblyResult(
AssemblyStatus status, std::vector<u8> data, string region,
string member, string archive, string detail,
std::vector<string> archiveChain
) : status(status), data(std::move(data)), region(std::move(region)), member(std::move(member)),
archive(std::move(archive)), detail(std::move(detail)), archiveChain(std::move(archiveChain)) {}
AssemblyResult(
AssemblyStatus status, string region, string member,
string archive, string detail,
std::vector<string> archiveChain
) : AssemblyResult(status, {}, std::move(region), std::move(member), std::move(archive),
std::move(detail), std::move(archiveChain)) {}
explicit operator bool() const { return status == AssemblyStatus::Successful; }
};
using ManifestResolver = std::function<Markup::Node (string)>;
auto loadRoms(string location, Markup::Node& info, string sectionName,
ManifestResolver resolver = {}) -> AssemblyResult;
auto loadRomFile(string location, string filename, string region, Markup::Node currentInfo,
const ManifestResolver& resolver) -> AssemblyResult;
auto parseNatural(Markup::Node node, u64& value) const -> bool;
auto assemblyError(const AssemblyResult& result) const -> string;
auto endianSwap(std::vector<u8>& memory, u64 address = 0, u64 size = std::numeric_limits<u64>::max()) -> bool;
};
auto Mame::loadRoms(string location, Markup::Node& info, string sectionName) -> std::vector<u8> {
std::vector<u8> output;
auto Mame::parseNatural(Markup::Node node, u64& value) const -> bool {
if(!node) return false;
auto text = node.string().strip();
if(!text) return false;
u32 offset = 0;
bool hexadecimal = text.beginsWith("0x") || text.beginsWith("0X");
if(hexadecimal) offset = 2;
if(offset == text.size()) return false;
value = 0;
u32 base = hexadecimal ? 16 : 10;
for(u32 index = offset; index < text.size(); index++) {
auto character = text[index];
bool decimalDigit = character >= '0' && character <= '9';
bool hexadecimalDigit = hexadecimal && (
(character >= 'a' && character <= 'f')
|| (character >= 'A' && character <= 'F')
);
if(!decimalDigit && !hexadecimalDigit) return false;
u32 digit = decimalDigit ? character - '0' : (character | 0x20) - 'a' + 10;
if(value > (std::numeric_limits<u64>::max() - digit) / base) return false;
value = value * base + digit;
}
return true;
}
auto Mame::loadRoms(string location, Markup::Node& info, string sectionName,
ManifestResolver resolver) -> AssemblyResult {
if(!location.iendsWith(".zip")) {
return {AssemblyStatus::InvalidArchive, sectionName, {}, location, "expected a .zip archive", {}};
}
Decode::ZIP archive;
if(!location.iendsWith(".zip")) return output;
if(!archive.open(location)) return output;
if(!archive.open(location)) {
return {AssemblyStatus::InvalidArchive, sectionName, {}, location, "could not open the ZIP archive", {location}};
}
string filename = {};
std::vector<u8> input = {};
u32 readOffset = 0;
string loadType = {};
if(!resolver) {
resolver = [&](string parent) -> Markup::Node {
return BML::unserialize(manifestDatabaseArcade(parent));
};
}
string filename;
std::vector<u8> input;
string inputArchive;
std::vector<string> inputArchiveChain;
u64 readOffset = 0;
string loadType;
bool hasInput = false;
AssemblyResult result;
for(auto section : info[{"game/", sectionName}]) {
if(section.name() == "rom") {
if(section["type"] && section["type"].string() != "continue") loadType = section["type"].string();
if(section["name"]) {
filename = section["name"].string().strip();
if(filename) {
input = {};
input = loadRomFile(location, filename, info);
readOffset = 0;
if (input.empty()) return output;
}
if(section.name() != "rom") continue;
auto recordName = section["name"].string().strip();
auto assemblyFailure = [&](AssemblyStatus status, string detail) -> AssemblyResult {
auto member = recordName ? recordName : filename;
return {status, sectionName, member, {}, std::move(detail), {}};
};
auto type = section["type"].string();
if(type
&& type != "continue"
&& type != "fill"
&& type != "ignore"
&& type != "load"
&& type != "load16_byte"
&& type != "load16_word_swap"
&& type != "load32_byte"
&& type != "load32_word_swap") {
return assemblyFailure(AssemblyStatus::MalformedMetadata, {"unsupported ROM load type: ", type});
}
u64 fillValue = 0;
bool fillRecord = type == "fill";
if(type == "continue" || type == "ignore") {
if(section["name"] || section["value"]) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM reuse record must only contain offset and size");
}
if(type == "ignore") loadType = type;
} else if(fillRecord) {
if(section["name"] || !parseNatural(section["value"], fillValue) || fillValue > 0xff) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM fill record has an invalid name or value");
}
loadType = type;
hasInput = false;
} else {
if(!section["name"] || section["value"]) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM file record has an invalid name or value");
}
filename = recordName;
if(!filename) {
return {AssemblyStatus::MalformedMetadata, sectionName, {}, {}, "ROM file record has an empty member name", {}};
}
loadType = type;
auto file = loadRomFile(location, filename, sectionName, info, resolver);
if(!file) return file;
input = std::move(file.data);
inputArchive = std::move(file.archive);
inputArchiveChain = std::move(file.archiveChain);
readOffset = 0;
hasInput = true;
}
if(!section["offset"] || !section["size"]) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM record is missing offset or size");
}
u64 writeOffset = 0;
u64 sourceSize = 0;
if(!parseNatural(section["offset"], writeOffset) || !parseNatural(section["size"], sourceSize)) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM record has an invalid offset or size");
}
if(!sourceSize) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM record has an empty extent");
}
u64 outputSize = sourceSize;
u64 startIndex = 0;
u64 increment = 1;
if(loadType == "load16_byte") {
if(sourceSize > std::numeric_limits<u64>::max() / 2) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "interleaved ROM size overflows");
}
outputSize = sourceSize * 2;
increment = 2;
if(writeOffset & 1) {
startIndex = 1;
writeOffset--;
}
}
if(writeOffset > std::numeric_limits<u64>::max() - outputSize) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM destination range overflows");
}
u64 outputEnd = writeOffset + outputSize;
if(outputEnd > result.data.max_size()) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM destination range is too large");
}
if(!fillRecord) {
if(!hasInput) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "ROM continuation has no source member");
}
auto writeOffset = section["offset"].natural();
auto size = section["size"].natural();
auto startIndex = 0;
auto increment = 1;
if(loadType == "load16_byte") {
increment = 2;
size *= 2;
if(writeOffset & 1) {
startIndex = 1;
writeOffset--;
}
u64 bytesRequired = 0;
if(outputSize > startIndex) {
bytesRequired = (outputSize - startIndex + increment - 1) / increment;
}
if(output.size() < writeOffset + size) output.resize(writeOffset + size);
for(auto index = startIndex; index < size; index += increment) {
if(loadType == "fill") {
output[index + writeOffset] = section["value"].natural();
continue;
}
output[index + writeOffset] = input[readOffset++];
if(readOffset > input.size()
|| bytesRequired > input.size() - readOffset) {
string detail = {
"requested ", bytesRequired, " bytes at source offset ", readOffset,
", but only ", input.size() - std::min<u64>(readOffset, input.size()), " remain"
};
return {
AssemblyStatus::TruncatedMember,
sectionName, filename, inputArchive,
std::move(detail), inputArchiveChain
};
}
}
if(loadType == "load16_word_swap") endianSwap(output, section["offset"].natural(), size);
if(result.data.size() < outputEnd) result.data.resize(outputEnd);
for(u64 index = startIndex; index < outputSize; index += increment) {
if(fillRecord) {
result.data[index + writeOffset] = fillValue;
continue;
}
result.data[index + writeOffset] = input[readOffset++];
}
if(loadType == "load16_word_swap" && !endianSwap(result.data, writeOffset, sourceSize)) {
return assemblyFailure(AssemblyStatus::MalformedMetadata, "word-swap range is invalid");
}
}
return output;
return result;
}
auto Mame::loadRomFile(string location, string filename, Markup::Node& info) -> std::vector<u8> {
Decode::ZIP archive;
if(!archive.open(location)) return {};
auto Mame::loadRomFile(string location, string filename, string region, Markup::Node currentInfo,
const ManifestResolver& resolver) -> AssemblyResult {
std::vector<string> visited;
std::vector<string> archiveChain;
auto assemblyFailure = [&](AssemblyStatus status, string detail) {
return AssemblyResult{status, region, filename, location, std::move(detail), std::move(archiveChain)};
};
for(auto& file : archive.file) {
if(file.name.iequals(filename)) {
return archive.extract(file);
while(true) {
auto currentName = currentInfo["game/name"].string();
if(!currentName) {
return assemblyFailure(AssemblyStatus::MalformedParent, "manifest is missing game/name");
}
for(auto& name : visited) {
if(name.iequals(currentName)) {
return assemblyFailure(AssemblyStatus::ParentCycle, {"parent cycle repeats ", currentName});
}
}
visited.push_back(currentName);
archiveChain.push_back(location);
Decode::ZIP archive;
if(!archive.open(location)) {
return assemblyFailure(AssemblyStatus::InvalidArchive, "could not open a ZIP archive in the parent chain");
}
for(auto& file : archive.file) {
if(!file.name.iequals(filename)) continue;
auto data = archive.extract(file);
if(data.size() != file.size) {
auto extractedSize = data.size();
return {
AssemblyStatus::ExtractionFailed, std::move(data),
region, filename, location,
{"expected ", file.size, " extracted bytes, got ", extractedSize}, std::move(archiveChain)
};
}
return {AssemblyStatus::Successful, std::move(data), {}, filename, location, {}, std::move(archiveChain)};
}
auto parent = currentInfo["game/parent"].string();
if(!parent) {
return assemblyFailure(AssemblyStatus::MissingMember, "member was not found in the archive chain");
}
auto parentInfo = resolver(parent);
if(!parentInfo || !parentInfo["game/name"].string()) {
return assemblyFailure(AssemblyStatus::MalformedParent, {"database manifest is missing for parent ", parent});
}
location = {Location::path(location), "/", parentInfo["game/name"].string(), ".zip"};
currentInfo = parentInfo;
}
}
auto Mame::assemblyError(const AssemblyResult& result) const -> string {
string message;
switch(result.status) {
case AssemblyStatus::Successful:
return {};
case AssemblyStatus::InvalidArchive:
message = {"Could not open MAME archive: ", result.archive, ". "};
break;
case AssemblyStatus::ExtractionFailed:
message = {"Could not extract MAME member ", result.member, " from ", result.archive, ". "};
break;
case AssemblyStatus::MissingMember:
message = {"Missing MAME member: ", result.member, ". "};
break;
case AssemblyStatus::TruncatedMember:
message = {"MAME member is too short: ", result.member, ". "};
break;
case AssemblyStatus::MalformedMetadata:
message = {"Malformed MAME metadata for region ", result.region, ". "};
break;
case AssemblyStatus::MalformedParent:
message = {"Malformed MAME parent metadata for member ", result.member, ". "};
break;
case AssemblyStatus::ParentCycle:
message = {"MAME parent cycle while resolving member ", result.member, ". "};
break;
}
if(auto parent = info["game/parent"].string()) {
auto manifest = manifestDatabaseArcade(parent);
auto document = BML::unserialize(manifest);
if(!document) return {};
location = {Location::path(location), "/", document["game/name"].string(), ".zip"};
return loadRomFile(location, filename, document);
if(!result.archiveChain.empty()) {
message.append("Archive chain: ");
for(u32 index : range((u32)result.archiveChain.size())) {
if(index) message.append(" -> ");
message.append(Location::file(result.archiveChain[index]));
}
message.append(". ");
}
if(result.detail) message.append(result.detail, ". ");
return message;
}
return {};
};
auto Mame::endianSwap(std::vector<u8>& memory, u32 address, int size) -> void {
if(size < 0) size = (int)memory.size();
for(u32 index = 0; index < (u32)size; index += 2) {
auto Mame::endianSwap(std::vector<u8>& memory, u64 address, u64 size) -> bool {
if(address > memory.size()) return false;
if(size == std::numeric_limits<u64>::max()) size = memory.size() - address;
if(size > memory.size() - address || (size & 1)) return false;
for(u64 index = 0; index < size; index += 2) {
swap(memory[address + index + 0], memory[address + index + 1]);
}
return true;
}
+1 -1
View File
@@ -6,7 +6,7 @@ struct Database {
struct Medium : Pak {
static auto create(string name) -> std::shared_ptr<Pak>;
auto loadDatabase() -> bool;
auto database() -> Database;
virtual auto database() -> Database;
auto manifestDatabase(string sha256) -> string;
auto manifestDatabaseArcade(string name) -> string;
+30 -16
View File
@@ -5,7 +5,7 @@
struct NeoGeo : Mame {
auto name() -> string override { return "Neo Geo"; }
auto extensions() -> std::vector<string> override { return {"ng"}; }
auto read(string location, string match) -> std::vector<u8>;
auto read(string location, string match) -> AssemblyResult;
auto load(string location) -> LoadResult override;
auto board() -> string;
auto save(string location) -> bool override;
@@ -36,7 +36,7 @@ struct NeoGeo : Mame {
} cmc;
};
auto NeoGeo::read(string location, string match) -> std::vector<u8> {
auto NeoGeo::read(string location, string match) -> AssemblyResult {
// we expect mame style .zip rom images
if(!location.iendsWith(".zip")) {}
@@ -47,11 +47,14 @@ auto NeoGeo::read(string location, string match) -> std::vector<u8> {
if(match == "voice-a.rom") return loadRoms(location, info, "ymsnd-adpcma");
if(match == "voice-b.rom") return loadRoms(location, info, "ymsnd-adpcmb");
if(match == "music.rom") {
// music rom can be plaintext (audiocpu) or encrypted (audiocrypt)
// we must load both types
auto music = loadRoms(location, info, "audiocpu");
if(music.size() == 0) return loadRoms(location, info, "audiocrypt");
return music;
// Music ROMs are stored either as plaintext or encrypted data.
// Select the declared region so a broken plaintext set is not mistaken
// for an encrypted set.
if(info["game/audiocpu"]) {
auto music = loadRoms(location, info, "audiocpu");
if(!music || !music.data.empty()) return music;
}
return loadRoms(location, info, "audiocrypt");
}
}
@@ -70,17 +73,28 @@ auto NeoGeo::load(string location) -> LoadResult {
if(!foundDatabase) return { databaseNotFound, "Neo Geo.bml" };
this->info = BML::unserialize(manifestDatabaseArcade(Medium::name(location)));
if(file::exists(location)) {
programROM = NeoGeo::read(location, "program.rom");
musicROM = NeoGeo::read(location, "music.rom");
characterROM = NeoGeo::read(location, "character.rom");
staticROM = NeoGeo::read(location, "static.rom");
voiceAROM = NeoGeo::read(location, "voice-a.rom");
voiceBROM = NeoGeo::read(location, "voice-b.rom");
}
auto programResult = NeoGeo::read(location, "program.rom");
auto musicResult = NeoGeo::read(location, "music.rom");
auto characterResult = NeoGeo::read(location, "character.rom");
auto staticResult = NeoGeo::read(location, "static.rom");
auto voiceAResult = NeoGeo::read(location, "voice-a.rom");
auto voiceBResult = NeoGeo::read(location, "voice-b.rom");
string invalidRomInfo = "Ensure your ROM is in a MAME-compatible .zip format.";
if(!programResult) return {invalidROM, assemblyError(programResult)};
if(!musicResult) return {invalidROM, assemblyError(musicResult)};
if(!characterResult) return {invalidROM, assemblyError(characterResult)};
if(!staticResult) return {invalidROM, assemblyError(staticResult)};
if(!voiceAResult) return {invalidROM, assemblyError(voiceAResult)};
programROM = std::move(programResult.data);
musicROM = std::move(musicResult.data);
characterROM = std::move(characterResult.data);
staticROM = std::move(staticResult.data);
voiceAROM = std::move(voiceAResult.data);
if(voiceBResult) voiceBROM = std::move(voiceBResult.data);
if(programROM.empty() ) return { invalidROM, invalidRomInfo };
if(musicROM.empty() ) return { invalidROM, invalidRomInfo };
if(characterROM.empty()) return { invalidROM, invalidRomInfo };
+1
View File
@@ -32,6 +32,7 @@ enum ResultEnum {
romNotFoundInDatabase,
romNotFound,
invalidROM,
unsupportedMedia,
wrongMediaType,
couldNotParseManifest,
noFirmware,
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.py[cod]
+1
View File
@@ -0,0 +1 @@
"""Tools for acquiring and transforming MAME machine metadata."""
+89
View File
@@ -0,0 +1,89 @@
"""Command-line entry point for generated MAME metadata databases."""
from __future__ import annotations
import argparse
import contextlib
import os
import stat
import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import List, Optional, cast
from . import mame, vsnes
DEFAULT_VSNES_OUTPUT = (
Path(__file__).resolve().parents[2] / "mia" / "Database" / "VsSystem.bml"
)
def write_atomically(destination: Path, contents: str) -> None:
destination.parent.mkdir(parents=True, exist_ok=True)
try:
existing_mode = stat.S_IMODE(destination.stat().st_mode)
except FileNotFoundError:
existing_mode = None
descriptor, temporary_name = tempfile.mkstemp(
prefix=f".{destination.name}.", dir=destination.parent
)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as output:
_ = output.write(contents)
output.flush()
os.fsync(output.fileno())
if existing_mode is not None:
os.chmod(temporary_name, existing_mode)
os.replace(temporary_name, destination)
except BaseException:
with contextlib.suppress(FileNotFoundError):
os.unlink(temporary_name)
raise
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
subcommands = parser.add_subparsers(dest="database", required=True)
vsnes_parser = subcommands.add_parser(
"vsnes", help="generate the Nintendo Vs. System database"
)
_ = vsnes_parser.add_argument(
"mame",
nargs="?",
help="MAME executable or directory (default: find MAME on PATH)",
)
_ = vsnes_parser.add_argument(
"--jobs",
type=int,
default=4,
help="maximum parallel MAME -listxml queries (default: 4)",
)
_ = vsnes_parser.add_argument(
"--output",
type=Path,
default=DEFAULT_VSNES_OUTPUT,
help="output BML path (default: repository mia/Database/VsSystem.bml)",
)
return parser
def main(argv: Optional[List[str]] = None) -> int:
parser = _parser()
args = parser.parse_args(argv)
mame_argument = cast(Optional[str], args.mame)
jobs = cast(int, args.jobs)
output = cast(Path, args.output)
if jobs < 1:
parser.error("--jobs must be at least 1")
try:
executable = mame.resolve_executable(mame_argument)
root = mame.load_source(executable, vsnes.SOURCE, vsnes.EXPECTED_MACHINES, jobs)
write_atomically(output, vsnes.process_root(root))
except (mame.MameError, vsnes.MetadataError, ET.ParseError, OSError) as error:
parser.exit(1, f"error: {error}\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+191
View File
@@ -0,0 +1,191 @@
"""Generic acquisition of machine metadata from a local MAME executable."""
from __future__ import annotations
import concurrent.futures
import re
import shutil
import subprocess
import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Set, Tuple
class MameError(RuntimeError):
"""A normalized executable, process, inventory, or XML acquisition error."""
def resolve_executable(value: Optional[str]) -> Path:
if value is None:
discovered = shutil.which("mame") or shutil.which("mame.exe")
if discovered is None:
raise MameError(
"MAME could not be found on PATH; pass its executable or directory"
)
return Path(discovered)
supplied = Path(value).expanduser()
if supplied.is_dir():
for filename in ("mame", "mame.exe"):
candidate = supplied / filename
if candidate.is_file():
return candidate
raise MameError(f"MAME executable not found in directory: {supplied}")
if not supplied.is_file():
raise MameError(f"MAME executable not found: {supplied}")
return supplied
def _decode_output(output: object, context: str) -> str:
if isinstance(output, str):
return output
if not isinstance(output, bytes):
raise MameError(f"MAME returned invalid output for {context}")
try:
return output.decode("utf-8-sig")
except UnicodeDecodeError as error:
raise MameError(
f"MAME returned non-UTF-8 output for {context}: {error}"
) from error
def _decode_stderr(output: object, fallback: bytes) -> str:
"""Normalize real and mocked subprocess stderr without masking failures."""
if isinstance(output, str):
return output
if isinstance(output, bytes):
return output.decode("utf-8", errors="replace")
return fallback.decode("utf-8", errors="replace")
def _run(executable: Path, arguments: List[str], context: str) -> str:
command = [str(executable), *arguments]
try:
with tempfile.TemporaryFile() as stderr_file:
completed = subprocess.run(
command, stdout=subprocess.PIPE, stderr=stderr_file, check=False
)
_ = stderr_file.seek(0)
stderr_bytes = stderr_file.read(65536)
except OSError as error:
raise MameError(
f"could not run MAME command for {context}: {command!r}: {error}"
) from error
stderr = _decode_stderr(completed.stderr, stderr_bytes)
if completed.returncode != 0:
detail = " ".join(stderr.split())
if len(detail) > 1000:
detail = detail[:997] + "..."
message = (
f"MAME command failed for {context} with exit code "
f"{completed.returncode}: {command!r}"
)
if detail:
message += f": {detail}"
raise MameError(message)
return _decode_output(completed.stdout, context)
def _inventory(output: str, source: str) -> List[str]:
names: List[str] = []
seen: Set[str] = set()
for line in output.splitlines():
fields = line.split()
if len(fields) < 2 or fields[-1].replace("\\", "/") != source:
continue
name = fields[0]
if not re.fullmatch(r"[A-Za-z0-9_]+", name):
raise MameError(
f"invalid {source} machine name in MAME -listsource: {name!r}"
)
if name in seen:
raise MameError(f"duplicate {source} machine in MAME -listsource: {name}")
seen.add(name)
names.append(name)
if not names:
raise MameError(f"MAME -listsource contains no machines for {source}")
return sorted(names)
def _check_inventory(actual: Iterable[str], expected: Iterable[str]) -> None:
actual_names = set(actual)
expected_names = set(expected)
if actual_names == expected_names:
return
details: List[str] = []
unknown = sorted(actual_names - expected_names)
missing = sorted(expected_names - actual_names)
if unknown:
details.append(f"unexpected source machines: {', '.join(unknown)}")
if missing:
details.append(f"expected machines absent from source: {', '.join(missing)}")
raise MameError("; ".join(details))
def _query_machine(executable: Path, source: str, name: str) -> Tuple[str, ET.Element]:
output = _run(executable, ["-listxml", name], f"machine {name}")
try:
root = ET.fromstring(output)
except ET.ParseError as error:
raise MameError(f"invalid MAME XML for machine {name}: {error}") from error
if root.tag != "mame":
raise MameError(f"MAME -listxml target {name} did not return a mame root")
revision = root.get("build")
if not revision:
raise MameError(f"MAME -listxml target {name} has no build revision")
named = [item for item in root.findall("machine") if item.get("name") == name]
selected = [item for item in named if item.get("sourcefile") == source]
if len(selected) != 1:
if named and not selected:
sources = sorted({item.get("sourcefile", "<missing>") for item in named})
raise MameError(
"MAME -listxml target {} has wrong source: {}; expected {}".format(
name, ", ".join(sources), source
)
)
condition = "missing" if not selected else "duplicate"
raise MameError(
f"{condition} requested {source} machine in MAME -listxml target {name}"
)
return revision, selected[0]
def load_source(
executable: Path, source: str, expected_machines: Iterable[str], jobs: int = 4
) -> ET.Element:
"""Load one complete source-driver inventory into a deterministic XML root."""
if jobs < 1:
raise MameError("--jobs must be at least 1")
names = _inventory(_run(executable, ["-listsource"], source), source)
_check_inventory(names, expected_machines)
results: Dict[str, Tuple[str, ET.Element]] = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as executor:
futures = {
executor.submit(_query_machine, executable, source, name): name
for name in names
}
for future in concurrent.futures.as_completed(futures):
name = futures[future]
try:
results[name] = future.result()
except MameError:
for pending in futures:
_ = pending.cancel()
raise
except Exception as error:
for pending in futures:
_ = pending.cancel()
raise MameError(
f"worker failed for MAME -listxml machine {name}: {error}"
) from error
revisions = {revision for revision, _ in results.values()}
if len(revisions) != 1:
details = ", ".join(f"{name}={results[name][0]}" for name in sorted(results))
raise MameError(
f"MAME build revision mismatch across machine queries: {details}"
)
root = ET.Element("mame", build=next(iter(revisions)))
for name in sorted(results):
root.append(results[name][1])
return root
+368
View File
@@ -0,0 +1,368 @@
"""Transform Nintendo Vs. System MAME XML into deterministic BML metadata."""
from __future__ import annotations
import re
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional, Tuple
SOURCE = "nintendo/vsnes.cpp"
@dataclass(frozen=True)
class Machine:
mapper: str
protection: str
input: str
not_working: bool = False
def machine(
mapper: str = "standard",
protection: str = "normal",
input_mode: str = "standard",
not_working: bool = False,
) -> Machine:
return Machine(mapper, protection, input_mode, not_working)
SUPPORTED = {
"btlecity": machine(),
"starlstr": machine(),
"cstlevna": machine("unrom"),
"cluclu": machine(),
"drmario": machine("mmc1"),
"excitebk": machine(),
"excitebko": machine(),
"excitebkj": machine(),
"goonies": machine("vrc1"),
"iceclimb": machine(protection="ice-climber"),
"iceclimba": machine(),
"nvs_machrider": machine(),
"nvs_machridera": machine(),
"rbibb": machine("namco-108", protection="rbi-baseball"),
"rbibba": machine("namco-108", protection="rbi-baseball"),
"suprmrio": machine(input_mode="swapped"),
"suprmrioa": machine(input_mode="swapped"),
"skatekds": machine(input_mode="swapped"),
"vsskykid": machine("namco-108"),
"tkoboxng": machine("namco-108", protection="tko-boxing", input_mode="swapped"),
"smgolf": machine(),
"smgolfb": machine(),
"smgolfj": machine(),
"ladygolfe": machine(),
"ladygolf": machine(),
"vspinbal": machine(),
"vspinbalj": machine(input_mode="swap-ab"),
"vsslalom": machine(),
"vssoccer": machine(),
"vssoccera": machine(),
"vsgradus": machine("vrc1"),
"nvs_platoon": machine("sunsoft-3"),
"vstetris": machine(),
"nvs_mightybj": machine(input_mode="swapped"),
"jajamaru": machine(),
"topgun": machine("unrom"),
"supxevs": machine("namco-108", protection="super-xevious"),
"duckhunt": machine(input_mode="zapper"),
"hogalley": machine(input_mode="zapper"),
"vsgshoe": machine(input_mode="zapper"),
"vsfdf": machine("namco-108", input_mode="zapper"),
}
UNSUPPORTED = {
"bnglngby": "raid-protection",
"hogalleyi": "dualsystem",
"suprmriobl": "bootleg-z80",
"suprmriobl2": "bootleg-z80",
"vstennis": "dualsystem",
"vstennisa": "dualsystem",
"vstennisb": "dualsystem",
"wrecking": "dualsystem",
"balonfgt": "dualsystem",
"vsmahjng": "dualsystem",
"vsbball": "dualsystem",
"vsbballj": "dualsystem",
"vsbballja": "dualsystem",
"vsbballjb": "dualsystem",
"iceclmrd": "dualsystem",
}
class MetadataError(ValueError):
pass
def require_classification() -> None:
overlap = SUPPORTED.keys() & UNSUPPORTED.keys()
if overlap:
raise MetadataError(f"duplicate Vs classification: {sorted(overlap)[0]}")
if len(SUPPORTED) != 41 or len(UNSUPPORTED) != 15:
raise MetadataError("expected 41 supported and 15 unsupported Vs machines")
def parse_number(value: Optional[str], field: str, machine_name: str) -> int:
if value is None:
raise MetadataError(f"missing {field} for {machine_name}")
base = 16 if value.lower().startswith("0x") else 10
return int(value, base)
def parse_hex_number(value: Optional[str], field: str, machine_name: str) -> int:
if value is None:
raise MetadataError(f"missing {field} for {machine_name}")
if value.lower().startswith("0x"):
value = value[2:]
return int(value, 16)
def normalize_port(value: Optional[str], machine_name: str) -> Optional[str]:
value = optional_text(value, "DIP port", machine_name)
if value is None:
return None
port = value[1:] if value.startswith(":") else value
return port
def required_text(value: Optional[str], field: str, machine_name: str) -> str:
if not value or "\n" in value or "\r" in value:
raise MetadataError(f"missing {field} for {machine_name}")
return value
def optional_text(value: Optional[str], field: str, machine_name: str) -> Optional[str]:
if not value:
return None
if "\n" in value or "\r" in value:
raise MetadataError(f"{field} cannot be emitted for {machine_name}")
return value
def ppu_devices_for(machine_element: ET.Element) -> List[str]:
"""Return target PPU identifiers independently of palette ROM data."""
machine_name = machine_element.get("name", "<unnamed>")
devices: List[str] = []
for device in machine_element.findall("device_ref"):
if device.get("tag") != ":ppu1":
continue
name = optional_text(device.get("name"), "PPU device identifier", machine_name)
if name is not None:
devices.append(name)
return devices
def bml_number(value: Optional[str], field: str, machine_name: str) -> Optional[str]:
text = optional_text(value, field, machine_name)
if text is None:
return None
try:
return f"{parse_number(text, field, machine_name):#x}"
except ValueError:
return text
def dips_for(machine_element: ET.Element) -> List[str]:
result: List[str] = []
machine_name = machine_element.get("name", "<unnamed>")
for dip in machine_element.findall("dipswitch"):
name = optional_text(dip.get("name"), "DIP name", machine_name)
port = normalize_port(dip.get("tag"), machine_name)
mask = bml_number(dip.get("mask"), "DIP mask", machine_name)
choices: List[Tuple[Optional[str], Optional[str], bool]] = []
for choice in dip.findall("dipvalue"):
choice_name = optional_text(
choice.get("name"), "DIP choice name", machine_name
)
value = bml_number(choice.get("value"), "DIP choice", machine_name)
choices.append((choice_name, value, choice.get("default") == "yes"))
defaults = [value for _, value, default in choices if default]
result.append(" dip\n")
if name is not None:
result.append(f" name: {name}\n")
if port is not None:
result.append(f" port: {port}\n")
if mask is not None:
result.append(f" mask: {mask}\n")
for default in defaults:
if default is not None:
result.append(f" default: {default}\n")
for choice_name, value, _ in choices:
result.append(" option\n")
if choice_name is not None:
result.append(f" name: {choice_name}\n")
if value is not None:
result.append(f" value: {value}\n")
return result
def hardware_lines(name: str, ppu_devices: Iterable[str] = ()) -> List[str]:
if name in UNSUPPORTED:
return [
" hardware\n",
" topology: unsupported\n",
f" reason: {UNSUPPORTED[name]}\n",
]
item = SUPPORTED[name]
lines = [
" hardware\n",
" topology: unisystem\n",
]
for ppu_device in ppu_devices:
lines.append(f" ppu: {ppu_device}\n")
lines.extend(
(
f" mapper: {item.mapper}\n",
f" protection: {item.protection}\n",
f" input: {item.input}\n",
)
)
if item.not_working:
lines.append(" upstream-status: not-working\n")
return lines
@dataclass(frozen=True)
class RomRecord:
name: Optional[str]
value: Optional[int]
load_type: Optional[str]
offset: int
size: int
status: str
crc: Optional[str]
sha1: Optional[str]
def parse_rom_record(rom: ET.Element, machine_name: str) -> RomRecord:
loadflag = rom.get("loadflag")
legacy_type = rom.get("type")
if loadflag and legacy_type and loadflag != legacy_type:
raise MetadataError(f"conflicting ROM load types for {machine_name}")
raw_value = optional_text(rom.get("value"), "ROM value", machine_name)
return RomRecord(
name=optional_text(rom.get("name"), "ROM name", machine_name),
value=(
parse_number(raw_value, "ROM value", machine_name)
if raw_value is not None
else None
),
load_type=optional_text(loadflag or legacy_type, "ROM load type", machine_name),
offset=parse_hex_number(rom.get("offset"), "ROM offset", machine_name),
size=parse_number(rom.get("size"), "ROM size", machine_name),
status=optional_text(rom.get("status"), "ROM status", machine_name) or "good",
crc=optional_text(rom.get("crc"), "ROM CRC", machine_name),
sha1=optional_text(rom.get("sha1"), "ROM SHA1", machine_name),
)
def bml_rom_lines(record: RomRecord) -> List[str]:
lines = [" rom\n"]
if record.name:
lines.append(f" name: {record.name}\n")
if record.value is not None:
lines.append(f" value: {record.value}\n")
if record.load_type:
lines.append(f" type: {record.load_type}\n")
lines.extend(
(
f" offset: {record.offset:#x}\n",
f" size: {record.size}\n",
)
)
if record.crc is not None:
lines.append(f" crc: {record.crc.lower()}\n")
if record.sha1 is not None:
lines.append(f" sha1: {record.sha1.lower()}\n")
if record.status != "good":
lines.append(f" status: {record.status}\n")
return lines
def rom_lines(machine_element: ET.Element) -> List[str]:
machine_name = machine_element.get("name", "<unnamed>")
lines: List[str] = []
current_region = ""
for rom in machine_element.findall("rom"):
region = required_text(rom.get("region"), "ROM region", machine_name).replace(
":", "-"
)
if not re.fullmatch(r"[A-Za-z0-9_.-]+", region):
raise MetadataError(f"ROM region cannot be emitted for {machine_name}")
if region != current_region:
lines.append(f" {region}\n")
current_region = region
record = parse_rom_record(rom, machine_name)
lines.extend(bml_rom_lines(record))
return lines
def source_machines(root: ET.Element) -> Tuple[str, Dict[str, ET.Element]]:
if root.tag != "mame":
raise MetadataError("input XML is not a MAME machine list")
mame_revision = required_text(root.get("build"), "MAME build revision", "<root>")
machines: Dict[str, ET.Element] = {}
for element in root.findall("machine"):
if element.get("sourcefile") != SOURCE:
continue
name = required_text(element.get("name"), "machine name", "<source>")
if name in machines:
raise MetadataError(f"duplicate Vs source machine: {name}")
machines[name] = element
validate_inventory(machines)
return mame_revision, machines
def process_root(root: ET.Element) -> str:
require_classification()
mame_revision, machines = source_machines(root)
output: List[str] = [
"database\n",
f" revision: {mame_revision}\n",
f" romset: {mame_revision}\n",
f" drivers: {SOURCE}\n",
]
for name in sorted(machines):
element = machines[name]
title = required_text(element.findtext("description"), "title", name)
ppu_devices = ppu_devices_for(element) if name in SUPPORTED else ()
machine_type = "bios" if element.get("isbios") == "yes" else "game"
parent = optional_text(element.get("romof"), "parent", name)
roms = rom_lines(element)
output.extend(
(
"game\n",
f" name: {name}\n",
f" title: {title}\n",
" board: nintendo/vs\n",
f" type: {machine_type}\n",
)
)
if parent:
output.append(f" parent: {parent}\n")
output.extend(hardware_lines(name, ppu_devices))
if name in SUPPORTED:
output.extend(dips_for(element))
output.extend(roms)
return "".join(output)
def validate_inventory(names: Iterable[str]) -> None:
actual = set(names)
classified = set(SUPPORTED) | set(UNSUPPORTED)
if actual == classified:
return
unknown = sorted(actual - classified)
missing = sorted(classified - actual)
details: List[str] = []
if unknown:
details.append(f"unclassified source machines: {', '.join(unknown)}")
if missing:
details.append(f"classified machines absent from source: {', '.join(missing)}")
raise MetadataError("; ".join(details))
EXPECTED_MACHINES = frozenset(SUPPORTED) | frozenset(UNSUPPORTED)