add ep3 data inspection option

This commit is contained in:
Martin Michelsen
2022-10-05 15:03:22 -07:00
parent 1cb14b48c9
commit 5b3a94f018
3 changed files with 136 additions and 56 deletions
+92 -51
View File
@@ -313,7 +313,7 @@ static const std::vector<Ep3AbilityDescription> name_for_effect_command({
{0x76, false, "Reflect", "Generate reverse attack"}, {0x76, false, "Reflect", "Generate reverse attack"},
}); });
void Ep3CardStats::Stat::decode_code() { void Ep3CardDefinition::Stat::decode_code() {
this->type = static_cast<Type>(this->code / 1000); this->type = static_cast<Type>(this->code / 1000);
int16_t value = this->code - (this->type * 1000); int16_t value = this->code - (this->type * 1000);
if (value != 999) { if (value != 999) {
@@ -338,7 +338,7 @@ void Ep3CardStats::Stat::decode_code() {
} }
} }
string Ep3CardStats::Stat::str() const { string Ep3CardDefinition::Stat::str() const {
switch (this->type) { switch (this->type) {
case Type::BLANK: case Type::BLANK:
return ""; return "";
@@ -365,7 +365,7 @@ string Ep3CardStats::Stat::str() const {
bool Ep3CardStats::Effect::is_empty() const { bool Ep3CardDefinition::Effect::is_empty() const {
return (this->command == 0 && return (this->command == 0 &&
this->expr.is_filled_with(0) && this->expr.is_filled_with(0) &&
this->when == 0 && this->when == 0 &&
@@ -375,7 +375,7 @@ bool Ep3CardStats::Effect::is_empty() const {
this->unknown_a3.is_filled_with(0)); this->unknown_a3.is_filled_with(0));
} }
string Ep3CardStats::Effect::str_for_arg(const std::string& arg) { string Ep3CardDefinition::Effect::str_for_arg(const std::string& arg) {
if (arg.empty()) { if (arg.empty()) {
return arg; return arg;
} }
@@ -428,7 +428,7 @@ string Ep3CardStats::Effect::str_for_arg(const std::string& arg) {
} }
} }
string Ep3CardStats::Effect::str() const { string Ep3CardDefinition::Effect::str() const {
string cmd_str = string_printf("%02hhX", this->command); string cmd_str = string_printf("%02hhX", this->command);
try { try {
const char* name = name_for_effect_command.at(this->command).name; const char* name = name_for_effect_command.at(this->command).name;
@@ -463,7 +463,7 @@ string Ep3CardStats::Effect::str() const {
void Ep3CardStats::decode_range() { void Ep3CardDefinition::decode_range() {
// If the cell representing the FC is nonzero, the card has a range from a // If the cell representing the FC is nonzero, the card has a range from a
// list of constants. Otherwise, its range is already defined in the range // list of constants. Otherwise, its range is already defined in the range
// array and should be left alone. // array and should be left alone.
@@ -599,7 +599,7 @@ string string_for_range(const parray<be_uint32_t, 6>& range) {
return ret; return ret;
} }
string Ep3CardStats::str() const { string Ep3CardDefinition::str() const {
string type_str; string type_str;
try { try {
type_str = name_for_card_type.at(this->type); type_str = name_for_card_type.at(this->type);
@@ -663,12 +663,12 @@ string Ep3CardStats::str() const {
Ep3DataIndex::Ep3DataIndex(const string& directory) { Ep3DataIndex::Ep3DataIndex(const string& directory, bool debug)
static constexpr bool debug_enabled = false; : debug(debug) {
unordered_map<uint32_t, vector<string>> card_tags; unordered_map<uint32_t, vector<string>> card_tags;
if (debug_enabled) { unordered_map<uint32_t, string> card_text;
unordered_map<uint32_t, string> card_text; if (this->debug) {
try { try {
string data = prs_decompress(load_file(directory + "/cardtext.mnr")); string data = prs_decompress(load_file(directory + "/cardtext.mnr"));
StringReader r(data); StringReader r(data);
@@ -676,43 +676,69 @@ Ep3DataIndex::Ep3DataIndex(const string& directory) {
while (!r.eof()) { while (!r.eof()) {
uint32_t card_id = stoul(r.get_cstr()); uint32_t card_id = stoul(r.get_cstr());
// Most cards have multiple pages, but we only care about the first page // Read all pages for this card
// (for now) string text;
string text = r.get_cstr(); string first_page;
for (;;) {
string line = r.get_cstr();
if (line.empty()) {
break;
}
if (first_page.empty()) {
first_page = line;
}
text += '\n';
text += line;
}
// Preprocess text: first, delete all color markers // In orig_text, turn all \t into $ (following newserv conventions)
size_t offset = text.find("\tC"); string orig_text = text;
for (char& ch : orig_text) {
if (ch == '\t') {
ch = '$';
}
}
// Preprocess first page: first, delete all color markers
size_t offset = first_page.find("\tC");
while (offset != string::npos) { while (offset != string::npos) {
text = text.substr(0, offset) + text.substr(offset + 3); first_page = first_page.substr(0, offset) + first_page.substr(offset + 3);
offset = text.find("\tC"); offset = first_page.find("\tC");
} }
// Preprocess text: delete all initial lines that don't start with \t // Preprocess first page: delete all lines that don't start with \t
offset = text.find('\t'); offset = first_page.find('\t');
if (offset == string::npos) { if (offset == string::npos) {
text.clear(); first_page.clear();
} else { } else {
text = text.substr(offset); first_page = first_page.substr(offset);
} }
// Preprocess text: merge lines that don't begin with \t // Preprocess first page: merge lines that don't begin with \t
for (offset = 0; offset < text.size(); offset++) { for (offset = 0; offset < first_page.size(); offset++) {
if (text[offset] == '\n' && text[offset + 1] != '\t') { if (first_page[offset] == '\n' && first_page[offset + 1] != '\t') {
text = text.substr(0, offset) + text.substr(offset + 1); first_page = first_page.substr(0, offset) + first_page.substr(offset + 1);
offset--; offset--;
} }
} }
// Split text into tags // Split first page into tags, and collapse whitespace in the tag names
vector<string> tags; vector<string> tags;
auto lines = split(text, '\n'); auto lines = split(first_page, '\n');
for (const auto& line : lines) { for (const auto& line : lines) {
string tag;
if (line[0] == '\t' && line[1] == 'D') { if (line[0] == '\t' && line[1] == 'D') {
tags.emplace_back("D: " + line.substr(2)); tag = "D: " + line.substr(2);
} else if (line[0] == '\t' && line[1] == 'S') { } else if (line[0] == '\t' && line[1] == 'S') {
tags.emplace_back("S: " + line.substr(2)); tag = "S: " + line.substr(2);
}
if (!tag.empty()) {
for (size_t offset = tag.find(" "); offset != string::npos; offset = tag.find(" ")) {
tag = tag.substr(0, offset) + tag.substr(offset + 1);
}
tags.emplace_back(move(tag));
} }
} }
if (!card_text.emplace(card_id, move(text)).second) { if (!card_text.emplace(card_id, move(orig_text)).second) {
throw runtime_error("duplicate card text id"); throw runtime_error("duplicate card text id");
} }
if (!card_tags.emplace(card_id, move(tags)).second) { if (!card_tags.emplace(card_id, move(tags)).second) {
@@ -731,41 +757,40 @@ Ep3DataIndex::Ep3DataIndex(const string& directory) {
this->compressed_card_definitions = load_file(directory + "/cardupdate.mnr"); this->compressed_card_definitions = load_file(directory + "/cardupdate.mnr");
string data = prs_decompress(this->compressed_card_definitions); string data = prs_decompress(this->compressed_card_definitions);
// There's a footer after the card definitions, but we ignore it // There's a footer after the card definitions, but we ignore it
if (data.size() % sizeof(Ep3CardStats) != sizeof(Ep3CardStatsFooter)) { if (data.size() % sizeof(Ep3CardDefinition) != sizeof(Ep3CardDefinitionsFooter)) {
throw runtime_error(string_printf( throw runtime_error(string_printf(
"decompressed card update file size %zX is not aligned with card definition size %zX (%zX extra bytes)", "decompressed card update file size %zX is not aligned with card definition size %zX (%zX extra bytes)",
data.size(), sizeof(Ep3CardStats), data.size() % sizeof(Ep3CardStats))); data.size(), sizeof(Ep3CardDefinition), data.size() % sizeof(Ep3CardDefinition)));
} }
const auto* stats = reinterpret_cast<const Ep3CardStats*>(data.data()); const auto* def = reinterpret_cast<const Ep3CardDefinition*>(data.data());
size_t max_cards = data.size() / sizeof(Ep3CardStats); size_t max_cards = data.size() / sizeof(Ep3CardDefinition);
for (size_t x = 0; x < max_cards; x++) { for (size_t x = 0; x < max_cards; x++) {
// The last card entry has the build date and some other metadata (and // The last card entry has the build date and some other metadata (and
// isn't a real card, obviously), so skip it. Seems like the card ID is // isn't a real card, obviously), so skip it. Seems like the card ID is
// always a large number that won't fit in a uint16_t, so we use that to // always a large number that won't fit in a uint16_t, so we use that to
// determine if the entry is a real card or not. // determine if the entry is a real card or not.
if (stats[x].card_id & 0xFFFF0000) { if (def[x].card_id & 0xFFFF0000) {
continue; continue;
} }
shared_ptr<CardEntry> entry(new CardEntry({stats[x], {}})); shared_ptr<CardEntry> entry(new CardEntry({def[x], {}, {}}));
if (!this->card_definitions.emplace(entry->stats.card_id, entry).second) { if (!this->card_definitions.emplace(entry->def.card_id, entry).second) {
throw runtime_error(string_printf( throw runtime_error(string_printf(
"duplicate card id: %08" PRIX32, entry->stats.card_id.load())); "duplicate card id: %08" PRIX32, entry->def.card_id.load()));
} }
entry->stats.hp.decode_code(); entry->def.hp.decode_code();
entry->stats.ap.decode_code(); entry->def.ap.decode_code();
entry->stats.tp.decode_code(); entry->def.tp.decode_code();
entry->stats.mv.decode_code(); entry->def.mv.decode_code();
entry->stats.decode_range(); entry->def.decode_range();
if (debug_enabled) { if (this->debug) {
string card_str = entry->stats.str();
try { try {
string tags_str = join(card_tags.at(stats[x].card_id), ", "); entry->text = move(card_text.at(def[x].card_id));
fprintf(stderr, "%s tags: [%s]\n", card_str.c_str(), tags_str.c_str()); } catch (const out_of_range&) { }
} catch (const out_of_range&) { try {
fprintf(stderr, "%s\n", card_str.c_str()); entry->debug_tags = move(card_tags.at(def[x].card_id));
} } catch (const out_of_range&) { }
} }
} }
@@ -832,6 +857,14 @@ shared_ptr<const Ep3DataIndex::CardEntry> Ep3DataIndex::get_card_definition(
return this->card_definitions.at(id); return this->card_definitions.at(id);
} }
std::set<uint32_t> Ep3DataIndex::all_card_ids() const {
std::set<uint32_t> ret;
for (const auto& it : this->card_definitions) {
ret.emplace(it.first);
}
return ret;
}
const string& Ep3DataIndex::get_compressed_map_list() const { const string& Ep3DataIndex::get_compressed_map_list() const {
if (this->compressed_map_list.empty()) { if (this->compressed_map_list.empty()) {
// TODO: Write a version of prs_compress that takes iovecs (or something // TODO: Write a version of prs_compress that takes iovecs (or something
@@ -893,3 +926,11 @@ const string& Ep3DataIndex::get_compressed_map_list() const {
shared_ptr<const Ep3DataIndex::MapEntry> Ep3DataIndex::get_map(uint32_t id) const { shared_ptr<const Ep3DataIndex::MapEntry> Ep3DataIndex::get_map(uint32_t id) const {
return this->maps.at(id); return this->maps.at(id);
} }
std::set<uint32_t> Ep3DataIndex::all_map_ids() const {
std::set<uint32_t> ret;
for (const auto& it : this->maps) {
ret.emplace(it.first);
}
return ret;
}
+11 -5
View File
@@ -4,6 +4,7 @@
#include <string> #include <string>
#include <map> #include <map>
#include <set>
#include <memory> #include <memory>
#include <unordered_map> #include <unordered_map>
#include <phosg/Encoding.hh> #include <phosg/Encoding.hh>
@@ -16,7 +17,7 @@
// and comparing the card text with the data in the file. Some inferences may be // and comparing the card text with the data in the file. Some inferences may be
// incorrect here, since Episode 3's card text is wrong in various places. // incorrect here, since Episode 3's card text is wrong in various places.
struct Ep3CardStats { struct Ep3CardDefinition {
enum Rarity : uint8_t { enum Rarity : uint8_t {
N1 = 0x01, N1 = 0x01,
R1 = 0x02, R1 = 0x02,
@@ -132,7 +133,7 @@ struct Ep3CardStats {
std::string str() const; std::string str() const;
} __attribute__((packed)); // 0x128 bytes in total } __attribute__((packed)); // 0x128 bytes in total
struct Ep3CardStatsFooter { struct Ep3CardDefinitionsFooter {
be_uint32_t num_cards1; be_uint32_t num_cards1;
be_uint32_t unknown_a1; be_uint32_t unknown_a1;
be_uint32_t num_cards2; be_uint32_t num_cards2;
@@ -310,11 +311,12 @@ struct Ep3Map { // .mnmd format; also the format of (decompressed) Ep3 quests
class Ep3DataIndex { class Ep3DataIndex {
public: public:
explicit Ep3DataIndex(const std::string& directory); explicit Ep3DataIndex(const std::string& directory, bool debug = false);
struct CardEntry { struct CardEntry {
Ep3CardStats stats; Ep3CardDefinition def;
std::vector<std::string> text; std::string text;
std::vector<std::string> debug_tags; // Empty unless debug == true
}; };
class MapEntry { class MapEntry {
@@ -332,11 +334,15 @@ public:
const std::string& get_compressed_card_definitions() const; const std::string& get_compressed_card_definitions() const;
std::shared_ptr<const CardEntry> get_card_definition(uint32_t id) const; std::shared_ptr<const CardEntry> get_card_definition(uint32_t id) const;
std::set<uint32_t> all_card_ids() const;
const std::string& get_compressed_map_list() const; const std::string& get_compressed_map_list() const;
std::shared_ptr<const MapEntry> get_map(uint32_t id) const; std::shared_ptr<const MapEntry> get_map(uint32_t id) const;
std::set<uint32_t> all_map_ids() const;
private: private:
bool debug;
std::string compressed_card_definitions; std::string compressed_card_definitions;
std::unordered_map<uint32_t, std::shared_ptr<CardEntry>> card_definitions; std::unordered_map<uint32_t, std::shared_ptr<CardEntry>> card_definitions;
+33
View File
@@ -283,6 +283,9 @@ The options are:\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\
--show-ep3-data\n\
Print the Episode 3 data files (maps and card definitions) from the\n\
system/ep3 directory in a human-readable format.\n\
--replay-log\n\ --replay-log\n\
Replay a terminal log as if it were a client session. input-filename may\n\ Replay a terminal log as if it were a client session. input-filename may\n\
be specified for this option. This is used for regression testing, to\n\ be specified for this option. This is used for regression testing, to\n\
@@ -316,6 +319,7 @@ enum class Behavior {
DECODE_QUEST_FILE, DECODE_QUEST_FILE,
DECODE_SJIS, DECODE_SJIS,
EXTRACT_GSL, EXTRACT_GSL,
SHOW_EP3_DATA,
REPLAY_LOG, REPLAY_LOG,
CAT_CLIENT, CAT_CLIENT,
}; };
@@ -426,6 +430,8 @@ 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 (!strcmp(argv[x], "--show-ep3-data")) {
behavior = Behavior::SHOW_EP3_DATA;
} else if (!strcmp(argv[x], "--replay-log")) { } else if (!strcmp(argv[x], "--replay-log")) {
behavior = Behavior::REPLAY_LOG; behavior = Behavior::REPLAY_LOG;
} else if (!strcmp(argv[x], "--extract-gsl")) { } else if (!strcmp(argv[x], "--extract-gsl")) {
@@ -696,6 +702,33 @@ int main(int argc, char** argv) {
break; break;
} }
case Behavior::SHOW_EP3_DATA: {
config_log.info("Collecting Episode 3 data");
Ep3DataIndex index("system/ep3", true);
auto map_ids = index.all_map_ids();
log_info("%zu maps", map_ids.size());
for (uint32_t map_id : map_ids) {
auto map = index.get_map(map_id);
string name = map->map.name;
string location = map->map.location_name;
log_info("(Map %08" PRIX32 ") %s @ %s", map_id, name.c_str(), location.c_str());
// TODO: Print more information about the map here
}
auto card_ids = index.all_card_ids();
log_info("%zu card definitions", card_ids.size());
for (uint32_t card_id : card_ids) {
auto entry = index.get_card_definition(card_id);
string s = entry->def.str();
string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", ");
string text = entry->text.empty() ? "(No text available)" : entry->text;
log_info("%s\nTags: %s\n%s\n", s.c_str(), tags.c_str(), text.c_str());
}
break;
}
case Behavior::REPLAY_LOG: case Behavior::REPLAY_LOG:
case Behavior::RUN_SERVER: { case Behavior::RUN_SERVER: {
signal(SIGPIPE, SIG_IGN); signal(SIGPIPE, SIG_IGN);