rewrite map data model

This commit is contained in:
Martin Michelsen
2024-12-30 09:20:42 -08:00
parent 69f7bb3db9
commit 72ac20e574
95 changed files with 7596 additions and 5125 deletions
+113 -233
View File
@@ -1407,7 +1407,8 @@ Action a_encode_qst(
pvr_data = make_shared<string>(phosg::load_file(pvr_filename));
} catch (const phosg::cannot_open_file&) {
}
auto vq = make_shared<VersionedQuest>(0, 0, version, 0, bin_data, dat_data, pvr_data);
auto vq = make_shared<VersionedQuest>(0, 0, version, 0, bin_data, dat_data, nullptr, pvr_data);
if (download) {
vq = vq->create_download_quest();
}
@@ -1446,21 +1447,22 @@ Action a_disassemble_quest_script(
});
Action a_disassemble_quest_map(
"disassemble-quest-map", "\
disassemble-quest-map [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
disassemble-quest-map [OPTIONS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Disassemble the input quest map (.dat file) into a text representation of\n\
the data it contains.\n",
the data it contains. If --decompressed is given, don\'t decompress before\n\
disassembling.\n",
+[](phosg::Arguments& args) {
string data = read_input_data(args);
auto data = make_shared<string>(read_input_data(args));
if (!args.get<bool>("decompressed")) {
data = prs_decompress(data);
*data = prs_decompress(*data);
}
string result = Map::disassemble_quest_data(data.data(), data.size());
string result = MapFile(data).disassemble();
write_output_data(args, result.data(), result.size(), "txt");
});
Action a_disassemble_free_map(
"disassemble-free-map", "\
disassemble-free-map INPUT-FILENAME [OUTPUT-FILENAME]\n\
Disassemble the input free-roam map (.dat or .evt file) into a text\n\
Disassemble the input free-play map (.dat or .evt file) into a text\n\
representation of the data it contains. Unlike othe disassembly actions,\n\
this action expects its input to be already decompressed. If the input is\n\
compressed, use the --compressed option. Also unlike other options, the\n\
@@ -1475,18 +1477,19 @@ Action a_disassemble_free_map(
throw runtime_error("cannot determine input file type");
}
string data = read_input_data(args);
auto data = make_shared<string>(read_input_data(args));
if (args.get<bool>("compressed")) {
data = prs_decompress(data);
*data = prs_decompress(*data);
}
uint8_t floor = args.get<uint8_t>("floor", 0);
string result;
if (is_objects) {
result = Map::disassemble_objects_data(data.data(), data.size());
result = MapFile(floor, data, nullptr, nullptr).disassemble();
} else if (is_enemies) {
result = Map::disassemble_enemies_data(data.data(), data.size());
result = MapFile(floor, nullptr, data, nullptr).disassemble();
} else if (is_events) {
result = Map::disassemble_wave_events_data(data.data(), data.size());
result = MapFile(floor, nullptr, nullptr, data).disassemble();
} else {
throw logic_error("unhandled input type");
}
@@ -1912,7 +1915,7 @@ Action a_convert_rare_item_set(
auto data = make_shared<string>(read_input_data(args));
shared_ptr<RareItemSet> rs;
if (phosg::ends_with(input_filename, ".json")) {
rs = make_shared<RareItemSet>(phosg::JSON::parse(*data), s->item_name_index(version));
rs = make_shared<RareItemSet>(phosg::JSON::parse(*data), s->item_name_index_opt(version));
} else if (phosg::ends_with(input_filename, ".gsl")) {
rs = make_shared<RareItemSet>(GSLArchive(data, false), false);
} else if (phosg::ends_with(input_filename, ".gslb")) {
@@ -1931,9 +1934,9 @@ Action a_convert_rare_item_set(
string output_filename = args.get<string>(2, false);
if (output_filename.empty() || (output_filename == "-")) {
rs->print_all_collections(stdout, s->item_name_index(version));
rs->print_all_collections(stdout, s->item_name_index_opt(version));
} else if (phosg::ends_with(output_filename, ".json")) {
auto json = rs->json(s->item_name_index(version));
auto json = rs->json(s->item_name_index_opt(version));
string data = json.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::SORT_DICT_KEYS);
write_output_data(args, data.data(), data.size(), nullptr);
} else if (phosg::ends_with(output_filename, ".gsl")) {
@@ -2088,23 +2091,21 @@ Action a_name_all_items(
}
fprintf(stderr, "IDENT :");
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
Version version = static_cast<Version>(v_s);
const auto& index = s->item_name_indexes.at(v_s);
for (Version v : ALL_VERSIONS) {
const auto& index = s->item_name_index_opt(v);
if (index) {
fprintf(stderr, " %30s ", phosg::name_for_enum(version));
fprintf(stderr, " %30s ", phosg::name_for_enum(v));
}
}
fputc('\n', stderr);
for (uint32_t primary_identifier : all_primary_identifiers) {
fprintf(stderr, "%08" PRIX32 ":", primary_identifier);
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
const auto& index = s->item_name_indexes.at(v_s);
for (Version v : ALL_VERSIONS) {
const auto& index = s->item_name_index_opt(v);
if (index) {
Version version = static_cast<Version>(v_s);
auto pmt = s->item_parameter_table(version);
ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(version), primary_identifier);
auto pmt = s->item_parameter_table(v);
ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(v), primary_identifier);
string name = index->describe_item(item);
try {
bool is_rare = pmt->is_item_rare(item);
@@ -2212,10 +2213,9 @@ Action a_print_item_parameter_tables(
s->load_text_index(false);
s->load_item_definitions(false);
s->load_item_name_indexes(false);
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
const auto& index = s->item_name_indexes.at(v_s);
for (Version v : ALL_VERSIONS) {
const auto& index = s->item_name_index_opt(v);
if (index) {
Version v = static_cast<Version>(v_s);
fprintf(stdout, "======== %s\n", phosg::name_for_enum(v));
index->print_table(stdout);
}
@@ -2569,224 +2569,104 @@ 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",
+[](phosg::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);
auto s = make_shared<ServerState>(get_config_filename(args));
shared_ptr<const VersionedQuest> vq;
if (!quest_name.empty()) {
s->load_config_early();
s->load_quest_index(false);
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_config_early();
} else if (version == Version::PC_V2) {
s->load_patch_indexes(false);
} else {
s->clear_file_caches(false);
}
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) {
if (!vq->dat_contents_decompressed) {
throw runtime_error("quest does not have DAT data");
}
map = Lobby::load_maps(
version, episode, difficulty, 0, 0, rare_rates, seed, random_crypt, vq->dat_contents_decompressed);
} else {
generate_variations_deprecated(variations, random_crypt, version, episode, (mode == GameMode::SOLO));
map = Lobby::load_maps(
version,
episode,
mode,
difficulty,
0,
0,
s->set_data_table(version, episode, mode, difficulty),
bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2),
rare_rates,
seed,
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) {
lock_guard g(output_lock);
fprintf(stdout, "%08" PRIX64 ":", seed);
for (size_t index : rare_indexes) {
fprintf(stdout, " E-%zX:%s", index, phosg::name_for_enum(map->enemies[index].type));
}
fprintf(stdout, "\n");
}
return false;
};
phosg::parallel_range_blocks<uint64_t>(thread_fn, 0, 0x100000000, 0x1000, num_threads, nullptr);
});
Action a_load_maps_test(
"load-maps-test", nullptr, +[](phosg::Arguments& args) {
using SDT = SetDataTable;
bool save_disassembly = args.get<bool>("disassemble");
auto s = make_shared<ServerState>(get_config_filename(args));
s->load_config_early();
s->clear_file_caches(false);
s->load_patch_indexes(false);
s->load_set_data_tables(false);
s->load_quest_index(false);
for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) {
Version v = static_cast<Version>(v_s);
if (is_ep3(v)) {
continue;
}
const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
if (episode == Episode::EP4 && !is_v4(v)) {
continue;
}
if (episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) {
continue;
}
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
if ((mode == GameMode::BATTLE || mode == GameMode::CHALLENGE) && is_v1(v)) {
continue;
}
if (mode == GameMode::SOLO && !is_v4(v)) {
continue;
}
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
if (difficulty == 3 && is_v1(v)) {
continue;
}
auto sdt = s->set_data_table(v, episode, mode, difficulty);
for (uint8_t floor = 0; floor < 0x12; floor++) {
auto variation_maxes = sdt->num_free_roam_variations_for_floor(episode, mode == GameMode::SOLO, floor);
for (size_t var1 = 0; var1 < variation_maxes.first; var1++) {
for (size_t var2 = 0; var2 < variation_maxes.second; var2++) {
auto enemies_filename = sdt->map_filename_for_variation(
floor, var1, var2, episode, mode, SDT::FilenameType::ENEMIES);
auto objects_filename = sdt->map_filename_for_variation(
floor, var1, var2, episode, mode, SDT::FilenameType::OBJECTS);
auto events_filename = sdt->map_filename_for_variation(
floor, var1, var2, episode, mode, SDT::FilenameType::EVENTS);
s->load_maps(false);
fprintf(stderr, "... %s %s %s %s %02hhX %zX %zX",
phosg::name_for_enum(v), name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), floor, var1, var2);
auto map = make_shared<Map>(v, 0, 0, nullptr);
if (!enemies_filename.empty()) {
fprintf(stderr, " [%s => ", enemies_filename.c_str());
auto map_data = s->load_map_file(v, enemies_filename);
if (map_data) {
map->add_enemies_from_map_data(
episode, difficulty, 0, 0, map_data->data(), map_data->size(), Map::DEFAULT_RARE_ENEMIES);
fprintf(stderr, "%zu enemies, %zu sets]", map->enemies.size(), map->enemy_set_flags.size());
} else {
fprintf(stderr, "__MISSING__]");
}
}
if (!objects_filename.empty()) {
fprintf(stderr, " [%s => ", objects_filename.c_str());
auto map_data = s->load_map_file(v, objects_filename);
if (map_data) {
map->add_objects_from_map_data(floor, map_data);
fprintf(stderr, "%zu objects]", map->objects.size());
} else {
fprintf(stderr, "__MISSING__]");
}
}
if (!events_filename.empty()) {
fprintf(stderr, " [%s => ", events_filename.c_str());
auto map_data = s->load_map_file(v, events_filename);
if (map_data) {
map->add_events_from_map_data(floor, map_data->data(), map_data->size());
fprintf(stderr, "%zu events, %zu action bytes]", map->events.size(), map->event_action_stream.size());
} else {
fprintf(stderr, "__MISSING__]");
}
}
fputc('\n', stderr);
}
}
}
}
}
for (const auto& it : s->supermaps) {
auto episode = static_cast<Episode>((it.first >> 28) & 7);
auto mode = static_cast<GameMode>((it.first >> 26) & 3);
uint8_t difficulty = (it.first >> 24) & 3;
uint8_t floor = (it.first >> 16) & 0xFF;
uint8_t layout = (it.first >> 8) & 0xFF;
uint8_t entities = (it.first >> 0) & 0xFF;
fprintf(stderr, "FREE MAP: %08" PRIX32 " => %s %s %c floor=%02hhX layout=%02hhX entities=%02hhX\n",
it.first,
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty),
floor, layout, entities);
if (save_disassembly) {
string filename = phosg::string_printf(
"supermap_%s_%s_%c_%02hhX_%02hhx_%02hhX.txt",
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty),
floor, layout, entities);
auto f = phosg::fopen_unique(filename, "wt");
it.second->print(f.get());
}
}
for (const auto& q_it : s->default_quest_index->quests_by_number) {
for (const auto& vq_it : q_it.second->versions) {
auto vq = vq_it.second;
shared_ptr<const Map> map = Lobby::load_maps(
vq->version,
vq->episode,
0,
0,
0,
Map::DEFAULT_RARE_ENEMIES,
0,
nullptr,
vq->dat_contents_decompressed);
fprintf(stderr, "... %" PRIu32 " (%s) %s %s %s => %zu enemies (%zu sets), %zu objects, %zu events\n",
vq->quest_number,
vq->name.c_str(),
name_for_episode(vq->episode),
phosg::name_for_enum(vq->version),
name_for_language_code(vq->language),
map->enemies.size(),
map->enemy_set_flags.size(),
map->objects.size(),
map->events.size());
// Generate MapStates for a few random variations
for (size_t z = 0; z < 0x20; z++) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
Episode episode = episodes[phosg::random_object<uint32_t>() % episodes.size()];
GameMode mode = modes[phosg::random_object<uint32_t>() % modes.size()];
uint8_t difficulty = phosg::random_object<uint32_t>() % 4;
uint8_t event = phosg::random_object<uint32_t>() % 8;
uint32_t random_seed = phosg::random_object<uint32_t>();
fprintf(stderr, "FREE MAP STATE TEST: %s %s %c\n",
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty));
auto sdt = s->set_data_table(Version::BB_V4, episode, mode, difficulty);
auto variations = sdt->generate_variations(episode, (mode == GameMode::SOLO), nullptr);
auto supermaps = s->supermaps_for_variations(episode, mode, difficulty, variations);
auto map_state = make_shared<MapState>(
0, difficulty, event, random_seed, MapState::DEFAULT_RARE_ENEMIES, nullptr, supermaps);
map_state->verify();
fprintf(stderr, " map state ok: 0x%zX objects, 0x%zX enemies, 0x%zX enemy sets, 0x%zX events\n",
map_state->object_states.size(),
map_state->enemy_states.size(),
map_state->enemy_set_states.size(),
map_state->event_states.size());
}
s->load_quest_index(false);
uint32_t random_seed = args.get<uint32_t>("random-seed", 0, phosg::Arguments::IntFormat::HEX);
for (const auto& it : s->default_quest_index->quests_by_number) {
auto supermap = it.second->get_supermap(random_seed);
if (!supermap) {
fprintf(stderr, "QUEST MAP: %08" PRIX32 " => (no supermap)\n", it.first);
} else {
string filename = phosg::string_printf("supermap_quest_%" PRIu32 "_%08" PRIX32 ".txt", it.first, random_seed);
fprintf(stderr, "QUEST MAP: %08" PRIX32 " => %s\n", it.first, filename.c_str());
if (save_disassembly) {
auto f = phosg::fopen_unique(filename, "wt");
fprintf(f.get(), "QUEST %" PRIu32 " (%s)\n", it.first, it.second->name.c_str());
supermap->print(f.get());
}
}
auto map_state = make_shared<MapState>(
0,
phosg::random_object<uint8_t>() & 3,
0,
phosg::random_object<uint32_t>(),
MapState::DEFAULT_RARE_ENEMIES,
nullptr,
supermap);
map_state->verify();
fprintf(stderr, " map state ok: 0x%zX objects, 0x%zX enemies, 0x%zX enemy sets, 0x%zX events\n",
map_state->object_states.size(),
map_state->enemy_states.size(),
map_state->enemy_set_states.size(),
map_state->event_states.size());
}
});