Justifier: Add controller implementation

This commit is contained in:
Stenzek 2024-04-27 21:51:03 +10:00
parent 3be02c53c1
commit d094978214
No known key found for this signature in database
16 changed files with 1180 additions and 64 deletions

View file

@ -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

View file

@ -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

View file

@ -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> 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);

View file

@ -59,6 +59,7 @@
<ClCompile Include="hotkeys.cpp" />
<ClCompile Include="imgui_overlays.cpp" />
<ClCompile Include="interrupt_controller.cpp" />
<ClCompile Include="justifier.cpp" />
<ClCompile Include="mdec.cpp" />
<ClCompile Include="memory_card.cpp" />
<ClCompile Include="memory_card_image.cpp" />
@ -138,6 +139,7 @@
<ClInclude Include="imgui_overlays.h" />
<ClInclude Include="input_types.h" />
<ClInclude Include="interrupt_controller.h" />
<ClInclude Include="justifier.h" />
<ClInclude Include="mdec.h" />
<ClInclude Include="memory_card.h" />
<ClInclude Include="memory_card_image.h" />

View file

@ -66,6 +66,7 @@
<ClCompile Include="cpu_newrec_compiler_aarch64.cpp" />
<ClCompile Include="cpu_newrec_compiler_riscv64.cpp" />
<ClCompile Include="cpu_newrec_compiler_aarch32.cpp" />
<ClCompile Include="justifier.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -138,5 +139,6 @@
<ClInclude Include="cpu_newrec_compiler_riscv64.h" />
<ClInclude Include="cpu_newrec_compiler_aarch32.h" />
<ClInclude Include="achievements_private.h" />
<ClInclude Include="justifier.h" />
</ItemGroup>
</Project>

View file

@ -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),

475
src/core/justifier.cpp Normal file
View file

@ -0,0 +1,475 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// 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 <array>
Log_SetChannel(Justifier);
// #define CHECK_TIMING 1
#ifdef CHECK_TIMING
static u32 s_irq_current_line;
#endif
static constexpr std::array<u8, static_cast<size_t>(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<Justifier*>(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<float>(((m_button_state >> bit) & 1u) ^ 1u);
}
void Justifier::SetBindState(u32 index, float value)
{
const bool pressed = (value >= 0.5f);
if (index == static_cast<u32>(Binding::ShootOffscreen))
{
if (pressed)
m_shoot_offscreen = m_shoot_offscreen ? m_shoot_offscreen : m_offscreen_oob_frames;
return;
}
else if (index >= static_cast<u32>(Binding::ButtonCount))
{
if (index >= static_cast<u32>(Binding::BindingCount) || !m_has_relative_binds)
return;
if (m_relative_pos[index - static_cast<u32>(Binding::RelativeLeft)] != value)
{
m_relative_pos[index - static_cast<u32>(Binding::RelativeLeft)] = value;
UpdateSoftwarePointerPosition();
}
return;
}
if (pressed)
m_button_state &= ~(u16(1) << s_button_indices[static_cast<u8>(index)]);
else
m_button_state |= u16(1) << s_button_indices[static_cast<u8>(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<u32>(Binding::Trigger), 1.0f);
else if (m_shoot_offscreen == m_offscreen_release_frames)
SetBindState(static_cast<u32>(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<u16>(static_cast<TickCount>(tick) +
System::ScaleTicksToOverclock(static_cast<TickCount>(m_tick_offset)));
m_irq_first_line = static_cast<u16>(std::clamp<s32>(static_cast<s32>(line) + m_first_line_offset,
static_cast<s32>(g_gpu->GetCRTCActiveStartLine()),
static_cast<s32>(g_gpu->GetCRTCActiveEndLine())));
m_irq_last_line = static_cast<u16>(std::clamp<s32>(static_cast<s32>(line) + m_last_line_offset,
static_cast<s32>(g_gpu->GetCRTCActiveStartLine()),
static_cast<s32>(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(&current_tick, &current_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<float, float> 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> Justifier::Create(u32 index)
{
return std::make_unique<Justifier>(index);
}
static const Controller::ControllerBindingInfo s_binding_info[] = {
#define BUTTON(name, display_name, icon_name, binding, genb) \
{ \
name, display_name, icon_name, static_cast<u32>(binding), InputBindingInfo::Type::Button, genb \
}
#define HALFAXIS(name, display_name, icon_name, binding, genb) \
{ \
name, display_name, icon_name, static_cast<u32>(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<u32> cursor_color_opt(StringUtil::FromChars<u32>(
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<u32>(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<u32>(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<s8>(std::clamp<int>(si.GetIntValue(section, "FirstLineOffset", DEFAULT_FIRST_LINE_OFFSET),
std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max()));
m_last_line_offset =
static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "LastLineOffset", DEFAULT_LAST_LINE_OFFSET),
std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max()));
m_tick_offset = static_cast<s16>(std::clamp<int>(si.GetIntValue(section, "TickOffset", DEFAULT_TICK_OFFSET),
std::numeric_limits<s16>::min(), std::numeric_limits<s16>::max()));
const s8 offscreen_oob_frames =
static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "OffscreenOOBFrames", DEFAULT_OFFSCREEN_OOB_FRAMES),
std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max()));
const s8 offscreen_trigger_frames =
static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "OffscreenTriggerFrames", DEFAULT_OFFSCREEN_TRIGGER_FRAMES),
std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max()));
const s8 offscreen_release_frames =
static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "OffscreenReleaseFrames", DEFAULT_OFFSCREEN_RELEASE_FRAMES),
std::numeric_limits<s8>::min(), std::numeric_limits<s8>::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;
}

108
src/core/justifier.h Normal file
View file

@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "controller.h"
#include <memory>
#include <optional>
#include <string_view>
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<Justifier> 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<float, float> 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<TimingEvent> 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;
};

View file

@ -195,6 +195,7 @@ enum class ControllerType : u8
PlayStationMouse,
NeGcon,
NeGconRumble,
Justifier,
Count
};

View file

@ -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

View file

@ -115,7 +115,7 @@
<item row="2" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -388,7 +388,7 @@
<item row="2" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -401,7 +401,7 @@
<item row="0" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -414,7 +414,7 @@
<item row="1" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -452,7 +452,7 @@
<item row="1" column="2">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -473,12 +473,12 @@
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;p&gt;By default, GunCon will use the mouse pointer. To use the mouse, you &lt;strong&gt;do not&lt;/strong&gt; need to configure any bindings apart from the trigger and buttons.&lt;/p&gt;
<string>&lt;p&gt;By default, lightguns will use the mouse pointer. To use the mouse, you &lt;strong&gt;do not&lt;/strong&gt; need to configure any bindings apart from the trigger and buttons.&lt;/p&gt;
&lt;p&gt;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 &lt;strong&gt;left unbound&lt;/strong&gt;.&lt;/p&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>

View file

@ -0,0 +1,504 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerBindingWidget_Justifier</class>
<widget class="QWidget" name="ControllerBindingWidget_Justifier">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1010</width>
<height>418</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1000</width>
<height>400</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Trigger</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Fire Offscreen</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="ShootOffscreen">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Fire</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Trigger">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="2">
<widget class="QGroupBox" name="groupBox_16">
<property name="title">
<string>Side Buttons</string>
</property>
<layout class="QGridLayout" name="gridLayout_16">
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_17">
<property name="title">
<string>Back</string>
</property>
<layout class="QGridLayout" name="gridLayout_17">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Back">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_19">
<property name="title">
<string>Start</string>
</property>
<layout class="QGridLayout" name="gridLayout_19">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Start">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_10">
<property name="title">
<string extracomment="Try to use Sony's official terminology for this. A good place to start would be in the console or the DualShock 2's manual. If this element was officially translated to your language by Sony in later DualShocks, you may use that term.">Relative Aiming</string>
</property>
<layout class="QGridLayout" name="gridLayout_11">
<item row="3" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_11">
<property name="title">
<string>Down</string>
</property>
<layout class="QGridLayout" name="gridLayout_12">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="RelativeDown">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_12">
<property name="title">
<string>Left</string>
</property>
<layout class="QGridLayout" name="gridLayout_13">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="RelativeLeft">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_13">
<property name="title">
<string>Up</string>
</property>
<layout class="QGridLayout" name="gridLayout_14">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="RelativeUp">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="2" colspan="2">
<widget class="QGroupBox" name="groupBox_14">
<property name="title">
<string>Right</string>
</property>
<layout class="QGridLayout" name="gridLayout_15">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="RelativeRight">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="2">
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>266</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="resources/resources.qrc">:/controllers/guncon.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox_15">
<property name="title">
<string>Pointer Setup</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;p&gt;By default, lightguns will use the mouse pointer. To use the mouse, you &lt;strong&gt;do not&lt;/strong&gt; need to configure any bindings apart from the trigger and buttons.&lt;/p&gt;
&lt;p&gt;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 &lt;strong&gt;left unbound&lt;/strong&gt;.&lt;/p&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InputBindingWidget</class>
<extends>QPushButton</extends>
<header>inputbindingwidgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View file

@ -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"));

View file

@ -344,6 +344,9 @@
<QtUi Include="audiostretchsettingsdialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="controllerbindingwidget_justifier.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>

View file

@ -296,6 +296,7 @@
<QtUi Include="controllerbindingwidget_negconrumble.ui" />
<QtUi Include="audioexpansionsettingsdialog.ui" />
<QtUi Include="audiostretchsettingsdialog.ui" />
<QtUi Include="controllerbindingwidget_justifier.ui" />
</ItemGroup>
<ItemGroup>
<Natvis Include="qt5.natvis" />

View file

@ -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;