allow specifying input and output filenames

This commit is contained in:
Martin Michelsen
2022-10-03 21:44:03 -07:00
parent f6987d6627
commit a16c207f4d
2 changed files with 171 additions and 92 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ foreach(TestCase IN ITEMS ${TestCases})
add_test( add_test(
NAME ${TestCase} NAME ${TestCase}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log=${TestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-password=password --require-access-key=111111111111) COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log ${TestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-password=password --require-access-key=111111111111)
endforeach() endforeach()
+170 -91
View File
@@ -216,27 +216,38 @@ void print_usage() {
newserv - a Phantasy Star Online Swiss Army knife\n\ newserv - a Phantasy Star Online Swiss Army knife\n\
\n\ \n\
Usage:\n\ Usage:\n\
newserv [options]\n\ newserv [options] [input-filename [output-filename]]\n\
\n\ \n\
With no options, newserv runs in server mode. PSO clients can connect normally,\n\ With no options, newserv runs in server mode. PSO clients can connect normally,\n\
join lobbies, play games, and use the proxy server. See README.md and\n\ join lobbies, play games, and use the proxy server. See README.md and\n\
system/config.json for more information.\n\ system/config.json for more information.\n\
\n\ \n\
When options are given, newserv will do things other than running the server.\n\ When options are given, newserv will do things other than running the server.\n\
Specifically:\n\ \n\
--compress-data\n\ Some modes accept input and/or output filenames; see the descriptions below for\n\
--decompress-data\n\ details. If input-filename is missing or is '-', newserv reads from stdin;\n\
Compress or decompress data using the PRS algorithm.\n\ similarly, if output-filename is missing or is '-', newserv writes to stdout.\n\
--decrypt-data\n\ \n\
The options are:\n\
--compress-prs\n\
--decompress-prs\n\
Compress or decompress data using the PRS algorithm. Both input-filename\n\
and output-filename may be specified.\n\
--decompress-gjs [input-filename [output-filename]]\n\
Decompress data using the GJS algorithm.Both input-filename and\n\
output-filename may be specified.\n\
--encrypt-data\n\ --encrypt-data\n\
Read from stdin, encrypt or decrypt the data, and write the result to\n\ --decrypt-data\n\
stdout. By default, PSO V3 encryption is used, but this can be overridden\n\ Encrypt or decrypt data using PSO's standard network protocol encryption.\n\
with --pc or --bb. The --seed option specifies the encryption seed (4 hex\n\ Both input-filename and output-filename may be specified. By default, PSO\n\
bytes for PC or GC, or 48 hex bytes for BB). For BB, the --key option is\n\ V3 (GameCube/XBOX) encryption is used, but this can be overridden with\n\
required as well, and refers to a .nsk file in system/blueburst/keys\n\ the --pc or --bb options. The --seed= option specifies the encryption\n\
(without the directory or .nsk extension). For non-BB ciphers, the\n\ seed (4 hex bytes for PC or GC, or 48 hex bytes for BB). For BB, the\n\
--big-endian option applies the cipher masks as big-endian instead of\n\ --key option is required as well, and refers to a .nsk file in\n\
little-endian, which is necessary for some GameCube file formats.\n\ system/blueburst/keys (without the directory or .nsk extension). For\n\
non-BB ciphers, the --big-endian option applies the cipher masks as\n\
big-endian instead of little-endian, which is necessary for some GameCube\n\
file formats.\n\
--find-decryption-seed\n\ --find-decryption-seed\n\
Perform a brute-force search for a decryption seed of the given data.\n\ Perform a brute-force search for a decryption seed of the given data.\n\
The ciphertext is specified with the --encrypted= option and the expected\n\ The ciphertext is specified with the --encrypted= option and the expected\n\
@@ -249,17 +260,21 @@ Specifically:\n\
system, but this can be overridden with the --threads= option.\n\ system, but this can be overridden with the --threads= option.\n\
--decode-sjis\n\ --decode-sjis\n\
Apply newserv\'s text decoding algorithm to the data on stdin, producing\n\ Apply newserv\'s text decoding algorithm to the data on stdin, producing\n\
little-endian UTF-16 data on stdout.\n\ little-endian UTF-16 data on stdout. Both input-filename and\n\
--decode-gci=FILENAME\n\ output-filename may be specified.\n\
--decode-dlq=FILENAME\n\ --decode-gci\n\
--decode-qst=FILENAME\n\ --decode-dlq\n\
--decode-qst\n\
Decode the given quest file into a compressed, unencrypted .bin or .dat\n\ Decode the given quest file into a compressed, unencrypted .bin or .dat\n\
file (or in the case of --decode-qst, both a .bin and a .dat file). The\n\ file (or in the case of --decode-qst, both a .bin and a .dat file).\n\
--decode-gci option can be used to decrypt encrypted GCI files. If you\n\ input-filename must be specified, but output-filename msut not be; the\n\
know the player\'s serial number who generated the GCI file, use the\n\ output is written to <input-filename>.dec (or .bin, or .dat). DLQ and QST\n\
--seed= option and give the serial number (as a hex-encoded integer). If\n\ decoding is a relatively simple operation, but GCI decoding can be\n\
you don\'t know the serial number, newserv will find it via a brute-force\n\ computationally expensive if the file is encrypted and doesn't contain an\n\
search, but this will likely take a long time.\n\ embedded seed. If you know the player\'s serial number who generated the\n\
GCI file, use the --seed= option and give the serial number (as a\n\
hex-encoded 32-bit integer). If you don\'t know the serial number, newserv\n\
will find it via a brute-force search, but this will take a long time.\n\
--cat-client=ADDR:PORT\n\ --cat-client=ADDR:PORT\n\
Connect to the given server and simulate a PSO client. newserv will then\n\ Connect to the given server and simulate a PSO client. newserv will then\n\
print all the received commands to stdout, and forward any commands typed\n\ print all the received commands to stdout, and forward any commands typed\n\
@@ -268,17 +283,23 @@ Specifically:\n\
--gc, and --bb options can be used to select the command format and\n\ --gc, and --bb options can be used to select the command format and\n\
encryption. If --bb is used, the --key option is also required (as in\n\ encryption. If --bb is used, the --key option is also required (as in\n\
--decrypt-data above).\n\ --decrypt-data above).\n\
--replay-log=FILENAME\n\ --replay-log\n\
Replay a terminal log as if it were a client session. This is used for\n\ Replay a terminal log as if it were a client session. input-filename may\n\
regression testing, to make sure client sessions are repeatable and code\n\ be specified for this option. This is used for regression testing, to\n\
changes don\'t affect existing (working) functionality.\n\ make sure client sessions are repeatable and code changes don\'t affect\n\
--extract-gsl=FILENAME\n\ existing (working) functionality.\n\
--extract-gsl\n\
Extract all files from a GSL archive into the current directory.\n\ Extract all files from a GSL archive into the current directory.\n\
input-filename may be specified. If output-filename is specified, then it\n\
is treated as a prefix which is prepended to the filename of each file\n\
contained in the GSL archive. Importantly, if you want to put the files\n\
into a directory, you'll have to create the directory first, and include\n\
a trailing / on output-filename.\n\
\n\ \n\
A few options apply to multiple modes described above:\n\ A few options apply to multiple modes described above:\n\
--parse-data\n\ --parse-data\n\
For modes that take input on stdin, parse the input as a hex string\n\ For modes that take input (from a file or from stdin), parse the input as\n\
before encrypting/decoding/etc.\n\ a hex string before encrypting/decoding/etc.\n\
--config=FILENAME\n\ --config=FILENAME\n\
Use this file instead of system/config.json.\n\ Use this file instead of system/config.json.\n\
", stderr); ", stderr);
@@ -286,10 +307,11 @@ A few options apply to multiple modes described above:\n\
enum class Behavior { enum class Behavior {
RUN_SERVER = 0, RUN_SERVER = 0,
DECOMPRESS_DATA, COMPRESS_PRS,
COMPRESS_DATA, DECOMPRESS_PRS,
DECRYPT_DATA, DECOMPRESS_GJS,
ENCRYPT_DATA, ENCRYPT_DATA,
DECRYPT_DATA,
FIND_DECRYPTION_SEED, FIND_DECRYPTION_SEED,
DECODE_QUEST_FILE, DECODE_QUEST_FILE,
DECODE_SJIS, DECODE_SJIS,
@@ -298,6 +320,27 @@ enum class Behavior {
CAT_CLIENT, CAT_CLIENT,
}; };
static bool behavior_takes_input_filename(Behavior b) {
return (b == Behavior::COMPRESS_PRS) ||
(b == Behavior::DECOMPRESS_PRS) ||
(b == Behavior::DECOMPRESS_GJS) ||
(b == Behavior::ENCRYPT_DATA) ||
(b == Behavior::DECRYPT_DATA) ||
(b == Behavior::DECODE_QUEST_FILE) ||
(b == Behavior::DECODE_SJIS) ||
(b == Behavior::EXTRACT_GSL) ||
(b == Behavior::REPLAY_LOG);
}
static bool behavior_takes_output_filename(Behavior b) {
return (b == Behavior::COMPRESS_PRS) ||
(b == Behavior::DECOMPRESS_PRS) ||
(b == Behavior::DECOMPRESS_GJS) ||
(b == Behavior::ENCRYPT_DATA) ||
(b == Behavior::DECRYPT_DATA) ||
(b == Behavior::DECODE_SJIS);
}
enum class QuestFileFormat { enum class QuestFileFormat {
GCI = 0, GCI = 0,
DLQ, DLQ,
@@ -308,7 +351,6 @@ int main(int argc, char** argv) {
Behavior behavior = Behavior::RUN_SERVER; Behavior behavior = Behavior::RUN_SERVER;
GameVersion cli_version = GameVersion::GC; GameVersion cli_version = GameVersion::GC;
QuestFileFormat quest_file_type = QuestFileFormat::GCI; QuestFileFormat quest_file_type = QuestFileFormat::GCI;
string quest_filename;
string seed; string seed;
string key_file_name; string key_file_name;
const char* config_filename = "system/config.json"; const char* config_filename = "system/config.json";
@@ -319,8 +361,8 @@ int main(int argc, char** argv) {
size_t num_threads = 0; size_t num_threads = 0;
const char* find_decryption_seed_ciphertext = nullptr; const char* find_decryption_seed_ciphertext = nullptr;
vector<const char*> find_decryption_seed_plaintexts; vector<const char*> find_decryption_seed_plaintexts;
const char* replay_log_filename = nullptr; const char* input_filename = nullptr;
const char* extract_gsl_filename = nullptr; const char* output_filename = nullptr;
const char* replay_required_access_key = ""; const char* replay_required_access_key = "";
const char* replay_required_password = ""; const char* replay_required_password = "";
struct sockaddr_storage cat_client_remote; struct sockaddr_storage cat_client_remote;
@@ -328,30 +370,29 @@ int main(int argc, char** argv) {
if (!strcmp(argv[x], "--help")) { if (!strcmp(argv[x], "--help")) {
print_usage(); print_usage();
return 0; return 0;
} else if (!strcmp(argv[x], "--decompress-data")) { } else if (!strcmp(argv[x], "--compress-prs")) {
behavior = Behavior::DECOMPRESS_DATA; behavior = Behavior::COMPRESS_PRS;
} else if (!strcmp(argv[x], "--compress-data")) { } else if (!strcmp(argv[x], "--decompress-prs")) {
behavior = Behavior::COMPRESS_DATA; behavior = Behavior::DECOMPRESS_PRS;
} else if (!strcmp(argv[x], "--decrypt-data")) { } else if (!strcmp(argv[x], "--decompress-gjs")) {
behavior = Behavior::DECRYPT_DATA; behavior = Behavior::DECOMPRESS_PRS;
} else if (!strcmp(argv[x], "--encrypt-data")) { } else if (!strcmp(argv[x], "--encrypt-data")) {
behavior = Behavior::ENCRYPT_DATA; behavior = Behavior::ENCRYPT_DATA;
} else if (!strcmp(argv[x], "--decrypt-data")) {
behavior = Behavior::DECRYPT_DATA;
} else if (!strcmp(argv[x], "--find-decryption-seed")) { } else if (!strcmp(argv[x], "--find-decryption-seed")) {
behavior = Behavior::FIND_DECRYPTION_SEED; behavior = Behavior::FIND_DECRYPTION_SEED;
} else if (!strcmp(argv[x], "--decode-sjis")) { } else if (!strcmp(argv[x], "--decode-sjis")) {
behavior = Behavior::DECODE_SJIS; behavior = Behavior::DECODE_SJIS;
} else if (!strncmp(argv[x], "--decode-gci=", 13)) { } else if (!strcmp(argv[x], "--decode-gci")) {
behavior = Behavior::DECODE_QUEST_FILE; behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::GCI; quest_file_type = QuestFileFormat::GCI;
quest_filename = &argv[x][13]; } else if (!strcmp(argv[x], "--decode-dlq")) {
} else if (!strncmp(argv[x], "--decode-dlq=", 13)) {
behavior = Behavior::DECODE_QUEST_FILE; behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::DLQ; quest_file_type = QuestFileFormat::DLQ;
quest_filename = &argv[x][13]; } else if (!strcmp(argv[x], "--decode-qst")) {
} else if (!strncmp(argv[x], "--decode-qst=", 13)) {
behavior = Behavior::DECODE_QUEST_FILE; behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::QST; quest_file_type = QuestFileFormat::QST;
quest_filename = &argv[x][13];
} else if (!strncmp(argv[x], "--cat-client=", 13)) { } else if (!strncmp(argv[x], "--cat-client=", 13)) {
behavior = Behavior::CAT_CLIENT; behavior = Behavior::CAT_CLIENT;
cat_client_remote = make_sockaddr_storage(parse_netloc(&argv[x][13])).first; cat_client_remote = make_sockaddr_storage(parse_netloc(&argv[x][13])).first;
@@ -385,46 +426,70 @@ int main(int argc, char** argv) {
skip_little_endian = true; skip_little_endian = true;
} else if (!strcmp(argv[x], "--skip-big-endian")) { } else if (!strcmp(argv[x], "--skip-big-endian")) {
skip_big_endian = true; skip_big_endian = true;
} else if (!strncmp(argv[x], "--replay-log=", 13)) { } else if (!strcmp(argv[x], "--replay-log")) {
behavior = Behavior::REPLAY_LOG; behavior = Behavior::REPLAY_LOG;
replay_log_filename = &argv[x][13]; } else if (!strcmp(argv[x], "--extract-gsl")) {
} else if (!strncmp(argv[x], "--extract-gsl=", 14)) {
behavior = Behavior::EXTRACT_GSL; behavior = Behavior::EXTRACT_GSL;
extract_gsl_filename = &argv[x][14];
} else if (!strncmp(argv[x], "--require-password=", 19)) { } else if (!strncmp(argv[x], "--require-password=", 19)) {
replay_required_password = &argv[x][19]; replay_required_password = &argv[x][19];
} else if (!strncmp(argv[x], "--require-access-key=", 21)) { } else if (!strncmp(argv[x], "--require-access-key=", 21)) {
replay_required_access_key = &argv[x][21]; replay_required_access_key = &argv[x][21];
} else if (!strncmp(argv[x], "--config=", 9)) { } else if (!strncmp(argv[x], "--config=", 9)) {
config_filename = &argv[x][9]; config_filename = &argv[x][9];
} else if (!strcmp(argv[x], "-") || argv[x][0] != '-') {
if (!input_filename && behavior_takes_input_filename(behavior)) {
input_filename = argv[x];
} else if (!output_filename && behavior_takes_output_filename(behavior)) {
output_filename = argv[x];
} else {
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
}
} else { } else {
throw invalid_argument(string_printf("unknown option: %s", argv[x])); throw invalid_argument(string_printf("unknown option: %s", argv[x]));
} }
} }
auto read_input_data = [&]() -> string {
string data;
if (input_filename && strcmp(input_filename, "-")) {
return load_file(input_filename);
} else {
return read_all(stdin);
}
};
auto write_output_data = [&](const void* data, size_t size) {
if (output_filename && strcmp(output_filename, "-")) {
save_file(output_filename, data, size);
} else if (isatty(fileno(stdout))) {
print_data(stdout, data, size);
fflush(stdout);
} else {
fwritex(stdout, data, size);
fflush(stdout);
}
};
switch (behavior) { switch (behavior) {
case Behavior::DECOMPRESS_DATA: case Behavior::COMPRESS_PRS:
case Behavior::COMPRESS_DATA: { case Behavior::DECOMPRESS_PRS:
string data = read_all(stdin); case Behavior::DECOMPRESS_GJS: {
string data = read_input_data();
if (parse_data) { if (parse_data) {
data = parse_data_string(data); data = parse_data_string(data);
} }
if (behavior == Behavior::DECOMPRESS_DATA) { if (behavior == Behavior::COMPRESS_PRS) {
data = prs_decompress(data);
} else if (behavior == Behavior::COMPRESS_DATA) {
data = prs_compress(data); data = prs_compress(data);
} else if (behavior == Behavior::DECOMPRESS_PRS) {
data = prs_decompress(data);
} else if (behavior == Behavior::DECOMPRESS_GJS) {
data = gjs_decompress(data);
} else { } else {
throw logic_error("invalid behavior"); throw logic_error("invalid behavior");
} }
if (isatty(fileno(stdout))) { write_output_data(data.data(), data.size());
print_data(stdout, data);
} else {
fwritex(stdout, data);
}
fflush(stdout);
break; break;
} }
@@ -452,7 +517,7 @@ int main(int argc, char** argv) {
throw logic_error("invalid game version"); throw logic_error("invalid game version");
} }
string data = read_all(stdin); string data = read_input_data();
if (parse_data) { if (parse_data) {
data = parse_data_string(data); data = parse_data_string(data);
} }
@@ -484,12 +549,7 @@ int main(int argc, char** argv) {
data.resize(original_size); data.resize(original_size);
if (isatty(fileno(stdout))) { write_output_data(data.data(), data.size());
print_data(stdout, data);
} else {
fwritex(stdout, data);
}
fflush(stdout);
break; break;
} }
@@ -566,38 +626,53 @@ int main(int argc, char** argv) {
break; break;
} }
case Behavior::DECODE_QUEST_FILE: case Behavior::DECODE_QUEST_FILE: {
if (!input_filename || !strcmp(input_filename, "-")) {
throw invalid_argument("an input filename is required");
}
string output_filename_base = input_filename;
if (quest_file_type == QuestFileFormat::GCI) { if (quest_file_type == QuestFileFormat::GCI) {
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16); int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
save_file(quest_filename + ".dec", Quest::decode_gci(quest_filename, num_threads, dec_seed)); save_file(output_filename_base + ".dec", Quest::decode_gci(
input_filename, num_threads, dec_seed));
} else if (quest_file_type == QuestFileFormat::DLQ) { } else if (quest_file_type == QuestFileFormat::DLQ) {
save_file(quest_filename + ".dec", Quest::decode_dlq(quest_filename)); save_file(output_filename_base + ".dec", Quest::decode_dlq(
input_filename));
} else if (quest_file_type == QuestFileFormat::QST) { } else if (quest_file_type == QuestFileFormat::QST) {
auto data = Quest::decode_qst(quest_filename); auto data = Quest::decode_qst(input_filename);
save_file(quest_filename + ".bin", data.first); save_file(output_filename_base + ".bin", data.first);
save_file(quest_filename + ".dat", data.second); save_file(output_filename_base + ".dat", data.second);
} else { } else {
throw logic_error("invalid quest file format"); throw logic_error("invalid quest file format");
} }
break; break;
}
case Behavior::DECODE_SJIS: { case Behavior::DECODE_SJIS: {
string data = read_all(stdin); string data = read_input_data();
if (parse_data) { if (parse_data) {
data = parse_data_string(data); data = parse_data_string(data);
} }
auto decoded = decode_sjis(data); auto decoded = decode_sjis(data);
print_data(stderr, decoded.data(), decoded.size() * sizeof(decoded[0])); write_output_data(decoded.data(), decoded.size() * sizeof(decoded[0]));
break; break;
} }
case Behavior::EXTRACT_GSL: { case Behavior::EXTRACT_GSL: {
shared_ptr<string> data(new string(load_file(extract_gsl_filename))); if (!output_filename) {
GSLArchive gsl(data); output_filename = "";
} else if (!strcmp(output_filename, "-")) {
throw invalid_argument("output prefix cannot be stdout");
}
string data = read_input_data();
shared_ptr<string> data_shared(new string(move(data)));
GSLArchive gsl(data_shared);
for (const auto& entry_it : gsl.all_entries()) { for (const auto& entry_it : gsl.all_entries()) {
auto e = gsl.get(entry_it.first); auto e = gsl.get(entry_it.first);
save_file(entry_it.first, e.first, e.second); save_file(output_filename + entry_it.first, e.first, e.second);
fprintf(stderr, "... %s\n", entry_it.first.c_str()); fprintf(stderr, "... %s\n", entry_it.first.c_str());
} }
break; break;
@@ -640,7 +715,7 @@ int main(int argc, char** argv) {
auto config_json = JSONObject::parse(load_file(config_filename)); auto config_json = JSONObject::parse(load_file(config_filename));
populate_state_from_config(state, config_json); populate_state_from_config(state, config_json);
if (!replay_log_filename) { if (behavior != Behavior::REPLAY_LOG) {
config_log.info("Loading license list"); config_log.info("Loading license list");
state->license_manager.reset(new LicenseManager("system/licenses.nsi")); state->license_manager.reset(new LicenseManager("system/licenses.nsi"));
} else { } else {
@@ -690,7 +765,7 @@ int main(int argc, char** argv) {
config_log.info("Collecting quest metadata"); config_log.info("Collecting quest metadata");
state->quest_index.reset(new QuestIndex("system/quests")); state->quest_index.reset(new QuestIndex("system/quests"));
if (!replay_log_filename) { if (behavior != Behavior::REPLAY_LOG) {
config_log.info("Compiling client functions"); config_log.info("Compiling client functions");
state->function_code_index.reset(new FunctionCodeIndex("system/ppc")); state->function_code_index.reset(new FunctionCodeIndex("system/ppc"));
config_log.info("Loading DOL files"); config_log.info("Loading DOL files");
@@ -703,14 +778,14 @@ int main(int argc, char** argv) {
config_log.info("Creating menus"); config_log.info("Creating menus");
state->create_menus(config_json); state->create_menus(config_json);
if (replay_log_filename) { if (behavior == Behavior::REPLAY_LOG) {
state->allow_saving = false; state->allow_saving = false;
state->license_manager->set_autosave(false); state->license_manager->set_autosave(false);
config_log.info("Saving disabled because this is a replay session"); config_log.info("Saving disabled because this is a replay session");
} }
shared_ptr<DNSServer> dns_server; shared_ptr<DNSServer> dns_server;
if (state->dns_server_port && !replay_log_filename) { if (state->dns_server_port && (behavior != Behavior::REPLAY_LOG)) {
config_log.info("Starting DNS server"); config_log.info("Starting DNS server");
dns_server.reset(new DNSServer(base, state->local_address, dns_server.reset(new DNSServer(base, state->local_address,
state->external_address)); state->external_address));
@@ -728,9 +803,13 @@ int main(int argc, char** argv) {
config_log.info("Starting game server"); config_log.info("Starting game server");
state->game_server.reset(new Server(base, state)); state->game_server.reset(new Server(base, state));
auto f = fopen_unique(replay_log_filename, "rt"); shared_ptr<FILE> log_f(stdin, +[](FILE*) { });
if (input_filename && strcmp(input_filename, "-")) {
log_f = fopen_shared(input_filename, "rt");
}
replay_session.reset(new ReplaySession( replay_session.reset(new ReplaySession(
base, f.get(), state, replay_required_access_key, replay_required_password)); base, log_f.get(), state, replay_required_access_key, replay_required_password));
replay_session->start(); replay_session->start();
} else if (behavior == Behavior::RUN_SERVER) { } else if (behavior == Behavior::RUN_SERVER) {