diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 821080afca..eb11771fa0 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -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 diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp index 2bb6713c5d..2bd7455b8a 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp @@ -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()); m_serial_device_b = std::make_unique(&m_mag_card_settings); break; + case VirtuaStriker4: + { + auto slot_a = std::make_unique(0); + auto slot_b = std::make_unique(1); + m_io_ports.AddIOAdapter( + std::make_unique(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(0); + auto slot_b = std::make_unique(1); + m_io_ports.AddIOAdapter( + std::make_unique(slot_a.get(), slot_b.get())); + m_io_ports.AddIOAdapter( + std::make_unique(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(0); + auto slot_b = std::make_unique(1); + m_io_ports.AddIOAdapter( + std::make_unique(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(); + m_io_ports.AddIOAdapter( + std::make_unique(deck_reader->GetICCardReader())); + m_serial_device_a = std::move(deck_reader); m_serial_device_b = std::make_unique(); 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(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(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(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 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) diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h index 85a321b424..4d1990297c 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h +++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h @@ -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, 2> m_response_buffers{}; u8 m_current_response_buffer_index = 0; + Triforce::IOPorts m_io_ports; + std::array m_coin{}; std::array 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 m_serial_device_a; + // Serial B std::unique_ptr 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 diff --git a/Source/Core/Core/HW/Triforce/DeckReader.cpp b/Source/Core/Core/HW/Triforce/DeckReader.cpp new file mode 100644 index 0000000000..a475b63bea --- /dev/null +++ b/Source/Core/Core/HW/Triforce/DeckReader.cpp @@ -0,0 +1,449 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/Triforce/DeckReader.h" + +#include + +#include + +#include + +#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 card_index; +}; +#pragma pack(pop) + +std::optional> 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()) + { + 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()) + { + ERROR_LOG_FMT(SERIALINTERFACE_CARD, "LoadCardDeckFromFile: Invalid \"cards\" array."); + return std::nullopt; + } + + std::optional> result; + result.emplace(); + + for (const auto& item : cards_obj.get()) + { + if (!item.is()) + continue; + + const auto& item_obj = item.get(); + + const auto card_index = ReadNumericFromJson(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(item_obj, "table").value_or(0); + + const auto quantity = ReadNumericFromJson(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{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{}); + + break; + } + case CDReaderCommand::BootVersion: + { + INFO_LOG_FMT(SERIALINTERFACE_CARD, "BootVersion"); + + WriteTxBytes(Common::AsU8Span(CDR_BOOT_VERSION)); + + // Pad with zeros. + WriteTxBytes(std::array{}); + + break; + } + case CDReaderCommand::ProgramChecksum: + { + INFO_LOG_FMT(SERIALINTERFACE_CARD, "ProgramChecksum"); + + // Avalon seems to not care about the actual value. + Common::BigEndianValue 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 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 response{}; + WriteTxBytes(response); + + break; + } + case CDReaderCommand::ShutterGet: + { + INFO_LOG_FMT(SERIALINTERFACE_CARD, "ShutterGet"); + + Common::BigEndianValue shutter_time{SHUTTER_TIME}; + WriteTxBytes(Common::AsU8Span(shutter_time)); + + break; + } + case CDReaderCommand::ShutterAuto: + { + INFO_LOG_FMT(SERIALINTERFACE_CARD, "ShutterAuto"); + + Common::BigEndianValue 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{0xaa, 0x52}); + } + else + { + // We've had one, yes, but what about second header ? + WriteTxBytes(std::array{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 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 diff --git a/Source/Core/Core/HW/Triforce/DeckReader.h b/Source/Core/Core/HW/Triforce/DeckReader.h new file mode 100644 index 0000000000..1e1fb23dad --- /dev/null +++ b/Source/Core/Core/HW/Triforce/DeckReader.h @@ -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 diff --git a/Source/Core/Core/HW/Triforce/ICCardReader.cpp b/Source/Core/Core/HW/Triforce/ICCardReader.cpp new file mode 100644 index 0000000000..b438992fb4 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/ICCardReader.cpp @@ -0,0 +1,774 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/Triforce/ICCardReader.h" + +#include + +#include + +#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 data) +{ + return std::accumulate(data.data(), data.data() + data.size(), u8{}, std::bit_xor{}); +} + +bool LoadCardData(const std::string& filename, std::span 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(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 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 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(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 small_reply_payload{}; + + // Will be later assigned to `small_reply_payload` or some region of the card data itself. + std::span 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::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 payload) +{ + struct ICCardReplyHeader + { + u8 fixed = 0x10; // Games seem to expect 0x10. + u8 command; + Common::BigEndianValue length; // Includes status and payload bytes. + Common::BigEndianValue 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 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 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(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 diff --git a/Source/Core/Core/HW/Triforce/ICCardReader.h b/Source/Core/Core/HW/Triforce/ICCardReader.h new file mode 100644 index 0000000000..7f4911c85d --- /dev/null +++ b/Source/Core/Core/HW/Triforce/ICCardReader.h @@ -0,0 +1,110 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#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 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; + + 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 ReadData(u16 page, u16 page_count); + + bool WriteData(u16 page, std::span 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 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> 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 diff --git a/Source/Core/Core/HW/Triforce/IOPorts.cpp b/Source/Core/Core/HW/Triforce/IOPorts.cpp new file mode 100644 index 0000000000..2a600f94cd --- /dev/null +++ b/Source/Core/Core/HW/Triforce/IOPorts.cpp @@ -0,0 +1,226 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/Triforce/IOPorts.h" + +#include + +#include + +#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 adapter) +{ + adapter->SetIOPorts(this); + m_io_adapters.emplace_back(std::move(adapter)); +} + +IOAdapter::IOAdapter() = default; +IOAdapter::~IOAdapter() = default; + +void IOPorts::SetGenericOutputs(std::span 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 bits_set, + std::span bits_cleared) +{ +} + +void MarioKartGPCommon_IOAdapter::HandleGenericOutputsChanged(std::span bits_set, + std::span 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 bits_set, + std::span 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 bits_set, + std::span 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 bits_set, + std::span 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 diff --git a/Source/Core/Core/HW/Triforce/IOPorts.h b/Source/Core/Core/HW/Triforce/IOPorts.h new file mode 100644 index 0000000000..a5379c3a50 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/IOPorts.h @@ -0,0 +1,183 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#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 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 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 GetGenericOutputs() { return m_generic_output_data; } + std::span GetGenericOutputs() const { return m_generic_output_data; } + + void SetGenericOutputs(std::span bytes); + + void AddIOAdapter(std::unique_ptr adapter); + + void DoState(PointerWrap& p); + +private: + std::vector> m_io_adapters; + + static constexpr std::size_t PLAYER_COUNT = 2; + static constexpr std::size_t SWITCH_INPUT_BYTES_PER_PLAYER = 2; + + std::array m_switch_input_data{}; + + static constexpr std::size_t GENERIC_OUTPUT_BYTE_COUNT = 4; + + std::array 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 bits_set, + std::span 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 bits_set, + std::span 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 bits_set, + std::span 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 bits_set, + std::span 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 bits_set, + std::span 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 diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 7d63c5a4b9..1713c0f88e 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -341,6 +341,9 @@ + + + @@ -1043,6 +1046,9 @@ + + +