add brute-force search command for game seeds that result in rare enemies
This commit is contained in:
@@ -74,3 +74,25 @@ FileContentsCache::GetResult FileContentsCache::get(const char* name,
|
|||||||
std::function<std::string(const std::string&)> generate) {
|
std::function<std::string(const std::string&)> generate) {
|
||||||
return this->get(string(name), generate);
|
return this->get(string(name), generate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shared_ptr<const string> ThreadSafeFileCache::get(
|
||||||
|
const string& name, std::function<shared_ptr<const string>(const std::string&)> generate) {
|
||||||
|
try {
|
||||||
|
shared_lock g(this->lock);
|
||||||
|
auto ret = this->name_to_file.at(name);
|
||||||
|
if (!ret) {
|
||||||
|
throw cannot_open_file(name);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
} catch (const out_of_range&) {
|
||||||
|
unique_lock g(this->lock);
|
||||||
|
auto it = this->name_to_file.find(name);
|
||||||
|
if (it == this->name_to_file.end()) {
|
||||||
|
it = this->name_to_file.emplace(name, generate(name)).first;
|
||||||
|
}
|
||||||
|
if (!it->second) {
|
||||||
|
throw cannot_open_file(name);
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <shared_mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
@@ -102,3 +103,21 @@ private:
|
|||||||
std::unordered_map<std::string, std::shared_ptr<File>> name_to_file;
|
std::unordered_map<std::string, std::shared_ptr<File>> name_to_file;
|
||||||
uint64_t ttl_usecs;
|
uint64_t ttl_usecs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ThreadSafeFileCache {
|
||||||
|
public:
|
||||||
|
explicit ThreadSafeFileCache() = default;
|
||||||
|
ThreadSafeFileCache(const ThreadSafeFileCache&) = delete;
|
||||||
|
ThreadSafeFileCache(ThreadSafeFileCache&&) = delete;
|
||||||
|
ThreadSafeFileCache& operator=(const ThreadSafeFileCache&) = delete;
|
||||||
|
ThreadSafeFileCache& operator=(ThreadSafeFileCache&&) = delete;
|
||||||
|
~ThreadSafeFileCache() = default;
|
||||||
|
|
||||||
|
// Warning: generate() is called while the lock is held for writing, so it
|
||||||
|
// will block other threads.
|
||||||
|
std::shared_ptr<const std::string> get(const std::string& name, std::function<std::shared_ptr<const std::string>(const std::string&)> generate);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_mutex lock;
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<const std::string>> name_to_file;
|
||||||
|
};
|
||||||
|
|||||||
+95
-78
@@ -261,87 +261,64 @@ void Lobby::create_item_creator() {
|
|||||||
this->quest ? this->quest->battle_rules : nullptr);
|
this->quest ? this->quest->battle_rules : nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Lobby::load_maps() {
|
shared_ptr<Map> Lobby::load_maps(
|
||||||
this->map = make_shared<Map>(this->base_version, this->lobby_id, this->random_crypt);
|
Version version,
|
||||||
|
Episode episode,
|
||||||
auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates)
|
uint8_t difficulty,
|
||||||
? this->rare_enemy_rates
|
uint8_t event,
|
||||||
: Map::DEFAULT_RARE_ENEMIES;
|
uint32_t lobby_id,
|
||||||
|
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||||
auto s = this->require_server_state();
|
shared_ptr<PSOLFGEncryption> random_crypt,
|
||||||
if (this->quest) {
|
shared_ptr<const VersionedQuest> vq) {
|
||||||
auto leader_c = this->clients.at(this->leader_id);
|
auto map = make_shared<Map>(version, lobby_id, random_crypt);
|
||||||
if (!leader_c) {
|
|
||||||
throw logic_error("lobby leader is missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto vq = this->quest->version(this->base_version, leader_c->language());
|
|
||||||
auto dat_contents = prs_decompress(*vq->dat_contents);
|
auto dat_contents = prs_decompress(*vq->dat_contents);
|
||||||
this->map->clear();
|
map->add_enemies_and_objects_from_quest_data(
|
||||||
this->map->add_enemies_and_objects_from_quest_data(
|
episode,
|
||||||
this->episode,
|
difficulty,
|
||||||
this->difficulty,
|
event,
|
||||||
this->event,
|
|
||||||
dat_contents.data(),
|
dat_contents.data(),
|
||||||
dat_contents.size(),
|
dat_contents.size(),
|
||||||
this->random_seed,
|
|
||||||
rare_rates);
|
rare_rates);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
} else if (this->mode != GameMode::CHALLENGE) {
|
shared_ptr<Map> Lobby::load_maps(
|
||||||
// No quest loaded - load free-roam maps instead. Don't load free-roam maps
|
Version version,
|
||||||
// in Challenge mode, since players can't go to Ragol without a quest loaded
|
Episode episode,
|
||||||
|
GameMode mode,
|
||||||
|
uint8_t difficulty,
|
||||||
|
uint8_t event,
|
||||||
|
uint32_t lobby_id,
|
||||||
|
function<shared_ptr<const string>(Version, const string&)> get_file_data,
|
||||||
|
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||||
|
shared_ptr<PSOLFGEncryption> random_crypt,
|
||||||
|
const parray<le_uint32_t, 0x20>& variations) {
|
||||||
|
auto map = make_shared<Map>(version, lobby_id, random_crypt);
|
||||||
|
|
||||||
|
// Don't load free-roam maps in Challenge mode, since players can't go to
|
||||||
|
// Ragol without a quest loaded
|
||||||
|
if (mode == GameMode::CHALLENGE) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
for (size_t floor = 0; floor < 0x10; floor++) {
|
for (size_t floor = 0; floor < 0x10; floor++) {
|
||||||
this->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32,
|
|
||||||
floor, this->variations[floor * 2].load(), this->variations[floor * 2 + 1].load());
|
|
||||||
|
|
||||||
auto get_file_data = [&](const string& filename) -> shared_ptr<const string> {
|
|
||||||
if (this->base_version == Version::BB_V4) {
|
|
||||||
try {
|
|
||||||
return s->load_bb_file(filename);
|
|
||||||
} catch (const exception& e) {
|
|
||||||
this->log.info("[Map/%zu:e] Failed to load %s from BB patch tree: %s", floor, filename.c_str(), e.what());
|
|
||||||
}
|
|
||||||
} else if (this->base_version == Version::PC_V2) {
|
|
||||||
try {
|
|
||||||
string path = "system/patch-pc/Media/PSO/" + filename;
|
|
||||||
auto ret = make_shared<string>(load_file(path));
|
|
||||||
this->log.info("[Map/%zu:e] Loaded %s from PC patch tree", floor, filename.c_str());
|
|
||||||
return ret;
|
|
||||||
} catch (const exception& e) {
|
|
||||||
this->log.info("[Map/%zu:e] Failed to load %s from PC patch tree: %s", floor, filename.c_str(), e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
string path = string_printf("system/maps/%s/%s", file_path_token_for_version(this->base_version), filename.c_str());
|
|
||||||
auto ret = make_shared<string>(load_file(path));
|
|
||||||
this->log.info("[Map/%zu:e] Loaded %s from default maps", floor, filename.c_str());
|
|
||||||
return ret;
|
|
||||||
} catch (const exception& e) {
|
|
||||||
this->log.info("[Map/%zu:e] Failed to load %s default maps: %s", floor, filename.c_str(), e.what());
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto enemy_filenames = map_filenames_for_variation(
|
auto enemy_filenames = map_filenames_for_variation(
|
||||||
this->base_version,
|
version,
|
||||||
this->episode,
|
episode,
|
||||||
this->mode,
|
mode,
|
||||||
floor,
|
floor,
|
||||||
this->variations[floor * 2],
|
variations[floor * 2],
|
||||||
this->variations[floor * 2 + 1],
|
variations[floor * 2 + 1],
|
||||||
true);
|
true);
|
||||||
if (enemy_filenames.empty()) {
|
if (!enemy_filenames.empty()) {
|
||||||
this->log.info("[Map/%zu:e] No file to load", floor);
|
|
||||||
} else {
|
|
||||||
bool any_map_loaded = false;
|
bool any_map_loaded = false;
|
||||||
for (const string& filename : enemy_filenames) {
|
for (const string& filename : enemy_filenames) {
|
||||||
auto map_data = get_file_data(filename);
|
auto map_data = get_file_data(version, filename);
|
||||||
if (map_data) {
|
if (map_data) {
|
||||||
this->map->add_enemies_from_map_data(
|
map->add_enemies_from_map_data(
|
||||||
this->episode,
|
episode,
|
||||||
this->difficulty,
|
difficulty,
|
||||||
this->event,
|
event,
|
||||||
floor,
|
floor,
|
||||||
map_data->data(),
|
map_data->data(),
|
||||||
map_data->size(),
|
map_data->size(),
|
||||||
@@ -356,21 +333,19 @@ void Lobby::load_maps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto object_filenames = map_filenames_for_variation(
|
auto object_filenames = map_filenames_for_variation(
|
||||||
this->base_version,
|
version,
|
||||||
this->episode,
|
episode,
|
||||||
this->mode,
|
mode,
|
||||||
floor,
|
floor,
|
||||||
this->variations[floor * 2],
|
variations[floor * 2],
|
||||||
this->variations[floor * 2 + 1],
|
variations[floor * 2 + 1],
|
||||||
false);
|
false);
|
||||||
if (object_filenames.empty()) {
|
if (!object_filenames.empty()) {
|
||||||
this->log.info("[Map/%zu:o] No file to load", floor);
|
|
||||||
} else {
|
|
||||||
bool any_map_loaded = false;
|
bool any_map_loaded = false;
|
||||||
for (const string& filename : object_filenames) {
|
for (const string& filename : object_filenames) {
|
||||||
auto map_data = get_file_data(filename);
|
auto map_data = get_file_data(version, filename);
|
||||||
if (map_data) {
|
if (map_data) {
|
||||||
this->map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
||||||
any_map_loaded = true;
|
any_map_loaded = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -380,6 +355,48 @@ void Lobby::load_maps() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::load_maps() {
|
||||||
|
auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates)
|
||||||
|
? this->rare_enemy_rates
|
||||||
|
: Map::DEFAULT_RARE_ENEMIES;
|
||||||
|
|
||||||
|
if (this->quest) {
|
||||||
|
auto leader_c = this->clients.at(this->leader_id);
|
||||||
|
if (!leader_c) {
|
||||||
|
throw logic_error("lobby leader is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto vq = this->quest->version(this->base_version, leader_c->language());
|
||||||
|
this->map = this->load_maps(
|
||||||
|
this->base_version,
|
||||||
|
this->episode,
|
||||||
|
this->difficulty,
|
||||||
|
this->event,
|
||||||
|
this->lobby_id,
|
||||||
|
rare_rates,
|
||||||
|
this->random_crypt,
|
||||||
|
vq);
|
||||||
|
|
||||||
|
} else if (this->mode != GameMode::CHALLENGE) {
|
||||||
|
auto s = this->require_server_state();
|
||||||
|
this->map = this->load_maps(
|
||||||
|
this->base_version,
|
||||||
|
this->episode,
|
||||||
|
this->mode,
|
||||||
|
this->difficulty,
|
||||||
|
this->event,
|
||||||
|
this->lobby_id,
|
||||||
|
bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2),
|
||||||
|
rare_rates,
|
||||||
|
this->random_crypt,
|
||||||
|
this->variations);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this->map = make_shared<Map>(this->base_version, this->lobby_id, this->random_crypt);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->log.info("Generated objects list (%zu entries):", this->map->objects.size());
|
this->log.info("Generated objects list (%zu entries):", this->map->objects.size());
|
||||||
|
|||||||
@@ -195,6 +195,26 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
|||||||
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
|
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
|
||||||
void set_drop_mode(DropMode new_mode);
|
void set_drop_mode(DropMode new_mode);
|
||||||
void create_item_creator();
|
void create_item_creator();
|
||||||
|
static std::shared_ptr<Map> load_maps(
|
||||||
|
Version version,
|
||||||
|
Episode episode,
|
||||||
|
uint8_t difficulty,
|
||||||
|
uint8_t event,
|
||||||
|
uint32_t lobby_id,
|
||||||
|
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||||
|
std::shared_ptr<PSOLFGEncryption> random_crypt,
|
||||||
|
std::shared_ptr<const VersionedQuest> vq);
|
||||||
|
static std::shared_ptr<Map> load_maps(
|
||||||
|
Version version,
|
||||||
|
Episode episode,
|
||||||
|
GameMode mode,
|
||||||
|
uint8_t difficulty,
|
||||||
|
uint8_t event,
|
||||||
|
uint32_t lobby_id,
|
||||||
|
std::function<std::shared_ptr<const std::string>(Version, const std::string&)> get_file_data,
|
||||||
|
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||||
|
std::shared_ptr<PSOLFGEncryption> random_crypt,
|
||||||
|
const parray<le_uint32_t, 0x20>& variations);
|
||||||
void load_maps();
|
void load_maps();
|
||||||
void create_ep3_server();
|
void create_ep3_server();
|
||||||
|
|
||||||
|
|||||||
+151
@@ -125,6 +125,44 @@ Version get_cli_version(Arguments& args, Version default_value = Version::UNKNOW
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Episode get_cli_episode(Arguments& args) {
|
||||||
|
if (args.get<bool>("ep1")) {
|
||||||
|
return Episode::EP1;
|
||||||
|
} else if (args.get<bool>("ep2")) {
|
||||||
|
return Episode::EP2;
|
||||||
|
} else if (args.get<bool>("ep3")) {
|
||||||
|
return Episode::EP3;
|
||||||
|
} else if (args.get<bool>("ep4")) {
|
||||||
|
return Episode::EP4;
|
||||||
|
} else {
|
||||||
|
throw runtime_error("an episode option is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GameMode get_cli_game_mode(Arguments& args) {
|
||||||
|
if (args.get<bool>("battle")) {
|
||||||
|
return GameMode::BATTLE;
|
||||||
|
} else if (args.get<bool>("challenge")) {
|
||||||
|
return GameMode::CHALLENGE;
|
||||||
|
} else if (args.get<bool>("solo")) {
|
||||||
|
return GameMode::SOLO;
|
||||||
|
} else {
|
||||||
|
return GameMode::NORMAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t get_cli_difficulty(Arguments& args) {
|
||||||
|
if (args.get<bool>("hard")) {
|
||||||
|
return 1;
|
||||||
|
} else if (args.get<bool>("very-hard")) {
|
||||||
|
return 2;
|
||||||
|
} else if (args.get<bool>("ultimate")) {
|
||||||
|
return 3;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
string read_input_data(Arguments& args) {
|
string read_input_data(Arguments& args) {
|
||||||
const string& input_filename = args.get<string>(1, false);
|
const string& input_filename = args.get<string>(1, false);
|
||||||
|
|
||||||
@@ -1783,6 +1821,103 @@ Action a_show_battle_params(
|
|||||||
s.battle_params->get_table(true, Episode::EP4).print(stdout);
|
s.battle_params->get_table(true, Episode::EP4).print(stdout);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Action a_find_rare_enemy_seeds(
|
||||||
|
"find-rare-enemy-seeds", "\
|
||||||
|
find-rare-enemy-seeds OPTIONS...\n\
|
||||||
|
Search all possible rare seeds to find those that produce one or more rare\n\
|
||||||
|
enemies in any set of variations. A version option (e.g. --gc) is required;\n\
|
||||||
|
an episode option (--ep1, --ep2, or --ep4) is also required. A difficulty\n\
|
||||||
|
option (--normal, --hard, --very-hard, or --ultimate) may be given; this\n\
|
||||||
|
affects which rare rates from config.json are used if --bb was given.\n\
|
||||||
|
Similarly, --battle, --challenge, or --solo may also be given; this affects\n\
|
||||||
|
which variations are used on all versions and which rare rates to use for\n\
|
||||||
|
BB. --threads=COUNT controls the number of threads to use for the search\n\
|
||||||
|
by default, one thread per CPU core is used. --min-count specifies how many\n\
|
||||||
|
rare enemies must be found to output the seed. Finally, --quest=NAME may be\n\
|
||||||
|
given to use that quest\'s map instead of the free-roam maps.\n",
|
||||||
|
+[](Arguments& args) {
|
||||||
|
auto version = get_cli_version(args);
|
||||||
|
auto episode = get_cli_episode(args);
|
||||||
|
auto difficulty = get_cli_difficulty(args);
|
||||||
|
auto mode = get_cli_game_mode(args);
|
||||||
|
size_t num_threads = args.get<size_t>("threads", 0);
|
||||||
|
size_t min_count = args.get<size_t>("min-count", 1);
|
||||||
|
string quest_name = args.get<string>("quest", false);
|
||||||
|
|
||||||
|
ServerState s("system/config.json");
|
||||||
|
shared_ptr<const VersionedQuest> vq;
|
||||||
|
if (!quest_name.empty()) {
|
||||||
|
s.load_objects_and_upstream_dependents("quest_index");
|
||||||
|
auto q = s.quest_index(version)->get(quest_name);
|
||||||
|
if (!q) {
|
||||||
|
throw runtime_error("quest does not exist");
|
||||||
|
}
|
||||||
|
vq = q->version(version, 1);
|
||||||
|
if (!vq) {
|
||||||
|
throw runtime_error("quest version does not exist");
|
||||||
|
}
|
||||||
|
} else if (version == Version::BB_V4) {
|
||||||
|
s.load_objects_and_upstream_dependents("config");
|
||||||
|
} else if (version == Version::PC_V2) {
|
||||||
|
s.load_objects_and_upstream_dependents("patch_indexes");
|
||||||
|
} else {
|
||||||
|
s.load_objects_and_upstream_dependents("map_file_caches");
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_ptr<const Map::RareEnemyRates> rare_rates;
|
||||||
|
if (version != Version::BB_V4) {
|
||||||
|
rare_rates = Map::DEFAULT_RARE_ENEMIES;
|
||||||
|
} else if (mode == GameMode::CHALLENGE) {
|
||||||
|
rare_rates = s.rare_enemy_rates_challenge;
|
||||||
|
} else {
|
||||||
|
rare_rates = s.rare_enemy_rates_by_difficulty[difficulty];
|
||||||
|
}
|
||||||
|
|
||||||
|
mutex output_lock;
|
||||||
|
auto thread_fn = [&](uint64_t seed, size_t) -> bool {
|
||||||
|
auto random_crypt = make_shared<PSOV2Encryption>(seed);
|
||||||
|
parray<le_uint32_t, 0x20> variations;
|
||||||
|
|
||||||
|
shared_ptr<Map> map;
|
||||||
|
if (vq) {
|
||||||
|
map = Lobby::load_maps(version, episode, difficulty, 0, 0, rare_rates, random_crypt, vq);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
generate_variations(variations, random_crypt, version, episode, (mode == GameMode::SOLO));
|
||||||
|
map = Lobby::load_maps(
|
||||||
|
version,
|
||||||
|
episode,
|
||||||
|
mode,
|
||||||
|
difficulty,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
bind(&ServerState::load_map_file, &s, placeholders::_1, placeholders::_2),
|
||||||
|
rare_rates,
|
||||||
|
random_crypt,
|
||||||
|
variations);
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<size_t> rare_indexes;
|
||||||
|
for (size_t z = 0; z < map->enemies.size(); z++) {
|
||||||
|
if (enemy_type_is_rare(map->enemies[z].type)) {
|
||||||
|
rare_indexes.emplace_back(z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rare_indexes.size() >= min_count) {
|
||||||
|
fprintf(stdout, "%08" PRIX64 ":", seed);
|
||||||
|
for (size_t index : rare_indexes) {
|
||||||
|
fprintf(stdout, " E-%zX:%s", index, name_for_enum(map->enemies[index].type));
|
||||||
|
}
|
||||||
|
fprintf(stdout, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
parallel_range<uint64_t>(thread_fn, 0, 0x100000000, num_threads);
|
||||||
|
});
|
||||||
|
|
||||||
Action a_parse_object_graph(
|
Action a_parse_object_graph(
|
||||||
"parse-object-graph", nullptr, +[](Arguments& args) {
|
"parse-object-graph", nullptr, +[](Arguments& args) {
|
||||||
uint32_t root_object_address = args.get<uint32_t>("root", Arguments::IntFormat::HEX);
|
uint32_t root_object_address = args.get<uint32_t>("root", Arguments::IntFormat::HEX);
|
||||||
@@ -2117,6 +2252,22 @@ Most options that take data as input also accept the following option:\n\
|
|||||||
--parse-data\n\
|
--parse-data\n\
|
||||||
For modes that take input (from a file or from stdin), parse the input as\n\
|
For modes that take input (from a file or from stdin), parse the input as\n\
|
||||||
a hex string before encrypting/decoding/etc.\n\
|
a hex string before encrypting/decoding/etc.\n\
|
||||||
|
\n\
|
||||||
|
Many versions also accept or require a version option. The version options are:\n\
|
||||||
|
--pc-patch: PC patch server\n\
|
||||||
|
--bb-patch: BB patch server\n\
|
||||||
|
--dc-nte: DC Network Trial Edition\n\
|
||||||
|
--dc-proto or --dc-11-2000: DC 11/2000 prototype\n\
|
||||||
|
--dc-v1: DC v1\n\
|
||||||
|
--dc-v2 or --dc: DC v2\n\
|
||||||
|
--pc-nte: PC Network Trial Edition\n\
|
||||||
|
--pc: PC v2\n\
|
||||||
|
--gc-nte: GC Episodes 1&2 Trial Edition\n\
|
||||||
|
--gc: GC Episodes 1&2\n\
|
||||||
|
--xb: Xbox Episodes 1&2\n\
|
||||||
|
--ep3-trial: GC Episode 3 Trial Edition\n\
|
||||||
|
--ep3: GC Episode 3\n\
|
||||||
|
--bb: Blue Burst\n\
|
||||||
\n",
|
\n",
|
||||||
stderr);
|
stderr);
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-9
@@ -914,10 +914,15 @@ void Map::add_enemy(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 0x0064: // TObjEneSlime
|
case 0x0064: // TObjEneSlime
|
||||||
add(this->check_and_log_rare_enemy((this->version == Version::BB_V4) && (e.uparam1 & 1), rare_rates->pouilly_slime)
|
if ((e.num_children != 0) && (e.num_children != 4)) {
|
||||||
|
this->log.warning("POFUILLY_SLIME has an unusual num_children (0x%hX)", e.num_children.load());
|
||||||
|
}
|
||||||
|
default_num_children = -1; // Skip adding children (because we do it here)
|
||||||
|
for (size_t z = 0; z < 5; z++) {
|
||||||
|
add(this->check_and_log_rare_enemy((this->version == Version::BB_V4) && (e.uparam2 & 1), rare_rates->pouilly_slime)
|
||||||
? EnemyType::POUILLY_SLIME
|
? EnemyType::POUILLY_SLIME
|
||||||
: EnemyType::POFUILLY_SLIME);
|
: EnemyType::POFUILLY_SLIME);
|
||||||
default_num_children = 4;
|
}
|
||||||
break;
|
break;
|
||||||
case 0x0065: // TObjEnePanarms
|
case 0x0065: // TObjEnePanarms
|
||||||
if ((e.num_children != 0) && (e.num_children != 2)) {
|
if ((e.num_children != 0) && (e.num_children != 2)) {
|
||||||
@@ -1489,7 +1494,6 @@ void Map::add_enemies_and_objects_from_quest_data(
|
|||||||
uint8_t event,
|
uint8_t event,
|
||||||
const void* data,
|
const void* data,
|
||||||
size_t size,
|
size_t size,
|
||||||
uint32_t rare_seed,
|
|
||||||
std::shared_ptr<const RareEnemyRates> rare_rates) {
|
std::shared_ptr<const RareEnemyRates> rare_rates) {
|
||||||
auto all_floor_sections = this->collect_quest_map_data_sections(data, size);
|
auto all_floor_sections = this->collect_quest_map_data_sections(data, size);
|
||||||
|
|
||||||
@@ -1503,7 +1507,6 @@ void Map::add_enemies_and_objects_from_quest_data(
|
|||||||
if (header.data_size % sizeof(ObjectEntry)) {
|
if (header.data_size % sizeof(ObjectEntry)) {
|
||||||
throw runtime_error("quest layout object section size is not a multiple of object entry size");
|
throw runtime_error("quest layout object section size is not a multiple of object entry size");
|
||||||
}
|
}
|
||||||
this->log.info("(Floor %02zX) Adding objects", floor);
|
|
||||||
this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size);
|
this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1512,7 +1515,6 @@ void Map::add_enemies_and_objects_from_quest_data(
|
|||||||
if (header.data_size % sizeof(EnemyEntry)) {
|
if (header.data_size % sizeof(EnemyEntry)) {
|
||||||
throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size");
|
throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size");
|
||||||
}
|
}
|
||||||
this->log.info("(Floor %02zX) Adding enemies", floor);
|
|
||||||
this->add_enemies_from_map_data(
|
this->add_enemies_from_map_data(
|
||||||
episode,
|
episode,
|
||||||
difficulty,
|
difficulty,
|
||||||
@@ -1525,12 +1527,11 @@ void Map::add_enemies_and_objects_from_quest_data(
|
|||||||
} else if ((floor_sections.wave_events != 0xFFFFFFFF) &&
|
} else if ((floor_sections.wave_events != 0xFFFFFFFF) &&
|
||||||
(floor_sections.random_enemy_locations != 0xFFFFFFFF) &&
|
(floor_sections.random_enemy_locations != 0xFFFFFFFF) &&
|
||||||
(floor_sections.random_enemy_definitions != 0xFFFFFFFF)) {
|
(floor_sections.random_enemy_definitions != 0xFFFFFFFF)) {
|
||||||
this->log.info("(Floor %02zX) Adding random enemies", floor);
|
|
||||||
const auto& wave_events_header = r.pget<SectionHeader>(floor_sections.wave_events);
|
const auto& wave_events_header = r.pget<SectionHeader>(floor_sections.wave_events);
|
||||||
const auto& random_enemy_locations_header = r.pget<SectionHeader>(floor_sections.random_enemy_locations);
|
const auto& random_enemy_locations_header = r.pget<SectionHeader>(floor_sections.random_enemy_locations);
|
||||||
const auto& random_enemy_definitions_header = r.pget<SectionHeader>(floor_sections.random_enemy_definitions);
|
const auto& random_enemy_definitions_header = r.pget<SectionHeader>(floor_sections.random_enemy_definitions);
|
||||||
if (!random_state) {
|
if (!random_state) {
|
||||||
random_state = make_shared<DATParserRandomState>(rare_seed);
|
random_state = make_shared<DATParserRandomState>(this->random_crypt->seed());
|
||||||
}
|
}
|
||||||
this->add_random_enemies_from_map_data(
|
this->add_random_enemies_from_map_data(
|
||||||
episode,
|
episode,
|
||||||
@@ -1907,12 +1908,49 @@ void generate_variations(
|
|||||||
variations[z * 2 + 0] = 0;
|
variations[z * 2 + 0] = 0;
|
||||||
variations[z * 2 + 1] = 0;
|
variations[z * 2 + 1] = 0;
|
||||||
} else {
|
} else {
|
||||||
variations[z * 2 + 0] = (a.variation1_values.size() < 2) ? 0 : (random_crypt->next() % a.variation1_values.size());
|
variations[z * 2 + 0] = (a.variation1_values.size() <= 1) ? 0 : (random_crypt->next() % a.variation1_values.size());
|
||||||
variations[z * 2 + 1] = (a.variation2_values.size() < 2) ? 0 : (random_crypt->next() % a.variation2_values.size());
|
variations[z * 2 + 1] = (a.variation2_values.size() <= 1) ? 0 : (random_crypt->next() % a.variation2_values.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vector<parray<le_uint32_t, 0x20>> generate_all_possible_variations(Version version, Episode episode, bool is_solo) {
|
||||||
|
parray<uint32_t, 0x20> maxes;
|
||||||
|
for (size_t z = 0; z < 0x10; z++) {
|
||||||
|
const auto& a = file_info_for_variation(version, episode, z, is_solo);
|
||||||
|
if (!a.name_token) {
|
||||||
|
maxes[z * 2 + 0] = 0;
|
||||||
|
maxes[z * 2 + 1] = 0;
|
||||||
|
} else {
|
||||||
|
maxes[z * 2 + 0] = (a.variation1_values.size() <= 1) ? 0 : (a.variation1_values.size() - 1);
|
||||||
|
maxes[z * 2 + 1] = (a.variation2_values.size() <= 1) ? 0 : (a.variation2_values.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<parray<le_uint32_t, 0x20>> ret;
|
||||||
|
parray<le_uint32_t, 0x20> current;
|
||||||
|
for (;;) {
|
||||||
|
ret.emplace_back(current);
|
||||||
|
|
||||||
|
// Increment current by 1 as if it were an 0x20-place integer, with each
|
||||||
|
// "place" having a base of maxes[x] + 1
|
||||||
|
ssize_t x;
|
||||||
|
for (x = 0x1F; x >= 0; x--) {
|
||||||
|
if (current[x] < maxes[x]) {
|
||||||
|
current[x]++;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
current[x] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
vector<string> map_filenames_for_variation(
|
vector<string> map_filenames_for_variation(
|
||||||
Version version,
|
Version version,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
|
|||||||
+3
-1
@@ -306,7 +306,6 @@ struct Map {
|
|||||||
uint8_t event,
|
uint8_t event,
|
||||||
const void* data,
|
const void* data,
|
||||||
size_t size,
|
size_t size,
|
||||||
uint32_t rare_seed,
|
|
||||||
std::shared_ptr<const RareEnemyRates> rare_rates = Map::DEFAULT_RARE_ENEMIES);
|
std::shared_ptr<const RareEnemyRates> rare_rates = Map::DEFAULT_RARE_ENEMIES);
|
||||||
|
|
||||||
const Enemy& find_enemy(uint8_t floor, EnemyType type) const;
|
const Enemy& find_enemy(uint8_t floor, EnemyType type) const;
|
||||||
@@ -357,5 +356,8 @@ void generate_variations(
|
|||||||
Version version,
|
Version version,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
bool is_solo);
|
bool is_solo);
|
||||||
|
std::vector<parray<le_uint32_t, 0x20>> generate_all_possible_variations(
|
||||||
|
Version version, Episode episode, bool is_solo);
|
||||||
|
|
||||||
std::vector<std::string> map_filenames_for_variation(
|
std::vector<std::string> map_filenames_for_variation(
|
||||||
Version version, Episode episode, GameMode mode, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies);
|
Version version, Episode episode, GameMode mode, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies);
|
||||||
|
|||||||
@@ -725,6 +725,7 @@ QuestIndex::QuestIndex(
|
|||||||
} else {
|
} else {
|
||||||
auto q = make_shared<Quest>(vq);
|
auto q = make_shared<Quest>(vq);
|
||||||
this->quests_by_number.emplace(vq->quest_number, q);
|
this->quests_by_number.emplace(vq->quest_number, q);
|
||||||
|
this->quests_by_name.emplace(vq->name, q);
|
||||||
this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q);
|
this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q);
|
||||||
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)",
|
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)",
|
||||||
filenames_str.c_str(),
|
filenames_str.c_str(),
|
||||||
@@ -751,6 +752,14 @@ shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shared_ptr<const Quest> QuestIndex::get(const std::string& name) const {
|
||||||
|
try {
|
||||||
|
return this->quests_by_name.at(name);
|
||||||
|
} catch (const out_of_range&) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
|
vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
|
||||||
QuestMenuType menu_type,
|
QuestMenuType menu_type,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
|
|||||||
@@ -135,11 +135,13 @@ struct QuestIndex {
|
|||||||
std::shared_ptr<const QuestCategoryIndex> category_index;
|
std::shared_ptr<const QuestCategoryIndex> category_index;
|
||||||
|
|
||||||
std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
|
std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
|
||||||
|
std::map<std::string, std::shared_ptr<Quest>> quests_by_name;
|
||||||
std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
|
std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
|
||||||
|
|
||||||
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index, bool is_ep3);
|
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index, bool is_ep3);
|
||||||
|
|
||||||
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
|
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
|
||||||
|
std::shared_ptr<const Quest> get(const std::string& name) const;
|
||||||
|
|
||||||
std::vector<std::shared_ptr<const QuestCategoryIndex::Category>> categories(
|
std::vector<std::shared_ptr<const QuestCategoryIndex::Category>> categories(
|
||||||
QuestMenuType menu_type,
|
QuestMenuType menu_type,
|
||||||
|
|||||||
+48
-2
@@ -30,7 +30,9 @@ ServerState::QuestF960Result::QuestF960Result(const JSON& json, std::shared_ptr<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerState::ServerState() : creation_time(now()) {
|
ServerState::ServerState(const string& config_filename)
|
||||||
|
: creation_time(now()),
|
||||||
|
config_filename(config_filename) {
|
||||||
this->create_load_step_graph();
|
this->create_load_step_graph();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,6 +499,39 @@ shared_ptr<const string> ServerState::load_bb_file(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shared_ptr<const string> ServerState::load_map_file(Version version, const string& filename) const {
|
||||||
|
auto& cache = this->map_file_caches.at(static_cast<size_t>(version));
|
||||||
|
return cache->get(filename, bind(&ServerState::load_map_file_uncached, this, version, placeholders::_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_ptr<const string> ServerState::load_map_file_uncached(Version version, const string& filename) const {
|
||||||
|
if (version == Version::BB_V4) {
|
||||||
|
try {
|
||||||
|
return this->load_bb_file(filename);
|
||||||
|
} catch (const exception& e) {
|
||||||
|
static_game_data_log.info("Failed to load %s from BB patch tree: %s", filename.c_str(), e.what());
|
||||||
|
}
|
||||||
|
} else if (version == Version::PC_V2) {
|
||||||
|
try {
|
||||||
|
string path = "system/patch-pc/Media/PSO/" + filename;
|
||||||
|
auto ret = make_shared<string>(load_file(path));
|
||||||
|
static_game_data_log.info("Loaded %s from PC patch tree", filename.c_str());
|
||||||
|
return ret;
|
||||||
|
} catch (const exception& e) {
|
||||||
|
static_game_data_log.info("Failed to load %s from PC patch tree: %s", filename.c_str(), e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
string path = string_printf("system/maps/%s/%s", file_path_token_for_version(version), filename.c_str());
|
||||||
|
auto ret = make_shared<string>(load_file(path));
|
||||||
|
static_game_data_log.info("Loaded %s from default maps", filename.c_str());
|
||||||
|
return ret;
|
||||||
|
} catch (const exception& e) {
|
||||||
|
static_game_data_log.info("Failed to load %s from default maps: %s", filename.c_str(), e.what());
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
pair<string, uint16_t> ServerState::parse_port_spec(const JSON& json) const {
|
pair<string, uint16_t> ServerState::parse_port_spec(const JSON& json) const {
|
||||||
if (json.is_list()) {
|
if (json.is_list()) {
|
||||||
string addr = json.at(0).as_string();
|
string addr = json.at(0).as_string();
|
||||||
@@ -1087,6 +1122,13 @@ void ServerState::load_patch_indexes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ServerState::clear_map_file_caches() {
|
||||||
|
config_log.info("Clearing map file caches");
|
||||||
|
for (auto& cache : this->map_file_caches) {
|
||||||
|
cache = make_shared<ThreadSafeFileCache>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ServerState::load_battle_params() {
|
void ServerState::load_battle_params() {
|
||||||
config_log.info("Loading battle parameters");
|
config_log.info("Loading battle parameters");
|
||||||
this->battle_params = make_shared<BattleParamsIndex>(
|
this->battle_params = make_shared<BattleParamsIndex>(
|
||||||
@@ -1439,9 +1481,13 @@ void ServerState::create_load_step_graph() {
|
|||||||
// Out: license_index
|
// Out: license_index
|
||||||
this->load_step_graph.add_step("licenses", {"all"}, bind(&ServerState::load_licenses, this));
|
this->load_step_graph.add_step("licenses", {"all"}, bind(&ServerState::load_licenses, this));
|
||||||
|
|
||||||
|
// In: none
|
||||||
|
// Out: map_file_caches
|
||||||
|
this->load_step_graph.add_step("map_file_caches", {"all"}, bind(&ServerState::clear_map_file_caches, this));
|
||||||
|
|
||||||
// In: none
|
// In: none
|
||||||
// Out: pc_patch_file_index, bb_patch_file_index, bb_data_gsl
|
// Out: pc_patch_file_index, bb_patch_file_index, bb_data_gsl
|
||||||
this->load_step_graph.add_step("patch_indexes", {"all"}, bind(&ServerState::load_patch_indexes, this));
|
this->load_step_graph.add_step("patch_indexes", {"all", "map_file_caches"}, bind(&ServerState::load_patch_indexes, this));
|
||||||
|
|
||||||
// In: none
|
// In: none
|
||||||
// Out: ep3_map_index, ep3_card_index, ep3_card_index_trial, ep3_com_deck_index, ep3_tournament_index
|
// Out: ep3_map_index, ep3_card_index, ep3_card_index_trial, ep3_com_deck_index, ep3_tournament_index
|
||||||
|
|||||||
+5
-1
@@ -121,6 +121,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
std::shared_ptr<const FunctionCodeIndex> function_code_index;
|
std::shared_ptr<const FunctionCodeIndex> function_code_index;
|
||||||
std::shared_ptr<const PatchFileIndex> pc_patch_file_index;
|
std::shared_ptr<const PatchFileIndex> pc_patch_file_index;
|
||||||
std::shared_ptr<const PatchFileIndex> bb_patch_file_index;
|
std::shared_ptr<const PatchFileIndex> bb_patch_file_index;
|
||||||
|
std::array<std::shared_ptr<ThreadSafeFileCache>, NUM_VERSIONS> map_file_caches;
|
||||||
std::shared_ptr<const DOLFileIndex> dol_file_index;
|
std::shared_ptr<const DOLFileIndex> dol_file_index;
|
||||||
std::shared_ptr<const Episode3::CardIndex> ep3_card_index;
|
std::shared_ptr<const Episode3::CardIndex> ep3_card_index;
|
||||||
std::shared_ptr<const Episode3::CardIndex> ep3_card_index_trial;
|
std::shared_ptr<const Episode3::CardIndex> ep3_card_index_trial;
|
||||||
@@ -230,7 +231,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
std::shared_ptr<ProxyServer> proxy_server;
|
std::shared_ptr<ProxyServer> proxy_server;
|
||||||
std::shared_ptr<Server> game_server;
|
std::shared_ptr<Server> game_server;
|
||||||
|
|
||||||
ServerState();
|
explicit ServerState(const std::string& config_filename = "");
|
||||||
ServerState(std::shared_ptr<struct event_base> base, const std::string& config_filename, bool is_replay);
|
ServerState(std::shared_ptr<struct event_base> base, const std::string& config_filename, bool is_replay);
|
||||||
ServerState(const ServerState&) = delete;
|
ServerState(const ServerState&) = delete;
|
||||||
ServerState(ServerState&&) = delete;
|
ServerState(ServerState&&) = delete;
|
||||||
@@ -290,6 +291,8 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
const std::string& patch_index_filename,
|
const std::string& patch_index_filename,
|
||||||
const std::string& gsl_filename = "",
|
const std::string& gsl_filename = "",
|
||||||
const std::string& bb_directory_filename = "") const;
|
const std::string& bb_directory_filename = "") const;
|
||||||
|
std::shared_ptr<const std::string> load_map_file(Version version, const std::string& filename) const;
|
||||||
|
std::shared_ptr<const std::string> load_map_file_uncached(Version version, const std::string& filename) const;
|
||||||
|
|
||||||
std::pair<std::string, uint16_t> parse_port_spec(const JSON& json) const;
|
std::pair<std::string, uint16_t> parse_port_spec(const JSON& json) const;
|
||||||
std::vector<PortConfiguration> parse_port_configuration(const JSON& json) const;
|
std::vector<PortConfiguration> parse_port_configuration(const JSON& json) const;
|
||||||
@@ -302,6 +305,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
void load_licenses();
|
void load_licenses();
|
||||||
void load_teams();
|
void load_teams();
|
||||||
void load_patch_indexes();
|
void load_patch_indexes();
|
||||||
|
void clear_map_file_caches();
|
||||||
void load_battle_params();
|
void load_battle_params();
|
||||||
void load_level_table();
|
void load_level_table();
|
||||||
void load_text_index();
|
void load_text_index();
|
||||||
|
|||||||
+9
-2
@@ -82,7 +82,9 @@ struct parray {
|
|||||||
parray(const parray& other) {
|
parray(const parray& other) {
|
||||||
this->operator=(other);
|
this->operator=(other);
|
||||||
}
|
}
|
||||||
parray(parray&& s) = delete;
|
parray(parray&& other) {
|
||||||
|
this->operator=(std::move(other));
|
||||||
|
}
|
||||||
|
|
||||||
template <size_t OtherCount>
|
template <size_t OtherCount>
|
||||||
parray(const parray<ItemT, OtherCount>& s) {
|
parray(const parray<ItemT, OtherCount>& s) {
|
||||||
@@ -168,7 +170,12 @@ struct parray {
|
|||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
parray& operator=(parray&& s) = delete;
|
parray& operator=(parray&& s) {
|
||||||
|
for (size_t x = 0; x < Count; x++) {
|
||||||
|
this->items[x] = s.items[x];
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
template <size_t OtherCount>
|
template <size_t OtherCount>
|
||||||
parray& operator=(const parray<ItemT, OtherCount>& s) {
|
parray& operator=(const parray<ItemT, OtherCount>& s) {
|
||||||
|
|||||||
Reference in New Issue
Block a user