use EnemyType in ItemCreator; fix incorrect drop tables

This commit is contained in:
Martin Michelsen
2026-03-08 20:37:57 -07:00
parent 3cbf64dda2
commit 4e3549ba6b
33 changed files with 735 additions and 607 deletions
+150 -130
View File
@@ -134,67 +134,57 @@ CommonItemSet::Table::Table(std::shared_ptr<const Table> prev_table, const phosg
parse_field("UnitMaxStarsTable", this->unit_max_stars_table, prev_table ? &prev_table->unit_max_stars_table : nullptr);
parse_field("BoxItemClassProbTable", this->box_item_class_prob_table, prev_table ? &prev_table->box_item_class_prob_table : nullptr);
const auto* enemy_meseta_ranges_json = json.count("EnemyMesetaRanges") ? &json.at("EnemyMesetaRanges").as_dict() : nullptr;
const auto* enemy_type_drop_probs_json = json.count("EnemyTypeDropProbs") ? &json.at("EnemyTypeDropProbs").as_dict() : nullptr;
const auto* enemy_item_classes_json = json.count("EnemyItemClasses") ? &json.at("EnemyItemClasses").as_dict() : nullptr;
if (enemy_item_classes_json) {
// Unspecified is 0xFF, not 0, unlike the other enemy-indexed arrays (except for [0], apparently... sigh)
this->enemy_item_classes[0] = 0;
this->enemy_item_classes.clear_after(1, 0xFF);
if (json.count("EnemyMesetaRanges")) {
const auto& dict = json.at("EnemyMesetaRanges").as_dict();
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
try {
from_json_into(*dict.at(phosg::name_for_enum(enemy_type)), this->enemy_type_meseta_ranges[enemy_type]);
} catch (const out_of_range&) {
}
}
} else {
this->enemy_type_meseta_ranges = prev_table->enemy_type_meseta_ranges;
}
for (size_t z = 0; z < NUM_RT_INDEXES_V4; z++) {
auto types = enemy_types_for_rare_table_index(this->episode, z);
vector<string> names;
if (types.empty()) {
names.emplace_back(std::format("!{:02X}", z));
} else {
for (auto type : types) {
names.emplace_back(phosg::name_for_enum(type));
if (json.count("EnemyTypeDropProbs")) {
const auto& dict = json.at("EnemyTypeDropProbs").as_dict();
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
try {
this->enemy_type_drop_probs[enemy_type] = dict.at(phosg::name_for_enum(enemy_type))->as_int();
} catch (const out_of_range&) {
}
}
for (const auto& name : names) {
if (enemy_meseta_ranges_json) {
try {
from_json_into(*enemy_meseta_ranges_json->at(name), this->enemy_meseta_ranges[z]);
} catch (const out_of_range&) {
}
} else if (prev_table) {
this->enemy_meseta_ranges = prev_table->enemy_meseta_ranges;
}
if (enemy_type_drop_probs_json) {
try {
this->enemy_type_drop_probs[z] = enemy_type_drop_probs_json->at(name)->as_int();
} catch (const out_of_range&) {
}
} else if (prev_table) {
this->enemy_type_drop_probs = prev_table->enemy_type_drop_probs;
}
if (enemy_item_classes_json) {
try {
this->enemy_item_classes[z] = enemy_item_classes_json->at(name)->as_int();
} catch (const out_of_range&) {
}
} else if (prev_table) {
this->enemy_item_classes = prev_table->enemy_item_classes;
} else {
this->enemy_type_drop_probs = prev_table->enemy_type_drop_probs;
}
if (json.count("EnemyItemClasses")) {
const auto& dict = json.at("EnemyItemClasses").as_dict();
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
try {
this->enemy_type_item_classes[enemy_type] = dict.at(phosg::name_for_enum(enemy_type))->as_int();
} catch (const out_of_range&) {
}
}
} else {
this->enemy_type_item_classes = prev_table->enemy_type_item_classes;
}
}
static const char* name_for_common_item_class(uint8_t item_class) {
switch (item_class) {
case 0x00:
return "WEAPON ";
return "WEAPON";
case 0x01:
return "ARMOR ";
return "ARMOR";
case 0x02:
return "SHIELD ";
return "SHIELD";
case 0x03:
return "UNIT ";
return "UNIT";
case 0x04:
return "TOOL ";
return "TOOL";
case 0x05:
return "MESETA ";
return "MESETA";
case 0x06:
return "NOTHING";
default:
@@ -203,42 +193,29 @@ static const char* name_for_common_item_class(uint8_t item_class) {
}
void CommonItemSet::Table::print(FILE* stream) const {
const auto& meseta_ranges = this->enemy_meseta_ranges;
const auto& meseta_ranges = this->enemy_type_meseta_ranges;
const auto& drop_probs = this->enemy_type_drop_probs;
const auto& item_classes = this->enemy_item_classes;
const auto& item_classes = this->enemy_type_item_classes;
phosg::fwrite_fmt(stream, "Enemy tables:\n");
phosg::fwrite_fmt(stream, " ## $LOW $HIGH DAR% ITEM ENEMIES\n");
for (size_t z = 0; z < NUM_RT_INDEXES_V4; z++) {
string enemies_str;
for (EnemyType enemy_type : enemy_types_for_rare_table_index(this->episode, z)) {
if (!enemies_str.empty()) {
enemies_str += ", ";
}
enemies_str += phosg::name_for_enum(enemy_type);
}
if (drop_probs[z] || !enemies_str.empty()) {
phosg::fwrite_fmt(stream, " {:02X} {:5} {:5} {:3}% {:02X}:{} {}\n",
z, meseta_ranges[z].min, meseta_ranges[z].max, drop_probs[z], item_classes[z],
name_for_common_item_class(item_classes[z]), enemies_str);
} else {
phosg::fwrite_fmt(stream, " {:02X} ----- ----- 0% --\n", z);
phosg::fwrite_fmt(stream, " ##:ENEMY $LOW $HIGH DAR% ITEM\n");
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
const auto& def = type_definition_for_enemy(enemy_type);
try {
const auto& meseta_range = meseta_ranges.at(enemy_type);
const auto& drop_prob = drop_probs.at(enemy_type);
const auto& item_class = item_classes.at(enemy_type);
phosg::fwrite_fmt(stream, " {:02X}:{:<23} {:5} {:5} {:3}% {:02X}:{:<7}\n",
def.rt_index, phosg::name_for_enum(enemy_type),
meseta_range.min, meseta_range.max, drop_prob, item_class,
name_for_common_item_class(item_class));
} catch (const out_of_range&) {
phosg::fwrite_fmt(stream, " {:02X}:{:<23} ----- ----- ---- --:-------\n",
def.rt_index, phosg::name_for_enum(enemy_type));
}
}
static const array<const char*, 12> base_weapon_type_names = {
"SABER ",
"SWORD ",
"DAGGER ",
"PARTISAN",
"SLICER ",
"HANDGUN ",
"RIFLE ",
"MECHGUN ",
"SHOT ",
"CANE ",
"ROD ",
"WAND ",
};
"SABER", "SWORD", "DAGGER", "PARTISAN", "SLICER", "HANDGUN", "RIFLE", "MECHGUN", "SHOT", "CANE", "ROD", "WAND"};
phosg::fwrite_fmt(stream, "Base weapon config:\n");
phosg::fwrite_fmt(stream, " TYPE PROB [SB AL] FLOORS\n");
for (size_t z = 0; z < 12; z++) {
@@ -256,7 +233,7 @@ void CommonItemSet::Table::print(FILE* stream) const {
floor_to_class[x] = this->subtype_base_table[z] + (x / this->subtype_area_length_table[z]);
}
}
phosg::fwrite_fmt(stream, " {:02X}:{} {:3}% [{:02X} {:02X}] {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}\n",
phosg::fwrite_fmt(stream, " {:02X}:{:<8} {:3}% [{:02X} {:02X}] {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}\n",
z, base_weapon_type_names[z], this->base_weapon_type_prob_table[z],
this->subtype_base_table[z], this->subtype_area_length_table[z],
floor_to_class[0], floor_to_class[1], floor_to_class[2], floor_to_class[3], floor_to_class[4],
@@ -413,20 +390,50 @@ void CommonItemSet::Table::print_diff(FILE* stream, const Table& other) const {
phosg::format_data_string(&this->armor_slot_count_prob_table, sizeof(this->armor_slot_count_prob_table)),
phosg::format_data_string(&other.armor_slot_count_prob_table, sizeof(other.armor_slot_count_prob_table)));
}
if (this->enemy_meseta_ranges != other.enemy_meseta_ranges) {
phosg::fwrite_fmt(stream, "> enemy_meseta_ranges: {} -> {}\n",
phosg::format_data_string(&this->enemy_meseta_ranges, sizeof(this->enemy_meseta_ranges)),
phosg::format_data_string(&other.enemy_meseta_ranges, sizeof(other.enemy_meseta_ranges)));
auto format_enemy_range_table = [&](const std::unordered_map<EnemyType, Range<uint16_t>>& table) -> std::string {
string ret = "";
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
try {
const auto& range = table.at(enemy_type);
if (!ret.empty()) {
ret += ",";
}
ret += std::format("{}=[{},{}]", phosg::name_for_enum(enemy_type), range.min, range.max);
} catch (const out_of_range&) {
}
}
return ret;
};
auto format_enemy_u8_table = [&](const std::unordered_map<EnemyType, uint8_t>& table) -> std::string {
string ret = "";
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
try {
uint8_t value = table.at(enemy_type);
if (!ret.empty()) {
ret += ",";
}
ret += std::format("{}={}", phosg::name_for_enum(enemy_type), value);
} catch (const out_of_range&) {
}
}
return ret;
};
if (this->enemy_type_meseta_ranges != other.enemy_type_meseta_ranges) {
phosg::fwrite_fmt(stream, "> enemy_type_meseta_ranges: {} -> {}\n",
format_enemy_range_table(this->enemy_type_meseta_ranges),
format_enemy_range_table(other.enemy_type_meseta_ranges));
}
if (this->enemy_type_drop_probs != other.enemy_type_drop_probs) {
phosg::fwrite_fmt(stream, "> enemy_type_drop_probs: {} -> {}\n",
phosg::format_data_string(&this->enemy_type_drop_probs, sizeof(this->enemy_type_drop_probs)),
phosg::format_data_string(&other.enemy_type_drop_probs, sizeof(other.enemy_type_drop_probs)));
format_enemy_u8_table(this->enemy_type_drop_probs),
format_enemy_u8_table(other.enemy_type_drop_probs));
}
if (this->enemy_item_classes != other.enemy_item_classes) {
phosg::fwrite_fmt(stream, "> enemy_item_classes: {} -> {}\n",
phosg::format_data_string(&this->enemy_item_classes, sizeof(this->enemy_item_classes)),
phosg::format_data_string(&other.enemy_item_classes, sizeof(other.enemy_item_classes)));
if (this->enemy_type_item_classes != other.enemy_type_item_classes) {
phosg::fwrite_fmt(stream, "> enemy_type_item_classes: {} -> {}\n",
format_enemy_u8_table(this->enemy_type_item_classes),
format_enemy_u8_table(other.enemy_type_item_classes));
}
if (this->box_meseta_ranges != other.box_meseta_ranges) {
phosg::fwrite_fmt(stream, "> box_meseta_ranges: {} -> {}\n",
@@ -517,44 +524,45 @@ phosg::JSON CommonItemSet::Table::json(std::shared_ptr<const Table> prev_table)
ret.emplace("ArmorSlotCountProbTable", to_json(this->armor_slot_count_prob_table));
}
bool needs_enemy_meseta_ranges = (!prev_table || (this->enemy_meseta_ranges != prev_table->enemy_meseta_ranges));
bool needs_enemy_type_drop_probs = (!prev_table || (this->enemy_type_drop_probs != prev_table->enemy_type_drop_probs));
bool needs_enemy_item_classes = (!prev_table || (this->enemy_item_classes != prev_table->enemy_item_classes));
if (needs_enemy_meseta_ranges || needs_enemy_type_drop_probs || needs_enemy_item_classes) {
phosg::JSON enemy_meseta_ranges_json = phosg::JSON::dict();
bool needs_enemy_type_meseta_ranges = (!prev_table ||
(this->enemy_type_meseta_ranges != prev_table->enemy_type_meseta_ranges));
bool needs_enemy_type_drop_probs = (!prev_table ||
(this->enemy_type_drop_probs != prev_table->enemy_type_drop_probs));
bool needs_enemy_type_item_classes = (!prev_table ||
(this->enemy_type_item_classes != prev_table->enemy_type_item_classes));
if (needs_enemy_type_meseta_ranges || needs_enemy_type_drop_probs || needs_enemy_type_item_classes) {
phosg::JSON enemy_type_meseta_ranges_json = phosg::JSON::dict();
phosg::JSON enemy_type_drop_probs_json = phosg::JSON::dict();
phosg::JSON enemy_item_classes_json = phosg::JSON::dict();
for (size_t z = 0; z < NUM_RT_INDEXES_V4; z++) {
auto types = enemy_types_for_rare_table_index(this->episode, z);
vector<string> names;
if (types.empty()) {
names.emplace_back(std::format("!{:02X}", z));
} else {
for (auto type : types) {
names.emplace_back(phosg::name_for_enum(type));
phosg::JSON enemy_type_item_classes_json = phosg::JSON::dict();
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
auto name = phosg::name_for_enum(enemy_type);
if (needs_enemy_type_meseta_ranges) {
try {
enemy_type_meseta_ranges_json.emplace(name, to_json(this->enemy_type_meseta_ranges.at(enemy_type)));
} catch (const std::out_of_range&) {
}
}
for (const auto& name : names) {
if (needs_enemy_meseta_ranges && (!types.empty() || !this->enemy_meseta_ranges[z].empty())) {
enemy_meseta_ranges_json.emplace(name, to_json(this->enemy_meseta_ranges[z]));
if (needs_enemy_type_drop_probs) {
try {
enemy_type_drop_probs_json.emplace(name, this->enemy_type_drop_probs.at(enemy_type));
} catch (const std::out_of_range&) {
}
if (needs_enemy_type_drop_probs && (!types.empty() || this->enemy_type_drop_probs[z])) {
enemy_type_drop_probs_json.emplace(name, this->enemy_type_drop_probs[z]);
}
if (needs_enemy_item_classes && (!types.empty() || (this->enemy_item_classes[z] != ((z == 0) ? 0x00 : 0xFF)))) {
enemy_item_classes_json.emplace(name, this->enemy_item_classes[z]);
}
if (needs_enemy_type_item_classes) {
try {
enemy_type_item_classes_json.emplace(name, this->enemy_type_item_classes.at(enemy_type));
} catch (const std::out_of_range&) {
}
}
}
if (needs_enemy_meseta_ranges) {
ret.emplace("EnemyMesetaRanges", std::move(enemy_meseta_ranges_json));
if (needs_enemy_type_meseta_ranges) {
ret.emplace("EnemyMesetaRanges", std::move(enemy_type_meseta_ranges_json));
}
if (needs_enemy_type_drop_probs) {
ret.emplace("EnemyTypeDropProbs", std::move(enemy_type_drop_probs_json));
}
if (needs_enemy_item_classes) {
ret.emplace("EnemyItemClasses", std::move(enemy_item_classes_json));
if (needs_enemy_type_item_classes) {
ret.emplace("EnemyItemClasses", std::move(enemy_type_item_classes_json));
}
}
@@ -705,13 +713,27 @@ void CommonItemSet::Table::parse_itempt_t(const phosg::StringReader& r, bool is_
this->grind_prob_table = r.pget<parray<parray<uint8_t, 4>, 9>>(offsets.grind_prob_table_offset);
this->armor_shield_type_index_prob_table = r.pget<parray<uint8_t, 0x05>>(offsets.armor_shield_type_index_prob_table_offset);
this->armor_slot_count_prob_table = r.pget<parray<uint8_t, 0x05>>(offsets.armor_slot_count_prob_table_offset);
const auto& data = r.pget<parray<Range<U16T<BE>>, NUM_RT_INDEXES_V3>>(offsets.enemy_meseta_ranges_offset);
for (size_t z = 0; z < data.size(); z++) {
this->enemy_meseta_ranges[z] = Range<uint16_t>{data[z].min, data[z].max};
const auto& enemy_rt_index_meseta_ranges = r.pget<parray<Range<U16T<BE>>, NUM_RT_INDEXES_V3>>(
offsets.enemy_rt_index_meseta_ranges_offset);
const auto& enemy_rt_index_drop_probs = r.pget<parray<uint8_t, NUM_RT_INDEXES_V3>>(
offsets.enemy_rt_index_drop_probs_offset);
const auto& enemy_rt_index_item_classes = r.pget<parray<uint8_t, NUM_RT_INDEXES_V3>>(
offsets.enemy_rt_index_item_classes_offset);
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
const auto& def = type_definition_for_enemy(enemy_type);
if (def.valid_in_episode(this->episode) && (def.rt_index < enemy_rt_index_meseta_ranges.size())) {
const auto& meseta_range = enemy_rt_index_meseta_ranges[def.rt_index];
if (meseta_range.max > 0) {
this->enemy_type_meseta_ranges.emplace(enemy_type, Range<uint16_t>{meseta_range.min, meseta_range.max});
}
if (enemy_rt_index_drop_probs[def.rt_index] > 0) {
this->enemy_type_drop_probs.emplace(enemy_type, enemy_rt_index_drop_probs[def.rt_index]);
}
if (enemy_rt_index_item_classes[def.rt_index] != 0xFF) {
this->enemy_type_item_classes.emplace(enemy_type, enemy_rt_index_item_classes[def.rt_index]);
}
}
}
this->enemy_type_drop_probs = r.pget<parray<uint8_t, NUM_RT_INDEXES_V3>>(offsets.enemy_type_drop_probs_offset);
this->enemy_item_classes = r.pget<parray<uint8_t, NUM_RT_INDEXES_V3>>(offsets.enemy_item_classes_offset);
this->enemy_item_classes.clear_after(NUM_RT_INDEXES_V3, 0xFF);
{
const auto& data = r.pget<parray<Range<U16T<BE>>, 0x0A>>(offsets.box_meseta_ranges_offset);
for (size_t z = 0; z < data.size(); z++) {
@@ -873,7 +895,7 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
section_id);
};
for (Episode episode : ALL_EPISODES_V4) {
for (Episode episode : ALL_EPISODES_V3) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (size_t section_id = 0; section_id < 10; section_id++) {
phosg::StringReader r;
@@ -898,17 +920,15 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
}
}
if (episode != Episode::EP4) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
try {
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
auto table = make_shared<Table>(r, is_big_endian, true, episode);
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
}
} catch (const out_of_range&) {
// GC NTE doesn't have Ep2 challenge; just skip adding the table
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
try {
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
auto table = make_shared<Table>(r, is_big_endian, true, episode);
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
}
} catch (const out_of_range&) {
// GC NTE doesn't have Ep2 challenge; just skip adding the table
}
}
}