Support multiple controllers for local multiplayer (#1583)

* Support multiple controllers for local co-op
This commit is contained in:
Felipe
2026-10-17 08:15:22 -03:00
committed by GitHub
parent 890393fbe6
commit 415387f00d
46 changed files with 664 additions and 196 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "تم كسب الكأس!" "trophy.toast.earned": "تم كسب الكأس!",
"user.guest": "ضيف"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trofej získána!" "trophy.toast.earned": "Trofej získána!",
"user.guest": "Host"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trofæ optjent!" "trophy.toast.earned": "Trofæ optjent!",
"user.guest": "Gæst"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trophäe erhalten!" "trophy.toast.earned": "Trophäe erhalten!",
"user.guest": "Gast"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!" "trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!",
"user.guest": "Επισκ."
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trophy earned!" "trophy.toast.earned": "Trophy earned!",
"user.guest": "Guest"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trophy earned!" "trophy.toast.earned": "Trophy earned!",
"user.guest": "Guest"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "¡Ganaste un trofeo!" "trophy.toast.earned": "¡Ganaste un trofeo!",
"user.guest": "Invitado"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "¡Ganaste un trofeo!" "trophy.toast.earned": "¡Ganaste un trofeo!",
"user.guest": "Invitado"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Pokaali ansaittu!" "trophy.toast.earned": "Pokaali ansaittu!",
"user.guest": "Vieras"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trophée obtenu !" "trophy.toast.earned": "Trophée obtenu !",
"user.guest": "Invité"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trophée obtenu !" "trophy.toast.earned": "Trophée obtenu !",
"user.guest": "Invité"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trófea megszerezve!" "trophy.toast.earned": "Trófea megszerezve!",
"user.guest": "Vendég"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trofi diperoleh!" "trophy.toast.earned": "Trofi diperoleh!",
"user.guest": "Tamu"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trofeo ottenuto!" "trophy.toast.earned": "Trofeo ottenuto!",
"user.guest": "Ospite"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "トロフィーを獲得しました!" "trophy.toast.earned": "トロフィーを獲得しました!",
"user.guest": "ゲスト"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "트로피를 획득했습니다!" "trophy.toast.earned": "트로피를 획득했습니다!",
"user.guest": "게스트"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trofee behaald!" "trophy.toast.earned": "Trofee behaald!",
"user.guest": "Gast"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trofé opptjent!" "trophy.toast.earned": "Trofé opptjent!",
"user.guest": "Gjest"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Zdobyto trofeum!" "trophy.toast.earned": "Zdobyto trofeum!",
"user.guest": "Gość"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Troféu conquistado!" "trophy.toast.earned": "Troféu conquistado!",
"user.guest": "Convidado"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Troféu conquistado!" "trophy.toast.earned": "Troféu conquistado!",
"user.guest": "Convidado"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trofeu câștigat!" "trophy.toast.earned": "Trofeu câștigat!",
"user.guest": "Invitat"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Трофей получен!" "trophy.toast.earned": "Трофей получен!",
"user.guest": "Гость"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Trofé intjänad!" "trophy.toast.earned": "Trofé intjänad!",
"user.guest": "Gäst"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!" "trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!",
"user.guest": "แขก"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Kupa kazanıldı!" "trophy.toast.earned": "Kupa kazanıldı!",
"user.guest": "Misafir"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "Đã nhận cúp!" "trophy.toast.earned": "Đã nhận cúp!",
"user.guest": "Khách"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "已获得奖杯!" "trophy.toast.earned": "已获得奖杯!",
"user.guest": "访客"
} }
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"trophy.toast.earned": "已獲得獎盃!" "trophy.toast.earned": "已獲得獎盃!",
"user.guest": "訪客"
} }
+22
View File
@@ -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(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id)); EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100); 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; *g_config = cfg;
} }
@@ -60,6 +61,27 @@ uint32_t GetControllerVibrationIntensity() {
return g_config->controller_vibration_intensity; 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() { PresentMode GetPresentMode() {
return g_config->present_mode; return g_config->present_mode;
} }
+17 -1
View File
@@ -6,6 +6,7 @@
#include <cstddef> #include <cstddef>
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <limits>
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -35,10 +36,16 @@ constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
constexpr std::size_t MAX_USER_NAME_LENGTH = 16; constexpr std::size_t MAX_USER_NAME_LENGTH = 16;
constexpr int32_t DEFAULT_USER_ID = 1000; 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 bool IsConfiguredUserIdValid(int32_t user_id) {
constexpr int32_t USER_ID_EVERYONE = 0xfe; constexpr int32_t USER_ID_EVERYONE = 0xfe;
constexpr int32_t USER_ID_SYSTEM = 0xff; 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 { struct ConfigOptions {
@@ -50,6 +57,8 @@ struct ConfigOptions {
std::optional<ControllerColor> controller_color; std::optional<ControllerColor> controller_color;
uint32_t controller_speaker_volume = 50; uint32_t controller_speaker_volume = 50;
uint32_t controller_vibration_intensity = 100; 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; PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1; int32_t gpu_index = -1;
bool fullscreen_enabled = false; bool fullscreen_enabled = false;
@@ -100,6 +109,13 @@ const std::string& GetAudioInputDevice();
const std::optional<ControllerColor>& GetControllerColor(); const std::optional<ControllerColor>& GetControllerColor();
uint32_t GetControllerSpeakerVolume(); uint32_t GetControllerSpeakerVolume();
uint32_t GetControllerVibrationIntensity(); 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(); PresentMode GetPresentMode();
int32_t GetGpuIndex(); int32_t GetGpuIndex();
bool FullscreenEnabled(); bool FullscreenEnabled();
@@ -254,7 +254,8 @@ public:
} }
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_F1 || 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 || if (binding.control == INVALID_CONTROL || reserved ||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) { (binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
EXIT("Invalid input mapping: %s\n", value.c_str()); EXIT("Invalid input mapping: %s\n", value.c_str());
@@ -466,6 +466,13 @@ void WindowContext::ProcessEvent(double time_s) {
} }
return; 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)) { if (ProcessSystemOverlayInput(*event)) {
return; return;
} }
@@ -66,7 +66,6 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QSpinBox" name="spinBox_user_id"> <widget class="QSpinBox" name="spinBox_user_id">
<property name="toolTip"><string>Local user ID exposed to games</string></property> <property name="toolTip"><string>Local user ID exposed to games</string></property>
<property name="maximum"><number>2147483647</number></property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
@@ -347,6 +346,16 @@
</item> </item>
</layout> </layout>
</item> </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> </layout>
</widget> </widget>
</item> </item>
+5
View File
@@ -48,11 +48,14 @@ struct ControllerSettings {
QString color; QString color;
int speaker_volume = 50; int speaker_volume = 50;
int vibration_intensity = 100; 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 { void WriteSettings(QSettings* s) const {
s->setValue("controller_color", color); s->setValue("controller_color", color);
s->setValue("controller_speaker_volume", speaker_volume); s->setValue("controller_speaker_volume", speaker_volume);
s->setValue("controller_vibration_intensity", vibration_intensity); s->setValue("controller_vibration_intensity", vibration_intensity);
s->setValue("controller_keyboard_player", keyboard_player);
} }
void ReadSettings(QSettings* s) { void ReadSettings(QSettings* s) {
@@ -65,6 +68,8 @@ struct ControllerSettings {
}; };
speaker_volume = read_percent("controller_speaker_volume", 50); speaker_volume = read_percent("controller_speaker_volume", 50);
vibration_intensity = read_percent("controller_vibration_intensity", 100); 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) { 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->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
m_ui->lineEdit_user_name->setText(info.user_name); 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); m_ui->spinBox_user_id->setValue(info.user_id);
UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color); UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color);
m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity); m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity);
m_ui->slider_controller_volume->setValue(info.controller.speaker_volume); 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; auto* microphone = m_ui->comboBox_audio_input_device;
microphone->clear(); microphone->clear();
microphone->addItem(tr("None"), QString {}); 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.color = ui.button_controller_color->property("controllerColor").toString();
info.controller.vibration_intensity = ui.slider_controller_vibration->value(); info.controller.vibration_intensity = ui.slider_controller_vibration->value();
info.controller.speaker_volume = ui.slider_controller_volume->value(); info.controller.speaker_volume = ui.slider_controller_volume->value();
info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt();
} }
info.screen_resolution = info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText()); TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
+1
View File
@@ -246,6 +246,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
} }
args << "--controller-volume" << QString::number(info.controller.speaker_volume); args << "--controller-volume" << QString::number(info.controller.speaker_volume);
args << "--controller-vibration" << QString::number(info.controller.vibration_intensity); 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); args << "--present-mode" << EnumToText(info.present_mode);
if (info.gpu_index >= 0) { if (info.gpu_index >= 0) {
args << "--gpu" << QString::number(info.gpu_index); args << "--gpu" << QString::number(info.gpu_index);
+245 -124
View File
@@ -18,6 +18,7 @@
#include <atomic> #include <atomic>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include <memory>
#include <vector> #include <vector>
namespace Libs::Controller { namespace Libs::Controller {
@@ -107,18 +108,19 @@ struct ControllerState {
class GameController { class GameController {
public: public:
GameController() = default; explicit GameController(int player): m_player(player) {}
KYTY_CLASS_NO_COPY(GameController); KYTY_CLASS_NO_COPY(GameController);
[[nodiscard]] int Handle() const { return PAD_HANDLE + m_player; }
int InitPort(); int InitPort();
int OpenPort(int user_id, int type, int index); int OpenPort(int user_id, int type, int index);
int GetPortHandle(int user_id, int type, int index); int GetPortHandle(int user_id, int type, int index);
int ClosePort(int handle); int ClosePort(int handle);
int CheckPortHandle(int handle); int CheckPortHandle(int handle);
void Connect(int id); void SetDevices(int active_id);
void Disconnect(int id);
void Button(int id, uint32_t button, bool down); void Button(int id, uint32_t button, bool down);
void Axis(int id, Axis axis, int value); void Axis(int id, Axis axis, int value);
void RightStick(int id, int x, int y); void RightStick(int id, int x, int y);
@@ -128,7 +130,6 @@ public:
void ResetOrientation(); void ResetOrientation();
void ToggleGyroEmulation(int id); void ToggleGyroEmulation(int id);
void ResetInputState(); void ResetInputState();
void ReleaseHostPads();
void GetConnectionInfo(bool* flag, int* count); void GetConnectionInfo(bool* flag, int* count);
void SetVibration(uint8_t large_motor, uint8_t small_motor); void SetVibration(uint8_t large_motor, uint8_t small_motor);
int GetActiveControllerId(); int GetActiveControllerId();
@@ -145,8 +146,10 @@ private:
enum class PortState { Uninitialized, Closed, Open }; enum class PortState { Uninitialized, Closed, Open };
int CheckPortHandleLocked(int handle) const; int CheckPortHandleLocked(int handle) const;
[[nodiscard]] bool PadOpenArgsAreValid(int user_id, int type, int index) const;
const int m_player;
void CheckActive();
void AddState(); void AddState();
// Emulated tilt: the right stick drives pitch and roll instead of the stick axes. // Emulated tilt: the right stick drives pitch and roll instead of the stick axes.
void UpdateEmulatedTilt(); void UpdateEmulatedTilt();
@@ -157,7 +160,6 @@ private:
Common::Mutex m_mutex; Common::Mutex m_mutex;
PortState m_port_state = PortState::Uninitialized; PortState m_port_state = PortState::Uninitialized;
std::vector<int> m_connected_ids;
int m_active_id = -1; int m_active_id = -1;
bool m_connected = false; bool m_connected = false;
int m_connected_count = 0; int m_connected_count = 0;
@@ -184,7 +186,39 @@ private:
std::array<std::atomic<uint32_t>, 3> m_setting_steps {}; 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, static void pad_fill_data(PadData* data, const ControllerState& state, bool connected,
int connected_count) { 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}; constexpr std::array INTENSITY = {1.0f, 0.0f, 0.33f, 0.66f};
void CycleSetting(Setting setting) { void CycleSetting(Setting setting) {
if (g_controller != nullptr) { for (const auto& player: g_players) {
g_controller->CycleSetting(setting); if (player != nullptr) {
player->CycleSetting(setting);
}
} }
} }
float GetSettingScale(Setting 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) { void GameController::CycleSetting(Setting setting) {
@@ -404,35 +440,53 @@ static int32_t TriggerEffectState(const PadTriggerEffectCommand& command, int va
} }
void Initialize() { void Initialize() {
EXIT_IF(g_controller != nullptr); EXIT_IF(g_players[0] != nullptr);
g_controller = new GameController; for (int i = 0; i < Config::MAX_PLAYERS; i++) {
g_controller->Connect(HOST_INPUT_CONTROLLER_ID); g_players[i] = std::make_unique<GameController>(i);
}
g_keyboard_player = Config::GetKeyboardPlayer();
RefreshPlayers();
} }
void Shutdown() { void Shutdown() {
EmergencyShutdown(); EmergencyShutdown();
delete g_controller; for (auto& player: g_players) {
g_controller = nullptr; player.reset();
}
} }
void EmergencyShutdown() { void EmergencyShutdown() {
if (g_controller != nullptr) { Common::LockGuard lock(g_router_mutex);
g_controller->ReleaseHostPads(); 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); Common::LockGuard lock(m_mutex);
if (m_active_id == active_id) {
if (std::find(m_connected_ids.begin(), m_connected_ids.end(), id) != m_connected_ids.end()) {
return; return;
} }
if (active_id != HOST_INPUT_CONTROLLER_ID && active_id != -1) {
m_connected_ids.push_back(id); if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(active_id));
if (id != HOST_INPUT_CONTROLLER_ID) {
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
pad != nullptr) { pad != nullptr) {
if (const auto& color = Config::GetControllerColor()) { if (const auto& color = Config::GetControllerColor()) {
(void)SDL_SetGamepadLED(pad, (*color)[0], (*color)[1], (*color)[2]); (void)SDL_SetGamepadLED(pad, (*color)[0], (*color)[1], (*color)[2]);
@@ -446,42 +500,11 @@ void GameController::Connect(int id) {
} }
} }
CheckActive(); const bool new_connected = active_id != -1;
}
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;
}
if (!m_connected && new_connected) { if (!m_connected && new_connected) {
m_connected_count++; m_connected_count++;
} }
m_active_id = new_active_id; m_active_id = active_id;
m_connected = new_connected; m_connected = new_connected;
m_state = {}; m_state = {};
m_gyro_time = 0; m_gyro_time = 0;
@@ -513,7 +536,7 @@ void GameController::AddState() {
void GameController::Button(int id, uint32_t button, bool down) { void GameController::Button(int id, uint32_t button, bool down) {
Common::LockGuard lock(m_mutex); 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) { if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
m_state.time = LibKernel::KernelGetProcessTime(); m_state.time = LibKernel::KernelGetProcessTime();
@@ -798,39 +821,16 @@ void GameController::ResetInputState() {
AddState(); AddState();
} }
void GameController::ReleaseHostPads() { static void StopHostPad(SDL_Gamepad* pad) {
Common::LockGuard lock(m_mutex); if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
DualSenseHaptics::Shutdown(); DualSenseEffects effect {};
effect.enable_bits = 0x0c;
std::vector<SDL_Gamepad*> pads; effect.right_trigger[0] = 0x05;
for (const auto id: m_connected_ids) { effect.left_trigger[0] = 0x05;
if (id == HOST_INPUT_CONTROLLER_ID) { (void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
continue;
}
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
pad != nullptr) {
if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
DualSenseEffects effect {};
effect.enable_bits = 0x0c;
effect.right_trigger[0] = 0x05;
effect.left_trigger[0] = 0x05;
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
}
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
(void)SDL_SetGamepadLED(pad, 0, 0, 0);
pads.push_back(pad);
}
} }
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
if (!pads.empty()) { (void)SDL_SetGamepadLED(pad, 0, 0, 0);
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) { 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 scale = duration != 0 ? GetSettingScale(Setting::VibrationIntensity) : 0.0f;
const auto large_motor = Scale(m_vibration[0], scale); const auto large_motor = Scale(m_vibration[0], scale);
const auto small_motor = Scale(m_vibration[1], 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; return;
} }
@@ -999,49 +1001,149 @@ int GameController::ReadStates(ControllerState* states, int states_num, bool* fl
return ret_num; 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { void ToggleGyroEmulation(int id) {
g_controller->ToggleGyroEmulation(id); Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->ToggleGyroEmulation(id);
}
} }
void ResetInputState() { void ResetInputState() {
g_controller->ResetInputState(); for (const auto& player: g_players) {
if (player != nullptr) {
player->ResetInputState();
}
}
} }
int GetActiveControllerId() { 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) { bool IsPlayerConnected(int player) {
// The host keyboard and gamepad feed one standard port for the configured user. if (player < 0 || player >= Config::MAX_PLAYERS || g_players[player] == nullptr) {
return user_id == Config::GetUserId() && type == 0 && index == 0; 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() { int GameController::InitPort() {
@@ -1064,7 +1166,7 @@ int GameController::OpenPort(int user_id, int type, int index) {
return PAD_ERROR_ALREADY_OPENED; return PAD_ERROR_ALREADY_OPENED;
} }
m_port_state = PortState::Open; m_port_state = PortState::Open;
return PAD_HANDLE; return Handle();
} }
int GameController::GetPortHandle(int user_id, int type, int index) { int GameController::GetPortHandle(int user_id, int type, int index) {
@@ -1072,15 +1174,16 @@ int GameController::GetPortHandle(int user_id, int type, int index) {
if (m_port_state == PortState::Uninitialized) { if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED; 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)
: PAD_ERROR_NO_HANDLE; ? Handle()
: PAD_ERROR_NO_HANDLE;
} }
int GameController::CheckPortHandleLocked(int handle) const { int GameController::CheckPortHandleLocked(int handle) const {
if (m_port_state == PortState::Uninitialized) { if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED; 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) { int GameController::CheckPortHandle(int handle) {
@@ -1098,12 +1201,19 @@ int GameController::ClosePort(int handle) {
} }
int PadCheckHandle(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() { int KYTY_SYSV_ABI PadInit() {
PRINT_NAME(); 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) { 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", "\t param = 0x%016" PRIx64 "\n",
user_id, type, index, reinterpret_cast<uint64_t>(param)); 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) { 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", "\t index = %d\n",
user_id, type, index); 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) { int KYTY_SYSV_ABI PadClose(int handle) {
PRINT_NAME(); PRINT_NAME();
LOGF("\t handle = %d\n", handle); 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) { 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")); LOGF("\t enable = %s\n", (enable ? "true" : "false"));
g_controller->SetMotionSensorState(enable); PlayerForHandle(handle)->SetMotionSensorState(enable);
return OK; return OK;
} }
@@ -1167,7 +1284,7 @@ int KYTY_SYSV_ABI PadResetOrientation(int handle) {
return error; return error;
} }
g_controller->ResetOrientation(); PlayerForHandle(handle)->ResetOrientation();
return OK; return OK;
} }
@@ -1183,7 +1300,7 @@ int KYTY_SYSV_ABI PadGetControllerInformation(int handle, PadControllerInformati
int connected_count = 0; int connected_count = 0;
bool connected = false; bool connected = false;
g_controller->GetConnectionInfo(&connected, &connected_count); PlayerForHandle(handle)->GetConnectionInfo(&connected, &connected_count);
std::memset(info, 0, sizeof(*info)); std::memset(info, 0, sizeof(*info));
@@ -1228,7 +1345,7 @@ int KYTY_SYSV_ABI PadReadState(int handle, PadData* data) {
bool connected = false; bool connected = false;
ControllerState state; ControllerState state;
g_controller->ReadState(&state, &connected, &connected_count); PlayerForHandle(handle)->ReadState(&state, &connected, &connected_count);
pad_fill_data(data, 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; bool connected = false;
ControllerState states[64] = {}; 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 || ret_num == 0) {
if (connected) { if (connected) {
g_controller->ReadState(&states[0], &connected, &connected_count); player->ReadState(&states[0], &connected, &connected_count);
} }
ret_num = 1; 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) { int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
PRINT_NAME(); PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) { if (const auto error = PadCheckHandle(handle); error != OK) {
return error; return error;
@@ -1282,7 +1401,7 @@ int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
"\t small_motor = %d\n", "\t small_motor = %d\n",
static_cast<int>(param->large_motor), static_cast<int>(param->small_motor)); 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; return OK;
} }
@@ -1299,6 +1418,7 @@ int KYTY_SYSV_ABI PadResetLightBar(int handle) {
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) { int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
PRINT_NAME(); PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) { if (const auto error = PadCheckHandle(handle); error != OK) {
return error; return error;
@@ -1307,13 +1427,14 @@ int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
return PAD_ERROR_INVALID_ARG; 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; return OK;
} }
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param) { int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param) {
PRINT_NAME(); PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) { if (const auto error = PadCheckHandle(handle); error != OK) {
return error; return error;
@@ -1322,7 +1443,7 @@ int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* p
return PAD_ERROR_INVALID_ARG; 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) { 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; return PAD_ERROR_INVALID_ARG;
} }
g_controller->GetTriggerEffectState(info); PlayerForHandle(handle)->GetTriggerEffectState(info);
return OK; return OK;
} }
+4
View File
@@ -68,6 +68,8 @@ inline int controller_get_axis(int min, int max, int value) {
void Connect(int id); void Connect(int id);
void Disconnect(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 SetButton(int id, uint32_t button, bool down);
void SetAxis(int id, Axis axis, int value); void SetAxis(int id, Axis axis, int value);
void SetRightStick(int id, int x, int y); 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 ToggleGyroEmulation(int id);
void ResetInputState(); void ResetInputState();
int GetActiveControllerId(); int GetActiveControllerId();
// True when the player has a keyboard or gamepad assigned.
bool IsPlayerConnected(int player);
enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity }; enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity };
+13 -4
View File
@@ -1,6 +1,7 @@
#include "common/abi.h" #include "common/abi.h"
#include "common/assert.h" #include "common/assert.h"
#include "common/common.h" #include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h" #include "common/file.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/stringUtils.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)); 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, static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
int32_t user_id) { 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++) { for (uint32_t slot = 0; slot < 4; slot++) {
if (dir_name == memory_dir_name(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) { 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); Common::LockGuard lock(g_mount_mutex);
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id(); 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; std::vector<std::string> dir_list;
if (Common::File::IsDirectoryExisting(root)) { if (Common::File::IsDirectoryExisting(root)) {
+12 -3
View File
@@ -105,6 +105,8 @@ struct TrophyState {
const Trophies::Package* package = nullptr; const Trophies::Package* package = nullptr;
Trophies::UnlockData unlocks; Trophies::UnlockData unlocks;
std::filesystem::path path; std::filesystem::path path;
// Guests (players 2+) have no profile: nothing is loaded, saved or announced.
bool guest = false;
}; };
struct TrophyContext { struct TrophyContext {
@@ -153,9 +155,12 @@ static TrophyState* LoadTrophyState(TrophyKey key) {
package.event_rules.size(), uds_filename.c_str()); package.event_rules.size(), uds_filename.c_str());
} }
} }
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second); const bool guest = Config::IsGuestUserId(key.first);
auto unlocks = Trophies::LoadUnlockData(path); auto path = guest ? std::filesystem::path {}
return &g_states.emplace(key, TrophyState {&package, std::move(unlocks), std::move(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; .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) { 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); const auto found = state.package->trophies.find(trophy_id);
if (found == state.package->trophies.end()) { if (found == state.package->trophies.end()) {
LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id); LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id);
+54 -27
View File
@@ -1,12 +1,14 @@
#include "common/abi.h" #include "common/abi.h"
#include "common/assert.h" #include "common/assert.h"
#include "common/emulatorConfig.h" #include "common/emulatorConfig.h"
#include "common/localization.h"
#include "libs/controller.h"
#include "libs/errno.h" #include "libs/errno.h"
#include "libs/libs.h" #include "libs/libs.h"
#include "loader/symbolDatabase.h" #include "loader/symbolDatabase.h"
#include <cstdio>
#include <cstring> #include <cstring>
#include <string>
namespace Libs { namespace Libs {
@@ -39,6 +41,15 @@ struct UserServiceGamePresets {
static_assert(sizeof(UserServiceGamePresets) == 40); 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*/) { static KYTY_SYSV_ABI int UserServiceInitialize(const void* /*params*/) {
PRINT_NAME(); PRINT_NAME();
@@ -66,13 +77,16 @@ static KYTY_SYSV_ABI int UserServiceGetEvent(SceUserServiceEvent* event) {
EXIT_NOT_IMPLEMENTED(event == nullptr); EXIT_NOT_IMPLEMENTED(event == nullptr);
static bool logged_in = false; static bool logged_in[Config::MAX_PLAYERS] = {};
if (!logged_in) { for (int player = 0; player < Config::MAX_PLAYERS; player++) {
logged_in = true; const bool connected = IsPlayerLoggedIn(player);
event->event_type = UserServiceEventTypeLogin; if (connected != logged_in[player]) {
event->user_id = Config::GetUserId(); logged_in[player] = connected;
return OK; event->event_type = connected ? UserServiceEventTypeLogin : UserServiceEventTypeLogout;
event->user_id = Config::GetPlayerUserId(player);
return OK;
}
} }
return USER_SERVICE_ERROR_NO_EVENT; return USER_SERVICE_ERROR_NO_EVENT;
@@ -83,24 +97,36 @@ static KYTY_SYSV_ABI int UserServiceGetLoginUserIdList(UserServiceLoginUserIdLis
EXIT_NOT_IMPLEMENTED(user_id_list == nullptr); EXIT_NOT_IMPLEMENTED(user_id_list == nullptr);
user_id_list->user_id[0] = Config::GetUserId(); int count = 0;
user_id_list->user_id[1] = -1; for (int player = 0; player < Config::MAX_PLAYERS; player++) {
user_id_list->user_id[2] = -1; if (IsPlayerLoggedIn(player)) {
user_id_list->user_id[3] = -1; user_id_list->user_id[count++] = Config::GetPlayerUserId(player);
}
}
while (count < Config::MAX_PLAYERS) {
user_id_list->user_id[count++] = -1;
}
return OK; return OK;
} }
static KYTY_SYSV_ABI int UserServiceGetUserName(int user_id, char* name, size_t size) { 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; return USER_SERVICE_ERROR_NOT_LOGGED_IN;
} }
EXIT_NOT_IMPLEMENTED(size < 5);
const auto& user_name = Config::GetUserName(); const auto user_name =
int s = snprintf(name, size, "%s", user_name.c_str()); player > 0 ? std::string(Common::Localization::Get(Config::GetConsoleLanguage(), "user.guest")) +
" " + std::to_string(player + 1)
EXIT_NOT_IMPLEMENTED(static_cast<size_t>(s) >= size); : 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; return OK;
} }
@@ -111,11 +137,12 @@ static KYTY_SYSV_ABI int UserServiceGetUserNumber(int user_id, int32_t* number)
if (number == nullptr) { if (number == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; 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; return USER_SERVICE_ERROR_NOT_LOGGED_IN;
} }
*number = 1; *number = player + 1;
return OK; return OK;
} }
@@ -126,7 +153,7 @@ static KYTY_SYSV_ABI int UserServiceGetGamePresets(int user_id, UserServiceGameP
if (presets == nullptr) { if (presets == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; return USER_SERVICE_ERROR_INVALID_ARGUMENT;
} }
if (user_id != Config::GetUserId()) { if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN; return USER_SERVICE_ERROR_NOT_LOGGED_IN;
} }
@@ -147,7 +174,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityVibration(int user_id, int32
if (vibration == nullptr) { if (vibration == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; return USER_SERVICE_ERROR_INVALID_ARGUMENT;
} }
if (user_id != Config::GetUserId()) { if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN; return USER_SERVICE_ERROR_NOT_LOGGED_IN;
} }
@@ -163,7 +190,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityTriggerEffect(int user_
if (trigger_effect == nullptr) { if (trigger_effect == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; return USER_SERVICE_ERROR_INVALID_ARGUMENT;
} }
if (user_id != Config::GetUserId()) { if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN; 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) { if (age_level == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; return USER_SERVICE_ERROR_INVALID_ARGUMENT;
} }
if (user_id != Config::GetUserId()) { if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN; return USER_SERVICE_ERROR_NOT_LOGGED_IN;
} }
@@ -194,7 +221,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityChatTranscription(int u
if (chat_transcription == nullptr) { if (chat_transcription == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; return USER_SERVICE_ERROR_INVALID_ARGUMENT;
} }
if (user_id != Config::GetUserId()) { if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN; 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) { if (press_and_hold_delay == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; return USER_SERVICE_ERROR_INVALID_ARGUMENT;
} }
if (user_id != Config::GetUserId()) { if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN; return USER_SERVICE_ERROR_NOT_LOGGED_IN;
} }
@@ -226,7 +253,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomEnabled(int user_id
if (zoom_enabled == nullptr) { if (zoom_enabled == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; return USER_SERVICE_ERROR_INVALID_ARGUMENT;
} }
if (user_id != Config::GetUserId()) { if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN; return USER_SERVICE_ERROR_NOT_LOGGED_IN;
} }
@@ -242,7 +269,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomFollowFocus(int use
if (zoom_follow_focus == nullptr) { if (zoom_follow_focus == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT; return USER_SERVICE_ERROR_INVALID_ARGUMENT;
} }
if (user_id != Config::GetUserId()) { if (GetLoggedInPlayer(user_id) < 0) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN; return USER_SERVICE_ERROR_NOT_LOGGED_IN;
} }
+7
View File
@@ -57,6 +57,7 @@ static void PrintUsage() {
::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n"); ::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n");
::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 50.\n"); ::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 50.\n");
::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\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( ::printf(
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n"); " --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n");
::printf( ::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()); ::printf("invalid controller vibration intensity: %s\n", value.c_str());
return false; 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") { } else if (arg == "--present-mode") {
if (!ParseEnum(value, options.config.present_mode)) { if (!ParseEnum(value, options.config.present_mode)) {
::printf("invalid present mode: %s\n", value.c_str()); ::printf("invalid present mode: %s\n", value.c_str());
+139 -4
View File
@@ -4,6 +4,7 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <limits>
#include <vector> #include <vector>
namespace { namespace {
@@ -23,6 +24,9 @@ std::vector<Rumble> rumble;
std::vector<Rumble> haptics; std::vector<Rumble> haptics;
std::vector<std::array<Uint8, 32>> effects; std::vector<std::array<Uint8, 32>> effects;
bool haptics_handles_rumble = false; 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
namespace Fake { namespace Fake {
@@ -30,14 +34,15 @@ Uint64 GetTicks() {
return now; return now;
} }
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) { 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; : nullptr;
} }
SDL_GamepadType GetGamepadType(SDL_Gamepad*) { SDL_GamepadType GetGamepadType(SDL_Gamepad*) {
return SDL_GAMEPAD_TYPE_PS5; 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}); rumble.push_back({large, small, duration});
pad_rumble[reinterpret_cast<uintptr_t>(pad)] = rumble.back();
return true; return true;
} }
bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) { 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); effects.push_back(effect);
return true; 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; return true;
} }
bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) { bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
@@ -56,7 +62,9 @@ bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) { bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) {
return true; 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) {} void Delay(Uint32) {}
} // namespace Fake } // namespace Fake
@@ -368,6 +376,10 @@ void TestIndependentOutputsAndPadSwitch() {
"speaker volume resent controller effects"); "speaker volume resent controller effects");
Connect(2); Connect(2);
Disconnect(1); 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"); Check(GetActiveControllerId() == 2, "active controller did not change");
haptics.clear(); haptics.clear();
effects.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() { void TestTriggerEffectState() {
Controller controller; Controller controller;
PadTriggerEffectStateInformation info {{-1, -1}}; PadTriggerEffectStateInformation info {{-1, -1}};
@@ -553,6 +685,7 @@ void TestTriggerEffectState() {
int main() { int main() {
Config::Initialize(); Config::Initialize();
TestPlayerUserIds();
TestPadPortOwnership(); TestPadPortOwnership();
TestSettingCycles(); TestSettingCycles();
TestGlobalControllerLevels(); TestGlobalControllerLevels();
@@ -561,6 +694,8 @@ int main() {
TestMaskedTriggersAndValidation(); TestMaskedTriggersAndValidation();
TestIndependentOutputsAndPadSwitch(); TestIndependentOutputsAndPadSwitch();
TestTriggerEffectState(); TestTriggerEffectState();
TestMultiplayerRouting();
TestDedicatedKeyboardOutputs();
Config::Shutdown(); Config::Shutdown();
return 0; return 0;
} }
+56 -1
View File
@@ -378,7 +378,7 @@ void TestClassicSavePaths() {
CHECK(SaveDataUmount2(0, &result.mount_point) == OK); CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
} }
Reset("CLASSIC"); 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); auto name = DirName(text);
struct SaveDataMount3 mount {}; struct SaveDataMount3 mount {};
mount.user_id = 1; mount.user_id = 1;
@@ -396,6 +396,57 @@ void TestClassicSavePaths() {
CHECK(Search(1) == names); 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() { void TestSaveAllocations() {
Reset("CAPACITY"); Reset("CAPACITY");
std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")}; std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")};
@@ -596,6 +647,7 @@ void RunChild(const fs::path& executable, const char* mode) {
} // namespace } // namespace
int main(int argc, char** argv) { int main(int argc, char** argv) {
Config::Initialize();
if (argc == 2) { if (argc == 2) {
if (std::string_view(argv[1]) == "write") { if (std::string_view(argv[1]) == "write") {
WriteRestartFixture(); WriteRestartFixture();
@@ -603,6 +655,7 @@ int main(int argc, char** argv) {
CHECK(std::string_view(argv[1]) == "read"); CHECK(std::string_view(argv[1]) == "read");
ReadRestartFixture(); ReadRestartFixture();
} }
Config::Shutdown();
return 0; return 0;
} }
const auto executable = fs::absolute(argv[0]); const auto executable = fs::absolute(argv[0]);
@@ -617,10 +670,12 @@ int main(int argc, char** argv) {
TestIsolationAndSync(); TestIsolationAndSync();
TestFailedWritePreservesSave(); TestFailedWritePreservesSave();
TestClassicSavePaths(); TestClassicSavePaths();
TestGuestSaveIsolation();
TestSaveAllocations(); TestSaveAllocations();
TestClassicSaveParams(); TestClassicSaveParams();
CHECK(SaveDataTerminate() == OK); CHECK(SaveDataTerminate() == OK);
fs::current_path(previous); fs::current_path(previous);
fs::remove_all(temp); fs::remove_all(temp);
Config::Shutdown();
return 0; return 0;
} }