summaryrefslogtreecommitdiff
path: root/Source
diff options
context:
space:
mode:
authorJordan Woyak <jordan.woyak@gmail.com>2026-02-10 01:56:00 -0600
committerJordan Woyak <jordan.woyak@gmail.com>2026-02-15 20:14:15 -0600
commitf657b3252149c6e1854aa82a5378a9d3a97f3978 (patch)
treedfd7027a57037e301187816ffb8db8403edca793 /Source
parentd893565b781a0a033ae2ff12962df436de695343 (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.
Diffstat (limited to 'Source')
-rw-r--r--Source/Core/Core/CMakeLists.txt6
-rw-r--r--Source/Core/Core/HW/MagCard/C1231BR.cpp82
-rw-r--r--Source/Core/Core/HW/MagCard/C1231BR.h37
-rw-r--r--Source/Core/Core/HW/MagCard/C1231LR.cpp27
-rw-r--r--Source/Core/Core/HW/MagCard/C1231LR.h26
-rw-r--r--Source/Core/Core/HW/MagCard/MagneticCardReader.cpp1002
-rw-r--r--Source/Core/Core/HW/MagCard/MagneticCardReader.h207
-rw-r--r--Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp569
-rw-r--r--Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h27
-rw-r--r--Source/Core/DolphinLib.props6
10 files changed, 1460 insertions, 529 deletions
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" />