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 }