diff --git a/pcsx2/USB/usb-pad/usb-train.cpp b/pcsx2/USB/usb-pad/usb-train.cpp index 4fadd7dcce..9ef6939fa2 100644 --- a/pcsx2/USB/usb-pad/usb-train.cpp +++ b/pcsx2/USB/usb-pad/usb-train.cpp @@ -30,6 +30,7 @@ namespace usb_pad static const char* subtypes[] = { TRANSLATE_NOOP("USB", "Type 2"), TRANSLATE_NOOP("USB", "Shinkansen"), + TRANSLATE_NOOP("USB", "Ryojōhen"), }; return subtypes; } @@ -51,6 +52,11 @@ namespace usb_pad CID_TC_SELECT, CID_TC_START, + // Ryojouhen controller has 7 buttons, map L/R onto existing indexes. + CID_TC_CAMERA, + CID_TC_L = CID_TC_C, + CID_TC_R = CID_TC_D, + BUTTONS_OFFSET = CID_TC_B, }; @@ -78,6 +84,26 @@ namespace usb_pad return bindings; } + case TRAIN_RYOJOUHEN: + { + static constexpr const InputBindingInfo bindings[] = { + {"Power", TRANSLATE_NOOP("USB", "Power"), ICON_PF_LEFT_ANALOG_DOWN, InputBindingInfo::Type::Axis, CID_TC_POWER, GenericInputBinding::LeftStickDown}, + {"Brake", TRANSLATE_NOOP("USB", "Brake"), ICON_PF_LEFT_ANALOG_UP, InputBindingInfo::Type::Axis, CID_TC_BRAKE, GenericInputBinding::LeftStickUp}, + {"Up", TRANSLATE_NOOP("USB", "D-Pad Up"), ICON_PF_DPAD_UP, InputBindingInfo::Type::Button, CID_TC_UP, GenericInputBinding::DPadUp}, + {"Down", TRANSLATE_NOOP("USB", "D-Pad Down"), ICON_PF_DPAD_DOWN, InputBindingInfo::Type::Button, CID_TC_DOWN, GenericInputBinding::DPadDown}, + {"Left", TRANSLATE_NOOP("USB", "D-Pad Left"), ICON_PF_DPAD_LEFT, InputBindingInfo::Type::Button, CID_TC_LEFT, GenericInputBinding::DPadLeft}, + {"Right", TRANSLATE_NOOP("USB", "D-Pad Right"), ICON_PF_DPAD_RIGHT, InputBindingInfo::Type::Button, CID_TC_RIGHT, GenericInputBinding::DPadRight}, + {"Announce", TRANSLATE_NOOP("USB", "Announce"), ICON_PF_KEY_B, InputBindingInfo::Type::Button, CID_TC_A, GenericInputBinding::Circle}, + {"Horn", TRANSLATE_NOOP("USB", "Horn"), ICON_PF_KEY_A, InputBindingInfo::Type::Button, CID_TC_B, GenericInputBinding::Cross}, + {"LeftDoor", TRANSLATE_NOOP("USB", "Left Door"), ICON_PF_KEY_L, InputBindingInfo::Type::Button, CID_TC_L, GenericInputBinding::Square}, + {"RightDoor", TRANSLATE_NOOP("USB", "Right Door"), ICON_PF_KEY_R, InputBindingInfo::Type::Button, CID_TC_R, GenericInputBinding::Triangle}, + {"Camera", TRANSLATE_NOOP("USB", "Camera Button"), ICON_PF_CAMERA, InputBindingInfo::Type::Button, CID_TC_CAMERA, GenericInputBinding::R1}, + {"Select", TRANSLATE_NOOP("USB", "Select"), ICON_PF_SELECT_SHARE, InputBindingInfo::Type::Button, CID_TC_SELECT, GenericInputBinding::Select}, + {"Start", TRANSLATE_NOOP("USB", "Start"), ICON_PF_START, InputBindingInfo::Type::Button, CID_TC_START, GenericInputBinding::Start}, + }; + + return bindings; + } default: break; } @@ -151,6 +177,7 @@ namespace usb_pad case CID_TC_D: case CID_TC_SELECT: case CID_TC_START: + case CID_TC_CAMERA: { return (button_at(s->data.buttons, bind_index) != 0u) ? 1.0f : 0.0f; } @@ -196,6 +223,7 @@ namespace usb_pad case CID_TC_D: case CID_TC_SELECT: case CID_TC_START: + case CID_TC_CAMERA: { const u32 mask = button_mask(bind_index); if (value >= 0.5f) @@ -345,7 +373,44 @@ namespace usb_pad return notches[std::size(notches) - 1].second; } + static u8 dct03_power(u8 value) + { + // (N) 0x00 0x3C 0x78 0xB4 0xF0 (P4) + static std::pair const notches[] = { + // { control_in, emulated_out }, + {0xC0, 0xF0}, + {0x90, 0xB4}, + {0x50, 0x78}, + {0x30, 0x3C}, + {0x00, 0x00}, + }; + + for (const auto& x : notches) + { + if (value >= x.first) + return x.second; + } + return notches[std::size(notches) - 1].second; + } + static u8 dct03_brake(u8 value) + { + // Depending on the game, this device presents in either Non Self-Lapping or Self-Lapping mode. + // NSL Release Maintain Increase Emergency + // 0x23-0x64 0x65-0x89 0x8A-0xD6 0xD7 + // SL Released B1 B2 B3 B4 B5 B6 EB + // 0x23-0x2A 0x2B-0x3C 0x3D-0x4E 0x4F-0x63 0x64-0x8A 0x8B-0xB0 0xB1-0xD6 0xD7 + if (0x18 >= value) + return 0x23; + if (value >= 0xF8) + return 0xD7; + // We've trimmed 0x20 (0x8 for EB, 0x18 for release) leaving us with ~0xE0. + // 0xD7-0x23=0xB3 represents about 80% of the number space of 0xE0, so compress the remaining input values into that range. + u8 offset = 0x9 + value / 85; + return value / 5 * 4 + offset; + } + #define get_ab(buttons) (button_at(buttons, CID_TC_A) | button_at(buttons, CID_TC_B)) +#define get_cd(buttons) (button_at(buttons, CID_TC_C) | button_at(buttons, CID_TC_D)) #define swap_cd(buttons) ((button_at(buttons, CID_TC_C) << 1) | (button_at(buttons, CID_TC_D) >> 1)) #define get_ss(buttons) (button_at(buttons, CID_TC_START) | button_at(buttons, CID_TC_SELECT)) @@ -355,6 +420,10 @@ namespace usb_pad { return ((get_ab(buttons) << 2) | (swap_cd(buttons) >> 2) | get_ss(buttons)); } + constexpr u8 dct03_buttons(u8 buttons) + { + return (get_ab(buttons) | (button_at(buttons, CID_TC_CAMERA) >> 4) | ((get_cd(buttons) | get_ss(buttons)) << 1)); + } static void train_handle_data(USBDevice* dev, USBPacket* p) { @@ -394,6 +463,17 @@ namespace usb_pad usb_packet_copy(p, &out, sizeof(out)); break; } + case TRAIN_RYOJOUHEN: + { + TrainConData_Ryojouhen out = {}; + out.brake = dct03_brake(s->data.brake); + out.power = dct03_power(s->data.power); + out.horn = 0xFF; // Dedicated horn button, skip. + out.hat = s->data.hatswitch & 0x0F; + out.buttons = dct03_buttons(s->data.buttons); + usb_packet_copy(p, &out, sizeof(out)); + break; + } default: Console.Error("Unhandled TrainController USB_TOKEN_IN pid=%d ep=%u type=%u", p->pid, p->ep->nr, s->type); p->status = USB_RET_IOERROR; @@ -419,6 +499,11 @@ namespace usb_pad if (usb_desc_parse_dev(dct02_dev_descriptor, sizeof(dct02_dev_descriptor), s->desc, s->desc_dev) < 0) goto fail; break; + case TRAIN_RYOJOUHEN: + s->desc.str = dct03_desc_strings; + if (usb_desc_parse_dev(dct03_dev_descriptor, sizeof(dct03_dev_descriptor), s->desc, s->desc_dev) < 0) + goto fail; + break; default: goto fail; diff --git a/pcsx2/USB/usb-pad/usb-train.h b/pcsx2/USB/usb-pad/usb-train.h index 8301b7a8d4..29a8ec03f3 100644 --- a/pcsx2/USB/usb-pad/usb-train.h +++ b/pcsx2/USB/usb-pad/usb-train.h @@ -13,6 +13,7 @@ namespace usb_pad { TRAIN_TYPE2, // TCPP20009 or similar TRAIN_SHINKANSEN, // TCPP20011 + TRAIN_RYOJOUHEN, // TCPP20014 TRAIN_COUNT, }; @@ -51,6 +52,17 @@ namespace usb_pad u8 pad; }; static_assert(sizeof(TrainConData_Shinkansen) == 6); + + struct TrainConData_Ryojouhen + { + u8 brake; + u8 power; + u8 horn; + u8 hat; + u8 buttons; + u8 pad[3]; + }; + static_assert(sizeof(TrainConData_Ryojouhen) == 8); #pragma pack(pop) struct TrainDeviceState @@ -157,4 +169,16 @@ namespace usb_pad // dct02_dev_descriptor DEFINE_DCT_DEV_DESCRIPTOR(dct02, 0x05, 0x0005); + // ---- Ryojouhen controller ---- + + static const USBDescStrings dct03_desc_strings = { + "", + "TAITO", + "TAITO_DENSYA_CON_T03", + "TCPP20014", + }; + + // dct03_dev_descriptor + DEFINE_DCT_DEV_DESCRIPTOR(dct03, 0xFF, 0x0007); + } // namespace usb_pad