mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
Support multiple controllers for local multiplayer (#1583)
* Support multiple controllers for local co-op
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "تم كسب الكأس!"
|
||||
"trophy.toast.earned": "تم كسب الكأس!",
|
||||
"user.guest": "ضيف"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofej získána!"
|
||||
"trophy.toast.earned": "Trofej získána!",
|
||||
"user.guest": "Host"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofæ optjent!"
|
||||
"trophy.toast.earned": "Trofæ optjent!",
|
||||
"user.guest": "Gæst"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophäe erhalten!"
|
||||
"trophy.toast.earned": "Trophäe erhalten!",
|
||||
"user.guest": "Gast"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!",
|
||||
"user.guest": "Επισκ."
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
"trophy.toast.earned": "Trophy earned!",
|
||||
"user.guest": "Guest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
"trophy.toast.earned": "Trophy earned!",
|
||||
"user.guest": "Guest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!",
|
||||
"user.guest": "Invitado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!",
|
||||
"user.guest": "Invitado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Pokaali ansaittu!"
|
||||
"trophy.toast.earned": "Pokaali ansaittu!",
|
||||
"user.guest": "Vieras"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
"trophy.toast.earned": "Trophée obtenu !",
|
||||
"user.guest": "Invité"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
"trophy.toast.earned": "Trophée obtenu !",
|
||||
"user.guest": "Invité"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trófea megszerezve!"
|
||||
"trophy.toast.earned": "Trófea megszerezve!",
|
||||
"user.guest": "Vendég"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofi diperoleh!"
|
||||
"trophy.toast.earned": "Trofi diperoleh!",
|
||||
"user.guest": "Tamu"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeo ottenuto!"
|
||||
"trophy.toast.earned": "Trofeo ottenuto!",
|
||||
"user.guest": "Ospite"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!"
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!",
|
||||
"user.guest": "ゲスト"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!"
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!",
|
||||
"user.guest": "게스트"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofee behaald!"
|
||||
"trophy.toast.earned": "Trofee behaald!",
|
||||
"user.guest": "Gast"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé opptjent!"
|
||||
"trophy.toast.earned": "Trofé opptjent!",
|
||||
"user.guest": "Gjest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Zdobyto trofeum!"
|
||||
"trophy.toast.earned": "Zdobyto trofeum!",
|
||||
"user.guest": "Gość"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
"trophy.toast.earned": "Troféu conquistado!",
|
||||
"user.guest": "Convidado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
"trophy.toast.earned": "Troféu conquistado!",
|
||||
"user.guest": "Convidado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeu câștigat!"
|
||||
"trophy.toast.earned": "Trofeu câștigat!",
|
||||
"user.guest": "Invitat"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Трофей получен!"
|
||||
"trophy.toast.earned": "Трофей получен!",
|
||||
"user.guest": "Гость"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé intjänad!"
|
||||
"trophy.toast.earned": "Trofé intjänad!",
|
||||
"user.guest": "Gäst"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!",
|
||||
"user.guest": "แขก"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Kupa kazanıldı!"
|
||||
"trophy.toast.earned": "Kupa kazanıldı!",
|
||||
"user.guest": "Misafir"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Đã nhận cúp!"
|
||||
"trophy.toast.earned": "Đã nhận cúp!",
|
||||
"user.guest": "Khách"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "已获得奖杯!"
|
||||
"trophy.toast.earned": "已获得奖杯!",
|
||||
"user.guest": "访客"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "已獲得獎盃!"
|
||||
"trophy.toast.earned": "已獲得獎盃!",
|
||||
"user.guest": "訪客"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ void Load(const ConfigOptions& cfg) {
|
||||
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
|
||||
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
|
||||
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
|
||||
EXIT_IF(cfg.keyboard_player > static_cast<uint32_t>(MAX_PLAYERS));
|
||||
|
||||
*g_config = cfg;
|
||||
}
|
||||
@@ -60,6 +61,27 @@ uint32_t GetControllerVibrationIntensity() {
|
||||
return g_config->controller_vibration_intensity;
|
||||
}
|
||||
|
||||
uint32_t GetKeyboardPlayer() {
|
||||
return g_config->keyboard_player;
|
||||
}
|
||||
|
||||
int32_t GetPlayerUserId(int player) {
|
||||
return g_config->user_id + player * PLAYER_USER_ID_STEP;
|
||||
}
|
||||
|
||||
int GetPlayerFromUserId(int32_t user_id) {
|
||||
for (int player = 0; player < MAX_PLAYERS; player++) {
|
||||
if (user_id == GetPlayerUserId(player)) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool IsGuestUserId(int32_t user_id) {
|
||||
return GetPlayerFromUserId(user_id) > 0;
|
||||
}
|
||||
|
||||
PresentMode GetPresentMode() {
|
||||
return g_config->present_mode;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -35,10 +36,16 @@ constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
|
||||
constexpr std::size_t MAX_USER_NAME_LENGTH = 16;
|
||||
constexpr int32_t DEFAULT_USER_ID = 1000;
|
||||
|
||||
constexpr int MAX_PLAYERS = 4;
|
||||
constexpr int32_t PLAYER_USER_ID_STEP = 0x100;
|
||||
constexpr int32_t MAX_USER_ID =
|
||||
std::numeric_limits<int32_t>::max() - (MAX_PLAYERS - 1) * PLAYER_USER_ID_STEP;
|
||||
|
||||
constexpr bool IsConfiguredUserIdValid(int32_t user_id) {
|
||||
constexpr int32_t USER_ID_EVERYONE = 0xfe;
|
||||
constexpr int32_t USER_ID_SYSTEM = 0xff;
|
||||
return user_id >= 0 && user_id != USER_ID_EVERYONE && user_id != USER_ID_SYSTEM;
|
||||
return user_id >= 0 && user_id <= MAX_USER_ID && user_id != USER_ID_EVERYONE &&
|
||||
user_id != USER_ID_SYSTEM;
|
||||
}
|
||||
|
||||
struct ConfigOptions {
|
||||
@@ -50,6 +57,8 @@ struct ConfigOptions {
|
||||
std::optional<ControllerColor> controller_color;
|
||||
uint32_t controller_speaker_volume = 50;
|
||||
uint32_t controller_vibration_intensity = 100;
|
||||
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
|
||||
uint32_t keyboard_player = 0;
|
||||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int32_t gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
@@ -100,6 +109,13 @@ const std::string& GetAudioInputDevice();
|
||||
const std::optional<ControllerColor>& GetControllerColor();
|
||||
uint32_t GetControllerSpeakerVolume();
|
||||
uint32_t GetControllerVibrationIntensity();
|
||||
uint32_t GetKeyboardPlayer();
|
||||
// Player 0 is the configured user; later players derive their IDs from it.
|
||||
int32_t GetPlayerUserId(int player);
|
||||
// Returns -1 when the user ID does not belong to any player.
|
||||
int GetPlayerFromUserId(int32_t user_id);
|
||||
// Players after the first are guests: they have no profile, trophies or NP account.
|
||||
bool IsGuestUserId(int32_t user_id);
|
||||
PresentMode GetPresentMode();
|
||||
int32_t GetGpuIndex();
|
||||
bool FullscreenEnabled();
|
||||
|
||||
@@ -254,7 +254,8 @@ public:
|
||||
}
|
||||
|
||||
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_F1 ||
|
||||
binding.key == SDLK_F7 || binding.key == SDLK_F11;
|
||||
binding.key == SDLK_F7 || binding.key == SDLK_F8 ||
|
||||
binding.key == SDLK_F11;
|
||||
if (binding.control == INVALID_CONTROL || reserved ||
|
||||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
|
||||
EXIT("Invalid input mapping: %s\n", value.c_str());
|
||||
|
||||
@@ -466,6 +466,13 @@ void WindowContext::ProcessEvent(double time_s) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) &&
|
||||
event->key.key == SDLK_F8) {
|
||||
if (event->type == SDL_EVENT_KEY_DOWN && event->key.repeat == 0) {
|
||||
Controller::CycleKeyboardPlayer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ProcessSystemOverlayInput(*event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_user_id">
|
||||
<property name="toolTip"><string>Local user ID exposed to games</string></property>
|
||||
<property name="maximum"><number>2147483647</number></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
@@ -347,6 +346,16 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_keyboard_player">
|
||||
<property name="text"><string>Keyboard player:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="comboBox_keyboard_player">
|
||||
<property name="toolTip"><string>Gamepads fill the remaining players in connection order. Every player uses the same input mapping.</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -48,11 +48,14 @@ struct ControllerSettings {
|
||||
QString color;
|
||||
int speaker_volume = 50;
|
||||
int vibration_intensity = 100;
|
||||
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
|
||||
int keyboard_player = 0;
|
||||
|
||||
void WriteSettings(QSettings* s) const {
|
||||
s->setValue("controller_color", color);
|
||||
s->setValue("controller_speaker_volume", speaker_volume);
|
||||
s->setValue("controller_vibration_intensity", vibration_intensity);
|
||||
s->setValue("controller_keyboard_player", keyboard_player);
|
||||
}
|
||||
|
||||
void ReadSettings(QSettings* s) {
|
||||
@@ -65,6 +68,8 @@ struct ControllerSettings {
|
||||
};
|
||||
speaker_volume = read_percent("controller_speaker_volume", 50);
|
||||
vibration_intensity = read_percent("controller_vibration_intensity", 100);
|
||||
keyboard_player = qBound(0, s->value("controller_keyboard_player", 0).toInt(),
|
||||
Config::MAX_PLAYERS);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -209,10 +209,19 @@ static void ListInit(QComboBox* combo, T value) {
|
||||
void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
|
||||
m_ui->lineEdit_user_name->setText(info.user_name);
|
||||
m_ui->spinBox_user_id->setMaximum(Config::MAX_USER_ID);
|
||||
m_ui->spinBox_user_id->setValue(info.user_id);
|
||||
UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color);
|
||||
m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity);
|
||||
m_ui->slider_controller_volume->setValue(info.controller.speaker_volume);
|
||||
auto* keyboard_player = m_ui->comboBox_keyboard_player;
|
||||
keyboard_player->clear();
|
||||
keyboard_player->addItem(tr("Shared with player 1 (default)"), 0);
|
||||
for (int player = 1; player <= Config::MAX_PLAYERS; player++) {
|
||||
keyboard_player->addItem(tr("Player %1").arg(player), player);
|
||||
}
|
||||
keyboard_player->setCurrentIndex(
|
||||
qMax(0, keyboard_player->findData(info.controller.keyboard_player)));
|
||||
auto* microphone = m_ui->comboBox_audio_input_device;
|
||||
microphone->clear();
|
||||
microphone->addItem(tr("None"), QString {});
|
||||
@@ -458,6 +467,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
|
||||
info.controller.color = ui.button_controller_color->property("controllerColor").toString();
|
||||
info.controller.vibration_intensity = ui.slider_controller_vibration->value();
|
||||
info.controller.speaker_volume = ui.slider_controller_volume->value();
|
||||
info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt();
|
||||
}
|
||||
info.screen_resolution =
|
||||
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
|
||||
|
||||
@@ -246,6 +246,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
}
|
||||
args << "--controller-volume" << QString::number(info.controller.speaker_volume);
|
||||
args << "--controller-vibration" << QString::number(info.controller.vibration_intensity);
|
||||
args << "--keyboard-player" << QString::number(info.controller.keyboard_player);
|
||||
args << "--present-mode" << EnumToText(info.present_mode);
|
||||
if (info.gpu_index >= 0) {
|
||||
args << "--gpu" << QString::number(info.gpu_index);
|
||||
|
||||
+236
-115
@@ -18,6 +18,7 @@
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Controller {
|
||||
@@ -107,18 +108,19 @@ struct ControllerState {
|
||||
|
||||
class GameController {
|
||||
public:
|
||||
GameController() = default;
|
||||
explicit GameController(int player): m_player(player) {}
|
||||
|
||||
KYTY_CLASS_NO_COPY(GameController);
|
||||
|
||||
[[nodiscard]] int Handle() const { return PAD_HANDLE + m_player; }
|
||||
|
||||
int InitPort();
|
||||
int OpenPort(int user_id, int type, int index);
|
||||
int GetPortHandle(int user_id, int type, int index);
|
||||
int ClosePort(int handle);
|
||||
int CheckPortHandle(int handle);
|
||||
|
||||
void Connect(int id);
|
||||
void Disconnect(int id);
|
||||
void SetDevices(int active_id);
|
||||
void Button(int id, uint32_t button, bool down);
|
||||
void Axis(int id, Axis axis, int value);
|
||||
void RightStick(int id, int x, int y);
|
||||
@@ -128,7 +130,6 @@ public:
|
||||
void ResetOrientation();
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
void ReleaseHostPads();
|
||||
void GetConnectionInfo(bool* flag, int* count);
|
||||
void SetVibration(uint8_t large_motor, uint8_t small_motor);
|
||||
int GetActiveControllerId();
|
||||
@@ -145,8 +146,10 @@ private:
|
||||
enum class PortState { Uninitialized, Closed, Open };
|
||||
|
||||
int CheckPortHandleLocked(int handle) const;
|
||||
[[nodiscard]] bool PadOpenArgsAreValid(int user_id, int type, int index) const;
|
||||
|
||||
const int m_player;
|
||||
|
||||
void CheckActive();
|
||||
void AddState();
|
||||
// Emulated tilt: the right stick drives pitch and roll instead of the stick axes.
|
||||
void UpdateEmulatedTilt();
|
||||
@@ -157,7 +160,6 @@ private:
|
||||
|
||||
Common::Mutex m_mutex;
|
||||
PortState m_port_state = PortState::Uninitialized;
|
||||
std::vector<int> m_connected_ids;
|
||||
int m_active_id = -1;
|
||||
bool m_connected = false;
|
||||
int m_connected_count = 0;
|
||||
@@ -184,7 +186,39 @@ private:
|
||||
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
|
||||
};
|
||||
|
||||
static GameController* g_controller = nullptr;
|
||||
static std::array<std::unique_ptr<GameController>, Config::MAX_PLAYERS> g_players;
|
||||
|
||||
// Maps host gamepads to players; the keyboard follows the configured keyboard player.
|
||||
static Common::Mutex g_router_mutex;
|
||||
static std::vector<std::pair<int, int>> g_pad_players;
|
||||
|
||||
static uint32_t g_keyboard_player = 0;
|
||||
|
||||
static void RefreshPlayers();
|
||||
static void StopHostPad(SDL_Gamepad* pad);
|
||||
|
||||
static int KeyboardPlayer() {
|
||||
return g_keyboard_player == 0 ? 0 : static_cast<int>(g_keyboard_player) - 1;
|
||||
}
|
||||
|
||||
// Caller holds g_router_mutex.
|
||||
static GameController* PlayerForDevice(int id) {
|
||||
if (id == HOST_INPUT_CONTROLLER_ID) {
|
||||
return g_players[KeyboardPlayer()].get();
|
||||
}
|
||||
for (const auto& [pad, player]: g_pad_players) {
|
||||
if (pad == id) {
|
||||
return player >= 0 ? g_players[player].get() : nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static GameController* PlayerForHandle(int handle) {
|
||||
return handle >= PAD_HANDLE && handle < PAD_HANDLE + Config::MAX_PLAYERS
|
||||
? g_players[handle - PAD_HANDLE].get()
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
static void pad_fill_data(PadData* data, const ControllerState& state, bool connected,
|
||||
int connected_count) {
|
||||
@@ -228,13 +262,15 @@ constexpr std::array SPEAKER_VOLUME = {1.0f, 0.0f, 0.02f, 0.12f, 0.42f};
|
||||
constexpr std::array INTENSITY = {1.0f, 0.0f, 0.33f, 0.66f};
|
||||
|
||||
void CycleSetting(Setting setting) {
|
||||
if (g_controller != nullptr) {
|
||||
g_controller->CycleSetting(setting);
|
||||
for (const auto& player: g_players) {
|
||||
if (player != nullptr) {
|
||||
player->CycleSetting(setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float GetSettingScale(Setting setting) {
|
||||
return g_controller != nullptr ? g_controller->GetSettingScale(setting) : 1.0f;
|
||||
return g_players[0] != nullptr ? g_players[0]->GetSettingScale(setting) : 1.0f;
|
||||
}
|
||||
|
||||
void GameController::CycleSetting(Setting setting) {
|
||||
@@ -404,35 +440,53 @@ static int32_t TriggerEffectState(const PadTriggerEffectCommand& command, int va
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
EXIT_IF(g_controller != nullptr);
|
||||
EXIT_IF(g_players[0] != nullptr);
|
||||
|
||||
g_controller = new GameController;
|
||||
g_controller->Connect(HOST_INPUT_CONTROLLER_ID);
|
||||
for (int i = 0; i < Config::MAX_PLAYERS; i++) {
|
||||
g_players[i] = std::make_unique<GameController>(i);
|
||||
}
|
||||
g_keyboard_player = Config::GetKeyboardPlayer();
|
||||
RefreshPlayers();
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
EmergencyShutdown();
|
||||
delete g_controller;
|
||||
g_controller = nullptr;
|
||||
for (auto& player: g_players) {
|
||||
player.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void EmergencyShutdown() {
|
||||
if (g_controller != nullptr) {
|
||||
g_controller->ReleaseHostPads();
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
DualSenseHaptics::Shutdown();
|
||||
std::vector<SDL_Gamepad*> pads;
|
||||
for (const auto& [id, player]: g_pad_players) {
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id))) {
|
||||
StopHostPad(pad);
|
||||
pads.push_back(pad);
|
||||
}
|
||||
}
|
||||
if (!pads.empty()) {
|
||||
SDL_Delay(RELEASE_FLUSH_MS);
|
||||
for (auto* pad: pads) {
|
||||
SDL_CloseGamepad(pad);
|
||||
}
|
||||
}
|
||||
g_pad_players.clear();
|
||||
for (const auto& player: g_players) {
|
||||
if (player != nullptr) {
|
||||
player->SetDevices(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameController::Connect(int id) {
|
||||
void GameController::SetDevices(int active_id) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
if (std::find(m_connected_ids.begin(), m_connected_ids.end(), id) != m_connected_ids.end()) {
|
||||
if (m_active_id == active_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_connected_ids.push_back(id);
|
||||
|
||||
if (id != HOST_INPUT_CONTROLLER_ID) {
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
|
||||
if (active_id != HOST_INPUT_CONTROLLER_ID && active_id != -1) {
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(active_id));
|
||||
pad != nullptr) {
|
||||
if (const auto& color = Config::GetControllerColor()) {
|
||||
(void)SDL_SetGamepadLED(pad, (*color)[0], (*color)[1], (*color)[2]);
|
||||
@@ -446,42 +500,11 @@ void GameController::Connect(int id) {
|
||||
}
|
||||
}
|
||||
|
||||
CheckActive();
|
||||
}
|
||||
|
||||
void GameController::Disconnect(int id) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
const auto it = std::find(m_connected_ids.begin(), m_connected_ids.end(), id);
|
||||
EXIT_IF(it == m_connected_ids.end());
|
||||
|
||||
m_connected_ids.erase(it);
|
||||
|
||||
CheckActive();
|
||||
}
|
||||
|
||||
void GameController::CheckActive() {
|
||||
int new_active_id = -1;
|
||||
bool new_connected = false;
|
||||
|
||||
if (!m_connected_ids.empty()) {
|
||||
new_active_id = m_connected_ids[0];
|
||||
for (const auto id: m_connected_ids) {
|
||||
if (id != HOST_INPUT_CONTROLLER_ID) {
|
||||
new_active_id = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
new_connected = true;
|
||||
}
|
||||
|
||||
if (m_connected == new_connected && m_active_id == new_active_id) {
|
||||
return;
|
||||
}
|
||||
const bool new_connected = active_id != -1;
|
||||
if (!m_connected && new_connected) {
|
||||
m_connected_count++;
|
||||
}
|
||||
m_active_id = new_active_id;
|
||||
m_active_id = active_id;
|
||||
m_connected = new_connected;
|
||||
m_state = {};
|
||||
m_gyro_time = 0;
|
||||
@@ -513,7 +536,7 @@ void GameController::AddState() {
|
||||
void GameController::Button(int id, uint32_t button, bool down) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
// The keyboard shares the player-1 pad with the active gamepad.
|
||||
// Keyboard events are routed to the configured player's port.
|
||||
if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
|
||||
@@ -798,17 +821,7 @@ void GameController::ResetInputState() {
|
||||
AddState();
|
||||
}
|
||||
|
||||
void GameController::ReleaseHostPads() {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
DualSenseHaptics::Shutdown();
|
||||
|
||||
std::vector<SDL_Gamepad*> pads;
|
||||
for (const auto id: m_connected_ids) {
|
||||
if (id == HOST_INPUT_CONTROLLER_ID) {
|
||||
continue;
|
||||
}
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
|
||||
pad != nullptr) {
|
||||
static void StopHostPad(SDL_Gamepad* pad) {
|
||||
if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
|
||||
DualSenseEffects effect {};
|
||||
effect.enable_bits = 0x0c;
|
||||
@@ -818,19 +831,6 @@ void GameController::ReleaseHostPads() {
|
||||
}
|
||||
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
|
||||
(void)SDL_SetGamepadLED(pad, 0, 0, 0);
|
||||
pads.push_back(pad);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pads.empty()) {
|
||||
SDL_Delay(RELEASE_FLUSH_MS);
|
||||
for (auto* pad: pads) {
|
||||
SDL_CloseGamepad(pad);
|
||||
}
|
||||
}
|
||||
|
||||
m_connected_ids.clear();
|
||||
CheckActive();
|
||||
}
|
||||
|
||||
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
|
||||
@@ -847,7 +847,9 @@ void GameController::ApplyVibration() {
|
||||
const auto scale = duration != 0 ? GetSettingScale(Setting::VibrationIntensity) : 0.0f;
|
||||
const auto large_motor = Scale(m_vibration[0], scale);
|
||||
const auto small_motor = Scale(m_vibration[1], scale);
|
||||
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
|
||||
// The DualSense haptics backend drives a single pad, so only player 1 uses it.
|
||||
if (m_player == 0 &&
|
||||
DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -999,49 +1001,149 @@ int GameController::ReadStates(ControllerState* states, int states_num, bool* fl
|
||||
return ret_num;
|
||||
}
|
||||
|
||||
// Keep every opened pad, including those waiting for a free player slot.
|
||||
// Caller holds g_router_mutex.
|
||||
static void RefreshPlayers() {
|
||||
std::array<bool, Config::MAX_PLAYERS> taken {};
|
||||
const int keyboard = KeyboardPlayer();
|
||||
if (g_keyboard_player != 0) {
|
||||
taken[keyboard] = true;
|
||||
}
|
||||
for (auto& [id, player]: g_pad_players) {
|
||||
if (g_keyboard_player != 0 && player == keyboard) {
|
||||
player = -1;
|
||||
}
|
||||
if (player >= 0) {
|
||||
taken[player] = true;
|
||||
}
|
||||
}
|
||||
for (auto& [id, player]: g_pad_players) {
|
||||
if (player >= 0) {
|
||||
continue;
|
||||
}
|
||||
const auto free = std::find(taken.begin(), taken.end(), false);
|
||||
if (free != taken.end()) {
|
||||
player = static_cast<int>(free - taken.begin());
|
||||
*free = true;
|
||||
LOGF("Controller %d assigned to player %d\n", id, player + 1);
|
||||
}
|
||||
}
|
||||
std::array<int, Config::MAX_PLAYERS> active;
|
||||
active.fill(-1);
|
||||
active[keyboard] = HOST_INPUT_CONTROLLER_ID;
|
||||
for (const auto& [id, player]: g_pad_players) {
|
||||
if (player >= 0) {
|
||||
active[player] = id;
|
||||
}
|
||||
}
|
||||
// Stop old owners before configuring new owners of the same physical pad.
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
const int previous = g_players[player]->GetActiveControllerId();
|
||||
if (previous != active[player]) {
|
||||
if (player == 0) {
|
||||
(void)DualSenseHaptics::SetVibration(previous, 0, 0, 0);
|
||||
}
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(previous))) {
|
||||
StopHostPad(pad);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
g_players[player]->SetDevices(active[player]);
|
||||
}
|
||||
}
|
||||
|
||||
void Connect(int id) {
|
||||
g_controller->Connect(id);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (std::any_of(g_pad_players.begin(), g_pad_players.end(),
|
||||
[id](const auto& pad) { return pad.first == id; })) {
|
||||
return;
|
||||
}
|
||||
g_pad_players.emplace_back(id, -1);
|
||||
RefreshPlayers();
|
||||
}
|
||||
|
||||
void Disconnect(int id) {
|
||||
g_controller->Disconnect(id);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
std::erase_if(g_pad_players, [id](const auto& pad) { return pad.first == id; });
|
||||
RefreshPlayers();
|
||||
}
|
||||
|
||||
void CycleKeyboardPlayer() {
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
g_keyboard_player = (g_keyboard_player + 1) % (Config::MAX_PLAYERS + 1);
|
||||
RefreshPlayers();
|
||||
if (g_keyboard_player == 0) {
|
||||
LOGF("Keyboard shared with player 1\n");
|
||||
} else {
|
||||
LOGF("Keyboard assigned to player %u\n", g_keyboard_player);
|
||||
}
|
||||
}
|
||||
|
||||
void SetButton(int id, uint32_t button, bool down) {
|
||||
g_controller->Button(id, button, down);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->Button(id, button, down);
|
||||
}
|
||||
}
|
||||
|
||||
void SetAxis(int id, Axis axis, int value) {
|
||||
g_controller->Axis(id, axis, value);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->Axis(id, axis, value);
|
||||
}
|
||||
}
|
||||
|
||||
void SetRightStick(int id, int x, int y) {
|
||||
g_controller->RightStick(id, x, y);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->RightStick(id, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void SetTouchPad(int id, int finger, bool down, float x, float y) {
|
||||
g_controller->TouchPad(id, finger, down, x, y);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->TouchPad(id, finger, down, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us) {
|
||||
g_controller->Motion(id, sensor, data, time_us);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->Motion(id, sensor, data, time_us);
|
||||
}
|
||||
}
|
||||
|
||||
void ToggleGyroEmulation(int id) {
|
||||
g_controller->ToggleGyroEmulation(id);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->ToggleGyroEmulation(id);
|
||||
}
|
||||
}
|
||||
|
||||
void ResetInputState() {
|
||||
g_controller->ResetInputState();
|
||||
for (const auto& player: g_players) {
|
||||
if (player != nullptr) {
|
||||
player->ResetInputState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int GetActiveControllerId() {
|
||||
return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1;
|
||||
return g_players[0] != nullptr ? g_players[0]->GetActiveControllerId() : -1;
|
||||
}
|
||||
|
||||
static bool PadOpenArgsAreValid(int user_id, int type, int index) {
|
||||
// The host keyboard and gamepad feed one standard port for the configured user.
|
||||
return user_id == Config::GetUserId() && type == 0 && index == 0;
|
||||
bool IsPlayerConnected(int player) {
|
||||
if (player < 0 || player >= Config::MAX_PLAYERS || g_players[player] == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return g_players[player]->GetActiveControllerId() != -1;
|
||||
}
|
||||
|
||||
bool GameController::PadOpenArgsAreValid(int user_id, int type, int index) const {
|
||||
// The host keyboard and gamepads of a player feed one standard port for that player's user.
|
||||
return user_id == Config::GetPlayerUserId(m_player) && type == 0 && index == 0;
|
||||
}
|
||||
|
||||
int GameController::InitPort() {
|
||||
@@ -1064,7 +1166,7 @@ int GameController::OpenPort(int user_id, int type, int index) {
|
||||
return PAD_ERROR_ALREADY_OPENED;
|
||||
}
|
||||
m_port_state = PortState::Open;
|
||||
return PAD_HANDLE;
|
||||
return Handle();
|
||||
}
|
||||
|
||||
int GameController::GetPortHandle(int user_id, int type, int index) {
|
||||
@@ -1072,7 +1174,8 @@ int GameController::GetPortHandle(int user_id, int type, int index) {
|
||||
if (m_port_state == PortState::Uninitialized) {
|
||||
return PAD_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index) ? PAD_HANDLE
|
||||
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index)
|
||||
? Handle()
|
||||
: PAD_ERROR_NO_HANDLE;
|
||||
}
|
||||
|
||||
@@ -1080,7 +1183,7 @@ int GameController::CheckPortHandleLocked(int handle) const {
|
||||
if (m_port_state == PortState::Uninitialized) {
|
||||
return PAD_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
return m_port_state == PortState::Open && handle == PAD_HANDLE ? OK : PAD_ERROR_INVALID_HANDLE;
|
||||
return m_port_state == PortState::Open && handle == Handle() ? OK : PAD_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
int GameController::CheckPortHandle(int handle) {
|
||||
@@ -1098,12 +1201,19 @@ int GameController::ClosePort(int handle) {
|
||||
}
|
||||
|
||||
int PadCheckHandle(int handle) {
|
||||
return g_controller->CheckPortHandle(handle);
|
||||
if (g_players[0] == nullptr) {
|
||||
return PAD_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
auto* player = PlayerForHandle(handle);
|
||||
return player != nullptr ? player->CheckPortHandle(handle) : PAD_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadInit() {
|
||||
PRINT_NAME();
|
||||
return g_controller->InitPort();
|
||||
for (const auto& player: g_players) {
|
||||
(void)player->InitPort();
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
|
||||
@@ -1115,7 +1225,8 @@ int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
|
||||
"\t param = 0x%016" PRIx64 "\n",
|
||||
user_id, type, index, reinterpret_cast<uint64_t>(param));
|
||||
|
||||
return g_controller->OpenPort(user_id, type, index);
|
||||
const int player = Config::GetPlayerFromUserId(user_id);
|
||||
return player >= 0 ? g_players[player]->OpenPort(user_id, type, index) : PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
|
||||
@@ -1126,13 +1237,19 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
|
||||
"\t index = %d\n",
|
||||
user_id, type, index);
|
||||
|
||||
return g_controller->GetPortHandle(user_id, type, index);
|
||||
const int player = Config::GetPlayerFromUserId(user_id);
|
||||
return player >= 0 ? g_players[player]->GetPortHandle(user_id, type, index)
|
||||
: PAD_ERROR_NO_HANDLE;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadClose(int handle) {
|
||||
PRINT_NAME();
|
||||
LOGF("\t handle = %d\n", handle);
|
||||
return g_controller->ClosePort(handle);
|
||||
auto* player = PlayerForHandle(handle);
|
||||
if (player == nullptr) {
|
||||
return PAD_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
return player->ClosePort(handle);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
|
||||
@@ -1143,7 +1260,7 @@ int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
|
||||
}
|
||||
|
||||
LOGF("\t enable = %s\n", (enable ? "true" : "false"));
|
||||
g_controller->SetMotionSensorState(enable);
|
||||
PlayerForHandle(handle)->SetMotionSensorState(enable);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -1167,7 +1284,7 @@ int KYTY_SYSV_ABI PadResetOrientation(int handle) {
|
||||
return error;
|
||||
}
|
||||
|
||||
g_controller->ResetOrientation();
|
||||
PlayerForHandle(handle)->ResetOrientation();
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -1183,7 +1300,7 @@ int KYTY_SYSV_ABI PadGetControllerInformation(int handle, PadControllerInformati
|
||||
|
||||
int connected_count = 0;
|
||||
bool connected = false;
|
||||
g_controller->GetConnectionInfo(&connected, &connected_count);
|
||||
PlayerForHandle(handle)->GetConnectionInfo(&connected, &connected_count);
|
||||
|
||||
std::memset(info, 0, sizeof(*info));
|
||||
|
||||
@@ -1228,7 +1345,7 @@ int KYTY_SYSV_ABI PadReadState(int handle, PadData* data) {
|
||||
bool connected = false;
|
||||
ControllerState state;
|
||||
|
||||
g_controller->ReadState(&state, &connected, &connected_count);
|
||||
PlayerForHandle(handle)->ReadState(&state, &connected, &connected_count);
|
||||
|
||||
pad_fill_data(data, state, connected, connected_count);
|
||||
|
||||
@@ -1252,11 +1369,12 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
|
||||
bool connected = false;
|
||||
ControllerState states[64] = {};
|
||||
|
||||
int ret_num = g_controller->ReadStates(states, num, &connected, &connected_count);
|
||||
auto* player = PlayerForHandle(handle);
|
||||
int ret_num = player->ReadStates(states, num, &connected, &connected_count);
|
||||
|
||||
if (!connected || ret_num == 0) {
|
||||
if (connected) {
|
||||
g_controller->ReadState(&states[0], &connected, &connected_count);
|
||||
player->ReadState(&states[0], &connected, &connected_count);
|
||||
}
|
||||
ret_num = 1;
|
||||
}
|
||||
@@ -1270,6 +1388,7 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
|
||||
|
||||
int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
|
||||
PRINT_NAME();
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
|
||||
if (const auto error = PadCheckHandle(handle); error != OK) {
|
||||
return error;
|
||||
@@ -1282,7 +1401,7 @@ int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
|
||||
"\t small_motor = %d\n",
|
||||
static_cast<int>(param->large_motor), static_cast<int>(param->small_motor));
|
||||
|
||||
g_controller->SetVibration(param->large_motor, param->small_motor);
|
||||
PlayerForHandle(handle)->SetVibration(param->large_motor, param->small_motor);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -1299,6 +1418,7 @@ int KYTY_SYSV_ABI PadResetLightBar(int handle) {
|
||||
|
||||
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
|
||||
PRINT_NAME();
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
|
||||
if (const auto error = PadCheckHandle(handle); error != OK) {
|
||||
return error;
|
||||
@@ -1307,13 +1427,14 @@ int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
|
||||
return PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
g_controller->SetLightBar(param->r, param->g, param->b);
|
||||
PlayerForHandle(handle)->SetLightBar(param->r, param->g, param->b);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param) {
|
||||
PRINT_NAME();
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
|
||||
if (const auto error = PadCheckHandle(handle); error != OK) {
|
||||
return error;
|
||||
@@ -1322,7 +1443,7 @@ int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* p
|
||||
return PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return g_controller->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
|
||||
return PlayerForHandle(handle)->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) {
|
||||
@@ -1337,7 +1458,7 @@ int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInfo
|
||||
return PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
g_controller->GetTriggerEffectState(info);
|
||||
PlayerForHandle(handle)->GetTriggerEffectState(info);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ inline int controller_get_axis(int min, int max, int value) {
|
||||
|
||||
void Connect(int id);
|
||||
void Disconnect(int id);
|
||||
// Moves the keyboard to the next player (shared with player 1, then 1-4) while the game runs.
|
||||
void CycleKeyboardPlayer();
|
||||
void SetButton(int id, uint32_t button, bool down);
|
||||
void SetAxis(int id, Axis axis, int value);
|
||||
void SetRightStick(int id, int x, int y);
|
||||
@@ -76,6 +78,8 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
int GetActiveControllerId();
|
||||
// True when the player has a keyboard or gamepad assigned.
|
||||
bool IsPlayerConnected(int player);
|
||||
|
||||
enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity };
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
@@ -300,15 +301,23 @@ static std::string memory_dir_name(uint32_t slot_id) {
|
||||
return "sce_sdmemory" + (slot_id == 0 ? std::string() : std::to_string(slot_id));
|
||||
}
|
||||
|
||||
static std::filesystem::path save_user_directory(std::string_view title_id, int32_t user_id) {
|
||||
auto directory = std::filesystem::path(SAVE_DATA_DIR);
|
||||
if (Config::IsGuestUserId(user_id)) {
|
||||
directory /= std::filesystem::path("[users]") / std::to_string(user_id);
|
||||
}
|
||||
return directory / title_id;
|
||||
}
|
||||
|
||||
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
|
||||
int32_t user_id) {
|
||||
const auto directory = std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name;
|
||||
for (uint32_t slot = 0; slot < 4; slot++) {
|
||||
if (dir_name == memory_dir_name(slot)) {
|
||||
return directory / std::to_string(user_id);
|
||||
return std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name /
|
||||
std::to_string(user_id);
|
||||
}
|
||||
}
|
||||
return directory;
|
||||
return save_user_directory(title_id, user_id) / dir_name;
|
||||
}
|
||||
|
||||
static std::filesystem::path memory_directory(int32_t user_id, uint32_t slot_id) {
|
||||
@@ -590,7 +599,7 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
|
||||
|
||||
Common::LockGuard lock(g_mount_mutex);
|
||||
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
|
||||
const auto root = std::filesystem::path(SAVE_DATA_DIR) / title_id;
|
||||
const auto root = save_user_directory(title_id, cond->user_id);
|
||||
std::vector<std::string> dir_list;
|
||||
|
||||
if (Common::File::IsDirectoryExisting(root)) {
|
||||
|
||||
+12
-3
@@ -105,6 +105,8 @@ struct TrophyState {
|
||||
const Trophies::Package* package = nullptr;
|
||||
Trophies::UnlockData unlocks;
|
||||
std::filesystem::path path;
|
||||
// Guests (players 2+) have no profile: nothing is loaded, saved or announced.
|
||||
bool guest = false;
|
||||
};
|
||||
|
||||
struct TrophyContext {
|
||||
@@ -153,9 +155,12 @@ static TrophyState* LoadTrophyState(TrophyKey key) {
|
||||
package.event_rules.size(), uds_filename.c_str());
|
||||
}
|
||||
}
|
||||
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocks = Trophies::LoadUnlockData(path);
|
||||
return &g_states.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path)})
|
||||
const bool guest = Config::IsGuestUserId(key.first);
|
||||
auto path = guest ? std::filesystem::path {}
|
||||
: Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocks = guest ? Trophies::UnlockData {} : Trophies::LoadUnlockData(path);
|
||||
return &g_states
|
||||
.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path), guest})
|
||||
.first->second;
|
||||
}
|
||||
|
||||
@@ -240,6 +245,10 @@ static void FillGroupInfo(const TrophyState& state, int group_id, const std::str
|
||||
}
|
||||
|
||||
static void RecordTrophyUnlock(TrophyState& state, int trophy_id, bool from_stat = false) {
|
||||
if (state.guest) {
|
||||
LOGF("[Trophy] guest users cannot earn trophies (trophy %d)\n", trophy_id);
|
||||
return;
|
||||
}
|
||||
const auto found = state.package->trophies.find(trophy_id);
|
||||
if (found == state.package->trophies.end()) {
|
||||
LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id);
|
||||
|
||||
+53
-26
@@ -1,12 +1,14 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/localization.h"
|
||||
#include "libs/controller.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Libs {
|
||||
|
||||
@@ -39,6 +41,15 @@ struct UserServiceGamePresets {
|
||||
|
||||
static_assert(sizeof(UserServiceGamePresets) == 40);
|
||||
|
||||
static bool IsPlayerLoggedIn(int player) {
|
||||
return player == 0 || (player > 0 && Controller::IsPlayerConnected(player));
|
||||
}
|
||||
|
||||
static int GetLoggedInPlayer(int user_id) {
|
||||
const int player = Config::GetPlayerFromUserId(user_id);
|
||||
return IsPlayerLoggedIn(player) ? player : -1;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int UserServiceInitialize(const void* /*params*/) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -66,14 +77,17 @@ static KYTY_SYSV_ABI int UserServiceGetEvent(SceUserServiceEvent* event) {
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(event == nullptr);
|
||||
|
||||
static bool logged_in = false;
|
||||
static bool logged_in[Config::MAX_PLAYERS] = {};
|
||||
|
||||
if (!logged_in) {
|
||||
logged_in = true;
|
||||
event->event_type = UserServiceEventTypeLogin;
|
||||
event->user_id = Config::GetUserId();
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
const bool connected = IsPlayerLoggedIn(player);
|
||||
if (connected != logged_in[player]) {
|
||||
logged_in[player] = connected;
|
||||
event->event_type = connected ? UserServiceEventTypeLogin : UserServiceEventTypeLogout;
|
||||
event->user_id = Config::GetPlayerUserId(player);
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
|
||||
return USER_SERVICE_ERROR_NO_EVENT;
|
||||
}
|
||||
@@ -83,24 +97,36 @@ static KYTY_SYSV_ABI int UserServiceGetLoginUserIdList(UserServiceLoginUserIdLis
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(user_id_list == nullptr);
|
||||
|
||||
user_id_list->user_id[0] = Config::GetUserId();
|
||||
user_id_list->user_id[1] = -1;
|
||||
user_id_list->user_id[2] = -1;
|
||||
user_id_list->user_id[3] = -1;
|
||||
int count = 0;
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
if (IsPlayerLoggedIn(player)) {
|
||||
user_id_list->user_id[count++] = Config::GetPlayerUserId(player);
|
||||
}
|
||||
}
|
||||
while (count < Config::MAX_PLAYERS) {
|
||||
user_id_list->user_id[count++] = -1;
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int UserServiceGetUserName(int user_id, char* name, size_t size) {
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (name == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
const int player = GetLoggedInPlayer(user_id);
|
||||
if (player < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(size < 5);
|
||||
|
||||
const auto& user_name = Config::GetUserName();
|
||||
int s = snprintf(name, size, "%s", user_name.c_str());
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(static_cast<size_t>(s) >= size);
|
||||
const auto user_name =
|
||||
player > 0 ? std::string(Common::Localization::Get(Config::GetConsoleLanguage(), "user.guest")) +
|
||||
" " + std::to_string(player + 1)
|
||||
: Config::GetUserName();
|
||||
if (size <= user_name.size()) {
|
||||
return USER_SERVICE_ERROR_BUFFER_TOO_SHORT;
|
||||
}
|
||||
std::memcpy(name, user_name.c_str(), user_name.size() + 1);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -111,11 +137,12 @@ static KYTY_SYSV_ABI int UserServiceGetUserNumber(int user_id, int32_t* number)
|
||||
if (number == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
const int player = GetLoggedInPlayer(user_id);
|
||||
if (player < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
*number = 1;
|
||||
*number = player + 1;
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -126,7 +153,7 @@ static KYTY_SYSV_ABI int UserServiceGetGamePresets(int user_id, UserServiceGameP
|
||||
if (presets == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -147,7 +174,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityVibration(int user_id, int32
|
||||
if (vibration == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -163,7 +190,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityTriggerEffect(int user_
|
||||
if (trigger_effect == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -178,7 +205,7 @@ static KYTY_SYSV_ABI int UserServiceGetAgeLevel(int user_id, uint32_t* age_level
|
||||
if (age_level == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -194,7 +221,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityChatTranscription(int u
|
||||
if (chat_transcription == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -210,7 +237,7 @@ UserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hol
|
||||
if (press_and_hold_delay == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -226,7 +253,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomEnabled(int user_id
|
||||
if (zoom_enabled == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -242,7 +269,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomFollowFocus(int use
|
||||
if (zoom_follow_focus == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ static void PrintUsage() {
|
||||
::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n");
|
||||
::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 50.\n");
|
||||
::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\n");
|
||||
::printf(" --keyboard-player <0-4> Keyboard player; 0 shares player 1 with the gamepad. Default: 0.\n");
|
||||
::printf(
|
||||
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n");
|
||||
::printf(
|
||||
@@ -378,6 +379,12 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
::printf("invalid controller vibration intensity: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--keyboard-player") {
|
||||
if (!ParseUint32(value, options.config.keyboard_player) ||
|
||||
options.config.keyboard_player > static_cast<uint32_t>(Config::MAX_PLAYERS)) {
|
||||
::printf("invalid keyboard player: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--present-mode") {
|
||||
if (!ParseEnum(value, options.config.present_mode)) {
|
||||
::printf("invalid present mode: %s\n", value.c_str());
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -23,6 +24,9 @@ std::vector<Rumble> rumble;
|
||||
std::vector<Rumble> haptics;
|
||||
std::vector<std::array<Uint8, 32>> effects;
|
||||
bool haptics_handles_rumble = false;
|
||||
std::array<Rumble, 6> pad_rumble {};
|
||||
std::vector<int> closed_pads;
|
||||
std::array<std::array<Uint8, 3>, 6> pad_colors {};
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
@@ -30,14 +34,15 @@ Uint64 GetTicks() {
|
||||
return now;
|
||||
}
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
return id >= 1 && id <= 5 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
SDL_GamepadType GetGamepadType(SDL_Gamepad*) {
|
||||
return SDL_GAMEPAD_TYPE_PS5;
|
||||
}
|
||||
bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
bool RumbleGamepad(SDL_Gamepad* pad, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
rumble.push_back({large, small, duration});
|
||||
pad_rumble[reinterpret_cast<uintptr_t>(pad)] = rumble.back();
|
||||
return true;
|
||||
}
|
||||
bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
|
||||
@@ -47,7 +52,8 @@ bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
|
||||
effects.push_back(effect);
|
||||
return true;
|
||||
}
|
||||
bool SetGamepadLED(SDL_Gamepad*, Uint8, Uint8, Uint8) {
|
||||
bool SetGamepadLED(SDL_Gamepad* pad, Uint8 r, Uint8 g, Uint8 b) {
|
||||
pad_colors[reinterpret_cast<uintptr_t>(pad)] = {r, g, b};
|
||||
return true;
|
||||
}
|
||||
bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
|
||||
@@ -56,7 +62,9 @@ bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
|
||||
bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) {
|
||||
return true;
|
||||
}
|
||||
void CloseGamepad(SDL_Gamepad*) {}
|
||||
void CloseGamepad(SDL_Gamepad* pad) {
|
||||
closed_pads.push_back(static_cast<int>(reinterpret_cast<uintptr_t>(pad)));
|
||||
}
|
||||
void Delay(Uint32) {}
|
||||
} // namespace Fake
|
||||
|
||||
@@ -368,6 +376,10 @@ void TestIndependentOutputsAndPadSwitch() {
|
||||
"speaker volume resent controller effects");
|
||||
Connect(2);
|
||||
Disconnect(1);
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID,
|
||||
"disconnecting player 1 stole player 2's pad");
|
||||
Disconnect(2);
|
||||
Connect(2);
|
||||
Check(GetActiveControllerId() == 2, "active controller did not change");
|
||||
haptics.clear();
|
||||
effects.clear();
|
||||
@@ -394,6 +406,126 @@ void TestIndependentOutputsAndPadSwitch() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestPlayerUserIds() {
|
||||
Check(Config::IsConfiguredUserIdValid(Config::MAX_USER_ID) &&
|
||||
!Config::IsConfiguredUserIdValid(Config::MAX_USER_ID + 1) &&
|
||||
!Config::IsConfiguredUserIdValid(254) && !Config::IsConfiguredUserIdValid(255),
|
||||
"configured user IDs did not reserve a valid range for guests");
|
||||
Config::ConfigOptions options;
|
||||
options.user_id = Config::MAX_USER_ID;
|
||||
Config::Load(options);
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(Config::GetPlayerFromUserId(Config::GetPlayerUserId(player)) == player,
|
||||
"player user ID did not round-trip at the upper limit");
|
||||
}
|
||||
Check(Config::GetPlayerUserId(3) == std::numeric_limits<int32_t>::max() &&
|
||||
Config::GetPlayerFromUserId(-1) == -1,
|
||||
"guest user ID overflowed or accepted an unknown user");
|
||||
Config::Load(Config::ConfigOptions {});
|
||||
}
|
||||
|
||||
void TestMultiplayerRouting() {
|
||||
Controller controller;
|
||||
for (int player = 1; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(PadOpen(Config::GetPlayerUserId(player), 0, 0, nullptr) == player + 1,
|
||||
"a guest could not open its own pad port");
|
||||
}
|
||||
for (int id = 2; id <= 5; id++) {
|
||||
Connect(id);
|
||||
}
|
||||
Connect(2); // Duplicate device reports must not consume another port.
|
||||
const auto read = [](int handle) {
|
||||
PadData data {};
|
||||
Check(PadReadState(handle, &data) == OK, "player pad read failed");
|
||||
return data;
|
||||
};
|
||||
SetButton(1, PAD_BUTTON_CROSS, true);
|
||||
SetButton(2, PAD_BUTTON_CIRCLE, true);
|
||||
SetButton(5, PAD_BUTTON_TRIANGLE, true);
|
||||
Check(read(1).buttons == PAD_BUTTON_CROSS && read(2).buttons == PAD_BUTTON_CIRCLE &&
|
||||
read(3).buttons == 0 && read(4).buttons == 0,
|
||||
"physical pads did not route independently or overflow reached a player");
|
||||
|
||||
SetRumble(100, 50);
|
||||
CycleKeyboardPlayer(); // Dedicated player 1; physical pad 1 waits.
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID &&
|
||||
read(2).buttons == PAD_BUTTON_CIRCLE && pad_rumble[1].large == 0 &&
|
||||
pad_rumble[1].small == 0,
|
||||
"keyboard reassignment changed an unaffected player or left pad output "
|
||||
"running");
|
||||
for (int mode = 2; mode <= Config::MAX_PLAYERS; mode++) {
|
||||
CycleKeyboardPlayer();
|
||||
}
|
||||
CycleKeyboardPlayer(); // Shared player 1; all original pads must return.
|
||||
ResetInputState();
|
||||
for (int id = 1; id <= 5; id++) {
|
||||
SetButton(id, 1u << id, true);
|
||||
}
|
||||
for (int handle = 1; handle <= Config::MAX_PLAYERS; handle++) {
|
||||
Check(read(handle).buttons == (1u << handle),
|
||||
"cycling keyboard players lost or duplicated a connected pad");
|
||||
}
|
||||
|
||||
// Moving the keyboard clears its held input from the old player.
|
||||
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_TRIANGLE, true);
|
||||
CycleKeyboardPlayer();
|
||||
CycleKeyboardPlayer();
|
||||
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_TRIANGLE, true);
|
||||
CycleKeyboardPlayer(); // Keyboard now moves from player 2 to player 3.
|
||||
Check((read(2).buttons & PAD_BUTTON_TRIANGLE) == 0,
|
||||
"moving the keyboard retained a held button on its old player");
|
||||
CycleKeyboardPlayer();
|
||||
CycleKeyboardPlayer();
|
||||
Disconnect(2);
|
||||
SetButton(5, PAD_BUTTON_SQUARE, true);
|
||||
Check(read(2).buttons == PAD_BUTTON_SQUARE && GetActiveControllerId() == 1,
|
||||
"waiting pad did not fill a free slot or an unaffected pad moved");
|
||||
Disconnect(99);
|
||||
Check(PadCheckHandle(std::numeric_limits<int>::min()) == PAD_ERROR_INVALID_HANDLE &&
|
||||
PadClose(std::numeric_limits<int>::max()) == PAD_ERROR_INVALID_HANDLE,
|
||||
"an extreme handle aliased a player port");
|
||||
closed_pads.clear();
|
||||
EmergencyShutdown();
|
||||
std::sort(closed_pads.begin(), closed_pads.end());
|
||||
Check(closed_pads == std::vector<int> {1, 3, 4, 5},
|
||||
"shutdown did not close each registered host pad exactly once");
|
||||
}
|
||||
|
||||
void TestDedicatedKeyboardOutputs() {
|
||||
Config::ConfigOptions options;
|
||||
options.keyboard_player = 1;
|
||||
options.controller_color = Config::ControllerColor {30, 40, 50};
|
||||
Config::Load(options);
|
||||
Initialize();
|
||||
PadInit();
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(PadOpen(Config::GetPlayerUserId(player), 0, 0, nullptr) == player + 1,
|
||||
"dedicated keyboard port did not open");
|
||||
}
|
||||
Connect(1);
|
||||
Connect(2);
|
||||
const PadVibrationParam request {100, 50};
|
||||
Check(PadSetVibration(3, &request) == OK, "player three vibration failed");
|
||||
SetButton(2, PAD_BUTTON_CROSS, true);
|
||||
CycleKeyboardPlayer(); // Pad 1 moves from player 2 to player 1.
|
||||
PadData data {};
|
||||
Check(GetActiveControllerId() == 1 && pad_colors[1] == *options.controller_color,
|
||||
"moving a pad to an earlier slot cleared its configured lightbar");
|
||||
Check(PadReadState(3, &data) == OK && data.buttons == PAD_BUTTON_CROSS &&
|
||||
pad_rumble[2].large == 100 * 257 && pad_rumble[2].small == 50 * 257,
|
||||
"keyboard reassignment reset an unaffected player's input or output");
|
||||
haptics_handles_rumble = true;
|
||||
SetRumble(200, 100);
|
||||
for (int mode = 0; mode < 4; mode++) {
|
||||
CycleKeyboardPlayer();
|
||||
}
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID && haptics.back().large == 0 &&
|
||||
haptics.back().small == 0 && haptics.back().duration == 0,
|
||||
"moving player one's pad retained its DualSense vibration request");
|
||||
Shutdown();
|
||||
Config::Load(Config::ConfigOptions {});
|
||||
}
|
||||
|
||||
void TestTriggerEffectState() {
|
||||
Controller controller;
|
||||
PadTriggerEffectStateInformation info {{-1, -1}};
|
||||
@@ -553,6 +685,7 @@ void TestTriggerEffectState() {
|
||||
|
||||
int main() {
|
||||
Config::Initialize();
|
||||
TestPlayerUserIds();
|
||||
TestPadPortOwnership();
|
||||
TestSettingCycles();
|
||||
TestGlobalControllerLevels();
|
||||
@@ -561,6 +694,8 @@ int main() {
|
||||
TestMaskedTriggersAndValidation();
|
||||
TestIndependentOutputsAndPadSwitch();
|
||||
TestTriggerEffectState();
|
||||
TestMultiplayerRouting();
|
||||
TestDedicatedKeyboardOutputs();
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ void TestClassicSavePaths() {
|
||||
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||
}
|
||||
Reset("CLASSIC");
|
||||
for (const char* text: {"", ".", "..", "../save1", "save/1", "save\\1"}) {
|
||||
for (const char* text: {"", ".", "..", "../save1", "save/1", "save\\1", "[users]"}) {
|
||||
auto name = DirName(text);
|
||||
struct SaveDataMount3 mount {};
|
||||
mount.user_id = 1;
|
||||
@@ -396,6 +396,57 @@ void TestClassicSavePaths() {
|
||||
CHECK(Search(1) == names);
|
||||
}
|
||||
|
||||
void TestGuestSaveIsolation() {
|
||||
Reset("MULTIUSER");
|
||||
const auto name = DirName("slot");
|
||||
const std::array<int32_t, 3> users {Config::GetUserId(), Config::GetPlayerUserId(1),
|
||||
Config::GetPlayerUserId(2)};
|
||||
std::array<fs::path, users.size()> paths;
|
||||
for (size_t i = 0; i < users.size(); ++i) {
|
||||
CHECK(Search(users[i]).empty());
|
||||
struct SaveDataMount3 mount {};
|
||||
mount.user_id = users[i];
|
||||
mount.dir_name = &name;
|
||||
mount.mount_mode = 4;
|
||||
mount.blocks = 48;
|
||||
SaveDataMountResult result {};
|
||||
CHECK(SaveDataMount3(&mount, &result) == OK);
|
||||
paths[i] = g_mounted_directory;
|
||||
std::ofstream(paths[i] / "progress") << users[i];
|
||||
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||
CHECK(Search(users[i]) == std::vector<std::string> {"slot"});
|
||||
}
|
||||
CHECK(paths[0] == fs::path("_SaveData") / "MULTIUSER" / "slot");
|
||||
CHECK(paths[0] != paths[1] && paths[1] != paths[2] && paths[0] != paths[2]);
|
||||
SceSaveDataTitleId title {};
|
||||
std::strcpy(title.data, "MULTIUSER");
|
||||
Reset("OTHER");
|
||||
for (size_t i = 0; i < users.size(); ++i) {
|
||||
CHECK(Search(users[i]).empty());
|
||||
CHECK(Search(users[i], &title) == std::vector<std::string> {"slot"});
|
||||
struct SaveDataTransferringMount transfer {};
|
||||
transfer.user_id = users[i];
|
||||
transfer.title_id = &title;
|
||||
transfer.dir_name = &name;
|
||||
SaveDataMountResult result {};
|
||||
CHECK(SaveDataTransferringMount(&transfer, &result) == OK);
|
||||
CHECK(g_mounted_directory == paths[i]);
|
||||
std::ifstream progress(paths[i] / "progress");
|
||||
int saved_user = -1;
|
||||
progress >> saved_user;
|
||||
CHECK(saved_user == users[i]);
|
||||
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||
}
|
||||
struct SaveDataDelete remove {};
|
||||
remove.user_id = users[1];
|
||||
remove.title_id = &title;
|
||||
remove.dir_name = &name;
|
||||
CHECK(SaveDataDelete(&remove) == OK);
|
||||
CHECK(Search(users[1], &title).empty());
|
||||
CHECK(Search(users[0], &title) == std::vector<std::string> {"slot"});
|
||||
CHECK(Search(users[2], &title) == std::vector<std::string> {"slot"});
|
||||
}
|
||||
|
||||
void TestSaveAllocations() {
|
||||
Reset("CAPACITY");
|
||||
std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")};
|
||||
@@ -596,6 +647,7 @@ void RunChild(const fs::path& executable, const char* mode) {
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Config::Initialize();
|
||||
if (argc == 2) {
|
||||
if (std::string_view(argv[1]) == "write") {
|
||||
WriteRestartFixture();
|
||||
@@ -603,6 +655,7 @@ int main(int argc, char** argv) {
|
||||
CHECK(std::string_view(argv[1]) == "read");
|
||||
ReadRestartFixture();
|
||||
}
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
const auto executable = fs::absolute(argv[0]);
|
||||
@@ -617,10 +670,12 @@ int main(int argc, char** argv) {
|
||||
TestIsolationAndSync();
|
||||
TestFailedWritePreservesSave();
|
||||
TestClassicSavePaths();
|
||||
TestGuestSaveIsolation();
|
||||
TestSaveAllocations();
|
||||
TestClassicSaveParams();
|
||||
CHECK(SaveDataTerminate() == OK);
|
||||
fs::current_path(previous);
|
||||
fs::remove_all(temp);
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user