add support for cross-episode quests

This commit is contained in:
Martin Michelsen
2025-11-28 12:40:14 -08:00
parent 23eb6b29a5
commit e02a006b60
12 changed files with 504 additions and 407 deletions
+261 -256
View File
@@ -30,16 +30,14 @@ ItemCreator::ItemCreator(
shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set,
shared_ptr<const ItemParameterTable> item_parameter_table,
std::shared_ptr<const ItemData::StackLimits> stack_limits,
Episode episode,
GameMode mode,
Difficulty difficulty,
uint8_t section_id,
std::shared_ptr<RandomGenerator> rand_crypt,
shared_ptr<const BattleRules> restrictions)
: log(std::format("[ItemCreator:{}/{}/{}/{}/{}] ", phosg::name_for_enum(stack_limits->version), abbreviation_for_episode(episode), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level),
: log(std::format("[ItemCreator:{}/{}/{}/{}] ", phosg::name_for_enum(stack_limits->version), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level),
logic_version(stack_limits->version),
stack_limits(stack_limits),
episode(episode),
mode(mode),
difficulty(difficulty),
section_id(section_id),
@@ -50,7 +48,6 @@ ItemCreator::ItemCreator(
tekker_adjustment_set(tekker_adjustment_set),
item_parameter_table(item_parameter_table),
common_item_set(common_item_set),
pt(common_item_set->get_table(this->episode, this->mode, this->difficulty, this->section_id)),
restrictions(restrictions),
rand_crypt(rand_crypt) {
this->generate_unit_stars_tables();
@@ -59,13 +56,11 @@ ItemCreator::ItemCreator(
void ItemCreator::set_section_id(uint8_t new_section_id) {
if (this->section_id != new_section_id) {
this->section_id = new_section_id;
this->log.prefix = std::format("[ItemCreator:{}/{}/{}/{}/{}] ",
this->log.prefix = std::format("[ItemCreator:{}/{}/{}/{}] ",
phosg::name_for_enum(stack_limits->version),
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty),
this->section_id);
this->pt = common_item_set->get_table(this->episode, this->mode, this->difficulty, this->section_id);
}
}
@@ -80,71 +75,114 @@ bool ItemCreator::are_rare_drops_allowed() const {
return (this->mode != GameMode::CHALLENGE);
}
uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
uint8_t ItemCreator::table_index_for_area(uint8_t area) const {
if (this->restrictions && (this->restrictions->box_drop_area != 0)) {
return this->restrictions->box_drop_area - 1;
} else {
switch (this->episode) {
case Episode::EP1:
if (area >= 0x11) {
throw runtime_error("invalid Episode 1 area number");
}
switch (area) {
case 0x0B: // Dragon -> Cave 1
return 2;
case 0x0C: // De Rol Le -> Mine 1
return 5;
case 0x0D: // Vol Opt -> Ruins 1
return 7;
case 0x0E: // Dark Falz -> Ruins 3
case 0x10: // Palace -> Ruins 3
case 0x11: // Spaceship -> Ruins 3
return 9;
case 0x0F: // Lobby
throw runtime_error("visual lobby does not have item drop tables");
default:
return area - 1;
}
throw logic_error("this should be impossible");
case Episode::EP2: {
static const array<uint8_t, 0x11> area_subs = {
0x00, // 13 (VR Temple Alpha)
0x01, // 14 (VR Temple Beta)
0x02, // 15 (VR Spaceship Alpha)
0x03, // 16 (VR Spaceship Beta)
0x07, // 17 (Central Control Area)
0x04, // 18 (Jungle North)
0x05, // 19 (Jungle South)
0x06, // 1A (Mountain)
0x07, // 1B (Seaside)
0x08, // 1C (Seabed Upper)
0x09, // 1D (Seabed Lower)
0x08, // 1E (Gal Gryphon)
0x09, // 1F (Olga Flow)
0x02, // 20 (Barba Ray)
0x04, // 21 (Gol Dragon)
0x07, // 22 (Seaside Night)
0x09, // 23 (Tower)
};
if ((area >= 0x13) && (area < 0x24)) {
return area_subs.at(area - 0x13);
}
throw runtime_error("invalid Episode 2 area number");
}
case Episode::EP4:
if (area >= 0x24 && area < 0x2D) {
return area - 0x23;
}
throw runtime_error("invalid Episode 4 area number");
default:
throw logic_error("invalid episode number");
}
}
static constexpr std::array<uint8_t, 0x2F> data = {
// Episode 1
0xFF, // 00 => Pioneer 2 (no drops)
0x00, // 01 => Forest 1
0x01, // 02 => Forest 2
0x02, // 03 => Cave 1
0x03, // 04 => Cave 2
0x04, // 05 => Cave 3
0x05, // 06 => Mine 1
0x06, // 07 => Mine 2
0x07, // 08 => Ruins 1
0x08, // 09 => Ruins 2
0x09, // 0A => Ruins 3
0x02, // 0B => Dragon -> Cave 1
0x05, // 0C => De Rol Le -> Mine 1
0x07, // 0D => Vol Opt -> Ruins 1
0x09, // 0E => Dark Falz -> Ruins 3
0xFF, // 0F => Lobby (no drops)
0x09, // 10 => Palace -> Ruins 3
0x09, // 11 => Spaceship -> Ruins 3
// Episode 2
0xFF, // 12 => Lab (no drops)
0x00, // 13 => VR Temple Alpha
0x01, // 14 => VR Temple Beta
0x02, // 15 => VR Spaceship Alpha
0x03, // 16 => VR Spaceship Beta
0x07, // 17 => Central Control Area -> Seaside
0x04, // 18 => Jungle North
0x05, // 19 => Jungle South
0x06, // 1A => Mountain
0x07, // 1B => Seaside
0x08, // 1C => Seabed Upper
0x09, // 1D => Seabed Lower
0x08, // 1E => Gal Gryphon -> Seabed Upper
0x09, // 1F => Olga Flow -> Seabed Lower
0x02, // 20 => Barba Ray -> VR Spaceship Alpha
0x04, // 21 => Gol Dragon -> Jungle North
0x07, // 22 => Seaside Night -> Seaside
0x09, // 23 => Tower -> Seabed Lower
// Episode 4
0x01, // 24 => Crater East
0x02, // 25 => Crater West
0x03, // 26 => Crater South
0x04, // 27 => Crater North
0x05, // 28 => Crater Interior
0x06, // 29 => Subterranean Desert 1
0x07, // 2A => Subterranean Desert 2
0x08, // 2B => Subterranean Desert 3
0x09, // 2C => Saint-Milion
0xFF, // 2D => Pioneer 2 (no drops)
0xFF, // 2E => Test area (no drops)
};
if (area >= data.size() || data[area] == 0xFF) {
throw std::runtime_error("invalid area number");
}
return data[area];
}
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) {
try {
return this->on_box_item_drop_with_area_norm(this->normalize_area_number(area));
uint8_t table_index = this->table_index_for_area(area);
this->log.info_f("Box drop checks for area {:02X} (table index {:02X})", area, table_index);
DropResult res;
res.item = this->check_rare_specs_and_create_rare_box_item(area);
if (!res.item.empty()) {
res.is_from_rare_table = true;
} else {
uint8_t item_class = this->get_rand_from_weighted_tables_2d_vertical(
this->pt(area)->box_item_class_prob_table, table_index);
this->log.info_f("Item class is {:02X}", item_class);
switch (item_class) {
case 0: // Weapon
res.item.data1[0] = 0;
break;
case 1: // Armor
res.item.data1[0] = 1;
res.item.data1[1] = 1;
break;
case 2: // Shield
res.item.data1[0] = 1;
res.item.data1[1] = 2;
break;
case 3: // Unit
res.item.data1[0] = 1;
res.item.data1[1] = 3;
break;
case 4: // Tool
res.item.data1[0] = 3;
break;
case 5: // Meseta
res.item.data1[0] = 4;
break;
case 6: // Nothing
break;
default:
throw logic_error("this should be impossible");
}
if (item_class < 6) {
this->generate_common_item_variances(res.item, area);
}
}
return res;
} catch (const exception& e) {
this->log.error_f("Exception in item creation: {}", e.what());
return DropResult();
@@ -153,143 +191,99 @@ ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) {
ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, uint8_t area) {
try {
return this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area));
// Note: The original GC implementation uses (enemy_type > 0x58) here; we
// extend it to the full array size for BB
if (enemy_type >= 0x64) {
this->log.warning_f("Invalid enemy type: {:X}", enemy_type);
return DropResult();
}
this->log.info_f("Enemy type: {:X}", enemy_type);
auto pt = this->pt(area);
uint8_t type_drop_prob = pt->enemy_type_drop_probs.at(enemy_type);
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);
return DropResult();
} else {
this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob);
}
DropResult res;
res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area);
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;
switch (item_class_determinant) {
case 0:
item_class = 5;
break;
case 1:
item_class = 4;
break;
case 2:
item_class = pt->enemy_item_classes.at(enemy_type);
break;
default:
throw logic_error("invalid item class determinant");
}
this->log.info_f("Rare drop not chosen; item class determinant is {}; item class is {}", item_class_determinant, item_class);
switch (item_class) {
case 0: // Weapon
res.item.data1[0] = 0x00;
break;
case 1: // Armor
res.item.data1w[0] = 0x0101;
break;
case 2: // Shield
res.item.data1w[0] = 0x0201;
break;
case 3: // Unit
res.item.data1w[0] = 0x0301;
break;
case 4: // Tool
res.item.data1[0] = 0x03;
break;
case 5: // Meseta
res.item.data1[0] = 0x04;
res.item.data2d = this->choose_meseta_amount(pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
break;
default:
return res;
}
if (res.item.data1[0] != 0x04) {
this->generate_common_item_variances(res.item, area);
}
}
return res;
} catch (const exception& e) {
this->log.error_f("Exception in item creation: {}", e.what());
return DropResult();
}
}
ItemCreator::DropResult ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
this->log.info_f("Box drop checks for area_norm {:02X}", area_norm);
DropResult res;
res.item = this->check_rare_specs_and_create_rare_box_item(area_norm);
if (!res.item.empty()) {
res.is_from_rare_table = true;
} else {
uint8_t item_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->box_item_class_prob_table, area_norm);
this->log.info_f("Item class is {:02X}", item_class);
switch (item_class) {
case 0: // Weapon
res.item.data1[0] = 0;
break;
case 1: // Armor
res.item.data1[0] = 1;
res.item.data1[1] = 1;
break;
case 2: // Shield
res.item.data1[0] = 1;
res.item.data1[1] = 2;
break;
case 3: // Unit
res.item.data1[0] = 1;
res.item.data1[1] = 3;
break;
case 4: // Tool
res.item.data1[0] = 3;
break;
case 5: // Meseta
res.item.data1[0] = 4;
break;
case 6: // Nothing
break;
default:
throw logic_error("this should be impossible");
}
if (item_class < 6) {
this->generate_common_item_variances(area_norm, res.item);
}
}
return res;
}
ItemCreator::DropResult ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm) {
// Note: The original GC implementation uses (enemy_type > 0x58) here; we
// extend it to the full array size for BB
if (enemy_type >= 0x64) {
this->log.warning_f("Invalid enemy type: {:X}", enemy_type);
return DropResult();
}
this->log.info_f("Enemy type: {:X}", enemy_type);
uint8_t type_drop_prob = this->pt->enemy_type_drop_probs.at(enemy_type);
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);
return DropResult();
} else {
this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob);
}
DropResult res;
res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area_norm);
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;
switch (item_class_determinant) {
case 0:
item_class = 5;
break;
case 1:
item_class = 4;
break;
case 2:
item_class = this->pt->enemy_item_classes.at(enemy_type);
break;
default:
throw logic_error("invalid item class determinant");
}
this->log.info_f("Rare drop not chosen; item class determinant is {}; item class is {}", item_class_determinant, item_class);
switch (item_class) {
case 0: // Weapon
res.item.data1[0] = 0x00;
break;
case 1: // Armor
res.item.data1w[0] = 0x0101;
break;
case 2: // Shield
res.item.data1w[0] = 0x0201;
break;
case 3: // Unit
res.item.data1w[0] = 0x0301;
break;
case 4: // Tool
res.item.data1[0] = 0x03;
break;
case 5: // Meseta
res.item.data1[0] = 0x04;
res.item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
break;
default:
return res;
}
if (res.item.data1[0] != 0x04) {
this->generate_common_item_variances(area_norm, res.item);
}
}
return res;
}
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area_norm) {
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area) {
ItemData item;
if (!this->are_rare_drops_allowed()) {
return item;
}
auto rare_specs = this->rare_item_set->get_box_specs(
this->mode, this->episode, this->difficulty, this->section_id, area_norm);
uint8_t table_index = this->table_index_for_area(area);
Episode episode = episode_for_area(area);
auto rare_specs = this->rare_item_set->get_box_specs(this->mode, episode, this->difficulty, this->section_id, table_index);
for (const auto& spec : rare_specs) {
item = this->check_rate_and_create_rare_item(spec, area_norm);
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();
@@ -340,17 +334,17 @@ 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_norm) {
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type, uint8_t area) {
ItemData item;
if (this->are_rare_drops_allowed() && (enemy_type > 0) && (enemy_type < 0x64)) {
// 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).
auto rare_specs = this->rare_item_set->get_enemy_specs(
this->mode, this->episode, this->difficulty, this->section_id, 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_norm);
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();
@@ -367,7 +361,7 @@ ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_
return item;
}
ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area_norm) {
ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area) {
if (drop.probability == 0) {
return ItemData();
}
@@ -382,15 +376,15 @@ ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::Expande
if (item.can_be_encoded_in_rel_rare_table()) {
switch (item.data1[0]) {
case 0:
if (this->pt->has_rare_bonus_value_prob_table) {
this->generate_rare_weapon_bonuses(item, this->rand_int(10));
if (this->pt(area)->has_rare_bonus_value_prob_table) {
this->generate_rare_weapon_bonuses(item, episode_for_area(area), this->rand_int(10));
} else {
this->generate_common_weapon_bonuses(item, area_norm);
this->generate_common_weapon_bonuses(item, area);
}
this->set_item_unidentified_flag_if_not_challenge(item);
break;
case 1:
this->generate_common_armor_slots_and_bonuses(item);
this->generate_common_armor_slots_and_bonuses(item, episode_for_area(area));
break;
case 2:
this->generate_common_mag_variances(item);
@@ -411,18 +405,19 @@ ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::Expande
return item;
}
void ItemCreator::generate_rare_weapon_bonuses(ItemData& item, uint32_t random_sample) {
void ItemCreator::generate_rare_weapon_bonuses(ItemData& item, Episode episode, uint32_t table_index) {
if (item.data1[0] != 0) {
return;
}
if (!this->pt->has_rare_bonus_value_prob_table) {
auto pt = this->pt(episode);
if (!pt->has_rare_bonus_value_prob_table) {
throw logic_error("generate_rare_weapon_bonuses called for common item table without rare bonus value probability table");
}
for (size_t z = 0; z < 6; z += 2) {
uint8_t bonus_type = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_type_prob_table, random_sample);
int16_t bonus_value = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_value_prob_table, 5);
uint8_t bonus_type = this->get_rand_from_weighted_tables_2d_vertical(pt->bonus_type_prob_table, table_index);
int16_t bonus_value = this->get_rand_from_weighted_tables_2d_vertical(pt->bonus_value_prob_table, 5);
item.data1[z + 6] = bonus_type;
item.data1[z + 7] = bonus_value * 5 - 10;
// Note: The original code has a special case here, which divides
@@ -434,20 +429,22 @@ void ItemCreator::generate_rare_weapon_bonuses(ItemData& item, uint32_t random_s
this->deduplicate_weapon_bonuses(item);
}
void ItemCreator::generate_common_weapon_bonuses(ItemData& item, uint8_t area_norm) {
void ItemCreator::generate_common_weapon_bonuses(ItemData& item, uint8_t area) {
if (item.data1[0] != 0) {
return;
}
auto pt = this->pt(area);
uint8_t table_index = this->table_index_for_area(area);
for (size_t row = 0; row < 3; row++) {
uint8_t spec = this->pt->nonrare_bonus_prob_spec.at(row).at(area_norm);
uint8_t spec = pt->nonrare_bonus_prob_spec.at(row).at(table_index);
if (spec == 0xFF) {
this->log.info_f("Bonus {} is forbidden", row);
} else {
item.data1[(row * 2) + 6] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_type_prob_table, area_norm);
int16_t amount = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_value_prob_table, spec);
item.data1[(row * 2) + 6] = this->get_rand_from_weighted_tables_2d_vertical(pt->bonus_type_prob_table, table_index);
int16_t amount = this->get_rand_from_weighted_tables_2d_vertical(pt->bonus_value_prob_table, spec);
item.data1[(row * 2) + 7] = amount * 5 - 10;
this->log.info_f("Bonus {} generated as {:02X} {:02X} from area_norm {:02X} and spec {:02X}", row, item.data1[(row * 2) + 6], item.data1[(row * 2) + 7], area_norm, spec);
this->log.info_f("Bonus {} generated as {:02X} {:02X} from table index {:02X} and spec {:02X}", row, item.data1[(row * 2) + 6], item.data1[(row * 2) + 7], table_index, spec);
}
// Note: The original code has a special case here, which divides
// item.data1[z + 7] by 5 and multiplies it by 5 again if bonus_type is 5
@@ -605,14 +602,14 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
}
}
void ItemCreator::generate_common_item_variances(uint32_t area_norm, ItemData& item) {
void ItemCreator::generate_common_item_variances(ItemData& item, uint8_t area) {
switch (item.data1[0]) {
case 0:
this->generate_common_weapon_variances(area_norm, item);
this->generate_common_weapon_variances(item, area);
break;
case 1:
if (item.data1[1] == 3) {
float f1 = 1.0 + this->pt->unit_max_stars_table.at(area_norm);
float f1 = 1.0 + this->pt(area)->unit_max_stars_table.at(this->table_index_for_area(area));
float f2 = this->rand_float_0_1_from_crypt();
uint8_t stars = static_cast<uint32_t>(f1 * f2) & 0xFF;
this->log.info_f("Unit stars: {:g} * {:g} = {}", f1, f2, stars);
@@ -622,17 +619,17 @@ void ItemCreator::generate_common_item_variances(uint32_t area_norm, ItemData& i
item.clear();
}
} else {
this->generate_common_armor_or_shield_type_and_variances(area_norm, item);
this->generate_common_armor_or_shield_type_and_variances(item, area);
}
break;
case 2:
this->generate_common_mag_variances(item);
break;
case 3:
this->generate_common_tool_variances(area_norm, item);
this->generate_common_tool_variances(item, area);
break;
case 4:
item.data2d = this->choose_meseta_amount(this->pt->box_meseta_ranges, area_norm) & 0xFFFF;
item.data2d = this->choose_meseta_amount(this->pt(area)->box_meseta_ranges, this->table_index_for_area(area)) & 0xFFFF;
break;
default:
// Note: The original code does the following here:
@@ -645,27 +642,30 @@ void ItemCreator::generate_common_item_variances(uint32_t area_norm, ItemData& i
this->set_item_kill_count_if_unsealable(item);
}
void ItemCreator::generate_common_armor_or_shield_type_and_variances(char area_norm, ItemData& item) {
this->generate_common_armor_slots_and_bonuses(item);
void ItemCreator::generate_common_armor_or_shield_type_and_variances(ItemData& item, uint8_t area) {
this->generate_common_armor_slots_and_bonuses(item, episode_for_area(area));
uint8_t type = this->get_rand_from_weighted_tables_1d(this->pt->armor_shield_type_index_prob_table);
item.data1[2] = area_norm + type + this->pt->armor_or_shield_type_bias;
auto pt = this->pt(area);
uint8_t table_index = this->table_index_for_area(area);
uint8_t type = this->get_rand_from_weighted_tables_1d(pt->armor_shield_type_index_prob_table);
item.data1[2] = table_index + type + pt->armor_or_shield_type_bias;
if (item.data1[2] < 3) {
item.data1[2] = 0;
} else {
item.data1[2] -= 3;
}
this->log.info_f("Armor/shield type: max({:02X} + {:02X} + {:02X} - 3, 0) = {:02X}",
area_norm, type, this->pt->armor_or_shield_type_bias, item.data1[2]);
table_index, type, pt->armor_or_shield_type_bias, item.data1[2]);
}
void ItemCreator::generate_common_armor_slots_and_bonuses(ItemData& item) {
void ItemCreator::generate_common_armor_slots_and_bonuses(ItemData& item, Episode episode) {
if ((item.data1[0] != 0x01) || (item.data1[1] < 1) || (item.data1[1] > 2)) {
return;
}
if (item.data1[1] == 1) {
this->generate_common_armor_slot_count(item);
this->generate_common_armor_slot_count(item, episode);
}
const auto& def = this->item_parameter_table->get_armor_or_shield(item.data1[1], item.data1[2]);
@@ -673,14 +673,17 @@ void ItemCreator::generate_common_armor_slots_and_bonuses(ItemData& item) {
item.set_common_armor_evasion_bonus(def.evp_range * this->rand_float_0_1_from_crypt());
}
void ItemCreator::generate_common_armor_slot_count(ItemData& item) {
item.data1[5] = this->get_rand_from_weighted_tables_1d(this->pt->armor_slot_count_prob_table);
void ItemCreator::generate_common_armor_slot_count(ItemData& item, Episode episode) {
item.data1[5] = this->get_rand_from_weighted_tables_1d(this->pt(episode)->armor_slot_count_prob_table);
}
void ItemCreator::generate_common_tool_variances(uint32_t area_norm, ItemData& item) {
void ItemCreator::generate_common_tool_variances(ItemData& item, uint8_t area) {
item.clear();
uint8_t tool_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->tool_class_prob_table, area_norm);
auto pt = this->pt(area);
uint8_t table_index = this->table_index_for_area(area);
uint8_t tool_class = this->get_rand_from_weighted_tables_2d_vertical(pt->tool_class_prob_table, table_index);
if ((!is_v1_or_v2(this->logic_version) || (this->logic_version == Version::GC_NTE)) && (tool_class == 0x1A)) {
tool_class = 0x73;
}
@@ -705,15 +708,15 @@ void ItemCreator::generate_common_tool_variances(uint32_t area_norm, ItemData& i
}
if (item.data1[1] == 0x02) { // Tech disk
item.data1[4] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->technique_index_prob_table, area_norm);
item.data1[2] = this->generate_tech_disk_level(item.data1[4], area_norm);
item.data1[4] = this->get_rand_from_weighted_tables_2d_vertical(pt->technique_index_prob_table, table_index);
item.data1[2] = this->generate_tech_disk_level(item.data1[4], area);
this->clear_tool_item_if_invalid(item);
}
this->set_tool_item_amount_to_1(item);
}
uint8_t ItemCreator::generate_tech_disk_level(uint32_t tech_num, uint32_t area_norm) {
const auto& range = this->pt->technique_level_ranges.at(tech_num).at(area_norm);
uint8_t ItemCreator::generate_tech_disk_level(uint32_t tech_num, uint8_t area) {
const auto& range = this->pt(area)->technique_level_ranges.at(tech_num).at(this->table_index_for_area(area));
if (((range.min == 0xFF) || (range.max == 0xFF)) || (range.max < range.min)) {
return 0xFF;
} else if (range.min != range.max) {
@@ -739,20 +742,20 @@ void ItemCreator::generate_common_mag_variances(ItemData& item) {
}
}
void ItemCreator::generate_common_weapon_variances(uint8_t area_norm, ItemData& item) {
void ItemCreator::generate_common_weapon_variances(ItemData& item, uint8_t area) {
item.clear();
item.data1[0] = 0x00;
auto pt = this->pt(area);
uint8_t table_index = this->table_index_for_area(area);
parray<uint8_t, 0x0D> weapon_type_prob_table;
weapon_type_prob_table[0] = 0;
memmove(
weapon_type_prob_table.data() + 1,
this->pt->base_weapon_type_prob_table.data(),
0x0C);
memmove(weapon_type_prob_table.data() + 1, pt->base_weapon_type_prob_table.data(), 0x0C);
for (size_t z = 1; z < 13; z++) {
// Technically this should be `if (... < 0)`, but whatever
if ((area_norm + this->pt->subtype_base_table.at(z - 1)) & 0x80) {
if ((table_index + pt->subtype_base_table.at(z - 1)) & 0x80) {
weapon_type_prob_table[z] = 0;
}
}
@@ -768,33 +771,35 @@ void ItemCreator::generate_common_weapon_variances(uint8_t area_norm, ItemData&
this->log.info_f("00 chosen from subtype table; skipping item");
item.clear();
} else {
int8_t subtype_base = this->pt->subtype_base_table.at(item.data1[1] - 1);
uint8_t area_length = this->pt->subtype_area_length_table.at(item.data1[1] - 1);
int8_t subtype_base = pt->subtype_base_table.at(item.data1[1] - 1);
uint8_t area_length = pt->subtype_area_length_table.at(item.data1[1] - 1);
this->log.info_f("Subtype table yielded {:02X}; subtype base is {} with area length {}", item.data1[1], subtype_base, area_length);
if (subtype_base < 0) {
item.data1[2] = (area_norm + subtype_base) / area_length;
this->log.info_f("Resulting subtype: ({:02X} + {:02X}) / {:02X} = {:02X}", area_norm, subtype_base, area_length, item.data1[2]);
this->generate_common_weapon_grind(item, (area_norm + subtype_base) - (item.data1[2] * area_length));
item.data1[2] = (table_index + subtype_base) / area_length;
this->log.info_f("Resulting subtype: ({:02X} + {:02X}) / {:02X} = {:02X}", table_index, subtype_base, area_length, item.data1[2]);
this->generate_common_weapon_grind(item, area, (table_index + subtype_base) - (item.data1[2] * area_length));
} else {
item.data1[2] = subtype_base + (area_norm / area_length);
this->log.info_f("Resulting subtype: {:02X} + ({:02X} / {:02X}) = {:02X}", subtype_base, area_norm, area_length, item.data1[2]);
this->generate_common_weapon_grind(item, area_norm - (area_norm / area_length) * area_length);
item.data1[2] = subtype_base + (table_index / area_length);
this->log.info_f("Resulting subtype: {:02X} + ({:02X} / {:02X}) = {:02X}", subtype_base, table_index, area_length, item.data1[2]);
this->generate_common_weapon_grind(item, area, table_index - (table_index / area_length) * area_length);
}
this->generate_common_weapon_bonuses(item, area_norm);
this->generate_common_weapon_special(item, area_norm);
this->generate_common_weapon_bonuses(item, area);
this->generate_common_weapon_special(item, area);
this->set_item_unidentified_flag_if_not_challenge(item);
}
}
void ItemCreator::generate_common_weapon_grind(ItemData& item, uint8_t offset_within_subtype_range) {
void ItemCreator::generate_common_weapon_grind(ItemData& item, uint8_t area, uint8_t offset_within_subtype_range) {
if (item.data1[0] == 0) {
uint8_t offset = clamp<uint8_t>(offset_within_subtype_range, 0, 3);
item.data1[3] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->grind_prob_table, offset);
item.data1[3] = this->get_rand_from_weighted_tables_2d_vertical(this->pt(area)->grind_prob_table, offset);
this->log.info_f("Generated grind {:02X} from offset within subtype range {:02X}", item.data1[3], offset_within_subtype_range);
}
}
void ItemCreator::generate_common_weapon_special(ItemData& item, uint8_t area_norm) {
void ItemCreator::generate_common_weapon_special(ItemData& item, uint8_t area) {
auto pt = this->pt(area);
uint8_t table_index = this->table_index_for_area(area);
if (item.data1[0] != 0) {
return;
}
@@ -802,13 +807,13 @@ void ItemCreator::generate_common_weapon_special(ItemData& item, uint8_t area_no
this->log.info_f("Item is rare; skipping special generation");
return;
}
uint8_t special_mult = this->pt->special_mult.at(area_norm);
uint8_t special_mult = pt->special_mult.at(table_index);
if (special_mult == 0) {
this->log.info_f("Special multiplier is zero for area_norm {:02X}; skipping special generation", area_norm);
this->log.info_f("Special multiplier is zero for table index {:02X}; skipping special generation", table_index);
return;
}
uint8_t det = this->rand_int(100);
uint8_t prob = this->pt->special_percent.at(area_norm);
uint8_t prob = pt->special_percent.at(table_index);
if (det >= prob) {
this->log.info_f("Special not chosen ({:02X} > {:02X})", det, prob);
return;
@@ -963,9 +968,9 @@ IntT ItemCreator::get_rand_from_weighted_tables_2d_vertical(const parray<parray<
return ItemCreator::get_rand_from_weighted_tables<IntT>(tables[0].data(), offset, Y, X);
}
vector<ItemData> ItemCreator::generate_armor_shop_contents(size_t player_level) {
vector<ItemData> ItemCreator::generate_armor_shop_contents(Episode episode, size_t player_level) {
vector<ItemData> shop;
this->generate_armor_shop_armors(shop, player_level);
this->generate_armor_shop_armors(shop, episode, player_level);
this->generate_armor_shop_shields(shop, player_level);
this->generate_armor_shop_units(shop, player_level);
return shop;
@@ -1044,7 +1049,7 @@ bool ItemCreator::shop_does_not_contain_duplicate_item_by_data1_0_1_2(
return true;
}
void ItemCreator::generate_armor_shop_armors(vector<ItemData>& shop, size_t player_level) {
void ItemCreator::generate_armor_shop_armors(vector<ItemData>& shop, Episode episode, size_t player_level) {
size_t num_items;
if (player_level < 11) {
num_items = 4;
@@ -1080,7 +1085,7 @@ void ItemCreator::generate_armor_shop_armors(vector<ItemData>& shop, size_t play
}
}
this->generate_common_armor_slot_count(item);
this->generate_common_armor_slot_count(item, episode);
if (this->shop_does_not_contain_duplicate_armor(shop, item)) {
shop.emplace_back(std::move(item));
items_generated++;
@@ -1711,7 +1716,7 @@ ItemCreator::DropResult ItemCreator::on_specialized_box_item_drop(
uint16_t type = res.item.data1w[0];
res.item.clear();
res.item.data1w[0] = type;
this->generate_common_item_variances(this->normalize_area_number(area), res.item);
this->generate_common_item_variances(res.item, area);
}
return res;
}
+41 -49
View File
@@ -9,6 +9,11 @@
#include "RareItemSet.hh"
#include "StaticGameData.hh"
// This file and ItemCreator.cc are essentially a direct reverse-engineering of the item creation algorithm in PSO GC.
// Only minor changes have been made to support BB (as described in the comments in the implementation) and to support
// cross-episode quests. The latter consists mostly of delaying table_index lookups and ItemPT table lookups until much
// later than in the original implementation; the actual logic for generating item data is the same.
class ItemCreator {
public:
ItemCreator(
@@ -20,7 +25,6 @@ public:
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set,
std::shared_ptr<const ItemParameterTable> item_parameter_table,
std::shared_ptr<const ItemData::StackLimits> stack_limits,
Episode episode,
GameMode mode,
Difficulty difficulty,
uint8_t section_id,
@@ -39,7 +43,7 @@ public:
DropResult on_specialized_box_item_drop(uint8_t area, float param3, uint32_t param4, uint32_t param5, uint32_t param6);
ItemData base_item_for_specialized_box(uint32_t param4, uint32_t param5, uint32_t param6) const;
std::vector<ItemData> generate_armor_shop_contents(size_t player_level);
std::vector<ItemData> generate_armor_shop_contents(Episode episode, size_t player_level);
std::vector<ItemData> generate_tool_shop_contents(size_t player_level);
std::vector<ItemData> generate_weapon_shop_contents(size_t player_level);
@@ -56,10 +60,16 @@ public:
void set_section_id(uint8_t new_section_id);
private:
inline std::shared_ptr<const CommonItemSet::Table> pt(Episode episode) const {
return this->common_item_set->get_table(episode, this->mode, this->difficulty, this->section_id);
}
inline std::shared_ptr<const CommonItemSet::Table> pt(uint8_t area) const {
return this->pt(episode_for_area(area));
}
phosg::PrefixedLogger log;
Version logic_version;
std::shared_ptr<const ItemData::StackLimits> stack_limits;
Episode episode;
GameMode mode;
Difficulty difficulty;
uint8_t section_id;
@@ -70,7 +80,6 @@ private:
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
std::shared_ptr<const ItemParameterTable> item_parameter_table;
std::shared_ptr<const CommonItemSet> common_item_set;
std::shared_ptr<const CommonItemSet::Table> pt;
std::shared_ptr<const BattleRules> restrictions;
struct UnitResult {
@@ -102,42 +111,37 @@ private:
std::shared_ptr<RandomGenerator> rand_crypt;
bool are_rare_drops_allowed() const;
uint8_t normalize_area_number(uint8_t area) const;
DropResult on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm);
DropResult on_box_item_drop_with_area_norm(uint8_t area_norm);
uint8_t table_index_for_area(uint8_t area) const;
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 parray<CommonItemSet::Table::Range<uint16_t>, NumRanges> ranges, size_t table_index);
bool should_allow_meseta_drops() const;
ItemData check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type, uint8_t area_norm);
ItemData check_rare_specs_and_create_rare_box_item(uint8_t area_norm);
ItemData check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area_norm);
ItemData check_rare_spec_and_create_rare_enemy_item(uint32_t 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);
void generate_rare_weapon_bonuses(ItemData& item, uint32_t random_sample);
void generate_rare_weapon_bonuses(ItemData& item, Episode episode, uint32_t random_sample);
void deduplicate_weapon_bonuses(ItemData& item) const;
void set_item_kill_count_if_unsealable(ItemData& item) const;
void set_item_unidentified_flag_if_not_challenge(ItemData& item) const;
void set_tool_item_amount_to_1(ItemData& item) const;
void generate_common_item_variances(uint32_t area_norm, ItemData& item);
void generate_common_armor_slots_and_bonuses(ItemData& item);
void generate_common_armor_slot_count(ItemData& item);
void generate_common_armor_or_shield_type_and_variances(char area_norm, ItemData& item);
void generate_common_tool_variances(uint32_t area_norm, ItemData& item);
uint8_t generate_tech_disk_level(uint32_t tech_num, uint32_t area_norm);
void generate_common_item_variances(ItemData& item, uint8_t area);
void generate_common_armor_slots_and_bonuses(ItemData& item, Episode episode);
void generate_common_armor_slot_count(ItemData& item, Episode episode);
void generate_common_armor_or_shield_type_and_variances(ItemData& item, uint8_t area);
void generate_common_tool_variances(ItemData& item, uint8_t area);
uint8_t generate_tech_disk_level(uint32_t tech_num, uint8_t area);
void generate_common_mag_variances(ItemData& item);
void generate_common_weapon_variances(uint8_t area_norm, ItemData& item);
void generate_common_weapon_grind(ItemData& item, uint8_t offset_within_subtype_range);
void generate_common_weapon_bonuses(ItemData& item, uint8_t area_norm);
void generate_common_weapon_special(ItemData& item, uint8_t area_norm);
void generate_common_weapon_variances(ItemData& item, uint8_t area);
void generate_common_weapon_grind(ItemData& item, uint8_t area, uint8_t offset_within_subtype_range);
void generate_common_weapon_bonuses(ItemData& item, uint8_t area);
void generate_common_weapon_special(ItemData& item, uint8_t area);
uint8_t choose_weapon_special(uint8_t det);
void generate_unit_stars_tables();
void generate_common_unit_variances(uint8_t stars, ItemData& item);
@@ -146,28 +150,18 @@ private:
void clear_item_if_restricted(ItemData& item) const;
static size_t get_table_index_for_armor_shop(size_t player_level);
static bool shop_does_not_contain_duplicate_armor(
const std::vector<ItemData>& shop, const ItemData& item);
static bool shop_does_not_contain_duplicate_tech_disk(
const std::vector<ItemData>& shop, const ItemData& item);
static bool shop_does_not_contain_duplicate_or_too_many_similar_weapons(
const std::vector<ItemData>& shop, const ItemData& item);
static bool shop_does_not_contain_duplicate_item_by_data1_0_1_2(
const std::vector<ItemData>& shop, const ItemData& item);
void generate_armor_shop_armors(
std::vector<ItemData>& shop, size_t player_level);
void generate_armor_shop_shields(
std::vector<ItemData>& shop, size_t player_level);
void generate_armor_shop_units(
std::vector<ItemData>& shop, size_t player_level);
static bool shop_does_not_contain_duplicate_armor(const std::vector<ItemData>& shop, const ItemData& item);
static bool shop_does_not_contain_duplicate_tech_disk(const std::vector<ItemData>& shop, const ItemData& item);
static bool shop_does_not_contain_duplicate_or_too_many_similar_weapons(const std::vector<ItemData>& shop, const ItemData& item);
static bool shop_does_not_contain_duplicate_item_by_data1_0_1_2(const std::vector<ItemData>& shop, const ItemData& item);
void generate_armor_shop_armors(std::vector<ItemData>& shop, Episode episode, size_t player_level);
void generate_armor_shop_shields(std::vector<ItemData>& shop, size_t player_level);
void generate_armor_shop_units(std::vector<ItemData>& shop, size_t player_level);
static size_t get_table_index_for_tool_shop(size_t player_level);
void generate_common_tool_shop_recovery_items(
std::vector<ItemData>& shop, size_t player_level);
void generate_rare_tool_shop_recovery_items(
std::vector<ItemData>& shop, size_t player_level);
void generate_tool_shop_tech_disks(
std::vector<ItemData>& shop, size_t player_level);
void generate_common_tool_shop_recovery_items(std::vector<ItemData>& shop, size_t player_level);
void generate_rare_tool_shop_recovery_items(std::vector<ItemData>& shop, size_t player_level);
void generate_tool_shop_tech_disks(std::vector<ItemData>& shop, size_t player_level);
void generate_weapon_shop_item_grind(ItemData& item, size_t player_level);
void generate_weapon_shop_item_special(ItemData& item, size_t player_level);
@@ -175,11 +169,9 @@ private:
void generate_weapon_shop_item_bonus2(ItemData& item, size_t player_level);
template <typename IntT>
IntT get_rand_from_weighted_tables(
const IntT* tables, size_t offset, size_t num_values, size_t stride);
IntT get_rand_from_weighted_tables(const IntT* tables, size_t offset, size_t num_values, size_t stride);
template <typename IntT, size_t X>
IntT get_rand_from_weighted_tables_1d(const parray<IntT, X>& tables);
template <typename IntT, size_t X, size_t Y>
IntT get_rand_from_weighted_tables_2d_vertical(
const parray<parray<IntT, X>, Y>& tables, size_t offset);
IntT get_rand_from_weighted_tables_2d_vertical(const parray<parray<IntT, X>, Y>& tables, size_t offset);
};
+1 -2
View File
@@ -171,7 +171,7 @@ void Lobby::reset_next_item_ids() {
uint8_t Lobby::area_for_floor(Version version, uint8_t floor) const {
if (this->quest) {
return this->quest->meta.area_for_floor.at(floor);
return this->quest->meta.floor_assignments.at(floor).area;
}
auto sdt = this->require_server_state()->set_data_table(version, this->episode, this->mode, this->difficulty);
return sdt->default_area_for_floor(this->episode, floor);
@@ -224,7 +224,6 @@ void Lobby::create_item_creator(Version logic_version) {
s->tekker_adjustment_set,
s->item_parameter_table(logic_version),
s->item_stack_limits(logic_version),
this->episode,
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
this->difficulty,
effective_section_id,
-1
View File
@@ -35,7 +35,6 @@ void ProxySession::set_drop_mode(
s->tekker_adjustment_set,
s->item_parameter_table(version),
s->item_stack_limits(version),
this->lobby_episode,
(this->lobby_mode == GameMode::SOLO) ? GameMode::NORMAL : this->lobby_mode,
this->lobby_difficulty,
this->lobby_section_id,
+27 -23
View File
@@ -206,52 +206,56 @@ void VersionedQuest::assert_valid() const {
if (this->meta.language == Language::UNKNOWN) {
throw runtime_error("language is not set");
}
uint8_t num_floors = is_v1(this->meta.version) ? 0x10 : 0x12;
uint8_t num_areas;
if (is_v1(this->meta.version)) {
num_areas = 0x10;
} else if (is_v2(this->meta.version) && (this->meta.version != Version::GC_NTE)) {
num_areas = 0x12;
} else if (is_v3(this->meta.version) || (this->meta.version == Version::GC_NTE)) {
num_areas = 0x24;
} else if (is_ep3(this->meta.version)) {
num_areas = 0x00; // Floor overrides not allowed on Ep3
} else if (is_v4(this->meta.version)) {
num_areas = 0x2F;
} else {
throw std::logic_error("unhandled quest version");
}
for (size_t floor = 0; floor < num_floors; floor++) {
const auto& fa = this->meta.floor_assignments[floor];
if (fa.floor != floor) {
throw logic_error("floor assignment is inconsistent");
}
if ((fa.area != 0xFF) && (fa.area > num_areas)) {
throw runtime_error(std::format("floor assignment 0x{:02X} specifies invalid area 0x{:02X}", floor, fa.area));
}
}
switch (this->meta.episode) {
case Episode::EP1:
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if (area >= 0x12) {
throw runtime_error("Episode 1 quest specifies invalid area");
}
}
break;
case Episode::EP2:
if (is_v1_or_v2(this->meta.version)) {
throw runtime_error("v1 or v2 quest specifies Episode 2");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if ((area < 0x12) || (area >= 0x24)) {
throw runtime_error("Episode 2 quest specifies invalid area");
}
}
break;
case Episode::EP3:
if (!is_ep3(this->meta.version)) {
throw runtime_error("non-Ep3 quest specifies Episode 3");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
if (this->meta.area_for_floor[floor] != 0xFF) {
throw runtime_error("Episode 3 quest specifies floor overrides");
}
}
break;
case Episode::EP4:
if (!is_v4(this->meta.version)) {
throw runtime_error("non-v4 quest specifies Episode 4");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if (area != 0xFF && (area < 0x24 || area >= 0x2F)) {
throw runtime_error("Episode 4 quest specifies invalid floor");
}
}
break;
case Episode::NONE:
throw runtime_error("episode is not set");
default:
throw runtime_error("episode is not valid");
}
if (!this->bin_contents) {
throw runtime_error("bin file is missing");
}
+30 -11
View File
@@ -2,6 +2,21 @@
using namespace std;
phosg::JSON QuestMetadata::FloorAssignment::json() const {
return phosg::JSON::dict({
{"Floor", this->floor},
{"Area", this->area},
{"Type", this->type},
{"LayoutVariation", this->layout_var},
{"EntitiesVariation", this->entities_var},
});
}
std::string QuestMetadata::FloorAssignment::str() const {
return std::format("FloorAssignment(floor=0x{:02X}, area=0x{:02X}, type=0x{:02X}, layout_var=0x{:02X}, entities_var=0x{:02X})",
this->floor, this->area, this->type, this->layout_var, this->entities_var);
}
void QuestMetadata::apply_json_overrides(const phosg::JSON& json) {
try {
this->description_flag = json.at("DescriptionFlag").as_int();
@@ -49,9 +64,14 @@ void QuestMetadata::apply_json_overrides(const phosg::JSON& json) {
}
}
void QuestMetadata::assign_default_areas(Version version, Episode episode) {
void QuestMetadata::assign_default_floors() {
for (size_t z = 0; z < 0x12; z++) {
this->area_for_floor[z] = SetDataTableBase::default_area_for_floor(version, episode, z);
auto& fa = this->floor_assignments[z];
fa.floor = z;
fa.area = SetDataTableBase::default_area_for_floor(this->version, this->episode, z);
fa.type = 0;
fa.layout_var = 0;
fa.entities_var = 0;
}
}
@@ -141,13 +161,12 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
"quest version has different challenge difficulty (existing: {}, new: {})",
name_for_difficulty(this->challenge_difficulty), name_for_difficulty(other.challenge_difficulty)));
}
for (size_t z = 0; z < this->area_for_floor.size(); z++) {
const auto& this_fa = this->area_for_floor[z];
const auto& other_fa = other.area_for_floor[z];
if (this_fa != other_fa) {
for (size_t z = 0; z < this->floor_assignments.size(); z++) {
const auto& this_fa = this->floor_assignments[z];
const auto& other_fa = other.floor_assignments[z];
if (this_fa.area != other_fa.area) {
throw runtime_error(std::format(
"quest version has different area on floor 0x{:02X} (existing: {}, new: {})",
z, phosg::format_data_string(this->area_for_floor.data(), 0x12), phosg::format_data_string(other.area_for_floor.data(), 0x12)));
"quest version has different area on floor 0x{:02X} (existing: {}, new: {})", z, this_fa.str(), other_fa.str()));
}
}
if (this->description_flag != other.description_flag) {
@@ -201,8 +220,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
phosg::JSON QuestMetadata::json() const {
auto floors_json = phosg::JSON::list();
for (const auto& fa : this->area_for_floor) {
floors_json.emplace_back(fa);
for (const auto& fa : this->floor_assignments) {
floors_json.emplace_back(fa.json());
}
auto enemy_exp_overrides_json = phosg::JSON::dict();
for (const auto& [key, exp_override] : this->enemy_exp_overrides) {
@@ -220,7 +239,7 @@ phosg::JSON QuestMetadata::json() const {
return phosg::JSON::dict({
{"CategoryID", this->category_id},
{"Number", this->quest_number},
{"QuestNumber", this->quest_number},
{"Episode", name_for_episode(this->episode)},
{"FloorAssignments", std::move(floors_json)},
{"Joinable", this->joinable},
+17 -3
View File
@@ -26,10 +26,23 @@ struct QuestMetadata {
Language language;
// Fields that must match across all quest versions
struct FloorAssignment {
uint8_t floor = 0xFF;
uint8_t area = 0xFF;
uint8_t type = 0xFF;
uint8_t layout_var = 0xFF;
uint8_t entities_var = 0xFF;
bool operator==(const FloorAssignment& other) const = default;
bool operator!=(const FloorAssignment& other) const = default;
phosg::JSON json() const;
std::string str() const;
};
uint32_t category_id = 0xFFFFFFFF;
uint32_t quest_number = 0xFFFFFFFF;
Episode episode = Episode::NONE;
std::array<uint8_t, 0x12> area_for_floor;
std::array<FloorAssignment, 0x12> floor_assignments;
bool joinable = false;
uint8_t max_players = 4;
std::shared_ptr<const BattleRules> battle_rules;
@@ -72,6 +85,7 @@ struct QuestMetadata {
bool match(const ItemData& item) const;
uint32_t primary_identifier() const; // Raises if any of data1[0-2] are ambiguous
};
std::vector<FloorAssignment> bb_map_designate_opcodes;
std::vector<CreateItemMask> create_item_mask_entries;
// Unknown header fields. These are not used by the client, so they are not required to match across quest versions;
@@ -83,7 +97,7 @@ struct QuestMetadata {
uint16_t header_unknown_a6 = 0; // BB only
int16_t header_episode = -1; // -1 = unspecified; BB only; newserv uses script analysis instead
int16_t header_language = -1; // -1 = unspecified; DCv1 and later; newserv uses the filename instead
std::shared_ptr<parray<uint8_t, 0x94>> header_unknown_a5; // BB only; null for non-BB quests
std::shared_ptr<parray<uint8_t, 0x14>> header_unknown_a5; // BB only; null for non-BB quests
// Fields that may be different across quest versions (and are only used on VersionedQuest, not Quest)
std::string name;
@@ -99,7 +113,7 @@ struct QuestMetadata {
void apply_json_overrides(const phosg::JSON& json);
void assign_default_areas(Version version, Episode episode);
void assign_default_floors();
void assert_compatible(const QuestMetadata& other) const;
phosg::JSON json() const;
std::string areas_str() const;
+70 -32
View File
@@ -2773,7 +2773,10 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF950, "bb_p2_menu", "BB_p2_menu", {I32}, F_V4 | F_ARGS},
// Behaves exactly the same as map_designate_ex, but the arguments are
// specified as immediate values and not via registers or arg_push.
// specified as immediate values and not via registers or arg_push. Sega
// probably added this opcode so their quest authoring tools could easily
// generate the necessary header fields without doing any fancy script
// analysis.
// valueA = floor number
// valueB = area number
// valueC = type (0: use layout, 1: use offline template, 2: use online
@@ -3202,7 +3205,7 @@ std::string disassemble_quest_script(
}
}
if (fs.enemy_sets) {
uint8_t area = meta.area_for_floor.at(floor);
uint8_t area = meta.floor_assignments.at(floor).area;
for (size_t z = 0; z < fs.enemy_set_count; z++) {
// Only NPCs use script labels; no other enemies do
const auto& ene_set = fs.enemy_sets[z];
@@ -4313,11 +4316,11 @@ AssembledQuestScript assemble_quest_script(
ret.meta.header_unknown_a6 = stoul(line.text.substr(19), nullptr, 0);
} else if (line.text.starts_with(".header_unknown_a5 ")) {
std::string data = phosg::parse_data_string(line.text.substr(19));
if (data.size() != 0x94) {
throw std::runtime_error(".header_unknown_a5 directive must specify 0x94 bytes of data");
if (data.size() != 0x14) {
throw std::runtime_error(".header_unknown_a5 directive must specify 0x14 bytes of data");
}
ret.meta.header_unknown_a5 = std::make_shared<parray<uint8_t, 0x94>>();
for (size_t z = 0; z < 0x94; z++) {
ret.meta.header_unknown_a5 = std::make_shared<parray<uint8_t, 0x14>>();
for (size_t z = 0; z < 0x14; z++) {
ret.meta.header_unknown_a5->at(z) = static_cast<uint8_t>(data[z]);
}
}
@@ -4491,6 +4494,7 @@ AssembledQuestScript assemble_quest_script(
bool version_has_args = F_HAS_ARGS & v_flag(ret.meta.version);
const auto& opcodes = opcodes_by_name_for_version(ret.meta.version);
phosg::StringWriter code_w;
std::vector<size_t> bb_map_designate_args_offsets;
for (const auto& line : lines) {
wrap_exceptions_with_line_ref(line, [&]() -> void {
if (line.text.empty()) {
@@ -4569,6 +4573,15 @@ AssembledQuestScript assemble_quest_script(
}
}
// Hack: Collect bb_map_designate offsets during assembly, to generate
// the necessary server-side header field afterward
if (opcode_def->opcode == 0xF951) {
if (bb_map_designate_args_offsets.size() >= 0x10) {
throw std::runtime_error("bb_map_designate was used too many times; up to 16 uses are allowed");
}
bb_map_designate_args_offsets.emplace_back(code_w.size());
}
if (opcode_def->args.empty()) {
if (line_tokens.size() > 1) {
throw runtime_error(std::format("arguments not allowed for {}", opcode_def->name));
@@ -4968,6 +4981,17 @@ AssembledQuestScript assemble_quest_script(
} else {
header.unknown_a5.clear(0);
}
phosg::StringReader code_r(code_w.str());
for (size_t z = 0; z < bb_map_designate_args_offsets.size(); z++) {
code_r.go(bb_map_designate_args_offsets[z]);
auto& fa = header.floor_assignments[z];
fa.floor = code_r.get_u8();
fa.area = code_r.get_u8();
fa.type = code_r.get_u8();
fa.layout_var = code_r.get_u8();
fa.entities_var = code_r.get_u8();
fa.unused.clear(0);
}
for (size_t z = 0; z < ret.meta.create_item_mask_entries.size(); z++) {
header.create_item_mask_entries[z] = ret.meta.create_item_mask_entries[z];
}
@@ -4986,10 +5010,11 @@ AssembledQuestScript assemble_quest_script(
void populate_quest_metadata_from_script(
QuestMetadata& meta, const void* data, size_t size, Version version, Language language) {
meta.version = version;
meta.language = language;
phosg::StringReader r(data, size);
switch (version) {
switch (meta.version) {
case Version::DC_NTE: {
const auto& header = r.get<PSOQuestHeaderDCNTE>();
meta.header_unknown_a1 = header.unknown_a1;
@@ -5109,11 +5134,10 @@ void populate_quest_metadata_from_script(
// Quests saved with Qedit may not have the full header, so only parse
// the full header if the code and function table offsets don't point to
// space within it
if ((header.text_offset >= sizeof(PSOQuestHeaderBB)) &&
(header.label_table_offset >= sizeof(PSOQuestHeaderBB))) {
if ((header.text_offset >= sizeof(PSOQuestHeaderBB)) && (header.label_table_offset >= sizeof(PSOQuestHeaderBB))) {
r.go(0);
const auto& header = r.get<PSOQuestHeaderBB>();
meta.header_unknown_a5 = std::make_shared<parray<uint8_t, 0x94>>(header.unknown_a5);
meta.header_unknown_a5 = std::make_shared<parray<uint8_t, 0x14>>(header.unknown_a5);
for (size_t z = 0; z < header.create_item_mask_entries.size(); z++) {
const auto& item = header.create_item_mask_entries[z];
if (!item.is_valid()) {
@@ -5130,8 +5154,8 @@ void populate_quest_metadata_from_script(
throw logic_error("invalid quest version");
}
const auto& opcodes = opcodes_for_version(version);
bool version_has_args = F_HAS_ARGS & v_flag(version);
const auto& opcodes = opcodes_for_version(meta.version);
bool version_has_args = F_HAS_ARGS & v_flag(meta.version);
struct RegisterFile {
// All registers are initially zero
@@ -5200,7 +5224,7 @@ void populate_quest_metadata_from_script(
deque<uint32_t> pending_fn_offsets{get_label_offset(0)};
unordered_set<uint32_t> done_fn_offsets;
shared_ptr<BattleRules> battle_rules;
meta.assign_default_areas(version, meta.episode);
meta.assign_default_floors();
while (!pending_fn_offsets.empty()) {
uint32_t start_offset = pending_fn_offsets.front();
pending_fn_offsets.pop_front();
@@ -5276,7 +5300,7 @@ void populate_quest_metadata_from_script(
}
case 0x000A: { // leta (v1/v2); letb (v3/v4)
uint8_t a = r.get_u8();
if (is_v1_or_v2(version)) { // leta
if (is_v1_or_v2(meta.version)) { // leta
regs.invalidate(a);
r.skip(1);
} else { // letb
@@ -5387,7 +5411,7 @@ void populate_quest_metadata_from_script(
break;
case 0x004E: // arg_pushs
args_list.emplace_back(r.where());
if (uses_utf16(version)) {
if (uses_utf16(meta.version)) {
while (r.get_u16l()) {
}
} else {
@@ -5412,9 +5436,14 @@ void populate_quest_metadata_from_script(
}
case 0x00C4: { // map_designate
uint32_t floor = regs.get(r.get_u8());
if (floor < meta.area_for_floor.size()) {
meta.area_for_floor[floor] = floor;
uint8_t base_reg = r.get_u8();
uint32_t floor = regs.get(base_reg);
if (floor < meta.floor_assignments.size()) {
auto& fa = meta.floor_assignments[floor];
fa.area = floor;
fa.type = regs.get(base_reg + 1);
fa.layout_var = regs.get(base_reg + 2);
fa.entities_var = 0;
}
// phosg::fwrite_fmt(stderr, ">>> Trace: map_designate fa[{}]={}\n", floor, floor);
break;
@@ -5423,9 +5452,12 @@ void populate_quest_metadata_from_script(
case 0xF80D: { // map_designate_ex
uint8_t base_reg = r.get_u8();
uint32_t floor = regs.get(base_reg);
uint32_t area = regs.get(base_reg + 1);
if (floor < meta.area_for_floor.size()) {
meta.area_for_floor[floor] = area;
if (floor < meta.floor_assignments.size()) {
auto& fa = meta.floor_assignments[floor];
fa.area = regs.get(base_reg + 1);
fa.type = regs.get(base_reg + 2);
fa.layout_var = regs.get(base_reg + 3);
fa.entities_var = regs.get(base_reg + 4);
}
// phosg::fwrite_fmt(stderr, ">>> Trace: map_designate_ex fa[{}]={}\n", floor, area);
break;
@@ -5626,9 +5658,9 @@ void populate_quest_metadata_from_script(
break;
case 0xF88C: // get_game_version
if (is_v1_or_v2(version)) {
if (is_v1_or_v2(meta.version)) {
regs.set(r.get_u8(), 2);
} else if (is_gc(version)) {
} else if (is_gc(meta.version)) {
regs.set(r.get_u8(), 3);
} else {
regs.set(r.get_u8(), 4);
@@ -5652,7 +5684,7 @@ void populate_quest_metadata_from_script(
meta.episode = Episode::EP2;
break;
case 2:
if (!is_v4(version)) {
if (!is_v4(meta.version)) {
throw runtime_error("invalid argument to set_episode");
}
meta.episode = Episode::EP4;
@@ -5660,8 +5692,7 @@ void populate_quest_metadata_from_script(
default:
throw runtime_error("invalid argument to set_episode");
}
meta.assign_default_areas(version, meta.episode);
// phosg::fwrite_fmt(stderr, ">>> Trace: meta.episode = {}\n", name_for_episode(meta.episode));
meta.assign_default_floors();
break;
case 0xF932: // set_episode2
@@ -5670,13 +5701,20 @@ void populate_quest_metadata_from_script(
case 0xF951: { // bb_map_designate
uint8_t floor = r.get_u8();
if (floor < meta.area_for_floor.size()) {
meta.area_for_floor.at(floor) = r.get_u8();
r.skip(3); // entities_list_type, vars.layout, vars.entities
if (floor < meta.floor_assignments.size()) {
auto& fa = meta.floor_assignments[floor];
if (fa.floor != floor) {
throw std::logic_error("FloorAssignment\'s floor field is incorrect");
}
fa.area = r.get_u8();
fa.type = r.get_u8();
fa.layout_var = r.get_u8();
fa.entities_var = r.get_u8();
meta.bb_map_designate_opcodes.emplace_back(fa);
// phosg::fwrite_fmt(stderr, ">>> Trace: bb_map_designate fa[{}]={}\n", floor, meta.area_for_floor.at(floor));
} else {
r.skip(4); // area, entities_list_type, vars.layout, vars.entities
// phosg::fwrite_fmt(stderr, ">>> Trace: bb_map_designate fa[{}]=(ignored)\n", floor);
// Maybe we should throw in this case?
r.skip(4);
}
break;
}
@@ -5735,7 +5773,7 @@ void populate_quest_metadata_from_script(
break;
case Type::CSTRING:
if (!use_args) {
if (uses_utf16(version)) {
if (uses_utf16(meta.version)) {
while (r.get_u16l()) {
}
} else {
+14 -2
View File
@@ -114,12 +114,24 @@ struct PSOQuestHeaderBBBase {
} __packed_ws__(PSOQuestHeaderBBBase, 0x0398);
struct PSOQuestHeaderBB : PSOQuestHeaderBBBase {
/* 0398 */ parray<uint8_t, 0x94> unknown_a5;
struct FloorAssignment {
// These fields match the bb_map_designate arguments (see QuestScript.cc).
// Unused AreaAssignment structures should have all fields set to 0xFF.
uint8_t floor = 0xFF;
uint8_t area = 0xFF;
uint8_t type = 0xFF;
uint8_t layout_var = 0xFF;
uint8_t entities_var = 0xFF;
parray<uint8_t, 3> unused = 0xFF;
} __packed_ws__(FloorAssignment, 8);
/* 0398 */ parray<uint8_t, 0x14> unknown_a5;
/* 03AC */ parray<FloorAssignment, 0x10> floor_assignments;
/* 042C */ parray<CreateItemMaskEntry, 0x40> create_item_mask_entries;
/* 122C */
} __packed_ws__(PSOQuestHeaderBB, 0x122C);
void check_opcode_definitions();
void check_quest_opcode_definitions();
Episode episode_for_quest_episode_number(uint8_t episode_number);
+30 -28
View File
@@ -1805,8 +1805,7 @@ static asio::awaitable<void> on_switch_state_changed(shared_ptr<Client> c, Subco
const auto& obj_st = l->map_state->object_state_for_index(
c->version(), cmd.switch_flag_floor, cmd.header.entity_id - 0x4000);
auto s = c->require_server_state();
auto sdt = s->set_data_table(c->version(), l->episode, l->mode, l->difficulty);
uint8_t area = sdt->default_area_for_floor(l->episode, c->floor);
uint8_t area = l->area_for_floor(c->version(), c->floor);
auto type_name = obj_st->type_name(c->version(), area);
send_text_message_fmt(c, "$C5K-{:03X} A {}", obj_st->k_id, type_name);
}
@@ -2534,9 +2533,11 @@ static asio::awaitable<void> on_open_shop_bb_or_ep3_battle_subs(shared_ptr<Clien
case 1:
c->bb_shop_contents[1] = l->item_creator->generate_weapon_shop_contents(level);
break;
case 2:
c->bb_shop_contents[2] = l->item_creator->generate_armor_shop_contents(level);
case 2: {
Episode episode = episode_for_area(l->area_for_floor(c->version(), 0));
c->bb_shop_contents[2] = l->item_creator->generate_armor_shop_contents(episode, level);
break;
}
default:
throw runtime_error("invalid shop type");
}
@@ -2947,7 +2948,8 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
// mode, so that we can correctly mark enemies and objects as having dropped
// their items in persistent games.
G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(msg.data, msg.size);
auto rec = reconcile_drop_request_with_map(c, cmd, l->episode, l->difficulty, l->event, l->map_state, true);
Episode episode = episode_for_area(l->area_for_floor(c->version(), cmd.floor));
auto rec = reconcile_drop_request_with_map(c, cmd, episode, l->difficulty, l->event, l->map_state, true);
ServerDropMode drop_mode = l->drop_mode;
switch (drop_mode) {
@@ -3131,9 +3133,8 @@ static asio::awaitable<void> on_set_quest_flag(shared_ptr<Client> c, SubcommandM
if (l->drop_mode != ServerDropMode::DISABLED) {
EnemyType boss_enemy_type = EnemyType::NONE;
bool is_ep2 = (l->episode == Episode::EP2);
uint8_t area = l->area_for_floor(c->version(), c->floor);
if ((l->episode == Episode::EP1) && (area == 0x0E)) {
if (area == 0x0E) {
// On Normal, Dark Falz does not have a third phase, so send the drop
// request after the end of the second phase. On all other difficulty
// levels, send it after the third phase.
@@ -3142,21 +3143,19 @@ static asio::awaitable<void> on_set_quest_flag(shared_ptr<Client> c, SubcommandM
} else if ((difficulty != Difficulty::NORMAL) && (flag_num == 0x0037)) {
boss_enemy_type = EnemyType::DARK_FALZ_3;
}
} else if (is_ep2 && (flag_num == 0x0057) && (area == 0x0D)) {
} else if ((flag_num == 0x0057) && (area == 0x1F)) {
boss_enemy_type = EnemyType::OLGA_FLOW_2;
}
if (boss_enemy_type != EnemyType::NONE) {
l->log.info_f("Creating item from final boss ({})", phosg::name_for_enum(boss_enemy_type));
uint16_t enemy_index = 0xFFFF;
uint8_t enemy_floor = 0xFF;
try {
auto ene_st = l->map_state->enemy_state_for_floor_type(c->version(), c->floor, boss_enemy_type);
if (ene_st->alias_target_ene_st) {
ene_st = ene_st->alias_target_ene_st;
}
enemy_index = l->map_state->index_for_enemy_state(c->version(), ene_st);
enemy_floor = ene_st->super_ene->floor;
if (c->floor != ene_st->super_ene->floor) {
l->log.warning_f("Floor {:02X} from client does not match entity\'s expected floor {:02X}",
c->floor, ene_st->super_ene->floor);
@@ -3184,7 +3183,6 @@ static asio::awaitable<void> on_set_quest_flag(shared_ptr<Client> c, SubcommandM
}
auto s = c->require_server_state();
auto sdt = s->set_data_table(c->version(), l->episode, l->mode, l->difficulty);
G_StandardDropItemRequest_PC_V3_BB_6x60 drop_req = {
{
{0x60, 0x06, 0x0000},
@@ -3195,8 +3193,7 @@ static asio::awaitable<void> on_set_quest_flag(shared_ptr<Client> c, SubcommandM
2,
0,
},
sdt->default_area_for_floor(l->episode, enemy_floor),
{}};
area, {}};
SubcommandMessage drop_msg{0x62, l->leader_id, &drop_req, sizeof(drop_req)};
co_await on_entity_drop_item_request(c, drop_msg);
}
@@ -3768,9 +3765,6 @@ static asio::awaitable<void> on_set_entity_pos_and_angle_6x17(shared_ptr<Client>
// 6x17 is used to transport players to the other part of the Vol Opt boss
// arena, so phase 2 can begin. We only allow 6x17 in the Monitor Room (Vol
// Opt arena).
if (l->episode != Episode::EP1) {
throw runtime_error("client sent 6x17 command in non-Ep1 game");
}
if (l->area_for_floor(c->version(), c->floor) != 0x0D) {
throw runtime_error("client sent 6x17 command in area other than Vol Opt");
}
@@ -3961,7 +3955,7 @@ static uint32_t base_exp_for_enemy_type(
episode_order[2] = Episode::EP4;
} else if (current_episode == Episode::EP4) {
uint8_t area = quest
? quest->meta.area_for_floor.at(floor)
? quest->meta.floor_assignments.at(floor).area
: SetDataTableBase::default_area_for_floor(Version::BB_V4, Episode::EP4, floor);
if (area <= 0x28) { // Crater
episode_order[1] = Episode::EP1;
@@ -4034,15 +4028,16 @@ static asio::awaitable<void> on_steal_exp_bb(shared_ptr<Client> c, SubcommandMes
co_return;
}
auto type = ene_st->type(c->version(), l->episode, l->difficulty, l->event);
auto episode = episode_for_area(l->area_for_floor(c->version(), ene_st->super_ene->floor));
auto type = ene_st->type(c->version(), episode, l->difficulty, l->event);
uint32_t enemy_exp = base_exp_for_enemy_type(
s->battle_params, l->quest, type, l->episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
// Note: The original code checks if special.type is 9, 10, or 11, and skips
// applying the android bonus if so. We don't do anything for those special
// types, so we don't check for that here.
float percent = special.amount + ((l->difficulty == Difficulty::ULTIMATE) && char_class_is_android(p->disp.visual.char_class) ? 30 : 0);
float ep2_factor = (l->episode == Episode::EP2) ? 1.3 : 1.0;
float ep2_factor = (episode == Episode::EP2) ? 1.3 : 1.0;
uint32_t stolen_exp = max<uint32_t>(min<uint32_t>((enemy_exp * percent * ep2_factor) / 100.0f, (static_cast<size_t>(l->difficulty) + 1) * 20), 1);
if (c->check_flag(Client::Flag::DEBUG_ENABLED)) {
c->log.info_f("Stolen EXP from E-{:03X} with enemy_exp={} percent={:g} stolen_exp={}",
@@ -4084,9 +4079,10 @@ static asio::awaitable<void> on_enemy_exp_request_bb(shared_ptr<Client> c, Subco
}
ene_st->server_flags |= MapState::EnemyState::Flag::EXP_GIVEN;
auto type = ene_st->type(c->version(), l->episode, l->difficulty, l->event);
auto episode = episode_for_area(l->area_for_floor(c->version(), ene_st->super_ene->floor));
auto type = ene_st->type(c->version(), episode, l->difficulty, l->event);
double base_exp = base_exp_for_enemy_type(
s->battle_params, l->quest, type, l->episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
l->log.info_f("Base EXP for this enemy ({}) is {:g}", phosg::name_for_enum(type), base_exp);
for (size_t client_id = 0; client_id < 4; client_id++) {
@@ -4130,12 +4126,11 @@ static asio::awaitable<void> on_enemy_exp_request_bb(shared_ptr<Client> c, Subco
// something far lazier instead: they just stuck an if statement in the
// client's EXP request function. We, unfortunately, have to do the
// same thing here.
bool is_ep2 = (l->episode == Episode::EP2);
uint32_t player_exp = base_exp *
rate_factor *
l->base_exp_multiplier *
l->challenge_exp_multiplier *
(is_ep2 ? 1.3 : 1.0);
((episode == Episode::EP2) ? 1.3 : 1.0);
l->log.info_f("Client in slot {} receives {} EXP", client_id, player_exp);
if (lc->check_flag(Client::Flag::DEBUG_ENABLED)) {
send_text_message_fmt(lc, "$C5+{} E-{:03X} {}", player_exp, ene_st->e_id, phosg::name_for_enum(type));
@@ -4183,16 +4178,23 @@ static asio::awaitable<void> on_adjust_player_meseta_bb(shared_ptr<Client> c, Su
}
static void assert_quest_item_create_allowed(shared_ptr<const Lobby> l, const ItemData& item) {
// We always enforce these restrictions, even if the client has cheat mode
// enabled or has debug enabled. If the client can cheat, there are much
// easier ways to create items (e.g. the $item chat command) than spoofing
// these quest item creation commands, so they should just do that instead.
// We always enforce these restrictions if the quest has any restrictions
// defined, even if the client has cheat mode enabled or has debug enabled.
// If the client can cheat, there are much easier ways to create items (e.g.
// the $item chat command) than spoofing these quest item creation commands,
// so they should just do that instead.
if (!l->quest) {
throw std::runtime_error("cannot create quest reward item with no quest loaded");
}
if (l->quest->meta.create_item_mask_entries.empty()) {
l->log.warning_f("Player created quest item {}, but the loaded quest ({}) has no item creation masks", item.hex(), l->quest->meta.name);
return;
}
for (const auto& mask : l->quest->meta.create_item_mask_entries) {
if (mask.match(item)) {
l->log.info_f("Player created quest item {} which matches create item mask {}", item.hex(), mask.str());
return;
}
}
+12
View File
@@ -58,6 +58,18 @@ Episode episode_for_token_name(const string& name) {
throw runtime_error("unknown episode");
}
Episode episode_for_area(uint8_t area) {
if (area < 0x12) {
return Episode::EP1;
} else if (area < 0x24) {
return Episode::EP2;
} else if (area < 0x2F) {
return Episode::EP4;
} else {
throw std::runtime_error("invalid area number");
}
}
const char* abbreviation_for_episode(Episode ep) {
switch (ep) {
case Episode::NONE:
+1
View File
@@ -42,6 +42,7 @@ const char* name_for_episode(Episode ep);
const char* token_name_for_episode(Episode ep);
const char* abbreviation_for_episode(Episode ep);
Episode episode_for_token_name(const std::string& name);
Episode episode_for_area(uint8_t area);
enum class GameMode {
NORMAL = 0,