mirror of
https://github.com/ares-emulator/ares.git
synced 2026-10-11 22:39:55 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
@@ -38,6 +38,7 @@ auto Cartridge::disconnect() -> void {
|
||||
board->pak.reset();
|
||||
board.reset();
|
||||
pak.reset();
|
||||
information = {};
|
||||
node.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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] = {};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -32,6 +32,7 @@ enum ResultEnum {
|
||||
romNotFoundInDatabase,
|
||||
romNotFound,
|
||||
invalidROM,
|
||||
unsupportedMedia,
|
||||
wrongMediaType,
|
||||
couldNotParseManifest,
|
||||
noFirmware,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -0,0 +1 @@
|
||||
"""Tools for acquiring and transforming MAME machine metadata."""
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user