fix many edge cases in item name parsing

This commit is contained in:
Martin Michelsen
2025-11-05 21:31:44 -08:00
parent a99f552e7c
commit 766d4e0c7a
3 changed files with 149 additions and 22 deletions
+14 -1
View File
@@ -653,7 +653,20 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* `$warpme <floor-id>` (or `$warp <floor-id>`): Warp yourself to the given floor.
* `$warpall <floor-id>`: Warp everyone in the game to the given floor. You must be the leader to use this command, unless you're on the proxy.
* `$next`: Warp yourself to the next floor.
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item (e.g. "Hell Saber +5 0/10/25/0/10") or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions.
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions. Here are some examples to illustrate the syntax (nothing is case-sensitive, and everything except the item name itself is optional):
* `$item Saber +5 0/10/25/0/10` (weapon with special, grind and attributes)
* `$item ???? Draw Autogun` (untekked weapon with special; can have grind/attributes too, as above)
* `$item SEALED J-SWORD K:2000` (weapon with kill count)
* `$item ES APHEX ZALURE TWIN +200` (ES weapon must be prefixed with "ES"; name comes before special)
* `$item DF FIELD +10DEF +20EVP +4` (armor with DFP bonus, EVP bonus, and slot count)
* `$item RED MERGE +10DFP +20EVP` (shield; same as armor except without slot count)
* `$item Knight/Power++` (unit)
* `$item LIMITER K:1000` (sealed unit with kill count)
* `$item Tapas PB:F,G,M&Y 120% 200IQ 5/195/0/0 green` (mag with PBs, synchro, IO, stats, and color)
* `$item Trimate x10` (tool with stack size)
* `$item Disk:Reverser` (technique disk without level)
* `$item Disk:Razonde Lv.30` (technique disk with level)
* `$item 1000 Meseta`
* `$unset <index>` (non-proxy only): In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed. You can also destroy the assist card set on yourself with `$unset 0`.
* `$dropmode [mode]` (proxy only): Change the way item drops behave in the current game, if you are not on BB. Unlike the game server version of this command, using this on the proxy requires cheats to be enabled. This works by intercepting the drop requests sent to and from the leader. (So, if you are the leader and not using server drop mode on the remote server, it affects the entire game; otherwise, it affects only items generated by your actions.) `mode` can be `none` (no drops), `default` (normal drops), or `proxy` (use newserv's drop tables instead of the remote server's). If `mode` is not given, tells you the current drop mode without changing it.
+134 -21
View File
@@ -12,9 +12,16 @@ ItemNameIndex::ItemNameIndex(
limits(limits) {
for (uint32_t primary_identifier : item_parameter_table->compute_all_valid_primary_identifiers()) {
// 00000000 is a valid primary identifier but not a valid weapon; skip it
if (primary_identifier == 0x00000000) {
continue;
}
const string* name = nullptr;
bool is_es_weapon = false;
try {
ItemData item = ItemData::from_primary_identifier(*this->limits, primary_identifier);
is_es_weapon = item.is_s_rank_weapon();
name = &name_coll.at(item_parameter_table->get_item_id(item));
} catch (const out_of_range&) {
}
@@ -25,11 +32,14 @@ ItemNameIndex::ItemNameIndex(
meta->name = *name;
this->primary_identifier_index.emplace(meta->primary_identifier, meta);
this->name_index.emplace(phosg::tolower(meta->name), meta);
if (is_es_weapon && ((primary_identifier & 0x0000FFFF) == 0x00000000)) {
this->es_name_index.emplace(phosg::tolower(meta->name), meta);
}
}
}
}
static const char* s_rank_name_characters = "\0ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_";
static std::string s_rank_name_characters("\0ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_", 0x20);
// clang-format off
static const array<const char*, 0x29> name_for_weapon_special = {
@@ -201,7 +211,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, uint8_t flags) co
string name;
for (size_t x = 0; x < 8; x++) {
char ch = s_rank_name_characters[char_indexes[x]];
char ch = s_rank_name_characters.at(char_indexes[x]);
if (ch == 0) {
break;
}
@@ -413,6 +423,112 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
return ret;
}
if (desc.starts_with("es ")) {
auto parse_name = [&](const std::string& token) -> void {
if (token.size() > 8) {
throw runtime_error("s-rank name too long");
}
uint8_t char_indexes[8] = {0, 0, 0, 0, 0, 0, 0, 0};
for (size_t z = 0; z < token.size(); z++) {
char ch = toupper(token[z]);
size_t pos = s_rank_name_characters.find(ch);
if (pos == std::string::npos) {
throw runtime_error(std::format("s-rank name contains invalid character {:02X} ({})", ch, ch));
}
char_indexes[z] = pos;
}
ret.data1w[3] = phosg::bswap16(0x8000 | (char_indexes[1] & 0x1F) | ((char_indexes[0] & 0x1F) << 5));
ret.data1w[4] = phosg::bswap16(0x8000 | (char_indexes[4] & 0x1F) | ((char_indexes[3] & 0x1F) << 5) | ((char_indexes[2] & 0x1F) << 10));
ret.data1w[5] = phosg::bswap16(0x8000 | (char_indexes[7] & 0x1F) | ((char_indexes[6] & 0x1F) << 5) | ((char_indexes[5] & 0x1F) << 10));
};
auto parse_special = [&](const std::string& token) -> bool {
for (size_t z = 0; z < name_for_s_rank_special.size(); z++) {
if (name_for_s_rank_special[z] && (token == phosg::tolower(name_for_s_rank_special[z]))) {
ret.data1[2] = z;
return true;
}
}
return false;
};
auto parse_grind = [&](const std::string& token) -> bool {
if (token.starts_with('+')) {
ret.data1[3] = stoul(token.substr(1), nullptr, 0);
return true;
}
return false;
};
auto parse_type = [&](const std::string& token) -> void {
const auto& meta = this->es_name_index.at(token);
if (meta->primary_identifier & 0xFF00FFFF) {
throw std::runtime_error("ES weapon has invalid bits in primary identifier");
}
ret.data1[1] = meta->primary_identifier >> 16;
};
// Syntax: ES [NAME] [SPECIAL] TYPE [+GRIND]
auto tokens = phosg::split(desc, ' ');
switch (tokens.size()) {
case 0:
case 1:
throw std::runtime_error("ES weapon type is missing");
case 2:
// Must be ES TYPE
parse_name(tokens[1]);
break;
case 3:
// Any of the following:
// ES TYPE +N
// ES SPECIAL TYPE
// ES NAME TYPE
if (parse_grind(tokens[2])) {
parse_type(tokens[1]);
} else if (parse_special(tokens[1])) {
parse_type(tokens[2]);
} else {
parse_name(tokens[1]);
parse_type(tokens[2]);
}
break;
case 4:
// Any of the following:
// ES SPECIAL TYPE +N
// ES NAME TYPE +N
// ES NAME SPECIAL TYPE
if (parse_grind(tokens[3])) {
if (!parse_special(tokens[1])) {
parse_name(tokens[1]);
}
parse_type(tokens[2]);
} else {
parse_name(tokens[1]);
if (!parse_special(tokens[2])) {
throw std::runtime_error("invalid ES special");
}
parse_type(tokens[3]);
}
break;
case 5:
// Must be ES NAME SPECIAL TYPE +N
parse_name(tokens[1]);
if (!parse_special(tokens[2])) {
throw std::runtime_error("invalid ES special");
}
parse_type(tokens[3]);
if (!parse_grind(tokens[4])) {
throw std::runtime_error("invalid grind");
}
break;
default:
throw std::runtime_error("too many ES weapon tokens");
}
return ret;
}
if (desc.starts_with("disk:")) {
auto tokens = phosg::split(desc, ' ');
tokens[0] = tokens[0].substr(5); // Trim off "disk:"
@@ -454,7 +570,6 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
desc = desc.substr(z);
}
// TODO: It'd be nice to be able to parse S-rank weapon specials here too.
uint8_t weapon_special = 0;
if (!skip_special) {
for (size_t z = 0; z < name_for_weapon_special.size(); z++) {
@@ -507,6 +622,7 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
// kill count if unsealable
ret.data1[4] = weapon_special | (is_wrapped ? 0x40 : 0x00) | (is_unidentified ? 0x80 : 0x00);
bool kill_count_set = false;
auto tokens = phosg::split(desc, ' ');
for (auto& token : tokens) {
if (token.empty()) {
@@ -517,26 +633,11 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
ret.data1[3] = stoul(token, nullptr, 10);
} else if (ret.is_s_rank_weapon()) {
if (token.size() > 8) {
throw runtime_error("s-rank name too long");
}
uint8_t char_indexes[8] = {0, 0, 0, 0, 0, 0, 0, 0};
for (size_t z = 0; z < token.size(); z++) {
char ch = toupper(token[z]);
const char* pos = strchr(s_rank_name_characters, ch);
if (!pos) {
throw runtime_error(std::format("s-rank name contains invalid character {:02X} ({})", ch, ch));
}
char_indexes[z] = (pos - s_rank_name_characters);
}
ret.data1w[3] = phosg::bswap16(0x8000 | (char_indexes[1] & 0x1F) | ((char_indexes[0] & 0x1F) << 5));
ret.data1w[4] = phosg::bswap16(0x8000 | (char_indexes[4] & 0x1F) | ((char_indexes[3] & 0x1F) << 5) | ((char_indexes[2] & 0x1F) << 10));
ret.data1w[5] = phosg::bswap16(0x8000 | (char_indexes[7] & 0x1F) | ((char_indexes[6] & 0x1F) << 5) | ((char_indexes[5] & 0x1F) << 10));
throw std::runtime_error("ES weapon must be prefixed with \"ES\"");
} else if (token.starts_with("k:")) {
ret.set_kill_count(stoul(token.substr(2), nullptr, 0));
kill_count_set = true;
} else {
auto p_tokens = phosg::split(token, '/');
@@ -560,7 +661,7 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
}
}
if (this->item_parameter_table->is_unsealable_item(ret)) {
if (this->item_parameter_table->is_unsealable_item(ret) && !kill_count_set) {
ret.set_kill_count(0);
}
@@ -575,6 +676,18 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
});
ret.data1w[3] = modifiers.at(desc);
bool kill_count_set = false;
for (auto& token : phosg::split(desc, ' ')) {
if (token.starts_with("k:")) {
ret.set_kill_count(stoul(token.substr(2), nullptr, 0));
kill_count_set = true;
break;
}
}
if (this->item_parameter_table->is_unsealable_item(ret) && !kill_count_set) {
ret.set_kill_count(0);
}
} else { // Armor/shield
for (const auto& token : phosg::split(desc, ' ')) {
if (token.empty()) {
+1
View File
@@ -56,4 +56,5 @@ private:
std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>> primary_identifier_index;
std::map<std::string, std::shared_ptr<const ItemMetadata>> name_index;
std::map<std::string, std::shared_ptr<const ItemMetadata>> es_name_index;
};