Triforce: ICCardReader and DeckReader improvements.

Significantly rewrote ICCardReader and DeckReader functionality.
Dual IC card slots of VirtuaStriker4 + Gekitou now work.
IC cards are automatically inserted/ejected in VirtuaStriker4 + Gekitou + Avalon.
IC card data is saved to a file.
Avalon deck contents are loaded from a JSON file.
Added IOPorts class to handle bespoke GPIO functionality.
This commit is contained in:
Jordan Woyak
2026-03-17 21:46:52 -05:00
parent b5bc70a6cc
commit 85400539ba
10 changed files with 1915 additions and 675 deletions
+6
View File
@@ -294,6 +294,12 @@ add_library(core
HW/StreamADPCM.h
HW/SystemTimers.cpp
HW/SystemTimers.h
HW/Triforce/DeckReader.cpp
HW/Triforce/DeckReader.h
HW/Triforce/ICCardReader.cpp
HW/Triforce/ICCardReader.h
HW/Triforce/IOPorts.cpp
HW/Triforce/IOPorts.h
HW/Triforce/SerialDevice.cpp
HW/Triforce/SerialDevice.h
HW/Triforce/Touchscreen.cpp
+114 -602
View File
@@ -27,6 +27,8 @@
#include "Core/HW/SI/SI.h"
#include "Core/HW/SI/SI_Device.h"
#include "Core/HW/SystemTimers.h"
#include "Core/HW/Triforce/DeckReader.h"
#include "Core/HW/Triforce/ICCardReader.h"
#include "Core/HW/Triforce/Touchscreen.h"
#include "Core/Movie.h"
#include "Core/System.h"
@@ -112,21 +114,6 @@ void JVSIOMessage::End()
}
}
static constexpr u8 CheckSumXOR(const u8* data, u32 length)
{
return std::accumulate(data, data + length, u8{}, std::bit_xor());
}
static constexpr char s_cdr_program_version[] = {" Version 1.22,2003/09/19,171-8213B"};
static constexpr char s_cdr_boot_version[] = {" Version 1.04,2003/06/17,171-8213B"};
static constexpr u8 s_cdr_card_data[] = {
0x00, 0x6E, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x0B,
0x00, 0x00, 0x0E, 0x00, 0x00, 0x10, 0x00, 0x00, 0x17, 0x00, 0x00, 0x19, 0x00, 0x00,
0x1A, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x1D, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x20, 0x00,
0x00, 0x22, 0x00, 0x00, 0x23, 0x00, 0x00, 0x24, 0x00, 0x00, 0x27, 0x00, 0x00, 0x28,
0x00, 0x00, 0x2C, 0x00, 0x00, 0x2F, 0x00, 0x00, 0x34, 0x00, 0x00, 0x35, 0x00, 0x00,
0x37, 0x00, 0x00, 0x38, 0x00, 0x00, 0x39, 0x00, 0x00, 0x3D, 0x00};
const constexpr u8 s_region_flags[] = "\x00\x00\x30\x00"
// "\x01\xfe\x00\x00" // JAPAN
"\x02\xfd\x00\x00" // USA
@@ -137,30 +124,10 @@ CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices dev
int device_number)
: ISIDevice(system, device, device_number)
{
// Card ID
m_ic_card_data[0x20] = 0x95;
m_ic_card_data[0x21] = 0x71;
if (AMMediaboard::GetGameType() == KeyOfAvalon)
{
m_ic_card_data[0x22] = 0x26;
m_ic_card_data[0x23] = 0x40;
}
else if (AMMediaboard::GetGameType() == VirtuaStriker4)
{
m_ic_card_data[0x22] = 0x44;
m_ic_card_data[0x23] = 0x00;
}
// Use count
m_ic_card_data[0x28] = 0xFF;
m_ic_card_data[0x29] = 0xFF;
// Magnetic Card Reader
m_mag_card_settings.card_path = File::GetUserPath(D_TRIUSER_IDX);
m_mag_card_settings.card_name = fmt::format("tricard_{}.bin", SConfig::GetInstance().GetGameID());
// TODO: Do any other games use the Magnetic Card Reader ?
switch (AMMediaboard::GetGameType())
{
case FZeroAX:
@@ -169,33 +136,59 @@ CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices dev
case MarioKartGP:
case MarioKartGP2:
m_io_ports.AddIOAdapter(std::make_unique<Triforce::MarioKartGPCommon_IOAdapter>());
m_serial_device_b = std::make_unique<MagCard::C1231LR>(&m_mag_card_settings);
break;
case VirtuaStriker4:
{
auto slot_a = std::make_unique<Triforce::ICCardReader>(0);
auto slot_b = std::make_unique<Triforce::ICCardReader>(1);
m_io_ports.AddIOAdapter(
std::make_unique<Triforce::VirtuaStriker4Common_IOAdapter>(slot_a.get(), slot_b.get()));
m_serial_device_a = std::move(slot_a);
m_serial_device_b = std::move(slot_b);
break;
}
case VirtuaStriker4_2006:
{
auto slot_a = std::make_unique<Triforce::ICCardReader>(0);
auto slot_b = std::make_unique<Triforce::ICCardReader>(1);
m_io_ports.AddIOAdapter(
std::make_unique<Triforce::VirtuaStriker4Common_IOAdapter>(slot_a.get(), slot_b.get()));
m_io_ports.AddIOAdapter(
std::make_unique<Triforce::VirtuaStriker4_2006_IOAdapter>(slot_a.get(), slot_b.get()));
m_serial_device_a = std::move(slot_a);
m_serial_device_b = std::move(slot_b);
break;
}
case GekitouProYakyuu:
{
auto slot_a = std::make_unique<Triforce::ICCardReader>(0);
auto slot_b = std::make_unique<Triforce::ICCardReader>(1);
m_io_ports.AddIOAdapter(
std::make_unique<Triforce::GekitouProYakyuu_IOAdapter>(slot_a.get(), slot_b.get()));
m_serial_device_a = std::move(slot_a);
m_serial_device_b = std::move(slot_b);
break;
}
case KeyOfAvalon:
{
auto deck_reader = std::make_unique<Triforce::DeckReader>();
m_io_ports.AddIOAdapter(
std::make_unique<Triforce::KeyOfAvalon_IOAdapter>(deck_reader->GetICCardReader()));
m_serial_device_a = std::move(deck_reader);
m_serial_device_b = std::make_unique<Triforce::Touchscreen>();
break;
}
default:
break;
}
}
void CSIDevice_AMBaseboard::ICCardSendReply(ICCommand* iccommand, u8* buffer, u32* length)
{
iccommand->status = Common::swap16(iccommand->status);
const auto iccommand_data = reinterpret_cast<const u8*>(iccommand);
const u8 crc = CheckSumXOR(iccommand_data + 2, iccommand->pktlen - 1);
for (u32 i = 0; i <= iccommand->pktlen; ++i)
{
buffer[(*length)++] = iccommand_data[i];
}
buffer[(*length)++] = crc;
}
int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
{
// Debug logging
@@ -452,6 +445,13 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
if (!validate_data_in_out(length, 0, "SerialA"))
break;
if (m_serial_device_a != nullptr)
{
m_serial_device_a->WriteRxBytes({data_in, length});
data_in += length;
break;
}
INFO_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x31, length=0x{:02x}, hexdump:\n{}",
length, HexDump(data_in, length));
@@ -512,462 +512,6 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
data_in += length;
break;
}
// Serial - Unknown
if (AMMediaboard::GetGameType() == GekitouProYakyuu)
{
if (!validate_data_in_out(sizeof(u32), 0, "SerialA (Unknown)"))
break;
const u32 serial_command = Common::BitCastPtr<u32>(data_in);
if (serial_command == 0x00001000)
{
if (!validate_data_in_out(0, 5, "SerialA (Unknown)"))
break;
data_out[data_offset++] = gcam_command;
data_out[data_offset++] = 0x03;
data_out[data_offset++] = 1;
data_out[data_offset++] = 2;
data_out[data_offset++] = 3;
}
if (!validate_data_in_out(length, 0, "SerialA (Unknown)"))
break;
data_in += length;
break;
}
// Serial IC-CARD / Serial Deck Reader
if (AMMediaboard::GetGameType() == VirtuaStriker4 ||
AMMediaboard::GetGameType() == VirtuaStriker4_2006 ||
AMMediaboard::GetGameType() == KeyOfAvalon)
{
if (!validate_data_in_out(2, 0, "SerialA (IC-CARD)"))
break;
u32 serial_command = data_in[1];
ICCommand icco;
// Set default reply
icco.pktcmd = gcam_command;
icco.pktlen = 7;
icco.fixed = 0x10;
icco.command = serial_command;
icco.flag = 0;
icco.length = 2;
icco.status = 0;
icco.extlen = 0;
// Check for rest of data from the write pages command
if (m_ic_write_size && m_ic_write_offset)
{
const u32 size = data_in[1];
if (!validate_data_in_out(size + 2, 0, "SerialA (IC-CARD)"))
break;
DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Command: {}", HexDump(data_in, size + 2));
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 25 (IC-CARD) Write Pages: Off:{:x} Size:{:x} PSize:{:x}",
m_ic_write_offset, m_ic_write_size, size);
if (u64{m_ic_write_offset} + size > sizeof(m_ic_write_buffer))
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 25 (IC-CARD) m_ic_write_buffer overflow:\n"
" - m_ic_write_buffer(offset={}, size={})\n"
" - size={}\n",
m_ic_write_offset, sizeof(m_ic_write_buffer), size);
data_in = data_in_end;
break;
}
memcpy(m_ic_write_buffer + m_ic_write_offset, data_in + 2, size);
m_ic_write_offset += size;
if (m_ic_write_offset > m_ic_write_size)
{
m_ic_write_offset = 0;
const u16 page = m_ic_write_buffer[5];
const u16 count = m_ic_write_buffer[7];
const u32 write_size = u32(count) * 8;
const u32 write_offset = u32(page) * 8;
if ((write_size + write_offset) > sizeof(m_ic_card_data) ||
(10 + write_size) > sizeof(m_ic_write_buffer))
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 25 (IC-CARD) Write Pages overflow:\n"
" - m_ic_card_data(offset={}, size={})\n"
" - m_ic_write_buffer(offset={}, size={})\n"
" - size={}, page={}, count={}\n",
write_offset, sizeof(m_ic_card_data), 10, sizeof(m_ic_write_buffer),
write_size, page, count);
data_in = data_in_end;
break;
}
memcpy(m_ic_card_data + write_offset, m_ic_write_buffer + 10, write_size);
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 25 (IC-CARD) Write Pages:{} Count:{}({:x})", page,
count, size);
icco.command = WritePages;
if (!validate_data_in_out(0, icco.pktlen + 2, "SerialA (IC-CARD)"))
break;
ICCardSendReply(&icco, data_out.data(), &data_offset);
}
if (!validate_data_in_out(length, 0, "SerialA (IC-CARD)"))
break;
data_in += length;
break;
}
switch (ICCARDCommand(serial_command))
{
case ICCARDCommand::GetStatus:
icco.status = m_ic_card_state;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Get Status:{:02x}",
m_ic_card_state);
break;
case ICCARDCommand::SetBaudrate:
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Set Baudrate");
break;
case ICCARDCommand::FieldOn:
m_ic_card_state |= 0x10;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Field On");
break;
case ICCARDCommand::InsertCheck:
icco.status = m_ic_card_status;
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (IC-CARD) Insert Check:{:02x}", m_ic_card_status);
break;
case ICCARDCommand::AntiCollision:
icco.extlen = 8;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
// Card ID
icco.extdata[0] = 0x00;
icco.extdata[1] = 0x00;
icco.extdata[2] = 0x54;
icco.extdata[3] = 0x4D;
icco.extdata[4] = 0x50;
icco.extdata[5] = 0x00;
icco.extdata[6] = 0x00;
icco.extdata[7] = 0x00;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Anti Collision");
break;
case ICCARDCommand::SelectCard:
icco.extlen = 8;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
// Session
icco.extdata[0] = 0x00;
icco.extdata[1] = m_ic_card_session;
icco.extdata[2] = 0x00;
icco.extdata[3] = 0x00;
icco.extdata[4] = 0x00;
icco.extdata[5] = 0x00;
icco.extdata[6] = 0x00;
icco.extdata[7] = 0x00;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Select Card:{}",
m_ic_card_session);
break;
case ICCARDCommand::ReadPage:
case ICCARDCommand::ReadUseCount:
{
if (!validate_data_in_out(8, 0, "SerialA (IC-CARD)"))
break;
const u16 page = Common::swap16(data_in + 6) & 0xFF; // 255 is max page
icco.extlen = 8;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
memcpy(icco.extdata, m_ic_card_data + page * 8, 8);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 31 (IC-CARD) Read Page:{}", page);
break;
}
case ICCARDCommand::WritePage:
{
if (!validate_data_in_out(10, 0, "SerialA (IC-CARD)"))
break;
const u16 page = Common::swap16(data_in + 8) & 0xFF; // 255 is max page
// Write only one page
if (page == 4)
{
icco.status = 0x80;
}
else
{
if (!validate_data_in_out(18, 0, "SerialA (IC-CARD)"))
break;
memcpy(m_ic_card_data + page * 8, data_in + 10, 8);
}
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Write Page:{}",
page);
break;
}
case ICCARDCommand::DecreaseUseCount:
{
if (!validate_data_in_out(8, 0, "SerialA (IC-CARD)"))
break;
const u16 page = Common::swap16(data_in + 6) & 0xFF; // 255 is max page
icco.extlen = 2;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
auto ic_card_data = Common::BitCastPtr<u16>(m_ic_card_data + 0x28);
ic_card_data = ic_card_data - 1;
// Counter
icco.extdata[0] = m_ic_card_data[0x28];
icco.extdata[1] = m_ic_card_data[0x29];
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 31 (IC-CARD) Decrease Use Count:{}", page);
break;
}
case ICCARDCommand::ReadPages:
{
if (!validate_data_in_out(10, 0, "SerialA (IC-CARD)"))
break;
const u16 page = Common::swap16(data_in + 6) & 0xFF; // 255 is max page
const u16 count = Common::swap16(data_in + 8);
const u32 offs = page * 8;
u32 cnt = count * 8;
// Limit read size to not overwrite the reply buffer
const std::size_t reply_buffer_size = sizeof(icco.extdata) - 1;
if (data_offset > reply_buffer_size)
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 31 (IC-CARD) Read Pages overflow:"
" offset={} > buffer_size={}",
data_offset, reply_buffer_size);
data_in = data_in_end;
break;
}
if (cnt > reply_buffer_size - data_offset)
{
cnt = 5 * 8;
}
icco.extlen = cnt;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
if (offs + cnt > sizeof(icco.extdata))
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 31 (IC-CARD) Read Pages overflow:"
" offset={} + count={} > buffer_size={}",
offs, cnt, sizeof(icco.extdata));
data_in = data_in_end;
break;
}
memcpy(icco.extdata, m_ic_card_data + offs, cnt);
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 31 (IC-CARD) Read Pages:{} Count:{}", page, count);
break;
}
case ICCARDCommand::WritePages:
{
if (!validate_data_in_out(10, 0, "SerialA (IC-CARD)"))
break;
const u16 pksize = length;
const u16 size = Common::swap16(data_in + 2);
const u16 page = Common::swap16(data_in + 6) & 0xFF; // 255 is max page
const u16 count = Common::swap16(data_in + 8);
const u32 write_size = u32(count) * 8;
const u32 write_offset = u32(page) * 8;
// We got a complete packet
if (pksize - 5 == size)
{
if (page == 4) // Read Only Page, must return error
{
icco.status = 0x80;
}
else
{
if (write_size + write_offset > sizeof(m_ic_card_data))
{
ERROR_LOG_FMT(
SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (IC-CARD) Data overflow: Pages:{} Count:{}({:x})",
page, count, size);
}
else
{
if (!validate_data_in_out(13 + write_size, 0, "SerialA (IC-CARD)"))
break;
memcpy(m_ic_card_data + write_offset, data_in + 13, write_size);
}
}
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (IC-CARD) Write Pages:{} Count:{}({:x})", page,
count, size);
}
// VirtuaStriker 4 splits the writes over multiple packets
else
{
if (!validate_data_in_out(2 + pksize, 0, "SerialA (IC-CARD)"))
break;
memcpy(m_ic_write_buffer, data_in + 2, pksize);
m_ic_write_offset += pksize;
m_ic_write_size = size;
}
break;
}
default:
// Handle Deck Reader commands
if (!validate_data_in_out(1, 0, "SerialA (DECK READER)"))
break;
serial_command = data_in[0];
icco.command = serial_command;
icco.flag = 0;
switch (CDReaderCommand(serial_command))
{
case CDReaderCommand::ProgramVersion:
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (DECK READER) Program Version");
icco.extlen = (u32)strlen(s_cdr_program_version);
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
memcpy(icco.extdata, s_cdr_program_version, icco.extlen);
break;
case CDReaderCommand::BootVersion:
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (DECK READER) Boot Version");
icco.extlen = (u32)strlen(s_cdr_boot_version);
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
memcpy(icco.extdata, s_cdr_boot_version, icco.extlen);
break;
case CDReaderCommand::ShutterGet:
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Shutter Get");
icco.extlen = 4;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
icco.extdata[0] = 0;
icco.extdata[1] = 0;
icco.extdata[2] = 0;
icco.extdata[3] = 0;
break;
case CDReaderCommand::CameraCheck:
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (DECK READER) Camera Check");
icco.extlen = 6;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
icco.extdata[0] = 0x23;
icco.extdata[1] = 0x28;
icco.extdata[2] = 0x45;
icco.extdata[3] = 0x29;
icco.extdata[4] = 0x45;
icco.extdata[5] = 0x29;
break;
case CDReaderCommand::ProgramChecksum:
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (DECK READER) Program Checksum");
icco.extlen = 4;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
icco.extdata[0] = 0x23;
icco.extdata[1] = 0x28;
icco.extdata[2] = 0x45;
icco.extdata[3] = 0x29;
break;
case CDReaderCommand::BootChecksum:
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (DECK READER) Boot Checksum");
icco.extlen = 4;
icco.length += icco.extlen;
icco.pktlen += icco.extlen;
icco.extdata[0] = 0x23;
icco.extdata[1] = 0x28;
icco.extdata[2] = 0x45;
icco.extdata[3] = 0x29;
break;
case CDReaderCommand::SelfTest:
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Self Test");
icco.flag = 0x00;
break;
case CDReaderCommand::SensLock:
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Sens Lock");
icco.flag = 0x01;
break;
case CDReaderCommand::SensCard:
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Sens Card");
break;
case CDReaderCommand::ShutterCard:
INFO_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (DECK READER) Shutter Card");
break;
case CDReaderCommand::ReadCard:
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Read Card");
icco.fixed = 0xAA;
icco.flag = 0xAA;
icco.extlen = sizeof(s_cdr_card_data);
icco.length = 0x72;
icco.status = Common::swap16(icco.extlen);
icco.pktlen += icco.extlen;
memcpy(icco.extdata, s_cdr_card_data, sizeof(s_cdr_card_data));
break;
default:
if (!validate_data_in_out(14, 0, "SerialA (DECK READER)"))
break;
WARN_LOG_FMT(SERIALINTERFACE_CARD,
"GC-AM: Command 0x31 (IC-Card) {:02x} {:02x} {:02x} {:02x} {:02x} "
"{:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}",
data_in[2], data_in[3], data_in[4], data_in[5], data_in[6], data_in[7],
data_in[8], data_in[9], data_in[10], data_in[11], data_in[12],
data_in[13]);
break;
}
break;
}
if (!validate_data_in_out(0, icco.pktlen + 2, "SerialA (IC-CARD)"))
break;
ICCardSendReply(&icco, data_out.data(), &data_offset);
if (!validate_data_in_out(2, 0, "SerialA (IC-CARD)"))
break;
data_in += length;
break;
}
}
u32 command_offset = 0;
@@ -1375,6 +919,11 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
{
std::array<u8, 3> player_data{};
const auto io_ports_player_data = m_io_ports.GetSwitchInputs(i);
std::copy_n(io_ports_player_data.data(),
std::min(io_ports_player_data.size(), player_data.size()),
player_data.data());
// Service button
if (pad_status.switches & SWITCH_SERVICE)
player_data[0] |= 0x40;
@@ -1545,15 +1094,6 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
// Tactics (D)
if (pad_status.button & PAD_BUTTON_RIGHT)
player_data[0] |= 0x04;
if (i == 0)
{
player_data[0] |= 0x10; // IC-Card Switch ON
// IC-Card Lock
if (pad_status.button & PAD_BUTTON_DOWN)
player_data[1] |= 0x20;
}
}
break;
// Controller configuration for Gekitou Pro Yakyuu
@@ -1611,11 +1151,6 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
// Switch 2
if (pad_status.button & PAD_BUTTON_B)
player_data[0] |= 0x08;
// Toggle inserted card
if (pad_status.button & PAD_TRIGGER_L)
{
m_ic_card_status ^= ICCARDStatus::NoCard;
}
}
break;
}
@@ -1806,70 +1341,45 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
break;
const u32 bytes = *jvs_io++;
if (!validate_jvs_io(bytes, "GeneralDriverOutput"))
break;
message.AddData(StatusOkay);
if (bytes)
{
message.AddData(StatusOkay);
m_io_ports.SetGenericOutputs({jvs_io, bytes});
// The lamps are controlled via this
if (AMMediaboard::GetGameType() == MarioKartGP)
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO,
"JVS-IO: GPO: delay=0x{:02x}, rx_reply=0x{:02x},"
" bytes={}, buffer:\n{}",
m_delay, m_rx_reply, bytes, HexDump(jvs_io, bytes));
if ((AMMediaboard::GetGameType() == FZeroAX) && bytes >= 3)
{
if (!validate_jvs_io(1, "GeneralDriverOutput (MarioKartGP)"))
// Handling of the motion seat used in F-Zero AXs DX version
const u16 seat_state = Common::swap16(jvs_io + 1) >> 2;
switch (seat_state)
{
case 0x70:
m_delay++;
if ((m_delay % 10) == 0)
{
m_rx_reply = 0xFB;
}
break;
case 0xF0:
m_rx_reply = 0xF0;
break;
default:
case 0xA0:
case 0x60:
break;
const u32 status = *jvs_io++;
if (status & 4)
{
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 32, Item Button ON");
}
else
{
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 32, Item Button OFF");
}
if (status & 8)
{
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 32, Cancel Button ON");
}
else
{
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 32, Cancel Button OFF");
}
break;
}
if (!validate_jvs_io(bytes, "GeneralDriverOutput"))
break;
INFO_LOG_FMT(SERIALINTERFACE_JVSIO,
"JVS-IO: Command 0x32, GPO: delay=0x{:02x}, rx_reply=0x{:02x},"
" bytes={}, buffer:\n{}",
m_delay, m_rx_reply, bytes, HexDump(jvs_io, bytes));
if (bytes < 3)
{
jvs_io += bytes;
break;
}
// Handling of the motion seat used in F-Zero AXs DX version
const u16 seat_state = Common::swap16(jvs_io + 1) >> 2;
jvs_io += bytes;
switch (seat_state)
{
case 0x70:
m_delay++;
if ((m_delay % 10) == 0)
{
m_rx_reply = 0xFB;
}
break;
case 0xF0:
m_rx_reply = 0xF0;
break;
default:
case 0xA0:
case 0x60:
break;
}
}
break;
}
@@ -1917,7 +1427,6 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0xF0, Reset");
m_delay = 0;
m_wheel_init = 0;
m_ic_card_state = 0x20;
}
message.AddData(StatusOkay);
@@ -1984,30 +1493,38 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
// Update attached serial devices and read data into our response buffer.
// This is done regardless of the game having just sent a SerialA/B write.
if (m_serial_device_b != nullptr)
{
m_serial_device_b->Update();
const auto process_serial_device = [&](Triforce::SerialDevice* serial_device, GCAMCommand cmd) {
if (serial_device == nullptr)
return;
serial_device->Update();
const auto out_length =
std::min(u32(m_serial_device_b->GetTxByteCount()), SERIAL_PORT_MAX_READ_SIZE);
std::min(u32(serial_device->GetTxByteCount()), SERIAL_PORT_MAX_READ_SIZE);
if (out_length != 0)
{
// Also accounting for the 2-byte header.
if (!validate_data_in_out(0, out_length + 2, "SerialB"))
break;
if (out_length == 0)
return;
// Write the 2-byte header.
data_out[data_offset++] = GCAMCommand::SerialB;
data_out[data_offset++] = u8(out_length);
// Also accounting for the 2-byte header.
if (!validate_data_in_out(0, out_length + 2, "SerialDevice"))
return;
const auto out_span = std::span{data_out}.subspan(data_offset, out_length);
// Write the 2-byte header.
data_out[data_offset++] = cmd;
data_out[data_offset++] = u8(out_length);
m_serial_device_b->TakeTxBytes(out_span);
const auto out_span = std::span{data_out}.subspan(data_offset, out_length);
data_offset += out_length;
}
}
serial_device->TakeTxBytes(out_span);
data_offset += out_length;
};
m_io_ports.Update();
process_serial_device(m_serial_device_a.get(), GCAMCommand::SerialA);
process_serial_device(m_serial_device_b.get(), GCAMCommand::SerialB);
data_out[0] = 0x01; // Status code ?
data_out[1] = data_offset - response_header_size; // Length
@@ -2065,16 +1582,11 @@ void CSIDevice_AMBaseboard::DoState(PointerWrap& p)
p.Do(m_coin);
p.Do(m_coin_pressed);
p.Do(m_ic_card_data);
m_io_ports.DoState(p);
// Setup IC-card
p.Do(m_ic_card_state);
p.Do(m_ic_card_status);
p.Do(m_ic_card_session);
p.Do(m_ic_write_buffer);
p.Do(m_ic_write_offset);
p.Do(m_ic_write_size);
// Serial A
if (m_serial_device_a != nullptr)
m_serial_device_a->DoState(p);
// Serial B
if (m_serial_device_b != nullptr)
+6 -73
View File
@@ -6,6 +6,7 @@
#include "Core/HW/MagCard/MagneticCardReader.h"
#include "Core/HW/SI/SI.h"
#include "Core/HW/SI/SI_Device.h"
#include "Core/HW/Triforce/IOPorts.h"
namespace Triforce
{
@@ -118,66 +119,6 @@ private:
AcknowledgeOverflow = 4,
};
enum ICCARDCommand
{
GetStatus = 0x10,
SetBaudrate = 0x11,
FieldOn = 0x14,
FieldOff = 0x15,
InsertCheck = 0x20,
AntiCollision = 0x21,
SelectCard = 0x22,
ReadPage = 0x24,
WritePage = 0x25,
DecreaseUseCount = 0x26,
ReadUseCount = 0x33,
ReadPages = 0x34,
WritePages = 0x35,
};
enum ICCARDStatus
{
Okay = 0,
NoCard = 0x8000,
Unknown = 0x800E,
BadCard = 0xFFFF,
};
enum CDReaderCommand
{
ShutterAuto = 0x61,
BootVersion = 0x62,
SensLock = 0x63,
SensCard = 0x65,
FirmwareUpdate = 0x66,
ShutterGet = 0x67,
CameraCheck = 0x68,
ShutterCard = 0x69,
ProgramChecksum = 0x6B,
BootChecksum = 0x6D,
ShutterLoad = 0x6F,
ReadCard = 0x72,
ShutterSave = 0x73,
SelfTest = 0x74,
ProgramVersion = 0x76,
};
// NOTE: Used to be an union with `u8 data[81 + 4 + 4 + 4]`
// TODO: Should the struct be packed?
struct ICCommand
{
u32 pktcmd : 8;
u32 pktlen : 8;
u32 fixed : 8;
u32 command : 8;
u32 flag : 8;
u32 length : 8;
u32 status : 16;
u8 extdata[81] = {};
u32 extlen;
};
static constexpr u32 RESPONSE_SIZE = SerialInterfaceManager::BUFFER_SIZE;
// This value prevents F-Zero AX mag card breakage.
@@ -189,23 +130,17 @@ private:
std::array<std::array<u8, RESPONSE_SIZE>, 2> m_response_buffers{};
u8 m_current_response_buffer_index = 0;
Triforce::IOPorts m_io_ports;
std::array<u16, 2> m_coin{};
std::array<u32, 2> m_coin_pressed{};
u8 m_ic_card_data[2048] = {};
// Setup IC-card
u16 m_ic_card_state = 0x20;
u16 m_ic_card_status = ICCARDStatus::Okay;
u16 m_ic_card_session = 0x23;
u8 m_ic_write_buffer[512] = {};
u32 m_ic_write_offset = 0;
u32 m_ic_write_size = 0;
// Magnetic Card Reader
MagCard::MagneticCardReader::Settings m_mag_card_settings;
// Serial A
std::unique_ptr<Triforce::SerialDevice> m_serial_device_a;
// Serial B
std::unique_ptr<Triforce::SerialDevice> m_serial_device_b;
@@ -232,8 +167,6 @@ private:
u32 m_dip_switch_0 = 0xFF;
int m_delay = 0;
void ICCardSendReply(ICCommand* iccommand, u8* buffer, u32* length);
};
} // namespace SerialInterface
+449
View File
@@ -0,0 +1,449 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/Triforce/DeckReader.h"
#include <string_view>
#include <fmt/ranges.h>
#include <picojson.h>
#include "Common/BitUtils.h"
#include "Common/ChunkFile.h"
#include "Common/FileUtil.h"
#include "Common/JsonUtil.h"
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "Common/Swap.h"
#include "Core/ConfigManager.h"
namespace
{
// Note: Avalon literally skips `strlen("Version ")` bytes in the display.
constexpr std::string_view CDR_PROGRAM_VERSION = "Version 1.22,2003/09/19,171-8213B";
constexpr std::string_view CDR_BOOT_VERSION = "Version 1.04,2003/06/17,171-8213B";
constexpr std::size_t EXPECTED_VERSION_STR_LENGTH = 48;
// Avalon seems to not care about the actual value.
constexpr u32 SHUTTER_TIME = 1234567890;
constexpr u32 FIRMWARE_UPDATE_TIMEOUT = 240;
auto GetFirmwareDumpFilename()
{
return fmt::format("{}card_deck_reader_firmware.bin", File::GetUserPath(D_TRIUSER_IDX));
}
enum class CDReaderCommand : u8
{
ShutterAuto = 0x61,
BootVersion = 0x62,
SensLock = 0x63,
SensCard = 0x65,
FirmwareUpdate = 0x66,
ShutterGet = 0x67,
CameraCheck = 0x68,
Shutter = 0x69,
ProgramChecksum = 0x6b,
ShutterAlt = 0x6c, // Not sure what the difference is. Avalon seems to only use 0x69.
BootChecksum = 0x6d,
ShutterLoad = 0x6f,
ReadCard = 0x72,
ShutterSave = 0x73,
SelfTest = 0x74,
ProgramVersion = 0x76,
};
#pragma pack(push, 1)
struct CardIdentifier
{
// When the 0x01 bit is set, Avalon indexes a separate smaller table.
// Avalon specifically requires 0x80, 0x40, and 0x20 bits are not set.
// We don't know the relevance of this second table.
// There are even some duplicates between the two tables.
u8 table_index;
Common::BigEndianValue<u16> card_index;
};
#pragma pack(pop)
std::optional<std::vector<CardIdentifier>> LoadCardDeckFromFile()
{
// Example json format:
// table defaults to 0. quantity defaults to 1.
//
// {
// "cards": [
// {
// "index": 12
// },
// {
// "table": 1,
// "index": 13
// },
// {
// "index": 18,
// "quantity": 3
// }
// ]
// }
const std::string filename =
fmt::format("{}tricard_{}_deck.json", File::GetUserPath(D_TRIUSER_IDX),
SConfig::GetInstance().GetGameID());
std::string file_contents;
File::ReadFileToString(filename, file_contents);
picojson::value json_root;
const auto err = picojson::parse(json_root, file_contents);
if (!err.empty())
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "LoadCardDeckFromFile: {}", err);
return std::nullopt;
}
if (!json_root.is<picojson::object>())
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "LoadCardDeckFromFile: Invalid JSON root object.");
return std::nullopt;
}
const auto cards_obj = json_root.get("cards");
if (!cards_obj.is<picojson::array>())
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "LoadCardDeckFromFile: Invalid \"cards\" array.");
return std::nullopt;
}
std::optional<std::vector<CardIdentifier>> result;
result.emplace();
for (const auto& item : cards_obj.get<picojson::array>())
{
if (!item.is<picojson::object>())
continue;
const auto& item_obj = item.get<picojson::object>();
const auto card_index = ReadNumericFromJson<u16>(item_obj, "index");
if (!card_index)
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "LoadCardDeckFromFile: Invalid \"index\" field.");
continue;
}
CardIdentifier card_id{};
card_id.card_index = *card_index;
card_id.table_index = ReadNumericFromJson<u8>(item_obj, "table").value_or(0);
const auto quantity = ReadNumericFromJson<u8>(item_obj, "quantity").value_or(1);
result->resize(result->size() + quantity, card_id);
}
return result;
}
} // namespace
namespace Triforce
{
void DeckReader::Update()
{
Common::ScopeGuard update_ic_card_reader{[this] {
// Update and take any response data from the IC card reader.
m_ic_card_reader.Update();
PassThroughTxBytes(m_ic_card_reader);
}};
// All Deck Reader commands are exactly one byte (except FirmwareUpdate).
// Responses must be the correct length, which varies by command.
// Only the ReadCard response actually includes some kind of length field.
//
// A leading 2 bytes and trailing 1 byte are expected for all responses.
// Avalon seems to largely not check the actual values,
// but a few handlers do verify them.
// FirmwareUpdate is the only known stateful command.
if (m_firmware_update_timeout != 0)
{
HandleFirmwareUpdate();
return;
}
const auto input_span = GetRxByteSpan();
if (input_span.empty())
return; // Wait for more data.
const u8 cd_reader_command = input_span.front();
if (cd_reader_command == 0x00)
{
// This is an IC Card Reader command.
// How does the actual hardware deal with this ?
// Does the Deck Reader really parse the IC Card Reader commands ?
if (input_span.size() < 4)
return; // Wait for more data.
const u16 input_payload_size = Common::swap16(input_span.data() + 2);
const u32 total_request_size = input_payload_size + 5u;
if (input_span.size() >= total_request_size)
{
// Forward the data to the attached IC Card Reader.
m_ic_card_reader.WriteRxBytes(input_span.first(total_request_size));
ConsumeRxBytes(total_request_size);
}
return;
}
// Write header.
WriteTxBytes(std::array<u8, 2>{0xaa, cd_reader_command});
switch (CDReaderCommand(cd_reader_command))
{
case CDReaderCommand::SelfTest:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "SelfTest");
// Avalon appears to not inspect this value.
WriteTxByte(0x00);
break;
}
case CDReaderCommand::SensLock:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "SensLock");
// Avalon will ask the user to close the shutter if open.
constexpr bool is_closed = true;
WriteTxByte(is_closed ? 0x01 : 0x00);
break;
}
case CDReaderCommand::ProgramVersion:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ProgramVersion");
WriteTxBytes(Common::AsU8Span((CDR_PROGRAM_VERSION)));
// Pad with zeros.
WriteTxBytes(std::array<u8, EXPECTED_VERSION_STR_LENGTH - CDR_PROGRAM_VERSION.size()>{});
break;
}
case CDReaderCommand::BootVersion:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "BootVersion");
WriteTxBytes(Common::AsU8Span(CDR_BOOT_VERSION));
// Pad with zeros.
WriteTxBytes(std::array<u8, EXPECTED_VERSION_STR_LENGTH - CDR_BOOT_VERSION.size()>{});
break;
}
case CDReaderCommand::ProgramChecksum:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ProgramChecksum");
// Avalon seems to not care about the actual value.
Common::BigEndianValue<u32> fake_checksum{0x89abcdef};
WriteTxBytes(Common::AsU8Span(fake_checksum));
break;
}
case CDReaderCommand::BootChecksum:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "BootChecksum");
// Avalon seems to not care about the actual value.
Common::BigEndianValue<u32> fake_checksum{0x01234567};
WriteTxBytes(Common::AsU8Span(fake_checksum));
break;
}
case CDReaderCommand::CameraCheck:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "CameraCheck");
// FYI: If `header[1] != 0x68` the game inspects some bits and 9 bytes. Some error state ?
std::array<u8, 10> response{};
WriteTxBytes(response);
break;
}
case CDReaderCommand::ShutterGet:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ShutterGet");
Common::BigEndianValue<u32> shutter_time{SHUTTER_TIME};
WriteTxBytes(Common::AsU8Span(shutter_time));
break;
}
case CDReaderCommand::ShutterAuto:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ShutterAuto");
Common::BigEndianValue<u32> shutter_time{SHUTTER_TIME};
WriteTxBytes(Common::AsU8Span(shutter_time));
break;
}
case CDReaderCommand::Shutter:
case CDReaderCommand::ShutterAlt:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "Shutter");
break;
}
case CDReaderCommand::ShutterLoad:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ShutterLoad");
break;
}
case CDReaderCommand::ShutterSave:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ShutterSave");
break;
}
case CDReaderCommand::SensCard:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "SensCard");
// TODO: What is the meaning of this value ?
// Avalon tests for 0x01.
WriteTxByte(0x01);
break;
}
case CDReaderCommand::ReadCard:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ReadCard");
constexpr bool is_deck_empty = false;
if (is_deck_empty)
{
// I think this means that there are no cards or maybe "not ready".
WriteTxBytes(std::array<u8, 2>{0xaa, 0x52});
}
else
{
// We've had one, yes, but what about second header ?
WriteTxBytes(std::array<u8, 2>{0xaa, u8(CDReaderCommand::ReadCard)});
if (const auto deck = LoadCardDeckFromFile())
{
// What happens with more than 30 cards ?
WriteTxByte(u8(deck->size() * sizeof(CardIdentifier)));
WriteTxBytes(Common::AsU8Span(*deck));
}
else
{
constexpr u8 deck_size = 0;
WriteTxByte(deck_size);
}
}
break;
}
case CDReaderCommand::FirmwareUpdate:
{
INFO_LOG_FMT(SERIALINTERFACE_CARD, "FirmwareUpdate");
m_firmware_update_timeout = FIRMWARE_UPDATE_TIMEOUT;
// The game will now send the raw firmware data which we write to a file.
// FYI: It appears to be SuperH4:LE code.
const auto filename = GetFirmwareDumpFilename();
if (m_firmware_dump_file.Open(filename, File::AccessMode::Write))
{
NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Writing firmware to: {}", filename);
}
else
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Failed to open: {}", filename);
}
break;
}
default:
{
// TODO: I think maybe we're supposed to produce a header of {0xaa, 0x55} ?
// Responses seem to be variable in length and unspecified so we can't do much.
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Unknown command: {:02x}", cd_reader_command);
break;
}
}
// Write footer.
// TODO: Is this an error code maybe ?
WriteTxByte(0x00);
// Every known command (other than FirmwareUpdate) is just one byte.
ConsumeRxBytes(1);
}
void DeckReader::HandleFirmwareUpdate()
{
const auto input_span = GetRxByteSpan();
// No idea how the real hardware works.
// There doesn't seem to be any sort of size field so.. here's a timeout.
if (input_span.empty())
{
if (--m_firmware_update_timeout == 0)
{
constexpr std::array<u8, 3> fw_update_done{0xaa, u8(CDReaderCommand::FirmwareUpdate), 0x00};
WriteTxBytes(fw_update_done);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "Fake FirmwareUpdate done.");
m_firmware_dump_file.Close();
}
}
else
{
if (m_firmware_dump_file.IsOpen())
{
if (!m_firmware_dump_file.Write(input_span))
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Failed to write {} bytes to firmware.",
input_span.size());
}
}
ConsumeRxBytes(input_span.size());
m_firmware_update_timeout = FIRMWARE_UPDATE_TIMEOUT;
}
}
void DeckReader::DoState(PointerWrap& p)
{
SerialDevice::DoState(p);
m_ic_card_reader.DoState(p);
p.Do(m_firmware_update_timeout);
if (p.IsReadMode())
m_firmware_dump_file.Close();
}
} // namespace Triforce
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Common/DirectIOFile.h"
#include "Core/HW/Triforce/ICCardReader.h"
#include "Core/HW/Triforce/SerialDevice.h"
class PointerWrap;
namespace Triforce
{
// Serial deck reader used by The Key of Avalon games.
class DeckReader final : public SerialDevice
{
public:
void Update() override;
void DoState(PointerWrap& p) override;
auto* GetICCardReader() { return &m_ic_card_reader; }
private:
// It seems that the IC Card Reader must be connected through the Deck Reader.
// The Deck Reader forwards appropriate commands to the IC Card Reader.
// IC Card Reader responses are passed through back to the baseboard as-is.
// It can't be the other way around because FirmwareUpdate sends many raw bytes.
// Unless FirmwareUpdate temporarily cuts off the IC Card Reader ?
ICCardReader m_ic_card_reader{0};
void HandleFirmwareUpdate();
u8 m_firmware_update_timeout = 0;
File::DirectIOFile m_firmware_dump_file;
};
} // namespace Triforce
@@ -0,0 +1,774 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/Triforce/ICCardReader.h"
#include <numeric>
#include <fmt/ranges.h>
#include "Common/BitUtils.h"
#include "Common/ChunkFile.h"
#include "Common/DirectIOFile.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "Common/Swap.h"
#include "Core/ConfigManager.h"
#include "Core/HW/DVD/AMMediaboard.h"
namespace
{
// We return a card session with our card index in the lower bits.
constexpr u16 CARD_SESSION_BASE = 0x2300;
// The serial number is located here. Games seem to expect it to be read-only.
constexpr u32 READ_ONLY_PAGE_INDEX = 4;
constexpr u8 CheckSumXOR(std::span<const u8> data)
{
return std::accumulate(data.data(), data.data() + data.size(), u8{}, std::bit_xor{});
}
bool LoadCardData(const std::string& filename, std::span<u8> data)
{
File::DirectIOFile file{filename, File::AccessMode::Read};
if (!File::Exists(filename))
return false;
const auto file_size = file.GetSize();
if (file_size > data.size())
{
WARN_LOG_FMT(SERIALINTERFACE_CARD, "Unexpected size of {} for file: {}", file_size, filename);
}
const auto read_size = std::min<u64>(file_size, data.size());
if (!file.Read(std::span{data}.first(read_size)))
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Failed to read from file: {}", filename);
return false;
}
INFO_LOG_FMT(SERIALINTERFACE_CARD, "Loaded {} bytes from file: {}", read_size, filename);
return true;
}
void SanitizeSerialNumber(std::span<u8> data)
{
DEBUG_ASSERT(data.size() == 8);
// This seems to be treated as a 16-digit BCD value.
// Avalon: 9571 264_ XXXX XXXX
// Where _ is mod 10 of the sum of all the X digits.
// VirtuaStriker4: 9571 440_ XXXX XXXX
// I think this might be generated from the SN physically printed on the card.
// The game code that deals with this is very complicated.
// We're lucky that all zeros seems to work.
// Gekitou: 9571 765_ XXXX XXXX
// How does the checksum work ?
data[0] = 0x95;
data[1] = 0x71;
switch (AMMediaboard::GetGameType())
{
case AMMediaboard::KeyOfAvalon:
{
data[2] = 0x26;
data[3] = 0x40;
u32 checksum = 0;
for (auto bcd_pair : data.subspan(4))
checksum += (bcd_pair & 0x0f) + (bcd_pair >> 4);
data[3] |= (checksum % 10);
break;
}
case AMMediaboard::VirtuaStriker4:
case AMMediaboard::VirtuaStriker4_2006:
{
data[2] = 0x44;
data[3] = 0x00;
break;
}
case AMMediaboard::GekitouProYakyuu:
{
data[2] = 0x76;
data[3] = 0x50;
break;
}
default:
break;
}
}
void InitializeCardData(std::span<u8> data)
{
SanitizeSerialNumber(data.subspan(READ_ONLY_PAGE_INDEX * 8, 8));
constexpr u32 use_count_offset = 0x28;
constexpr u16 use_count = 0;
// The use count seems to be a big endian count down from 0xffff.
Common::WriteSwap16(data.data() + use_count_offset, 0xffff - use_count);
}
// TODO: I'm not so sure that this is really a device status.
// The values that Avalon seems to test for seem very command-specific.
// It might be more like a command result code.
enum ICCardStatus : u16
{
Okay = 0x0000,
NoCard = 0x8000,
Unknown = 0x800e,
BadCard = 0xffff,
// TODO: Figure out what's actually returned in these situations.
HardwareUntestedErrorCode = 0x0080,
};
enum class ICCardCommand : u8
{
GetStatus = 0x10,
SetBaudrate = 0x11,
FieldOn = 0x14,
FieldOff = 0x15,
EjectCard = 0x16,
InsertCheck = 0x20,
AntiCollision = 0x21,
SelectCard = 0x22,
ReadPage = 0x24,
WritePage = 0x25,
DecreaseUseCount = 0x26,
HaltCard = 0x27,
ReadUseCount = 0x33,
ReadPages = 0x34,
WritePages = 0x35,
};
} // namespace
namespace Triforce
{
ICCardReader::ICCardReader(u8 slot_index) : m_slot_index{slot_index}
{
}
void ICCardReader::CreateCards(u8 card_count)
{
m_ic_cards.clear();
// Filled in with slot index and card number just for uniqueness.
ICCard::UID card_id = {0x00, 0x00, 0x54, 0x4D, 0x50, 0x00, m_slot_index, 0x00};
for (u8 i = 0; i != card_count; ++i)
{
std::string slot_name = fmt::format("slot{}", m_slot_index + 1);
// Files for additional tags get naming like "slot1b.bin"
if (i > 0)
slot_name += char('a' + i);
const auto filename = fmt::format("{}tricard_{}_{}.bin", File::GetUserPath(D_TRIUSER_IDX),
SConfig::GetInstance().GetGameID(), slot_name);
card_id.back() = i;
m_ic_cards.emplace_back(std::make_unique<ICCard>(filename, card_id));
}
}
void ICCardReader::Update()
{
if (m_eject_timer != 0)
--m_eject_timer;
const auto rx_byte_span = GetRxByteSpan();
if (rx_byte_span.size() < 4)
return; // Wait for more data.
// For reference:
// struct RequestPacket
// {
// u8 fixed; // Seems to be always 0x00.
// u8 ic_card_command;
// u16 payload_size; // Big-endian.
// u8 payload[payload_size];
// u8 checksum; // XOR of all previous bytes.
// };
const u16 input_payload_size = Common::swap16(rx_byte_span.data() + 2);
// 4 header bytes + 1 checksum byte
const u32 entire_request_size = input_payload_size + 5u;
if (rx_byte_span.size() < entire_request_size)
return; // Wait for more data.
if (CheckSumXOR(rx_byte_span.first(entire_request_size)) != 0)
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Bad checksum!");
ConsumeRxBytes(1);
return;
}
Common::ScopeGuard consume_request{[&] { ConsumeRxBytes(entire_request_size); }};
const u8 card_command = rx_byte_span[1];
const auto input_payload = rx_byte_span.subspan(4, input_payload_size);
u16 status_code = 0x00;
const auto validate_input_payload_size = [&](u32 expected_size) {
if (input_payload_size < expected_size)
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Undersized payload size {} for command: {:02x}",
input_payload_size, card_command);
status_code = HardwareUntestedErrorCode;
return false;
}
if (input_payload_size > expected_size)
{
WARN_LOG_FMT(SERIALINTERFACE_CARD, "Oversized payload size {} for command: {:02x}",
input_payload_size, card_command);
}
return true;
};
const auto get_card_for_session = [&](u16 card_session) -> ICCard* {
if ((card_session & 0xff00) == CARD_SESSION_BASE)
{
// FYI: We don't verify that this session was actually created before using it.
// This is fine, but I'm sure it's not really hardware accurate.
const std::size_t index = card_session & 0xffu;
if (index < m_ic_cards.size())
return m_ic_cards[index].get();
}
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Unexpected card session: {:04x}", card_session);
status_code = HardwareUntestedErrorCode;
return nullptr;
};
// Note: Command and response payload sizes are multiples of 8-bytes.
std::array<u8, 8> small_reply_payload{};
// Will be later assigned to `small_reply_payload` or some region of the card data itself.
std::span<const u8> reply_payload_span;
// FYI: Many of the big-endian u16 parameters may just be u8 values.
// Avalon writes single bytes, but at odd addresses, so u16 seems like the intention.
// TODO: Many of these functions should probably fail when the field is off.
// It might make sense for that logic to be inside ICCard.
if (ICCardCommand(card_command) != ICCardCommand::InsertCheck)
{
m_card_present_insert_check_count = 0;
}
switch (ICCardCommand(card_command))
{
case ICCardCommand::GetStatus:
{
if (!validate_input_payload_size(0))
break;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "GetStatus");
// Avalon's tests expect one of these specific values.
status_code = m_is_field_on ? 0x30 : 0x20;
break;
}
case ICCardCommand::SetBaudrate:
{
if (!validate_input_payload_size(8))
break;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "SetBaudrate: {:02x}", fmt::join(input_payload, " "));
break;
}
case ICCardCommand::FieldOn:
{
if (!validate_input_payload_size(0))
break;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "FieldOn");
m_is_field_on = true;
// FYI: Avalon's tests expect status code here to be 0x0000.
break;
}
case ICCardCommand::FieldOff:
{
if (!validate_input_payload_size(0))
break;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "FieldOff");
m_is_field_on = false;
std::ranges::for_each(m_ic_cards, &ICCard::SetIdle);
break;
}
case ICCardCommand::EjectCard:
{
if (!validate_input_payload_size(0))
break;
EjectCard();
break;
}
case ICCardCommand::InsertCheck:
{
if (!validate_input_payload_size(8))
break;
// Avalon sends 0 or 1 here, not sure what the meaning is.
// This is maybe the "Time Slot Number" (TSN)? 0=1-slot, 1=2-slots, etc.
const u16 unknown_parameter = Common::swap16(input_payload.data() + 0);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "InsertCheck: {}", unknown_parameter);
const bool is_card_present = std::ranges::any_of(m_ic_cards, std::not_fn(&ICCard::IsHalted));
if (is_card_present &&
m_card_present_insert_check_count <
std::numeric_limits<decltype(m_card_present_insert_check_count)>::max())
{
++m_card_present_insert_check_count;
}
// Avalon seems to test specifically for zero.
status_code = is_card_present ? 0x00 : HardwareUntestedErrorCode;
break;
}
case ICCardCommand::AntiCollision:
{
if (!validate_input_payload_size(16))
break;
// Avalon's logic optionally sets param0=0x20 and 8 memcpy'd bytes but never seems to do it.
const u16 unknown_param0 = Common::swap16(input_payload.data() + 0);
const auto unknown_param1 = input_payload.subspan(2, 8);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "AntiCollision: unk0:{:04x} unk1:{:02x}", unknown_param0,
fmt::join(unknown_param1, " "));
const auto first_non_halted_card = std::ranges::find_if_not(m_ic_cards, &ICCard::IsHalted);
if (first_non_halted_card != m_ic_cards.end())
{
reply_payload_span = (*first_non_halted_card)->GetUID();
const bool additional_cards =
std::ranges::count_if(m_ic_cards, std::not_fn(&ICCard::IsHalted)) > 1;
// Avalon seems to like a value of 0x00 or 0x01. 0x01 causes two cards to be processed.
status_code = additional_cards ? 0x01 : 0x00;
}
else
{
status_code = HardwareUntestedErrorCode;
}
break;
}
case ICCardCommand::SelectCard:
{
if (!validate_input_payload_size(8))
break;
// FYI: The request includes the UID that we produced in `AntiCollision`.
const auto card_uid = input_payload.first(8);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "SelectCard: {:02x}", fmt::join(card_uid, " "));
const auto found_card = std::ranges::find_if(
m_ic_cards, std::bind_front(std::ranges::equal, card_uid), &ICCard::GetUID);
if (found_card != m_ic_cards.end())
{
const u16 card_session = CARD_SESSION_BASE | u16(found_card - m_ic_cards.begin());
Common::WriteSwap16(small_reply_payload.data(), card_session);
reply_payload_span = small_reply_payload;
// Avalon's tests specifically want 0x00.
status_code = 0x00;
}
else
{
WARN_LOG_FMT(SERIALINTERFACE_CARD, "SelectCard: Unexpected Card ID.");
status_code = HardwareUntestedErrorCode;
break;
}
break;
}
// FYI: These two seem to have the same parameters.
// Maybe ReadUseCount is meant to copy just 2 bytes? The response is still expected to be 8.
case ICCardCommand::ReadPage:
case ICCardCommand::ReadUseCount:
{
if (!validate_input_payload_size(8))
break;
const u16 card_session = Common::swap16(input_payload.data() + 0);
const u16 page = Common::swap16(input_payload.data() + 2) & PAGE_INDEX_MASK;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ReadPage: session:{:04x} page:{}", card_session, page);
auto* const ic_card = get_card_for_session(card_session);
if (!ic_card)
break;
const auto read_span = ic_card->ReadData(page, 1);
if (read_span.empty())
{
status_code = HardwareUntestedErrorCode;
}
else
{
reply_payload_span = read_span;
}
break;
}
case ICCardCommand::WritePage:
{
if (!validate_input_payload_size(16))
break;
const u16 card_session = Common::swap16(input_payload.data() + 0);
// Avalon specifically puts a zero here.
const u16 unknown = Common::swap16(input_payload.data() + 2);
const u16 page = Common::swap16(input_payload.data() + 4) & PAGE_INDEX_MASK;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "WritePage: session:{:04x} unk:{} page:{}", card_session,
unknown, page);
auto* const ic_card = get_card_for_session(card_session);
if (!ic_card)
break;
if (!ic_card->WriteData(page, input_payload.subspan(6, PAGE_SIZE)))
{
status_code = HardwareUntestedErrorCode;
}
break;
}
case ICCardCommand::DecreaseUseCount:
{
if (!validate_input_payload_size(8))
break;
const u16 card_session = Common::swap16(input_payload.data() + 0);
// Avalon seems to always sends 5 and 1. Guessing on the meaning and behavior.
const u16 page = Common::swap16(input_payload.data() + 2) & PAGE_INDEX_MASK;
const u16 amount = Common::swap16(input_payload.data() + 4);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "DecreaseUseCount: session:{:04x}, page:{} amount:{}",
card_session, page, amount);
auto* const ic_card = get_card_for_session(card_session);
if (!ic_card)
break;
const u16 new_count = ic_card->DecreaseUseCount(page, amount);
Common::WriteSwap16(small_reply_payload.data(), new_count);
reply_payload_span = small_reply_payload;
break;
}
case ICCardCommand::HaltCard:
{
if (!validate_input_payload_size(8))
break;
const u16 card_session = Common::swap16(input_payload.data() + 0);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "HaltCard: session:{:04x}", card_session);
auto* const ic_card = get_card_for_session(card_session);
if (!ic_card)
break;
ic_card->SetHalted();
break;
}
case ICCardCommand::ReadPages:
{
if (!validate_input_payload_size(8))
break;
const u16 card_session = Common::swap16(input_payload.data() + 0);
const u16 page = Common::swap16(input_payload.data() + 2) & PAGE_INDEX_MASK;
const u16 page_count = Common::swap16(input_payload.data() + 4);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "ReadPages: session:{:04x} page:{} page_count:{}",
card_session, page, page_count);
auto* const ic_card = get_card_for_session(card_session);
if (!ic_card)
break;
const auto read_span = ic_card->ReadData(page, page_count);
if (read_span.empty())
{
status_code = HardwareUntestedErrorCode;
}
else
{
reply_payload_span = read_span;
}
break;
}
case ICCardCommand::WritePages:
{
if (input_payload_size < 8)
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "WritePages: Undersized payload size: {}",
input_payload_size);
break;
}
const u16 card_session = Common::swap16(input_payload.data() + 0);
const u32 page = Common::swap16(input_payload.data() + 2) & PAGE_INDEX_MASK;
const u32 page_count = Common::swap16(input_payload.data() + 4);
INFO_LOG_FMT(SERIALINTERFACE_CARD, "WritePages: session:{:04x} page:{} page_count:{}",
card_session, page, page_count);
auto* const ic_card = get_card_for_session(card_session);
if (!ic_card)
break;
const u32 byte_count = page_count * PAGE_SIZE;
if (!validate_input_payload_size(8 + byte_count))
break;
if (!ic_card->WriteData(page, input_payload.subspan(6, byte_count)))
{
status_code = HardwareUntestedErrorCode;
}
break;
}
default:
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Unknown command: {:02x}", card_command);
break;
}
}
SendReply(card_command, status_code, reply_payload_span);
}
void ICCardReader::SendReply(u8 command, u16 status_code, std::span<const u8> payload)
{
struct ICCardReplyHeader
{
u8 fixed = 0x10; // Games seem to expect 0x10.
u8 command;
Common::BigEndianValue<u16> length; // Includes status and payload bytes.
Common::BigEndianValue<u16> status;
};
ICCardReplyHeader header{.command = command};
header.length = u16(sizeof(header.status) + payload.size());
header.status = status_code;
const auto header_span = Common::AsU8Span(header);
const u8 checksum = CheckSumXOR(header_span) ^ CheckSumXOR(payload);
WriteTxBytes(header_span);
WriteTxBytes(payload);
WriteTxByte(checksum);
}
std::span<const u8> ICCardReader::ICCard::ReadData(u16 page, u16 page_count)
{
const u32 byte_offset = page * PAGE_SIZE;
const u32 byte_count = page_count * PAGE_SIZE;
if (byte_count + byte_offset > m_data.size())
{
WARN_LOG_FMT(SERIALINTERFACE_CARD, "ReadData: Attempt to read beyond end of card.");
return {};
}
const auto read_span = std::span{m_data}.subspan(byte_offset, byte_count);
DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "\n{}", HexDump(read_span));
return read_span;
}
bool ICCardReader::ICCard::WriteData(u16 page, std::span<const u8> write_span)
{
constexpr u32 read_only_area_begin = READ_ONLY_PAGE_INDEX * PAGE_SIZE;
constexpr u32 read_only_area_end = read_only_area_begin + PAGE_SIZE;
const u32 byte_offset = page * PAGE_SIZE;
if ((byte_offset < read_only_area_end) &&
(byte_offset + write_span.size() > read_only_area_begin))
{
WARN_LOG_FMT(SERIALINTERFACE_CARD, "WriteData: Read-only page.");
// Read-only page, must return error.
return false;
}
if (write_span.size() + byte_offset > m_data.size())
{
WARN_LOG_FMT(SERIALINTERFACE_CARD, "WriteData: Attempt to write beyond end of card.");
return false;
}
std::ranges::copy(write_span, m_data.data() + byte_offset);
DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "\n{}", HexDump(write_span));
FlushData(byte_offset, u32(write_span.size()));
return true;
}
u16 ICCardReader::ICCard::DecreaseUseCount(u16 page, u16 amount)
{
const u32 byte_offset = page * PAGE_SIZE;
auto* const addr = m_data.data() + byte_offset;
const u16 previous_use_count = Common::swap16(addr);
// We're not sure how the real hardware functions when decreasing below 0.
// We'll clamp it to 0 for now.
const u16 new_use_count = MathUtil::SaturatingCast<u16>(s32(previous_use_count) - amount);
NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "DecreaseUseCount: {} -> {}", previous_use_count,
new_use_count);
Common::WriteSwap16(addr, new_use_count);
FlushData(byte_offset, sizeof(u16));
return new_use_count;
}
// TODO: Maybe in the future we should write to disk after a delay.
// Games seem to write many chunks when saving.
void ICCardReader::ICCard::FlushData(u32 byte_offset, u32 byte_count)
{
File::DirectIOFile file{m_filename, File::AccessMode::Write, File::OpenMode::Always};
if (!file.OffsetWrite(byte_offset, std::span{m_data}.subspan(byte_offset, byte_count)))
{
ERROR_LOG_FMT(SERIALINTERFACE_CARD, "FlushData: Failed to write to: {}", m_filename);
}
}
void ICCardReader::DoState(PointerWrap& p)
{
SerialDevice::DoState(p);
// TODO: Think about what to do with the card data on the filesystem.
auto card_count = u8(m_ic_cards.size());
p.Do(card_count);
if (card_count != m_ic_cards.size())
CreateCards(card_count);
for (auto& card : m_ic_cards)
card->DoState(p);
p.Do(m_is_field_on);
p.Do(m_eject_timer);
p.Do(m_card_present_insert_check_count);
}
void ICCardReader::ICCard::DoState(PointerWrap& p)
{
p.Do(m_data);
p.Do(m_uid); // UID is deterministically generated, but we'll sync it in case that changes.
p.Do(m_current_state);
}
ICCardReader::ICCard::ICCard(std::string filename, const UID& uid)
: m_filename{std::move(filename)}, m_uid{uid}
{
}
void ICCardReader::ICCard::Initialize()
{
if (!LoadCardData(m_filename, m_data))
{
NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Creating new IC Card data.");
InitializeCardData(m_data);
FlushData(READ_ONLY_PAGE_INDEX * PAGE_SIZE, PAGE_SIZE * 2);
}
}
bool ICCardReader::IsCardPresent() const
{
return !m_ic_cards.empty();
}
bool ICCardReader::IsEjecting() const
{
return m_eject_timer > 60;
}
bool ICCardReader::IsReadyToInsertCard() const
{
return !IsCardPresent() && m_eject_timer == 0;
}
void ICCardReader::InsertCard()
{
if (IsCardPresent())
return;
NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "InsertCard");
CreateCards(1);
std::ranges::for_each(m_ic_cards, &ICCard::Initialize);
}
void ICCardReader::EjectCard()
{
if (!IsCardPresent())
return;
NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "EjectCard");
m_ic_cards.clear();
m_eject_timer = 120;
m_card_present_insert_check_count = 0;
}
} // namespace Triforce
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <span>
#include <string>
#include <vector>
#include "Common/CommonTypes.h"
#include "Core/HW/Triforce/SerialDevice.h"
namespace Triforce
{
// IC card reader.
// Used by: GekitouProYakyuu, KeyOfAvalon, VirtuaStriker4[_2006]
class ICCardReader final : public SerialDevice
{
public:
explicit ICCardReader(u8 slot_index);
void Update() override;
bool IsCardPresent() const;
bool IsEjecting() const;
bool IsReadyToInsertCard() const;
void InsertCard();
void EjectCard();
u8 GetCardPresentInsertCheckCount() const { return m_card_present_insert_check_count; }
void DoState(PointerWrap& p) override;
private:
void CreateCards(u8 card_count);
void SendReply(u8 command, u16 status_code, std::span<const u8> payload);
static constexpr std::size_t PAGE_SIZE = 8;
static constexpr u32 PAGE_COUNT = 256;
// TODO: Maybe we're supposed to error for out-of-bound pages instead?
static constexpr u32 PAGE_INDEX_MASK = 0xff;
class ICCard
{
public:
using UID = std::array<u8, PAGE_SIZE>;
ICCard(std::string filename, const UID& uid);
// Load from file if it exists, else create fresh card data.
void Initialize();
const UID& GetUID() const { return m_uid; }
bool IsHalted() const { return m_current_state == State::Halted; }
// Returns an empty span on error.
std::span<const u8> ReadData(u16 page, u16 page_count);
bool WriteData(u16 page, std::span<const u8> write_span);
// Returns the new value.
u16 DecreaseUseCount(u16 page, u16 amount);
void SetHalted() { m_current_state = State::Halted; }
void SetIdle() { m_current_state = State::Idle; }
void DoState(PointerWrap& p);
private:
void FlushData(u32 byte_offset, u32 byte_count);
std::array<u8, PAGE_SIZE * PAGE_COUNT> m_data{};
const std::string m_filename;
const UID m_uid;
enum class State : u8
{
Idle = 0,
Halted = 1,
};
// I believe "Halted" cards don't respond in "AntiCollision".
State m_current_state = State::Idle;
};
// FYI: These are more like multiple RFID tags on a single card.
// Avalon makes use of this for not yet fully understood reasons.
std::vector<std::unique_ptr<ICCard>> m_ic_cards;
bool m_is_field_on = false;
const u8 m_slot_index;
u8 m_eject_timer{};
// Avalon spams InsertCheck when it's waiting for a card to be removed.
// We listen for that to automatically remove the card.
u8 m_card_present_insert_check_count{};
};
} // namespace Triforce
+226
View File
@@ -0,0 +1,226 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/Triforce/IOPorts.h"
#include <functional>
#include <fmt/ranges.h>
#include "Common/BitUtils.h"
#include "Common/ChunkFile.h"
#include "Core/HW/Triforce/ICCardReader.h"
namespace Triforce
{
void IOPorts::Update()
{
std::ranges::for_each(m_io_adapters, &IOAdapter::Update);
}
void IOPorts::DoState(PointerWrap& p)
{
p.Do(m_switch_input_data);
p.Do(m_generic_output_data);
}
void IOPorts::AddIOAdapter(std::unique_ptr<IOAdapter> adapter)
{
adapter->SetIOPorts(this);
m_io_adapters.emplace_back(std::move(adapter));
}
IOAdapter::IOAdapter() = default;
IOAdapter::~IOAdapter() = default;
void IOPorts::SetGenericOutputs(std::span<const u8> bytes)
{
const auto bytes_to_copy = std::min(bytes.size(), GENERIC_OUTPUT_BYTE_COUNT);
if (bytes.size() > m_generic_output_data.size())
{
WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: Unexpected byte count: {}",
bytes.size());
}
decltype(m_generic_output_data) bits_set{};
decltype(m_generic_output_data) bits_cleared{};
for (std::size_t i = 0; i != bytes_to_copy; ++i)
{
bits_set[i] = u8(~m_generic_output_data[i]) & bytes[i];
bits_cleared[i] = m_generic_output_data[i] & u8(~bytes[i]);
m_generic_output_data[i] = bytes[i];
}
bool bits_changed = false;
if (std::ranges::any_of(bits_set, std::bind_front(std::not_equal_to{}, 0x00)))
{
bits_changed = true;
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: bits_set: {:02x}",
fmt::join(bits_set, " "));
}
if (std::ranges::any_of(bits_cleared, std::bind_front(std::not_equal_to{}, 0x00)))
{
bits_changed = true;
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: bits_cleared: {:02x}",
fmt::join(bits_cleared, " "));
}
if (!bits_changed)
return;
for (auto& adapter : m_io_adapters)
{
adapter->HandleGenericOutputsChanged(bits_set, bits_cleared);
}
}
void IOAdapter::Update()
{
}
void IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared)
{
}
void MarioKartGPCommon_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared)
{
const u8 bits_changed_0 = bits_set[0] | bits_cleared[0];
if (bits_changed_0 & ITEM_LIGHT_BIT)
{
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Item Button: {}",
(bits_set[0] & ITEM_LIGHT_BIT) ? "ON" : "OFF");
}
if (bits_changed_0 & CANCEL_LIGHT_BIT)
{
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Cancel Button: {}",
(bits_set[0] & CANCEL_LIGHT_BIT) ? "ON" : "OFF");
}
}
void VirtuaStriker4Common_IOAdapter::Update()
{
const auto generic_outputs = GetIOPorts()->GetGenericOutputs();
const bool is_slot_a_locked = generic_outputs[0] & SLOT_A_LOCK_BIT;
const bool is_slot_b_locked = generic_outputs[0] & SLOT_B_LOCK_BIT;
// FYI: VS4 and VS4_2006 won't accept cards when both are presented at the same time.
// It expects to load and lock one before the second is inserted.
if (is_slot_a_locked)
{
// If slot 1 is locked, insert slot 2.
if (m_card_reader_b->IsReadyToInsertCard())
m_card_reader_b->InsertCard();
}
else
{
// If slot 1 is unlocked, remove slot 2 if unlocked.
if (m_card_reader_b->IsCardPresent() && !is_slot_b_locked)
m_card_reader_b->EjectCard();
}
if (m_card_reader_a->IsReadyToInsertCard())
m_card_reader_a->InsertCard();
const auto p1_inputs = GetIOPorts()->GetSwitchInputs(0);
const auto p2_inputs = GetIOPorts()->GetSwitchInputs(1);
// Bit 0x10 of each player's 1st byte is a card presence switch.
Common::SetBit(p1_inputs[0], 4, m_card_reader_a->IsCardPresent());
Common::SetBit(p2_inputs[0], 4, m_card_reader_b->IsCardPresent());
// Bit 0x20 of each player's 2nd byte is some kind of "eject" sensor.
Common::SetBit(p1_inputs[1], 5, m_card_reader_a->IsEjecting());
Common::SetBit(p2_inputs[1], 5, m_card_reader_b->IsEjecting());
}
void VirtuaStriker4Common_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared)
{
const u8 bits_changed_0 = bits_set[0] | bits_cleared[0];
if (bits_changed_0 & SLOT_A_LOCK_BIT)
{
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Slot 1: {}",
(bits_set[0] & SLOT_A_LOCK_BIT) ? "Locked" : "Unlocked");
}
if (bits_changed_0 & SLOT_B_LOCK_BIT)
{
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Slot 2: {}",
(bits_set[0] & SLOT_B_LOCK_BIT) ? "Locked" : "Unlocked");
}
}
void VirtuaStriker4_2006_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared)
{
(void)bits_cleared;
// Unlike VirtuaStriker4, which uses ICCardCommand::Eject,
// VirtuaStriker4_2006 triggers card ejection with JVS-IO.
if (bits_set[0] & SLOT_A_EJECT_BIT)
m_card_reader_a->EjectCard();
if (bits_set[0] & SLOT_B_EJECT_BIT)
m_card_reader_b->EjectCard();
}
void GekitouProYakyuu_IOAdapter::Update()
{
// Gekitou isn't as picky as VS4. We can insert both cards simultaneously.
for (const auto& card_reader : {m_card_reader_a, m_card_reader_b})
{
if (card_reader->IsReadyToInsertCard())
card_reader->InsertCard();
}
const auto p1_inputs = GetIOPorts()->GetSwitchInputs(0);
const auto p2_inputs = GetIOPorts()->GetSwitchInputs(1);
// Bit 0x40 of each player's 2nd byte is a card presence switch.
Common::SetBit(p1_inputs[1], 6, m_card_reader_a->IsCardPresent());
Common::SetBit(p2_inputs[1], 6, m_card_reader_b->IsCardPresent());
}
void GekitouProYakyuu_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared)
{
(void)bits_set;
// When a card is inserted Gekitou sets the relevant "lock" bit.
// When done with the card it clears this bit, then expects the card presence to be cleared.
// I guess we need to treat this as an alternative to ICCardCommand::Eject.
if (bits_cleared[0] & SLOT_A_LOCK_BIT)
m_card_reader_a->EjectCard();
if (bits_cleared[0] & SLOT_B_LOCK_BIT)
m_card_reader_b->EjectCard();
}
void KeyOfAvalon_IOAdapter::Update()
{
// Note that the game sometimes does a few InsertCheck when it still wants a card.
constexpr u8 insert_check_count_before_removing_card = 10;
if (m_card_reader->GetCardPresentInsertCheckCount() >= insert_check_count_before_removing_card)
m_card_reader->EjectCard();
if (m_card_reader->IsReadyToInsertCard())
m_card_reader->InsertCard();
}
} // namespace Triforce
+183
View File
@@ -0,0 +1,183 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <span>
#include <vector>
#include "Common/Assert.h"
#include "Common/CommonTypes.h"
class PointerWrap;
namespace Triforce
{
class ICCardReader;
class IOAdapter;
// Triforce GPIO peripherals connect to JVS-IO and eachother in game specific ways.
// This class hopes to handle those customizable connections.
// TODO: Use this for other JVS-IO (Analog Input, Coin, etc.).
class IOPorts
{
public:
void Update();
std::span<u8> GetSwitchInputs(u32 player_index)
{
ASSERT(player_index < PLAYER_COUNT);
return std::span{m_switch_input_data}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index,
SWITCH_INPUT_BYTES_PER_PLAYER);
}
std::span<const u8> GetSwitchInputs(u32 player_index) const
{
ASSERT(player_index < PLAYER_COUNT);
return std::span{m_switch_input_data}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index,
SWITCH_INPUT_BYTES_PER_PLAYER);
}
std::span<u8> GetGenericOutputs() { return m_generic_output_data; }
std::span<const u8> GetGenericOutputs() const { return m_generic_output_data; }
void SetGenericOutputs(std::span<const u8> bytes);
void AddIOAdapter(std::unique_ptr<IOAdapter> adapter);
void DoState(PointerWrap& p);
private:
std::vector<std::unique_ptr<IOAdapter>> m_io_adapters;
static constexpr std::size_t PLAYER_COUNT = 2;
static constexpr std::size_t SWITCH_INPUT_BYTES_PER_PLAYER = 2;
std::array<u8, PLAYER_COUNT * SWITCH_INPUT_BYTES_PER_PLAYER> m_switch_input_data{};
static constexpr std::size_t GENERIC_OUTPUT_BYTE_COUNT = 4;
std::array<u8, GENERIC_OUTPUT_BYTE_COUNT> m_generic_output_data{};
};
class IOAdapter
{
friend IOPorts;
public:
IOAdapter();
virtual ~IOAdapter();
IOAdapter(const IOAdapter&) = delete;
IOAdapter& operator=(const IOAdapter&) = delete;
IOAdapter(IOAdapter&&) = delete;
IOAdapter& operator=(IOAdapter&&) = delete;
virtual void Update();
protected:
IOPorts* GetIOPorts() { return m_io_ports; }
// Invoked with newly set and cleared bits.
virtual void HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared);
private:
void SetIOPorts(IOPorts* io_ports) { m_io_ports = io_ports; }
IOPorts* m_io_ports{};
};
// Used for both MarioKartGP and MarioKartGP2.
class MarioKartGPCommon_IOAdapter final : public IOAdapter
{
protected:
void HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared) override;
private:
static constexpr u8 ITEM_LIGHT_BIT = 0x04;
static constexpr u8 CANCEL_LIGHT_BIT = 0x08;
};
// Common functionality for both VirtuaStriker4 and VirtuaStriker4_2006.
class VirtuaStriker4Common_IOAdapter final : public IOAdapter
{
public:
VirtuaStriker4Common_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b)
: m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b}
{
}
void Update() override;
protected:
static constexpr u8 SLOT_A_LOCK_BIT = 0x40;
static constexpr u8 SLOT_B_LOCK_BIT = 0x10;
void HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared) override;
private:
ICCardReader* const m_card_reader_a;
ICCardReader* const m_card_reader_b;
};
class VirtuaStriker4_2006_IOAdapter final : public IOAdapter
{
public:
VirtuaStriker4_2006_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b)
: m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b}
{
}
protected:
void HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared) override;
private:
static constexpr u8 SLOT_A_EJECT_BIT = 0x80;
static constexpr u8 SLOT_B_EJECT_BIT = 0x20;
ICCardReader* const m_card_reader_a;
ICCardReader* const m_card_reader_b;
};
class GekitouProYakyuu_IOAdapter final : public IOAdapter
{
public:
GekitouProYakyuu_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b)
: m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b}
{
}
void Update() override;
protected:
void HandleGenericOutputsChanged(std::span<const u8> bits_set,
std::span<const u8> bits_cleared) override;
private:
static constexpr u8 SLOT_A_LOCK_BIT = 0x40;
static constexpr u8 SLOT_B_LOCK_BIT = 0x10;
ICCardReader* const m_card_reader_a;
ICCardReader* const m_card_reader_b;
};
class KeyOfAvalon_IOAdapter final : public IOAdapter
{
public:
explicit KeyOfAvalon_IOAdapter(ICCardReader* card_reader) : m_card_reader{card_reader} {}
void Update() override;
private:
ICCardReader* const m_card_reader;
};
} // namespace Triforce
+6
View File
@@ -341,6 +341,9 @@
<ClInclude Include="Core\HW\Sram.h" />
<ClInclude Include="Core\HW\StreamADPCM.h" />
<ClInclude Include="Core\HW\SystemTimers.h" />
<ClInclude Include="Core\HW\Triforce\DeckReader.h" />
<ClInclude Include="Core\HW\Triforce\ICCardReader.h" />
<ClInclude Include="Core\HW\Triforce\IOPorts.h" />
<ClInclude Include="Core\HW\Triforce\SerialDevice.h" />
<ClInclude Include="Core\HW\Triforce\Touchscreen.h" />
<ClInclude Include="Core\HW\VideoInterface.h" />
@@ -1043,6 +1046,9 @@
<ClCompile Include="Core\HW\Sram.cpp" />
<ClCompile Include="Core\HW\StreamADPCM.cpp" />
<ClCompile Include="Core\HW\SystemTimers.cpp" />
<ClCompile Include="Core\HW\Triforce\DeckReader.cpp" />
<ClCompile Include="Core\HW\Triforce\ICCardReader.cpp" />
<ClCompile Include="Core\HW\Triforce\IOPorts.cpp" />
<ClCompile Include="Core\HW\Triforce\SerialDevice.cpp" />
<ClCompile Include="Core\HW\Triforce\Touchscreen.cpp" />
<ClCompile Include="Core\HW\VideoInterface.cpp" />