add support for cross-episode quests
This commit is contained in:
+261
-256
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user