mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-08-28 05:37:36 -05:00
Merge pull request #14501 from jordan-woyak/triforce-jvs-io
Triforce: Rewrite JVS IO board emulation.
This commit is contained in:
@@ -296,10 +296,16 @@ add_library(core
|
||||
HW/SystemTimers.h
|
||||
HW/Triforce/DeckReader.cpp
|
||||
HW/Triforce/DeckReader.h
|
||||
HW/Triforce/FZeroAX.cpp
|
||||
HW/Triforce/FZeroAX.h
|
||||
HW/Triforce/ICCardReader.cpp
|
||||
HW/Triforce/ICCardReader.h
|
||||
HW/Triforce/IOPorts.cpp
|
||||
HW/Triforce/IOPorts.h
|
||||
HW/Triforce/JVSIO.cpp
|
||||
HW/Triforce/JVSIO.h
|
||||
HW/Triforce/MarioKartGP.cpp
|
||||
HW/Triforce/MarioKartGP.h
|
||||
HW/Triforce/SerialDevice.cpp
|
||||
HW/Triforce/SerialDevice.h
|
||||
HW/Triforce/Touchscreen.cpp
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,38 +10,20 @@
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
class JVSIOBoard;
|
||||
class SerialDevice;
|
||||
}
|
||||
} // namespace Triforce
|
||||
|
||||
namespace SerialInterface
|
||||
{
|
||||
|
||||
// "JAMMA Video Standard" I/O
|
||||
class JVSIOMessage
|
||||
{
|
||||
public:
|
||||
void Start(int node);
|
||||
void AddData(const u8* dst, std::size_t len, int sync);
|
||||
void AddData(const void* data, std::size_t len);
|
||||
void AddData(const char* data);
|
||||
void AddData(u32 n);
|
||||
void End();
|
||||
|
||||
u32 m_pointer = 0;
|
||||
std::array<u8, 0x80> m_message;
|
||||
|
||||
private:
|
||||
u32 m_last_start = 0;
|
||||
u32 m_checksum = 0;
|
||||
};
|
||||
|
||||
// Triforce (GC-AM) baseboard
|
||||
class CSIDevice_AMBaseboard : public ISIDevice
|
||||
class CSIDevice_AMBaseboard final : public ISIDevice
|
||||
{
|
||||
public:
|
||||
CSIDevice_AMBaseboard(Core::System& system, SIDevices device, int device_number);
|
||||
~CSIDevice_AMBaseboard() override;
|
||||
|
||||
// run the SI Buffer
|
||||
int RunBuffer(u8* buffer, int request_length) override;
|
||||
|
||||
DataResponse GetData(u32& hi, u32& low) override;
|
||||
@@ -51,89 +33,15 @@ public:
|
||||
void DoState(PointerWrap&) override;
|
||||
|
||||
private:
|
||||
enum GCAMCommand
|
||||
{
|
||||
StatusSwitches = 0x10,
|
||||
SerialNumber = 0x11,
|
||||
Unknown_12 = 0x12,
|
||||
Unknown_14 = 0x14,
|
||||
FirmVersion = 0x15,
|
||||
FPGAVersion = 0x16,
|
||||
RegionSettings = 0x1F,
|
||||
|
||||
Unknown_21 = 0x21,
|
||||
Unknown_22 = 0x22,
|
||||
Unknown_23 = 0x23,
|
||||
Unknown_24 = 0x24,
|
||||
|
||||
SerialA = 0x31,
|
||||
SerialB = 0x32,
|
||||
|
||||
JVSIOA = 0x40,
|
||||
JVSIOB = 0x41,
|
||||
|
||||
Unknown_60 = 0x60,
|
||||
};
|
||||
|
||||
enum JVSIOCommand
|
||||
{
|
||||
IOID = 0x10,
|
||||
CommandRevision = 0x11,
|
||||
JVRevision = 0x12,
|
||||
CommunicationVersion = 0x13,
|
||||
CheckFunctionality = 0x14,
|
||||
MainID = 0x15,
|
||||
|
||||
SwitchesInput = 0x20,
|
||||
CoinInput = 0x21,
|
||||
AnalogInput = 0x22,
|
||||
RotaryInput = 0x23,
|
||||
KeyCodeInput = 0x24,
|
||||
PositionInput = 0x25,
|
||||
GeneralSwitchInput = 0x26,
|
||||
|
||||
PayoutRemain = 0x2E,
|
||||
Retrans = 0x2F,
|
||||
CoinSubOutput = 0x30,
|
||||
PayoutAddOutput = 0x31,
|
||||
GeneralDriverOutput = 0x32,
|
||||
AnalogOutput = 0x33,
|
||||
CharacterOutput = 0x34,
|
||||
CoinAddOutput = 0x35,
|
||||
PayoutSubOutput = 0x36,
|
||||
GeneralDriverOutput2 = 0x37,
|
||||
GeneralDriverOutput3 = 0x38,
|
||||
|
||||
NAMCOCommand = 0x70,
|
||||
|
||||
Reset = 0xF0,
|
||||
SetAddress = 0xF1,
|
||||
ChangeComm = 0xF2,
|
||||
};
|
||||
|
||||
enum JVSIOStatusCode
|
||||
{
|
||||
StatusOkay = 1,
|
||||
UnsupportedCommand = 2,
|
||||
ChecksumError = 3,
|
||||
AcknowledgeOverflow = 4,
|
||||
};
|
||||
|
||||
static constexpr u32 RESPONSE_SIZE = SerialInterfaceManager::BUFFER_SIZE;
|
||||
|
||||
// This value prevents F-Zero AX mag card breakage.
|
||||
// It's now used for serial port reads in general.
|
||||
// TODO: Verify how the hardware actually works.
|
||||
static constexpr u32 SERIAL_PORT_MAX_READ_SIZE = 0x1f;
|
||||
|
||||
// Reply has to be delayed due a bug in the parser
|
||||
std::array<std::array<u8, RESPONSE_SIZE>, 2> m_response_buffers{};
|
||||
u8 m_current_response_buffer_index = 0;
|
||||
|
||||
Triforce::IOPorts m_io_ports;
|
||||
|
||||
std::array<u16, 2> m_coin{};
|
||||
std::array<u32, 2> m_coin_pressed{};
|
||||
std::unique_ptr<Triforce::JVSIOBoard> m_jvs_io_board;
|
||||
|
||||
// Magnetic Card Reader
|
||||
MagCard::MagneticCardReader::Settings m_mag_card_settings;
|
||||
@@ -143,30 +51,6 @@ private:
|
||||
|
||||
// Serial B
|
||||
std::unique_ptr<Triforce::SerialDevice> m_serial_device_b;
|
||||
|
||||
u32 m_wheel_init = 0;
|
||||
|
||||
u32 m_motor_init = 0;
|
||||
u8 m_motor_reply[64] = {};
|
||||
s16 m_motor_force_y = 0;
|
||||
|
||||
// F-Zero AX (DX)
|
||||
bool m_fzdx_seatbelt = true;
|
||||
bool m_fzdx_motion_stop = false;
|
||||
bool m_fzdx_sensor_right = false;
|
||||
bool m_fzdx_sensor_left = false;
|
||||
u8 m_rx_reply = 0xF0;
|
||||
|
||||
// F-Zero AX (CyCraft)
|
||||
bool m_fzcc_seatbelt = true;
|
||||
bool m_fzcc_sensor = false;
|
||||
bool m_fzcc_emergency = false;
|
||||
bool m_fzcc_service = false;
|
||||
|
||||
u32 m_dip_switch_1 = 0xFE;
|
||||
u32 m_dip_switch_0 = 0xFF;
|
||||
|
||||
int m_delay = 0;
|
||||
};
|
||||
|
||||
} // namespace SerialInterface
|
||||
|
||||
275
Source/Core/Core/HW/Triforce/FZeroAX.cpp
Normal file
275
Source/Core/Core/HW/Triforce/FZeroAX.cpp
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Core/HW/Triforce/FZeroAX.h"
|
||||
|
||||
#include <numeric>
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "Common/BitUtils.h"
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/Swap.h"
|
||||
|
||||
#include "Core/HW/GCPad.h"
|
||||
|
||||
#include "InputCommon/GCPadStatus.h"
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
|
||||
void FZeroAXCommon_IOAdapter::Update()
|
||||
{
|
||||
auto* const io_ports = GetIOPorts();
|
||||
|
||||
// Horizontal Scanning Frequency switch.
|
||||
// Required for booting via Sega Boot.
|
||||
io_ports->GetStatusSwitches()[0] &= ~0x20u;
|
||||
|
||||
const GCPadStatus pad_status = Pad::GetStatus(0);
|
||||
|
||||
const auto switch_inputs_0 = io_ports->GetSwitchInputs(0);
|
||||
// Start
|
||||
if (pad_status.button & PAD_BUTTON_START)
|
||||
switch_inputs_0[0] |= 0x80;
|
||||
// View Change 1
|
||||
if (pad_status.button & PAD_BUTTON_RIGHT)
|
||||
switch_inputs_0[0] |= 0x20;
|
||||
// View Change 2
|
||||
if (pad_status.button & PAD_BUTTON_LEFT)
|
||||
switch_inputs_0[0] |= 0x10;
|
||||
// View Change 3
|
||||
if (pad_status.button & PAD_BUTTON_UP)
|
||||
switch_inputs_0[0] |= 0x08;
|
||||
// View Change 4
|
||||
if (pad_status.button & PAD_BUTTON_DOWN)
|
||||
switch_inputs_0[0] |= 0x04;
|
||||
// Boost
|
||||
if (pad_status.button & PAD_BUTTON_A)
|
||||
switch_inputs_0[0] |= 0x02;
|
||||
|
||||
const auto switch_inputs_1 = io_ports->GetSwitchInputs(1);
|
||||
// Paddle left
|
||||
if (pad_status.button & PAD_BUTTON_X)
|
||||
switch_inputs_1[0] |= 0x20;
|
||||
// Paddle right
|
||||
if (pad_status.button & PAD_BUTTON_Y)
|
||||
switch_inputs_1[0] |= 0x10;
|
||||
|
||||
const auto analog_inputs = io_ports->GetAnalogInputs();
|
||||
|
||||
// Steering
|
||||
if (m_steering_wheel->IsInitializing())
|
||||
{
|
||||
// Override X position during initialization to make the calibration happy.
|
||||
analog_inputs[0] =
|
||||
(m_steering_wheel->GetServoPosition() * (1 << 9)) + IOPorts::NEUTRAL_ANALOG_VALUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
analog_inputs[0] = Common::ExpandValue<u16>(pad_status.stickX, 8);
|
||||
}
|
||||
|
||||
analog_inputs[1] = Common::ExpandValue<u16>(pad_status.stickY, 8);
|
||||
|
||||
// Gas
|
||||
analog_inputs[4] = Common::ExpandValue<u16>(pad_status.triggerRight, 8);
|
||||
// Brake
|
||||
analog_inputs[5] = Common::ExpandValue<u16>(pad_status.triggerLeft, 8);
|
||||
// Seat Motion
|
||||
analog_inputs[6] = IOPorts::NEUTRAL_ANALOG_VALUE;
|
||||
}
|
||||
|
||||
void FZeroAXCommon_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
std::span<const u8> bits_cleared)
|
||||
{
|
||||
const u8 bits_changed_0 = bits_set[0] | bits_cleared[0];
|
||||
|
||||
constexpr auto LED_NAMES = std::to_array<std::pair<u8, const char*>>({
|
||||
{0x80, "START BUTTON"},
|
||||
{0x20, "VIEW CHANGE 1"},
|
||||
{0x10, "VIEW CHANGE 2"},
|
||||
{0x08, "VIEW CHANGE 3"},
|
||||
{0x04, "VIEW CHANGE 4"},
|
||||
{0x40, "BOOST"},
|
||||
});
|
||||
|
||||
for (const auto& [led_value, led_name] : LED_NAMES)
|
||||
{
|
||||
if (bits_changed_0 & led_value)
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: {}: {}", led_name,
|
||||
(bits_set[0] & led_value) ? "ON" : "OFF");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FZeroAXDeluxe_IOAdapter::Update()
|
||||
{
|
||||
auto* const io_ports = GetIOPorts();
|
||||
const auto generic_outputs = io_ports->GetGenericOutputs();
|
||||
|
||||
// Not fully understood trickery to satisfy the game's initialization sequence.
|
||||
const u16 seat_state = Common::swap16(generic_outputs.data() + 1) >> 2;
|
||||
switch (seat_state)
|
||||
{
|
||||
case 0x70:
|
||||
++m_delay;
|
||||
if ((m_delay % 10) == 0)
|
||||
{
|
||||
m_rx_reply = 0xFB;
|
||||
}
|
||||
break;
|
||||
case 0xF0:
|
||||
m_rx_reply = 0xF0;
|
||||
break;
|
||||
default:
|
||||
case 0xA0:
|
||||
case 0x60:
|
||||
break;
|
||||
}
|
||||
|
||||
constexpr bool seatbelt = true;
|
||||
constexpr bool motion_stop = false;
|
||||
constexpr bool sensor_left = false;
|
||||
constexpr bool sensor_right = false;
|
||||
|
||||
const auto switch_inputs_p0 = io_ports->GetSwitchInputs(0);
|
||||
|
||||
if (seatbelt)
|
||||
switch_inputs_p0[0] |= 0x01;
|
||||
|
||||
switch_inputs_p0[1] = m_rx_reply & 0xF0;
|
||||
|
||||
const auto switch_inputs_p1 = io_ports->GetSwitchInputs(1);
|
||||
|
||||
if (sensor_left)
|
||||
switch_inputs_p1[0] |= 0x08;
|
||||
if (sensor_right)
|
||||
switch_inputs_p1[0] |= 0x04;
|
||||
if (motion_stop)
|
||||
switch_inputs_p1[0] |= 0x02;
|
||||
|
||||
switch_inputs_p1[1] = m_rx_reply << 4;
|
||||
}
|
||||
|
||||
void FZeroAXMonster_IOAdapter::Update()
|
||||
{
|
||||
auto* const io_ports = GetIOPorts();
|
||||
|
||||
constexpr bool sensor = false;
|
||||
constexpr bool emergency = false;
|
||||
constexpr bool service = false;
|
||||
constexpr bool seatbelt = true;
|
||||
|
||||
const auto switch_inputs_p0 = io_ports->GetSwitchInputs(0);
|
||||
|
||||
if (sensor)
|
||||
switch_inputs_p0[0] |= 0x01;
|
||||
|
||||
const auto switch_inputs_p1 = io_ports->GetSwitchInputs(1);
|
||||
|
||||
if (emergency)
|
||||
switch_inputs_p1[0] |= 0x08;
|
||||
if (service)
|
||||
switch_inputs_p1[0] |= 0x04;
|
||||
if (seatbelt)
|
||||
switch_inputs_p1[0] |= 0x02;
|
||||
}
|
||||
|
||||
void FZeroAXDeluxe_IOAdapter::DoState(PointerWrap& p)
|
||||
{
|
||||
p.Do(m_delay);
|
||||
p.Do(m_rx_reply);
|
||||
}
|
||||
|
||||
void FZeroAXSteeringWheel::Update()
|
||||
{
|
||||
constexpr std::size_t REQUEST_SIZE = 4;
|
||||
|
||||
std::size_t rx_position = 0;
|
||||
while (true)
|
||||
{
|
||||
const auto rx_span = GetRxByteSpan().subspan(rx_position);
|
||||
|
||||
if (rx_span.size() < REQUEST_SIZE)
|
||||
break; // Wait for more data.
|
||||
|
||||
const auto request = rx_span.first<REQUEST_SIZE>();
|
||||
|
||||
// The first byte is XOR'd with 0x80.
|
||||
// The last byte is an XOR of the previous bytes.
|
||||
if (std::accumulate(request.begin(), request.end(), u8{0x80}, std::bit_xor{}) != 0)
|
||||
{
|
||||
WARN_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Bad checksum!");
|
||||
++rx_position;
|
||||
continue;
|
||||
}
|
||||
|
||||
ProcessRequest(request);
|
||||
rx_position += REQUEST_SIZE;
|
||||
}
|
||||
|
||||
ConsumeRxBytes(rx_position);
|
||||
}
|
||||
|
||||
void FZeroAXSteeringWheel::ProcessRequest(std::span<const u8> request)
|
||||
{
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Request: {:02x}", fmt::join(request, " "));
|
||||
|
||||
// The first byte is XOR'd with 0x80.
|
||||
const u8 cmd = request[0] ^ 0x80u;
|
||||
switch (cmd)
|
||||
{
|
||||
case 0: // Power on/off ?
|
||||
// Sent before force commands: 00 01
|
||||
// Sent after force commands: 00 00
|
||||
case 1: // Set Maximum?
|
||||
case 2:
|
||||
break;
|
||||
|
||||
case 4: // Move Steering Wheel
|
||||
{
|
||||
// This seems to be a u8 value but the MSb is the LSb of the previous byte.
|
||||
// e.g. 01 7f -> ff
|
||||
|
||||
// This produces a value in the range around [-56, +56].
|
||||
m_servo_position = s8(0x80 - (u8(request[1] << 7u) | request[2]));
|
||||
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: servo_position: {}", m_servo_position);
|
||||
break;
|
||||
}
|
||||
|
||||
case 6: // nice
|
||||
case 9:
|
||||
default:
|
||||
break;
|
||||
|
||||
// Switch back to normal controls
|
||||
case 7:
|
||||
m_init_state = 2;
|
||||
break;
|
||||
|
||||
// Reset
|
||||
case 0x7F:
|
||||
m_init_state = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// Simple 4 byte response.
|
||||
WriteTxBytes(std::array<u8, 4>{});
|
||||
}
|
||||
|
||||
bool FZeroAXSteeringWheel::IsInitializing() const
|
||||
{
|
||||
return m_init_state == 1;
|
||||
}
|
||||
|
||||
void FZeroAXSteeringWheel::DoState(PointerWrap& p)
|
||||
{
|
||||
p.Do(m_init_state);
|
||||
p.Do(m_servo_position);
|
||||
}
|
||||
|
||||
} // namespace Triforce
|
||||
70
Source/Core/Core/HW/Triforce/FZeroAX.h
Normal file
70
Source/Core/Core/HW/Triforce/FZeroAX.h
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Core/HW/Triforce/IOPorts.h"
|
||||
#include "Core/HW/Triforce/SerialDevice.h"
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
|
||||
// FFB wheel used by FZeroAX and FZeroAXMonster.
|
||||
class FZeroAXSteeringWheel final : public SerialDevice
|
||||
{
|
||||
public:
|
||||
void Update() override;
|
||||
|
||||
bool IsInitializing() const;
|
||||
|
||||
s8 GetServoPosition() const { return m_servo_position; }
|
||||
|
||||
void DoState(PointerWrap&) override;
|
||||
|
||||
private:
|
||||
void ProcessRequest(std::span<const u8>);
|
||||
|
||||
u8 m_init_state = 0;
|
||||
|
||||
s8 m_servo_position = 0;
|
||||
};
|
||||
|
||||
// Used for both FZeroAX and FZeroAXMonster.
|
||||
class FZeroAXCommon_IOAdapter final : public IOAdapter
|
||||
{
|
||||
public:
|
||||
explicit FZeroAXCommon_IOAdapter(FZeroAXSteeringWheel* steering_wheel)
|
||||
: m_steering_wheel{steering_wheel}
|
||||
{
|
||||
}
|
||||
|
||||
void Update() override;
|
||||
|
||||
protected:
|
||||
void HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
std::span<const u8> bits_cleared) override;
|
||||
|
||||
private:
|
||||
FZeroAXSteeringWheel* const m_steering_wheel;
|
||||
};
|
||||
|
||||
// Includes seat motion handling.
|
||||
class FZeroAXDeluxe_IOAdapter final : public IOAdapter
|
||||
{
|
||||
public:
|
||||
void Update() override;
|
||||
|
||||
void DoState(PointerWrap&) override;
|
||||
|
||||
private:
|
||||
u32 m_delay = 0;
|
||||
u8 m_rx_reply = 0xF0;
|
||||
};
|
||||
|
||||
class FZeroAXMonster_IOAdapter final : public IOAdapter
|
||||
{
|
||||
public:
|
||||
void Update() override;
|
||||
};
|
||||
|
||||
} // namespace Triforce
|
||||
@@ -3,27 +3,63 @@
|
||||
|
||||
#include "Core/HW/Triforce/IOPorts.h"
|
||||
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/BitUtils.h"
|
||||
#include "Common/ChunkFile.h"
|
||||
|
||||
#include "Core/HW/DVD/AMMediaboard.h"
|
||||
#include "Core/HW/GCPad.h"
|
||||
#include "Core/HW/SI/SI.h"
|
||||
#include "Core/HW/SI/SI_Device.h"
|
||||
#include "Core/HW/Triforce/ICCardReader.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
#include "InputCommon/GCPadStatus.h"
|
||||
#include "VideoCommon/OnScreenDisplay.h"
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
|
||||
void IOPorts::Update()
|
||||
{
|
||||
m_system_inputs = 0x00;
|
||||
m_switch_inputs.fill(0x00);
|
||||
m_analog_inputs.fill(NEUTRAL_ANALOG_VALUE);
|
||||
m_coin_inputs.fill(false);
|
||||
|
||||
std::ranges::for_each(m_io_adapters, &IOAdapter::Update);
|
||||
}
|
||||
|
||||
std::span<u8> IOPorts::GetSwitchInputs(u32 player_index)
|
||||
{
|
||||
ASSERT(player_index < PLAYER_COUNT);
|
||||
|
||||
return std::span{m_switch_inputs}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index,
|
||||
SWITCH_INPUT_BYTES_PER_PLAYER);
|
||||
}
|
||||
|
||||
std::span<const u8> IOPorts::GetSwitchInputs(u32 player_index) const
|
||||
{
|
||||
ASSERT(player_index < PLAYER_COUNT);
|
||||
|
||||
return std::span{m_switch_inputs}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index,
|
||||
SWITCH_INPUT_BYTES_PER_PLAYER);
|
||||
}
|
||||
|
||||
void IOPorts::DoState(PointerWrap& p)
|
||||
{
|
||||
p.Do(m_switch_input_data);
|
||||
p.Do(m_generic_output_data);
|
||||
p.Do(m_status_switches);
|
||||
|
||||
// Input states need not be state saved. They are updated before use.
|
||||
|
||||
p.Do(m_generic_outputs);
|
||||
|
||||
for (auto& io_adapter : m_io_adapters)
|
||||
io_adapter->DoState(p);
|
||||
}
|
||||
|
||||
void IOPorts::AddIOAdapter(std::unique_ptr<IOAdapter> adapter)
|
||||
@@ -37,42 +73,33 @@ IOAdapter::~IOAdapter() = default;
|
||||
|
||||
void IOPorts::SetGenericOutputs(std::span<const u8> bytes)
|
||||
{
|
||||
const auto bytes_to_copy = std::min(bytes.size(), GENERIC_OUTPUT_BYTE_COUNT);
|
||||
|
||||
if (bytes.size() > m_generic_output_data.size())
|
||||
if (bytes.size() > m_generic_outputs.size())
|
||||
{
|
||||
WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: Unexpected byte count: {}",
|
||||
WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "SetGenericOutputs: Unexpected byte count: {}",
|
||||
bytes.size());
|
||||
}
|
||||
|
||||
decltype(m_generic_output_data) bits_set{};
|
||||
decltype(m_generic_output_data) bits_cleared{};
|
||||
const auto bytes_to_copy = std::min(bytes.size(), GENERIC_OUTPUT_BYTE_COUNT);
|
||||
|
||||
const bool no_change = std::ranges::equal(bytes.first(bytes_to_copy),
|
||||
std::span{m_generic_outputs}.first(bytes_to_copy));
|
||||
if (no_change)
|
||||
return;
|
||||
|
||||
decltype(m_generic_outputs) bits_set{};
|
||||
decltype(m_generic_outputs) bits_cleared{};
|
||||
|
||||
for (std::size_t i = 0; i != bytes_to_copy; ++i)
|
||||
{
|
||||
bits_set[i] = u8(~m_generic_output_data[i]) & bytes[i];
|
||||
bits_cleared[i] = m_generic_output_data[i] & u8(~bytes[i]);
|
||||
bits_set[i] = u8(~m_generic_outputs[i]) & bytes[i];
|
||||
bits_cleared[i] = m_generic_outputs[i] & u8(~bytes[i]);
|
||||
|
||||
m_generic_output_data[i] = bytes[i];
|
||||
m_generic_outputs[i] = bytes[i];
|
||||
}
|
||||
|
||||
bool bits_changed = false;
|
||||
|
||||
if (std::ranges::any_of(bits_set, std::bind_front(std::not_equal_to{}, 0x00)))
|
||||
{
|
||||
bits_changed = true;
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: bits_set: {:02x}",
|
||||
fmt::join(bits_set, " "));
|
||||
}
|
||||
if (std::ranges::any_of(bits_cleared, std::bind_front(std::not_equal_to{}, 0x00)))
|
||||
{
|
||||
bits_changed = true;
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: bits_cleared: {:02x}",
|
||||
fmt::join(bits_cleared, " "));
|
||||
}
|
||||
|
||||
if (!bits_changed)
|
||||
return;
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "SetGenericOutputs: set:{:02x} clr:{:02x} ({:02x})",
|
||||
fmt::join(bits_set, " "), fmt::join(bits_cleared, " "),
|
||||
fmt::join(m_generic_outputs, " "));
|
||||
|
||||
for (auto& adapter : m_io_adapters)
|
||||
{
|
||||
@@ -80,36 +107,109 @@ void IOPorts::SetGenericOutputs(std::span<const u8> bytes)
|
||||
}
|
||||
}
|
||||
|
||||
void IOPorts::ResetGenericOutputs()
|
||||
{
|
||||
SetGenericOutputs(std::array<u8, GENERIC_OUTPUT_BYTE_COUNT>{});
|
||||
}
|
||||
|
||||
void IOAdapter::Update()
|
||||
{
|
||||
}
|
||||
|
||||
void IOAdapter::DoState(PointerWrap& p)
|
||||
{
|
||||
}
|
||||
|
||||
void IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
std::span<const u8> bits_cleared)
|
||||
{
|
||||
}
|
||||
|
||||
void MarioKartGPCommon_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
std::span<const u8> bits_cleared)
|
||||
void Common_IOAdapter::Update()
|
||||
{
|
||||
const u8 bits_changed_0 = bits_set[0] | bits_cleared[0];
|
||||
auto* const io_ports = GetIOPorts();
|
||||
const auto coin_inputs = io_ports->GetCoinInputs();
|
||||
|
||||
if (bits_changed_0 & ITEM_LIGHT_BIT)
|
||||
// Test/Service input is also possible this way, but we handle them via JVS IO instead.
|
||||
// const auto status_switches = io_ports->GetStatusSwitches();
|
||||
// status_switches[0] &= ~0x80; // Test
|
||||
// status_switches[0] &= ~0x40; // Service
|
||||
|
||||
for (int i = 0; i != IOPorts::PLAYER_COUNT; ++i)
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Item Button: {}",
|
||||
(bits_set[0] & ITEM_LIGHT_BIT) ? "ON" : "OFF");
|
||||
if (m_system.GetSerialInterface().GetDeviceType(i) != SerialInterface::SIDEVICE_AM_BASEBOARD)
|
||||
continue;
|
||||
|
||||
const GCPadStatus pad_status = Pad::GetStatus(i);
|
||||
|
||||
// Test button
|
||||
if (pad_status.switches & SWITCH_TEST)
|
||||
{
|
||||
if (AMMediaboard::GetTestMenu())
|
||||
{
|
||||
*io_ports->GetSystemInputs() |= 0x80u;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Trying to access the test menu without SegaBoot present will cause a crash.
|
||||
OSD::AddMessage("Test menu is disabled due to missing SegaBoot.", OSD::Duration::NORMAL,
|
||||
OSD::Color::RED);
|
||||
}
|
||||
}
|
||||
|
||||
const auto switch_inputs = io_ports->GetSwitchInputs(i);
|
||||
|
||||
// Service button
|
||||
if (pad_status.switches & SWITCH_SERVICE)
|
||||
switch_inputs[0] |= 0x40;
|
||||
|
||||
// Coin button
|
||||
if (pad_status.switches & SWITCH_COIN)
|
||||
coin_inputs[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (bits_changed_0 & CANCEL_LIGHT_BIT)
|
||||
void VirtuaStriker3_IOAdapter::Update()
|
||||
{
|
||||
auto* const io_ports = GetIOPorts();
|
||||
|
||||
for (int i = 0; i != IOPorts::PLAYER_COUNT; ++i)
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Cancel Button: {}",
|
||||
(bits_set[0] & CANCEL_LIGHT_BIT) ? "ON" : "OFF");
|
||||
const auto switch_inputs = io_ports->GetSwitchInputs(i);
|
||||
const GCPadStatus pad_status = Pad::GetStatus(i);
|
||||
|
||||
// Start
|
||||
if (pad_status.button & PAD_BUTTON_START)
|
||||
switch_inputs[0] |= 0x80;
|
||||
// Up
|
||||
if (pad_status.button & PAD_BUTTON_UP)
|
||||
switch_inputs[0] |= 0x20;
|
||||
// Down
|
||||
if (pad_status.button & PAD_BUTTON_DOWN)
|
||||
switch_inputs[0] |= 0x10;
|
||||
// Left
|
||||
if (pad_status.button & PAD_BUTTON_LEFT)
|
||||
switch_inputs[0] |= 0x08;
|
||||
// Right
|
||||
if (pad_status.button & PAD_BUTTON_RIGHT)
|
||||
switch_inputs[0] |= 0x04;
|
||||
// Long Pass
|
||||
if (pad_status.button & PAD_BUTTON_X)
|
||||
switch_inputs[0] |= 0x02;
|
||||
// Shoot
|
||||
if (pad_status.button & PAD_BUTTON_B)
|
||||
switch_inputs[0] |= 0x01;
|
||||
|
||||
// Short Pass
|
||||
if (pad_status.button & PAD_BUTTON_A)
|
||||
switch_inputs[1] |= 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtuaStriker4Common_IOAdapter::Update()
|
||||
{
|
||||
const auto generic_outputs = GetIOPorts()->GetGenericOutputs();
|
||||
auto* const io_ports = GetIOPorts();
|
||||
const auto generic_outputs = io_ports->GetGenericOutputs();
|
||||
|
||||
const bool is_slot_a_locked = generic_outputs[0] & SLOT_A_LOCK_BIT;
|
||||
const bool is_slot_b_locked = generic_outputs[0] & SLOT_B_LOCK_BIT;
|
||||
@@ -119,30 +219,65 @@ void VirtuaStriker4Common_IOAdapter::Update()
|
||||
|
||||
if (is_slot_a_locked)
|
||||
{
|
||||
// If slot 1 is locked, insert slot 2.
|
||||
if (m_card_reader_b->IsReadyToInsertCard())
|
||||
m_card_reader_b->InsertCard();
|
||||
// If slot A is locked, insert slot B.
|
||||
if (m_card_readers[1]->IsReadyToInsertCard())
|
||||
m_card_readers[1]->InsertCard();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If slot 1 is unlocked, remove slot 2 if unlocked.
|
||||
if (m_card_reader_b->IsCardPresent() && !is_slot_b_locked)
|
||||
m_card_reader_b->EjectCard();
|
||||
// If slot A is unlocked, remove slot B if unlocked.
|
||||
if (m_card_readers[1]->IsCardPresent() && !is_slot_b_locked)
|
||||
m_card_readers[1]->EjectCard();
|
||||
}
|
||||
|
||||
if (m_card_reader_a->IsReadyToInsertCard())
|
||||
m_card_reader_a->InsertCard();
|
||||
// Insert slot A.
|
||||
if (m_card_readers[0]->IsReadyToInsertCard())
|
||||
m_card_readers[0]->InsertCard();
|
||||
|
||||
const auto p1_inputs = GetIOPorts()->GetSwitchInputs(0);
|
||||
const auto p2_inputs = GetIOPorts()->GetSwitchInputs(1);
|
||||
const auto analog_inputs = io_ports->GetAnalogInputs();
|
||||
|
||||
// Bit 0x10 of each player's 1st byte is a card presence switch.
|
||||
Common::SetBit(p1_inputs[0], 4, m_card_reader_a->IsCardPresent());
|
||||
Common::SetBit(p2_inputs[0], 4, m_card_reader_b->IsCardPresent());
|
||||
for (int i = 0; i != IOPorts::PLAYER_COUNT; ++i)
|
||||
{
|
||||
const auto switch_inputs = io_ports->GetSwitchInputs(i);
|
||||
|
||||
// Bit 0x20 of each player's 2nd byte is some kind of "eject" sensor.
|
||||
Common::SetBit(p1_inputs[1], 5, m_card_reader_a->IsEjecting());
|
||||
Common::SetBit(p2_inputs[1], 5, m_card_reader_b->IsEjecting());
|
||||
// Bit 0x10 of each player's 1st byte is a card presence switch.
|
||||
Common::SetBit(switch_inputs[0], 4, m_card_readers[i]->IsCardPresent());
|
||||
|
||||
// Bit 0x20 of each player's 2nd byte is some kind of "eject" sensor.
|
||||
Common::SetBit(switch_inputs[1], 5, m_card_readers[i]->IsEjecting());
|
||||
|
||||
const GCPadStatus pad_status = Pad::GetStatus(i);
|
||||
|
||||
// Start
|
||||
if (pad_status.button & PAD_BUTTON_START)
|
||||
switch_inputs[0] |= 0x80;
|
||||
// Tactics (U)
|
||||
if (pad_status.button & PAD_BUTTON_LEFT)
|
||||
switch_inputs[0] |= 0x20;
|
||||
// Tactics (M)
|
||||
if (pad_status.button & PAD_BUTTON_UP)
|
||||
switch_inputs[0] |= 0x08;
|
||||
// Tactics (D)
|
||||
if (pad_status.button & PAD_BUTTON_RIGHT)
|
||||
switch_inputs[0] |= 0x04;
|
||||
// Short Pass
|
||||
if (pad_status.button & PAD_BUTTON_A)
|
||||
switch_inputs[0] |= 0x02;
|
||||
// Long Pass
|
||||
if (pad_status.button & PAD_BUTTON_X)
|
||||
switch_inputs[0] |= 0x01;
|
||||
|
||||
// Shoot
|
||||
if (pad_status.button & PAD_BUTTON_B)
|
||||
switch_inputs[1] |= 0x80;
|
||||
// Dash
|
||||
if (pad_status.button & PAD_BUTTON_Y)
|
||||
switch_inputs[1] |= 0x40;
|
||||
|
||||
// Movement
|
||||
analog_inputs[(2 * i) + 0] = Common::ExpandValue<u16>(0xff - pad_status.stickY, 8);
|
||||
analog_inputs[(2 * i) + 1] = Common::ExpandValue<u16>(pad_status.stickX, 8);
|
||||
}
|
||||
}
|
||||
|
||||
void VirtuaStriker4Common_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
@@ -172,27 +307,55 @@ void VirtuaStriker4_2006_IOAdapter::HandleGenericOutputsChanged(std::span<const
|
||||
// VirtuaStriker4_2006 triggers card ejection with JVS-IO.
|
||||
|
||||
if (bits_set[0] & SLOT_A_EJECT_BIT)
|
||||
m_card_reader_a->EjectCard();
|
||||
m_card_readers[0]->EjectCard();
|
||||
|
||||
if (bits_set[0] & SLOT_B_EJECT_BIT)
|
||||
m_card_reader_b->EjectCard();
|
||||
m_card_readers[1]->EjectCard();
|
||||
}
|
||||
|
||||
void GekitouProYakyuu_IOAdapter::Update()
|
||||
{
|
||||
// Gekitou isn't as picky as VS4. We can insert both cards simultaneously.
|
||||
for (const auto& card_reader : {m_card_reader_a, m_card_reader_b})
|
||||
auto* const io_ports = GetIOPorts();
|
||||
|
||||
for (int i = 0; i != IOPorts::PLAYER_COUNT; ++i)
|
||||
{
|
||||
if (card_reader->IsReadyToInsertCard())
|
||||
card_reader->InsertCard();
|
||||
// Gekitou isn't as picky as VS4. We can insert cards whenever.
|
||||
if (m_card_readers[i]->IsReadyToInsertCard())
|
||||
m_card_readers[i]->InsertCard();
|
||||
|
||||
const auto switch_inputs = io_ports->GetSwitchInputs(i);
|
||||
|
||||
// Bit 0x40 of each player's 2nd byte is a card presence switch.
|
||||
Common::SetBit(switch_inputs[1], 6, m_card_readers[i]->IsCardPresent());
|
||||
|
||||
const GCPadStatus pad_status = Pad::GetStatus(i);
|
||||
|
||||
// Start
|
||||
if (pad_status.button & PAD_BUTTON_START)
|
||||
switch_inputs[0] |= 0x80;
|
||||
// Up
|
||||
if (pad_status.button & PAD_BUTTON_UP)
|
||||
switch_inputs[0] |= 0x20;
|
||||
// Down
|
||||
if (pad_status.button & PAD_BUTTON_DOWN)
|
||||
switch_inputs[0] |= 0x10;
|
||||
// Left
|
||||
if (pad_status.button & PAD_BUTTON_LEFT)
|
||||
switch_inputs[0] |= 0x08;
|
||||
// Right
|
||||
if (pad_status.button & PAD_BUTTON_RIGHT)
|
||||
switch_inputs[0] |= 0x04;
|
||||
// B
|
||||
if (pad_status.button & PAD_BUTTON_A)
|
||||
switch_inputs[0] |= 0x02;
|
||||
// A
|
||||
if (pad_status.button & PAD_BUTTON_B)
|
||||
switch_inputs[0] |= 0x01;
|
||||
|
||||
// Gekitou
|
||||
if (pad_status.button & PAD_TRIGGER_L)
|
||||
switch_inputs[1] |= 0x80;
|
||||
}
|
||||
|
||||
const auto p1_inputs = GetIOPorts()->GetSwitchInputs(0);
|
||||
const auto p2_inputs = GetIOPorts()->GetSwitchInputs(1);
|
||||
|
||||
// Bit 0x40 of each player's 2nd byte is a card presence switch.
|
||||
Common::SetBit(p1_inputs[1], 6, m_card_reader_a->IsCardPresent());
|
||||
Common::SetBit(p2_inputs[1], 6, m_card_reader_b->IsCardPresent());
|
||||
}
|
||||
|
||||
void GekitouProYakyuu_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
@@ -205,10 +368,10 @@ void GekitouProYakyuu_IOAdapter::HandleGenericOutputsChanged(std::span<const u8>
|
||||
// I guess we need to treat this as an alternative to ICCardCommand::Eject.
|
||||
|
||||
if (bits_cleared[0] & SLOT_A_LOCK_BIT)
|
||||
m_card_reader_a->EjectCard();
|
||||
m_card_readers[0]->EjectCard();
|
||||
|
||||
if (bits_cleared[0] & SLOT_B_LOCK_BIT)
|
||||
m_card_reader_b->EjectCard();
|
||||
m_card_readers[1]->EjectCard();
|
||||
}
|
||||
|
||||
void KeyOfAvalon_IOAdapter::Update()
|
||||
@@ -221,6 +384,19 @@ void KeyOfAvalon_IOAdapter::Update()
|
||||
|
||||
if (m_card_reader->IsReadyToInsertCard())
|
||||
m_card_reader->InsertCard();
|
||||
|
||||
const auto switch_inputs = GetIOPorts()->GetSwitchInputs(0);
|
||||
const GCPadStatus pad_status = Pad::GetStatus(0);
|
||||
|
||||
// Debug On
|
||||
if (pad_status.button & PAD_BUTTON_START)
|
||||
switch_inputs[0] |= 0x80;
|
||||
// Switch 2
|
||||
if (pad_status.button & PAD_BUTTON_B)
|
||||
switch_inputs[0] |= 0x08;
|
||||
// Switch 1
|
||||
if (pad_status.button & PAD_BUTTON_A)
|
||||
switch_inputs[0] |= 0x04;
|
||||
}
|
||||
|
||||
} // namespace Triforce
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
class PointerWrap;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class System;
|
||||
}
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
|
||||
@@ -21,47 +25,61 @@ class IOAdapter;
|
||||
|
||||
// Triforce GPIO peripherals connect to JVS-IO and eachother in game specific ways.
|
||||
// This class hopes to handle those customizable connections.
|
||||
// TODO: Use this for other JVS-IO (Analog Input, Coin, etc.).
|
||||
class IOPorts
|
||||
{
|
||||
public:
|
||||
void Update();
|
||||
|
||||
std::span<u8> GetSwitchInputs(u32 player_index)
|
||||
{
|
||||
ASSERT(player_index < PLAYER_COUNT);
|
||||
std::span<u8> GetStatusSwitches() { return m_status_switches; }
|
||||
std::span<const u8> GetStatusSwitches() const { return m_status_switches; }
|
||||
|
||||
return std::span{m_switch_input_data}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index,
|
||||
SWITCH_INPUT_BYTES_PER_PLAYER);
|
||||
}
|
||||
std::span<const u8> GetSwitchInputs(u32 player_index) const
|
||||
{
|
||||
ASSERT(player_index < PLAYER_COUNT);
|
||||
u8* GetSystemInputs() { return &m_system_inputs; }
|
||||
const u8* GetSystemInputs() const { return &m_system_inputs; }
|
||||
|
||||
return std::span{m_switch_input_data}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index,
|
||||
SWITCH_INPUT_BYTES_PER_PLAYER);
|
||||
}
|
||||
std::span<u8> GetSwitchInputs(u32 player_index);
|
||||
std::span<const u8> GetSwitchInputs(u32 player_index) const;
|
||||
|
||||
std::span<u8> GetGenericOutputs() { return m_generic_output_data; }
|
||||
std::span<const u8> GetGenericOutputs() const { return m_generic_output_data; }
|
||||
std::span<u16> GetAnalogInputs() { return m_analog_inputs; }
|
||||
std::span<const u16> GetAnalogInputs() const { return m_analog_inputs; }
|
||||
|
||||
std::span<const u8> GetGenericOutputs() const { return m_generic_outputs; }
|
||||
|
||||
void SetGenericOutputs(std::span<const u8> bytes);
|
||||
void ResetGenericOutputs();
|
||||
|
||||
static constexpr std::size_t PLAYER_COUNT = 2;
|
||||
static constexpr std::size_t COIN_SLOT_COUNT = 2;
|
||||
|
||||
std::span<bool, COIN_SLOT_COUNT> GetCoinInputs() { return m_coin_inputs; }
|
||||
std::span<const bool, COIN_SLOT_COUNT> GetCoinInputs() const { return m_coin_inputs; }
|
||||
|
||||
void AddIOAdapter(std::unique_ptr<IOAdapter> adapter);
|
||||
|
||||
void DoState(PointerWrap& p);
|
||||
|
||||
static constexpr u16 NEUTRAL_ANALOG_VALUE = 0x8000;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<IOAdapter>> m_io_adapters;
|
||||
|
||||
static constexpr std::size_t PLAYER_COUNT = 2;
|
||||
std::array<u8, 2> m_status_switches{0xff, 0xff};
|
||||
|
||||
// The first byte in JVSIO switch input data.
|
||||
u8 m_system_inputs = 0x00;
|
||||
|
||||
static constexpr std::size_t SWITCH_INPUT_BYTES_PER_PLAYER = 2;
|
||||
|
||||
std::array<u8, PLAYER_COUNT * SWITCH_INPUT_BYTES_PER_PLAYER> m_switch_input_data{};
|
||||
std::array<u8, PLAYER_COUNT * SWITCH_INPUT_BYTES_PER_PLAYER> m_switch_inputs{};
|
||||
|
||||
static constexpr std::size_t ANALOG_INPUT_COUNT = 8;
|
||||
|
||||
std::array<u16, ANALOG_INPUT_COUNT> m_analog_inputs{};
|
||||
|
||||
static constexpr std::size_t GENERIC_OUTPUT_BYTE_COUNT = 4;
|
||||
|
||||
std::array<u8, GENERIC_OUTPUT_BYTE_COUNT> m_generic_output_data{};
|
||||
std::array<u8, GENERIC_OUTPUT_BYTE_COUNT> m_generic_outputs{};
|
||||
|
||||
std::array<bool, COIN_SLOT_COUNT> m_coin_inputs{};
|
||||
};
|
||||
|
||||
class IOAdapter
|
||||
@@ -79,6 +97,8 @@ public:
|
||||
|
||||
virtual void Update();
|
||||
|
||||
virtual void DoState(PointerWrap& p);
|
||||
|
||||
protected:
|
||||
IOPorts* GetIOPorts() { return m_io_ports; }
|
||||
|
||||
@@ -92,16 +112,21 @@ private:
|
||||
IOPorts* m_io_ports{};
|
||||
};
|
||||
|
||||
// Used for both MarioKartGP and MarioKartGP2.
|
||||
class MarioKartGPCommon_IOAdapter final : public IOAdapter
|
||||
class Common_IOAdapter final : public IOAdapter
|
||||
{
|
||||
protected:
|
||||
void HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
std::span<const u8> bits_cleared) override;
|
||||
public:
|
||||
explicit Common_IOAdapter(Core::System& system) : m_system{system} {}
|
||||
|
||||
void Update() override;
|
||||
|
||||
private:
|
||||
static constexpr u8 ITEM_LIGHT_BIT = 0x04;
|
||||
static constexpr u8 CANCEL_LIGHT_BIT = 0x08;
|
||||
Core::System& m_system;
|
||||
};
|
||||
|
||||
class VirtuaStriker3_IOAdapter final : public IOAdapter
|
||||
{
|
||||
public:
|
||||
void Update() override;
|
||||
};
|
||||
|
||||
// Common functionality for both VirtuaStriker4 and VirtuaStriker4_2006.
|
||||
@@ -109,7 +134,7 @@ class VirtuaStriker4Common_IOAdapter final : public IOAdapter
|
||||
{
|
||||
public:
|
||||
VirtuaStriker4Common_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b)
|
||||
: m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b}
|
||||
: m_card_readers{card_reader_a, card_reader_b}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -123,15 +148,14 @@ protected:
|
||||
std::span<const u8> bits_cleared) override;
|
||||
|
||||
private:
|
||||
ICCardReader* const m_card_reader_a;
|
||||
ICCardReader* const m_card_reader_b;
|
||||
std::array<ICCardReader*, IOPorts::PLAYER_COUNT> const m_card_readers;
|
||||
};
|
||||
|
||||
class VirtuaStriker4_2006_IOAdapter final : public IOAdapter
|
||||
{
|
||||
public:
|
||||
VirtuaStriker4_2006_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b)
|
||||
: m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b}
|
||||
: m_card_readers{card_reader_a, card_reader_b}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -143,15 +167,14 @@ private:
|
||||
static constexpr u8 SLOT_A_EJECT_BIT = 0x80;
|
||||
static constexpr u8 SLOT_B_EJECT_BIT = 0x20;
|
||||
|
||||
ICCardReader* const m_card_reader_a;
|
||||
ICCardReader* const m_card_reader_b;
|
||||
std::array<ICCardReader*, IOPorts::PLAYER_COUNT> const m_card_readers;
|
||||
};
|
||||
|
||||
class GekitouProYakyuu_IOAdapter final : public IOAdapter
|
||||
{
|
||||
public:
|
||||
GekitouProYakyuu_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b)
|
||||
: m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b}
|
||||
: m_card_readers{card_reader_a, card_reader_b}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -165,8 +188,7 @@ private:
|
||||
static constexpr u8 SLOT_A_LOCK_BIT = 0x40;
|
||||
static constexpr u8 SLOT_B_LOCK_BIT = 0x10;
|
||||
|
||||
ICCardReader* const m_card_reader_a;
|
||||
ICCardReader* const m_card_reader_b;
|
||||
std::array<ICCardReader*, IOPorts::PLAYER_COUNT> const m_card_readers;
|
||||
};
|
||||
|
||||
class KeyOfAvalon_IOAdapter final : public IOAdapter
|
||||
|
||||
767
Source/Core/Core/HW/Triforce/JVSIO.cpp
Normal file
767
Source/Core/Core/HW/Triforce/JVSIO.cpp
Normal file
@@ -0,0 +1,767 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Core/HW/Triforce/JVSIO.h"
|
||||
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "Common/BitUtils.h"
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/ScopeGuard.h"
|
||||
#include "Common/Swap.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr u8 JVSIO_SYNC = 0xe0;
|
||||
constexpr u8 JVSIO_MARK = 0xd0;
|
||||
|
||||
constexpr u8 JVSIO_BROADCAST_ADDRESS = 0xff;
|
||||
constexpr u8 JVSIO_HOST_ADDRESS = 0x00;
|
||||
|
||||
enum class JVSIOFeature : u8
|
||||
{
|
||||
SwitchInput = 0x01, // players, buttons, 0
|
||||
CoinInput = 0x02, // slots, 0, 0
|
||||
AnalogInput = 0x03, // channels, bits, 0
|
||||
RotaryInput = 0x04, // channels, 0, 0
|
||||
KeycodeInput = 0x05, // 0, 0, 0
|
||||
ScreenPositionInput = 0x06, // X-bits, Y-bits, channels
|
||||
MiscSwitchInput = 0x07, // SW-MSB, SW-LSB, 0
|
||||
|
||||
CardSystem = 0x10, // slots, 0, 0
|
||||
MedalHopper = 0x11, // channels, 0, 0
|
||||
GeneralPurposeOutput = 0x12, // slots, 0, 0
|
||||
AnalogOutput = 0x13, // channels, 0, 0
|
||||
CharacterOutput = 0x14, // width, height, type
|
||||
Backup = 0x15, // 0, 0, 0
|
||||
};
|
||||
|
||||
struct JVSIOFeatureSpec
|
||||
{
|
||||
JVSIOFeature feature{};
|
||||
u8 param_a{};
|
||||
u8 param_b{};
|
||||
u8 param_c{};
|
||||
};
|
||||
|
||||
enum class JVSIOCoinConditionCode : u8
|
||||
{
|
||||
Normal = 0x00,
|
||||
CoinJam = 0x01,
|
||||
CounterDisconnected = 0x02,
|
||||
Busy = 0x03,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
|
||||
enum class JVSIOStatusCode : u8
|
||||
{
|
||||
Okay = 1,
|
||||
UnknownCommand = 2,
|
||||
ChecksumError = 3,
|
||||
ResponseOverflow = 4,
|
||||
};
|
||||
|
||||
enum class JVSIOReportCode : u8
|
||||
{
|
||||
Okay = 1,
|
||||
ParameterSizeError = 2,
|
||||
ParameterDataError = 3,
|
||||
Busy = 4,
|
||||
};
|
||||
|
||||
enum class JVSIOCommand : u8
|
||||
{
|
||||
IOIdentify = 0x10,
|
||||
CommandRevision = 0x11,
|
||||
JVSRevision = 0x12,
|
||||
CommVersion = 0x13,
|
||||
FeatureCheck = 0x14,
|
||||
MainID = 0x15,
|
||||
|
||||
SwitchInput = 0x20,
|
||||
CoinInput = 0x21,
|
||||
AnalogInput = 0x22,
|
||||
RotaryInput = 0x23,
|
||||
KeycodeInput = 0x24,
|
||||
ScreenPositionInput = 0x25,
|
||||
MiscSwitchInput = 0x26,
|
||||
|
||||
RemainingPayout = 0x2e,
|
||||
DataRetransmit = 0x2f,
|
||||
CoinCounterDec = 0x30,
|
||||
PayoutCounterInc = 0x31,
|
||||
GenericOutput = 0x32, // Multiple bytes.
|
||||
AnalogOutput = 0x33,
|
||||
CharacterOutput = 0x34,
|
||||
CoinCounterInc = 0x35,
|
||||
PayoutCounterDec = 0x36,
|
||||
GenericOutputByte = 0x37, // Single byte.
|
||||
GenericOutputBit = 0x38, // Single bit.
|
||||
|
||||
NamcoCommand = 0x70,
|
||||
|
||||
Reset = 0xf0,
|
||||
SetAddress = 0xf1,
|
||||
CommMethodChange = 0xf2,
|
||||
};
|
||||
|
||||
class JVSIORequestReader
|
||||
{
|
||||
public:
|
||||
explicit JVSIORequestReader(std::span<const u8> data)
|
||||
: m_data{data.data()}, m_data_end{data.data() + data.size()}
|
||||
{
|
||||
}
|
||||
|
||||
// Returns a span of all remaining bytes.
|
||||
constexpr std::span<const u8> PeekBytes() const { return {m_data, m_data_end}; }
|
||||
|
||||
// Returns the remaining readable byte count.
|
||||
constexpr std::size_t RemainingByteCount() const { return std::size_t(m_data_end - m_data); }
|
||||
|
||||
constexpr u8 ReadByte()
|
||||
{
|
||||
DEBUG_ASSERT(RemainingByteCount() != 0);
|
||||
return *(m_data++);
|
||||
}
|
||||
|
||||
constexpr void SkipBytes(std::size_t count)
|
||||
{
|
||||
DEBUG_ASSERT(RemainingByteCount() >= count);
|
||||
m_data += count;
|
||||
}
|
||||
|
||||
private:
|
||||
const u8* m_data;
|
||||
const u8* m_data_end;
|
||||
};
|
||||
|
||||
class JVSIOResponseWriter
|
||||
{
|
||||
public:
|
||||
explicit JVSIOResponseWriter(std::vector<u8>* buffer) : m_buffer{*buffer} {}
|
||||
|
||||
void AddData(u8 value) { m_buffer.emplace_back(value); }
|
||||
|
||||
void AddData(std::span<const u8> data)
|
||||
{
|
||||
#if defined(__cpp_lib_containers_ranges)
|
||||
m_buffer.append_range(data);
|
||||
#else
|
||||
m_buffer.insert(m_buffer.end(), data.begin(), data.end());
|
||||
#endif
|
||||
}
|
||||
|
||||
void StartFrame(u8 destination_node)
|
||||
{
|
||||
m_buffer.clear();
|
||||
m_buffer.reserve(2 + 255);
|
||||
|
||||
// Note: The JVSIO_SYNC is not included here.
|
||||
AddData(destination_node);
|
||||
AddData(0); // Later becomes byte count.
|
||||
AddData(0); // Later becomes status code.
|
||||
}
|
||||
|
||||
void EndFrame(JVSIOStatusCode status_code)
|
||||
{
|
||||
std::size_t count_with_checksum = m_buffer.size() - 1;
|
||||
|
||||
if (count_with_checksum > 255)
|
||||
{
|
||||
status_code = JVSIOStatusCode::ResponseOverflow;
|
||||
m_buffer.resize(3);
|
||||
count_with_checksum = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
// Write byte count to header.
|
||||
m_buffer[1] = u8(count_with_checksum);
|
||||
m_buffer[2] = u8(status_code);
|
||||
|
||||
// Write checksum.
|
||||
const u8 checksum = std::accumulate(m_buffer.begin(), m_buffer.end(), u8{});
|
||||
AddData(checksum);
|
||||
}
|
||||
|
||||
void StartReport()
|
||||
{
|
||||
// To be filled in later with SetLastReportCode.
|
||||
m_last_report_code_index = m_buffer.size();
|
||||
m_buffer.emplace_back();
|
||||
}
|
||||
|
||||
void SetLastReportCode(JVSIOReportCode code) { m_buffer[m_last_report_code_index] = u8(code); }
|
||||
|
||||
private:
|
||||
std::vector<u8>& m_buffer;
|
||||
|
||||
std::size_t m_last_report_code_index{};
|
||||
};
|
||||
|
||||
// Attempts to decode exactly output.size() bytes.
|
||||
// On success, returns the number of escaped bytes read.
|
||||
static std::optional<std::size_t> UnescapeData(std::span<const u8> input, std::span<u8> output)
|
||||
{
|
||||
auto in = input.begin();
|
||||
const auto in_end = input.end();
|
||||
|
||||
auto out = output.begin();
|
||||
const auto out_end = output.end();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (out == out_end)
|
||||
return in - input.begin(); // Success.
|
||||
|
||||
if (in == in_end)
|
||||
return std::nullopt;
|
||||
|
||||
u8 byte_value = *(in++);
|
||||
if (byte_value == JVSIO_MARK)
|
||||
{
|
||||
if (in == in_end)
|
||||
return std::nullopt;
|
||||
|
||||
byte_value = *(in++) + 0x01;
|
||||
}
|
||||
|
||||
*(out++) = byte_value;
|
||||
}
|
||||
}
|
||||
|
||||
JVSIOBoard::JVSIOBoard(IOPorts* io_ports) : m_io_ports{io_ports}
|
||||
{
|
||||
}
|
||||
|
||||
void JVSIOBoard::Update()
|
||||
{
|
||||
// Update coin counters.
|
||||
const auto coin_inputs = m_io_ports->GetCoinInputs();
|
||||
for (std::size_t i = 0; i != coin_inputs.size(); ++i)
|
||||
{
|
||||
if (!std::exchange(m_coin_prev_states[i], coin_inputs[i]) && coin_inputs[i])
|
||||
++m_coin_counts[i];
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
const auto rx_span = GetRxByteSpan();
|
||||
|
||||
if (rx_span.empty())
|
||||
break; // Wait for more data.
|
||||
|
||||
if (rx_span[0] != JVSIO_SYNC)
|
||||
{
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Expected JVSIO_SYNC");
|
||||
ConsumeRxBytes(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::array<u8, 2 + 255> unescaped_data;
|
||||
|
||||
// Read 2 header bytes.
|
||||
const auto header_escaped_size =
|
||||
UnescapeData(rx_span.subspan(1), std::span{unescaped_data}.first(2));
|
||||
|
||||
if (!header_escaped_size.has_value())
|
||||
break; // Wait for more data.
|
||||
|
||||
const u8 destination_node = unescaped_data[0];
|
||||
const u8 payload_size = unescaped_data[1];
|
||||
|
||||
// Read remaining frame bytes.
|
||||
const auto payload_escaped_size =
|
||||
UnescapeData(rx_span.subspan(1 + *header_escaped_size),
|
||||
std::span{unescaped_data}.subspan(2, payload_size));
|
||||
|
||||
if (!payload_escaped_size.has_value())
|
||||
break; // Wait for more data.
|
||||
|
||||
Common::ScopeGuard consume_frame{
|
||||
[&] { ConsumeRxBytes(1 + *header_escaped_size + *payload_escaped_size); }};
|
||||
|
||||
if (payload_size < 1)
|
||||
{
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Empty payload");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify checksum.
|
||||
const auto range_to_checksum = std::span{unescaped_data}.first(payload_size + 1);
|
||||
const u8 expected_checksum =
|
||||
std::accumulate(range_to_checksum.begin(), range_to_checksum.end(), u8());
|
||||
if (expected_checksum != unescaped_data[payload_size + 1])
|
||||
{
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Bad checksum");
|
||||
|
||||
JVSIOResponseWriter writer{&m_last_response};
|
||||
writer.StartFrame(JVSIO_HOST_ADDRESS);
|
||||
writer.EndFrame(JVSIOStatusCode::ChecksumError);
|
||||
WriteResponse(m_last_response);
|
||||
continue;
|
||||
}
|
||||
|
||||
JVSIORequestReader request{range_to_checksum.subspan(2)};
|
||||
|
||||
if (destination_node == JVSIO_BROADCAST_ADDRESS)
|
||||
{
|
||||
ProcessBroadcastRequest(&request);
|
||||
}
|
||||
else if (destination_node == m_client_address)
|
||||
{
|
||||
ProcessUnicastRequest(&request);
|
||||
}
|
||||
else
|
||||
{
|
||||
WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "Unexpected destination node: 0x{:02x}",
|
||||
destination_node);
|
||||
}
|
||||
|
||||
if (const auto remaining = request.RemainingByteCount(); remaining > 0)
|
||||
WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "Excess request bytes: {}", remaining);
|
||||
}
|
||||
}
|
||||
|
||||
void JVSIOBoard::ProcessBroadcastRequest(JVSIORequestReader* request)
|
||||
{
|
||||
if (request->RemainingByteCount() < 1)
|
||||
return;
|
||||
|
||||
const auto cmd = JVSIOCommand(request->ReadByte());
|
||||
switch (cmd)
|
||||
{
|
||||
case JVSIOCommand::Reset:
|
||||
{
|
||||
if (request->RemainingByteCount() < 1)
|
||||
{
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Reset: ParameterSizeError");
|
||||
break;
|
||||
}
|
||||
|
||||
if (request->ReadByte() == 0xd9)
|
||||
{
|
||||
NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "Reset");
|
||||
m_client_address = 0;
|
||||
m_io_ports->GetStatusSwitches()[1] |= 0x01u; // Update "sense" line.
|
||||
}
|
||||
|
||||
// TODO: Does the real hardware reset coin counts ?
|
||||
|
||||
m_io_ports->ResetGenericOutputs();
|
||||
m_last_response.clear();
|
||||
break;
|
||||
}
|
||||
case JVSIOCommand::SetAddress:
|
||||
{
|
||||
if (request->RemainingByteCount() < 1)
|
||||
{
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "SetAddress: ParameterSizeError");
|
||||
break;
|
||||
}
|
||||
|
||||
const u8 address = request->ReadByte();
|
||||
|
||||
// Note: We don't currently support daisy chaining.
|
||||
// This message would otherwise be conditionally ignored.
|
||||
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "SetAddress: {}", address);
|
||||
m_client_address = address;
|
||||
m_io_ports->GetStatusSwitches()[1] &= ~0x01u; // Update "sense" line.
|
||||
|
||||
JVSIOResponseWriter writer{&m_last_response};
|
||||
writer.StartFrame(JVSIO_HOST_ADDRESS);
|
||||
writer.AddData(u8(JVSIOReportCode::Okay));
|
||||
writer.EndFrame(JVSIOStatusCode::Okay);
|
||||
WriteResponse(m_last_response);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "ProcessBroadcastFrame: UnknownCommand: {:02x}", u8(cmd));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void JVSIOBoard::ProcessUnicastRequest(JVSIORequestReader* request)
|
||||
{
|
||||
if (request->RemainingByteCount() > 0 &&
|
||||
JVSIOCommand(request->PeekBytes().front()) == JVSIOCommand::DataRetransmit)
|
||||
{
|
||||
request->SkipBytes(1);
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "DataRetransmit");
|
||||
WriteResponse(m_last_response);
|
||||
return;
|
||||
}
|
||||
|
||||
JVSIOResponseWriter response{&m_last_response};
|
||||
response.StartFrame(JVSIO_HOST_ADDRESS);
|
||||
|
||||
FrameContext ctx{*request, response};
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (ctx.request.RemainingByteCount() == 0)
|
||||
{
|
||||
ctx.response.EndFrame(JVSIOStatusCode::Okay);
|
||||
break;
|
||||
}
|
||||
|
||||
ctx.response.StartReport();
|
||||
|
||||
const auto cmd = JVSIOCommand(ctx.request.ReadByte());
|
||||
const auto command_result = HandleCommand(cmd, ctx);
|
||||
|
||||
// Entire frame error.
|
||||
if (!command_result.has_value())
|
||||
{
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "HandleCommand: cmd:{:02x} StatusCode:{}", u8(cmd),
|
||||
u8(command_result.error()));
|
||||
ctx.response.EndFrame(command_result.error());
|
||||
break;
|
||||
}
|
||||
|
||||
ctx.response.SetLastReportCode(*command_result);
|
||||
|
||||
// Command error.
|
||||
if (*command_result != JVSIOReportCode::Okay)
|
||||
{
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "HandleCommand: cmd:{:02x} ReportCode:{}", u8(cmd),
|
||||
u8(*command_result));
|
||||
ctx.response.EndFrame(JVSIOStatusCode::Okay);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WriteResponse(m_last_response);
|
||||
}
|
||||
|
||||
void JVSIOBoard::WriteResponse(std::span<const u8> response)
|
||||
{
|
||||
WriteTxByte(JVSIO_SYNC);
|
||||
|
||||
for (const u8 byte_value : response)
|
||||
{
|
||||
const bool needs_escaping = byte_value == JVSIO_SYNC || byte_value == JVSIO_MARK;
|
||||
if (needs_escaping)
|
||||
{
|
||||
WriteTxByte(JVSIO_MARK);
|
||||
WriteTxByte(byte_value - 0x01);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteTxByte(byte_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto JVSIOBoard::HandleCommand(JVSIOCommand cmd, FrameContext ctx) -> HandlerResponse
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case JVSIOCommand::CommandRevision:
|
||||
{
|
||||
ctx.response.AddData(0x11);
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "CommandRevision");
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::JVSRevision:
|
||||
{
|
||||
ctx.response.AddData(0x20);
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVSRevision");
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::CommVersion:
|
||||
{
|
||||
ctx.response.AddData(0x10);
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "CommVersion");
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::MainID:
|
||||
{
|
||||
const auto remaining_bytes = ctx.request.PeekBytes();
|
||||
const auto null_iter = std::ranges::find(remaining_bytes, u8{});
|
||||
|
||||
if (null_iter == remaining_bytes.end())
|
||||
return JVSIOReportCode::ParameterDataError;
|
||||
|
||||
std::string_view main_id_str{reinterpret_cast<const char*>(remaining_bytes.data()),
|
||||
reinterpret_cast<const char*>(std::to_address(null_iter))};
|
||||
ctx.request.SkipBytes(main_id_str.size() + 1);
|
||||
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "MainID: {}", main_id_str);
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::SwitchInput:
|
||||
{
|
||||
if (ctx.request.RemainingByteCount() < 2)
|
||||
return JVSIOReportCode::ParameterSizeError;
|
||||
|
||||
const u8 player_count = ctx.request.ReadByte();
|
||||
const u8 bytes_per_player = ctx.request.ReadByte();
|
||||
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "SwitchInput: player_count:{} bytes_per_player:{}",
|
||||
player_count, bytes_per_player);
|
||||
|
||||
ctx.response.AddData(*m_io_ports->GetSystemInputs());
|
||||
|
||||
for (u8 player_index = 0; player_index != player_count; ++player_index)
|
||||
{
|
||||
const auto switch_inputs = m_io_ports->GetSwitchInputs(player_index);
|
||||
const auto bytes_to_use = std::min<std::size_t>(bytes_per_player, switch_inputs.size());
|
||||
|
||||
ctx.response.AddData(switch_inputs.first(bytes_to_use));
|
||||
|
||||
// Pad data if the game requests more inputs than we have (unlikely).
|
||||
for (std::size_t i = bytes_to_use; i != bytes_per_player; ++i)
|
||||
ctx.response.AddData(u8{});
|
||||
}
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::AnalogInput:
|
||||
{
|
||||
if (ctx.request.RemainingByteCount() < 1)
|
||||
return JVSIOReportCode::ParameterSizeError;
|
||||
|
||||
const u8 channel_count = ctx.request.ReadByte();
|
||||
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "AnalogInput: channel_count:{}", channel_count);
|
||||
|
||||
const auto analog_inputs = m_io_ports->GetAnalogInputs();
|
||||
const auto inputs_to_use = std::min<std::size_t>(channel_count, analog_inputs.size());
|
||||
|
||||
for (auto value : analog_inputs.first(inputs_to_use))
|
||||
ctx.response.AddData(Common::AsU8Span(Common::swap16(value)));
|
||||
|
||||
// Pad data if the game requests more inputs than we have (unlikely).
|
||||
for (std::size_t i = inputs_to_use; i != channel_count; ++i)
|
||||
ctx.response.AddData(Common::AsU8Span(Common::swap16(IOPorts::NEUTRAL_ANALOG_VALUE)));
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::GenericOutput:
|
||||
{
|
||||
if (ctx.request.RemainingByteCount() < 1)
|
||||
return JVSIOReportCode::ParameterSizeError;
|
||||
|
||||
const u8 byte_count = ctx.request.ReadByte();
|
||||
|
||||
if (ctx.request.RemainingByteCount() < byte_count)
|
||||
return JVSIOReportCode::ParameterSizeError;
|
||||
|
||||
const auto output_span = ctx.request.PeekBytes().first(byte_count);
|
||||
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "GenericOutput: {}", fmt::join(output_span, " "));
|
||||
|
||||
m_io_ports->SetGenericOutputs(output_span);
|
||||
|
||||
ctx.request.SkipBytes(byte_count);
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::CoinInput:
|
||||
{
|
||||
if (ctx.request.RemainingByteCount() < 1)
|
||||
return JVSIOReportCode::ParameterSizeError;
|
||||
|
||||
const u8 slot_count = ctx.request.ReadByte();
|
||||
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "CoinInput: slot_count:{}", slot_count);
|
||||
|
||||
for (u8 i = 0; i != slot_count; ++i)
|
||||
{
|
||||
auto condition_code = JVSIOCoinConditionCode::Normal;
|
||||
u16 coin_count = 0;
|
||||
|
||||
if (i < m_coin_counts.size())
|
||||
{
|
||||
coin_count = m_coin_counts[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
condition_code = JVSIOCoinConditionCode::CounterDisconnected;
|
||||
}
|
||||
|
||||
// Top 2 bits contain the condition code.
|
||||
// Bottom 14 bits contain the coin count.
|
||||
const Common::BigEndianValue<u16> data{
|
||||
u16((u32(condition_code) << 14) | (coin_count & 0x3fffu))};
|
||||
|
||||
ctx.response.AddData(Common::AsU8Span(data));
|
||||
}
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::CoinCounterDec:
|
||||
case JVSIOCommand::CoinCounterInc:
|
||||
{
|
||||
if (ctx.request.RemainingByteCount() < 3)
|
||||
return JVSIOReportCode::ParameterSizeError;
|
||||
|
||||
const u8 slot = ctx.request.ReadByte();
|
||||
const u32 coin_msb = ctx.request.ReadByte();
|
||||
const u32 coin_lsb = ctx.request.ReadByte();
|
||||
|
||||
const auto adjustment =
|
||||
s32((coin_msb << 8u) | coin_lsb) * ((cmd == JVSIOCommand::CoinCounterDec) ? -1 : +1);
|
||||
|
||||
NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "CoinCounter: slot:{} adjustment:{}", slot, adjustment);
|
||||
|
||||
if (slot < m_coin_counts.size())
|
||||
m_coin_counts[slot] = std::clamp(m_coin_counts[slot] + adjustment, 0, 0x3fff);
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
default:
|
||||
return std::unexpected{JVSIOStatusCode::UnknownCommand};
|
||||
}
|
||||
}
|
||||
|
||||
// JVS board specs largely sourced from:
|
||||
// https://www.arcade-projects.com/threads/jvs-information-extractor.2071/
|
||||
|
||||
auto Namco_FCA_JVSIOBoard::HandleCommand(JVSIOCommand cmd, FrameContext ctx) -> HandlerResponse
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case JVSIOCommand::IOIdentify:
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "IOIdentify");
|
||||
ctx.response.AddData(
|
||||
Common::AsU8Span(std::span{"namco ltd.;FCA-1;Ver1.01;JPN,Multipurpose + Rotary Encoder"}));
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::FeatureCheck:
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "FeatureCheck");
|
||||
constexpr auto features = std::to_array<JVSIOFeatureSpec>({
|
||||
{JVSIOFeature::SwitchInput, 1, 16},
|
||||
{JVSIOFeature::CoinInput, 2},
|
||||
{JVSIOFeature::AnalogInput, 7},
|
||||
// Disabled to avoid unnecessary implementation of JVSIOCommand::RotaryInput
|
||||
// {JVSIOFeature::RotaryInput, 2},
|
||||
{JVSIOFeature::GeneralPurposeOutput, 6},
|
||||
{JVSIOFeature::AnalogOutput, 4},
|
||||
});
|
||||
ctx.response.AddData(Common::AsU8Span(features));
|
||||
ctx.response.AddData(u8{}); // End code
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::NamcoCommand:
|
||||
{
|
||||
if (ctx.request.RemainingByteCount() < 1)
|
||||
return JVSIOReportCode::ParameterSizeError;
|
||||
|
||||
const u8 namco_command = ctx.request.ReadByte();
|
||||
|
||||
if (namco_command == 0x18)
|
||||
{
|
||||
constexpr u32 param_size = 4;
|
||||
|
||||
if (ctx.request.RemainingByteCount() < param_size)
|
||||
return JVSIOReportCode::ParameterSizeError;
|
||||
|
||||
const auto params = ctx.request.PeekBytes().first(param_size);
|
||||
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "NAMCO_COMMAND({:02x}): {:02x}", namco_command,
|
||||
fmt::join(params, " "));
|
||||
|
||||
ctx.request.SkipBytes(param_size);
|
||||
ctx.response.AddData(0xff);
|
||||
}
|
||||
else
|
||||
{
|
||||
ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Unknown NAMCO_COMMAND: {:02x}", namco_command);
|
||||
return JVSIOReportCode::ParameterDataError;
|
||||
}
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
default:
|
||||
return JVSIOBoard::HandleCommand(cmd, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
auto Sega_837_13551_JVSIOBoard::HandleCommand(JVSIOCommand cmd, FrameContext ctx) -> HandlerResponse
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case JVSIOCommand::IOIdentify:
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "IOIdentify");
|
||||
ctx.response.AddData(
|
||||
Common::AsU8Span(std::span{"SEGA ENTERPRISES,LTD.;I/O BD JVS;837-13551 ;Ver1.00;98/10"}));
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::FeatureCheck:
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "FeatureCheck");
|
||||
constexpr auto features = std::to_array<JVSIOFeatureSpec>({
|
||||
{JVSIOFeature::SwitchInput, 2, 13},
|
||||
{JVSIOFeature::CoinInput, 2},
|
||||
{JVSIOFeature::AnalogInput, 8},
|
||||
{JVSIOFeature::GeneralPurposeOutput, 6},
|
||||
});
|
||||
ctx.response.AddData(Common::AsU8Span(features));
|
||||
ctx.response.AddData(u8{}); // End code
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
default:
|
||||
return JVSIOBoard::HandleCommand(cmd, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
auto Sega_837_13844_JVSIOBoard::HandleCommand(JVSIOCommand cmd, FrameContext ctx) -> HandlerResponse
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case JVSIOCommand::IOIdentify:
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "IOIdentify");
|
||||
ctx.response.AddData(Common::AsU8Span(
|
||||
std::span{"SEGA ENTERPRISES,LTD.;837-13844-01 I/O CNTL BD2 ;Ver1.00;99/07"}));
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
case JVSIOCommand::FeatureCheck:
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "FeatureCheck");
|
||||
constexpr auto features = std::to_array<JVSIOFeatureSpec>({
|
||||
{JVSIOFeature::SwitchInput, 2, 12},
|
||||
{JVSIOFeature::CoinInput, 2},
|
||||
{JVSIOFeature::AnalogInput, 8},
|
||||
{JVSIOFeature::GeneralPurposeOutput, 22},
|
||||
});
|
||||
ctx.response.AddData(Common::AsU8Span(features));
|
||||
ctx.response.AddData(u8{}); // End code
|
||||
|
||||
return JVSIOReportCode::Okay;
|
||||
}
|
||||
default:
|
||||
return JVSIOBoard::HandleCommand(cmd, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
void JVSIOBoard::DoState(PointerWrap& p)
|
||||
{
|
||||
SerialDevice::DoState(p);
|
||||
|
||||
p.Do(m_client_address);
|
||||
|
||||
p.Do(m_last_response);
|
||||
|
||||
p.Do(m_coin_counts);
|
||||
p.Do(m_coin_prev_states);
|
||||
}
|
||||
|
||||
} // namespace Triforce
|
||||
93
Source/Core/Core/HW/Triforce/JVSIO.h
Normal file
93
Source/Core/Core/HW/Triforce/JVSIO.h
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <expected>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
#include "Core/HW/Triforce/IOPorts.h"
|
||||
#include "Core/HW/Triforce/SerialDevice.h"
|
||||
|
||||
class PointerWrap;
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
enum class JVSIOStatusCode : u8;
|
||||
enum class JVSIOReportCode : u8;
|
||||
enum class JVSIOCommand : u8;
|
||||
|
||||
class JVSIORequestReader;
|
||||
class JVSIOResponseWriter;
|
||||
|
||||
// "JAMMA Video Standard" I/O
|
||||
class JVSIOBoard : public SerialDevice
|
||||
{
|
||||
public:
|
||||
explicit JVSIOBoard(IOPorts* io_ports);
|
||||
|
||||
void Update() override;
|
||||
|
||||
void DoState(PointerWrap& p) override;
|
||||
|
||||
protected:
|
||||
using HandlerResponse = std::expected<JVSIOReportCode, JVSIOStatusCode>;
|
||||
|
||||
struct FrameContext
|
||||
{
|
||||
JVSIORequestReader& request;
|
||||
JVSIOResponseWriter& response;
|
||||
};
|
||||
|
||||
virtual HandlerResponse HandleCommand(JVSIOCommand cmd, FrameContext ctx);
|
||||
|
||||
private:
|
||||
void ProcessBroadcastRequest(JVSIORequestReader* request);
|
||||
void ProcessUnicastRequest(JVSIORequestReader* request);
|
||||
|
||||
void WriteResponse(std::span<const u8> response);
|
||||
|
||||
// 0 == address not yet assigned.
|
||||
u8 m_client_address = 0;
|
||||
|
||||
// Note: Does not include JVSIO_SYNC.
|
||||
std::vector<u8> m_last_response;
|
||||
|
||||
std::array<u16, IOPorts::COIN_SLOT_COUNT> m_coin_counts{};
|
||||
std::array<bool, IOPorts::COIN_SLOT_COUNT> m_coin_prev_states{};
|
||||
|
||||
IOPorts* const m_io_ports;
|
||||
};
|
||||
|
||||
// Used by MarioKartGP and MarioKartGP2.
|
||||
class Namco_FCA_JVSIOBoard final : public JVSIOBoard
|
||||
{
|
||||
public:
|
||||
using JVSIOBoard::JVSIOBoard;
|
||||
|
||||
HandlerResponse HandleCommand(JVSIOCommand cmd, FrameContext ctx) override;
|
||||
};
|
||||
|
||||
// Used by VirtuaStriker4 and others.
|
||||
class Sega_837_13551_JVSIOBoard final : public JVSIOBoard
|
||||
{
|
||||
public:
|
||||
using JVSIOBoard::JVSIOBoard;
|
||||
|
||||
HandlerResponse HandleCommand(JVSIOCommand cmd, FrameContext ctx) override;
|
||||
};
|
||||
|
||||
// Used by FZeroAX (Deluxe).
|
||||
class Sega_837_13844_JVSIOBoard final : public JVSIOBoard
|
||||
{
|
||||
public:
|
||||
using JVSIOBoard::JVSIOBoard;
|
||||
|
||||
HandlerResponse HandleCommand(JVSIOCommand cmd, FrameContext ctx) override;
|
||||
};
|
||||
|
||||
} // namespace Triforce
|
||||
138
Source/Core/Core/HW/Triforce/MarioKartGP.cpp
Normal file
138
Source/Core/Core/HW/Triforce/MarioKartGP.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Core/HW/Triforce/MarioKartGP.h"
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "Common/BitUtils.h"
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/Swap.h"
|
||||
|
||||
#include "Core/HW/GCPad.h"
|
||||
|
||||
#include "InputCommon/GCPadStatus.h"
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
|
||||
void MarioKartGPCommon_IOAdapter::Update()
|
||||
{
|
||||
auto* const io_ports = GetIOPorts();
|
||||
|
||||
// TODO: Devices (e.g. Camera, Networking) can be disabled but it's not yet fully figured out.
|
||||
constexpr u8 DISABLE_DEVICES = 0xff;
|
||||
io_ports->GetStatusSwitches()[0] &= DISABLE_DEVICES;
|
||||
|
||||
const GCPadStatus pad_status = Pad::GetStatus(0);
|
||||
|
||||
const auto switch_inputs = io_ports->GetSwitchInputs(0);
|
||||
// Start
|
||||
if (pad_status.button & PAD_BUTTON_START)
|
||||
switch_inputs[0] |= 0x80;
|
||||
|
||||
// Item button
|
||||
if (pad_status.button & PAD_BUTTON_A)
|
||||
switch_inputs[1] |= 0x20;
|
||||
// VS-Cancel button
|
||||
if (pad_status.button & PAD_BUTTON_B)
|
||||
switch_inputs[1] |= 0x02;
|
||||
|
||||
const auto analog_inputs = io_ports->GetAnalogInputs();
|
||||
// Steering
|
||||
analog_inputs[0] = Common::ExpandValue<u16>(pad_status.stickX, 8);
|
||||
// Gas
|
||||
analog_inputs[1] = Common::ExpandValue<u16>(pad_status.triggerRight, 8);
|
||||
// Brake
|
||||
analog_inputs[2] = Common::ExpandValue<u16>(pad_status.triggerLeft, 8);
|
||||
}
|
||||
|
||||
void MarioKartGPCommon_IOAdapter::HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
std::span<const u8> bits_cleared)
|
||||
{
|
||||
if (bits_set[0] & 0x80u)
|
||||
m_steering_wheel->Reset();
|
||||
|
||||
const u8 bits_changed_0 = bits_set[0] | bits_cleared[0];
|
||||
|
||||
constexpr auto LED_NAMES = std::to_array<std::pair<u8, const char*>>({
|
||||
{0x04, "ITEM BUTTON"},
|
||||
{0x08, "CANCEL BUTTON"},
|
||||
});
|
||||
|
||||
for (const auto& [led_value, led_name] : LED_NAMES)
|
||||
{
|
||||
if (bits_changed_0 & led_value)
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: {}: {}", led_name,
|
||||
(bits_set[0] & led_value) ? "ON" : "OFF");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MarioKartGPSteeringWheel::Update()
|
||||
{
|
||||
constexpr std::size_t REQUEST_SIZE = 10;
|
||||
|
||||
std::size_t rx_position = 0;
|
||||
while (true)
|
||||
{
|
||||
const auto rx_span = GetRxByteSpan().subspan(rx_position);
|
||||
|
||||
if (rx_span.size() < REQUEST_SIZE)
|
||||
break; // Wait for more data.
|
||||
|
||||
ProcessRequest(rx_span.first<REQUEST_SIZE>());
|
||||
rx_position += REQUEST_SIZE;
|
||||
}
|
||||
|
||||
ConsumeRxBytes(rx_position);
|
||||
}
|
||||
|
||||
void MarioKartGPSteeringWheel::ProcessRequest(std::span<const u8> request)
|
||||
{
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Request: {:02x}", fmt::join(request, " "));
|
||||
|
||||
const u8 cmd = request[3];
|
||||
if (cmd != 0x01)
|
||||
{
|
||||
WARN_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Unknown command: {:02x}", cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
const u16 centering_force = Common::swap16(request.data() + 4);
|
||||
const u16 friction_force = Common::swap16(request.data() + 6);
|
||||
const u16 roll = Common::swap16(request.data() + 8);
|
||||
|
||||
DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: FFB: {:04x} {:04x} {:04x}", centering_force,
|
||||
friction_force, roll);
|
||||
|
||||
switch (m_init_state)
|
||||
{
|
||||
case 0:
|
||||
WriteTxBytes(std::array<u8, 3>{'E', '0', '0'}); // Error
|
||||
++m_init_state;
|
||||
break;
|
||||
case 1:
|
||||
WriteTxBytes(std::array<u8, 3>{'C', '0', '6'}); // Power Off
|
||||
++m_init_state;
|
||||
break;
|
||||
default:
|
||||
WriteTxBytes(std::array<u8, 3>{'C', '0', '1'}); // Power On
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MarioKartGPSteeringWheel::Reset()
|
||||
{
|
||||
INFO_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Reset");
|
||||
m_init_state = 0;
|
||||
}
|
||||
|
||||
void MarioKartGPSteeringWheel::DoState(PointerWrap& p)
|
||||
{
|
||||
p.Do(m_init_state);
|
||||
}
|
||||
|
||||
} // namespace Triforce
|
||||
47
Source/Core/Core/HW/Triforce/MarioKartGP.h
Normal file
47
Source/Core/Core/HW/Triforce/MarioKartGP.h
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Core/HW/Triforce/IOPorts.h"
|
||||
#include "Core/HW/Triforce/SerialDevice.h"
|
||||
|
||||
namespace Triforce
|
||||
{
|
||||
|
||||
// FFB wheel used by MarioKartGP and MarioKartGP2.
|
||||
class MarioKartGPSteeringWheel final : public SerialDevice
|
||||
{
|
||||
public:
|
||||
void Update() override;
|
||||
|
||||
void Reset();
|
||||
|
||||
void DoState(PointerWrap&) override;
|
||||
|
||||
private:
|
||||
void ProcessRequest(std::span<const u8>);
|
||||
|
||||
u8 m_init_state = 0;
|
||||
};
|
||||
|
||||
// Used for both MarioKartGP and MarioKartGP2.
|
||||
class MarioKartGPCommon_IOAdapter final : public IOAdapter
|
||||
{
|
||||
public:
|
||||
explicit MarioKartGPCommon_IOAdapter(MarioKartGPSteeringWheel* steering_wheel)
|
||||
: m_steering_wheel{steering_wheel}
|
||||
{
|
||||
}
|
||||
|
||||
void Update() override;
|
||||
|
||||
protected:
|
||||
void HandleGenericOutputsChanged(std::span<const u8> bits_set,
|
||||
std::span<const u8> bits_cleared) override;
|
||||
|
||||
private:
|
||||
MarioKartGPSteeringWheel* const m_steering_wheel;
|
||||
};
|
||||
|
||||
} // namespace Triforce
|
||||
@@ -37,7 +37,8 @@ void Touchscreen::Update()
|
||||
if (const auto input = GetRxByteSpan(); !input.empty())
|
||||
{
|
||||
// The Key of Avalon doesn't write to the device, it only reads.
|
||||
WARN_LOG_FMT(AMMEDIABOARD, "Unexpected write of {} bytes to touchscreen.", input.size());
|
||||
WARN_LOG_FMT(SERIALINTERFACE_AMBB, "Unexpected write of {} bytes to touchscreen.",
|
||||
input.size());
|
||||
ConsumeRxBytes(input.size());
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs
|
||||
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
|
||||
|
||||
// Don't forget to increase this after doing changes on the savestate system
|
||||
constexpr u32 STATE_VERSION = 182; // Last changed in PR 14482
|
||||
constexpr u32 STATE_VERSION = 183; // Last changed in PR 14501
|
||||
|
||||
// Increase this if the StateExtendedHeader definition changes
|
||||
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
|
||||
|
||||
@@ -342,8 +342,11 @@
|
||||
<ClInclude Include="Core\HW\StreamADPCM.h" />
|
||||
<ClInclude Include="Core\HW\SystemTimers.h" />
|
||||
<ClInclude Include="Core\HW\Triforce\DeckReader.h" />
|
||||
<ClInclude Include="Core\HW\Triforce\FZeroAX.h" />
|
||||
<ClInclude Include="Core\HW\Triforce\ICCardReader.h" />
|
||||
<ClInclude Include="Core\HW\Triforce\IOPorts.h" />
|
||||
<ClInclude Include="Core\HW\Triforce\JVSIO.h" />
|
||||
<ClInclude Include="Core\HW\Triforce\MarioKartGP.h" />
|
||||
<ClInclude Include="Core\HW\Triforce\SerialDevice.h" />
|
||||
<ClInclude Include="Core\HW\Triforce\Touchscreen.h" />
|
||||
<ClInclude Include="Core\HW\VideoInterface.h" />
|
||||
@@ -1047,8 +1050,11 @@
|
||||
<ClCompile Include="Core\HW\StreamADPCM.cpp" />
|
||||
<ClCompile Include="Core\HW\SystemTimers.cpp" />
|
||||
<ClCompile Include="Core\HW\Triforce\DeckReader.cpp" />
|
||||
<ClCompile Include="Core\HW\Triforce\FZeroAX.cpp" />
|
||||
<ClCompile Include="Core\HW\Triforce\ICCardReader.cpp" />
|
||||
<ClCompile Include="Core\HW\Triforce\IOPorts.cpp" />
|
||||
<ClCompile Include="Core\HW\Triforce\JVSIO.cpp" />
|
||||
<ClCompile Include="Core\HW\Triforce\MarioKartGP.cpp" />
|
||||
<ClCompile Include="Core\HW\Triforce\SerialDevice.cpp" />
|
||||
<ClCompile Include="Core\HW\Triforce\Touchscreen.cpp" />
|
||||
<ClCompile Include="Core\HW\VideoInterface.cpp" />
|
||||
|
||||
Reference in New Issue
Block a user