duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

guncon.cpp (12790B)


      1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "guncon.h"
      5 #include "gpu.h"
      6 #include "host.h"
      7 #include "system.h"
      8 
      9 #include "util/imgui_manager.h"
     10 #include "util/input_manager.h"
     11 #include "util/state_wrapper.h"
     12 
     13 #include "common/assert.h"
     14 #include "common/path.h"
     15 #include "common/string_util.h"
     16 
     17 #include "IconsPromptFont.h"
     18 
     19 #include <array>
     20 
     21 #ifdef _DEBUG
     22 #include "common/log.h"
     23 Log_SetChannel(GunCon);
     24 #endif
     25 
     26 static constexpr std::array<u8, static_cast<size_t>(GunCon::Binding::ButtonCount)> s_button_indices = {{13, 3, 14}};
     27 
     28 GunCon::GunCon(u32 index) : Controller(index)
     29 {
     30 }
     31 
     32 GunCon::~GunCon()
     33 {
     34   if (!m_cursor_path.empty())
     35   {
     36     const u32 cursor_index = GetSoftwarePointerIndex();
     37     if (cursor_index < InputManager::MAX_SOFTWARE_CURSORS)
     38       ImGuiManager::ClearSoftwareCursor(cursor_index);
     39   }
     40 }
     41 
     42 ControllerType GunCon::GetType() const
     43 {
     44   return ControllerType::GunCon;
     45 }
     46 
     47 void GunCon::Reset()
     48 {
     49   m_transfer_state = TransferState::Idle;
     50 }
     51 
     52 bool GunCon::DoState(StateWrapper& sw, bool apply_input_state)
     53 {
     54   if (!Controller::DoState(sw, apply_input_state))
     55     return false;
     56 
     57   u16 button_state = m_button_state;
     58   u16 position_x = m_position_x;
     59   u16 position_y = m_position_y;
     60   sw.Do(&button_state);
     61   sw.Do(&position_x);
     62   sw.Do(&position_y);
     63   if (apply_input_state)
     64   {
     65     m_button_state = button_state;
     66     m_position_x = position_x;
     67     m_position_y = position_y;
     68   }
     69 
     70   sw.Do(&m_transfer_state);
     71   return true;
     72 }
     73 
     74 float GunCon::GetBindState(u32 index) const
     75 {
     76   if (index >= s_button_indices.size())
     77     return 0.0f;
     78 
     79   const u32 bit = s_button_indices[index];
     80   return static_cast<float>(((m_button_state >> bit) & 1u) ^ 1u);
     81 }
     82 
     83 void GunCon::SetBindState(u32 index, float value)
     84 {
     85   const bool pressed = (value >= 0.5f);
     86   if (index == static_cast<u32>(Binding::ShootOffscreen))
     87   {
     88     if (m_shoot_offscreen != pressed)
     89     {
     90       m_shoot_offscreen = pressed;
     91       SetBindState(static_cast<u32>(Binding::Trigger), pressed);
     92     }
     93 
     94     return;
     95   }
     96   else if (index >= static_cast<u32>(Binding::ButtonCount))
     97   {
     98     if (index >= static_cast<u32>(Binding::BindingCount) || !m_has_relative_binds)
     99       return;
    100 
    101     if (m_relative_pos[index - static_cast<u32>(Binding::RelativeLeft)] != value)
    102     {
    103       m_relative_pos[index - static_cast<u32>(Binding::RelativeLeft)] = value;
    104       UpdateSoftwarePointerPosition();
    105     }
    106 
    107     return;
    108   }
    109 
    110   if (pressed)
    111     m_button_state &= ~(u16(1) << s_button_indices[static_cast<u8>(index)]);
    112   else
    113     m_button_state |= u16(1) << s_button_indices[static_cast<u8>(index)];
    114 }
    115 
    116 void GunCon::ResetTransferState()
    117 {
    118   m_transfer_state = TransferState::Idle;
    119 }
    120 
    121 bool GunCon::Transfer(const u8 data_in, u8* data_out)
    122 {
    123   static constexpr u16 ID = 0x5A63;
    124 
    125   switch (m_transfer_state)
    126   {
    127     case TransferState::Idle:
    128     {
    129       *data_out = 0xFF;
    130 
    131       if (data_in == 0x01)
    132       {
    133         m_transfer_state = TransferState::Ready;
    134         return true;
    135       }
    136       return false;
    137     }
    138 
    139     case TransferState::Ready:
    140     {
    141       if (data_in == 0x42)
    142       {
    143         *data_out = Truncate8(ID);
    144         m_transfer_state = TransferState::IDMSB;
    145         return true;
    146       }
    147 
    148       *data_out = 0xFF;
    149       return false;
    150     }
    151 
    152     case TransferState::IDMSB:
    153     {
    154       *data_out = Truncate8(ID >> 8);
    155       m_transfer_state = TransferState::ButtonsLSB;
    156       return true;
    157     }
    158 
    159     case TransferState::ButtonsLSB:
    160     {
    161       *data_out = Truncate8(m_button_state);
    162       m_transfer_state = TransferState::ButtonsMSB;
    163       return true;
    164     }
    165 
    166     case TransferState::ButtonsMSB:
    167     {
    168       *data_out = Truncate8(m_button_state >> 8);
    169       m_transfer_state = TransferState::XLSB;
    170       return true;
    171     }
    172 
    173     case TransferState::XLSB:
    174     {
    175       UpdatePosition();
    176       *data_out = Truncate8(m_position_x);
    177       m_transfer_state = TransferState::XMSB;
    178       return true;
    179     }
    180 
    181     case TransferState::XMSB:
    182     {
    183       *data_out = Truncate8(m_position_x >> 8);
    184       m_transfer_state = TransferState::YLSB;
    185       return true;
    186     }
    187 
    188     case TransferState::YLSB:
    189     {
    190       *data_out = Truncate8(m_position_y);
    191       m_transfer_state = TransferState::YMSB;
    192       return true;
    193     }
    194 
    195     case TransferState::YMSB:
    196     {
    197       *data_out = Truncate8(m_position_y >> 8);
    198       m_transfer_state = TransferState::Idle;
    199       return false;
    200     }
    201 
    202     default:
    203       UnreachableCode();
    204   }
    205 }
    206 
    207 void GunCon::UpdatePosition()
    208 {
    209   float display_x, display_y;
    210   const auto& [window_x, window_y] = (m_has_relative_binds) ? GetAbsolutePositionFromRelativeAxes() :
    211                                                               InputManager::GetPointerAbsolutePosition(m_cursor_index);
    212   g_gpu->ConvertScreenCoordinatesToDisplayCoordinates(window_x, window_y, &display_x, &display_y);
    213 
    214   // are we within the active display area?
    215   u32 tick, line;
    216   if (display_x < 0 || display_y < 0 ||
    217       !g_gpu->ConvertDisplayCoordinatesToBeamTicksAndLines(display_x, display_y, m_x_scale, &tick, &line) ||
    218       m_shoot_offscreen)
    219   {
    220     DEBUG_LOG("Lightgun out of range for window coordinates {:.0f},{:.0f}", window_x, window_y);
    221     m_position_x = 0x01;
    222     m_position_y = 0x0A;
    223     return;
    224   }
    225 
    226   // 8MHz units for X = 44100*768*11/7 = 53222400 / 8000000 = 6.6528
    227   const double divider = static_cast<double>(g_gpu->GetCRTCFrequency()) / 8000000.0;
    228   m_position_x = static_cast<u16>(static_cast<float>(tick) / static_cast<float>(divider));
    229   m_position_y = static_cast<u16>(line);
    230   DEBUG_LOG("Lightgun window coordinates {:.0f},{:.0f} -> tick {} line {} 8mhz ticks {}", display_x, display_y, tick,
    231             line, m_position_x);
    232 }
    233 
    234 std::pair<float, float> GunCon::GetAbsolutePositionFromRelativeAxes() const
    235 {
    236   const float screen_rel_x = (((m_relative_pos[1] > 0.0f) ? m_relative_pos[1] : -m_relative_pos[0]) + 1.0f) * 0.5f;
    237   const float screen_rel_y = (((m_relative_pos[3] > 0.0f) ? m_relative_pos[3] : -m_relative_pos[2]) + 1.0f) * 0.5f;
    238   return std::make_pair(screen_rel_x * ImGuiManager::GetWindowWidth(), screen_rel_y * ImGuiManager::GetWindowHeight());
    239 }
    240 
    241 bool GunCon::CanUseSoftwareCursor() const
    242 {
    243   return (InputManager::MAX_POINTER_DEVICES + m_index) < InputManager::MAX_SOFTWARE_CURSORS;
    244 }
    245 
    246 u32 GunCon::GetSoftwarePointerIndex() const
    247 {
    248   return m_has_relative_binds ? (InputManager::MAX_POINTER_DEVICES + m_index) : m_cursor_index;
    249 }
    250 
    251 void GunCon::UpdateSoftwarePointerPosition()
    252 {
    253   if (m_cursor_path.empty() || !CanUseSoftwareCursor())
    254     return;
    255 
    256   const auto& [window_x, window_y] = GetAbsolutePositionFromRelativeAxes();
    257   ImGuiManager::SetSoftwareCursorPosition(GetSoftwarePointerIndex(), window_x, window_y);
    258 }
    259 
    260 std::unique_ptr<GunCon> GunCon::Create(u32 index)
    261 {
    262   return std::make_unique<GunCon>(index);
    263 }
    264 
    265 static const Controller::ControllerBindingInfo s_binding_info[] = {
    266 #define BUTTON(name, display_name, icon_name, binding, genb)                                                           \
    267   {                                                                                                                    \
    268     name, display_name, icon_name, static_cast<u32>(binding), InputBindingInfo::Type::Button, genb                     \
    269   }
    270 #define HALFAXIS(name, display_name, icon_name, binding, genb)                                                         \
    271   {                                                                                                                    \
    272     name, display_name, icon_name, static_cast<u32>(binding), InputBindingInfo::Type::HalfAxis, genb                   \
    273   }
    274 
    275   // clang-format off
    276   {"Pointer", TRANSLATE_NOOP("GunCon", "Pointer/Aiming"), ICON_PF_MOUSE, static_cast<u32>(GunCon::Binding::ButtonCount), InputBindingInfo::Type::Pointer, GenericInputBinding::Unknown},
    277   BUTTON("Trigger", TRANSLATE_NOOP("GunCon", "Trigger"), ICON_PF_CROSS, GunCon::Binding::Trigger, GenericInputBinding::R2),
    278   BUTTON("ShootOffscreen", TRANSLATE_NOOP("GunCon", "Shoot Offscreen"), nullptr, GunCon::Binding::ShootOffscreen, GenericInputBinding::L2),
    279   BUTTON("A", TRANSLATE_NOOP("GunCon", "A"), ICON_PF_BUTTON_A, GunCon::Binding::A, GenericInputBinding::Cross),
    280   BUTTON("B", TRANSLATE_NOOP("GunCon", "B"), ICON_PF_BUTTON_B, GunCon::Binding::B, GenericInputBinding::Circle),
    281 
    282   HALFAXIS("RelativeLeft", TRANSLATE_NOOP("GunCon", "Relative Left"), ICON_PF_ANALOG_LEFT, GunCon::Binding::RelativeLeft, GenericInputBinding::Unknown),
    283   HALFAXIS("RelativeRight", TRANSLATE_NOOP("GunCon", "Relative Right"), ICON_PF_ANALOG_RIGHT, GunCon::Binding::RelativeRight, GenericInputBinding::Unknown),
    284   HALFAXIS("RelativeUp", TRANSLATE_NOOP("GunCon", "Relative Up"), ICON_PF_ANALOG_UP, GunCon::Binding::RelativeUp, GenericInputBinding::Unknown),
    285   HALFAXIS("RelativeDown", TRANSLATE_NOOP("GunCon", "Relative Down"), ICON_PF_ANALOG_DOWN, GunCon::Binding::RelativeDown, GenericInputBinding::Unknown),
    286 // clang-format on
    287 
    288 #undef BUTTON
    289 };
    290 
    291 static const SettingInfo s_settings[] = {
    292   {SettingInfo::Type::Path, "CrosshairImagePath", TRANSLATE_NOOP("GunCon", "Crosshair Image Path"),
    293    TRANSLATE_NOOP("GunCon", "Path to an image to use as a crosshair/cursor."), nullptr, nullptr, nullptr, nullptr,
    294    nullptr, nullptr, 0.0f},
    295   {SettingInfo::Type::Float, "CrosshairScale", TRANSLATE_NOOP("GunCon", "Crosshair Image Scale"),
    296    TRANSLATE_NOOP("GunCon", "Scale of crosshair image on screen."), "1.0", "0.0001", "100.0", "0.10", "%.0f%%", nullptr,
    297    100.0f},
    298   {SettingInfo::Type::String, "CrosshairColor", TRANSLATE_NOOP("GunCon", "Cursor Color"),
    299    TRANSLATE_NOOP("GunCon", "Applies a color to the chosen crosshair images, can be used for multiple players. Specify "
    300                             "in HTML/CSS format (e.g. #aabbcc)"),
    301    "#ffffff", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f},
    302   {SettingInfo::Type::Float, "XScale", TRANSLATE_NOOP("GunCon", "X Scale"),
    303    TRANSLATE_NOOP("GunCon", "Scales X coordinates relative to the center of the screen."), "1.0", "0.01", "2.0", "0.01",
    304    "%.0f%%", nullptr, 100.0f}};
    305 
    306 const Controller::ControllerInfo GunCon::INFO = {
    307   ControllerType::GunCon, "GunCon",   TRANSLATE_NOOP("ControllerType", "GunCon"),    ICON_PF_LIGHT_GUN,
    308   s_binding_info,         s_settings, Controller::VibrationCapabilities::NoVibration};
    309 
    310 void GunCon::LoadSettings(SettingsInterface& si, const char* section, bool initial)
    311 {
    312   Controller::LoadSettings(si, section, initial);
    313 
    314   m_x_scale = si.GetFloatValue(section, "XScale", 1.0f);
    315 
    316   std::string cursor_path = si.GetStringValue(section, "CrosshairImagePath");
    317   const float cursor_scale = si.GetFloatValue(section, "CrosshairScale", 1.0f);
    318   u32 cursor_color = 0xFFFFFF;
    319   if (std::string cursor_color_str = si.GetStringValue(section, "CrosshairColor", ""); !cursor_color_str.empty())
    320   {
    321     // Strip the leading hash, if it's a CSS style colour.
    322     const std::optional<u32> cursor_color_opt(StringUtil::FromChars<u32>(
    323       cursor_color_str[0] == '#' ? std::string_view(cursor_color_str).substr(1) : std::string_view(cursor_color_str),
    324       16));
    325     if (cursor_color_opt.has_value())
    326       cursor_color = cursor_color_opt.value();
    327   }
    328 
    329 #ifndef __ANDROID__
    330   if (cursor_path.empty())
    331     cursor_path = Path::Combine(EmuFolders::Resources, "images/crosshair.png");
    332 #endif
    333 
    334   const s32 prev_pointer_index = GetSoftwarePointerIndex();
    335 
    336   m_has_relative_binds = (si.ContainsValue(section, "RelativeLeft") || si.ContainsValue(section, "RelativeRight") ||
    337                           si.ContainsValue(section, "RelativeUp") || si.ContainsValue(section, "RelativeDown"));
    338   m_cursor_index =
    339     static_cast<u8>(InputManager::GetIndexFromPointerBinding(si.GetStringValue(section, "Pointer")).value_or(0));
    340 
    341   const s32 new_pointer_index = GetSoftwarePointerIndex();
    342 
    343   if (prev_pointer_index != new_pointer_index || m_cursor_path != cursor_path || m_cursor_scale != cursor_scale ||
    344       m_cursor_color != cursor_color)
    345   {
    346     if (!initial && prev_pointer_index != new_pointer_index &&
    347         static_cast<u32>(prev_pointer_index) < InputManager::MAX_SOFTWARE_CURSORS)
    348     {
    349       ImGuiManager::ClearSoftwareCursor(prev_pointer_index);
    350     }
    351 
    352     // Pointer changed, so need to update software cursor.
    353     const bool had_software_cursor = m_cursor_path.empty();
    354     m_cursor_path = std::move(cursor_path);
    355     m_cursor_scale = cursor_scale;
    356     m_cursor_color = cursor_color;
    357     if (static_cast<u32>(new_pointer_index) < InputManager::MAX_SOFTWARE_CURSORS)
    358     {
    359       if (!m_cursor_path.empty())
    360       {
    361         ImGuiManager::SetSoftwareCursor(new_pointer_index, m_cursor_path, m_cursor_scale, m_cursor_color);
    362         if (m_has_relative_binds)
    363           UpdateSoftwarePointerPosition();
    364       }
    365       else if (had_software_cursor)
    366       {
    367         ImGuiManager::ClearSoftwareCursor(new_pointer_index);
    368       }
    369     }
    370   }
    371 }