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
}
}
}
+11 -10
View File
@@ -42,9 +42,10 @@ public:
parray<parray<uint8_t, 4>, 9> grind_prob_table;
parray<uint8_t, 0x05> armor_shield_type_index_prob_table;
parray<uint8_t, 0x05> armor_slot_count_prob_table;
parray<Range<uint16_t>, NUM_RT_INDEXES_V4> enemy_meseta_ranges;
parray<uint8_t, NUM_RT_INDEXES_V4> enemy_type_drop_probs;
parray<uint8_t, NUM_RT_INDEXES_V4> enemy_item_classes;
// Note: PSO originally uses arrays indexed by rt_index here, but we index enemies by the EnemyType enum instead
std::unordered_map<EnemyType, Range<uint16_t>> enemy_type_meseta_ranges;
std::unordered_map<EnemyType, uint8_t> enemy_type_drop_probs;
std::unordered_map<EnemyType, uint8_t> enemy_type_item_classes;
parray<Range<uint16_t>, 0x0A> box_meseta_ranges;
bool has_rare_bonus_value_prob_table;
parray<parray<uint16_t, 6>, 0x17> bonus_value_prob_table;
@@ -126,17 +127,17 @@ public:
// V2/V3: -> parray<uint8_t, 0x05>
/* 14 */ U32T<BE> armor_slot_count_prob_table_offset;
// This array (indexed by enemy_type) specifies the range of meseta values that each enemy can drop.
// This array (indexed by rt_index) specifies the range of meseta values that each enemy can drop.
// V2/V3: -> parray<Range<U16T>, NUM_RT_INDEXES_V3>
/* 18 */ U32T<BE> enemy_meseta_ranges_offset;
/* 18 */ U32T<BE> enemy_rt_index_meseta_ranges_offset;
// Each byte in this table (indexed by enemy_type) represents the percent chance that the enemy drops anything at
// Each byte in this table (indexed by rt_index) represents the percent chance that the enemy drops anything at
// all. (This check is done before the rare drop check, so the chance of getting a rare item from an enemy is
// essentially this probability multiplied by the rare drop rate.)
// V2/V3: -> parray<uint8_t, NUM_RT_INDEXES_V3>
/* 1C */ U32T<BE> enemy_type_drop_probs_offset;
/* 1C */ U32T<BE> enemy_rt_index_drop_probs_offset;
// Each byte in this table (indexed by enemy_type) represents the class of item that can drop. The values are:
// Each byte in this table (indexed by rt_index) represents the class of item that can drop. The values are:
// 00 = weapon
// 01 = armor
// 02 = shield
@@ -145,7 +146,7 @@ public:
// 05 = meseta
// Anything else = no item
// V2/V3: -> parray<uint8_t, NUM_RT_INDEXES_V3>
/* 20 */ U32T<BE> enemy_item_classes_offset;
/* 20 */ U32T<BE> enemy_rt_index_item_classes_offset;
// This table (indexed by area - 1) specifies the ranges of meseta values that can drop from boxes.
// V2/V3: -> parray<Range<U16T>, 0x0A>
@@ -229,7 +230,7 @@ public:
// This index probability table determines which type of items drop from boxes. The table is indexed as
// [item_class][area - 1], with item_class as the result value (that is, in the example below, the game looks at
// a single column and sums the values going down, then the chosen item class is one of the row indexes based on
// the weight values in the column.) The resulting value has the same meaning as in enemy_item_classes above.
// the weight values in the column.) The resulting value has the same meaning as in enemy_rt_index_item_classes.
// For example, this array might look like the following:
// [07 07 08 08 06 07 08 09 09 0A] // Chances per area of a weapon drop
// [02 02 02 02 03 02 02 02 03 03] // Chances per area of an armor drop
+2 -1
View File
@@ -13,6 +13,7 @@ static constexpr size_t NUM_RT_INDEXES_V3 = 0x64;
static constexpr size_t NUM_RT_INDEXES_V4 = 0x70;
enum class EnemyType : uint8_t {
MIN_VALUE = 0,
UNKNOWN = 0,
NONE,
NON_ENEMY_NPC,
@@ -146,7 +147,7 @@ enum class EnemyType : uint8_t {
ZOL_GIBBON,
ZU_CRATER,
ZU_DESERT,
MAX_ENEMY_TYPE,
MAX_VALUE,
};
struct EnemyTypeDefinition {
+61 -47
View File
@@ -188,17 +188,23 @@ ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) {
}
}
ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, uint8_t area) {
ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type, uint8_t area) {
try {
// Note: The original GC implementation uses (enemy_type > 0x58) here; we extend it to the full array size for BB
if (enemy_type >= NUM_RT_INDEXES_V4) {
this->log.warning_f("Invalid enemy type: {:X}", enemy_type);
return DropResult();
}
this->log.info_f("Enemy type: {:X}", enemy_type);
// Note: The original implementation has a bounds check for enemy_type here, because it uses rt_index instead
// if (enemy_type >= NUM_RT_INDEXES_V4) {
// this->log.warning_f("Invalid enemy type: {:X}", enemy_type);
// return DropResult();
// }
this->log.info_f("Enemy type: {}", phosg::name_for_enum(enemy_type));
auto pt = this->pt(area);
uint8_t type_drop_prob = pt->enemy_type_drop_probs.at(enemy_type);
uint8_t type_drop_prob = 0;
try {
type_drop_prob = pt->enemy_type_drop_probs.at(enemy_type);
} catch (const std::out_of_range&) {
this->log.info_f("No drop probability is set for this enemy type");
return DropResult();
}
uint8_t drop_sample = this->rand_int(100);
if (drop_sample >= type_drop_prob) {
this->log.info_f("Drop not chosen ({} >= {})", drop_sample, type_drop_prob);
@@ -212,10 +218,8 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, u
if (!res.item.empty()) {
res.is_from_rare_table = true;
} else {
uint32_t item_class_determinant =
this->should_allow_meseta_drops() ? this->rand_int(3) : (this->rand_int(2) + 1);
uint32_t item_class;
uint8_t item_class_determinant = this->should_allow_meseta_drops() ? this->rand_int(3) : (this->rand_int(2) + 1);
uint8_t item_class;
switch (item_class_determinant) {
case 0:
item_class = 5;
@@ -224,7 +228,12 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, u
item_class = 4;
break;
case 2:
item_class = pt->enemy_item_classes.at(enemy_type);
try {
item_class = pt->enemy_type_item_classes.at(enemy_type);
} catch (const out_of_range&) {
this->log.info_f("Item class is not set for this enemy type");
item_class = 0xFF;
}
break;
default:
throw logic_error("invalid item class determinant");
@@ -251,7 +260,12 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, u
break;
case 5: // Meseta
res.item.data1[0] = 0x04;
res.item.data2d = this->choose_meseta_amount(pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
try {
res.item.data2d = this->choose_meseta_amount(pt->enemy_type_meseta_ranges.at(enemy_type)) & 0xFFFF;
} catch (const out_of_range&) {
this->log.info_f("Meseta range is not set for this enemy type");
return DropResult();
}
break;
default:
return res;
@@ -305,24 +319,18 @@ float ItemCreator::rand_float_0_1_from_crypt() {
return (static_cast<double>(this->rand_crypt->next() >> 16) / 65536.0);
}
template <size_t NumRanges>
uint32_t ItemCreator::choose_meseta_amount(
const parray<CommonItemSet::Table::Range<uint16_t>, NumRanges> ranges,
size_t table_index) {
uint16_t min = ranges[table_index].min;
uint16_t max = ranges[table_index].max;
uint32_t ItemCreator::choose_meseta_amount(const CommonItemSet::Table::Range<uint16_t>& range) {
// Note: The original code returns 0xFF here if either limit is equal to 0xFF (despite them being 16-bit integers!)
uint16_t ret;
if (min == max) {
ret = min;
} else if (max < min) {
ret = this->rand_int((min - max) + 1) + max;
if (range.min == range.max) {
ret = range.min;
} else if (range.max < range.min) {
ret = this->rand_int((range.min - range.max) + 1) + range.max;
} else {
ret = this->rand_int((max - min) + 1) + min;
ret = this->rand_int((range.max - range.min) + 1) + range.min;
}
this->log.info_f("Chose {} Meseta from range [{}, {}]", ret, min, max);
this->log.info_f("Chose {} Meseta from range [{}, {}]", ret, range.min, range.max);
return ret;
}
@@ -330,28 +338,32 @@ bool ItemCreator::should_allow_meseta_drops() const {
return (this->mode != GameMode::CHALLENGE);
}
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type, uint8_t area) {
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area) {
// Note: The original implementation has a bounds check for enemy_type here, since it uses rt_index instead.
// if ((enemy_type <= 0) || (enemy_type >= NUM_RT_INDEXES_V4)) return ItemData{};
if (!this->are_rare_drops_allowed()) {
return ItemData{};
}
// Note: In the original implementation, enemies can only have one possible rare drop. In our implementation, they
// can have multiple rare drops if JSONRareItemSet is used (the other RareItemSet implementations never return
// multiple drops for an enemy type).
Episode episode = episode_for_area(area);
auto rare_specs = this->rare_item_set->get_enemy_specs(
this->mode, episode, this->difficulty, this->section_id, enemy_type);
ItemData item;
if (this->are_rare_drops_allowed() && (enemy_type > 0) && (enemy_type < NUM_RT_INDEXES_V4)) {
// Note: In the original implementation, enemies can only have one possible rare drop. In our implementation, they
// can have multiple rare drops if JSONRareItemSet is used (the other RareItemSet implementations never return
// multiple drops for an enemy type).
Episode episode = episode_for_area(area);
auto rare_specs = this->rare_item_set->get_enemy_specs(
this->mode, episode, this->difficulty, this->section_id, enemy_type);
for (const auto& spec : rare_specs) {
item = this->check_rate_and_create_rare_item(spec, area);
if (!item.empty()) {
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
auto hex = spec.data.hex();
this->log.info_f("Enemy spec {:08X} produced item {}", spec.probability, hex);
}
break;
}
for (const auto& spec : rare_specs) {
item = this->check_rate_and_create_rare_item(spec, area);
if (!item.empty()) {
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
auto hex = spec.data.hex();
this->log.info_f("Enemy spec {:08X} did not produce item {}", spec.probability, hex);
this->log.info_f("Enemy spec {:08X} produced item {}", spec.probability, hex);
}
break;
}
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
auto hex = spec.data.hex();
this->log.info_f("Enemy spec {:08X} did not produce item {}", spec.probability, hex);
}
}
return item;
@@ -617,9 +629,11 @@ void ItemCreator::generate_common_item_variances(ItemData& item, uint8_t area) {
case 3:
this->generate_common_tool_variances(item, area);
break;
case 4:
item.data2d = this->choose_meseta_amount(this->pt(area)->box_meseta_ranges, this->table_index_for_area(area)) & 0xFFFF;
case 4: {
const auto& range = this->pt(area)->box_meseta_ranges.at(this->table_index_for_area(area));
item.data2d = this->choose_meseta_amount(range) & 0xFFFF;
break;
}
default:
// Note: The original code does the following here:
// item.clear();
+3 -4
View File
@@ -37,7 +37,7 @@ public:
bool is_from_rare_table = false;
};
DropResult on_monster_item_drop(uint32_t enemy_type, uint8_t area);
DropResult on_monster_item_drop(EnemyType enemy_type, uint8_t area);
DropResult on_box_item_drop(uint8_t area);
// Note: param3-6 refer to the corresponding fields of the object definition
DropResult on_specialized_box_item_drop(uint8_t area, float param3, uint32_t param4, uint32_t param5, uint32_t param6);
@@ -116,12 +116,11 @@ private:
uint32_t rand_int(uint64_t max);
float rand_float_0_1_from_crypt();
template <size_t NumRanges>
uint32_t choose_meseta_amount(const parray<CommonItemSet::Table::Range<uint16_t>, NumRanges> ranges, size_t table_index);
uint32_t choose_meseta_amount(const CommonItemSet::Table::Range<uint16_t>& range);
bool should_allow_meseta_drops() const;
ItemData check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type, uint8_t area);
ItemData check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area);
ItemData check_rare_specs_and_create_rare_box_item(uint8_t area);
ItemData check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area);
+50 -24
View File
@@ -1922,8 +1922,7 @@ Action a_extract_ppk("extract-ppk", "\
PC/BB format. For PPK archives, the --password= option is required.\n",
a_extract_archive_fn);
Action a_encode_sjis(
"transcode-text", nullptr, +[](phosg::Arguments& args) {
Action a_transcode_text("transcode-text", nullptr, +[](phosg::Arguments& args) {
TextTranscoder* tt_from = nullptr;
{
std::string from_name = args.get<std::string>("from");
@@ -1963,8 +1962,7 @@ Action a_encode_sjis(
if (tt_to) {
data = (*tt_to)(data);
}
write_output_data(args, data.data(), data.size(), "txt");
});
write_output_data(args, data.data(), data.size(), "txt"); });
Action a_decode_text_archive(
"decode-text-archive", "\
@@ -2204,6 +2202,25 @@ Action a_download_files(
io_context->run();
});
std::shared_ptr<RareItemSet> load_rare_item_set(
const std::string& filename, bool is_v1, std::shared_ptr<const ItemNameIndex> v4_item_name_index) {
string filename_lower = phosg::tolower(filename);
auto data = make_shared<string>(phosg::load_file(filename));
if (filename_lower.ends_with(".json")) {
return make_shared<RareItemSet>(phosg::JSON::parse(*data), v4_item_name_index);
} else if (filename_lower.ends_with(".gsl")) {
return make_shared<RareItemSet>(GSLArchive(data, false), false);
} else if (filename_lower.ends_with(".gslb")) {
return make_shared<RareItemSet>(GSLArchive(data, true), true);
} else if (filename_lower.ends_with(".afs")) {
return make_shared<RareItemSet>(AFSArchive(data), is_v1);
} else if (filename_lower.ends_with(".rel")) {
return make_shared<RareItemSet>(*data, true);
} else {
throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, .afs, or .rel");
}
}
Action a_convert_rare_item_set(
"convert-rare-item-set", "\
convert-rare-item-set INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS]\n\
@@ -2233,24 +2250,8 @@ Action a_convert_rare_item_set(
if (input_filename.empty() || (input_filename == "-")) {
throw runtime_error("input filename must be given");
}
string input_filename_lower = phosg::tolower(input_filename);
auto data = make_shared<string>(read_input_data(args));
shared_ptr<RareItemSet> rs;
if (input_filename_lower.ends_with(".json")) {
rs = make_shared<RareItemSet>(phosg::JSON::parse(*data), s->item_name_index_opt(get_cli_version(args, Version::BB_V4)));
} else if (input_filename_lower.ends_with(".gsl")) {
rs = make_shared<RareItemSet>(GSLArchive(data, false), false);
} else if (input_filename_lower.ends_with(".gslb")) {
rs = make_shared<RareItemSet>(GSLArchive(data, true), true);
} else if (input_filename_lower.ends_with(".afs")) {
rs = make_shared<RareItemSet>(AFSArchive(data), is_v1(get_cli_version(args, Version::DC_V2)));
} else if (input_filename_lower.ends_with(".rel")) {
rs = make_shared<RareItemSet>(*data, true);
} else {
throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, .afs, or .rel");
}
auto rs = load_rare_item_set(
input_filename, is_v1(get_cli_version(args, Version::BB_V4)), s->item_name_index(Version::BB_V4));
if (rate_factor != 1.0) {
rs->multiply_all_rates(rate_factor);
}
@@ -2294,6 +2295,32 @@ Action a_convert_rare_item_set(
throw runtime_error("cannot determine output format; use a filename ending with .json, .gsl, .gslb, or .afs");
}
});
Action a_compare_rare_item_set(
"compare-rare-item-set", nullptr,
+[](phosg::Arguments& args) {
string input_filename1 = args.get<string>(1, false);
if (input_filename1.empty() || (input_filename1 == "-")) {
throw runtime_error("two input filenames must be given");
}
string input_filename2 = args.get<string>(2, false);
if (input_filename2.empty() || (input_filename2 == "-")) {
throw runtime_error("two input filenames must be given");
}
auto s = make_shared<ServerState>(get_config_filename(args));
s->load_config_early();
s->load_patch_indexes();
s->load_text_index();
s->load_item_definitions();
s->load_item_name_indexes();
s->load_drop_tables();
bool is_v1 = ::is_v1(get_cli_version(args, Version::BB_V4));
auto rs1 = load_rare_item_set(input_filename1, is_v1, s->item_name_index(Version::BB_V4));
auto rs2 = load_rare_item_set(input_filename2, is_v1, s->item_name_index(Version::BB_V4));
rs1->print_diff(stdout, *rs2);
});
static shared_ptr<CommonItemSet> load_common_item_set(
const std::string& filename, const std::string& ct_filename, bool big_endian) {
@@ -3098,8 +3125,7 @@ Action a_check_supermaps(
auto f = phosg::fopen_unique(filename, "wt");
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name);
phosg::fwrite_fmt(f.get(), "ENEMY--------------- DCNTE 11/2K DC-V1 DC-V2 PCNTE PC-V2 GCNTE GC-V3 XB-V3 BB-V4\n");
for (size_t type_ss = 0; type_ss < static_cast<size_t>(EnemyType::MAX_ENEMY_TYPE); type_ss++) {
EnemyType type = static_cast<EnemyType>(type_ss);
for (auto type : phosg::EnumRange<EnemyType>()) {
bool any_count_nonzero = false;
array<size_t, NUM_VERSIONS> counts;
for (Version v : ALL_NON_PATCH_VERSIONS) {
+1 -1
View File
@@ -970,7 +970,7 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
}
} else {
c->log.info_f("Creating item from enemy {:04X} (area {:02X})", cmd.entity_index, cmd.effective_area);
res = c->proxy_session->item_creator->on_monster_item_drop(rec.effective_rt_index, cmd.effective_area);
res = c->proxy_session->item_creator->on_monster_item_drop(rec.effective_enemy_type, cmd.effective_area);
}
if (res.item.empty()) {
+168 -89
View File
@@ -177,23 +177,31 @@ RareItemSet::ParsedRELData::ParsedRELData(phosg::StringReader r, bool big_endian
}
RareItemSet::ParsedRELData::ParsedRELData(const SpecCollection& collection) {
for (const auto& specs : collection.rt_index_to_specs) {
ExpandedDrop effective_spec;
this->monster_rares.resize(NUM_RT_INDEXES_V4);
for (const auto& [enemy_type, specs] : collection.enemy_specs) {
const auto& def = type_definition_for_enemy(enemy_type);
if (def.rt_index == 0xFF) {
throw runtime_error(std::format(
"monster spec for {} has no rt_index and cannot be converted to ItemRT format", def.enum_name));
}
auto& dest_spec = this->monster_rares.at(def.rt_index);
for (const auto& spec : specs) {
if (effective_spec.data.empty()) {
effective_spec = spec;
} else if ((effective_spec.probability != spec.probability) || (effective_spec.data != spec.data)) {
throw runtime_error("monster spec cannot be converted to ItemRT format");
if (dest_spec.data.empty()) {
dest_spec = spec;
} else if ((dest_spec.probability != spec.probability) || (dest_spec.data != spec.data)) {
throw runtime_error(std::format(
"monster spec for {} contains multiple drops and cannot be converted to ItemRT format", def.enum_name));
}
}
this->monster_rares.emplace_back(specs.empty() ? ExpandedDrop() : specs[0]);
}
if (collection.box_area_norm_to_specs.size() > 0xFF) {
if (collection.box_specs.size() > 0xFF) {
throw runtime_error("area_norm value too high");
}
for (uint8_t area_norm = 0; area_norm < collection.box_area_norm_to_specs.size(); area_norm++) {
for (const auto& spec : collection.box_area_norm_to_specs[area_norm]) {
for (uint8_t area_norm = 0; area_norm < collection.box_specs.size(); area_norm++) {
for (const auto& spec : collection.box_specs[area_norm]) {
uint8_t area_norm_plus_1 = area_norm + 1;
this->box_rares.emplace_back(BoxRare{.area_norm_plus_1 = area_norm_plus_1, .drop = spec});
}
@@ -208,27 +216,26 @@ std::string RareItemSet::ParsedRELData::serialize(bool big_endian, bool is_v1) c
}
}
RareItemSet::SpecCollection RareItemSet::ParsedRELData::as_collection() const {
RareItemSet::SpecCollection RareItemSet::ParsedRELData::as_collection(Episode episode) const {
SpecCollection ret;
for (size_t z = 0; z < this->monster_rares.size(); z++) {
const auto& drop = this->monster_rares[z];
for (size_t rt_index = 0; rt_index < this->monster_rares.size(); rt_index++) {
const auto& drop = this->monster_rares[rt_index];
if (drop.data.empty()) {
continue;
}
if (z >= ret.rt_index_to_specs.size()) {
ret.rt_index_to_specs.resize(z + 1);
for (auto enemy_type : enemy_types_for_rare_table_index(episode, rt_index)) {
ret.enemy_specs[enemy_type].emplace_back(drop);
}
ret.rt_index_to_specs[z].emplace_back(drop);
}
for (const auto& drop : this->box_rares) {
if ((drop.area_norm_plus_1 == 0) || drop.drop.data.empty()) {
continue;
}
uint8_t area_norm = drop.area_norm_plus_1 - 1;
if (area_norm >= ret.box_area_norm_to_specs.size()) {
ret.box_area_norm_to_specs.resize(area_norm + 1);
if (area_norm >= ret.box_specs.size()) {
ret.box_specs.resize(area_norm + 1);
}
ret.box_area_norm_to_specs[area_norm].emplace_back(drop.drop);
ret.box_specs[area_norm].emplace_back(drop.drop);
}
return ret;
}
@@ -241,8 +248,7 @@ RareItemSet::RareItemSet(const AFSArchive& afs, bool is_v1) {
size_t index = static_cast<size_t>(difficulty) * 10 + section_id;
ParsedRELData rel(afs.get_reader(index), false, is_v1);
this->collections.emplace(
this->key_for_params(mode, Episode::EP1, difficulty, section_id),
rel.as_collection());
this->key_for_params(mode, Episode::EP1, difficulty, section_id), rel.as_collection(Episode::EP1));
} catch (const out_of_range&) {
}
}
@@ -250,7 +256,8 @@ RareItemSet::RareItemSet(const AFSArchive& afs, bool is_v1) {
}
}
string RareItemSet::gsl_entry_name_for_table(GameMode mode, Episode episode, Difficulty difficulty, uint8_t section_id) {
string RareItemSet::gsl_entry_name_for_table(
GameMode mode, Episode episode, Difficulty difficulty, uint8_t section_id) {
return std::format("ItemRT{}{}{}{}.rel",
((mode == GameMode::CHALLENGE) ? "c" : ""),
((episode == Episode::EP2) ? "l" : ""),
@@ -259,7 +266,7 @@ string RareItemSet::gsl_entry_name_for_table(GameMode mode, Episode episode, Dif
}
RareItemSet::RareItemSet(const GSLArchive& gsl, bool is_big_endian) {
for (GameMode mode : ALL_GAME_MODES_V23) {
for (GameMode mode : {GameMode::NORMAL, GameMode::CHALLENGE}) {
for (Episode episode : ALL_EPISODES_V3) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (size_t section_id = 0; section_id < 10; section_id++) {
@@ -267,7 +274,7 @@ RareItemSet::RareItemSet(const GSLArchive& gsl, bool is_big_endian) {
string filename = this->gsl_entry_name_for_table(mode, episode, difficulty, section_id);
ParsedRELData rel(gsl.get_reader(filename), is_big_endian, false);
this->collections.emplace(
this->key_for_params(mode, episode, difficulty, section_id), rel.as_collection());
this->key_for_params(mode, episode, difficulty, section_id), rel.as_collection(episode));
} catch (const out_of_range&) {
}
}
@@ -287,7 +294,7 @@ RareItemSet::RareItemSet(const string& rel_data, bool is_big_endian) {
size_t index = (ep_index * 40) + static_cast<size_t>(difficulty) * 10 + section_id;
ParsedRELData rel(r.sub(0x280 * index, 0x280), is_big_endian, false);
this->collections.emplace(
this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id), rel.as_collection());
this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id), rel.as_collection(episode));
} catch (const out_of_range&) {
}
}
@@ -315,26 +322,19 @@ RareItemSet::RareItemSet(const phosg::JSON& json, shared_ptr<const ItemNameIndex
uint8_t section_id = section_id_for_name(section_id_it.first);
auto& collection = this->collections[this->key_for_params(mode, episode, difficulty, section_id)];
for (const auto& item_it : section_id_it.second->as_dict()) {
for (const auto& [enemy_type_name, specs_json] : section_id_it.second->as_dict()) {
vector<ExpandedDrop>* target;
if (item_it.first.starts_with("Box-")) {
uint8_t area_norm = FloorDefinition::get(episode, item_it.first.substr(4)).drop_area_norm;
if (collection.box_area_norm_to_specs.size() <= area_norm) {
collection.box_area_norm_to_specs.resize(area_norm + 1);
if (enemy_type_name.starts_with("Box-")) {
uint8_t area_norm = FloorDefinition::get(episode, enemy_type_name.substr(4)).drop_area_norm;
if (collection.box_specs.size() <= area_norm) {
collection.box_specs.resize(area_norm + 1);
}
target = &collection.box_area_norm_to_specs[area_norm];
target = &collection.box_specs[area_norm];
} else {
size_t rt_index = type_definition_for_enemy(phosg::enum_for_name<EnemyType>(item_it.first)).rt_index;
if (rt_index == 0xFF) {
throw runtime_error("enemy type " + item_it.first + " does not have an rt_index");
}
if (collection.rt_index_to_specs.size() <= rt_index) {
collection.rt_index_to_specs.resize(rt_index + 1);
}
target = &collection.rt_index_to_specs[rt_index];
target = &collection.enemy_specs[phosg::enum_for_name<EnemyType>(enemy_type_name)];
}
for (const auto& spec_json : item_it.second->as_list()) {
for (const auto& spec_json : specs_json->as_list()) {
auto& d = target->emplace_back();
auto prob_desc = spec_json->at(0);
@@ -702,7 +702,7 @@ string RareItemSet::serialize_html(
std::string exact_token = std::format("Exact rate: {} / {}", frac.first, frac.second);
if (common_item_set && type_def && type_def->rt_index != 0xFF) {
auto table = common_item_set->get_table(episode, mode, difficulty, section_id);
uint8_t dar = table->enemy_type_drop_probs.at(type_def->rt_index);
uint8_t dar = table->enemy_type_drop_probs.at(type_def->type);
exact_token += std::format(" (DAR: {}%)", dar);
frac.first *= dar;
frac.second *= 100;
@@ -747,13 +747,9 @@ string RareItemSet::serialize_html(
for (const auto& zone_type : zone_types) {
add_location_header(zone_type.name);
for (EnemyType type : zone_type.types) {
uint8_t rt_index = type_definition_for_enemy(type).rt_index;
if (rt_index == 0xFF) {
continue;
}
array<vector<ExpandedDrop>, 10> specs_lists;
for (uint8_t section_id = 0; section_id < 10; section_id++) {
specs_lists[section_id] = this->get_enemy_specs(mode, episode, difficulty, section_id, rt_index);
specs_lists[section_id] = this->get_enemy_specs(mode, episode, difficulty, section_id, type);
}
const auto& type_def = type_definition_for_enemy(type);
const char* name = (difficulty == Difficulty::ULTIMATE && type_def.ultimate_name) ? type_def.ultimate_name : type_def.in_game_name;
@@ -787,13 +783,9 @@ phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const
auto section_id_dict = phosg::JSON::dict();
for (uint8_t section_id = 0; section_id < 10; section_id++) {
auto collection_dict = phosg::JSON::dict();
for (size_t rt_index = 0; rt_index < 0x80; rt_index++) {
const auto& enemy_types = enemy_types_for_rare_table_index(episode, rt_index);
if (enemy_types.empty()) {
continue;
}
for (const auto& spec : this->get_enemy_specs(GameMode::NORMAL, episode, difficulty, section_id, rt_index)) {
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
const auto& specs = this->get_enemy_specs(GameMode::NORMAL, episode, difficulty, section_id, enemy_type);
for (const auto& spec : specs) {
if (spec.data.empty()) {
continue;
}
@@ -807,12 +799,8 @@ phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const
if (name_index) {
spec_json.emplace_back(name_index->describe_item(spec.data));
}
for (const auto& enemy_type : enemy_types) {
if (type_definition_for_enemy(enemy_type).valid_in_episode(episode)) {
phosg::JSON this_spec_json = spec_json;
collection_dict.emplace(phosg::name_for_enum(enemy_type), phosg::JSON::list()).first->second->emplace_back(std::move(this_spec_json));
}
}
auto list_emplace_ret = collection_dict.emplace(phosg::name_for_enum(enemy_type), phosg::JSON::list());
list_emplace_ret.first->second->emplace_back(std::move(spec_json));
}
}
@@ -858,17 +846,17 @@ phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const
}
void RareItemSet::multiply_all_rates(double factor) {
auto multiply_rates_vec = +[](vector<vector<ExpandedDrop>>& vec, double factor) -> void {
for (auto& vec_it : vec) {
for (auto& z_it : vec_it) {
uint64_t new_probability = z_it.probability * factor;
z_it.probability = min<uint64_t>(new_probability, 0xFFFFFFFF);
for (auto& [_, collection] : this->collections) {
for (auto& [_, specs] : collection.enemy_specs) {
for (auto& spec : specs) {
spec.probability = min<uint64_t>(spec.probability * factor, 0xFFFFFFFF);
}
}
for (auto& specs : collection.box_specs) {
for (auto& spec : specs) {
spec.probability = min<uint64_t>(spec.probability * factor, 0xFFFFFFFF);
}
}
};
for (auto& coll_it : this->collections) {
multiply_rates_vec(coll_it.second.rt_index_to_specs, factor);
multiply_rates_vec(coll_it.second.box_area_norm_to_specs, factor);
}
}
@@ -893,28 +881,22 @@ void RareItemSet::print_collection(
name_for_section_id(section_id));
phosg::fwrite_fmt(stream, " Monster rares:\n");
for (size_t z = 0; z < collection->rt_index_to_specs.size(); z++) {
string enemy_types_str;
const auto& enemy_types = enemy_types_for_rare_table_index(episode, z);
for (EnemyType enemy_type : enemy_types) {
enemy_types_str += phosg::name_for_enum(enemy_type);
enemy_types_str.push_back(',');
}
if (!enemy_types_str.empty()) {
enemy_types_str.resize(enemy_types_str.size() - 1);
}
for (const auto& spec : collection->rt_index_to_specs[z]) {
string s = name_index ? spec.str(name_index) : spec.str();
phosg::fwrite_fmt(stream, " {:02X}: {} ({})\n", z, s, enemy_types_str);
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
try {
const auto& def = type_definition_for_enemy(enemy_type);
for (const auto& spec : collection->enemy_specs.at(enemy_type)) {
string s = name_index ? spec.str(name_index) : spec.str();
phosg::fwrite_fmt(stream, " {:<23} {}\n", def.enum_name, s);
}
} catch (const out_of_range&) {
}
}
phosg::fwrite_fmt(stream, " Box rares:\n");
for (size_t area_norm = 0; area_norm < collection->box_area_norm_to_specs.size(); area_norm++) {
for (const auto& spec : collection->box_area_norm_to_specs[area_norm]) {
for (size_t area_norm = 0; area_norm < collection->box_specs.size(); area_norm++) {
for (const auto& spec : collection->box_specs[area_norm]) {
string s = name_index ? spec.str(name_index) : spec.str();
phosg::fwrite_fmt(stream, " (area-norm {:02X}) {}\n", area_norm, s);
phosg::fwrite_fmt(stream, " (area-norm {:02X}) {}\n", area_norm, s);
}
}
}
@@ -934,10 +916,100 @@ void RareItemSet::print_all_collections(FILE* stream, std::shared_ptr<const Item
}
}
void RareItemSet::SpecCollection::print_diff(FILE* stream, const SpecCollection& other) const {
auto format_specs = [](const std::vector<ExpandedDrop>& specs) -> std::string {
std::string ret;
for (const auto& spec : specs) {
if (!ret.empty()) {
ret += ",";
}
ret += std::format("{:08X}:{}", spec.probability, spec.data.short_hex());
}
return ret;
};
const std::vector<ExpandedDrop> empty_specs{};
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
const std::vector<ExpandedDrop>* this_specs = &empty_specs;
const std::vector<ExpandedDrop>* other_specs = &empty_specs;
try {
this_specs = &this->enemy_specs.at(enemy_type);
} catch (const out_of_range&) {
}
try {
other_specs = &other.enemy_specs.at(enemy_type);
} catch (const out_of_range&) {
}
if (*this_specs != *other_specs) {
phosg::fwrite_fmt(stream, " {}: {} -> {}\n",
phosg::name_for_enum(enemy_type), format_specs(*this_specs), format_specs(*other_specs));
}
}
for (size_t area_norm = 0; area_norm < 10; area_norm++) {
const auto& this_specs = (area_norm < this->box_specs.size()) ? this->box_specs[area_norm] : empty_specs;
const auto& other_specs = (area_norm < other.box_specs.size()) ? other.box_specs[area_norm] : empty_specs;
if (this_specs != other_specs) {
phosg::fwrite_fmt(stream, " Box (area_norm {}): {} -> {}\n",
area_norm, format_specs(this_specs), format_specs(other_specs));
}
}
}
void RareItemSet::print_diff(FILE* stream, const RareItemSet& other) const {
bool any_difference_found = false;
for (const auto& episode : ALL_EPISODES_V4) {
for (const auto& mode : ALL_GAME_MODES_V4) {
for (const auto& difficulty : ALL_DIFFICULTIES_V234) {
for (uint8_t section_id = 0; section_id < 10; section_id++) {
const SpecCollection* this_coll = nullptr;
const SpecCollection* other_coll = nullptr;
try {
this_coll = &this->get_collection(mode, episode, difficulty, section_id);
} catch (const out_of_range&) {
}
try {
other_coll = &other.get_collection(mode, episode, difficulty, section_id);
} catch (const out_of_range&) {
}
if (!this_coll && !other_coll) {
continue;
} else if (!this_coll) {
any_difference_found = true;
phosg::fwrite_fmt(stream, "> Collection present in other but not this: {} {} {} {}\n",
name_for_episode(episode),
name_for_mode(mode),
name_for_difficulty(difficulty),
name_for_section_id(section_id));
} else if (!other_coll) {
any_difference_found = true;
phosg::fwrite_fmt(stream, "> Collection present in this but not other: {} {} {} {}\n",
name_for_episode(episode),
name_for_mode(mode),
name_for_difficulty(difficulty),
name_for_section_id(section_id));
} else if (*this_coll != *other_coll) {
any_difference_found = true;
phosg::fwrite_fmt(stream, "> Collections do not match: {} {} {} {}\n",
name_for_episode(episode),
name_for_mode(mode),
name_for_difficulty(difficulty),
name_for_section_id(section_id));
this_coll->print_diff(stream, *other_coll);
}
}
}
}
}
if (!any_difference_found) {
phosg::fwrite_fmt(stream, "> These rare item sets are identical\n");
}
}
std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_enemy_specs(
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t rt_index) const {
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, EnemyType enemy_type) const {
try {
return this->get_collection(mode, episode, difficulty, secid).rt_index_to_specs.at(rt_index);
return this->get_collection(mode, episode, difficulty, secid).enemy_specs.at(enemy_type);
} catch (const out_of_range&) {
static const std::vector<ExpandedDrop> empty_vector;
return empty_vector;
@@ -947,7 +1019,7 @@ std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_enemy_specs(
std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_box_specs(
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t area_norm) const {
try {
return this->get_collection(mode, episode, difficulty, secid).box_area_norm_to_specs.at(area_norm);
return this->get_collection(mode, episode, difficulty, secid).box_specs.at(area_norm);
} catch (const out_of_range&) {
static const std::vector<ExpandedDrop> empty_vector;
return empty_vector;
@@ -965,7 +1037,14 @@ bool RareItemSet::has_entries_for_game_config(GameMode mode, Episode episode, Di
const RareItemSet::SpecCollection& RareItemSet::get_collection(
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid) const {
return this->collections.at(this->key_for_params(mode, episode, difficulty, secid));
try {
return this->collections.at(this->key_for_params(mode, episode, difficulty, secid));
} catch (const out_of_range&) {
if (mode == GameMode::BATTLE || mode == GameMode::SOLO) {
return this->collections.at(this->key_for_params(GameMode::NORMAL, episode, difficulty, secid));
}
throw;
}
}
uint16_t RareItemSet::key_for_params(GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid) {
+15 -6
View File
@@ -22,6 +22,9 @@ public:
uint32_t probability = 0;
ItemData data;
bool operator==(const ExpandedDrop& other) const = default;
bool operator!=(const ExpandedDrop& other) const = default;
std::string str() const;
std::string str(std::shared_ptr<const ItemNameIndex> name_index) const;
};
@@ -34,7 +37,7 @@ public:
~RareItemSet() = default;
std::vector<ExpandedDrop> get_enemy_specs(
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t rt_index) const;
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, EnemyType enemy_type) const;
std::vector<ExpandedDrop> get_box_specs(
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t area_norm) const;
@@ -60,11 +63,17 @@ public:
uint8_t section_id,
std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
void print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
void print_diff(FILE* stream, const RareItemSet& other) const;
protected:
struct SpecCollection {
std::vector<std::vector<ExpandedDrop>> rt_index_to_specs;
std::vector<std::vector<ExpandedDrop>> box_area_norm_to_specs;
std::unordered_map<EnemyType, std::vector<ExpandedDrop>> enemy_specs;
std::vector<std::vector<ExpandedDrop>> box_specs; // Indexed by area_norm
bool operator==(const SpecCollection& other) const = default;
bool operator!=(const SpecCollection& other) const = default;
void print_diff(FILE* stream, const SpecCollection& other) const;
};
struct ParsedRELData {
@@ -95,8 +104,8 @@ protected:
ExpandedDrop drop;
};
std::vector<ExpandedDrop> monster_rares;
std::vector<BoxRare> box_rares;
std::vector<ExpandedDrop> monster_rares; // Indexed by rt_index
std::vector<BoxRare> box_rares; // Not indexed (area_norm + 1 is in the struct)
ParsedRELData() = default;
ParsedRELData(phosg::StringReader r, bool big_endian, bool is_v1);
@@ -108,7 +117,7 @@ protected:
template <bool BE>
std::string serialize_t(bool is_v1) const;
SpecCollection as_collection() const;
SpecCollection as_collection(Episode episode) const;
};
std::unordered_map<uint16_t, SpecCollection> collections;
+15 -14
View File
@@ -2810,7 +2810,7 @@ DropReconcileResult reconcile_drop_request_with_map(
bool is_box = (cmd.rt_index == 0x30);
DropReconcileResult res;
res.effective_rt_index = 0xFF;
res.effective_enemy_type = EnemyType::UNKNOWN;
res.should_drop = true;
res.ignore_def = (cmd.ignore_def != 0);
if (!map) {
@@ -2856,22 +2856,22 @@ DropReconcileResult reconcile_drop_request_with_map(
res.ref_ene_st = map->enemy_state_for_index(version, cmd.floor, cmd.entity_index);
res.target_ene_st = res.ref_ene_st->alias_target_ene_st ? res.ref_ene_st->alias_target_ene_st : res.ref_ene_st;
uint8_t area = map->floor_to_area.at(res.target_ene_st->super_ene->floor);
EnemyType type = res.target_ene_st->type(version, area, difficulty, event);
res.effective_enemy_type = res.target_ene_st->type(version, area, difficulty, event);
c->log.info_f("Drop check for E-{:03X} (target E-{:03X}, type {})",
res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(type));
res.effective_rt_index = type_definition_for_enemy(type).rt_index;
res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(res.effective_enemy_type));
uint8_t expected_rt_index = type_definition_for_enemy(res.effective_enemy_type).rt_index;
bool mismatched_rt_index = false;
if (cmd.rt_index != res.effective_rt_index) {
if (cmd.rt_index != expected_rt_index) {
// Special cases: BULCLAW => BULK and DARK_GUNNER => DEATH_GUNNER
if (cmd.rt_index == 0x27 && type == EnemyType::BULCLAW) {
if ((cmd.rt_index == 0x27) && (res.effective_enemy_type == EnemyType::BULCLAW)) {
c->log.info_f("E-{:03X} killed as BULK instead of BULCLAW", res.target_ene_st->e_id);
res.effective_rt_index = 0x27;
} else if (cmd.rt_index == 0x23 && type == EnemyType::DARK_GUNNER) {
res.effective_enemy_type = EnemyType::BULK;
} else if ((cmd.rt_index == 0x23) && (res.effective_enemy_type == EnemyType::DARK_GUNNER)) {
c->log.info_f("E-{:03X} killed as DEATH_GUNNER instead of DARK_GUNNER", res.target_ene_st->e_id);
res.effective_rt_index = 0x23;
res.effective_enemy_type = EnemyType::DEATH_GUNNER;
} else {
c->log.warning_f("rt_index {:02X} from command does not match entity\'s expected index {:02X}",
cmd.rt_index, res.effective_rt_index);
cmd.rt_index, expected_rt_index);
mismatched_rt_index = true;
}
}
@@ -2881,9 +2881,10 @@ DropReconcileResult reconcile_drop_request_with_map(
}
if (c->check_flag(Client::Flag::DEBUG_ENABLED)) {
std::string rt_index_str = mismatched_rt_index
? std::format(" $C4{:02X}->{:02X}$C5", cmd.rt_index, res.effective_rt_index)
: std::format(" {:02X}", res.effective_rt_index);
send_text_message_fmt(c, "$C5E-{:03X}{} {}", res.target_ene_st->e_id, rt_index_str, phosg::name_for_enum(type));
? std::format(" $C4{:02X}->{:02X}$C5", cmd.rt_index, expected_rt_index)
: std::format(" {:02X}", expected_rt_index);
send_text_message_fmt(c, "$C5E-{:03X}{} {}",
res.target_ene_st->e_id, rt_index_str, phosg::name_for_enum(res.effective_enemy_type));
}
}
@@ -2963,7 +2964,7 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
} else if (rec.target_ene_st) {
l->log.info_f("Creating item from enemy {:04X} => E-{:03X} (area {:02X})",
cmd.entity_index, rec.target_ene_st->e_id, cmd.effective_area);
return l->item_creator->on_monster_item_drop(rec.effective_rt_index, cmd.effective_area);
return l->item_creator->on_monster_item_drop(rec.effective_enemy_type, cmd.effective_area);
} else {
throw runtime_error("neither object nor enemy were present");
}
+1 -1
View File
@@ -23,7 +23,7 @@ struct DropReconcileResult {
// for drop computation (which may be the result of following an alias from the ref ene_st)
std::shared_ptr<MapState::EnemyState> ref_ene_st;
std::shared_ptr<MapState::EnemyState> target_ene_st;
uint8_t effective_rt_index;
EnemyType effective_enemy_type;
bool should_drop;
bool ignore_def;
};