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) {
|
||||
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 <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -102,3 +103,21 @@ private:
|
||||
std::unordered_map<std::string, std::shared_ptr<File>> name_to_file;
|
||||
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;
|
||||
};
|
||||
|
||||
+118
-101
@@ -261,14 +261,109 @@ void Lobby::create_item_creator() {
|
||||
this->quest ? this->quest->battle_rules : nullptr);
|
||||
}
|
||||
|
||||
void Lobby::load_maps() {
|
||||
this->map = make_shared<Map>(this->base_version, this->lobby_id, this->random_crypt);
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
shared_ptr<PSOLFGEncryption> random_crypt,
|
||||
shared_ptr<const VersionedQuest> vq) {
|
||||
auto map = make_shared<Map>(version, lobby_id, random_crypt);
|
||||
auto dat_contents = prs_decompress(*vq->dat_contents);
|
||||
map->add_enemies_and_objects_from_quest_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
dat_contents.data(),
|
||||
dat_contents.size(),
|
||||
rare_rates);
|
||||
return map;
|
||||
}
|
||||
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
Version version,
|
||||
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++) {
|
||||
auto enemy_filenames = map_filenames_for_variation(
|
||||
version,
|
||||
episode,
|
||||
mode,
|
||||
floor,
|
||||
variations[floor * 2],
|
||||
variations[floor * 2 + 1],
|
||||
true);
|
||||
if (!enemy_filenames.empty()) {
|
||||
bool any_map_loaded = false;
|
||||
for (const string& filename : enemy_filenames) {
|
||||
auto map_data = get_file_data(version, filename);
|
||||
if (map_data) {
|
||||
map->add_enemies_from_map_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
floor,
|
||||
map_data->data(),
|
||||
map_data->size(),
|
||||
rare_rates);
|
||||
any_map_loaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_map_loaded) {
|
||||
throw runtime_error(string_printf("no enemy maps loaded for floor %zu", floor));
|
||||
}
|
||||
}
|
||||
|
||||
auto object_filenames = map_filenames_for_variation(
|
||||
version,
|
||||
episode,
|
||||
mode,
|
||||
floor,
|
||||
variations[floor * 2],
|
||||
variations[floor * 2 + 1],
|
||||
false);
|
||||
if (!object_filenames.empty()) {
|
||||
bool any_map_loaded = false;
|
||||
for (const string& filename : object_filenames) {
|
||||
auto map_data = get_file_data(version, filename);
|
||||
if (map_data) {
|
||||
map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
||||
any_map_loaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_map_loaded) {
|
||||
throw runtime_error(string_printf("no object maps loaded for floor %zu", floor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
auto s = this->require_server_state();
|
||||
if (this->quest) {
|
||||
auto leader_c = this->clients.at(this->leader_id);
|
||||
if (!leader_c) {
|
||||
@@ -276,110 +371,32 @@ void Lobby::load_maps() {
|
||||
}
|
||||
|
||||
auto vq = this->quest->version(this->base_version, leader_c->language());
|
||||
auto dat_contents = prs_decompress(*vq->dat_contents);
|
||||
this->map->clear();
|
||||
this->map->add_enemies_and_objects_from_quest_data(
|
||||
this->map = this->load_maps(
|
||||
this->base_version,
|
||||
this->episode,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
dat_contents.data(),
|
||||
dat_contents.size(),
|
||||
this->random_seed,
|
||||
rare_rates);
|
||||
this->lobby_id,
|
||||
rare_rates,
|
||||
this->random_crypt,
|
||||
vq);
|
||||
|
||||
} else if (this->mode != GameMode::CHALLENGE) {
|
||||
// No quest loaded - load free-roam maps instead. Don't load free-roam maps
|
||||
// in Challenge mode, since players can't go to Ragol without a quest loaded
|
||||
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);
|
||||
|
||||
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(
|
||||
this->base_version,
|
||||
this->episode,
|
||||
this->mode,
|
||||
floor,
|
||||
this->variations[floor * 2],
|
||||
this->variations[floor * 2 + 1],
|
||||
true);
|
||||
if (enemy_filenames.empty()) {
|
||||
this->log.info("[Map/%zu:e] No file to load", floor);
|
||||
} else {
|
||||
bool any_map_loaded = false;
|
||||
for (const string& filename : enemy_filenames) {
|
||||
auto map_data = get_file_data(filename);
|
||||
if (map_data) {
|
||||
this->map->add_enemies_from_map_data(
|
||||
this->episode,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
floor,
|
||||
map_data->data(),
|
||||
map_data->size(),
|
||||
rare_rates);
|
||||
any_map_loaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_map_loaded) {
|
||||
throw runtime_error(string_printf("no enemy maps loaded for floor %zu", floor));
|
||||
}
|
||||
}
|
||||
|
||||
auto object_filenames = map_filenames_for_variation(
|
||||
this->base_version,
|
||||
this->episode,
|
||||
this->mode,
|
||||
floor,
|
||||
this->variations[floor * 2],
|
||||
this->variations[floor * 2 + 1],
|
||||
false);
|
||||
if (object_filenames.empty()) {
|
||||
this->log.info("[Map/%zu:o] No file to load", floor);
|
||||
} else {
|
||||
bool any_map_loaded = false;
|
||||
for (const string& filename : object_filenames) {
|
||||
auto map_data = get_file_data(filename);
|
||||
if (map_data) {
|
||||
this->map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
||||
any_map_loaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_map_loaded) {
|
||||
throw runtime_error(string_printf("no object maps loaded for floor %zu", floor));
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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());
|
||||
|
||||
@@ -195,6 +195,26 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
|
||||
void set_drop_mode(DropMode new_mode);
|
||||
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 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) {
|
||||
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);
|
||||
});
|
||||
|
||||
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(
|
||||
"parse-object-graph", nullptr, +[](Arguments& args) {
|
||||
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\
|
||||
For modes that take input (from a file or from stdin), parse the input as\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",
|
||||
stderr);
|
||||
}
|
||||
|
||||
+49
-11
@@ -914,10 +914,15 @@ void Map::add_enemy(
|
||||
break;
|
||||
}
|
||||
case 0x0064: // TObjEneSlime
|
||||
add(this->check_and_log_rare_enemy((this->version == Version::BB_V4) && (e.uparam1 & 1), rare_rates->pouilly_slime)
|
||||
? EnemyType::POUILLY_SLIME
|
||||
: EnemyType::POFUILLY_SLIME);
|
||||
default_num_children = 4;
|
||||
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::POFUILLY_SLIME);
|
||||
}
|
||||
break;
|
||||
case 0x0065: // TObjEnePanarms
|
||||
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,
|
||||
const void* data,
|
||||
size_t size,
|
||||
uint32_t rare_seed,
|
||||
std::shared_ptr<const RareEnemyRates> rare_rates) {
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1512,7 +1515,6 @@ void Map::add_enemies_and_objects_from_quest_data(
|
||||
if (header.data_size % sizeof(EnemyEntry)) {
|
||||
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(
|
||||
episode,
|
||||
difficulty,
|
||||
@@ -1525,12 +1527,11 @@ void Map::add_enemies_and_objects_from_quest_data(
|
||||
} else if ((floor_sections.wave_events != 0xFFFFFFFF) &&
|
||||
(floor_sections.random_enemy_locations != 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& 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);
|
||||
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(
|
||||
episode,
|
||||
@@ -1907,12 +1908,49 @@ void generate_variations(
|
||||
variations[z * 2 + 0] = 0;
|
||||
variations[z * 2 + 1] = 0;
|
||||
} else {
|
||||
variations[z * 2 + 0] = (a.variation1_values.size() < 2) ? 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 + 0] = (a.variation1_values.size() <= 1) ? 0 : (random_crypt->next() % a.variation1_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(
|
||||
Version version,
|
||||
Episode episode,
|
||||
|
||||
+3
-1
@@ -306,7 +306,6 @@ struct Map {
|
||||
uint8_t event,
|
||||
const void* data,
|
||||
size_t size,
|
||||
uint32_t rare_seed,
|
||||
std::shared_ptr<const RareEnemyRates> rare_rates = Map::DEFAULT_RARE_ENEMIES);
|
||||
|
||||
const Enemy& find_enemy(uint8_t floor, EnemyType type) const;
|
||||
@@ -357,5 +356,8 @@ void generate_variations(
|
||||
Version version,
|
||||
Episode episode,
|
||||
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(
|
||||
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 {
|
||||
auto q = make_shared<Quest>(vq);
|
||||
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);
|
||||
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)",
|
||||
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(
|
||||
QuestMenuType menu_type,
|
||||
Episode episode,
|
||||
|
||||
@@ -135,11 +135,13 @@ struct QuestIndex {
|
||||
std::shared_ptr<const QuestCategoryIndex> category_index;
|
||||
|
||||
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;
|
||||
|
||||
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(const std::string& name) const;
|
||||
|
||||
std::vector<std::shared_ptr<const QuestCategoryIndex::Category>> categories(
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
if (json.is_list()) {
|
||||
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() {
|
||||
config_log.info("Loading battle parameters");
|
||||
this->battle_params = make_shared<BattleParamsIndex>(
|
||||
@@ -1439,9 +1481,13 @@ void ServerState::create_load_step_graph() {
|
||||
// Out: license_index
|
||||
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
|
||||
// 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
|
||||
// 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 PatchFileIndex> pc_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 Episode3::CardIndex> ep3_card_index;
|
||||
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<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(const 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& gsl_filename = "",
|
||||
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::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_teams();
|
||||
void load_patch_indexes();
|
||||
void clear_map_file_caches();
|
||||
void load_battle_params();
|
||||
void load_level_table();
|
||||
void load_text_index();
|
||||
|
||||
+9
-2
@@ -82,7 +82,9 @@ struct parray {
|
||||
parray(const parray& other) {
|
||||
this->operator=(other);
|
||||
}
|
||||
parray(parray&& s) = delete;
|
||||
parray(parray&& other) {
|
||||
this->operator=(std::move(other));
|
||||
}
|
||||
|
||||
template <size_t OtherCount>
|
||||
parray(const parray<ItemT, OtherCount>& s) {
|
||||
@@ -168,7 +170,12 @@ struct parray {
|
||||
}
|
||||
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>
|
||||
parray& operator=(const parray<ItemT, OtherCount>& s) {
|
||||
|
||||
Reference in New Issue
Block a user