diff --git a/data/resources/gamedb.yaml b/data/resources/gamedb.yaml index de23d3910..e30a52ac7 100644 --- a/data/resources/gamedb.yaml +++ b/data/resources/gamedb.yaml @@ -8679,7 +8679,7 @@ SLES-00578: name: "Area 51 (Europe) (En,Fr,De,Es)" controllers: - DigitalController - - KonamiJustifier + - Justifier - PlayStationMouse metadata: publisher: "GT Interactive / Midway" @@ -8702,7 +8702,7 @@ SLES-03783: name: "Area 51 (Europe) (En,Fr,De,Es) (Midway Classics)" controllers: - DigitalController - - KonamiJustifier + - Justifier - PlayStationMouse metadata: publisher: "GT Interactive / Midway" @@ -8725,7 +8725,7 @@ SLPS-00726: name: "Area 51 (Japan)" controllers: - DigitalController - - KonamiJustifier + - Justifier - PlayStationMouse metadata: publisher: "Soft Bank" @@ -8745,7 +8745,7 @@ SLPS-00725: name: "Area 51 (Japan) (Special Pack)" controllers: - DigitalController - - KonamiJustifier + - Justifier - PlayStationMouse metadata: publisher: "Soft Bank" @@ -8768,7 +8768,7 @@ SLUS-00164: versionTested: "0.1-986-gfc911de1" controllers: - DigitalController - - KonamiJustifier + - Justifier - PlayStationMouse metadata: publisher: "Midway" @@ -31509,7 +31509,7 @@ SLES-00292: name: "Crypt Killer (Europe)" controllers: - DigitalController - - KonamiJustifier + - Justifier metadata: publisher: "Konami" developer: "Konami" @@ -31528,7 +31528,7 @@ SLUS-00335: name: "Crypt Killer (USA)" controllers: - DigitalController - - KonamiJustifier + - Justifier metadata: publisher: "Konami" developer: "Konami" @@ -37025,7 +37025,7 @@ SLES-00445: controllers: - DigitalController - PlayStationMouse - - KonamiJustifier + - Justifier metadata: publisher: "Fox Interactive / Electronic Arts" developer: "Probe Entertainment Limited" @@ -37050,7 +37050,7 @@ SLPS-00585: controllers: - DigitalController - PlayStationMouse - - KonamiJustifier + - Justifier metadata: publisher: "Electronic Arts Victor" developer: "Probe Entertainment Limited" @@ -37073,7 +37073,7 @@ SLUS-00119: controllers: - DigitalController - PlayStationMouse - - KonamiJustifier + - Justifier metadata: publisher: "Fox Interactive" developer: "Probe Entertainment Limited" @@ -37095,7 +37095,7 @@ SLES-02746: - DigitalController - PlayStationMouse - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Fox Interactive" developer: "N-Space" @@ -37117,7 +37117,7 @@ SLES-02747: - DigitalController - PlayStationMouse - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Fox Interactive" developer: "N-Space" @@ -37139,7 +37139,7 @@ SLES-02748: - DigitalController - PlayStationMouse - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Fox Interactive" developer: "N-Space" @@ -37161,7 +37161,7 @@ SLES-02749: - DigitalController - PlayStationMouse - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Fox Interactive" developer: "N-Space" @@ -37183,7 +37183,7 @@ SLUS-01015: - DigitalController - PlayStationMouse - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Fox Interactive" developer: "N-Space" @@ -48310,7 +48310,7 @@ SLUS-00654: - AnalogController - DigitalController - GunCon - - KonamiJustifier + - Justifier - PlayStationMouse metadata: publisher: "Working Designs" @@ -63836,7 +63836,7 @@ SCPS-10038: - AnalogController - DigitalController - GunCon - - KonamiJustifier + - Justifier - PlayStationMouse metadata: publisher: "Sony" @@ -70483,7 +70483,7 @@ SLPM-86021: name: "Henry Explorers (Japan)" controllers: - DigitalController - - KonamiJustifier + - Justifier metadata: publisher: "Konami" developer: "Konami" @@ -72407,7 +72407,7 @@ SCPS-10016: name: "Horned Owl (Japan)" controllers: - DigitalController - - KonamiJustifier + - Justifier - PlayStationMouse codes: - SCPS-10016 @@ -79170,7 +79170,7 @@ SLES-00755: controllers: - DigitalController - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Gremlin Graphics" developer: "Gremlin Graphics" @@ -79193,7 +79193,7 @@ SLUS-00630: controllers: - DigitalController - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Activision" developer: "Gremlin Graphics" @@ -89788,7 +89788,7 @@ SLES-00542: name: "Lethal Enforcers (Europe)" controllers: - DigitalController - - KonamiJustifier + - Justifier metadata: publisher: "Konami" developer: "Konami" @@ -89807,7 +89807,7 @@ SLPM-86025: name: "Lethal Enforcers Deluxe Pack (Japan)" controllers: - DigitalController - - KonamiJustifier + - Justifier metadata: publisher: "Konami" developer: "Konami" @@ -89826,7 +89826,7 @@ SLUS-00293: name: "Lethal Enforcers I & II (USA)" controllers: - DigitalController - - KonamiJustifier + - Justifier metadata: publisher: "Konami" developer: "Konami" @@ -96536,7 +96536,7 @@ SLES-01001: - AnalogController - DigitalController - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Midway" developer: "Tantalus Entertainment" @@ -96563,7 +96563,7 @@ SLUS-00503: - AnalogController - DigitalController - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Midway" developer: "Tantalus Entertainment" @@ -99864,7 +99864,7 @@ SLPS-00583: name: "Mighty Hits (Japan)" controllers: - DigitalController - - KonamiJustifier + - Justifier metadata: publisher: "Altron" developer: "Altron" @@ -99885,7 +99885,7 @@ SLES-02244: - AnalogController - DigitalController - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "JVC" developer: "Altron" @@ -99908,7 +99908,7 @@ SLPS-02165: - AnalogController - DigitalController - GunCon - - KonamiJustifier + - Justifier metadata: publisher: "Altron" developer: "Altron" @@ -126754,7 +126754,7 @@ SCUS-94408: name: "Project - Horned Owl (USA)" controllers: - DigitalController - - KonamiJustifier + - Justifier - PlayStationMouse metadata: publisher: "Sony" @@ -143583,7 +143583,7 @@ SCPS-45380: controllers: - AnalogController - DigitalController - - KonamiJustifier + - Justifier metadata: publisher: "Konami" developer: "KCET" @@ -143610,7 +143610,7 @@ SLES-01514: controllers: - AnalogController - DigitalController - - KonamiJustifier + - Justifier traits: - ForceRecompilerICache metadata: @@ -143641,7 +143641,7 @@ SLPM-86192: controllers: - AnalogController - DigitalController - - KonamiJustifier + - Justifier traits: - ForceRecompilerICache metadata: @@ -143665,7 +143665,7 @@ SLPM-86498: controllers: - AnalogController - DigitalController - - KonamiJustifier + - Justifier codes: - SLPM-86498 - SLPM-87029 @@ -143692,7 +143692,7 @@ SLUS-00707: controllers: - AnalogController - DigitalController - - KonamiJustifier + - Justifier traits: - ForceRecompilerICache metadata: @@ -153685,7 +153685,7 @@ SLES-00654: - SLES-10654 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153712,7 +153712,7 @@ SLES-10654: - SLES-10654 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153739,7 +153739,7 @@ SLES-00656: - SLES-10656 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153766,7 +153766,7 @@ SLES-10656: - SLES-10656 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153793,7 +153793,7 @@ SLES-00584: - SLES-10584 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153820,7 +153820,7 @@ SLES-10584: - SLES-10584 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153847,7 +153847,7 @@ SLES-00643: - SLES-10643 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153874,7 +153874,7 @@ SLES-10643: - SLES-10643 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153901,7 +153901,7 @@ SLPS-00638: - SLPS-00639 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153928,7 +153928,7 @@ SLPS-00639: - SLPS-00639 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153958,7 +153958,7 @@ SLES-00644: versionTested: "0.1-4423-g32ab7c13" controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -153988,7 +153988,7 @@ SLES-10644: versionTested: "0.1-4423-g32ab7c13" controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -154015,7 +154015,7 @@ SLUS-00381: - SLUS-00386 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 @@ -154045,7 +154045,7 @@ SLUS-00386: - SLUS-00386 controllers: - DigitalController - - KonamiJustifier + - Justifier settings: dmaMaxSliceTicks: 200 gpuMaxRunAhead: 1 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index e56a2a96c..4710da5af 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -73,6 +73,8 @@ add_library(core imgui_overlays.h interrupt_controller.cpp interrupt_controller.h + justifier.cpp + justifier.h mdec.cpp mdec.h memory_card.cpp diff --git a/src/core/controller.cpp b/src/core/controller.cpp index 5660b258f..5847cb4f5 100644 --- a/src/core/controller.cpp +++ b/src/core/controller.cpp @@ -8,6 +8,7 @@ #include "fmt/format.h" #include "guncon.h" #include "host.h" +#include "justifier.h" #include "negcon.h" #include "negcon_rumble.h" #include "playstation_mouse.h" @@ -22,8 +23,9 @@ static const Controller::ControllerInfo s_none_info = {ControllerType::None, Controller::VibrationCapabilities::NoVibration}; static const Controller::ControllerInfo* s_controller_info[] = { - &s_none_info, &DigitalController::INFO, &AnalogController::INFO, &AnalogJoystick::INFO, - &NeGcon::INFO, &NeGconRumble::INFO, &GunCon::INFO, &PlayStationMouse::INFO, + &s_none_info, &DigitalController::INFO, &AnalogController::INFO, &AnalogJoystick::INFO, + &NeGcon::INFO, &NeGconRumble::INFO, &GunCon::INFO, &PlayStationMouse::INFO, + &Justifier::INFO, }; const char* Controller::ControllerInfo::GetDisplayName() const @@ -100,6 +102,9 @@ std::unique_ptr Controller::Create(ControllerType type, u32 index) case ControllerType::GunCon: return GunCon::Create(index); + case ControllerType::Justifier: + return Justifier::Create(index); + case ControllerType::PlayStationMouse: return PlayStationMouse::Create(index); diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index c97ae6da4..10f20d7e0 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -59,6 +59,7 @@ + @@ -138,6 +139,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 4b075aad0..5543de332 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -66,6 +66,7 @@ + @@ -138,5 +139,6 @@ + \ No newline at end of file diff --git a/src/core/guncon.cpp b/src/core/guncon.cpp index 36ea29dc3..13ffde9db 100644 --- a/src/core/guncon.cpp +++ b/src/core/guncon.cpp @@ -273,7 +273,7 @@ static const Controller::ControllerBindingInfo s_binding_info[] = { } // clang-format off - BUTTON("Trigger", TRANSLATE_NOOP("GunCon", "Trigger"), nullptr, GunCon::Binding::Trigger, GenericInputBinding::R2), + BUTTON("Trigger", TRANSLATE_NOOP("GunCon", "Trigger"), ICON_PF_CROSS, GunCon::Binding::Trigger, GenericInputBinding::R2), BUTTON("ShootOffscreen", TRANSLATE_NOOP("GunCon", "Shoot Offscreen"), nullptr, GunCon::Binding::ShootOffscreen, GenericInputBinding::L2), BUTTON("A", TRANSLATE_NOOP("GunCon", "A"), ICON_PF_BUTTON_A, GunCon::Binding::A, GenericInputBinding::Cross), BUTTON("B", TRANSLATE_NOOP("GunCon", "B"), ICON_PF_BUTTON_B, GunCon::Binding::B, GenericInputBinding::Circle), diff --git a/src/core/justifier.cpp b/src/core/justifier.cpp new file mode 100644 index 000000000..9a51c61d8 --- /dev/null +++ b/src/core/justifier.cpp @@ -0,0 +1,475 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "justifier.h" +#include "gpu.h" +#include "host.h" +#include "interrupt_controller.h" +#include "resources.h" +#include "system.h" + +#include "util/imgui_manager.h" +#include "util/input_manager.h" +#include "util/state_wrapper.h" + +#include "common/assert.h" +#include "common/log.h" +#include "common/path.h" +#include "common/string_util.h" + +#include "IconsPromptFont.h" +#include + +Log_SetChannel(Justifier); + +// #define CHECK_TIMING 1 +#ifdef CHECK_TIMING +static u32 s_irq_current_line; +#endif + +static constexpr std::array(Justifier::Binding::ButtonCount)> s_button_indices = {{15, 3, 14}}; + +Justifier::Justifier(u32 index) : Controller(index) +{ + m_irq_event = TimingEvents::CreateTimingEvent( + "Justifier IRQ", 1, 1, [](void* param, TickCount, TickCount) { static_cast(param)->IRQEvent(); }, this, + false); +} + +Justifier::~Justifier() +{ + if (!m_cursor_path.empty()) + { + const u32 cursor_index = GetSoftwarePointerIndex(); + if (cursor_index < InputManager::MAX_SOFTWARE_CURSORS) + ImGuiManager::ClearSoftwareCursor(cursor_index); + } +} + +ControllerType Justifier::GetType() const +{ + return ControllerType::Justifier; +} + +void Justifier::Reset() +{ + m_transfer_state = TransferState::Idle; +} + +bool Justifier::DoState(StateWrapper& sw, bool apply_input_state) +{ + if (!Controller::DoState(sw, apply_input_state)) + return false; + + u16 irq_first_line = m_irq_first_line; + u16 irq_last_line = m_irq_last_line; + u16 irq_tick = m_irq_tick; + u16 button_state = m_button_state; + bool shoot_offscreen = m_shoot_offscreen; + bool position_valid = m_position_valid; + + sw.Do(&irq_first_line); + sw.Do(&irq_last_line); + sw.Do(&irq_tick); + sw.Do(&button_state); + sw.Do(&shoot_offscreen); + sw.Do(&position_valid); + + if (apply_input_state) + { + m_irq_first_line = irq_first_line; + m_irq_last_line = irq_last_line; + m_irq_tick = irq_tick; + m_button_state = button_state; + m_shoot_offscreen = shoot_offscreen; + m_position_valid = position_valid; + } + + sw.Do(&m_transfer_state); + + if (sw.IsReading()) + UpdateIRQEvent(); + + return true; +} + +float Justifier::GetBindState(u32 index) const +{ + if (index >= s_button_indices.size()) + return 0.0f; + + const u32 bit = s_button_indices[index]; + return static_cast(((m_button_state >> bit) & 1u) ^ 1u); +} + +void Justifier::SetBindState(u32 index, float value) +{ + const bool pressed = (value >= 0.5f); + if (index == static_cast(Binding::ShootOffscreen)) + { + if (pressed) + m_shoot_offscreen = m_shoot_offscreen ? m_shoot_offscreen : m_offscreen_oob_frames; + + return; + } + else if (index >= static_cast(Binding::ButtonCount)) + { + if (index >= static_cast(Binding::BindingCount) || !m_has_relative_binds) + return; + + if (m_relative_pos[index - static_cast(Binding::RelativeLeft)] != value) + { + m_relative_pos[index - static_cast(Binding::RelativeLeft)] = value; + UpdateSoftwarePointerPosition(); + } + + return; + } + + if (pressed) + m_button_state &= ~(u16(1) << s_button_indices[static_cast(index)]); + else + m_button_state |= u16(1) << s_button_indices[static_cast(index)]; +} + +bool Justifier::IsTriggerPressed() const +{ + return ((m_button_state & (1u << 15)) != 0); +} + +void Justifier::ResetTransferState() +{ + m_transfer_state = TransferState::Idle; +} + +bool Justifier::Transfer(const u8 data_in, u8* data_out) +{ + static constexpr u16 ID = 0x5A31; + + switch (m_transfer_state) + { + case TransferState::Idle: + { + // ack when sent 0x01, send ID for 0x42 + if (data_in == 0x42) + { + *data_out = Truncate8(ID); + m_transfer_state = TransferState::IDMSB; + UpdatePosition(); + return true; + } + else + { + *data_out = 0xFF; + return (data_in == 0x01); + } + } + + case TransferState::IDMSB: + { + *data_out = Truncate8(ID >> 8); + m_transfer_state = TransferState::ButtonsLSB; + return true; + } + + case TransferState::ButtonsLSB: + { + *data_out = Truncate8(m_button_state); + m_transfer_state = TransferState::ButtonsMSB; + return true; + } + + case TransferState::ButtonsMSB: + { + *data_out = Truncate8(m_button_state >> 8); + m_transfer_state = TransferState::Idle; + return true; + } + + default: + { + UnreachableCode(); + } + } +} + +void Justifier::UpdatePosition() +{ + if (m_shoot_offscreen > 0) + { + if (m_shoot_offscreen == m_offscreen_trigger_frames) + SetBindState(static_cast(Binding::Trigger), 1.0f); + else if (m_shoot_offscreen == m_offscreen_release_frames) + SetBindState(static_cast(Binding::Trigger), 0.0f); + + m_shoot_offscreen--; + m_position_valid = false; + UpdateIRQEvent(); + return; + } + + float display_x, display_y; + const auto [window_x, window_y] = + (m_has_relative_binds) ? GetAbsolutePositionFromRelativeAxes() : InputManager::GetPointerAbsolutePosition(0); + g_gpu->ConvertScreenCoordinatesToDisplayCoordinates(window_x, window_y, &display_x, &display_y); + + // are we within the active display area? + u32 tick, line; + if (display_x < 0 || display_y < 0 || + !g_gpu->ConvertDisplayCoordinatesToBeamTicksAndLines(display_x, display_y, m_x_scale, &tick, &line) || + m_shoot_offscreen) + { + Log_DevFmt("Lightgun out of range for window coordinates {:.0f},{:.0f}", window_x, window_y); + m_position_valid = false; + UpdateIRQEvent(); + return; + } + + m_position_valid = true; + + m_irq_tick = static_cast(static_cast(tick) + + System::ScaleTicksToOverclock(static_cast(m_tick_offset))); + m_irq_first_line = static_cast(std::clamp(static_cast(line) + m_first_line_offset, + static_cast(g_gpu->GetCRTCActiveStartLine()), + static_cast(g_gpu->GetCRTCActiveEndLine()))); + m_irq_last_line = static_cast(std::clamp(static_cast(line) + m_last_line_offset, + static_cast(g_gpu->GetCRTCActiveStartLine()), + static_cast(g_gpu->GetCRTCActiveEndLine()))); + + Log_DevFmt("Lightgun window coordinates {},{} -> dpy {},{} -> tick {} line {} [{}-{}]", window_x, window_y, display_x, + display_y, tick, line, m_irq_first_line, m_irq_last_line); + + UpdateIRQEvent(); +} + +void Justifier::UpdateIRQEvent() +{ + // TODO: Avoid deactivate and event sort. + m_irq_event->Deactivate(); + + if (!m_position_valid) + return; + + u32 current_tick, current_line; + g_gpu->GetBeamPosition(¤t_tick, ¤t_line); + + u32 target_line; + if (current_line < m_irq_first_line || current_line >= m_irq_last_line) + target_line = m_irq_first_line; + else + target_line = current_line + 1; + + const TickCount ticks_until_pos = g_gpu->GetSystemTicksUntilTicksAndLine(m_irq_tick, target_line); + Log_DebugFmt("Triggering IRQ in {} ticks @ tick {} line {}", ticks_until_pos, m_irq_tick, target_line); + m_irq_event->Schedule(ticks_until_pos); +} + +void Justifier::IRQEvent() +{ +#ifdef CHECK_TIMING + u32 ticks, line; + g_gpu->GetBeamPosition(&ticks, &line); + + const u32 expected_line = (s_irq_current_line == m_irq_last_line) ? m_irq_first_line : (s_irq_current_line + 1); + if (line < expected_line) + Log_WarningFmt("IRQ event fired {} lines too early", expected_line - line); + else if (line > expected_line) + Log_WarningFmt("IRQ event fired {} lines too late", line - expected_line); + if (ticks < m_irq_tick) + Log_WarningFmt("IRQ event fired {} ticks too early", m_irq_tick - ticks); + else if (ticks > m_irq_tick) + Log_WarningFmt("IRQ event fired {} ticks too late", ticks - m_irq_tick); + s_irq_current_line = line; +#endif + + InterruptController::SetLineState(InterruptController::IRQ::IRQ10, true); + InterruptController::SetLineState(InterruptController::IRQ::IRQ10, false); + + UpdateIRQEvent(); +} + +// TODO: Merge all this crap with guncon + +std::pair Justifier::GetAbsolutePositionFromRelativeAxes() const +{ + const float screen_rel_x = (((m_relative_pos[1] > 0.0f) ? m_relative_pos[1] : -m_relative_pos[0]) + 1.0f) * 0.5f; + const float screen_rel_y = (((m_relative_pos[3] > 0.0f) ? m_relative_pos[3] : -m_relative_pos[2]) + 1.0f) * 0.5f; + return std::make_pair(screen_rel_x * ImGuiManager::GetWindowWidth(), screen_rel_y * ImGuiManager::GetWindowHeight()); +} + +bool Justifier::CanUseSoftwareCursor() const +{ + return (InputManager::MAX_POINTER_DEVICES + m_index) < InputManager::MAX_SOFTWARE_CURSORS; +} + +u32 Justifier::GetSoftwarePointerIndex() const +{ + return m_has_relative_binds ? (InputManager::MAX_POINTER_DEVICES + m_index) : 0; +} + +void Justifier::UpdateSoftwarePointerPosition() +{ + if (m_cursor_path.empty() || !CanUseSoftwareCursor()) + return; + + const auto& [window_x, window_y] = GetAbsolutePositionFromRelativeAxes(); + ImGuiManager::SetSoftwareCursorPosition(GetSoftwarePointerIndex(), window_x, window_y); +} + +std::unique_ptr Justifier::Create(u32 index) +{ + return std::make_unique(index); +} + +static const Controller::ControllerBindingInfo s_binding_info[] = { +#define BUTTON(name, display_name, icon_name, binding, genb) \ + { \ + name, display_name, icon_name, static_cast(binding), InputBindingInfo::Type::Button, genb \ + } +#define HALFAXIS(name, display_name, icon_name, binding, genb) \ + { \ + name, display_name, icon_name, static_cast(binding), InputBindingInfo::Type::HalfAxis, genb \ + } + + // clang-format off + BUTTON("Trigger", TRANSLATE_NOOP("Justifier", "Trigger"), ICON_PF_CROSS, Justifier::Binding::Trigger, GenericInputBinding::R2), + BUTTON("ShootOffscreen", TRANSLATE_NOOP("Justifier", "Shoot Offscreen"), nullptr, Justifier::Binding::ShootOffscreen, GenericInputBinding::L2), + BUTTON("Start", TRANSLATE_NOOP("Justifier", "Start"), ICON_PF_START, Justifier::Binding::Start, GenericInputBinding::Cross), + BUTTON("Back", TRANSLATE_NOOP("Justifier", "Back"), ICON_PF_BACK, Justifier::Binding::Back, GenericInputBinding::Circle), + + HALFAXIS("RelativeLeft", TRANSLATE_NOOP("Justifier", "Relative Left"), ICON_PF_ANALOG_LEFT, Justifier::Binding::RelativeLeft, GenericInputBinding::Unknown), + HALFAXIS("RelativeRight", TRANSLATE_NOOP("Justifier", "Relative Right"), ICON_PF_ANALOG_RIGHT, Justifier::Binding::RelativeRight, GenericInputBinding::Unknown), + HALFAXIS("RelativeUp", TRANSLATE_NOOP("Justifier", "Relative Up"), ICON_PF_ANALOG_UP, Justifier::Binding::RelativeUp, GenericInputBinding::Unknown), + HALFAXIS("RelativeDown", TRANSLATE_NOOP("Justifier", "Relative Down"), ICON_PF_ANALOG_DOWN, Justifier::Binding::RelativeDown, GenericInputBinding::Unknown), +// clang-format on + +#undef BUTTON +}; + +static const SettingInfo s_settings[] = { + {SettingInfo::Type::Path, "CrosshairImagePath", TRANSLATE_NOOP("Justifier", "Crosshair Image Path"), + TRANSLATE_NOOP("Justifier", "Path to an image to use as a crosshair/cursor."), nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, 0.0f}, + {SettingInfo::Type::Float, "CrosshairScale", TRANSLATE_NOOP("Justifier", "Crosshair Image Scale"), + TRANSLATE_NOOP("Justifier", "Scale of crosshair image on screen."), "1.0", "0.0001", "100.0", "0.10", "%.0f%%", + nullptr, 100.0f}, + {SettingInfo::Type::String, "CrosshairColor", TRANSLATE_NOOP("Justifier", "Cursor Color"), + TRANSLATE_NOOP("Justifier", + "Applies a color to the chosen crosshair images, can be used for multiple players. Specify " + "in HTML/CSS format (e.g. #aabbcc)"), + "#ffffff", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, + {SettingInfo::Type::Float, "XScale", TRANSLATE_NOOP("Justifier", "X Scale"), + TRANSLATE_NOOP("Justifier", "Scales X coordinates relative to the center of the screen."), "1.0", "0.01", "2.0", + "0.01", "%.0f%%", nullptr, 100.0f}, + {SettingInfo::Type::Integer, "FirstLineOffset", TRANSLATE_NOOP("Justifier", "Line Start Offset"), + TRANSLATE_NOOP("Justifier", + "Offset applied to lightgun vertical position that the Justifier will first trigger on."), + "-14", "-128", "127", "1", "%u", nullptr, 0.0f}, + {SettingInfo::Type::Integer, "LastLineOffset", TRANSLATE_NOOP("Justifier", "Line End Offset"), + TRANSLATE_NOOP("Justifier", "Offset applied to lightgun vertical position that the Justifier will last trigger on."), + "-8", "-128", "127", "1", "%u", nullptr, 0.0f}, + {SettingInfo::Type::Integer, "TickOffset", TRANSLATE_NOOP("Justifier", "Tick Offset"), + TRANSLATE_NOOP("Justifier", "Offset applied to lightgun horizontal position that the Justifier will trigger on."), + "50", "-1000", "1000", "1", "%u", nullptr, 0.0f}, + {SettingInfo::Type::Integer, "OffscreenOOBFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Out-Of-Bounds Frames"), + TRANSLATE_NOOP("Justifier", "Number of frames that the Justifier is pointed out-of-bounds for an off-screen shot."), + "5", "0", "80", "1", "%u", nullptr, 0.0f}, + {SettingInfo::Type::Integer, "OffscreenTriggerFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Trigger Frames"), + TRANSLATE_NOOP("Justifier", "Number of frames that the trigger is held for an off-screen shot."), "5", "0", "80", + "1", "%u", nullptr, 0.0f}, + {SettingInfo::Type::Integer, "OffscreenReleaseFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Trigger Frames"), + TRANSLATE_NOOP("Justifier", "Number of frames that the Justifier is pointed out-of-bounds after the trigger is " + "released, for an off-screen shot."), + "5", "0", "80", "1", "%u", nullptr, 0.0f}, +}; + +const Controller::ControllerInfo Justifier::INFO = {ControllerType::Justifier, + "Justifier", + TRANSLATE_NOOP("ControllerType", "Justifier"), + nullptr, + s_binding_info, + s_settings, + Controller::VibrationCapabilities::NoVibration}; + +void Justifier::LoadSettings(SettingsInterface& si, const char* section) +{ + Controller::LoadSettings(si, section); + + m_x_scale = si.GetFloatValue(section, "XScale", 1.0f); + + std::string cursor_path = si.GetStringValue(section, "CrosshairImagePath"); + const float cursor_scale = si.GetFloatValue(section, "CrosshairScale", 1.0f); + u32 cursor_color = 0xFFFFFF; + if (std::string cursor_color_str = si.GetStringValue(section, "CrosshairColor", ""); !cursor_color_str.empty()) + { + // Strip the leading hash, if it's a CSS style colour. + const std::optional cursor_color_opt(StringUtil::FromChars( + cursor_color_str[0] == '#' ? std::string_view(cursor_color_str).substr(1) : std::string_view(cursor_color_str), + 16)); + if (cursor_color_opt.has_value()) + cursor_color = cursor_color_opt.value(); + } + +#ifndef __ANDROID__ + if (cursor_path.empty()) + cursor_path = Path::Combine(EmuFolders::Resources, "images/crosshair.png"); +#endif + + const s32 prev_pointer_index = GetSoftwarePointerIndex(); + + m_has_relative_binds = (si.ContainsValue(section, "RelativeLeft") || si.ContainsValue(section, "RelativeRight") || + si.ContainsValue(section, "RelativeUp") || si.ContainsValue(section, "RelativeDown")); + + const s32 new_pointer_index = GetSoftwarePointerIndex(); + + if (prev_pointer_index != new_pointer_index || m_cursor_path != cursor_path || m_cursor_scale != cursor_scale || + m_cursor_color != cursor_color) + { + if (prev_pointer_index != new_pointer_index && + static_cast(prev_pointer_index) < InputManager::MAX_SOFTWARE_CURSORS) + { + ImGuiManager::ClearSoftwareCursor(prev_pointer_index); + } + + // Pointer changed, so need to update software cursor. + const bool had_software_cursor = m_cursor_path.empty(); + m_cursor_path = std::move(cursor_path); + m_cursor_scale = cursor_scale; + m_cursor_color = cursor_color; + if (static_cast(new_pointer_index) < InputManager::MAX_SOFTWARE_CURSORS) + { + if (!m_cursor_path.empty()) + { + ImGuiManager::SetSoftwareCursor(new_pointer_index, m_cursor_path, m_cursor_scale, m_cursor_color); + if (m_has_relative_binds) + UpdateSoftwarePointerPosition(); + } + else if (had_software_cursor) + { + ImGuiManager::ClearSoftwareCursor(new_pointer_index); + } + } + } + + m_first_line_offset = + static_cast(std::clamp(si.GetIntValue(section, "FirstLineOffset", DEFAULT_FIRST_LINE_OFFSET), + std::numeric_limits::min(), std::numeric_limits::max())); + m_last_line_offset = + static_cast(std::clamp(si.GetIntValue(section, "LastLineOffset", DEFAULT_LAST_LINE_OFFSET), + std::numeric_limits::min(), std::numeric_limits::max())); + m_tick_offset = static_cast(std::clamp(si.GetIntValue(section, "TickOffset", DEFAULT_TICK_OFFSET), + std::numeric_limits::min(), std::numeric_limits::max())); + + const s8 offscreen_oob_frames = + static_cast(std::clamp(si.GetIntValue(section, "OffscreenOOBFrames", DEFAULT_OFFSCREEN_OOB_FRAMES), + std::numeric_limits::min(), std::numeric_limits::max())); + const s8 offscreen_trigger_frames = + static_cast(std::clamp(si.GetIntValue(section, "OffscreenTriggerFrames", DEFAULT_OFFSCREEN_TRIGGER_FRAMES), + std::numeric_limits::min(), std::numeric_limits::max())); + const s8 offscreen_release_frames = + static_cast(std::clamp(si.GetIntValue(section, "OffscreenReleaseFrames", DEFAULT_OFFSCREEN_RELEASE_FRAMES), + std::numeric_limits::min(), std::numeric_limits::max())); + m_offscreen_oob_frames = offscreen_oob_frames + offscreen_trigger_frames + offscreen_release_frames; + m_offscreen_trigger_frames = m_offscreen_oob_frames - offscreen_trigger_frames; + m_offscreen_release_frames = m_offscreen_trigger_frames - offscreen_release_frames; +} diff --git a/src/core/justifier.h b/src/core/justifier.h new file mode 100644 index 000000000..ec19db955 --- /dev/null +++ b/src/core/justifier.h @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once +#include "controller.h" +#include +#include +#include + +class TimingEvent; + +class Justifier final : public Controller +{ +public: + enum class Binding : u8 + { + Trigger = 0, + Start = 1, + Back = 2, + ShootOffscreen = 3, + ButtonCount = 4, + + RelativeLeft = 4, + RelativeRight = 5, + RelativeUp = 6, + RelativeDown = 7, + BindingCount = 8, + }; + + static const Controller::ControllerInfo INFO; + + Justifier(u32 index); + ~Justifier() override; + + static std::unique_ptr Create(u32 index); + + ControllerType GetType() const override; + + void Reset() override; + bool DoState(StateWrapper& sw, bool apply_input_state) override; + + void LoadSettings(SettingsInterface& si, const char* section) override; + + float GetBindState(u32 index) const override; + void SetBindState(u32 index, float value) override; + + void ResetTransferState() override; + bool Transfer(const u8 data_in, u8* data_out) override; + +private: + bool IsTriggerPressed() const; + void UpdatePosition(); + void UpdateIRQEvent(); + void IRQEvent(); + + std::pair GetAbsolutePositionFromRelativeAxes() const; + bool CanUseSoftwareCursor() const; + u32 GetSoftwarePointerIndex() const; + void UpdateSoftwarePointerPosition(); + + enum class TransferState : u8 + { + Idle, + IDMSB, + ButtonsLSB, + ButtonsMSB, + XLSB, + XMSB, + YLSB, + YMSB + }; + + static constexpr s8 DEFAULT_FIRST_LINE_OFFSET = -12; + static constexpr s8 DEFAULT_LAST_LINE_OFFSET = -6; + static constexpr s16 DEFAULT_TICK_OFFSET = 50; + static constexpr u8 DEFAULT_OFFSCREEN_OOB_FRAMES = 5; + static constexpr u8 DEFAULT_OFFSCREEN_TRIGGER_FRAMES = 5; + static constexpr u8 DEFAULT_OFFSCREEN_RELEASE_FRAMES = 5; + + std::unique_ptr m_irq_event; + + s8 m_first_line_offset = 0; + s8 m_last_line_offset = 0; + s16 m_tick_offset = 0; + + u8 m_offscreen_oob_frames = 0; + u8 m_offscreen_trigger_frames = 0; + u8 m_offscreen_release_frames = 0; + + u16 m_irq_first_line = 0; + u16 m_irq_last_line = 0; + u16 m_irq_tick = 0; + + // buttons are active low + u16 m_button_state = UINT16_C(0xFFFF); + u8 m_shoot_offscreen = 0; + bool m_position_valid = false; + + TransferState m_transfer_state = TransferState::Idle; + + bool m_has_relative_binds = false; + float m_relative_pos[4] = {}; + + std::string m_cursor_path; + float m_cursor_scale = 1.0f; + u32 m_cursor_color = 0xFFFFFFFFu; + float m_x_scale = 1.0f; +}; diff --git a/src/core/types.h b/src/core/types.h index 66f527bcf..1227afbd6 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -195,6 +195,7 @@ enum class ControllerType : u8 PlayStationMouse, NeGcon, NeGconRumble, + Justifier, Count }; diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 266b5f31b..32cba8593 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -48,6 +48,7 @@ set(SRCS controllerbindingwidget_analog_joystick.ui controllerbindingwidget_digital_controller.ui controllerbindingwidget_guncon.ui + controllerbindingwidget_justifier.ui controllerbindingwidget_mouse.ui controllerbindingwidget_negcon.ui controllerbindingwidget_negconrumble.ui diff --git a/src/duckstation-qt/controllerbindingwidget_guncon.ui b/src/duckstation-qt/controllerbindingwidget_guncon.ui index a6ea54478..882b43e46 100644 --- a/src/duckstation-qt/controllerbindingwidget_guncon.ui +++ b/src/duckstation-qt/controllerbindingwidget_guncon.ui @@ -115,7 +115,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -388,7 +388,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -401,7 +401,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -414,7 +414,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -452,7 +452,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -473,12 +473,12 @@ - <p>By default, GunCon will use the mouse pointer. To use the mouse, you <strong>do not</strong> need to configure any bindings apart from the trigger and buttons.</p> + <p>By default, lightguns will use the mouse pointer. To use the mouse, you <strong>do not</strong> need to configure any bindings apart from the trigger and buttons.</p> <p>If you want to use a controller, or lightgun which simulates a controller instead of a mouse, then you should bind it to Relative Aiming. Otherwise, Relative Aiming should be <strong>left unbound</strong>.</p> - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop true diff --git a/src/duckstation-qt/controllerbindingwidget_justifier.ui b/src/duckstation-qt/controllerbindingwidget_justifier.ui new file mode 100644 index 000000000..e3e793530 --- /dev/null +++ b/src/duckstation-qt/controllerbindingwidget_justifier.ui @@ -0,0 +1,504 @@ + + + ControllerBindingWidget_Justifier + + + + 0 + 0 + 1010 + 418 + + + + + 0 + 0 + + + + + 1000 + 400 + + + + + + + Trigger + + + + + + Fire Offscreen + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + PushButton + + + + + + + + + + Fire + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + PushButton + + + + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + Side Buttons + + + + + + Back + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + PushButton + + + + + + + + + + Start + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + PushButton + + + + + + + + + + + + + Relative Aiming + + + + + + Down + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + PushButton + + + + + + + + + + Left + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + PushButton + + + + + + + + + + Up + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + PushButton + + + + + + + + + + Right + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + PushButton + + + + + + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 400 + 266 + + + + + + + :/controllers/guncon.svg + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + Pointer Setup + + + + + + <p>By default, lightguns will use the mouse pointer. To use the mouse, you <strong>do not</strong> need to configure any bindings apart from the trigger and buttons.</p> + +<p>If you want to use a controller, or lightgun which simulates a controller instead of a mouse, then you should bind it to Relative Aiming. Otherwise, Relative Aiming should be <strong>left unbound</strong>.</p> + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + true + + + + + + + + + + + InputBindingWidget + QPushButton +
inputbindingwidgets.h
+
+
+ + + + +
diff --git a/src/duckstation-qt/controllerbindingwidgets.cpp b/src/duckstation-qt/controllerbindingwidgets.cpp index 44d58a6e6..08db8f0e8 100644 --- a/src/duckstation-qt/controllerbindingwidgets.cpp +++ b/src/duckstation-qt/controllerbindingwidgets.cpp @@ -8,10 +8,12 @@ #include "qtutils.h" #include "settingswindow.h" #include "settingwidgetbinder.h" + #include "ui_controllerbindingwidget_analog_controller.h" #include "ui_controllerbindingwidget_analog_joystick.h" #include "ui_controllerbindingwidget_digital_controller.h" #include "ui_controllerbindingwidget_guncon.h" +#include "ui_controllerbindingwidget_justifier.h" #include "ui_controllerbindingwidget_mouse.h" #include "ui_controllerbindingwidget_negcon.h" #include "ui_controllerbindingwidget_negconrumble.h" @@ -175,6 +177,15 @@ void ControllerBindingWidget::populateWidgets() } break; + case ControllerType::Justifier: + { + Ui::ControllerBindingWidget_Justifier ui; + ui.setupUi(m_bindings_widget); + bindBindingWidgets(m_bindings_widget); + m_icon = QIcon::fromTheme(QStringLiteral("guncon-line")); + } + break; + case ControllerType::None: { m_icon = QIcon::fromTheme(QStringLiteral("controller-strike-line")); diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index c6fee9304..01e89f29a 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -344,6 +344,9 @@ Document + + Document + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index c69caafd2..2277aa219 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -296,6 +296,7 @@ + diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp index 44d2b0857..8dbc6692a 100644 --- a/src/util/imgui_manager.cpp +++ b/src/util/imgui_manager.cpp @@ -575,11 +575,12 @@ bool ImGuiManager::AddIconFonts(float size) 0xf5aa, 0xf5aa, 0xf5e7, 0xf5e7, 0xf65d, 0xf65e, 0xf6a9, 0xf6a9, 0xf6cf, 0xf6cf, 0xf70c, 0xf70c, 0xf794, 0xf794, 0xf7a0, 0xf7a0, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815, 0xf818, 0xf818, 0xf84c, 0xf84c, 0xf8cc, 0xf8cc, 0x0, 0x0}; - static constexpr ImWchar range_pf[] = { - 0x2196, 0x2199, 0x219e, 0x21a1, 0x21b0, 0x21b3, 0x21ba, 0x21c3, 0x21c7, 0x21ca, 0x21d0, 0x21d4, 0x21dc, 0x21dd, - 0x21e0, 0x21e3, 0x21ed, 0x21ee, 0x21f7, 0x21f8, 0x21fa, 0x21fb, 0x227a, 0x227f, 0x2284, 0x2284, 0x235e, 0x235e, - 0x2360, 0x2361, 0x2364, 0x2366, 0x23b2, 0x23b4, 0x23f4, 0x23f7, 0x2427, 0x243a, 0x243c, 0x243e, 0x2460, 0x246b, - 0x24f5, 0x24fd, 0x24ff, 0x24ff, 0x278a, 0x278e, 0x27fc, 0x27fc, 0xe001, 0xe001, 0xff21, 0xff3a, 0x0, 0x0}; + static constexpr ImWchar range_pf[] = {0x2196, 0x2199, 0x219e, 0x21a1, 0x21b0, 0x21b3, 0x21ba, 0x21c3, 0x21c7, 0x21ca, + 0x21d0, 0x21d4, 0x21dc, 0x21dd, 0x21e0, 0x21e3, 0x21ed, 0x21ee, 0x21f7, 0x21f8, + 0x21fa, 0x21fb, 0x227a, 0x227f, 0x2284, 0x2284, 0x235e, 0x235e, 0x2360, 0x2361, + 0x2364, 0x2366, 0x23b2, 0x23b4, 0x23ce, 0x23ce, 0x23f4, 0x23f7, 0x2427, 0x243a, + 0x243c, 0x243e, 0x2460, 0x246b, 0x24f5, 0x24fd, 0x24ff, 0x24ff, 0x2717, 0x2717, + 0x278a, 0x278e, 0x27fc, 0x27fc, 0xe001, 0xe001, 0xff21, 0xff3a, 0x0, 0x0}; { ImFontConfig cfg;