allow creating items by name instead of by data

This commit is contained in:
Martin Michelsen
2023-06-18 22:45:10 -07:00
parent f333a88aaf
commit 10ab688207
8 changed files with 473 additions and 86 deletions
+305 -32
View File
@@ -4,6 +4,8 @@
using namespace std;
static string S_RANK_NAME_CHARACTERS("\0ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_", 0x20);
ItemData::ItemData() {
this->clear();
}
@@ -50,17 +52,54 @@ uint32_t ItemData::primary_identifier() const {
// The game treats any item starting with 04 as Meseta, and ignores the rest
// of data1 (the value is in data2)
if (this->data1[0] == 0x04) {
return 0x00040000;
return 0x040000;
}
if (this->data1[0] == 0x03 && this->data1[1] == 0x02) {
return 0x00030200; // Tech disk (data1[2] is level, so omit it)
return 0x030200; // Tech disk (data1[2] is level, so omit it)
} else if (this->data1[0] == 0x02) {
return 0x00020000 | (this->data1[1] << 8); // Mag
return 0x020000 | (this->data1[1] << 8); // Mag
} else {
return (this->data1[0] << 16) | (this->data1[1] << 8) | this->data1[2];
}
}
bool ItemData::is_wrapped() const {
switch (this->data1[0]) {
case 0:
case 1:
return this->data1[4] & 0x40;
case 2:
return this->data2[2] & 0x40;
case 3:
return !this->is_stackable() && (this->data1[3] & 0x40);
case 4:
return false;
default:
throw runtime_error("invalid item data");
}
}
void ItemData::unwrap() {
switch (this->data1[0]) {
case 0:
case 1:
this->data1[4] &= 0xBF;
break;
case 2:
this->data2[2] &= 0xBF;
break;
case 3:
if (!this->is_stackable()) {
this->data1[3] &= 0xBF;
}
break;
case 4:
break;
default:
throw runtime_error("invalid item data");
}
}
bool ItemData::is_stackable() const {
return this->max_stack_size() > 1;
}
@@ -1364,6 +1403,236 @@ const unordered_map<uint32_t, ItemNameInfo> name_info_for_primary_identifier({
{0x031903, "Team Points 10000"},
});
ItemData::ItemData(const string& orig_description) {
this->data1d.clear(0);
this->id = 0xFFFFFFFF;
this->data2d = 0;
string desc = tolower(orig_description);
if (ends_with(desc, " meseta")) {
this->data1[0] = 0x04;
this->data2d = stol(desc, nullptr, 10);
return;
}
if (starts_with(desc, "disk:")) {
auto tokens = split(desc, ' ');
if (tokens.size() != 2) {
throw runtime_error("invalid tech disk name");
}
if (!starts_with(tokens[0], "disk:") || !starts_with(tokens[1], "lv.")) {
throw runtime_error("invalid tech disk format");
}
uint8_t tech = technique_for_name(tokens[0].substr(5));
uint8_t level = stoul(tokens[1].substr(3), nullptr, 10);
this->data1[0] = 0x03;
this->data1[1] = 0x02;
this->data1[2] = level;
this->data1[4] = tech;
return;
}
bool is_wrapped = starts_with(desc, "wrapped ");
if (is_wrapped) {
desc = desc.substr(8);
}
uint8_t weapon_special = 0;
for (const auto& it : name_for_weapon_special) {
if (!it.second) {
continue;
}
string prefix = tolower(it.second);
prefix += ' ';
if (starts_with(desc, prefix)) {
weapon_special = it.first;
desc = desc.substr(prefix.size());
break;
}
}
static map<string, uint32_t> primary_identifier_for_name;
if (primary_identifier_for_name.empty()) {
for (const auto& it : name_info_for_primary_identifier) {
primary_identifier_for_name.emplace(tolower(it.second.name), it.first);
}
}
auto name_it = primary_identifier_for_name.lower_bound(desc);
// Look up to 3 places before the lower bound. We have to do this to catch
// cases like Sange vs. Sange & Yasha - if the input is like "Sange 0/...",
// then we'll see Sange & Yasha first, which we should skip.
size_t lookback = 0;
while (lookback < 4) {
if (name_it != primary_identifier_for_name.end() &&
desc.starts_with(name_it->first)) {
break;
} else if (name_it == primary_identifier_for_name.begin()) {
throw runtime_error("no such item");
} else {
name_it--;
lookback++;
}
}
if (lookback >= 4) {
throw runtime_error("item not found");
}
desc = desc.substr(name_it->first.size());
if (starts_with(desc, " ")) {
desc = desc.substr(1);
}
uint32_t primary_identifier = name_it->second;
this->data1[0] = (primary_identifier >> 16) & 0xFF;
this->data1[1] = (primary_identifier >> 8) & 0xFF;
this->data1[2] = primary_identifier & 0xFF;
if (this->data1[0] == 0x00) {
// Weapons: add special, grind and percentages (or name, if S-rank)
this->data1[4] = weapon_special | (is_wrapped ? 0x40 : 0x00);
auto tokens = split(desc, ' ');
for (auto& token : tokens) {
if (token.empty()) {
continue;
}
if (starts_with(token, "+")) {
token = token.substr(1);
this->data1[3] = stoul(token, nullptr, 10);
} else if (this->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]);
size_t pos = S_RANK_NAME_CHARACTERS.find(ch);
if (pos == string::npos) {
throw runtime_error(string_printf("s-rank name contains invalid character %02hhX (%c)", ch, ch));
}
char_indexes[z] = pos;
}
this->data1w[3] = (char_indexes[1] & 0x1F) | ((char_indexes[0] & 0x1F) << 5);
this->data1w[4] = (char_indexes[4] & 0x1F) | ((char_indexes[3] & 0x1F) << 5) | ((char_indexes[2] & 0x1F) << 10);
this->data1w[5] = (char_indexes[7] & 0x1F) | ((char_indexes[6] & 0x1F) << 5) | ((char_indexes[5] & 0x1F) << 10);
} else {
auto p_tokens = split(token, '/');
if (p_tokens.size() > 5) {
throw runtime_error("invalid bonuses token");
}
uint8_t bonus_index = 0;
for (size_t z = 0; z < p_tokens.size(); z++) {
int8_t bonus_value = stol(p_tokens[z], nullptr, 10);
if (bonus_value == 0) {
continue;
}
if (bonus_index >= 3) {
throw runtime_error("weapon has too many bonuses");
}
this->data1[6 + (2 * bonus_index)] = z + 1;
this->data1[7 + (2 * bonus_index)] = static_cast<uint8_t>(bonus_value);
bonus_index++;
}
}
}
} else if (this->data1[0] == 0x01) {
if (this->data1[1] == 0x03) { // Unit
static const unordered_map<string, uint16_t> modifiers({
{"--", 0xFFFC},
{"-", 0xFFFE},
{"", 0x0000},
{"+", 0x0002},
{"++", 0x0004},
});
uint16_t modifier = modifiers.at(desc);
this->data1[7] = modifier & 0xFF;
this->data1[8] = (modifier >> 8) & 0xFF;
} else { // Armor/shield
for (const auto& token : split(desc, ' ')) {
if (token.empty()) {
continue;
} else if (!starts_with(token, "+")) {
throw runtime_error("invalid armor/shield modifier");
}
if (ends_with(token, "def")) {
this->data1w[3] = static_cast<uint16_t>(stol(token.substr(1, token.size() - 4), nullptr, 10));
} else if (ends_with(token, "evp")) {
this->data1w[4] = static_cast<uint16_t>(stol(token.substr(1, token.size() - 4), nullptr, 10));
} else {
this->data1[5] = stoul(token.substr(1), nullptr, 10);
}
}
}
if (is_wrapped) {
this->data1[4] |= 0x40;
}
} else if (this->data1[0] == 0x02) {
for (const auto& token : split(desc, ' ')) {
if (starts_with(token, "pb:")) { // Photon blasts
auto pb_tokens = split(token.substr(3), ',');
if (pb_tokens.size() > 3) {
throw runtime_error("too many photon blasts specified");
}
static const unordered_map<string, uint8_t> name_to_pb_num({
{"f", 0},
{"e", 1},
{"g", 2},
{"p", 3},
{"l", 4},
{"m&y", 5},
});
for (const auto& pb_token : pb_tokens) {
this->add_mag_photon_blast(name_to_pb_num.at(pb_token));
}
} else if (ends_with(token, "%")) { // Synchro
this->data2[0] = stoul(token.substr(0, token.size() - 1), nullptr, 10);
} else if (ends_with(token, "iq")) { // IQ
this->data2[1] = stoul(token.substr(0, token.size() - 2), nullptr, 10);
} else if (!token.empty() && isdigit(token[0])) { // Stats
auto s_tokens = split(token, '/');
if (s_tokens.size() != 4) {
throw runtime_error("incorrect stat count");
}
this->data1w[2] = stoul(s_tokens[0], nullptr, 10);
this->data1w[3] = stoul(s_tokens[1], nullptr, 10);
this->data1w[4] = stoul(s_tokens[2], nullptr, 10);
this->data1w[5] = stoul(s_tokens[3], nullptr, 10);
} else { // Color
this->data2[3] = mag_color_for_name.at(token);
}
}
if (is_wrapped) {
this->data2[2] |= 0x40;
}
} else if (this->data1[0] == 0x03) {
if (this->max_stack_size() > 1) {
if (starts_with(desc, "x")) {
this->data1[5] = stoul(desc.substr(1), nullptr, 10);
} else {
this->data1[5] = 1;
}
} else if (!desc.empty()) {
throw runtime_error("item cannot be stacked");
}
if (is_wrapped) {
this->data1[3] |= 0x40;
}
} else {
throw logic_error("invalid item class");
}
}
string ItemData::hex() const {
return string_printf("%02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX (%08" PRIX32 ") %02hhX%02hhX%02hhX%02hhX",
this->data1[0], this->data1[1], this->data1[2], this->data1[3],
@@ -1402,6 +1671,10 @@ string ItemData::name(bool include_color_codes) const {
if ((this->data1[0] == 0x02) && (this->data2[2] & 0x40)) {
ret_tokens.emplace_back("Wrapped");
}
// And tools can be wrapped if they aren't stackable
if ((this->data1[0] == 0x03) && !this->is_stackable() && (this->data1[3] & 0x40)) {
ret_tokens.emplace_back("Wrapped");
}
// Add the item name. Technique disks are special because the level is part of
// the primary identifier, so we manually generate the name instead of looking
@@ -1434,7 +1707,7 @@ string ItemData::name(bool include_color_codes) const {
ret_tokens.emplace_back(string_printf("+%hhu", this->data1[3]));
}
if (this->is_s_rank_weapon() && (this->data1[6] & 0x18)) {
if (this->is_s_rank_weapon()) {
// S-rank (has name instead of percent bonuses)
uint8_t char_indexes[8] = {
static_cast<uint8_t>((this->data1w[3] >> 5) & 0x1F),
@@ -1446,11 +1719,10 @@ string ItemData::name(bool include_color_codes) const {
static_cast<uint8_t>((this->data1w[5] >> 5) & 0x1F),
static_cast<uint8_t>(this->data1w[5] & 0x1F),
};
const char* translation_table = "\0ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_";
string name;
for (size_t x = 0; x < 8; x++) {
char ch = translation_table[char_indexes[x]];
char ch = S_RANK_NAME_CHARACTERS[char_indexes[x]];
if (ch == 0) {
break;
}
@@ -1552,33 +1824,12 @@ string ItemData::name(bool include_color_codes) const {
token += pb_names[x];
}
ret_tokens.emplace_back(std::move(token));
}
static const vector<const char*> mag_colors({
/* 00 */ "red",
/* 01 */ "blue",
/* 02 */ "yellow",
/* 03 */ "green",
/* 04 */ "purple",
/* 05 */ "black",
/* 06 */ "white",
/* 07 */ "cyan",
/* 08 */ "brown",
/* 09 */ "orange",
/* 0A */ "light blue",
/* 0B */ "olive",
/* 0C */ "light cyan",
/* 0D */ "dark purple",
/* 0E */ "grey",
/* 0F */ "light grey",
/* 10 */ "pink",
/* 11 */ "dark cyan",
/* 12 */ "costume color",
});
try {
ret_tokens.emplace_back(string_printf("(%s)", mag_colors.at(this->data2[3])));
} catch (const out_of_range&) {
ret_tokens.emplace_back(string_printf("(!CL:%02hhX)", this->data2[3]));
}
try {
ret_tokens.emplace_back(string_printf("(%s)", name_for_mag_color.at(this->data2[3])));
} catch (const out_of_range&) {
ret_tokens.emplace_back(string_printf("(!CL:%02hhX)", this->data2[3]));
}
// For tools, add the amount (if applicable)
@@ -1603,3 +1854,25 @@ string ItemData::name(bool include_color_codes) const {
return ret;
}
}
ItemData item_for_string(const string& desc) {
try {
return ItemData(desc);
} catch (const exception&) {
string data = parse_data_string(desc);
if (data.size() < 2) {
throw runtime_error("item code too short");
}
if (data.size() > 16) {
throw runtime_error("item code too long");
}
ItemData ret;
if (data.size() <= 12) {
memcpy(ret.data1.data(), data.data(), data.size());
} else {
memcpy(ret.data1.data(), data.data(), 12);
memcpy(ret.data2.data(), data.data() + 12, data.size() - 12);
}
return ret;
}
}