hid: Implement GC controller
This commit is contained in:
parent
0896089092
commit
a994a40467
@ -31,6 +31,7 @@ struct ControllerParameters {
|
||||
bool allow_dual_joycons{};
|
||||
bool allow_left_joycon{};
|
||||
bool allow_right_joycon{};
|
||||
bool allow_gamecube_controller{};
|
||||
};
|
||||
|
||||
class ControllerApplet {
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
namespace Service::HID {
|
||||
constexpr s32 HID_JOYSTICK_MAX = 0x7fff;
|
||||
constexpr s32 HID_TRIGGER_MAX = 0x7fff;
|
||||
[[maybe_unused]] constexpr s32 HID_JOYSTICK_MIN = -0x7fff;
|
||||
constexpr std::size_t NPAD_OFFSET = 0x9A00;
|
||||
constexpr u32 BATTERY_FULL = 2;
|
||||
@ -48,6 +49,8 @@ Controller_NPad::NPadControllerType Controller_NPad::MapSettingsTypeToNPad(
|
||||
return NPadControllerType::JoyRight;
|
||||
case Settings::ControllerType::Handheld:
|
||||
return NPadControllerType::Handheld;
|
||||
case Settings::ControllerType::GameCube:
|
||||
return NPadControllerType::GameCube;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
return NPadControllerType::ProController;
|
||||
@ -67,6 +70,8 @@ Settings::ControllerType Controller_NPad::MapNPadToSettingsType(
|
||||
return Settings::ControllerType::RightJoycon;
|
||||
case NPadControllerType::Handheld:
|
||||
return Settings::ControllerType::Handheld;
|
||||
case NPadControllerType::GameCube:
|
||||
return Settings::ControllerType::GameCube;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
return Settings::ControllerType::ProController;
|
||||
@ -209,6 +214,13 @@ void Controller_NPad::InitNewlyAddedController(std::size_t controller_idx) {
|
||||
controller.assignment_mode = NpadAssignments::Single;
|
||||
controller.footer_type = AppletFooterUiType::JoyRightHorizontal;
|
||||
break;
|
||||
case NPadControllerType::GameCube:
|
||||
controller.style_set.gamecube.Assign(1);
|
||||
// The GC Controller behaves like a wired Pro Controller
|
||||
controller.device_type.fullkey.Assign(1);
|
||||
controller.system_properties.is_vertical.Assign(1);
|
||||
controller.system_properties.use_plus.Assign(1);
|
||||
break;
|
||||
case NPadControllerType::Pokeball:
|
||||
controller.style_set.palma.Assign(1);
|
||||
controller.device_type.palma.Assign(1);
|
||||
@ -259,6 +271,7 @@ void Controller_NPad::OnInit() {
|
||||
style.joycon_right.Assign(1);
|
||||
style.joycon_dual.Assign(1);
|
||||
style.fullkey.Assign(1);
|
||||
style.gamecube.Assign(1);
|
||||
style.palma.Assign(1);
|
||||
}
|
||||
|
||||
@ -339,6 +352,7 @@ void Controller_NPad::RequestPadStateUpdate(u32 npad_id) {
|
||||
auto& pad_state = npad_pad_states[controller_idx].pad_states;
|
||||
auto& lstick_entry = npad_pad_states[controller_idx].l_stick;
|
||||
auto& rstick_entry = npad_pad_states[controller_idx].r_stick;
|
||||
auto& trigger_entry = npad_trigger_states[controller_idx];
|
||||
const auto& button_state = buttons[controller_idx];
|
||||
const auto& analog_state = sticks[controller_idx];
|
||||
const auto [stick_l_x_f, stick_l_y_f] =
|
||||
@ -404,6 +418,17 @@ void Controller_NPad::RequestPadStateUpdate(u32 npad_id) {
|
||||
pad_state.left_sl.Assign(button_state[SL - BUTTON_HID_BEGIN]->GetStatus());
|
||||
pad_state.left_sr.Assign(button_state[SR - BUTTON_HID_BEGIN]->GetStatus());
|
||||
}
|
||||
|
||||
if (controller_type == NPadControllerType::GameCube) {
|
||||
trigger_entry.l_analog = static_cast<s32>(
|
||||
button_state[ZL - BUTTON_HID_BEGIN]->GetStatus() ? HID_TRIGGER_MAX : 0);
|
||||
trigger_entry.r_analog = static_cast<s32>(
|
||||
button_state[ZR - BUTTON_HID_BEGIN]->GetStatus() ? HID_TRIGGER_MAX : 0);
|
||||
pad_state.zl.Assign(false);
|
||||
pad_state.zr.Assign(button_state[R - BUTTON_HID_BEGIN]->GetStatus());
|
||||
pad_state.l.Assign(button_state[ZL - BUTTON_HID_BEGIN]->GetStatus());
|
||||
pad_state.r.Assign(button_state[ZR - BUTTON_HID_BEGIN]->GetStatus());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller_NPad::OnUpdate(const Core::Timing::CoreTiming& core_timing, u8* data,
|
||||
@ -418,6 +443,11 @@ void Controller_NPad::OnUpdate(const Core::Timing::CoreTiming& core_timing, u8*
|
||||
&npad.joy_left_states, &npad.joy_right_states, &npad.palma_states,
|
||||
&npad.system_ext_states};
|
||||
|
||||
// There is the posibility to have more controllers with analog triggers
|
||||
const std::array<TriggerGeneric*, 1> controller_triggers{
|
||||
&npad.gc_trigger_states,
|
||||
};
|
||||
|
||||
for (auto* main_controller : controller_npads) {
|
||||
main_controller->common.entry_count = 16;
|
||||
main_controller->common.total_entry_count = 17;
|
||||
@ -435,6 +465,21 @@ void Controller_NPad::OnUpdate(const Core::Timing::CoreTiming& core_timing, u8*
|
||||
cur_entry.timestamp2 = cur_entry.timestamp;
|
||||
}
|
||||
|
||||
for (auto* analog_trigger : controller_triggers) {
|
||||
analog_trigger->entry_count = 16;
|
||||
analog_trigger->total_entry_count = 17;
|
||||
|
||||
const auto& last_entry = analog_trigger->trigger[analog_trigger->last_entry_index];
|
||||
|
||||
analog_trigger->timestamp = core_timing.GetCPUTicks();
|
||||
analog_trigger->last_entry_index = (analog_trigger->last_entry_index + 1) % 17;
|
||||
|
||||
auto& cur_entry = analog_trigger->trigger[analog_trigger->last_entry_index];
|
||||
|
||||
cur_entry.timestamp = last_entry.timestamp + 1;
|
||||
cur_entry.timestamp2 = cur_entry.timestamp;
|
||||
}
|
||||
|
||||
const auto& controller_type = connected_controllers[i].type;
|
||||
|
||||
if (controller_type == NPadControllerType::None || !connected_controllers[i].is_connected) {
|
||||
@ -444,6 +489,7 @@ void Controller_NPad::OnUpdate(const Core::Timing::CoreTiming& core_timing, u8*
|
||||
|
||||
RequestPadStateUpdate(npad_index);
|
||||
auto& pad_state = npad_pad_states[npad_index];
|
||||
auto& trigger_state = npad_trigger_states[npad_index];
|
||||
|
||||
auto& main_controller =
|
||||
npad.fullkey_states.npad[npad.fullkey_states.common.last_entry_index];
|
||||
@ -456,6 +502,8 @@ void Controller_NPad::OnUpdate(const Core::Timing::CoreTiming& core_timing, u8*
|
||||
auto& pokeball_entry = npad.palma_states.npad[npad.palma_states.common.last_entry_index];
|
||||
auto& libnx_entry =
|
||||
npad.system_ext_states.npad[npad.system_ext_states.common.last_entry_index];
|
||||
auto& trigger_entry =
|
||||
npad.gc_trigger_states.trigger[npad.gc_trigger_states.last_entry_index];
|
||||
|
||||
libnx_entry.connection_status.raw = 0;
|
||||
libnx_entry.connection_status.is_connected.Assign(1);
|
||||
@ -524,6 +572,18 @@ void Controller_NPad::OnUpdate(const Core::Timing::CoreTiming& core_timing, u8*
|
||||
|
||||
libnx_entry.connection_status.is_right_connected.Assign(1);
|
||||
break;
|
||||
case NPadControllerType::GameCube:
|
||||
main_controller.connection_status.raw = 0;
|
||||
main_controller.connection_status.is_connected.Assign(1);
|
||||
main_controller.connection_status.is_wired.Assign(1);
|
||||
main_controller.pad.pad_states.raw = pad_state.pad_states.raw;
|
||||
main_controller.pad.l_stick = pad_state.l_stick;
|
||||
main_controller.pad.r_stick = pad_state.r_stick;
|
||||
trigger_entry.l_analog = trigger_state.l_analog;
|
||||
trigger_entry.r_analog = trigger_state.r_analog;
|
||||
|
||||
libnx_entry.connection_status.is_wired.Assign(1);
|
||||
break;
|
||||
case NPadControllerType::Pokeball:
|
||||
pokeball_entry.connection_status.raw = 0;
|
||||
pokeball_entry.connection_status.is_connected.Assign(1);
|
||||
@ -674,6 +734,7 @@ void Controller_NPad::OnMotionUpdate(const Core::Timing::CoreTiming& core_timing
|
||||
right_sixaxis_entry.orientation = motion_devices[1].orientation;
|
||||
}
|
||||
break;
|
||||
case NPadControllerType::GameCube:
|
||||
case NPadControllerType::Pokeball:
|
||||
break;
|
||||
}
|
||||
@ -1135,6 +1196,8 @@ bool Controller_NPad::IsControllerSupported(NPadControllerType controller) const
|
||||
return style.joycon_left;
|
||||
case NPadControllerType::JoyRight:
|
||||
return style.joycon_right;
|
||||
case NPadControllerType::GameCube:
|
||||
return style.gamecube;
|
||||
case NPadControllerType::Pokeball:
|
||||
return style.palma;
|
||||
default:
|
||||
|
@ -51,6 +51,7 @@ public:
|
||||
JoyDual,
|
||||
JoyLeft,
|
||||
JoyRight,
|
||||
GameCube,
|
||||
Pokeball,
|
||||
};
|
||||
|
||||
@ -60,6 +61,7 @@ public:
|
||||
JoyconDual = 5,
|
||||
JoyconLeft = 6,
|
||||
JoyconRight = 7,
|
||||
GameCube = 8,
|
||||
Pokeball = 9,
|
||||
MaxNpadType = 10,
|
||||
};
|
||||
@ -389,6 +391,25 @@ private:
|
||||
};
|
||||
static_assert(sizeof(SixAxisGeneric) == 0x708, "SixAxisGeneric is an invalid size");
|
||||
|
||||
struct TriggerState {
|
||||
s64_le timestamp{};
|
||||
s64_le timestamp2{};
|
||||
s32_le l_analog{};
|
||||
s32_le r_analog{};
|
||||
};
|
||||
static_assert(sizeof(TriggerState) == 0x18, "TriggerState is an invalid size");
|
||||
|
||||
struct TriggerGeneric {
|
||||
INSERT_PADDING_BYTES(0x4);
|
||||
s64_le timestamp;
|
||||
INSERT_PADDING_BYTES(0x4);
|
||||
s64_le total_entry_count;
|
||||
s64_le last_entry_index;
|
||||
s64_le entry_count;
|
||||
std::array<TriggerState, 17> trigger{};
|
||||
};
|
||||
static_assert(sizeof(TriggerGeneric) == 0x1C8, "TriggerGeneric is an invalid size");
|
||||
|
||||
struct NPadSystemProperties {
|
||||
union {
|
||||
s64_le raw{};
|
||||
@ -509,7 +530,9 @@ private:
|
||||
AppletFooterUiType footer_type;
|
||||
// nfc_states needs to be checked switchbrew does not match with HW
|
||||
NfcXcdHandle nfc_states;
|
||||
INSERT_PADDING_BYTES(0xdef);
|
||||
INSERT_PADDING_BYTES(0x8); // Mutex
|
||||
TriggerGeneric gc_trigger_states;
|
||||
INSERT_PADDING_BYTES(0xc1f);
|
||||
};
|
||||
static_assert(sizeof(NPadEntry) == 0x5000, "NPadEntry is an invalid size");
|
||||
|
||||
@ -560,6 +583,7 @@ private:
|
||||
f32 sixaxis_fusion_parameter2{};
|
||||
bool sixaxis_at_rest{true};
|
||||
std::array<ControllerPad, 10> npad_pad_states{};
|
||||
std::array<TriggerState, 10> npad_trigger_states{};
|
||||
bool is_in_lr_assignment_mode{false};
|
||||
Core::System& system;
|
||||
};
|
||||
|
@ -340,6 +340,7 @@ enum class ControllerType {
|
||||
LeftJoycon,
|
||||
RightJoycon,
|
||||
Handheld,
|
||||
GameCube,
|
||||
};
|
||||
|
||||
struct PlayerInput {
|
||||
|
@ -67,6 +67,8 @@ bool IsControllerCompatible(Settings::ControllerType controller_type,
|
||||
return parameters.allow_right_joycon;
|
||||
case Settings::ControllerType::Handheld:
|
||||
return parameters.enable_single_mode && parameters.allow_handheld;
|
||||
case Settings::ControllerType::GameCube:
|
||||
return parameters.allow_gamecube_controller;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -370,7 +372,7 @@ void QtControllerSelectorDialog::SetSupportedControllers() {
|
||||
QStringLiteral("image: url(:/controller/applet_joycon_right%0_disabled); ").arg(theme));
|
||||
}
|
||||
|
||||
if (parameters.allow_pro_controller) {
|
||||
if (parameters.allow_pro_controller || parameters.allow_gamecube_controller) {
|
||||
ui->controllerSupported5->setStyleSheet(
|
||||
QStringLiteral("image: url(:/controller/applet_pro_controller%0); ").arg(theme));
|
||||
} else {
|
||||
@ -420,6 +422,10 @@ void QtControllerSelectorDialog::SetEmulatedControllers(std::size_t player_index
|
||||
Settings::ControllerType::Handheld);
|
||||
emulated_controllers[player_index]->addItem(tr("Handheld"));
|
||||
}
|
||||
|
||||
pairs.emplace_back(emulated_controllers[player_index]->count(),
|
||||
Settings::ControllerType::GameCube);
|
||||
emulated_controllers[player_index]->addItem(tr("GameCube Controller"));
|
||||
}
|
||||
|
||||
Settings::ControllerType QtControllerSelectorDialog::GetControllerTypeFromIndex(
|
||||
@ -461,6 +467,7 @@ void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index)
|
||||
switch (GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex(),
|
||||
player_index)) {
|
||||
case Settings::ControllerType::ProController:
|
||||
case Settings::ControllerType::GameCube:
|
||||
return QStringLiteral("image: url(:/controller/applet_pro_controller%0); ");
|
||||
case Settings::ControllerType::DualJoyconDetached:
|
||||
return QStringLiteral("image: url(:/controller/applet_dual_joycon%0); ");
|
||||
|
@ -467,10 +467,14 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
|
||||
|
||||
UpdateControllerIcon();
|
||||
UpdateControllerAvailableButtons();
|
||||
UpdateControllerEnabledButtons();
|
||||
UpdateControllerButtonNames();
|
||||
UpdateMotionButtons();
|
||||
connect(ui->comboControllerType, qOverload<int>(&QComboBox::currentIndexChanged), [this](int) {
|
||||
UpdateControllerIcon();
|
||||
UpdateControllerAvailableButtons();
|
||||
UpdateControllerEnabledButtons();
|
||||
UpdateControllerButtonNames();
|
||||
UpdateMotionButtons();
|
||||
});
|
||||
|
||||
@ -558,9 +562,6 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
|
||||
&ConfigureInputPlayer::SaveProfile);
|
||||
|
||||
LoadConfiguration();
|
||||
|
||||
// TODO(wwylele): enable this when we actually emulate it
|
||||
ui->buttonHome->setEnabled(false);
|
||||
ui->controllerFrame->SetPlayerInput(player_index, buttons_param, analogs_param);
|
||||
ui->controllerFrame->SetConnectedStatus(ui->groupConnectedController->isChecked());
|
||||
}
|
||||
@ -924,6 +925,12 @@ void ConfigureInputPlayer::SetConnectableControllers() {
|
||||
Settings::ControllerType::Handheld);
|
||||
ui->comboControllerType->addItem(tr("Handheld"));
|
||||
}
|
||||
|
||||
if (enable_all || npad_style_set.gamecube == 1) {
|
||||
index_controller_type_pairs.emplace_back(ui->comboControllerType->count(),
|
||||
Settings::ControllerType::GameCube);
|
||||
ui->comboControllerType->addItem(tr("GameCube Controller"));
|
||||
}
|
||||
};
|
||||
|
||||
Core::System& system{Core::System::GetInstance()};
|
||||
@ -1014,7 +1021,7 @@ void ConfigureInputPlayer::UpdateControllerAvailableButtons() {
|
||||
|
||||
// List of all the widgets that will be hidden by any of the following layouts that need
|
||||
// "unhidden" after the controller type changes
|
||||
const std::array<QWidget*, 9> layout_show = {
|
||||
const std::array<QWidget*, 11> layout_show = {
|
||||
ui->buttonShoulderButtonsSLSR,
|
||||
ui->horizontalSpacerShoulderButtonsWidget,
|
||||
ui->horizontalSpacerShoulderButtonsWidget2,
|
||||
@ -1024,6 +1031,8 @@ void ConfigureInputPlayer::UpdateControllerAvailableButtons() {
|
||||
ui->buttonShoulderButtonsRight,
|
||||
ui->buttonMiscButtonsPlusHome,
|
||||
ui->bottomRight,
|
||||
ui->buttonMiscButtonsMinusGroup,
|
||||
ui->buttonMiscButtonsScreenshotGroup,
|
||||
};
|
||||
|
||||
for (auto* widget : layout_show) {
|
||||
@ -1056,6 +1065,14 @@ void ConfigureInputPlayer::UpdateControllerAvailableButtons() {
|
||||
ui->bottomLeft,
|
||||
};
|
||||
break;
|
||||
case Settings::ControllerType::GameCube:
|
||||
layout_hidden = {
|
||||
ui->buttonShoulderButtonsSLSR,
|
||||
ui->horizontalSpacerShoulderButtonsWidget2,
|
||||
ui->buttonMiscButtonsMinusGroup,
|
||||
ui->buttonMiscButtonsScreenshotGroup,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
for (auto* widget : layout_hidden) {
|
||||
@ -1063,6 +1080,52 @@ void ConfigureInputPlayer::UpdateControllerAvailableButtons() {
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureInputPlayer::UpdateControllerEnabledButtons() {
|
||||
auto layout = GetControllerTypeFromIndex(ui->comboControllerType->currentIndex());
|
||||
if (debug) {
|
||||
layout = Settings::ControllerType::ProController;
|
||||
}
|
||||
|
||||
// List of all the widgets that will be disabled by any of the following layouts that need
|
||||
// "enabled" after the controller type changes
|
||||
const std::array<QWidget*, 4> layout_enable = {
|
||||
ui->buttonHome,
|
||||
ui->buttonLStickPressedGroup,
|
||||
ui->groupRStickPressed,
|
||||
ui->buttonShoulderButtonsButtonLGroup,
|
||||
};
|
||||
|
||||
for (auto* widget : layout_enable) {
|
||||
widget->setEnabled(true);
|
||||
}
|
||||
|
||||
std::vector<QWidget*> layout_disable;
|
||||
switch (layout) {
|
||||
case Settings::ControllerType::ProController:
|
||||
case Settings::ControllerType::DualJoyconDetached:
|
||||
case Settings::ControllerType::Handheld:
|
||||
case Settings::ControllerType::LeftJoycon:
|
||||
case Settings::ControllerType::RightJoycon:
|
||||
// TODO(wwylele): enable this when we actually emulate it
|
||||
layout_disable = {
|
||||
ui->buttonHome,
|
||||
};
|
||||
break;
|
||||
case Settings::ControllerType::GameCube:
|
||||
layout_disable = {
|
||||
ui->buttonHome,
|
||||
ui->buttonLStickPressedGroup,
|
||||
ui->groupRStickPressed,
|
||||
ui->buttonShoulderButtonsButtonLGroup,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
for (auto* widget : layout_disable) {
|
||||
widget->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureInputPlayer::UpdateMotionButtons() {
|
||||
if (debug) {
|
||||
// Motion isn't used with the debug controller, hide both groupboxes.
|
||||
@ -1085,6 +1148,11 @@ void ConfigureInputPlayer::UpdateMotionButtons() {
|
||||
ui->buttonMotionLeftGroup->hide();
|
||||
ui->buttonMotionRightGroup->show();
|
||||
break;
|
||||
case Settings::ControllerType::GameCube:
|
||||
// Hide both "Motion 1/2".
|
||||
ui->buttonMotionLeftGroup->hide();
|
||||
ui->buttonMotionRightGroup->hide();
|
||||
break;
|
||||
case Settings::ControllerType::DualJoyconDetached:
|
||||
default:
|
||||
// Show both "Motion 1/2".
|
||||
@ -1094,6 +1162,36 @@ void ConfigureInputPlayer::UpdateMotionButtons() {
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureInputPlayer::UpdateControllerButtonNames() {
|
||||
auto layout = GetControllerTypeFromIndex(ui->comboControllerType->currentIndex());
|
||||
if (debug) {
|
||||
layout = Settings::ControllerType::ProController;
|
||||
}
|
||||
|
||||
switch (layout) {
|
||||
case Settings::ControllerType::ProController:
|
||||
case Settings::ControllerType::DualJoyconDetached:
|
||||
case Settings::ControllerType::Handheld:
|
||||
case Settings::ControllerType::LeftJoycon:
|
||||
case Settings::ControllerType::RightJoycon:
|
||||
ui->buttonMiscButtonsPlusGroup->setTitle(tr("Plus"));
|
||||
ui->buttonShoulderButtonsButtonZLGroup->setTitle(tr("ZL"));
|
||||
ui->buttonShoulderButtonsZRGroup->setTitle(tr("ZR"));
|
||||
ui->buttonShoulderButtonsRGroup->setTitle(tr("R"));
|
||||
ui->LStick->setTitle(tr("Left Stick"));
|
||||
ui->RStick->setTitle(tr("Right Stick"));
|
||||
break;
|
||||
case Settings::ControllerType::GameCube:
|
||||
ui->buttonMiscButtonsPlusGroup->setTitle(tr("Start / Pause"));
|
||||
ui->buttonShoulderButtonsButtonZLGroup->setTitle(tr("L"));
|
||||
ui->buttonShoulderButtonsZRGroup->setTitle(tr("R"));
|
||||
ui->buttonShoulderButtonsRGroup->setTitle(tr("Z"));
|
||||
ui->LStick->setTitle(tr("Control Stick"));
|
||||
ui->RStick->setTitle(tr("C-Stick"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureInputPlayer::UpdateMappingWithDefaults() {
|
||||
if (ui->comboDevices->currentIndex() == 0) {
|
||||
return;
|
||||
|
@ -143,9 +143,15 @@ private:
|
||||
/// Hides and disables controller settings based on the current controller type.
|
||||
void UpdateControllerAvailableButtons();
|
||||
|
||||
/// Disables controller settings based on the current controller type.
|
||||
void UpdateControllerEnabledButtons();
|
||||
|
||||
/// Shows or hides motion groupboxes based on the current controller type.
|
||||
void UpdateMotionButtons();
|
||||
|
||||
/// Alters the button names based on the current controller type.
|
||||
void UpdateControllerButtonNames();
|
||||
|
||||
/// Gets the default controller mapping for this device and auto configures the input to match.
|
||||
void UpdateMappingWithDefaults();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user