Windows Store support for NetHack 3.6.
This commit is contained in:
@@ -8,8 +8,11 @@
|
||||
#include "hack.h"
|
||||
#include "dlb.h"
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys\stat.h>
|
||||
#include <errno.h>
|
||||
#include <appmodel.h>
|
||||
#include <ShlObj.h>
|
||||
|
||||
#if 0
|
||||
#include "wintty.h"
|
||||
@@ -22,7 +25,7 @@
|
||||
#define E extern
|
||||
static void FDECL(process_options, (int argc, char **argv));
|
||||
static void NDECL(nhusage);
|
||||
static char *FDECL(exepath, (char *));
|
||||
static char *NDECL(get_executable_path);
|
||||
char *NDECL(exename);
|
||||
boolean NDECL(fakeconsole);
|
||||
void NDECL(freefakeconsole);
|
||||
@@ -50,7 +53,6 @@ extern int NDECL(windows_console_custom_nhgetch);
|
||||
void NDECL(safe_routines);
|
||||
|
||||
char orgdir[PATHLEN];
|
||||
char *dir;
|
||||
boolean getreturn_enabled;
|
||||
extern int redirect_stdout; /* from sys/share/pcsys.c */
|
||||
extern int GUILaunched;
|
||||
@@ -67,6 +69,279 @@ static struct stat hbuf;
|
||||
|
||||
extern char orgdir[];
|
||||
|
||||
boolean
|
||||
is_desktop_bridge_application()
|
||||
{
|
||||
UINT32 length = 0;
|
||||
LONG rc = GetCurrentPackageFullName(&length, NULL);
|
||||
|
||||
return (rc == ERROR_INSUFFICIENT_BUFFER);
|
||||
}
|
||||
|
||||
void
|
||||
get_known_folder_path(
|
||||
const KNOWNFOLDERID * folder_id,
|
||||
char * path
|
||||
, size_t path_size)
|
||||
{
|
||||
PWSTR wide_path;
|
||||
if (FAILED(SHGetKnownFolderPath(folder_id, 0, NULL, &wide_path)))
|
||||
error("Unable to get known folder path");
|
||||
|
||||
size_t converted;
|
||||
errno_t err;
|
||||
|
||||
err = wcstombs_s(&converted, path, path_size, wide_path, path_size - 1);
|
||||
|
||||
CoTaskMemFree(wide_path);
|
||||
|
||||
if (err != 0) error("Failed folder path string conversion");
|
||||
}
|
||||
|
||||
void
|
||||
create_directory(const char * path)
|
||||
{
|
||||
HRESULT hr = CreateDirectoryA(path, NULL);
|
||||
|
||||
if (FAILED(hr) && hr != ERROR_ALREADY_EXISTS)
|
||||
error("Unable to create directory '%s'", path);
|
||||
}
|
||||
|
||||
void
|
||||
build_known_folder_path(
|
||||
const KNOWNFOLDERID * folder_id,
|
||||
char * path,
|
||||
size_t path_size)
|
||||
{
|
||||
get_known_folder_path(folder_id, path, path_size);
|
||||
strcat(path, "\\NetHack\\");
|
||||
create_directory(path);
|
||||
strcat(path, "3.6\\");
|
||||
create_directory(path);
|
||||
}
|
||||
|
||||
void
|
||||
build_environment_path(
|
||||
const char * env_str,
|
||||
const char * folder,
|
||||
char * path,
|
||||
size_t path_size)
|
||||
{
|
||||
path[0] = '\0';
|
||||
|
||||
const char * root_path = nh_getenv(env_str);
|
||||
|
||||
if (root_path == NULL) return;
|
||||
|
||||
strcpy_s(path, path_size, root_path);
|
||||
|
||||
char * colon = index(path, ';');
|
||||
if (colon != NULL) path[0] = '\0';
|
||||
|
||||
if (strlen(path) == 0) return;
|
||||
|
||||
append_slash(path);
|
||||
|
||||
if (folder != NULL) {
|
||||
strcat_s(path, path_size, folder);
|
||||
strcat_s(path, path_size, "\\");
|
||||
}
|
||||
}
|
||||
|
||||
boolean
|
||||
folder_file_exists(const char * folder, const char * file_name)
|
||||
{
|
||||
char path[MAX_PATH];
|
||||
|
||||
if (folder[0] == '\0') return FALSE;
|
||||
|
||||
strcpy(path, folder);
|
||||
strcat(path, file_name);
|
||||
return file_exists(path);
|
||||
}
|
||||
|
||||
/*
|
||||
* Rules for setting prefix locations
|
||||
*
|
||||
* COMMON_NETHACK_PATH = %COMMONPROGRAMFILES%\NetHack\3.6\
|
||||
* PROFILE_PATH = %SystemDrive%\Users\%USERNAME%\
|
||||
*
|
||||
* NETHACK_PROFILE_PATH = PROFILE_PATH\NetHack\3.6\
|
||||
* NETHACK_PER_USER_DATA_PATH = PROFILE_PATH\AppData\Local\NetHack\3.6\
|
||||
* NETHACK_GLOBAL_DATA_PATH = %SystemDrive%\ProgramData\NetHack\3.6\
|
||||
* EXECUTABLE_PATH = path to where .exe lives
|
||||
*
|
||||
* HACKPREFIX:
|
||||
* - use environment variable NETHACKDIR if variable is defined
|
||||
* - otherwise use environment variable HACKDIR if variable is defined
|
||||
* - otherwise if store install use NETHACK_PROFILE_PATH
|
||||
* - otherwise if manual install use EXECUTABLE_PATH
|
||||
*
|
||||
* LEVELPREFIX, SAVEPREFIX:
|
||||
* - if store install use NETHACK_PER_USER_DATA_PATH
|
||||
* - if manual install use HACKPREFIX
|
||||
*
|
||||
* BONESPREFIX, SCOREPREFIX, LOCKPREFIX:
|
||||
* - if store install use NETHACK_GLOBAL_DATA_PATH
|
||||
* - if manual install use HACKPREFIX
|
||||
*
|
||||
* DATAPREFIX
|
||||
* - if store install use EXECUTABLE_PATH
|
||||
* - if manual install use HACKPREFIX
|
||||
*
|
||||
* SYSCONFPREFIX
|
||||
* - use COMMON_NETHACK_PATH if sysconf present
|
||||
* - otherwise use HACKPREFIX
|
||||
*
|
||||
* CONFIGPREFIX
|
||||
* - if manual install use PROFILE_PATH
|
||||
* - if store install use NETHACK_PROFILE_PATH
|
||||
*/
|
||||
|
||||
void
|
||||
set_default_prefix_locations(const char *programPath)
|
||||
{
|
||||
char *envp = NULL;
|
||||
char *sptr = NULL;
|
||||
|
||||
static char hack_path[MAX_PATH];
|
||||
static char executable_path[MAX_PATH];
|
||||
static char nethack_profile_path[MAX_PATH];
|
||||
static char nethack_per_user_data_path[MAX_PATH];
|
||||
static char nethack_global_data_path[MAX_PATH];
|
||||
static char sysconf_path[MAX_PATH];
|
||||
|
||||
strcpy(executable_path, get_executable_path());
|
||||
append_slash(executable_path);
|
||||
|
||||
build_environment_path("NETHACKDIR", NULL, hack_path, sizeof(hack_path));
|
||||
|
||||
if (hack_path[0] == '\0')
|
||||
build_environment_path("HACKDIR", NULL, hack_path, sizeof(hack_path));
|
||||
|
||||
build_known_folder_path(&FOLDERID_Profile, nethack_profile_path,
|
||||
sizeof(nethack_profile_path));
|
||||
|
||||
build_known_folder_path(&FOLDERID_LocalAppData,
|
||||
nethack_per_user_data_path, sizeof(nethack_per_user_data_path));
|
||||
|
||||
build_known_folder_path(&FOLDERID_ProgramData,
|
||||
nethack_global_data_path, sizeof(nethack_global_data_path));
|
||||
|
||||
if (hack_path[0] == '\0')
|
||||
strcpy(hack_path, nethack_profile_path);
|
||||
|
||||
fqn_prefix[LEVELPREFIX] = nethack_per_user_data_path;
|
||||
fqn_prefix[SAVEPREFIX] = nethack_per_user_data_path;
|
||||
fqn_prefix[BONESPREFIX] = nethack_global_data_path;
|
||||
fqn_prefix[DATAPREFIX] = executable_path;
|
||||
fqn_prefix[SCOREPREFIX] = nethack_global_data_path;
|
||||
fqn_prefix[LOCKPREFIX] = nethack_global_data_path;
|
||||
fqn_prefix[CONFIGPREFIX] = nethack_profile_path;
|
||||
|
||||
fqn_prefix[HACKPREFIX] = hack_path;
|
||||
fqn_prefix[TROUBLEPREFIX] = hack_path;
|
||||
|
||||
build_environment_path("COMMONPROGRAMFILES", "NetHack\\3.6", sysconf_path,
|
||||
sizeof(sysconf_path));
|
||||
|
||||
if(!folder_file_exists(sysconf_path, SYSCF_FILE))
|
||||
strcpy(sysconf_path, hack_path);
|
||||
|
||||
fqn_prefix[SYSCONFPREFIX] = sysconf_path;
|
||||
|
||||
}
|
||||
|
||||
/* copy file if destination does not exist */
|
||||
void
|
||||
copy_file(
|
||||
const char * dst_folder,
|
||||
const char * dst_name,
|
||||
const char * src_folder,
|
||||
const char * src_name)
|
||||
{
|
||||
char dst_path[MAX_PATH];
|
||||
strcpy(dst_path, dst_folder);
|
||||
strcat(dst_path, dst_name);
|
||||
|
||||
char src_path[MAX_PATH];
|
||||
strcpy(src_path, src_folder);
|
||||
strcat(src_path, src_name);
|
||||
|
||||
if(!file_exists(src_path))
|
||||
error("Unable to copy file '%s' as it does not exist", src_path);
|
||||
|
||||
if(file_exists(dst_path))
|
||||
return;
|
||||
|
||||
BOOL success = CopyFileA(src_path, dst_path, TRUE);
|
||||
if(!success) error("Failed to copy '%s' to '%s'", src_path, dst_path);
|
||||
}
|
||||
|
||||
/* update file copying if it does not exist or src is newer then dst */
|
||||
void
|
||||
update_file(
|
||||
const char * dst_folder,
|
||||
const char * dst_name,
|
||||
const char * src_folder,
|
||||
const char * src_name)
|
||||
{
|
||||
char dst_path[MAX_PATH];
|
||||
strcpy(dst_path, dst_folder);
|
||||
strcat(dst_path, dst_name);
|
||||
|
||||
char src_path[MAX_PATH];
|
||||
strcpy(src_path, src_folder);
|
||||
strcat(src_path, src_name);
|
||||
|
||||
if(!file_exists(src_path))
|
||||
error("Unable to copy file '%s' as it does not exist", src_path);
|
||||
|
||||
if (!file_newer(src_path, dst_path))
|
||||
return;
|
||||
|
||||
BOOL success = CopyFileA(src_path, dst_path, FALSE);
|
||||
if(!success) error("Failed to update '%s' to '%s'", src_path, dst_path);
|
||||
|
||||
}
|
||||
|
||||
void copy_config_content()
|
||||
{
|
||||
/* Keep templates up to date */
|
||||
update_file(fqn_prefix[CONFIGPREFIX], "defaults.tmp",
|
||||
fqn_prefix[DATAPREFIX], "defaults.nh");
|
||||
update_file(fqn_prefix[SYSCONFPREFIX], "sysconf.tmp",
|
||||
fqn_prefix[DATAPREFIX], SYSCF_FILE);
|
||||
|
||||
/* If the required early game file does not exist, copy it */
|
||||
copy_file(fqn_prefix[CONFIGPREFIX], "defaults.nh",
|
||||
fqn_prefix[DATAPREFIX], "defaults.nh");
|
||||
copy_file(fqn_prefix[SYSCONFPREFIX], SYSCF_FILE,
|
||||
fqn_prefix[DATAPREFIX], SYSCF_FILE);
|
||||
|
||||
/* If a required game file does not exist, copy it */
|
||||
/* TODO: Can't HACKDIR be changed during option parsing
|
||||
causing us to perhaps be checking options against the wrong
|
||||
symbols file? */
|
||||
copy_file(fqn_prefix[HACKPREFIX], SYMBOLS,
|
||||
fqn_prefix[DATAPREFIX], SYMBOLS);
|
||||
}
|
||||
|
||||
void
|
||||
copy_hack_content()
|
||||
{
|
||||
/* Keep Guidebook and opthelp up to date */
|
||||
update_file(fqn_prefix[HACKPREFIX], "Guidebook.txt",
|
||||
fqn_prefix[DATAPREFIX], "Guidebook.txt");
|
||||
update_file(fqn_prefix[HACKPREFIX], "opthelp",
|
||||
fqn_prefix[DATAPREFIX], "opthelp");
|
||||
|
||||
/* Keep templates up to date */
|
||||
update_file(fqn_prefix[HACKPREFIX], "symbols.tmp",
|
||||
fqn_prefix[DATAPREFIX], "symbols");
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* __MINGW32__ Note
|
||||
* If the graphics version is built, we don't need a main; it is skipped
|
||||
@@ -120,110 +395,25 @@ _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);*/
|
||||
#endif
|
||||
|
||||
hname = "NetHack"; /* used for syntax messages */
|
||||
|
||||
#if defined(CHDIR) && !defined(NOCWD_ASSUMPTIONS)
|
||||
/* Save current directory and make sure it gets restored when
|
||||
* the game is exited.
|
||||
*/
|
||||
if (getcwd(orgdir, sizeof orgdir) == (char *) 0)
|
||||
error("NetHack: current directory path too long");
|
||||
dir = nh_getenv("NETHACKDIR");
|
||||
if (dir == (char *) 0)
|
||||
dir = nh_getenv("HACKDIR");
|
||||
if (dir == (char *) 0)
|
||||
dir = exepath(argv[0]);
|
||||
#ifdef _MSC_VER
|
||||
if (IsDebuggerPresent()) {
|
||||
static char exepath[_MAX_PATH];
|
||||
/* check if we're running under the debugger so we can get to the right folder anyway */
|
||||
if (dir != (char *)0) {
|
||||
char *top = (char *)0;
|
||||
|
||||
if (strlen(dir) < (_MAX_PATH - 1))
|
||||
strcpy(exepath, dir);
|
||||
top = strstr(exepath, "\\build\\.\\Debug");
|
||||
if (!top) top = strstr(exepath, "\\build\\.\\Release");
|
||||
if (top) {
|
||||
*top = '\0';
|
||||
if (strlen(exepath) < (_MAX_PATH - (strlen("\\binary\\") + 1))) {
|
||||
Strcat(exepath, "\\binary\\");
|
||||
if (strlen(exepath) < (PATHLEN - 1)) {
|
||||
dir = exepath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (dir != (char *)0) {
|
||||
int prefcnt;
|
||||
int fd;
|
||||
boolean have_syscf = FALSE;
|
||||
|
||||
(void) strncpy(hackdir, dir, PATHLEN - 1);
|
||||
hackdir[PATHLEN - 1] = '\0';
|
||||
fqn_prefix[0] = (char *) alloc(strlen(hackdir) + 2);
|
||||
Strcpy(fqn_prefix[0], hackdir);
|
||||
append_slash(fqn_prefix[0]);
|
||||
for (prefcnt = 1; prefcnt < PREFIX_COUNT; prefcnt++)
|
||||
fqn_prefix[prefcnt] = fqn_prefix[0];
|
||||
/* sysconf should be searched for in this location */
|
||||
envp = nh_getenv("COMMONPROGRAMFILES");
|
||||
if (envp) {
|
||||
if ((sptr = index(envp, ';')) != 0)
|
||||
*sptr = '\0';
|
||||
if (strlen(envp) > 0) {
|
||||
fqn_prefix[SYSCONFPREFIX] =
|
||||
(char *) alloc(strlen(envp) + 10);
|
||||
Strcpy(fqn_prefix[SYSCONFPREFIX], envp);
|
||||
append_slash(fqn_prefix[SYSCONFPREFIX]);
|
||||
Strcat(fqn_prefix[SYSCONFPREFIX], "NetHack\\");
|
||||
}
|
||||
}
|
||||
set_default_prefix_locations(argv[0]);
|
||||
|
||||
/* okay so we have the overriding and definitive locaton
|
||||
for sysconf, but only in the event that there is not a
|
||||
sysconf file there (for whatever reason), check a secondary
|
||||
location rather than abort. */
|
||||
#if defined(CHDIR) && !defined(NOCWD_ASSUMPTIONS)
|
||||
chdir(fqn_prefix[HACKPREFIX]);
|
||||
#endif
|
||||
|
||||
/* Is there a SYSCF_FILE there? */
|
||||
fd = open(fqname(SYSCF_FILE, SYSCONFPREFIX, 0), O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
/* readable */
|
||||
close(fd);
|
||||
have_syscf = TRUE;
|
||||
}
|
||||
copy_config_content();
|
||||
|
||||
if (!have_syscf) {
|
||||
/* No SYSCF_FILE where there should be one, and
|
||||
without an installer, a user may not be able
|
||||
to place one there. So, let's try somewhere else... */
|
||||
fqn_prefix[SYSCONFPREFIX] = fqn_prefix[0];
|
||||
|
||||
/* Is there a SYSCF_FILE there? */
|
||||
fd = open(fqname(SYSCF_FILE, SYSCONFPREFIX, 0), O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
/* readable */
|
||||
close(fd);
|
||||
have_syscf = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* user's home directory should default to this - unless
|
||||
* overridden */
|
||||
envp = nh_getenv("USERPROFILE");
|
||||
if (envp) {
|
||||
if ((sptr = index(envp, ';')) != 0)
|
||||
*sptr = '\0';
|
||||
if (strlen(envp) > 0) {
|
||||
fqn_prefix[CONFIGPREFIX] =
|
||||
(char *) alloc(strlen(envp) + 2);
|
||||
Strcpy(fqn_prefix[CONFIGPREFIX], envp);
|
||||
append_slash(fqn_prefix[CONFIGPREFIX]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (GUILaunched || IsDebuggerPresent()) {
|
||||
if (GUILaunched || IsDebuggerPresent())
|
||||
getreturn_enabled = TRUE;
|
||||
}
|
||||
|
||||
check_recordfile((char *) 0);
|
||||
iflags.windowtype_deferred = TRUE;
|
||||
@@ -233,10 +423,11 @@ _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);*/
|
||||
failbuf);
|
||||
nethack_exit(EXIT_FAILURE);
|
||||
}
|
||||
if (!hackdir[0])
|
||||
Strcpy(hackdir, orgdir);
|
||||
|
||||
process_options(argc, argv);
|
||||
|
||||
|
||||
copy_hack_content();
|
||||
|
||||
/*
|
||||
* It seems you really want to play.
|
||||
*/
|
||||
@@ -294,7 +485,7 @@ _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);*/
|
||||
iflags.renameallowed = FALSE;
|
||||
/* Obtain the name of the logged on user and incorporate
|
||||
* it into the name. */
|
||||
Sprintf(fnamebuf, "%s-%s", get_username(0), plname);
|
||||
Sprintf(fnamebuf, "%s", plname);
|
||||
(void) fname_encode(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-.", '%',
|
||||
fnamebuf, encodedfnamebuf, BUFSZ);
|
||||
@@ -401,7 +592,7 @@ char *argv[];
|
||||
*/
|
||||
argc--;
|
||||
argv++;
|
||||
dir = argv[0] + 2;
|
||||
const char * dir = argv[0] + 2;
|
||||
if (*dir == '=' || *dir == ':')
|
||||
dir++;
|
||||
if (!*dir && argc > 1) {
|
||||
@@ -664,33 +855,28 @@ void freefakeconsole()
|
||||
}
|
||||
#endif
|
||||
|
||||
#define EXEPATHBUFSZ 256
|
||||
char exepathbuf[EXEPATHBUFSZ];
|
||||
|
||||
char *
|
||||
exepath(str)
|
||||
char *str;
|
||||
get_executable_path()
|
||||
{
|
||||
char *tmp, *tmp2;
|
||||
int bsize;
|
||||
static char path_buffer[MAX_PATH];
|
||||
|
||||
if (!str)
|
||||
return (char *) 0;
|
||||
bsize = EXEPATHBUFSZ;
|
||||
tmp = exepathbuf;
|
||||
#ifdef UNICODE
|
||||
{
|
||||
TCHAR wbuf[BUFSZ];
|
||||
GetModuleFileName((HANDLE) 0, wbuf, BUFSZ);
|
||||
WideCharToMultiByte(CP_ACP, 0, wbuf, -1, tmp, bsize, NULL, NULL);
|
||||
WideCharToMultiByte(CP_ACP, 0, wbuf, -1, path_buffer, sizeof(path_buffer), NULL, NULL);
|
||||
}
|
||||
#else
|
||||
*(tmp + GetModuleFileName((HANDLE) 0, tmp, bsize)) = '\0';
|
||||
DWORD length = GetModuleFileName((HANDLE) 0, path_buffer, MAX_PATH);
|
||||
if (length == ERROR_INSUFFICIENT_BUFFER) error("Unable to get module name");
|
||||
path_buffer[length] = '\0';
|
||||
#endif
|
||||
tmp2 = strrchr(tmp, PATH_SEPARATOR);
|
||||
if (tmp2)
|
||||
*tmp2 = '\0';
|
||||
return tmp;
|
||||
|
||||
char * seperator = strrchr(path_buffer, PATH_SEPARATOR);
|
||||
if (seperator)
|
||||
*seperator = '\0';
|
||||
|
||||
return path_buffer;
|
||||
}
|
||||
|
||||
/*ARGSUSED*/
|
||||
@@ -943,4 +1129,28 @@ const char *path;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
file_newer returns TRUE if the file at a_path is newer then the file
|
||||
at b_path. If a_path does not exist, it returns FALSE. If b_path
|
||||
does not exist, it returns TRUE.
|
||||
*/
|
||||
boolean
|
||||
file_newer(a_path, b_path)
|
||||
const char * a_path;
|
||||
const char * b_path;
|
||||
{
|
||||
struct stat a_sb;
|
||||
struct stat b_sb;
|
||||
|
||||
if (stat(a_path, &a_sb))
|
||||
return FALSE;
|
||||
|
||||
if (stat(b_path, &b_sb))
|
||||
return TRUE;
|
||||
|
||||
if(difftime(a_sb.st_mtime, b_sb.st_mtime) < 0)
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*windmain.c*/
|
||||
|
||||
@@ -188,15 +188,21 @@ get_username(lan_username_size)
|
||||
int *lan_username_size;
|
||||
{
|
||||
static TCHAR username_buffer[BUFSZ];
|
||||
unsigned int status;
|
||||
DWORD i = BUFSZ - 1;
|
||||
|
||||
/* i gets updated with actual size */
|
||||
status = GetUserName(username_buffer, &i);
|
||||
if (status)
|
||||
username_buffer[i] = '\0';
|
||||
else
|
||||
Strcpy(username_buffer, "NetHack");
|
||||
Strcpy(username_buffer, "NetHack");
|
||||
|
||||
/* Our privacy policy for the windows store version of nethack makes
|
||||
* a promise about not collecting any personally identifiable information.
|
||||
* Do not allow getting user name if we being run from windows store
|
||||
* version of nethack. In 3.7, we should remove use of username.
|
||||
*/
|
||||
if (!is_desktop_bridge_application()) {
|
||||
/* i gets updated with actual size */
|
||||
if (GetUserName(username_buffer, &i))
|
||||
username_buffer[i] = '\0';
|
||||
}
|
||||
|
||||
if (lan_username_size)
|
||||
*lan_username_size = strlen(username_buffer);
|
||||
return username_buffer;
|
||||
|
||||
Reference in New Issue
Block a user