mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-09 12:37:18 -04:00
Add Gameplay settings & make Panes scrollable
This commit is contained in:
+59
-13
@@ -19,6 +19,9 @@ button {
|
||||
}
|
||||
|
||||
.window {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
max-width: 1088dp;
|
||||
max-height: 768dp;
|
||||
margin: auto;
|
||||
@@ -30,8 +33,18 @@ button {
|
||||
background-color: rgba(21, 22, 16, 90%);
|
||||
}
|
||||
|
||||
@media (max-height: 640dp) {
|
||||
body {
|
||||
padding: 16dp;
|
||||
}
|
||||
.window {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.window .tab-bar {
|
||||
display: flex;
|
||||
flex: 0 0 64dp;
|
||||
height: 64dp;
|
||||
background-color: rgba(217, 217, 217, 10%);
|
||||
font-family: "Fira Sans Condensed";
|
||||
@@ -68,7 +81,10 @@ button {
|
||||
|
||||
.window .content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window .content .pane {
|
||||
@@ -76,30 +92,56 @@ button {
|
||||
flex-flow: column;
|
||||
flex: 1 1 0;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 24dp;
|
||||
padding-bottom: 0dp;
|
||||
gap: 8dp;
|
||||
overflow: auto;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.window .content .pane:not(:last-of-type) {
|
||||
border-right: 1dp #92875B;
|
||||
}
|
||||
|
||||
.window .content .pane.detail-pane {
|
||||
gap: 16dp;
|
||||
.window .content .pane > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.window .content .detail-value {
|
||||
padding: 12dp 16dp;
|
||||
border-radius: 12dp;
|
||||
background-color: rgba(17, 16, 10, 20%);
|
||||
box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp;
|
||||
font-size: 20dp;
|
||||
.window .content .pane > .spacer {
|
||||
display: block;
|
||||
/* Completes the 24dp bottom inset after the pane's 8dp gap. */
|
||||
flex: 0 0 16dp;
|
||||
height: 16dp;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.window .content .detail-controls {
|
||||
display: flex;
|
||||
gap: 12dp;
|
||||
scrollbarvertical {
|
||||
width: 8dp;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderarrowdec,
|
||||
scrollbarvertical sliderarrowinc {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbarvertical slidertrack {
|
||||
width: 8dp;
|
||||
background-color: rgba(224, 219, 200, 5%);
|
||||
border-radius: 2dp;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderbar {
|
||||
width: 8dp;
|
||||
min-height: 24dp;
|
||||
background-color: rgba(224, 219, 200, 45%);
|
||||
border-radius: 2dp;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderbar:hover,
|
||||
scrollbarvertical sliderbar:active {
|
||||
background-color: rgba(194, 164, 45, 80%);
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
@@ -109,6 +151,10 @@ button {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.section-heading:not(:first-of-type) {
|
||||
padding-top: 12dp;
|
||||
}
|
||||
|
||||
.button {
|
||||
text-align: center;
|
||||
background-color: rgba(17, 16, 10, 20%);
|
||||
|
||||
@@ -15,6 +15,12 @@ void Component::update() {
|
||||
bool Component::focus() {
|
||||
// Can we focus self?
|
||||
if (mRoot->Focus(true)) {
|
||||
mRoot->ScrollIntoView(Rml::ScrollIntoViewOptions{
|
||||
Rml::ScrollAlignment::Center,
|
||||
Rml::ScrollAlignment::Nearest,
|
||||
Rml::ScrollBehavior::Smooth,
|
||||
Rml::ScrollParentage::Closest,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Otherwise, try to focus a child
|
||||
|
||||
+31
-11
@@ -38,16 +38,10 @@ Pane::Pane(Rml::Element* parent, Direction direction, const Rml::String& classNa
|
||||
break;
|
||||
}
|
||||
}
|
||||
int i = focusedChild + direction;
|
||||
if (focusedChild == -1) {
|
||||
// If the container itself is focused and next is pressed, focus the first element
|
||||
if (direction == 1) {
|
||||
i = 0;
|
||||
} else {
|
||||
// Otherwise, allow event to bubble
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
int i = focusedChild + direction;
|
||||
while (i >= 0 && i < static_cast<int>(mChildren.size())) {
|
||||
if (mChildren[i]->focus()) {
|
||||
event.StopPropagation();
|
||||
@@ -58,6 +52,11 @@ Pane::Pane(Rml::Element* parent, Direction direction, const Rml::String& classNa
|
||||
});
|
||||
}
|
||||
|
||||
void Pane::update() {
|
||||
finalize();
|
||||
Component::update();
|
||||
}
|
||||
|
||||
bool Pane::focus() {
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->focus()) {
|
||||
@@ -80,8 +79,29 @@ Rml::Element* Pane::add_text(const Rml::String& text) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
void Pane::clear() {
|
||||
clear_children();
|
||||
Rml::Element* Pane::add_rml(const Rml::String& rml) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetInnerRML(rml);
|
||||
return elem;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
void Pane::finalize() {
|
||||
if (finalized) {
|
||||
return;
|
||||
}
|
||||
finalized = true;
|
||||
|
||||
// Append spacer element to the bottom. RmlUi does not properly handle
|
||||
// padding-bottom or margin-bottom on a scrollable flex container, so
|
||||
// we need to create a fake spacer with an actual layout height to get
|
||||
// padding at the bottom of a scrollable container.
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetClass("spacer", true);
|
||||
}
|
||||
|
||||
void Pane::clear() {
|
||||
clear_children();
|
||||
finalized = false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
explicit Pane(Rml::Element* parent, Direction direction, const Rml::String& className = "pane");
|
||||
|
||||
bool focus() override;
|
||||
void update() override;
|
||||
|
||||
Rml::Element* add_section(const Rml::String& text);
|
||||
Button& add_button(Button::Props props) { return add_child<Button>(mRoot, std::move(props)); }
|
||||
@@ -23,10 +24,13 @@ public:
|
||||
return add_child<SelectButton>(mRoot, std::move(props));
|
||||
}
|
||||
Rml::Element* add_text(const Rml::String& text);
|
||||
Rml::Element* add_rml(const Rml::String& rml);
|
||||
void finalize();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
Direction mDirection;
|
||||
bool finalized = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui
|
||||
|
||||
+212
-11
@@ -5,12 +5,44 @@
|
||||
#include "aurora/gfx.h"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
#include "pane.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
void reset_for_speedrun_mode() {
|
||||
mDoMain::developmentMode = -1;
|
||||
|
||||
getSettings().game.damageMultiplier.setValue(1);
|
||||
getSettings().game.instantDeath.setValue(false);
|
||||
getSettings().game.noHeartDrops.setValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setValue(false);
|
||||
getSettings().game.infiniteArrows.setValue(false);
|
||||
getSettings().game.infiniteBombs.setValue(false);
|
||||
getSettings().game.infiniteOil.setValue(false);
|
||||
getSettings().game.infiniteOxygen.setValue(false);
|
||||
getSettings().game.infiniteRupees.setValue(false);
|
||||
getSettings().game.enableIndefiniteItemDrops.setValue(false);
|
||||
|
||||
getSettings().game.moonJump.setValue(false);
|
||||
getSettings().game.superClawshot.setValue(false);
|
||||
getSettings().game.alwaysGreatspin.setValue(false);
|
||||
getSettings().game.enableFastIronBoots.setValue(false);
|
||||
getSettings().game.canTransformAnywhere.setValue(false);
|
||||
getSettings().game.fastSpinner.setValue(false);
|
||||
getSettings().game.freeMagicArmor.setValue(false);
|
||||
|
||||
getSettings().game.enableTurboKeybind.setValue(false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
template <typename T>
|
||||
struct ConfigProps {
|
||||
@@ -34,11 +66,11 @@ public:
|
||||
if (!mHelpText.empty() && mRightPane != nullptr) {
|
||||
listen(Rml::EventId::Focus, [this](Rml::Event&) {
|
||||
mRightPane->clear();
|
||||
mRightPane->add_text(mHelpText);
|
||||
mRightPane->add_rml(mHelpText);
|
||||
});
|
||||
listen(Rml::EventId::Mouseover, [this](Rml::Event&) {
|
||||
mRightPane->clear();
|
||||
mRightPane->add_text(mHelpText);
|
||||
mRightPane->add_rml(mHelpText);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -87,23 +119,57 @@ protected:
|
||||
}
|
||||
};
|
||||
|
||||
class ConfigIntPercentSelect : public ConfigSelect<int> {
|
||||
class ConfigIntSelect : public ConfigSelect<int> {
|
||||
public:
|
||||
ConfigIntPercentSelect(Rml::Element* parent, Props props)
|
||||
: ConfigSelect(parent, std::move(props)) {}
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
ConfigVar<int>* value;
|
||||
std::function<void(int)> onChange;
|
||||
std::function<bool()> isDisabled;
|
||||
Rml::String helpText;
|
||||
Pane* rightPane = nullptr;
|
||||
int min = 0;
|
||||
int max = INT_MAX;
|
||||
int step = 1;
|
||||
Rml::String prefix;
|
||||
Rml::String suffix;
|
||||
};
|
||||
|
||||
ConfigIntSelect(Rml::Element* parent, Props props)
|
||||
: ConfigSelect(parent,
|
||||
{
|
||||
.key = std::move(props.key),
|
||||
.value = props.value,
|
||||
.onChange = std::move(props.onChange),
|
||||
.isDisabled = std::move(props.isDisabled),
|
||||
.helpText = std::move(props.helpText),
|
||||
.rightPane = props.rightPane,
|
||||
}),
|
||||
mMin(props.min), mMax(props.max), mStep(props.step), mPrefix(std::move(props.prefix)),
|
||||
mSuffix(std::move(props.suffix)) {}
|
||||
|
||||
protected:
|
||||
Rml::String get_value() override { return fmt::format("{}%", mVar->getValue()); }
|
||||
Rml::String get_value() override {
|
||||
return fmt::format("{}{}{}", mPrefix, mVar->getValue(), mSuffix);
|
||||
}
|
||||
|
||||
bool handle_nav_command(NavCommand cmd) override {
|
||||
if (cmd == NavCommand::Left) {
|
||||
set_value(std::max(mVar->getValue() - 1, 0));
|
||||
set_value(std::clamp(mVar->getValue() - mStep, mMin, mMax));
|
||||
return true;
|
||||
} else if (cmd == NavCommand::Right) {
|
||||
set_value(std::min(mVar->getValue() + 1, 100));
|
||||
set_value(std::clamp(mVar->getValue() + mStep, mMin, mMax));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
int mMin;
|
||||
int mMax;
|
||||
int mStep;
|
||||
Rml::String mPrefix;
|
||||
Rml::String mSuffix;
|
||||
};
|
||||
|
||||
SettingsWindow::SettingsWindow() {
|
||||
@@ -112,13 +178,15 @@ SettingsWindow::SettingsWindow() {
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
|
||||
leftPane.add_section("Volume");
|
||||
leftPane.add_child<ConfigIntPercentSelect>(leftPane.root(),
|
||||
ConfigIntPercentSelect::Props{
|
||||
leftPane.add_child<ConfigIntSelect>(leftPane.root(),
|
||||
ConfigIntSelect::Props{
|
||||
.key = "Master Volume",
|
||||
.value = &getSettings().audio.masterVolume,
|
||||
.onChange = [](int value) { audio::SetMasterVolume(value / 100.f); },
|
||||
.helpText = "Adjusts the volume of all sounds in the game.",
|
||||
.rightPane = &rightPane,
|
||||
.max = 100,
|
||||
.suffix = "%",
|
||||
});
|
||||
|
||||
leftPane.add_section("Effects");
|
||||
@@ -158,7 +226,7 @@ SettingsWindow::SettingsWindow() {
|
||||
leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = key,
|
||||
.value = &value,
|
||||
.isDisabled = [] { return !getSettings().game.speedrunMode; },
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.helpText = helpText,
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
@@ -195,7 +263,114 @@ SettingsWindow::SettingsWindow() {
|
||||
});
|
||||
|
||||
add_tab("Gameplay", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
|
||||
auto addOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
leftPane.add_child<ConfigBoolSelect>(leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = key,
|
||||
.value = &value,
|
||||
.helpText = helpText,
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
};
|
||||
|
||||
auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
leftPane.add_child<ConfigBoolSelect>(
|
||||
leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = key,
|
||||
.value = &value,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.helpText = helpText,
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
};
|
||||
|
||||
leftPane.add_section("General");
|
||||
addOption("Mirror Mode", getSettings().game.enableMirrorMode,
|
||||
"Mirrors the world horizontally, matching the Wii version of the game.");
|
||||
addOption("Disable Main HUD", getSettings().game.disableMainHUD,
|
||||
"Disables the main HUD of the game.<br/>Useful for recording or a more immersive "
|
||||
"experience.");
|
||||
addOption("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches,
|
||||
"Restores patched glitches from Wii USA 1.0, the first released version.");
|
||||
addOption("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation,
|
||||
"Enables rotating Link in the collection menu with the C-Stick.");
|
||||
|
||||
leftPane.add_section("Difficulty");
|
||||
leftPane.add_child<ConfigIntSelect>(
|
||||
leftPane.root(), ConfigIntSelect::Props{
|
||||
.key = "Damage Multiplier",
|
||||
.value = &getSettings().game.damageMultiplier,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.helpText = "Multiplies incoming damage.",
|
||||
.rightPane = &rightPane,
|
||||
.min = 1,
|
||||
.max = 8,
|
||||
.prefix = "x",
|
||||
});
|
||||
addSpeedrunDisabledOption(
|
||||
"Instant Death", getSettings().game.instantDeath, "Any hit will instantly kill you.");
|
||||
addSpeedrunDisabledOption("No Heart Drops", getSettings().game.noHeartDrops,
|
||||
"Hearts will never drop from enemies, pots, and various other places.");
|
||||
|
||||
leftPane.add_section("Quality of Life");
|
||||
addOption("Bigger Wallets", getSettings().game.biggerWallets,
|
||||
"Wallet sizes are like in the HD version. (500, 1000, 2000)");
|
||||
addOption("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes,
|
||||
"Rupees will not play cutscenes after you have collected them the first time.");
|
||||
addOption("Faster Climbing", getSettings().game.fastClimbing,
|
||||
"Quicker climbing on ladders and vines like the HD version.");
|
||||
addOption("Faster Tears of Light", getSettings().game.fastTears,
|
||||
"Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
|
||||
addOption("Instant Saves", getSettings().game.instantSaves,
|
||||
"Skips the delay when writing to the Memory Card.");
|
||||
addOption("Hold B for Instant Text", getSettings().game.instantText,
|
||||
"Makes text scroll immediately by holding B.");
|
||||
addOption("No Climbing Miss Animation", getSettings().game.noMissClimbing,
|
||||
"Prevents Link from playing a struggle animation when grabbing ledges or climbing on "
|
||||
"vines.");
|
||||
addOption("No Rupee Returns", getSettings().game.noReturnRupees,
|
||||
"Always collect Rupees even if your Wallet is too full.");
|
||||
addOption("No Sword Recoil", getSettings().game.noSwordRecoil,
|
||||
"Link will not recoil when his sword hits walls.");
|
||||
addOption("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen,
|
||||
"Skips the TV calibration screen shown when loading a save.");
|
||||
addOption("Skip Warning Screen", getSettings().game.skipWarningScreen,
|
||||
"Skips the warning screen shown when starting the game.");
|
||||
addOption("Sun's Song (R+X)", getSettings().game.sunsSong,
|
||||
"Allows Wolf Link to howl and change the time of day.");
|
||||
addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform,
|
||||
"Transform instantly by pressing R and Y simultaneously.");
|
||||
|
||||
leftPane.add_section("Speedrunning");
|
||||
leftPane.add_child<ConfigBoolSelect>(leftPane.root(),
|
||||
ConfigBoolSelect::Props{
|
||||
.key = "Speedrun Mode",
|
||||
.value = &getSettings().game.speedrunMode,
|
||||
.onChange = [](bool) { reset_for_speedrun_mode(); },
|
||||
.helpText = "Enables speedrunning options while restricting certain "
|
||||
"gameplay modifiers.",
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
leftPane.add_child<ConfigBoolSelect>(
|
||||
leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = "LiveSplit Connection",
|
||||
.value = &getSettings().game.liveSplitEnabled,
|
||||
.onChange =
|
||||
[](bool enabled) {
|
||||
if (enabled) {
|
||||
speedrun::connectLiveSplit();
|
||||
} else {
|
||||
speedrun::disconnectLiveSplit();
|
||||
}
|
||||
},
|
||||
.isDisabled = [] { return !getSettings().game.speedrunMode; },
|
||||
.helpText = "Connect to LiveSplit server on localhost:16834.",
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Graphics", [this](Rml::Element* content) {
|
||||
@@ -246,6 +421,32 @@ SettingsWindow::SettingsWindow() {
|
||||
});
|
||||
|
||||
leftPane.add_section("Resolution");
|
||||
// TODO: Internal Resolution
|
||||
|
||||
leftPane.add_section("Post-Processing");
|
||||
// TODO: Bloom
|
||||
// TODO: Bloom Brightness
|
||||
|
||||
leftPane.add_section("Rendering");
|
||||
leftPane.add_child<ConfigBoolSelect>(leftPane.root(),
|
||||
ConfigBoolSelect::Props{
|
||||
.key = "Unlock Framerate",
|
||||
.value = &getSettings().game.enableFrameInterpolation,
|
||||
.helpText =
|
||||
"Uses inter-frame interpolation to enable higher frame rates.<br/><br/>Visual "
|
||||
"artifacts, animation glitches, or instability may occur.",
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
leftPane.add_child<ConfigBoolSelect>(
|
||||
leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = "Enable Depth of Field",
|
||||
.value = &getSettings().game.enableDepthOfField,
|
||||
});
|
||||
leftPane.add_child<ConfigBoolSelect>(
|
||||
leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = "Enable Mini-Map Shadows",
|
||||
.value = &getSettings().game.enableMapBackground,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "button.hpp"
|
||||
#include "magic_enum.hpp"
|
||||
@@ -184,10 +183,6 @@ bool Window::handle_tab_bar_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
if (cmd == NavCommand::Up || cmd == NavCommand::Cancel) {
|
||||
return focus_active_tab();
|
||||
} else if (cmd == NavCommand::Down) {
|
||||
if (!mContentComponents.empty()) {
|
||||
return mContentComponents.front()->focus();
|
||||
}
|
||||
} else if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
int currentComponent = -1;
|
||||
for (int i = 0; i < mContentComponents.size(); ++i) {
|
||||
|
||||
@@ -590,9 +590,9 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
// TODO: just for testing
|
||||
auto& editorWindow = dusk::ui::add_window(std::make_unique<dusk::ui::EditorWindow>());
|
||||
editorWindow.show();
|
||||
// editorWindow.show();
|
||||
auto& settingsWindow = dusk::ui::add_window(std::make_unique<dusk::ui::SettingsWindow>());
|
||||
// settingsWindow.show();
|
||||
settingsWindow.show();
|
||||
|
||||
std::string dvd_path;
|
||||
bool dvd_opened = false;
|
||||
|
||||
Reference in New Issue
Block a user