diff options
| author | Jordan Woyak <jordan.woyak@gmail.com> | 2026-02-10 01:56:00 -0600 |
|---|---|---|
| committer | Jordan Woyak <jordan.woyak@gmail.com> | 2026-02-15 20:14:15 -0600 |
| commit | f657b3252149c6e1854aa82a5378a9d3a97f3978 (patch) | |
| tree | dfd7027a57037e301187816ffb8db8403edca793 | |
| parent | d893565b781a0a033ae2ff12962df436de695343 (diff) | |
HW: Rewrite MagCard features of SI_DeviceAMBaseboard in new MagneticCardReader class.
A huge thank you goes to GXTX and https://github.com/GXTX/YACardEmu which this code is based on.
| -rw-r--r-- | Externals/licenses.md | 2 | ||||
| -rw-r--r-- | Source/Core/Core/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | Source/Core/Core/HW/MagCard/C1231BR.cpp | 82 | ||||
| -rw-r--r-- | Source/Core/Core/HW/MagCard/C1231BR.h | 37 | ||||
| -rw-r--r-- | Source/Core/Core/HW/MagCard/C1231LR.cpp | 27 | ||||
| -rw-r--r-- | Source/Core/Core/HW/MagCard/C1231LR.h | 26 | ||||
| -rw-r--r-- | Source/Core/Core/HW/MagCard/MagneticCardReader.cpp | 1002 | ||||
| -rw-r--r-- | Source/Core/Core/HW/MagCard/MagneticCardReader.h | 207 | ||||
| -rw-r--r-- | Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp | 569 | ||||
| -rw-r--r-- | Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h | 27 | ||||
| -rw-r--r-- | Source/Core/DolphinLib.props | 6 |
11 files changed, 1462 insertions, 529 deletions
diff --git a/Externals/licenses.md b/Externals/licenses.md index 727a41e2a1..7aed5313ac 100644 --- a/Externals/licenses.md +++ b/Externals/licenses.md @@ -76,6 +76,8 @@ Dolphin includes or links code of the following third-party software projects: [MIT](https://github.com/microsoft/wil/blob/master/LICENSE) - [xxHash](https://github.com/Cyan4973/xxHash): [BSD 2-Clause](https://github.com/Cyan4973/xxHash/blob/master/LICENSE) +- [YACardEmu](https://github.com/GXTX/YACardEmu) + [GPLv2+](https://github.com/GXTX/YACardEmu/blob/master/license.txt) - [zlib-ng](https://github.com/zlib-ng/zlib-ng): [zlib license](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md) - [Zstandard](https://facebook.github.io/zstd/): diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index e0cad85863..e2ae3e9919 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -254,6 +254,12 @@ add_library(core HW/HSP/HSP_DeviceNull.h HW/HW.cpp HW/HW.h + HW/MagCard/C1231BR.cpp + HW/MagCard/C1231BR.h + HW/MagCard/C1231LR.cpp + HW/MagCard/C1231LR.h + HW/MagCard/MagneticCardReader.cpp + HW/MagCard/MagneticCardReader.h HW/Memmap.cpp HW/Memmap.h HW/MemoryInterface.cpp diff --git a/Source/Core/Core/HW/MagCard/C1231BR.cpp b/Source/Core/Core/HW/MagCard/C1231BR.cpp new file mode 100644 index 0000000000..de76df1839 --- /dev/null +++ b/Source/Core/Core/HW/MagCard/C1231BR.cpp @@ -0,0 +1,82 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on: https://github.com/GXTX/YACardEmu +// Copyright (C) 2020-2023 wutno (https://github.com/GXTX) +// Copyright (C) 2022-2023 tugpoat (https://github.com/tugpoat) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/MagCard/C1231BR.h" + +namespace MagCard +{ + +void C1231BR::Command_10_Initialize() +{ + // TODO: Not hardware tested. + if (!m_current_packet.empty()) + { + if (InitMode(m_current_packet[0]) == InitMode::EjectAfter) + { + m_is_shutter_open = true; + MoveCard(R::ENTRY_SLOT); + } + } + + // m_status.SoftReset(); // TODO: Needed ? + FinishCommand(); +} + +void C1231BR::Command_D0_ControlShutter() +{ + enum class Action : u8 + { + Close = 0x30, + Open = 0x31, + }; + + if (m_current_packet.empty()) + { + SetPError(P::SYSTEM_ERR); + FinishCommand(); + return; + } + + m_is_shutter_open = Action(m_current_packet[0]) == Action::Open; + + INFO_LOG_FMT(SERIALINTERFACE_CARD, "Shutter: {}", m_is_shutter_open ? "Open" : "Close"); + + FinishCommand(); +} + +u8 C1231BR::GetPositionValue() +{ + // True "R" (Card Position) values. + // FYI: I see a 0xd0 command transition from: 11000 -> 01110 -> 00111 + // Are these 5 bits individual sensors placed within the machine ? + static constexpr std::array<u8, std::size_t(R::MAX_POSITIONS)> POSITION_VALUES{ + 0b00000, // NO_CARD + 0b11000, // READ_WRITE_HEAD + 0b00111, // THERMAL_HEAD + 0b11100, // DISPENSER_THERMAL + 0b00001, // ENTRY_SLOT + }; + + return (m_is_shutter_open ? 0x80u : 0x40u) | + (m_card_settings->report_dispenser_empty ? 0x00u : 0x20u) | + POSITION_VALUES[u8(m_status.r)]; +} + +void C1231BR::DoState(PointerWrap& p) +{ + MagneticCardReader::DoState(p); + + p.Do(m_is_shutter_open); +} + +bool C1231BR::IsReadyForCard() +{ + return !IsCardPresent() && m_is_shutter_open; +} + +} // namespace MagCard diff --git a/Source/Core/Core/HW/MagCard/C1231BR.h b/Source/Core/Core/HW/MagCard/C1231BR.h new file mode 100644 index 0000000000..d339b2d5bb --- /dev/null +++ b/Source/Core/Core/HW/MagCard/C1231BR.h @@ -0,0 +1,37 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on: https://github.com/GXTX/YACardEmu +// Copyright (C) 2020-2023 wutno (https://github.com/GXTX) +// Copyright (C) 2022-2023 tugpoat (https://github.com/tugpoat) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Core/HW/MagCard/MagneticCardReader.h" + +namespace MagCard +{ + +// CRP-1231BR-10 +// Used by F-Zero AX +class C1231BR final : public MagneticCardReader +{ +public: + using MagneticCardReader::MagneticCardReader; + +protected: + // Specific to this model. + bool m_is_shutter_open = true; + + bool IsReadyForCard() override; + + u8 GetPositionValue() override; + + void Command_10_Initialize() override; + void Command_D0_ControlShutter() override; + + void DoState(PointerWrap& p) override; +}; + +} // namespace MagCard diff --git a/Source/Core/Core/HW/MagCard/C1231LR.cpp b/Source/Core/Core/HW/MagCard/C1231LR.cpp new file mode 100644 index 0000000000..0e9c32fa15 --- /dev/null +++ b/Source/Core/Core/HW/MagCard/C1231LR.cpp @@ -0,0 +1,27 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on: https://github.com/GXTX/YACardEmu +// Copyright (C) 2020-2023 wutno (https://github.com/GXTX) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/MagCard/C1231LR.h" + +namespace MagCard +{ + +u8 C1231LR::GetPositionValue() +{ + // True "R" (Card Position) values. + static constexpr std::array<u8, std::size_t(R::MAX_POSITIONS)> POSITION_VALUES{ + 0x30, // NO_CARD + 0x31, // READ_WRITE_HEAD + 0x32, // THERMAL_HEAD + 0x33, // DISPENSER_THERMAL + 0x34, // ENTRY_SLOT + }; + + return POSITION_VALUES[u8(m_status.r)]; +} + +} // namespace MagCard diff --git a/Source/Core/Core/HW/MagCard/C1231LR.h b/Source/Core/Core/HW/MagCard/C1231LR.h new file mode 100644 index 0000000000..e7ee627e1b --- /dev/null +++ b/Source/Core/Core/HW/MagCard/C1231LR.h @@ -0,0 +1,26 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on: https://github.com/GXTX/YACardEmu +// Copyright (C) 2020-2023 wutno (https://github.com/GXTX) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Core/HW/MagCard/MagneticCardReader.h" + +namespace MagCard +{ + +// CRP-1231LR-10NAB +// Used by Mario Kart Arcade GP + GP2 +class C1231LR final : public MagneticCardReader +{ +public: + using MagneticCardReader::MagneticCardReader; + +protected: + u8 GetPositionValue() override; +}; + +} // namespace MagCard diff --git a/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp b/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp new file mode 100644 index 0000000000..e84ef48a98 --- /dev/null +++ b/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp @@ -0,0 +1,1002 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on: https://github.com/GXTX/YACardEmu +// Copyright (C) 2020-2023 wutno (https://github.com/GXTX) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/MagCard/MagneticCardReader.h" + +#include <algorithm> +#include <numeric> + +#include "Common/BitUtils.h" +#include "Common/DirectIOFile.h" +#include "Common/FileUtil.h" +#include "Common/Lazy.h" +#include "Common/Logging/Log.h" +#include "Common/SmallVector.h" +#include "Common/StringUtil.h" +#include "Core/Core.h" + +namespace +{ + +constexpr std::string GetHexDump(const auto& data) +{ + const auto u8_span = Common::AsU8Span(data); + return HexDump(u8_span.data(), u8_span.size()); +} + +void AppendRange(auto* container, const auto& range) +{ + const auto prev_size = container->size(); + container->resize(prev_size + range.size()); + std::ranges::copy(range, container->data() + prev_size); +} + +} // namespace + +namespace MagCard +{ + +constexpr std::string_view VERSION_STRING = "AP:S1234-5678,OS:S9012-3456,0000"; + +constexpr u8 START_OF_TEXT = 0x02; +constexpr u8 END_OF_TEXT = 0x03; +constexpr u8 ENQUIRY = 0x05; +constexpr u8 ACK = 0x06; +constexpr u8 NACK = 0x15; + +enum class BitMode : u8 +{ + SevenBitParity = 0x30, + EightBitNoParity = 0x31, +}; + +enum class Track : u8 +{ + Track_1 = 0x30, + Track_2 = 0x31, + Track_3 = 0x32, + Track_1_And_2 = 0x33, + Track_1_And_3 = 0x34, + Track_2_And_3 = 0x35, + Track_1_2_And_3 = 0x36, +}; + +static auto GetTrackIndicesForTrackType(Track track) +{ + Common::SmallVector<std::size_t, MagneticCardReader::NUM_TRACKS> result; + + switch (track) + { + case Track::Track_1: + case Track::Track_2: + case Track::Track_3: + result.emplace_back(std::size_t(track) - 0x30); + break; + + case Track::Track_1_And_2: + case Track::Track_1_And_3: + case Track::Track_2_And_3: + result.emplace_back((track == Track::Track_2_And_3) ? 1 : 0); + result.emplace_back((track == Track::Track_1_And_2) ? 1 : 2); + break; + + case Track::Track_1_2_And_3: + result.emplace_back(0); + result.emplace_back(1); + result.emplace_back(2); + break; + + default: + break; + } + + return result; +} + +MagneticCardReader::MagneticCardReader(MagneticCardReader::Settings* settings) + : m_card_settings{settings} +{ +} + +MagneticCardReader::~MagneticCardReader() +{ + if (IsCardLoaded()) + { + // Games do seem to write the entire save data in a single command. + // But the printed messages are another story. + // I suppose there's potential for data loss here. + WARN_LOG_FMT(SERIALINTERFACE_CARD, "Shutdown before ejecting card!"); + } +} + +void MagneticCardReader::Command_10_Initialize() +{ + EjectCard(); + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::Command_20_ReadStatus() +{ + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::Command_33_ReadData() +{ + enum class Mode : u8 + { + Standard = 0x30, // read 69-bytes + ReadVariable = 0x31, // variable length read + CardCapture = 0x32, // pull in card + }; + + if (m_current_packet.size() < 3) + { + SetPError(P::SYSTEM_ERR); + return; + } + + const auto mode = Mode(m_current_packet[0]); + // const auto bit_mode = BitMode(m_current_packet[1]); + const auto track = Track(m_current_packet[2]); + + switch (m_current_step) + { + case 1: + if (mode == Mode::CardCapture) + INFO_LOG_FMT(SERIALINTERFACE_CARD, "Command_33_ReadData: CardCapture"); + break; + case 2: + { + // Doesn't return card data. + if (mode == Mode::CardCapture) + { + if (!IsCardPresent()) + { + m_status.s = S::WAITING_FOR_CARD; + --m_current_step; + break; + } + + // How does hardware behave when the card is already elsewhere in the machine ? + if (m_status.r != R::READ_WRITE_HEAD) + { + NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Capturing Card."); + + MoveCard(R::READ_WRITE_HEAD); + ReadCardFile(); + + Core::DisplayMessage("Inserted Magnetic Card", 4000); + } + break; + } + + DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Command_33_ReadData: mode {:02x} track {:02x}", u8(mode), + u8(track)); + + const auto track_indices = GetTrackIndicesForTrackType(track); + + if (!IsCardLoaded() || track_indices.size() == 0) + { + SetPError(P::ILLEGAL_ERR); + return; + } + + for (auto track_index : track_indices) + { + auto& track_data = m_card_data[track_index]; + if (!track_data.has_value()) + { + SetPError(P::READ_ERR); + WARN_LOG_FMT(SERIALINTERFACE_CARD, "Command_33_ReadData: Empty track {:02x}", track_index); + // TODO: Are we supposed to return partial data ? + return; + } + + AppendRange(&m_command_payload, *track_data); + } + } + default: + break; + } + + if (m_current_step >= 2) + FinishCommand(); +} + +void MagneticCardReader::Command_35_GetData() +{ + switch (m_current_step) + { + case 2: + if (!IsCardLoaded()) + { + // TODO: Is this correct ? + SetPError(P::ILLEGAL_ERR); + return; + } + + for (auto& track_data : m_card_data) + { + if (!track_data.has_value()) + { + SetPError(P::READ_ERR); + // TODO: Are we supposed to return partial data ? + return; + } + + AppendRange(&m_command_payload, *track_data); + } + break; + default: + break; + } + + if (m_current_step >= 2) + FinishCommand(); +} + +void MagneticCardReader::Command_40_Cancel() +{ + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::Command_53_WriteData() +{ + enum class Mode : u8 + { + Standard = 0x30, // 69-bytes + }; + + if (m_current_packet.size() < 3) + { + SetPError(P::SYSTEM_ERR); + return; + } + + if (m_current_step < 2) + return; + + const auto mode = Mode(m_current_packet[0]); + // const auto bit_mode = BitMode(m_current_packet[1]); + auto track = Track(m_current_packet[2]); + + const auto track_indices = GetTrackIndicesForTrackType(track); + const auto proper_payload_size = track_indices.size() * TRACK_SIZE; + const auto payload = std::span{m_current_packet}.subspan(3); + + if (mode != Mode::Standard || !IsCardLoaded() || track_indices.size() == 0 || + payload.size() != proper_payload_size) + { + WARN_LOG_FMT(SERIALINTERFACE_CARD, "Command_53_WriteData: Failed."); + SetPError(P::ILLEGAL_ERR); + return; + } + + INFO_LOG_FMT(SERIALINTERFACE_CARD, "Command_53_WriteData: Writing {} track(s) to memory.", + track_indices.size()); + + for (auto track_index : track_indices) + { + auto& track_data = m_card_data[track_index]; + track_data.emplace(); + std::ranges::copy(payload.subspan(track_index * TRACK_SIZE, TRACK_SIZE), track_data->data()); + } + + Core::DisplayMessage("Writing Magnetic Card", 4000); + + WriteCardFile(); + + FinishCommand(); +} + +void MagneticCardReader::Command_78_PrintSettings() +{ + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::Command_7A_RegisterFont() +{ + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::Command_7B_PrintImage() +{ + // TODO: Are these print functions supposed to succeed before the card is actually grabbed ? + if (!IsCardPresent()) + { + SetPError(P::PRINT_ERR); + return; + } + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::Command_7C_PrintLine() +{ + if (m_current_packet.size() < 3) + { + SetPError(P::SYSTEM_ERR); + return; + } + + switch (m_current_step) + { + case 1: + { + if (!IsCardPresent()) + { + SetPError(P::PRINT_ERR); + return; + } + + // FIXME: Should we only move the head when we're actually about to print? + MoveCard(R::THERMAL_HEAD); + break; + } + case 2: + MoveCard(R::READ_WRITE_HEAD); + break; + default: + break; + } + + if (m_current_step >= 2) + FinishCommand(); +} + +void MagneticCardReader::Command_7D_Erase() +{ + switch (m_current_step) + { + case 1: + if (!IsCardPresent()) + { + SetPError(P::PRINT_ERR); + return; + } + MoveCard(R::THERMAL_HEAD); + break; + case 2: + MoveCard(R::READ_WRITE_HEAD); + break; + default: + break; + } + + if (m_current_step >= 2) + FinishCommand(); +} + +void MagneticCardReader::Command_7E_PrintBarcode() +{ + switch (m_current_step) + { + case 2: + if (!IsCardPresent()) + { + SetPError(P::ILLEGAL_ERR); + return; + } + break; + default: + break; + } + + if (m_current_step >= 2) + FinishCommand(); +} + +void MagneticCardReader::Command_80_EjectCard() +{ + if (m_current_step >= 3) + { + EjectCard(); + FinishCommand(); + } +} + +void MagneticCardReader::Command_A0_Clean() +{ + switch (m_current_step) + { + case 2: + if (!IsCardPresent()) + { + m_status.s = S::WAITING_FOR_CARD; + --m_current_step; + break; + } + break; + case 3: + MoveCard(R::THERMAL_HEAD); + break; + case 4: + EjectCard(); + break; + default: + break; + } + + if (m_current_step >= 4) + FinishCommand(); +} + +void MagneticCardReader::Command_B0_DispenseCard() +{ + enum class Mode : u8 + { + Dispense = 0x31, + CheckOnly = 0x32, + }; + + // MarioKart GP1 issues this command without options. + auto mode = Mode::Dispense; + if (!m_current_packet.empty()) + { + mode = Mode(m_current_packet[0]); + } + + switch (m_current_step) + { + case 2: + if (mode == Mode::CheckOnly) + { + if (m_card_settings->report_dispenser_empty) + { + m_status.s = S::DISPENSER_EMPTY; + } + else + { + m_status.s = S::CARD_FULL; + } + } + else + { + DispenseCard(); + } + break; + case 3: + if (m_status.r == R::DISPENSER_THERMAL) + { + MoveCard(R::READ_WRITE_HEAD); + } + break; + default: + break; + } + + if (m_current_step >= 3) + FinishCommand(); +} + +void MagneticCardReader::Command_C0_ControlLED() +{ + // We don't need to handle this properly but let's leave some notes. + enum class Mode : u8 + { + Off = 0x30, + On = 0x31, + SlowBlink = 0x32, + FastBlink = 0x33, + }; + + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::Command_C1_SetPrintRetry() +{ + // We don't need to handle this properly but let's leave some notes + // m_current_packet[0] == 0x31 NONE ~ 0x39 MAX8 + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::Command_D0_ControlShutter() +{ + // Only BR model supports this command. + SetSError(S::ILLEGAL_COMMAND); +} + +void MagneticCardReader::Command_F0_GetVersion() +{ + AppendRange(&m_command_payload, VERSION_STRING); + + m_status.SoftReset(); + FinishCommand(); +} + +void MagneticCardReader::ClearCardInMemory() +{ + m_card_data.fill(std::nullopt); +} + +void MagneticCardReader::ReadCardFile() +{ + NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Reading card data from: {}", m_card_settings->card_name); + + ClearCardInMemory(); + + const std::string full_path = m_card_settings->card_path + m_card_settings->card_name; + + File::DirectIOFile file{full_path, File::AccessMode::Read}; + + const auto file_size = file.GetSize(); + const auto complete_track_count = file_size / TRACK_SIZE; + const auto excess_byte_count = file_size % TRACK_SIZE; + + if (file_size == 0 || excess_byte_count != 0 || complete_track_count > NUM_TRACKS) + { + ERROR_LOG_FMT(SERIALINTERFACE_CARD, "ReadCardFile: Incorrect card file size: {}", file_size); + } + + const auto tracks_to_read = std::min<u32>(complete_track_count, NUM_TRACKS); + for (u32 track_num = 0; track_num != tracks_to_read; ++track_num) + { + auto& track_data = m_card_data[track_num]; + track_data.emplace(); + + if (!file.Read(*track_data)) + { + ERROR_LOG_FMT(SERIALINTERFACE_CARD, "File read failed."); + track_data.reset(); + break; + } + } + + DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "ReadCard: 0:{} 1:{} 2:{}", GetHexDump(m_card_data[0]), + GetHexDump(m_card_data[1]), GetHexDump(m_card_data[2])); +} + +void MagneticCardReader::WriteCardFile() +{ + // Note: Creating empty upsets games when reinserting them later. + // Games expect to dispense their own blank cards, not have such cards inserted. + // We only write files when the game itself writes, so this won't happen. + + NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Writing card data to: {}", m_card_settings->card_name); + DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "WriteCard: 0:{} 1:{} 2:{}", GetHexDump(m_card_data[0]), + GetHexDump(m_card_data[1]), GetHexDump(m_card_data[2])); + + auto full_path = m_card_settings->card_path + m_card_settings->card_name; + + File::DirectIOFile file{full_path, File::AccessMode::Write}; + + for (auto& track_data : m_card_data) + { + if (track_data.has_value() && !file.Write(*track_data)) + { + ERROR_LOG_FMT(SERIALINTERFACE_CARD, "File write failed."); + break; + } + } +} + +void MagneticCardReader::SetPError(P error_code) +{ + WARN_LOG_FMT(SERIALINTERFACE_CARD, "SetPError: {:02x}", u8(error_code)); + m_status.p = error_code; + FinishCommand(); +} + +void MagneticCardReader::SetSError(S error_code) +{ + WARN_LOG_FMT(SERIALINTERFACE_CARD, "SetSError: {:02x}", u8(error_code)); + m_status.s = error_code; + FinishCommand(); +} + +void MagneticCardReader::Process(std::vector<u8>* read, std::vector<u8>* write) +{ + while (!read->empty()) + { + const u8 first_byte = read->front(); + if (first_byte == ENQUIRY) + { + // ENQUIRY + // The host sends this regularly. + // This triggers sending of the latest command buffer. + // We also step the state machine here. + + DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Process: ENQ"); + + // TODO: Get time from Core rather than assuming. + const auto elapsed_time = std::chrono::milliseconds{50}; + + StepStateMachine(elapsed_time); + StepStatePerson(elapsed_time); + + BuildPacket(*write); + + read->erase(read->begin()); // TODO: SLOW ! + continue; + } + + if (first_byte != START_OF_TEXT) + { + ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Process: Unexpected {:02x}", first_byte); + read->erase(read->begin()); // TODO: SLOW ! + continue; + } + + // START_OF_TEXT + // This is a command packet. The next byte provides the size. + // Upon read they ACK or NACK and start processing of a command. + + if (read->size() < 2) + break; // Wait for more data. + + const std::size_t packet_size = (*read)[1]; + if (packet_size > read->size() - 2) + break; // Wait for more data. + + if (ReceivePacket(std::span{*read}.subspan(2, packet_size))) + { + DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Writing ACK"); + write->emplace_back(ACK); + } + else + { + WARN_LOG_FMT(SERIALINTERFACE_CARD, "Writing NACK"); + write->emplace_back(NACK); + } + + read->erase(read->begin(), read->begin() + packet_size + 2); // TODO: SLOW ! + } +} + +bool MagneticCardReader::ReceivePacket(std::span<const u8> packet) +{ + DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: {}", GetHexDump(packet)); + + if (packet.size() < 6) + { + WARN_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: Bad packet size!"); + return false; + } + + if (packet[packet.size() - 2] != END_OF_TEXT) + { + WARN_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: Missing END_OF_TEXT!"); + return false; + } + + // Checksum is XOR of the previous bytes. + // This includes the count byte that we already chopped off. + const u8 read_checksum = packet.back(); + + // TODO C++23: + // std::ranges::fold_left(packet.first(packet.size() - 1), u8(packet.size()), std::bit_xor{}); + const auto range_to_checksum = packet.first(packet.size() - 1); + const auto proper_checksum = std::accumulate(range_to_checksum.begin(), range_to_checksum.end(), + u8(packet.size()), std::bit_xor{}); + + // Verify checksum. Skip packet if invalid. + if (read_checksum != proper_checksum) + { + WARN_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: Bad checksum!"); + return false; + } + + const u8 new_command = packet[0]; + + if (IsRunningCommand()) + { + if (new_command == 0x40) // Cancel. + { + INFO_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: Cancelling command: {:02x}", + m_current_command); + } + else + { + // What does the real hardware do here ? + WARN_LOG_FMT(SERIALINTERFACE_CARD, + "ReceivePacket: Running command: {:02x} interrupted by command: {:02x}", + m_current_command, new_command); + } + } + + m_current_command = new_command; + + // Copy the payload. + // We're skipping the command number and the host's 3 status bytes, + // and dropping the END_OF_TEXT and checksum. + m_current_packet.resize(packet.size() - 6); + std::ranges::copy(packet.subspan(4, m_current_packet.size()), m_current_packet.data()); + + // TODO: Is this sensible for every command ? + m_status.SoftReset(); + + // Start the new command. + m_status.s = S::RUNNING_COMMAND; + m_current_step = 1; + m_command_payload.clear(); + + return true; +} + +void MagneticCardReader::StepStatePerson(DT elapsed_time) +{ + m_time_since_person_moved_card += elapsed_time; + + // F-Zero AX: Polls for a card with command 0x20 ReadStatus(). + // It demands a card is removed from "ENTRY_SLOT" during boot. + // It pauses and spams ReadStatus() until that happens. + // When wanted, it loads a card detected at the "ENTRY_SLOT" position. + // There's not a perfectly reliably way to detect when it wants a card. + // + // MarioKart GP: Polls for wanted cards with command 0x33 ReadData(Mode:CardCapture). + // It does this regardless of the card being detected in the "ENTRY_SLOT" position. + // This makes it easy to detect when cards are wanted. + // It pauses until cards are pulled from "ENTRY_SLOT" on game over. + + // This logic tries to emulate a person following those rules. + + // People can't interact with cards inside the machine. + if (IsCardLoaded()) + return; + + Common::Lazy<bool> card_exists{[&] { + const auto full_path = m_card_settings->card_path + m_card_settings->card_name; + return File::Exists(full_path); + }}; + + if (!IsCardPresent()) + { + if (m_card_settings->insert_card_when_possible && IsReadyForCard() && + m_time_since_person_moved_card > std::chrono::milliseconds{500} && *card_exists) + { + // Place the card in the entry slot after a delay. + NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Inserting card."); + m_status.r = R::ENTRY_SLOT; + m_time_since_person_moved_card = {}; + } + + return; + } + + const bool machine_moved_card_last = + m_time_since_machine_moved_card < m_time_since_person_moved_card; + if ((machine_moved_card_last && + m_time_since_machine_moved_card > std::chrono::milliseconds{100}) || + !*card_exists) + { + // Take the card from the machine when ejected or if the file is deleted. + NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Removing card."); + m_status.r = R::NO_CARD; + m_time_since_person_moved_card = {}; + } +} + +void MagneticCardReader::StepStateMachine(DT elapsed_time) +{ + m_time_since_machine_moved_card += elapsed_time; + + if (!IsRunningCommand()) + return; + + DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "StepStateMachine command: {:02x}", GetCurrentCommand()); + + switch (GetCurrentCommand()) + { + case 0x10: + Command_10_Initialize(); + break; + case 0x20: + Command_20_ReadStatus(); + break; + case 0x33: + Command_33_ReadData(); + break; + case 0x35: + Command_35_GetData(); + break; + case 0x40: + Command_40_Cancel(); + break; + case 0x53: + Command_53_WriteData(); + break; + case 0x78: + Command_78_PrintSettings(); + break; + case 0x7A: + Command_7A_RegisterFont(); + break; + case 0x7B: + Command_7B_PrintImage(); + break; + case 0x7C: + Command_7C_PrintLine(); + break; + case 0x7D: + Command_7D_Erase(); + break; + case 0x7E: + Command_7E_PrintBarcode(); + break; + case 0x80: + Command_80_EjectCard(); + break; + case 0xA0: + Command_A0_Clean(); + break; + case 0xB0: + Command_B0_DispenseCard(); + break; + case 0xC0: + Command_C0_ControlLED(); + break; + case 0xC1: + Command_C1_SetPrintRetry(); + break; + case 0xD0: + Command_D0_ControlShutter(); + break; + case 0xF0: + Command_F0_GetVersion(); + break; + default: + WARN_LOG_FMT(SERIALINTERFACE_CARD, "HandlePacket: Unhandled command {:02x}", + GetCurrentCommand()); + SetSError(S::ILLEGAL_COMMAND); + break; + } + + if (IsRunningCommand()) + ++m_current_step; +} + +void MagneticCardReader::BuildPacket(std::vector<u8>& write_buffer) +{ + // Header and footer add 6 bytes. + const u8 payload_size = u8(m_command_payload.size() + 6); + // + START_OF_TEXT + the count byte + const auto total_write_size = payload_size + 2; + + const auto prev_buffer_size = write_buffer.size(); + write_buffer.resize(prev_buffer_size + total_write_size); + + auto* out_ptr = write_buffer.data() + prev_buffer_size; + u8 packet_checksum = 0; + + const auto write_and_checksum = [&](u8 value) { + *(out_ptr++) = value; + packet_checksum ^= value; + }; + + // Write the header. + *(out_ptr++) = START_OF_TEXT; + write_and_checksum(payload_size); + write_and_checksum(GetCurrentCommand()); + write_and_checksum(GetPositionValue()); + write_and_checksum(u8(m_status.p)); + write_and_checksum(u8(m_status.s)); + + // Write the payload. + std::ranges::for_each(m_command_payload, write_and_checksum); + + // Write the footer. + write_and_checksum(END_OF_TEXT); + *(out_ptr++) = packet_checksum; + + DEBUG_LOG_FMT( + SERIALINTERFACE_CARD, "BuildPacket: {}", + GetHexDump(std::span{write_buffer}.subspan(write_buffer.size() - payload_size - 2))); +} + +bool MagneticCardReader::IsRunningCommand() const +{ + return m_current_step > 0; +} + +void MagneticCardReader::FinishCommand() +{ + m_current_step = 0; + + if (m_status.s != S::ILLEGAL_COMMAND) + m_status.s = S::NO_JOB; +} + +u8 MagneticCardReader::GetCurrentCommand() const +{ + return m_current_command; +} + +bool MagneticCardReader::IsCardPresent() const +{ + return m_status.r != R::NO_CARD; +} + +bool MagneticCardReader::IsCardLoaded() const +{ + return m_status.r != R::NO_CARD && m_status.r != R::ENTRY_SLOT; +} + +void MagneticCardReader::MoveCard(R new_position) +{ + if (m_status.r == new_position) + return; + + static constexpr const char* position_descriptions[] = { + "NO_CARD", "READ_WRITE_HEAD", "THERMAL_HEAD", "DISPENSER_THERMAL", "ENTRY_SLOT", + }; + + INFO_LOG_FMT(SERIALINTERFACE_CARD, "MoveCard: {}", + position_descriptions[std::size_t(new_position)]); + + m_status.r = new_position; + m_time_since_machine_moved_card = {}; +} + +void MagneticCardReader::EjectCard() +{ + if (IsCardLoaded()) + { + NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Ejecting card."); + + ClearCardInMemory(); + MoveCard(R::ENTRY_SLOT); + + Core::DisplayMessage("Ejected Magnetic Card", 4000); + } +} + +void MagneticCardReader::DispenseCard() +{ + // TODO: Think about how multiple card management UX should work. + // Since this is a new card, we maybe shouldn't be overwriting existing files. + + if (IsCardLoaded()) + { + SetSError(S::ILLEGAL_COMMAND); + return; + } + + if (m_card_settings->report_dispenser_empty) + { + m_status.s = S::DISPENSER_EMPTY; + return; + } + + NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Dispensing a new card."); + Core::DisplayMessage("Created Magnetic Card", 4000); + + // TODO: This behavior doesn't seem to match protocol dumps. + MoveCard(R::DISPENSER_THERMAL); +} + +bool MagneticCardReader::IsReadyForCard() +{ + return !IsCardPresent(); +} + +void MagneticCardReader::DoState(PointerWrap& p) +{ + // Outgoing packet. + p.Do(m_status); + p.Do(m_current_command); + p.Do(m_command_payload); + + // Command state. + p.Do(m_current_packet); + p.Do(m_current_step); + + // The card data in memory. + for (auto& track : m_card_data) + p.Do(track); + + p.Do(m_time_since_machine_moved_card); + p.Do(m_time_since_person_moved_card); +} + +} // namespace MagCard diff --git a/Source/Core/Core/HW/MagCard/MagneticCardReader.h b/Source/Core/Core/HW/MagCard/MagneticCardReader.h new file mode 100644 index 0000000000..b60514a6ab --- /dev/null +++ b/Source/Core/Core/HW/MagCard/MagneticCardReader.h @@ -0,0 +1,207 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// Based on: https://github.com/GXTX/YACardEmu +// Copyright (C) 2020-2023 wutno (https://github.com/GXTX) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <array> +#include <optional> +#include <span> +#include <string> +#include <vector> + +#include "Common/ChunkFile.h" +#include "Common/CommonTypes.h" + +namespace MagCard +{ + +class MagneticCardReader +{ +public: + static constexpr std::size_t TRACK_SIZE = 69; // A nice amount of data. + static constexpr std::size_t NUM_TRACKS = 3; + + // This data is not part of the card reader state. + struct Settings + { + std::string card_name; + std::string card_path; + + bool insert_card_when_possible = true; + bool report_dispenser_empty = false; + }; + + explicit MagneticCardReader(Settings* settings); + virtual ~MagneticCardReader(); + + MagneticCardReader(const MagneticCardReader&) = delete; + MagneticCardReader& operator=(const MagneticCardReader&) = delete; + MagneticCardReader(MagneticCardReader&&) = delete; + MagneticCardReader& operator=(MagneticCardReader&&) = delete; + + // TODO: This std:vector buffer interface is a bit funky.. + + // read = MagCard <-- Baseboard. + // write = Magcard --> Baseboard. + void Process(std::vector<u8>* read, std::vector<u8>* write); + + virtual void DoState(PointerWrap& p); + +protected: + // Status bytes: + enum class R : u8 + { + // Note: Derived classes define the real values in GetPositionValue(). + NO_CARD, + READ_WRITE_HEAD, + THERMAL_HEAD, + DISPENSER_THERMAL, + ENTRY_SLOT, + + MAX_POSITIONS, + }; + enum class P : u8 + { + NO_ERR = 0x30, + READ_ERR = 0x31, + WRITE_ERR = 0x32, + CARD_JAM = 0x33, + MOTOR_ERR = 0x34, // transport system motor error + PRINT_ERR = 0x35, + ILLEGAL_ERR = 0x38, // generic error + BATTERY_ERR = 0x40, // low battery voltage + SYSTEM_ERR = 0x41, // reader/writer system err + TRACK_1_READ_ERR = 0x51, + TRACK_2_READ_ERR = 0x52, + TRACK_3_READ_ERR = 0x53, + TRACK_1_AND_2_READ_ERR = 0x54, + TRACK_1_AND_3_READ_ERR = 0x55, + TRACK_2_AND_3_READ_ERR = 0x56, + }; + enum class S : u8 + { + NO_JOB = 0x30, + ILLEGAL_COMMAND = 0x32, + RUNNING_COMMAND = 0x33, + WAITING_FOR_CARD = 0x34, + DISPENSER_EMPTY = 0x35, + NO_DISPENSER = 0x36, + CARD_FULL = 0x37, + }; + + struct Status + { + R r = R::NO_CARD; + P p = P::NO_ERR; + S s = S::NO_JOB; + + void SoftReset() + { + p = P::NO_ERR; + s = S::NO_JOB; + } + }; + + enum class InitMode : u8 + { + Standard = 0x30, + EjectAfter = 0x31, + ResetSpecifications = 0x32, + }; + + bool ReceivePacket(std::span<const u8> packet); + void BuildPacket(std::vector<u8>& write_buffer); + + void StepStateMachine(DT elapsed_time); + void StepStatePerson(DT elapsed_time); + + // This is what actually maps the "R" position value to a number. + virtual u8 GetPositionValue() = 0; + + void SetPError(P error_code); + void SetSError(S error_code); + + void ClearCardInMemory(); + + // Read/Write with host filesystem. + void ReadCardFile(); + void WriteCardFile(); + + bool IsRunningCommand() const; + void FinishCommand(); + + u8 GetCurrentCommand() const; + + // Is a card anywhere the machine can sense it ? + bool IsCardPresent() const; + + // Is a card beyond the "ENTRY_SLOT" position ? + bool IsCardLoaded() const; + + virtual bool IsReadyForCard(); + + // Move the card through the machine. + void MoveCard(R new_position); + + // Unloads the card and moves it to the "ENTRY_SLOT" position. + void EjectCard(); + + // Feeds a new card into the machine from an internal hopper. + void DispenseCard(); + + // Commands + virtual void Command_10_Initialize(); + void Command_20_ReadStatus(); + void Command_33_ReadData(); // Multi-track read. + void Command_35_GetData(); // Read the entire card. + void Command_40_Cancel(); + void Command_53_WriteData(); // Multi-track write. + void Command_78_PrintSettings(); + void Command_7A_RegisterFont(); + void Command_7B_PrintImage(); + void Command_7C_PrintLine(); + void Command_7D_Erase(); // Erase the printed image. + void Command_7E_PrintBarcode(); + void Command_80_EjectCard(); + void Command_A0_Clean(); + void Command_B0_DispenseCard(); + void Command_C0_ControlLED(); + void Command_C1_SetPrintRetry(); + virtual void Command_D0_ControlShutter(); + void Command_F0_GetVersion(); + + Settings* const m_card_settings; + + // R P S values that are sent in every ENQUIRY response. + Status m_status{}; + + // A byte sent in every ENQUIRY response. + // TODO: What's the default value on just powered up hardware ? + u8 m_current_command = 0; + + // The data included in every ENQUIRY response. + std::vector<u8> m_command_payload; + + // The card track data in memory. + using TrackData = std::optional<std::array<u8, TRACK_SIZE>>; + std::array<TrackData, NUM_TRACKS> m_card_data; + + // The parameter data of the command being processed. + std::vector<u8> m_current_packet; + + // The progress state of the current command. + // 0 == not running a command. + // TODO: Change this to emulated milliseconds or something. + s32 m_current_step = 0; + + // Used to delay re-insertion of cards after ejection. + // This avoids having to expose card "Take"/"Insert" actions. + DT m_time_since_machine_moved_card{}; + DT m_time_since_person_moved_card{}; +}; + +} // namespace MagCard diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp index 7cc5d93c01..0d23288539 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp @@ -9,10 +9,8 @@ #include <fmt/format.h> -#include "Common/Buffer.h" #include "Common/CommonTypes.h" #include "Common/FileUtil.h" -#include "Common/IOFile.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" #include "Common/Swap.h" @@ -28,6 +26,8 @@ #include "Core/HW/EXI/EXI.h" #include "Core/HW/GCPad.h" #include "Core/HW/MMIO.h" +#include "Core/HW/MagCard/C1231BR.h" +#include "Core/HW/MagCard/C1231LR.h" #include "Core/HW/Memmap.h" #include "Core/HW/ProcessorInterface.h" #include "Core/HW/SI/SI.h" @@ -160,6 +160,26 @@ CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices dev // 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: + m_mag_card_reader = std::make_unique<MagCard::C1231BR>(&m_mag_card_settings); + break; + + case MarioKartGP: + case MarioKartGP2: + m_mag_card_reader = std::make_unique<MagCard::C1231LR>(&m_mag_card_settings); + break; + + default: + break; + } } constexpr u32 SI_XFER_LENGTH_MASK = 0x7f; @@ -1147,506 +1167,43 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) case GCAMCommand::SerialB: { DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 32 (CARD-Interface)"); + if (!validate_data_in_out(1, 0, "SerialB")) break; - const u32 length = *data_in++; - if (!validate_data_in_out(length, 0, "SerialB")) - break; - if (length) - { - // Send Card Reply - if (length == 1 && data_in[0] == 0x05) - { - if (m_card_read_length) - { - if (!validate_data_in_out(0, 1, "SerialB")) - break; - data_out[data_offset++] = gcam_command; - u32 read_length = m_card_read_length - m_card_read; - - if (AMMediaboard::GetGameType() == FZeroAX) - { - read_length = std::min<u32>(read_length, 0x2F); - } - - if (!validate_data_in_out(0, 1, "SerialB")) - break; - data_out[data_offset++] = read_length; // 0x2F (max size per packet) - - if (!validate_data_in_out(0, read_length, "SerialB")) - break; - if (u64{m_card_read} + read_length > sizeof(m_card_read_packet)) - { - ERROR_LOG_FMT(SERIALINTERFACE_AMBB, - "GC-AM: Command SerialB, m_card_read_packet overflow:\n" - " - m_card_read_packet = {}\n" - " - m_card_read = {}\n" - " - read_length = {}", - fmt::ptr(m_card_read_packet), m_card_read, read_length); - data_in = data_in_end; - break; - } - memcpy(data_out.data() + data_offset, m_card_read_packet + m_card_read, - read_length); - - data_offset += read_length; - m_card_read += read_length; - - if (m_card_read >= m_card_read_length) - m_card_read_length = 0; - - data_in += length; - break; - } - - if (!validate_data_in_out(0, 5, "SerialB")) - break; - - data_out[data_offset++] = gcam_command; - const u32 command_length_offset = data_offset; - data_out[data_offset++] = 0x00; // len - - data_out[data_offset++] = 0x02; - const u32 checksum_start = data_offset; - - data_out[data_offset++] = 0x00; // 0x00 len - - data_out[data_offset++] = m_card_command; // 0x01 command - - switch (CARDCommand(m_card_command)) - { - case CARDCommand::Init: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x00; // 0x02 - data_out[data_offset++] = 0x30; // 0x03 - break; - case CARDCommand::GetState: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x20 | m_card_bit; // 0x02 - - // bit 0: Please take your card - // bit 1: Endless waiting causes UNK_E to be called - data_out[data_offset++] = 0x00; // 0x03 - break; - case CARDCommand::Read: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x02; // 0x02 - data_out[data_offset++] = 0x53; // 0x03 - break; - case CARDCommand::IsPresent: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x22; // 0x02 - data_out[data_offset++] = 0x30; // 0x03 - break; - case CARDCommand::Write: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x02; // 0x02 - data_out[data_offset++] = 0x00; // 0x03 - break; - case CARDCommand::SetPrintParam: - case CARDCommand::RegisterFont: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x00; // 0x02 - data_out[data_offset++] = 0x00; // 0x03 - break; - case CARDCommand::WriteInfo: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x02; // 0x02 - data_out[data_offset++] = 0x00; // 0x03 - break; - case CARDCommand::Erase: - // TODO: CARDCommand::Erase is not handled. - ERROR_LOG_FMT(SERIALINTERFACE_AMBB, "CARDCommand::Erase is not handled."); - break; - case CARDCommand::Eject: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - if (AMMediaboard::GetGameType() == FZeroAX) - { - data_out[data_offset++] = 0x01; // 0x02 - } - else - { - data_out[data_offset++] = 0x31; // 0x02 - } - data_out[data_offset++] = 0x30; // 0x03 - break; - case CARDCommand::Clean: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x02; // 0x02 - data_out[data_offset++] = 0x00; // 0x03 - break; - case CARDCommand::Load: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x02; // 0x02 - data_out[data_offset++] = 0x30; // 0x03 - break; - case CARDCommand::SetShutter: - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = 0x00; // 0x02 - data_out[data_offset++] = 0x00; // 0x03 - break; - } - - if (!validate_data_in_out(0, 3, "SerialB")) - break; - data_out[data_offset++] = 0x30; // 0x04 - data_out[data_offset++] = 0x00; // 0x05 - - data_out[data_offset++] = 0x03; // 0x06 - - data_out[checksum_start] = data_offset - checksum_start; // 0x00 len - - if (!validate_data_in_out(0, 1, "SerialB")) - break; - data_out[data_offset] = 0; // 0x07 - for (u32 i = 0; i < data_out[checksum_start]; ++i) - data_out[data_offset] ^= data_out[checksum_start + i]; - - if (!validate_data_in_out(0, 1, "SerialB")) - break; - data_offset++; - - data_out[command_length_offset] = data_out[checksum_start] + 2; - } - else - { - if (!validate_data_in_out(length, 0, "SerialB")) - break; - if (u64{m_card_offset} + length > std::size(m_card_buffer)) - { - ERROR_LOG_FMT(SERIALINTERFACE_AMBB, - "GC-AM: Command SerialB, m_card_buffer overflow:\n" - " - m_card_buffer = {}\n" - " - m_card_offset = {}\n" - " - length = {}", - fmt::ptr(m_card_buffer), m_card_offset, length); - data_in = data_in_end; - break; - } - for (u32 i = 0; i < length; ++i) - m_card_buffer[m_card_offset + i] = data_in[i]; + const u32 in_length = *data_in++; - m_card_offset += length; + static constexpr u32 max_packet_size = 0x2f; - // Check if we got a complete command - if (m_card_buffer[0] == 0x02) - { - if (m_card_offset < 2) - { - ERROR_LOG_FMT( - SERIALINTERFACE_AMBB, - "GC-AM: Command SerialB, m_card_buffer overflow (m_card_offset < 2):\n" - " - m_card_buffer = {}\n" - " - m_card_offset = {}\n" - " - length = {}", - fmt::ptr(m_card_buffer), m_card_offset, length); - data_in = data_in_end; - break; - } - if (const u32 offset = m_card_offset - 2; - m_card_buffer[1] == offset && m_card_buffer[offset] == 0x03) - { - m_card_command = m_card_buffer[2]; - - switch (CARDCommand(m_card_command)) - { - case CARDCommand::Init: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD Init"); - - m_card_write_length = 0; - m_card_bit = 0; - m_card_memory_size = 0; - m_card_state_call_count = 0; - break; - case CARDCommand::GetState: - { - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD GetState({:02X})", - m_card_bit); - - if (m_card_memory_size == 0) - { - const std::string card_filename( - fmt::format("{}tricard_{}.bin", File::GetUserPath(D_TRIUSER_IDX), - SConfig::GetInstance().GetGameID())); - - if (File::Exists(card_filename)) - { - File::IOFile card(card_filename, "rb+"); - m_card_memory_size = static_cast<u32>(card.GetSize()); - if (m_card_memory_size > sizeof(m_card_memory)) - { - ERROR_LOG_FMT(SERIALINTERFACE_CARD, - "GC-AM: Command CARD GetState overflow:\n" - " - file name = {}\n" - " - file size = {}\n" - " - card size = {}", - card_filename, m_card_memory_size, sizeof(m_card_memory)); - data_in = data_in_end; - break; - } - card.ReadBytes(m_card_memory, m_card_memory_size); - card.Close(); - - m_card_is_inserted = true; - } - } - - if (AMMediaboard::GetGameType() == FZeroAX && m_card_memory_size) - { - m_card_state_call_count++; - if (m_card_state_call_count > 10) - { - if (m_card_bit & 2) - m_card_bit &= ~2u; - else - m_card_bit |= 2; - - m_card_state_call_count = 0; - } - } - - if (m_card_clean == 1) - { - m_card_clean = 2; - } - else if (m_card_clean == 2) - { - const std::string card_filename( - fmt::format("{}tricard_{}.bin", File::GetUserPath(D_TRIUSER_IDX), - SConfig::GetInstance().GetGameID())); - - if (File::Exists(card_filename)) - { - m_card_memory_size = static_cast<u32>(File::GetSize(card_filename)); - if (m_card_memory_size) - { - if (AMMediaboard::GetGameType() == FZeroAX) - { - m_card_bit = 2; - } - else - { - m_card_bit = 1; - } - } - } - m_card_clean = 0; - } - break; - } - case CARDCommand::IsPresent: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD IsPresent"); - break; - case CARDCommand::RegisterFont: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD RegisterFont"); - break; - case CARDCommand::Load: - { - const u8 mode = m_card_buffer[6]; - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD Load({:02X})", mode); - break; - } - case CARDCommand::Clean: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD Clean"); - m_card_clean = 1; - break; - case CARDCommand::Read: - { - const u8 mode = m_card_buffer[6]; - const u8 bitmode = m_card_buffer[7]; - const u8 track = m_card_buffer[8]; - - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, - "GC-AM: Command CARD Read({:02X},{:02X},{:02X})", mode, bitmode, - track); - - // Prepare read packet - memset(m_card_read_packet, 0, 0xDB); - - const std::string card_filename( - fmt::format("{}tricard_{}.bin", File::GetUserPath(D_TRIUSER_IDX), - SConfig::GetInstance().GetGameID())); - - if (File::Exists(card_filename)) - { - File::IOFile card(card_filename, "rb+"); - if (m_card_memory_size == 0) - { - m_card_memory_size = static_cast<u32>(card.GetSize()); - } - - if (m_card_memory_size > sizeof(m_card_memory)) - { - ERROR_LOG_FMT(SERIALINTERFACE_CARD, - "GC-AM: Command CARD Read overflow:\n" - " - file name = {}\n" - " - file size = {}\n" - " - card size = {}", - card_filename, m_card_memory_size, sizeof(m_card_memory)); - data_in = data_in_end; - break; - } - card.ReadBytes(m_card_memory, m_card_memory_size); - card.Close(); - - m_card_is_inserted = true; - } - else if (m_card_memory_size > sizeof(m_card_memory)) - { - ERROR_LOG_FMT(SERIALINTERFACE_CARD, - "GC-AM: Command CARD Read overflow:\n" - " - requested size = {}\n" - " - card size = {}", - m_card_memory_size, sizeof(m_card_memory)); - data_in = data_in_end; - break; - } - - m_card_read_packet[0] = 0x02; // SUB CMD - m_card_read_packet[1] = 0x00; // SUB CMDLen - - m_card_read_packet[2] = 0x33; // CARD CMD - - if (m_card_is_inserted) // CARD Status - { - m_card_read_packet[3] = 0x31; - } - else - { - m_card_read_packet[3] = 0x30; - } - - m_card_read_packet[4] = 0x30; - m_card_read_packet[5] = 0x30; - - u32 packet_offset = 6; - // Data reply - static_assert(sizeof(m_card_read_packet) >= sizeof(m_card_memory) + 6); - memcpy(m_card_read_packet + packet_offset, m_card_memory, m_card_memory_size); - packet_offset += m_card_memory_size; - - static_assert(sizeof(m_card_read_packet) >= sizeof(m_card_memory) + 7); - m_card_read_packet[packet_offset++] = 0x03; - - m_card_read_packet[1] = packet_offset - 1; // SUB CMDLen - - static_assert(sizeof(m_card_read_packet) >= sizeof(m_card_memory) + 8); - for (u32 i = 0; i < packet_offset - 1; ++i) - m_card_read_packet[packet_offset] ^= m_card_read_packet[1 + i]; - - static_assert(sizeof(m_card_read_packet) >= sizeof(m_card_memory) + 9); - packet_offset++; - - m_card_read_length = packet_offset; - m_card_read = 0; - break; - } - case CARDCommand::Write: - { - const u8 mode = m_card_buffer[6]; - const u8 bitmode = m_card_buffer[7]; - const u8 track = m_card_buffer[8]; - - m_card_memory_size = m_card_buffer[1] - 9; - if (m_card_memory_size > sizeof(m_card_memory)) - { - ERROR_LOG_FMT(SERIALINTERFACE_CARD, - "GC-AM: Command CARD Write overflow:\n" - " - write size = {}\n" - " - card size = {}", - m_card_memory_size, sizeof(m_card_memory)); - data_in = data_in_end; - break; - } - - static_assert(sizeof(m_card_buffer) >= sizeof(m_card_memory) + 9); - memcpy(m_card_memory, m_card_buffer + 9, m_card_memory_size); + // Also accounting for the 2-byte header. + if (!validate_data_in_out(in_length, max_packet_size + 2, "SerialB")) + break; - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, - "GC-AM: Command CARD Write: {:02X} {:02X} {:02X} {}", mode, - bitmode, track, m_card_memory_size); + if (m_mag_card_reader) + { + // Append the data to our buffer. + const auto prev_size = m_mag_card_in_buffer.size(); + m_mag_card_in_buffer.resize(prev_size + in_length); + std::ranges::copy(std::span{data_in, in_length}, + m_mag_card_in_buffer.data() + prev_size); + + // Send and receive data with the magnetic card reader. + m_mag_card_reader->Process(&m_mag_card_in_buffer, &m_mag_card_out_buffer); + } - const std::string card_filename(File::GetUserPath(D_TRIUSER_IDX) + "tricard_" + - SConfig::GetInstance().GetGameID() + ".bin"); + data_in += in_length; + const auto out_length = std::min(u32(m_mag_card_out_buffer.size()), max_packet_size); - File::IOFile card(card_filename, "wb+"); - card.WriteBytes(m_card_memory, m_card_memory_size); - card.Close(); + // Write the 2-byte header. + data_out[data_offset++] = gcam_command; + data_out[data_offset++] = u8(out_length); - m_card_bit = 2; + // Write the data. + std::copy_n(m_mag_card_out_buffer.data(), out_length, data_out.data() + data_offset); + data_offset += out_length; - m_card_state_call_count = 0; - break; - } - case CARDCommand::SetPrintParam: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD SetPrintParam"); - break; - case CARDCommand::WriteInfo: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD WriteInfo"); - break; - case CARDCommand::Erase: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD Erase"); - break; - case CARDCommand::Eject: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD Eject"); - if (AMMediaboard::GetGameType() != FZeroAX) - { - m_card_bit = 0; - } - break; - case CARDCommand::SetShutter: - NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command CARD SetShutter"); - if (AMMediaboard::GetGameType() != FZeroAX) - { - m_card_bit = 0; - } - // Close - if (m_card_buffer[6] == 0x30) - { - m_card_shutter = false; - } - // Open - else if (m_card_buffer[6] == 0x31) - { - m_card_shutter = true; - } - break; - default: - ERROR_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: CARD:Unhandled command!"); - ERROR_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: CARD:[{:08X}]", m_card_command); - // hexdump( m_card_buffer, m_card_offset ); - break; - } - m_card_offset = 0; - } - } - - if (!validate_data_in_out(0, 3, "SerialB")) - break; - data_out[data_offset++] = 0x32; - data_out[data_offset++] = 0x01; // len - data_out[data_offset++] = 0x06; // OK - } - } - else - { - if (!validate_data_in_out(0, 2, "SerialB")) - break; - data_out[data_offset++] = gcam_command; - data_out[data_offset++] = 0x00; // len - } - data_in += length; + // Remove the data from our buffer. + m_mag_card_out_buffer.erase(m_mag_card_out_buffer.begin(), + m_mag_card_out_buffer.begin() + s32(out_length)); break; } case GCAMCommand::JVSIOA: @@ -2762,24 +2319,14 @@ void CSIDevice_AMBaseboard::DoState(PointerWrap& p) p.Do(m_ic_write_offset); p.Do(m_ic_write_size); - p.Do(m_card_memory); - p.Do(m_card_read_packet); - p.Do(m_card_buffer); - - // Setup CARD - p.Do(m_card_memory_size); - p.Do(m_card_is_inserted); - - p.Do(m_card_command); - p.Do(m_card_clean); - p.Do(m_card_write_length); - p.Do(m_card_wrote); - p.Do(m_card_read_length); - p.Do(m_card_read); - p.Do(m_card_bit); - p.Do(m_card_shutter); - p.Do(m_card_state_call_count); - p.Do(m_card_offset); + // Magnetic Card Reader + if (m_mag_card_reader) + { + m_mag_card_reader->DoState(p); + + p.Do(m_mag_card_in_buffer); + p.Do(m_mag_card_out_buffer); + } // Serial p.Do(m_wheel_init); diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h index 974325c7c2..31ed04053c 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h +++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h @@ -4,7 +4,9 @@ #pragma once #include "Core/HW/GCPad.h" +#include "Core/HW/MagCard/MagneticCardReader.h" #include "Core/HW/SI/SI_Device.h" + #include "InputCommon/GCPadStatus.h" namespace Movie @@ -274,24 +276,13 @@ private: u32 m_ic_write_offset = 0; u32 m_ic_write_size = 0; - u8 m_card_memory[0xD0] = {}; - u8 m_card_read_packet[0xDB] = {}; - u8 m_card_buffer[0x100] = {}; - - // Setup CARD - u32 m_card_memory_size = 0; - bool m_card_is_inserted = false; - - u32 m_card_command = 0; - u32 m_card_clean = 0; - u32 m_card_write_length = 0; - u32 m_card_wrote = 0; - u32 m_card_read_length = 0; - u32 m_card_read = 0; - u32 m_card_bit = 0; - bool m_card_shutter = true; // Open - u32 m_card_state_call_count = 0; - u8 m_card_offset = 0; + // Magnetic Card Reader + MagCard::MagneticCardReader::Settings m_mag_card_settings; + + std::vector<u8> m_mag_card_in_buffer; + std::vector<u8> m_mag_card_out_buffer; + + std::unique_ptr<MagCard::MagneticCardReader> m_mag_card_reader; // Serial u32 m_wheel_init = 0; diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 7ed0b580e3..34e4b83f13 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -317,6 +317,9 @@ <ClInclude Include="Core\HW\HSP\HSP_DeviceARAMExpansion.h" /> <ClInclude Include="Core\HW\HSP\HSP_DeviceNull.h" /> <ClInclude Include="Core\HW\HW.h" /> + <ClInclude Include="Core\HW\MagCard\C1231BR.h" /> + <ClInclude Include="Core\HW\MagCard\C1231LR.h" /> + <ClInclude Include="Core\HW\MagCard\MagneticCardReader.h" /> <ClInclude Include="Core\HW\Memmap.h" /> <ClInclude Include="Core\HW\MemoryInterface.h" /> <ClInclude Include="Core\HW\MMIO.h" /> @@ -1015,6 +1018,9 @@ <ClCompile Include="Core\HW\HSP\HSP_DeviceARAMExpansion.cpp" /> <ClCompile Include="Core\HW\HSP\HSP_DeviceNull.cpp" /> <ClCompile Include="Core\HW\HW.cpp" /> + <ClCompile Include="Core\HW\MagCard\C1231BR.cpp" /> + <ClCompile Include="Core\HW\MagCard\C1231LR.cpp" /> + <ClCompile Include="Core\HW\MagCard\MagneticCardReader.cpp" /> <ClCompile Include="Core\HW\Memmap.cpp" /> <ClCompile Include="Core\HW\MemoryInterface.cpp" /> <ClCompile Include="Core\HW\MMIO.cpp" /> |
