presets en postfx
This commit is contained in:
@@ -90,6 +90,10 @@ assets:
|
||||
path: ${SYSTEM_FOLDER}/cheevos.bin
|
||||
required: false
|
||||
absolute: true
|
||||
- type: DATA
|
||||
path: ${SYSTEM_FOLDER}/postfx.yaml
|
||||
required: false
|
||||
absolute: true
|
||||
|
||||
# ROOMS
|
||||
rooms:
|
||||
|
||||
@@ -91,9 +91,17 @@ void handleIncWindowZoom() {
|
||||
}
|
||||
}
|
||||
|
||||
void handleToggleShaders() {
|
||||
Screen::get()->toggleShaders();
|
||||
Notifier::get()->show({"SHADERS " + std::string(Options::video.shaders ? "ENABLED" : "DISABLED")});
|
||||
void handleTogglePostFX() {
|
||||
Screen::get()->togglePostFX();
|
||||
Notifier::get()->show({"POSTFX " + std::string(Options::video.postfx ? "ENABLED" : "DISABLED")});
|
||||
}
|
||||
|
||||
void handleNextPostFXPreset() {
|
||||
if (!Options::postfx_presets.empty()) {
|
||||
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
|
||||
Screen::get()->reloadPostFX();
|
||||
Notifier::get()->show({"POSTFX " + Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)].name});
|
||||
}
|
||||
}
|
||||
|
||||
void handleNextPalette() {
|
||||
@@ -152,8 +160,11 @@ auto getPressedAction() -> InputAction {
|
||||
return InputAction::WINDOW_INC_ZOOM;
|
||||
}
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_SHADERS, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_SHADERS;
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_POSTFX, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if (Options::video.postfx && (SDL_GetModState() & SDL_KMOD_SHIFT)) {
|
||||
return InputAction::NEXT_POSTFX_PRESET;
|
||||
}
|
||||
return InputAction::TOGGLE_POSTFX;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::NEXT_PALETTE;
|
||||
@@ -221,8 +232,12 @@ void handle() {
|
||||
handleIncWindowZoom();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_SHADERS:
|
||||
handleToggleShaders();
|
||||
case InputAction::TOGGLE_POSTFX:
|
||||
handleTogglePostFX();
|
||||
break;
|
||||
|
||||
case InputAction::NEXT_POSTFX_PRESET:
|
||||
handleNextPostFXPreset();
|
||||
break;
|
||||
|
||||
case InputAction::NEXT_PALETTE:
|
||||
|
||||
@@ -43,7 +43,7 @@ Input::Input(std::string game_controller_db_path)
|
||||
{Action::WINDOW_DEC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F1}},
|
||||
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
|
||||
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
|
||||
{Action::TOGGLE_SHADERS, KeyState{.scancode = SDL_SCANCODE_F4}},
|
||||
{Action::TOGGLE_POSTFX, KeyState{.scancode = SDL_SCANCODE_F4}},
|
||||
{Action::NEXT_PALETTE, KeyState{.scancode = SDL_SCANCODE_F5}},
|
||||
{Action::PREVIOUS_PALETTE, KeyState{.scancode = SDL_SCANCODE_F6}},
|
||||
{Action::TOGGLE_INTEGER_SCALE, KeyState{.scancode = SDL_SCANCODE_F7}},
|
||||
|
||||
@@ -20,7 +20,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||
{InputAction::TOGGLE_MUSIC, "TOGGLE_MUSIC"},
|
||||
{InputAction::NEXT_PALETTE, "NEXT_PALETTE"},
|
||||
{InputAction::PREVIOUS_PALETTE, "PREVIOUS_PALETTE"},
|
||||
{InputAction::TOGGLE_SHADERS, "TOGGLE_SHADERS"},
|
||||
{InputAction::TOGGLE_POSTFX, "TOGGLE_POSTFX"},
|
||||
{InputAction::NEXT_POSTFX_PRESET, "NEXT_POSTFX_PRESET"},
|
||||
{InputAction::SHOW_DEBUG_INFO, "SHOW_DEBUG_INFO"},
|
||||
{InputAction::TOGGLE_DEBUG, "TOGGLE_DEBUG"},
|
||||
{InputAction::NONE, "NONE"}};
|
||||
@@ -42,7 +43,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||
{"TOGGLE_MUSIC", InputAction::TOGGLE_MUSIC},
|
||||
{"NEXT_PALETTE", InputAction::NEXT_PALETTE},
|
||||
{"PREVIOUS_PALETTE", InputAction::PREVIOUS_PALETTE},
|
||||
{"TOGGLE_SHADERS", InputAction::TOGGLE_SHADERS},
|
||||
{"TOGGLE_POSTFX", InputAction::TOGGLE_POSTFX},
|
||||
{"NEXT_POSTFX_PRESET", InputAction::NEXT_POSTFX_PRESET},
|
||||
{"SHOW_DEBUG_INFO", InputAction::SHOW_DEBUG_INFO},
|
||||
{"TOGGLE_DEBUG", InputAction::TOGGLE_DEBUG},
|
||||
{"NONE", InputAction::NONE}};
|
||||
|
||||
@@ -24,7 +24,8 @@ enum class InputAction : int { // Acciones de entrada posibles en el juego
|
||||
TOGGLE_FULLSCREEN,
|
||||
TOGGLE_VSYNC,
|
||||
TOGGLE_INTEGER_SCALE,
|
||||
TOGGLE_SHADERS,
|
||||
TOGGLE_POSTFX,
|
||||
NEXT_POSTFX_PRESET,
|
||||
TOGGLE_BORDER,
|
||||
TOGGLE_MUSIC,
|
||||
NEXT_PALETTE,
|
||||
|
||||
@@ -132,7 +132,7 @@ void Screen::render() {
|
||||
|
||||
// En el path SDL3GPU, los píxeles se suben directamente desde la Surface.
|
||||
// En el path SDL_Renderer, primero copiamos la surface a la SDL_Texture.
|
||||
if (!(Options::video.shaders && shader_backend_ && shader_backend_->isHardwareAccelerated())) {
|
||||
if (!(Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated())) {
|
||||
surfaceToTexture();
|
||||
}
|
||||
|
||||
@@ -209,17 +209,26 @@ void Screen::renderNotifications() const {
|
||||
}
|
||||
}
|
||||
|
||||
// Cambia el estado de los shaders
|
||||
void Screen::toggleShaders() {
|
||||
Options::video.shaders = !Options::video.shaders;
|
||||
if (!Options::video.shaders && shader_backend_) {
|
||||
// Al desactivar shaders, limpiar el backend para liberar el swapchain de GPU
|
||||
// Cambia el estado del PostFX
|
||||
void Screen::togglePostFX() {
|
||||
Options::video.postfx = !Options::video.postfx;
|
||||
if (!Options::video.postfx && shader_backend_) {
|
||||
// Al desactivar PostFX, limpiar el backend para liberar el swapchain de GPU
|
||||
shader_backend_->cleanup();
|
||||
} else {
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader del preset actual sin toggle
|
||||
void Screen::reloadPostFX() {
|
||||
if (Options::video.postfx) {
|
||||
vertex_shader_source_.clear();
|
||||
fragment_shader_source_.clear();
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza la lógica de la clase (versión nueva con delta_time para escenas migradas)
|
||||
void Screen::update(float delta_time) {
|
||||
fps_.calculate(SDL_GetTicks());
|
||||
@@ -319,7 +328,7 @@ void Screen::surfaceToTexture() {
|
||||
void Screen::textureToRenderer() {
|
||||
SDL_Texture* texture_to_render = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
|
||||
if (Options::video.shaders && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
if (Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
// ---- SDL3 GPU path: convertir Surface → ARGB → upload → PostFX → present ----
|
||||
if (Options::video.border.enabled) {
|
||||
// El border_surface_ solo tiene el color de borde; hay que componer encima el game_surface_
|
||||
@@ -439,16 +448,32 @@ auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
|
||||
// Carga el contenido de los archivos GLSL
|
||||
void Screen::loadShaders() {
|
||||
// Obtener nombres de fichero desde el preset actual (o usar fallback)
|
||||
std::string preset_vertex = "crtpi_vertex.glsl";
|
||||
std::string preset_fragment = "crtpi_fragment.glsl";
|
||||
if (!Options::postfx_presets.empty()) {
|
||||
const auto& preset = Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)];
|
||||
if (!preset.vertex.empty()) {
|
||||
preset_vertex = preset.vertex;
|
||||
}
|
||||
if (!preset.fragment.empty()) {
|
||||
preset_fragment = preset.fragment;
|
||||
}
|
||||
}
|
||||
|
||||
if (vertex_shader_source_.empty()) {
|
||||
// Detectar si necesitamos OpenGL ES (Raspberry Pi)
|
||||
// Intentar cargar versión ES primero si existe
|
||||
std::string vertex_file = "crtpi_vertex_es.glsl";
|
||||
auto data = loadData(Resource::List::get()->get(vertex_file));
|
||||
// Intentar cargar versión ES primero si existe (reemplaza .glsl por _es.glsl)
|
||||
std::string vertex_es = preset_vertex;
|
||||
auto pos = vertex_es.rfind(".glsl");
|
||||
if (pos != std::string::npos) {
|
||||
vertex_es.insert(pos, "_es");
|
||||
}
|
||||
auto data = loadData(Resource::List::get()->get(vertex_es));
|
||||
|
||||
if (data.empty()) {
|
||||
// Si no existe versión ES, usar versión Desktop
|
||||
vertex_file = "crtpi_vertex.glsl";
|
||||
data = loadData(Resource::List::get()->get(vertex_file));
|
||||
data = loadData(Resource::List::get()->get(preset_vertex));
|
||||
std::cout << "Usando shaders OpenGL Desktop 3.3\n";
|
||||
} else {
|
||||
std::cout << "Usando shaders OpenGL ES 3.0 (Raspberry Pi)\n";
|
||||
@@ -460,13 +485,16 @@ void Screen::loadShaders() {
|
||||
}
|
||||
if (fragment_shader_source_.empty()) {
|
||||
// Intentar cargar versión ES primero si existe
|
||||
std::string fragment_file = "crtpi_fragment_es.glsl";
|
||||
auto data = loadData(Resource::List::get()->get(fragment_file));
|
||||
std::string fragment_es = preset_fragment;
|
||||
auto pos = fragment_es.rfind(".glsl");
|
||||
if (pos != std::string::npos) {
|
||||
fragment_es.insert(pos, "_es");
|
||||
}
|
||||
auto data = loadData(Resource::List::get()->get(fragment_es));
|
||||
|
||||
if (data.empty()) {
|
||||
// Si no existe versión ES, usar versión Desktop
|
||||
fragment_file = "crtpi_fragment.glsl";
|
||||
data = loadData(Resource::List::get()->get(fragment_file));
|
||||
data = loadData(Resource::List::get()->get(preset_fragment));
|
||||
}
|
||||
|
||||
if (!data.empty()) {
|
||||
@@ -477,7 +505,7 @@ void Screen::loadShaders() {
|
||||
|
||||
// Inicializa los shaders
|
||||
void Screen::initShaders() {
|
||||
if (!Options::video.shaders) {
|
||||
if (!Options::video.postfx) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -619,8 +647,8 @@ auto Screen::initSDLVideo() -> bool {
|
||||
return false;
|
||||
}
|
||||
// Sin OpenGL garantizado, deshabilitar shaders
|
||||
Options::video.shaders = false;
|
||||
std::cout << "WARNING: Shaders disabled (OpenGL not available)\n";
|
||||
Options::video.postfx = false;
|
||||
std::cout << "WARNING: PostFX disabled (OpenGL not available)\n";
|
||||
}
|
||||
|
||||
// Configurar renderer
|
||||
|
||||
@@ -53,11 +53,12 @@ class Screen {
|
||||
static void setBorderEnabled(bool value); // Establece si se ha de ver el borde
|
||||
void toggleBorder(); // Cambia entre borde visible y no visible
|
||||
|
||||
// Paletas y shaders
|
||||
// Paletas y PostFX
|
||||
void nextPalette(); // Cambia a la siguiente paleta
|
||||
void previousPalette(); // Cambia a la paleta anterior
|
||||
void setPalete(); // Establece la paleta actual
|
||||
void toggleShaders(); // Cambia el estado de los shaders
|
||||
void togglePostFX(); // Cambia el estado del PostFX
|
||||
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
|
||||
|
||||
// Surfaces y notificaciones
|
||||
void setRendererSurface(const std::shared_ptr<Surface>& surface = nullptr); // Establece el renderizador para las surfaces
|
||||
|
||||
@@ -122,6 +122,10 @@ Director::Director(std::vector<std::string> const& args) {
|
||||
Options::setConfigFile(Resource::List::get()->get("config.yaml"));
|
||||
Options::loadFromFile();
|
||||
|
||||
// Configura la ruta y carga los presets de PostFX
|
||||
Options::setPostFXFile(Resource::List::get()->get("postfx.yaml"));
|
||||
Options::loadPostFXFromFile();
|
||||
|
||||
// En mode quiosc, forçar pantalla completa independentment de la configuració
|
||||
if (Options::kiosk.enabled) {
|
||||
Options::video.fullscreen = true;
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Video {
|
||||
constexpr bool FULLSCREEN = false; // Modo de pantalla completa por defecto (false = ventana)
|
||||
constexpr Screen::Filter FILTER = Screen::Filter::NEAREST; // Filtro por defecto
|
||||
constexpr bool VERTICAL_SYNC = true; // Vsync activado por defecto
|
||||
constexpr bool SHADERS = false; // Shaders desactivados por defecto
|
||||
constexpr bool POSTFX = false; // PostFX desactivado por defecto
|
||||
constexpr bool INTEGER_SCALE = true; // Escalado entero activado por defecto
|
||||
constexpr bool KEEP_ASPECT = true; // Mantener aspecto activado por defecto
|
||||
constexpr const char* PALETTE_NAME = "zx-spectrum"; // Paleta por defecto
|
||||
|
||||
@@ -329,11 +329,11 @@ void loadBasicVideoFieldsFromYaml(const fkyaml::node& vid) {
|
||||
}
|
||||
}
|
||||
|
||||
if (vid.contains("shaders")) {
|
||||
if (vid.contains("postfx")) {
|
||||
try {
|
||||
video.shaders = vid["shaders"].get_value<bool>();
|
||||
video.postfx = vid["postfx"].get_value<bool>();
|
||||
} catch (...) {
|
||||
video.shaders = Defaults::Video::SHADERS;
|
||||
video.postfx = Defaults::Video::POSTFX;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,7 +606,7 @@ auto saveToFile() -> bool {
|
||||
file << "video:\n";
|
||||
file << " fullscreen: " << (video.fullscreen ? "true" : "false") << "\n";
|
||||
file << " filter: " << filterToString(video.filter) << " # filter: nearest (pixel perfect) | linear (smooth)\n";
|
||||
file << " shaders: " << (video.shaders ? "true" : "false") << "\n";
|
||||
file << " postfx: " << (video.postfx ? "true" : "false") << "\n";
|
||||
file << " vertical_sync: " << (video.vertical_sync ? "true" : "false") << "\n";
|
||||
file << " integer_scale: " << (video.integer_scale ? "true" : "false") << "\n";
|
||||
file << " keep_aspect: " << (video.keep_aspect ? "true" : "false") << "\n";
|
||||
@@ -649,4 +649,97 @@ auto saveToFile() -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Establece la ruta del fichero de PostFX
|
||||
void setPostFXFile(const std::string& path) {
|
||||
postfx_file_path = path;
|
||||
}
|
||||
|
||||
// Carga los presets de PostFX desde el fichero
|
||||
auto loadPostFXFromFile() -> bool {
|
||||
postfx_presets.clear();
|
||||
current_postfx_preset = 0;
|
||||
|
||||
std::ifstream file(postfx_file_path);
|
||||
if (!file.good()) {
|
||||
if (console) {
|
||||
std::cout << "PostFX file not found, creating default: " << postfx_file_path << '\n';
|
||||
}
|
||||
return savePostFXToFile();
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
|
||||
if (yaml.contains("presets")) {
|
||||
const auto& presets = yaml["presets"];
|
||||
for (size_t i = 0; i < presets.size(); ++i) {
|
||||
const auto& p = presets[i];
|
||||
PostFXPreset preset;
|
||||
if (p.contains("name")) {
|
||||
preset.name = p["name"].get_value<std::string>();
|
||||
}
|
||||
if (p.contains("vertex")) {
|
||||
preset.vertex = p["vertex"].get_value<std::string>();
|
||||
}
|
||||
if (p.contains("fragment")) {
|
||||
preset.fragment = p["fragment"].get_value<std::string>();
|
||||
}
|
||||
postfx_presets.push_back(preset);
|
||||
}
|
||||
}
|
||||
|
||||
if (console) {
|
||||
std::cout << "PostFX file loaded: " << postfx_presets.size() << " preset(s)\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
if (console) {
|
||||
std::cerr << "Error parsing PostFX YAML: " << e.what() << '\n';
|
||||
}
|
||||
return savePostFXToFile();
|
||||
}
|
||||
}
|
||||
|
||||
// Guarda los presets de PostFX por defecto
|
||||
auto savePostFXToFile() -> bool {
|
||||
if (postfx_file_path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(postfx_file_path);
|
||||
if (!file.is_open()) {
|
||||
if (console) {
|
||||
std::cerr << "Error: Unable to open file " << postfx_file_path << " for writing\n";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
file << "# JailDoctor's Dilemma - PostFX Presets\n";
|
||||
file << "# Add or modify presets to customize post-processing effects.\n";
|
||||
file << "# vertex and fragment reference shader filenames from the shaders directory.\n";
|
||||
file << "\n";
|
||||
file << "presets:\n";
|
||||
file << " - name: \"CRT\"\n";
|
||||
file << " vertex: \"crtpi_vertex.glsl\"\n";
|
||||
file << " fragment: \"crtpi_fragment.glsl\"\n";
|
||||
|
||||
file.close();
|
||||
|
||||
if (console) {
|
||||
std::cout << "PostFX file created with defaults: " << postfx_file_path << '\n';
|
||||
}
|
||||
|
||||
// Cargar los presets recién creados
|
||||
postfx_presets.clear();
|
||||
postfx_presets.push_back({"CRT", "crtpi_vertex.glsl", "crtpi_fragment.glsl"});
|
||||
current_postfx_preset = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Options
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <algorithm>
|
||||
#include <string> // Para string, basic_string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen::Filter
|
||||
#include "game/defaults.hpp"
|
||||
@@ -79,7 +80,7 @@ struct Video {
|
||||
bool fullscreen{Defaults::Video::FULLSCREEN}; // Contiene el valor del modo de pantalla completa
|
||||
Screen::Filter filter{Defaults::Video::FILTER}; // Filtro usado para el escalado de la imagen
|
||||
bool vertical_sync{Defaults::Video::VERTICAL_SYNC}; // Indica si se quiere usar vsync o no
|
||||
bool shaders{Defaults::Video::SHADERS}; // Indica si se van a usar shaders o no
|
||||
bool postfx{Defaults::Video::POSTFX}; // Indica si se van a usar efectos PostFX o no
|
||||
bool integer_scale{Defaults::Video::INTEGER_SCALE}; // Indica si el escalado de la imagen ha de ser entero en el modo a pantalla completa
|
||||
bool keep_aspect{Defaults::Video::KEEP_ASPECT}; // Indica si se ha de mantener la relación de aspecto al poner el modo a pantalla completa
|
||||
Border border{}; // Borde de la pantalla
|
||||
@@ -113,6 +114,13 @@ struct Game {
|
||||
float height{Defaults::Canvas::HEIGHT}; // Alto de la resolucion del juego
|
||||
};
|
||||
|
||||
// Estructura para un preset de PostFX
|
||||
struct PostFXPreset {
|
||||
std::string name; // Nombre del preset
|
||||
std::string vertex; // Nombre del fichero vertex shader
|
||||
std::string fragment; // Nombre del fichero fragment shader
|
||||
};
|
||||
|
||||
// --- Variables globales ---
|
||||
inline std::string version{}; // Versión del fichero de configuración. Sirve para saber si las opciones son compatibles
|
||||
inline bool console{false}; // Indica si ha de mostrar información por la consola de texto
|
||||
@@ -129,10 +137,18 @@ inline Kiosk kiosk{}; // Opciones del modo kiosko
|
||||
// Ruta completa del fichero de configuración (establecida mediante setConfigFile)
|
||||
inline std::string config_file_path{};
|
||||
|
||||
// --- Variables PostFX ---
|
||||
inline std::vector<PostFXPreset> postfx_presets{}; // Lista de presets de PostFX
|
||||
inline int current_postfx_preset{0}; // Índice del preset de PostFX actual
|
||||
inline std::string postfx_file_path{}; // Ruta del fichero postfx.yaml
|
||||
|
||||
// --- Funciones públicas ---
|
||||
void init(); // Crea e inicializa las opciones del programa
|
||||
void setConfigFile(const std::string& path); // Establece la ruta del fichero de configuración
|
||||
auto loadFromFile() -> bool; // Carga las opciones desde el fichero configurado
|
||||
auto saveToFile() -> bool; // Guarda las opciones al fichero configurado
|
||||
void init(); // Crea e inicializa las opciones del programa
|
||||
void setConfigFile(const std::string& path); // Establece la ruta del fichero de configuración
|
||||
auto loadFromFile() -> bool; // Carga las opciones desde el fichero configurado
|
||||
auto saveToFile() -> bool; // Guarda las opciones al fichero configurado
|
||||
void setPostFXFile(const std::string& path); // Establece la ruta del fichero de PostFX
|
||||
auto loadPostFXFromFile() -> bool; // Carga los presets de PostFX desde el fichero
|
||||
auto savePostFXToFile() -> bool; // Guarda los presets de PostFX por defecto
|
||||
|
||||
} // namespace Options
|
||||
Reference in New Issue
Block a user