diff --git a/src/Client.cc b/src/Client.cc index 391a5e3f..fce9d3a5 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -432,6 +432,9 @@ bool Client::can_play_quest( if (!q->has_version_any_language(this->version())) { return false; } + if (num_players >= q->max_players) { + return false; + } return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present); } diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 7877e580..1526bfb9 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -5552,10 +5552,10 @@ struct G_RevivePlayer_V3_BB_6xA1 { // server on BB) struct G_SpecializableItemDropRequest_6xA2 : G_StandardDropItemRequest_PC_V3_BB_6x60 { - /* 18 */ le_float param3 = 0.0f; - /* 1C */ le_uint32_t param4 = 0; - /* 20 */ le_uint32_t param5 = 0; - /* 24 */ le_uint32_t param6 = 0; + /* 18 */ le_float fparam3 = 0.0f; + /* 1C */ le_int32_t iparam4 = 0; + /* 20 */ le_int32_t iparam5 = 0; + /* 24 */ le_int32_t iparam6 = 0; /* 28 */ } __packed_ws__(G_SpecializableItemDropRequest_6xA2, 0x28); diff --git a/src/Main.cc b/src/Main.cc index 26ed203c..693e95ab 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1441,7 +1441,11 @@ Action a_encode_qst( } catch (const phosg::cannot_open_file&) { } - auto vq = make_shared(0, 0, version, 0, bin_data, dat_data, nullptr, pvr_data); + auto vq = make_shared(); + vq->version = version; + vq->bin_contents = bin_data; + vq->dat_contents = dat_data; + vq->pvr_contents = pvr_data; if (download) { vq = vq->create_download_quest(); } @@ -1558,19 +1562,20 @@ Action a_assemble_quest_script( ? phosg::dirname(input_filename) : "."; - string result = assemble_quest_script( + auto result = assemble_quest_script( text, {include_dir, "system/quests/includes"}, {include_dir, "system/quests/includes", "system/client-functions/System"}); + string result_data = std::move(result.data); bool compress = !args.get("decompressed"); if (compress) { if (args.get("optimal")) { - result = prs_compress_optimal(result); + result_data = prs_compress_optimal(result_data); } else { - result = prs_compress(result); + result_data = prs_compress(result_data); } } - write_output_data(args, result.data(), result.size(), compress ? "bin" : "bind"); + write_output_data(args, result_data.data(), result_data.size(), compress ? "bin" : "bind"); }); Action a_assemble_all_patches( diff --git a/src/Map.cc b/src/Map.cc index ba9c67a2..4807eb36 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -1006,12 +1006,12 @@ string MapFile::ObjectSetEntry::str() const { this->angle.x.load(), this->angle.y.load(), this->angle.z.load(), - this->param1.load(), - this->param2.load(), - this->param3.load(), - this->param4.load(), - this->param5.load(), - this->param6.load(), + this->fparam1.load(), + this->fparam2.load(), + this->fparam3.load(), + this->iparam4.load(), + this->iparam5.load(), + this->iparam6.load(), this->unused.load()); } @@ -1021,12 +1021,12 @@ uint64_t MapFile::ObjectSetEntry::semantic_hash(uint8_t floor) const { ret = phosg::fnv1a64(&this->room, sizeof(this->room), ret); ret = phosg::fnv1a64(&this->pos, sizeof(this->pos), ret); ret = phosg::fnv1a64(&this->angle, sizeof(this->angle), ret); - ret = phosg::fnv1a64(&this->param1, sizeof(this->param1), ret); - ret = phosg::fnv1a64(&this->param2, sizeof(this->param2), ret); - ret = phosg::fnv1a64(&this->param3, sizeof(this->param3), ret); - ret = phosg::fnv1a64(&this->param4, sizeof(this->param4), ret); - ret = phosg::fnv1a64(&this->param5, sizeof(this->param5), ret); - ret = phosg::fnv1a64(&this->param6, sizeof(this->param6), ret); + ret = phosg::fnv1a64(&this->fparam1, sizeof(this->fparam1), ret); + ret = phosg::fnv1a64(&this->fparam2, sizeof(this->fparam2), ret); + ret = phosg::fnv1a64(&this->fparam3, sizeof(this->fparam3), ret); + ret = phosg::fnv1a64(&this->iparam4, sizeof(this->iparam4), ret); + ret = phosg::fnv1a64(&this->iparam5, sizeof(this->iparam5), ret); + ret = phosg::fnv1a64(&this->iparam6, sizeof(this->iparam6), ret); ret = phosg::fnv1a64(&floor, sizeof(floor), ret); return ret; } @@ -1055,8 +1055,8 @@ string MapFile::EnemySetEntry::str() const { this->fparam3.load(), this->fparam4.load(), this->fparam5.load(), - this->iparam1.load(), - this->iparam2.load(), + this->iparam6.load(), + this->iparam7.load(), this->unused.load()); } @@ -1073,8 +1073,8 @@ uint64_t MapFile::EnemySetEntry::semantic_hash(uint8_t floor) const { ret = phosg::fnv1a64(&this->fparam3, sizeof(this->fparam3), ret); ret = phosg::fnv1a64(&this->fparam4, sizeof(this->fparam4), ret); ret = phosg::fnv1a64(&this->fparam5, sizeof(this->fparam5), ret); - ret = phosg::fnv1a64(&this->iparam1, sizeof(this->iparam1), ret); - ret = phosg::fnv1a64(&this->iparam2, sizeof(this->iparam2), ret); + ret = phosg::fnv1a64(&this->iparam6, sizeof(this->iparam6), ret); + ret = phosg::fnv1a64(&this->iparam7, sizeof(this->iparam7), ret); ret = phosg::fnv1a64(&floor, sizeof(floor), ret); return ret; } @@ -1132,8 +1132,8 @@ string MapFile::RandomEnemyDefinition::str() const { this->fparam3.load(), this->fparam4.load(), this->fparam5.load(), - this->iparam1.load(), - this->iparam2.load(), + this->iparam6.load(), + this->iparam7.load(), this->entry_num.load(), this->min_children.load(), this->max_children.load()); @@ -1496,8 +1496,8 @@ std::shared_ptr MapFile::materialize_random_sections(uint32_t random_se e.fparam3 = def.fparam3; e.fparam4 = def.fparam4; e.fparam5 = def.fparam5; - e.iparam1 = def.iparam1; - e.iparam2 = def.iparam2; + e.iparam6 = def.iparam6; + e.iparam7 = def.iparam7; e.num_children = random_state.rand_int_biased(def.min_children, def.max_children); } else { throw runtime_error("random enemy definition not found"); @@ -1929,7 +1929,7 @@ void SuperMap::link_object_version(std::shared_ptr obj, Version version, entities.object_for_floor_room_and_group.emplace(k, obj); // Add to door index - uint32_t base_switch_flag = set_entry->param4; + uint32_t base_switch_flag = set_entry->iparam4; uint32_t num_switch_flags = 0; switch (set_entry->base_type) { case 0x01AB: // TODoorFourLightRuins @@ -1937,11 +1937,11 @@ void SuperMap::link_object_version(std::shared_ptr obj, Version version, case 0x0202: // TObjDoorJung case 0x0221: // TODoorFourLightSeabed case 0x0222: // TODoorFourLightSeabedU - num_switch_flags = set_entry->param5; + num_switch_flags = set_entry->iparam5; break; case 0x00C1: // TODoorCave01 case 0x0100: // TODoorMachine01 - num_switch_flags = (4 - clamp(set_entry->param5, 0, 4)); + num_switch_flags = (4 - clamp(set_entry->iparam5, 0, 4)); break; case 0x014A: // TODoorAncient08 num_switch_flags = 4; @@ -2078,13 +2078,13 @@ shared_ptr SuperMap::add_enemy_and_children( add(EnemyType::NON_ENEMY_NPC); break; case 0x0040: { // TObjEneMoja - bool is_rare = (set_entry->iparam1.load() >= 1); + bool is_rare = (set_entry->iparam6.load() >= 1); add(EnemyType::HILDEBEAR, is_rare, is_rare); break; } case 0x0041: { // TObjEneLappy - bool is_rare_v123 = (set_entry->iparam1 != 0); - bool is_rare_bb = (set_entry->iparam1 & 1); + bool is_rare_v123 = (set_entry->iparam6 != 0); + bool is_rare_bb = (set_entry->iparam6 & 1); switch (this->episode) { case Episode::EP1: case Episode::EP2: @@ -2108,7 +2108,7 @@ shared_ptr SuperMap::add_enemy_and_children( break; case 0x0044: { // TObjEneBeast static const EnemyType types[3] = {EnemyType::BOOMA, EnemyType::GOBOOMA, EnemyType::GIGOBOOMA}; - add(types[clamp(set_entry->iparam1, 0, 2)]); + add(types[clamp(set_entry->iparam6, 0, 2)]); break; } case 0x0060: // TObjGrass @@ -2122,13 +2122,13 @@ shared_ptr SuperMap::add_enemy_and_children( break; case 0x0063: { // TObjEneShark static const EnemyType types[3] = {EnemyType::EVIL_SHARK, EnemyType::PAL_SHARK, EnemyType::GUIL_SHARK}; - add(types[clamp(set_entry->iparam1, 0, 2)]); + add(types[clamp(set_entry->iparam6, 0, 2)]); break; } case 0x0064: { // TObjEneSlime // Unlike all other versions, BB doesn't have a way to force slimes to be // rare via constructor args - bool is_rare_v123 = (set_entry->iparam2 & 1); + bool is_rare_v123 = (set_entry->iparam7 & 1); default_num_children = -1; // Skip adding children later (because we do it here) size_t num_children = set_entry->num_children ? set_entry->num_children.load() : 4; for (size_t z = 0; z < num_children + 1; z++) { @@ -2146,7 +2146,7 @@ shared_ptr SuperMap::add_enemy_and_children( add(EnemyType::MIGIUM); break; case 0x0080: // TObjEneDubchik - add((set_entry->iparam1 != 0) ? EnemyType::GILLCHIC : EnemyType::DUBCHIC); + add((set_entry->iparam6 != 0) ? EnemyType::GILLCHIC : EnemyType::DUBCHIC); break; case 0x0081: // TObjEneGyaranzo add(EnemyType::GARANZ); @@ -2192,7 +2192,7 @@ shared_ptr SuperMap::add_enemy_and_children( break; case 0x00A6: { // TObjEneDimedian static const EnemyType types[3] = {EnemyType::DIMENIAN, EnemyType::LA_DIMENIAN, EnemyType::SO_DIMENIAN}; - add(types[clamp(set_entry->iparam1, 0, 2)]); + add(types[clamp(set_entry->iparam6, 0, 2)]); break; } case 0x00A7: // TObjEneBalClawBody @@ -2277,14 +2277,14 @@ shared_ptr SuperMap::add_enemy_and_children( default_num_children = 5; break; case 0x00D4: // TObjEneMe3StelthReal - add((set_entry->iparam1 > 0) ? EnemyType::SINOW_SPIGELL : EnemyType::SINOW_BERILL); + add((set_entry->iparam6 > 0) ? EnemyType::SINOW_SPIGELL : EnemyType::SINOW_BERILL); default_num_children = 4; break; case 0x00D5: // TObjEneMerillLia - add((set_entry->iparam1 > 0) ? EnemyType::MERILTAS : EnemyType::MERILLIA); + add((set_entry->iparam6 > 0) ? EnemyType::MERILTAS : EnemyType::MERILLIA); break; case 0x00D6: { // TObjEneBm9Mericarol - switch (set_entry->iparam1) { + switch (set_entry->iparam6) { case 0: add(EnemyType::MERICAROL); break; @@ -2300,7 +2300,7 @@ shared_ptr SuperMap::add_enemy_and_children( break; } case 0x00D7: // TObjEneBm5GibonU - add((set_entry->iparam1 > 0) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); + add((set_entry->iparam6 > 0) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); break; case 0x00D8: // TObjEneGibbles add(EnemyType::GIBBLES); @@ -2318,7 +2318,7 @@ shared_ptr SuperMap::add_enemy_and_children( add(EnemyType::DELBITER); break; case 0x00DD: // TObjEneDolmOlm - add((set_entry->iparam1 > 0) ? EnemyType::DOLMDARL : EnemyType::DOLMOLM); + add((set_entry->iparam6 > 0) ? EnemyType::DOLMDARL : EnemyType::DOLMOLM); break; case 0x00DE: // TObjEneMorfos add(EnemyType::MORFOS); @@ -2333,7 +2333,7 @@ shared_ptr SuperMap::add_enemy_and_children( default_num_children = 4; child_type = EnemyType::EPSIGARD; } else { - add((set_entry->iparam1 > 0) ? EnemyType::SINOW_ZELE : EnemyType::SINOW_ZOA); + add((set_entry->iparam6 > 0) ? EnemyType::SINOW_ZELE : EnemyType::SINOW_ZOA); } break; case 0x00E1: // TObjEneIllGill @@ -2350,7 +2350,7 @@ shared_ptr SuperMap::add_enemy_and_children( } break; case 0x0112: { - bool is_rare = (set_entry->iparam1 & 1); + bool is_rare = (set_entry->iparam6 & 1); add(EnemyType::MERISSA_A, is_rare, is_rare); break; } @@ -2358,29 +2358,29 @@ shared_ptr SuperMap::add_enemy_and_children( add(EnemyType::GIRTABLULU); break; case 0x0114: { - bool is_rare = (set_entry->iparam1 & 1); + bool is_rare = (set_entry->iparam6 & 1); add((floor > 0x05) ? EnemyType::ZU_DESERT : EnemyType::ZU_CRATER, is_rare, is_rare); break; } case 0x0115: { static const EnemyType types[3] = {EnemyType::BOOTA, EnemyType::ZE_BOOTA, EnemyType::BA_BOOTA}; - add(types[clamp(set_entry->iparam1, 0, 2)]); + add(types[clamp(set_entry->iparam6, 0, 2)]); break; } case 0x0116: { - bool is_rare = (set_entry->iparam1 & 1); + bool is_rare = (set_entry->iparam6 & 1); add(EnemyType::DORPHON, is_rare, is_rare); break; } case 0x0117: { static const EnemyType types[3] = {EnemyType::GORAN, EnemyType::PYRO_GORAN, EnemyType::GORAN_DETONATOR}; - add(types[clamp(set_entry->iparam1, 0, 2)]); + add(types[clamp(set_entry->iparam6, 0, 2)]); break; } case 0x0119: // There isn't a way to force the Episode 4 boss to be rare via // constructor args - add((set_entry->iparam1 & 1) ? EnemyType::SHAMBERTIN : EnemyType::SAINT_MILION); + add((set_entry->iparam6 & 1) ? EnemyType::SHAMBERTIN : EnemyType::SAINT_MILION); default_num_children = 0x18; break; @@ -2728,12 +2728,12 @@ static double object_set_edit_cost(const MapFile::ObjectSetEntry& prev, const Ma ((prev.group != current.group) * 50.0) + ((prev.room != current.room) * 50.0) + (prev.pos - current.pos).norm() + - ((prev.param1 != current.param1) * 10.0) + - ((prev.param2 != current.param2) * 10.0) + - ((prev.param3 != current.param3) * 10.0) + - ((prev.param4 != current.param4) * 10.0) + - ((prev.param5 != current.param5) * 10.0) + - ((prev.param6 != current.param6) * 10.0)); + ((prev.fparam1 != current.fparam1) * 10.0) + + ((prev.fparam2 != current.fparam2) * 10.0) + + ((prev.fparam3 != current.fparam3) * 10.0) + + ((prev.iparam4 != current.iparam4) * 10.0) + + ((prev.iparam5 != current.iparam5) * 10.0) + + ((prev.iparam6 != current.iparam6) * 10.0)); } static double enemy_set_add_cost(const MapFile::EnemySetEntry&) { @@ -2759,8 +2759,8 @@ static double enemy_set_edit_cost(const MapFile::EnemySetEntry& prev, const MapF ((prev.fparam3 != current.fparam3) * 10.0) + ((prev.fparam4 != current.fparam4) * 10.0) + ((prev.fparam5 != current.fparam5) * 10.0) + - ((prev.iparam1 != current.iparam1) * 10.0) + - ((prev.iparam2 != current.iparam2) * 10.0)); + ((prev.iparam6 != current.iparam6) * 10.0) + + ((prev.iparam7 != current.iparam7) * 10.0)); } static double event_add_cost(const MapFile::Event1Entry&) { @@ -3717,7 +3717,7 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptr 2 are randomized to be + // On v3, Mericarols that have iparam6 > 2 are randomized to be // Mericus, Merikle, or Mericarol, but the former two are not // considered rare. (We use rare_flags anyway to distinguish them // from Mericarol.) diff --git a/src/Map.hh b/src/Map.hh index 9d2c56ed..f6bf7fb2 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -175,12 +175,12 @@ public: /* 0E */ le_uint16_t unknown_a3 = 0; /* 10 */ VectorXYZF pos; /* 1C */ VectorXYZI angle; - /* 28 */ le_float param1 = 0.0f; // Boxes: if <= 0, this is a specialized box, and the specialization is in param4/5/6 - /* 2C */ le_float param2 = 0.0f; - /* 30 */ le_float param3 = 0.0f; // Boxes: if == 0, the item should be varied by difficulty and area - /* 34 */ le_uint32_t param4 = 0; - /* 38 */ le_uint32_t param5 = 0; - /* 3C */ le_uint32_t param6 = 0; + /* 28 */ le_float fparam1 = 0.0f; // Boxes: if <= 0, this is a specialized box, and the specialization is in param4/5/6 + /* 2C */ le_float fparam2 = 0.0f; + /* 30 */ le_float fparam3 = 0.0f; // Boxes: if == 0, the item should be varied by difficulty and area + /* 34 */ le_int32_t iparam4 = 0; + /* 38 */ le_int32_t iparam5 = 0; + /* 3C */ le_int32_t iparam6 = 0; /* 40 */ le_uint32_t unused = 0; // Reserved for pointer in client's memory; unused by server /* 44 */ @@ -206,8 +206,8 @@ public: /* 34 */ le_float fparam3 = 0.0f; /* 38 */ le_float fparam4 = 0.0f; /* 3C */ le_float fparam5 = 0.0f; - /* 40 */ le_int16_t iparam1 = 0; - /* 42 */ le_int16_t iparam2 = 0; + /* 40 */ le_int16_t iparam6 = 0; + /* 42 */ le_int16_t iparam7 = 0; /* 44 */ le_uint32_t unused = 0; // Reserved for pointer in client's memory; unused by server /* 48 */ @@ -302,8 +302,8 @@ public: /* 08 */ le_float fparam3; /* 0C */ le_float fparam4; /* 10 */ le_float fparam5; - /* 14 */ le_int16_t iparam2; - /* 16 */ le_int16_t iparam1; + /* 14 */ le_int16_t iparam7; + /* 16 */ le_int16_t iparam6; /* 18 */ le_uint32_t entry_num; /* 1C */ le_uint16_t min_children; /* 1E */ le_uint16_t max_children; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 47e63f63..42aa0030 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -1043,8 +1043,8 @@ static HandlerResult SC_6x60_6xA2(shared_ptr ses, co res = ses->item_creator->on_box_item_drop(cmd.effective_area); } else { ses->log.info("Creating item from box %04hX (area %02hX; specialized with %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ")", - cmd.entity_index.load(), cmd.effective_area, cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load()); - res = ses->item_creator->on_specialized_box_item_drop(cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6); + cmd.entity_index.load(), cmd.effective_area, cmd.fparam3.load(), cmd.iparam4.load(), cmd.iparam5.load(), cmd.iparam6.load()); + res = ses->item_creator->on_specialized_box_item_drop(cmd.effective_area, cmd.fparam3, cmd.iparam4, cmd.iparam5, cmd.iparam6); } } else { ses->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_index.load(), cmd.effective_area); diff --git a/src/Quest.cc b/src/Quest.cc index abc978df..3dffc64f 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -192,148 +192,33 @@ struct PSODownloadQuestHeader { le_uint32_t encryption_seed; } __packed_ws__(PSODownloadQuestHeader, 8); -VersionedQuest::VersionedQuest( - uint32_t quest_number, - uint32_t category_id, - Version version, - uint8_t language, - std::shared_ptr bin_contents, - std::shared_ptr dat_contents, - std::shared_ptr map_file, - std::shared_ptr pvr_contents, - std::shared_ptr battle_rules, - ssize_t challenge_template_index, - uint8_t description_flag, - std::shared_ptr available_expression, - std::shared_ptr enabled_expression, - bool allow_start_from_chat_command, - bool force_joinable, - int16_t lock_status_register) - : quest_number(quest_number), - category_id(category_id), - episode(Episode::NONE), - allow_start_from_chat_command(allow_start_from_chat_command), - joinable(force_joinable), - lock_status_register(lock_status_register), - version(version), - language(language), - is_dlq_encoded(false), - bin_contents(bin_contents), - dat_contents(dat_contents), - map_file(map_file), - pvr_contents(pvr_contents), - battle_rules(battle_rules), - challenge_template_index(challenge_template_index), - description_flag(description_flag), - available_expression(available_expression), - enabled_expression(enabled_expression) { - - auto bin_decompressed = prs_decompress(*this->bin_contents); - - switch (this->version) { - case Version::DC_NTE: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderDCNTE)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - this->episode = Episode::EP1; - if (this->quest_number == 0xFFFFFFFF) { - this->quest_number = phosg::fnv1a32(header, sizeof(header)) & 0xFFFF; - } - this->name = header->name.decode(this->language); - break; - } - - case Version::DC_11_2000: - case Version::DC_V1: - case Version::DC_V2: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - this->episode = Episode::EP1; - if (this->quest_number == 0xFFFFFFFF) { - this->quest_number = header->quest_number; - } - this->name = header->name.decode(this->language); - this->short_description = header->short_description.decode(this->language); - this->long_description = header->long_description.decode(this->language); - break; - } - - case Version::PC_NTE: - case Version::PC_V2: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - this->episode = Episode::EP1; - if (this->quest_number == 0xFFFFFFFF) { - this->quest_number = header->quest_number; - } - this->name = header->name.decode(this->language); - this->short_description = header->short_description.decode(this->language); - this->long_description = header->long_description.decode(this->language); - break; - } - - case Version::GC_EP3_NTE: - case Version::GC_EP3: { - // Note: This codepath handles Episode 3 download quests, which are not - // the same as Episode 3 quest scripts. The latter are only used offline - // in story mode, but can be disassembled with disassemble_quest_script. - // It's unfortunate that Version::GC_EP3 is used here for Episode 3 - // download quests (maps) and there for offline story mode scripts, but - // it's probably not worth refactoring this logic, at least right now. - if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) { - throw invalid_argument("file is incorrect size"); - } - auto* map = reinterpret_cast(bin_decompressed.data()); - this->episode = Episode::EP3; - if (this->quest_number == 0xFFFFFFFF) { - this->quest_number = map->map_number; - } - this->name = map->name.decode(this->language); - this->short_description = map->quest_name.decode(this->language); - this->long_description = map->description.decode(this->language); - break; - } - - case Version::XB_V3: - case Version::GC_NTE: - case Version::GC_V3: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version); - if (this->quest_number == 0xFFFFFFFF) { - this->quest_number = header->quest_number; - } - this->name = header->name.decode(this->language); - this->short_description = header->short_description.decode(this->language); - this->long_description = header->long_description.decode(this->language); - break; - } - - case Version::BB_V4: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable |= header->joinable; - this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version); - if (this->quest_number == 0xFFFFFFFF) { - this->quest_number = header->quest_number; - } - this->name = header->name.decode(this->language); - this->short_description = header->short_description.decode(this->language); - this->long_description = header->long_description.decode(this->language); - break; - } - - default: - throw logic_error("invalid quest game version"); +void VersionedQuest::assert_valid() const { + if (this->category_id == 0xFFFFFFFF) { + throw runtime_error("category ID is not set"); + } + if (this->quest_number == 0xFFFFFFFF) { + throw runtime_error("quest number is not set"); + } + if (this->version == Version::UNKNOWN) { + throw runtime_error("version is not set"); + } + if (this->language == 0xFF) { + throw runtime_error("language is not set"); + } + if (this->episode == Episode::NONE) { + throw runtime_error("episode is not set"); + } + if (this->max_players == 0) { + throw runtime_error("max players is not set"); + } + if (!this->bin_contents) { + throw runtime_error("bin file is missing"); + } + if (!is_ep3(this->version) && !this->dat_contents) { + throw runtime_error("dat file is missing"); + } + if (!is_ep3(this->version) && !this->map_file) { + throw runtime_error("parsed map file is missing"); } } @@ -386,6 +271,7 @@ Quest::Quest(shared_ptr initial_version) episode(initial_version->episode), allow_start_from_chat_command(initial_version->allow_start_from_chat_command), joinable(initial_version->joinable), + max_players(initial_version->max_players), lock_status_register(initial_version->lock_status_register), name(initial_version->name), supermap(nullptr), @@ -421,6 +307,7 @@ phosg::JSON Quest::json() const { {"Episode", name_for_episode(this->episode)}, {"AllowStartFromChatCommand", this->allow_start_from_chat_command}, {"Joinable", this->joinable}, + {"MaxPlayers", this->max_players}, {"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)}, {"Name", this->name}, {"BattleRules", std::move(battle_rules_json)}, @@ -438,46 +325,85 @@ uint32_t Quest::versions_key(Version v, uint8_t language) { void Quest::add_version(shared_ptr vq) { if (this->quest_number != vq->quest_number) { - throw logic_error("incorrect versioned quest number"); + throw logic_error(phosg::string_printf( + "incorrect versioned quest number (existing: %08" PRIX32 ", new: %08" PRIX32 ")", + this->quest_number, vq->quest_number)); } if (this->category_id != vq->category_id) { - throw runtime_error("quest version is in a different category"); + throw runtime_error(phosg::string_printf( + "quest version is in a different category (existing: %08" PRIX32 ", new: %08" PRIX32 ")", + this->category_id, vq->category_id)); } if (this->episode != vq->episode) { - throw runtime_error("quest version is in a different episode"); + throw runtime_error(phosg::string_printf( + "quest version is in a different episode (existing: %s, new: %s)", + name_for_episode(this->episode), name_for_episode(vq->episode))); } if (this->allow_start_from_chat_command != vq->allow_start_from_chat_command) { - throw runtime_error("quest version has a different allow_start_from_chat_command state"); + throw runtime_error(phosg::string_printf( + "quest version has a different allow_start_from_chat_command state (existing: %s, new: %s)", + this->allow_start_from_chat_command ? "true" : "false", vq->allow_start_from_chat_command ? "true" : "false")); } if (this->joinable != vq->joinable) { - throw runtime_error("quest version has a different joinability state"); + throw runtime_error(phosg::string_printf( + "quest version has a different joinability state (existing: %s, new: %s)", + this->joinable ? "true" : "false", vq->joinable ? "true" : "false")); + } + if (this->max_players != vq->max_players) { + throw runtime_error(phosg::string_printf( + "quest version has a different maximum player count (existing: %hhu, new: %hhu)", + this->max_players, vq->max_players)); } if (this->lock_status_register != vq->lock_status_register) { - throw runtime_error("quest version has a different lock status register"); + throw runtime_error(phosg::string_printf( + "quest version has a different lock status register (existing: %04hX, new: %04hX)", + this->lock_status_register, vq->lock_status_register)); } if (!this->battle_rules != !vq->battle_rules) { - throw runtime_error("quest version has a different battle rules presence state"); + throw runtime_error(phosg::string_printf( + "quest version has a different battle rules presence state (existing: %s, new: %s)", + this->battle_rules ? "present" : "absent", vq->battle_rules ? "present" : "absent")); } if (this->battle_rules && (*this->battle_rules != *vq->battle_rules)) { - throw runtime_error("quest version has different battle rules"); + string existing_str = this->battle_rules->json().serialize(); + string new_str = vq->battle_rules->json().serialize(); + throw runtime_error(phosg::string_printf( + "quest version has different battle rules (existing: %s, new: %s)", + existing_str.c_str(), new_str.c_str())); } if (this->challenge_template_index != vq->challenge_template_index) { - throw runtime_error("quest version has different challenge template index"); + throw runtime_error(phosg::string_printf( + "quest version has different challenge template index (existing: %zd, new: %zd)", + this->challenge_template_index, vq->challenge_template_index)); } if (this->description_flag != vq->description_flag) { - throw runtime_error("quest version has different description flag"); + throw runtime_error(phosg::string_printf( + "quest version has different description flag (existing: %02hhX, new: %02hhX)", + this->description_flag, vq->description_flag)); } if (!this->available_expression != !vq->available_expression) { - throw runtime_error("quest version has available expression but root quest does not, or vice versa"); + throw runtime_error(phosg::string_printf( + "quest version has available expression but root quest does not, or vice versa (existing: %s, new: %s)", + this->available_expression ? "present" : "absent", vq->available_expression ? "present" : "absent")); } if (this->available_expression && *this->available_expression != *vq->available_expression) { - throw runtime_error("quest version has a different available expression"); + string existing_str = this->available_expression->str(); + string new_str = vq->available_expression->str(); + throw runtime_error(phosg::string_printf( + "quest version has a different available expression (existing: %s, new: %s)", + existing_str.c_str(), new_str.c_str())); } if (!this->enabled_expression != !vq->enabled_expression) { - throw runtime_error("quest version has enabled expression but root quest does not, or vice versa"); + throw runtime_error(phosg::string_printf( + "quest version has enabled expression but root quest does not, or vice versa (existing: %s, new: %s)", + this->enabled_expression ? "present" : "absent", vq->enabled_expression ? "present" : "absent")); } if (this->enabled_expression && *this->enabled_expression != *vq->enabled_expression) { - throw runtime_error("quest version has a different enabled expression"); + string existing_str = this->enabled_expression->str(); + string new_str = vq->enabled_expression->str(); + throw runtime_error(phosg::string_printf( + "quest version has a different enabled expression (existing: %s, new: %s)", + existing_str.c_str(), new_str.c_str())); } this->versions.emplace(this->versions_key(vq->version, vq->language), vq); @@ -563,12 +489,17 @@ QuestIndex::QuestIndex( string filename; shared_ptr data; }; + struct BINFileData { + string filename; + unique_ptr metadata; + shared_ptr data; + }; struct DATFileData { string filename; shared_ptr data; shared_ptr map_file; }; - map bin_files; + map bin_files; map dat_files; map pvr_files; map json_files; @@ -596,18 +527,34 @@ QuestIndex::QuestIndex( } }; - auto add_dat_file = [&](const string& basename, const string& filename, string&& value) { + auto add_bin_file = [&](const string& basename, const string& filename, string&& data, const QuestMetadata* metadata) { if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) { - throw runtime_error("file " + basename + " exists in multiple categories"); + throw runtime_error("bin file " + basename + " exists in multiple categories"); } - auto data_ptr = make_shared(std::move(value)); + auto data_ptr = make_shared(std::move(data)); + auto emplace_ret = bin_files.emplace(basename, BINFileData{}); + if (!emplace_ret.second) { + throw runtime_error("bin file " + basename + " already exists"); + } + auto& entry = emplace_ret.first->second; + entry.filename = filename; + entry.data = data_ptr; + if (metadata) { + entry.metadata = make_unique(*metadata); + } + if (!(data_ptr->size() & 0x3FF)) { + data_ptr->push_back(0x00); + } + }; + auto add_dat_file = [&](const string& basename, const string& filename, string&& data) { + if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) { + throw runtime_error("dat file " + basename + " exists in multiple categories"); + } + auto data_ptr = make_shared(std::move(data)); auto map_file = make_shared(make_shared(prs_decompress(*data_ptr))); if (!dat_files.emplace(basename, DATFileData{filename, data_ptr, map_file}).second) { - throw runtime_error("file " + basename + " already exists"); + throw runtime_error("dat file " + basename + " already exists"); } - // There is a bug in the client that prevents quests from loading properly - // if any file's size is a multiple of 0x400. See the comments on the 13 - // command in CommandFormats.hh for more details. if (!(data_ptr->size() & 0x3FF)) { data_ptr->push_back(0x00); } @@ -624,6 +571,7 @@ QuestIndex::QuestIndex( } string file_path = cat_path + "/" + filename; + unique_ptr assembled; try { string orig_filename = filename; string file_data; @@ -638,10 +586,11 @@ QuestIndex::QuestIndex( filename.resize(filename.size() - 4); } else if (phosg::ends_with(filename, ".bin.txt")) { string include_dir = phosg::dirname(file_path); - file_data = assemble_quest_script( + assembled = make_unique(assemble_quest_script( phosg::load_file(file_path), {include_dir, "system/quests/includes"}, - {include_dir, "system/quests/includes", "system/client-functions/System"}); + {include_dir, "system/quests/includes", "system/client-functions/System"})); + file_data = std::move(assembled->data); filename.resize(filename.size() - 4); if (phosg::ends_with(filename, ".bin")) { filename.push_back('d'); @@ -663,9 +612,9 @@ QuestIndex::QuestIndex( if (extension == "json") { add_file(json_files, file_basename, orig_filename, std::move(file_data), false); } else if (extension == "bin" || extension == "mnm") { - add_file(bin_files, file_basename, orig_filename, std::move(file_data), true); + add_bin_file(file_basename, orig_filename, std::move(file_data), assembled ? &assembled->metadata : nullptr); } else if (extension == "bind" || extension == "mnmd") { - add_file(bin_files, file_basename, orig_filename, prs_compress_optimal(file_data), true); + add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled ? &assembled->metadata : nullptr); } else if (extension == "dat") { add_dat_file(file_basename, orig_filename, std::move(file_data)); } else if (extension == "datd") { @@ -676,7 +625,7 @@ QuestIndex::QuestIndex( auto files = decode_qst_data(file_data); for (auto& it : files) { if (phosg::ends_with(it.first, ".bin")) { - add_file(bin_files, file_basename, orig_filename, std::move(it.second), true); + add_bin_file(file_basename, orig_filename, std::move(it.second), nullptr); } else if (phosg::ends_with(it.first, ".dat")) { add_dat_file(file_basename, orig_filename, std::move(it.second)); } else if (phosg::ends_with(it.first, ".pvr")) { @@ -696,11 +645,10 @@ QuestIndex::QuestIndex( // All quests have a bin file (even in Episode 3, though its format is // different), so we use bin_files as the primary list of all quests that // should be indexed - for (auto& bin_it : bin_files) { - const string& basename = bin_it.first; - const auto* bin_filedata = &bin_it.second; - + for (auto& [basename, entry] : bin_files) { try { + auto vq = make_shared(); + // Quest .bin filenames are like K###-VERS-LANG.EXT, where: // K can be any character (usually it's q) // # = quest number (does not have to match the internal quest number) @@ -719,42 +667,172 @@ QuestIndex::QuestIndex( version_token = std::move(filename_tokens[1]); language_token = std::move(filename_tokens[2]); } + vq->category_id = categories.at(basename); - uint32_t category_id = categories.at(basename); + // Find the quest's metadata. If the quest was assembled (that is, if it + // came from a .bin.txt file), use the metadata from the source file; + // otherwise, figure it out from the already-assembled code + if (entry.metadata) { + vq->quest_number = entry.metadata->quest_number; + vq->version = ::is_ep3(entry.metadata->version) ? Version::GC_V3 : entry.metadata->version; + vq->language = entry.metadata->language; + vq->episode = entry.metadata->episode; + vq->joinable = entry.metadata->joinable; + vq->max_players = entry.metadata->max_players; + vq->name = entry.metadata->name; + vq->short_description = entry.metadata->short_description; + vq->long_description = entry.metadata->long_description; - // Get the number from the first token - if (quest_number_token.empty()) { - throw runtime_error("quest number token is missing"); + } else { + // Get the number from the first token + if (quest_number_token.empty()) { + throw runtime_error("quest number token is missing"); + } + vq->quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10); + + // Get the version from the second token + static const unordered_map name_to_version({ + {"dn", Version::DC_NTE}, + {"dp", Version::DC_11_2000}, + {"d1", Version::DC_V1}, + {"dc", Version::DC_V2}, + {"pcn", Version::PC_NTE}, + {"pc", Version::PC_V2}, + {"gcn", Version::GC_NTE}, + {"gc", Version::GC_V3}, + {"gc3t", Version::GC_EP3_NTE}, + {"gc3", Version::GC_EP3}, + {"xb", Version::XB_V3}, + {"bb", Version::BB_V4}, + }); + vq->version = name_to_version.at(version_token); + + // Get the language from the last token + if (language_token.size() != 1) { + throw runtime_error("language token is not a single character"); + } + vq->language = language_code_for_char(language_token[0]); + + auto bin_decompressed = prs_decompress(*entry.data); + switch (vq->version) { + case Version::DC_NTE: { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderDCNTE)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + vq->episode = Episode::EP1; + vq->max_players = 4; + vq->name = header->name.decode(vq->language); + if (vq->quest_number == 0xFFFFFFFF) { + vq->quest_number = phosg::fnv1a64(vq->name); + } + break; + } + + case Version::DC_11_2000: + case Version::DC_V1: + case Version::DC_V2: { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + vq->episode = Episode::EP1; + vq->max_players = 4; + if (vq->quest_number == 0xFFFFFFFF) { + vq->quest_number = header->quest_number; + } + vq->name = header->name.decode(vq->language); + vq->short_description = header->short_description.decode(vq->language); + vq->long_description = header->long_description.decode(vq->language); + break; + } + + case Version::PC_NTE: + case Version::PC_V2: { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + vq->episode = Episode::EP1; + vq->max_players = 4; + if (vq->quest_number == 0xFFFFFFFF) { + vq->quest_number = header->quest_number; + } + vq->name = header->name.decode(vq->language); + vq->short_description = header->short_description.decode(vq->language); + vq->long_description = header->long_description.decode(vq->language); + break; + } + + case Version::GC_EP3_NTE: + case Version::GC_EP3: { + // Note: This codepath handles Episode 3 download quests, which are not + // the same as Episode 3 quest scripts. The latter are only used offline + // in story mode, but can be disassembled with disassemble_quest_script. + // It's unfortunate that Version::GC_EP3 is used here for Episode 3 + // download quests (maps) and there for offline story mode scripts, but + // it's probably not worth refactoring this logic, at least right now. + if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) { + throw invalid_argument("file is incorrect size"); + } + auto* map = reinterpret_cast(bin_decompressed.data()); + vq->episode = Episode::EP3; + vq->max_players = 4; + if (vq->quest_number == 0xFFFFFFFF) { + vq->quest_number = map->map_number; + } + vq->name = map->name.decode(vq->language); + vq->short_description = map->quest_name.decode(vq->language); + vq->long_description = map->description.decode(vq->language); + break; + } + + case Version::XB_V3: + case Version::GC_NTE: + case Version::GC_V3: { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + vq->episode = find_quest_episode_from_script( + bin_decompressed.data(), bin_decompressed.size(), vq->version); + vq->max_players = 4; + if (vq->quest_number == 0xFFFFFFFF) { + vq->quest_number = header->quest_number; + } + vq->name = header->name.decode(vq->language); + vq->short_description = header->short_description.decode(vq->language); + vq->long_description = header->long_description.decode(vq->language); + break; + } + + case Version::BB_V4: { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + vq->episode = find_quest_episode_from_script( + bin_decompressed.data(), bin_decompressed.size(), vq->version); + vq->joinable |= header->joinable; + vq->max_players = 4; + if (vq->quest_number == 0xFFFFFFFF) { + vq->quest_number = header->quest_number; + } + vq->name = header->name.decode(vq->language); + vq->short_description = header->short_description.decode(vq->language); + vq->long_description = header->long_description.decode(vq->language); + break; + } + + default: + throw logic_error("invalid quest game version"); + } } - uint32_t quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10); - - // Get the version from the second token - static const unordered_map name_to_version({ - {"dn", Version::DC_NTE}, - {"dp", Version::DC_11_2000}, - {"d1", Version::DC_V1}, - {"dc", Version::DC_V2}, - {"pcn", Version::PC_NTE}, - {"pc", Version::PC_V2}, - {"gcn", Version::GC_NTE}, - {"gc", Version::GC_V3}, - {"gc3t", Version::GC_EP3_NTE}, - {"gc3", Version::GC_EP3}, - {"xb", Version::XB_V3}, - {"bb", Version::BB_V4}, - }); - auto version = name_to_version.at(version_token); - - // Get the language from the last token - if (language_token.size() != 1) { - throw runtime_error("language token is not a single character"); - } - uint8_t language = language_code_for_char(language_token[0]); // Find the corresponding dat and pvr files const DATFileData* dat_filedata = nullptr; const FileData* pvr_filedata = nullptr; - if (!::is_ep3(version)) { + if (!::is_ep3(vq->version)) { // Look for dat and pvr files with the same basename as the bin file; if // not found, look for them without the language suffix try { @@ -777,17 +855,17 @@ QuestIndex::QuestIndex( } } } + vq->bin_contents = entry.data; + if (dat_filedata) { + vq->dat_contents = dat_filedata->data; + vq->map_file = dat_filedata->map_file; + } + if (pvr_filedata) { + vq->pvr_contents = pvr_filedata->data; + } - // Load the quest's metadata phosg::JSON file, if it exists + // Load the quest's metadata JSON file, if it exists const FileData* json_filedata = nullptr; - shared_ptr battle_rules; - ssize_t challenge_template_index = -1; - uint8_t description_flag = 0; - shared_ptr available_expression; - shared_ptr enabled_expression; - bool allow_start_from_chat_command = false; - bool force_joinable = false; - int16_t lock_status_register = -1; try { json_filedata = &json_files.at(basename); } catch (const out_of_range&) { @@ -803,59 +881,43 @@ QuestIndex::QuestIndex( if (json_filedata) { auto metadata_json = phosg::JSON::parse(*json_filedata->data); try { - battle_rules = make_shared(metadata_json.at("BattleRules")); + vq->battle_rules = make_shared(metadata_json.at("BattleRules")); } catch (const out_of_range&) { } try { - challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int(); + vq->challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int(); } catch (const out_of_range&) { } try { - description_flag = metadata_json.at("DescriptionFlag").as_int(); + vq->description_flag = metadata_json.at("DescriptionFlag").as_int(); } catch (const out_of_range&) { } try { - available_expression = make_shared(metadata_json.get_string("AvailableIf")); + vq->available_expression = make_shared(metadata_json.get_string("AvailableIf")); } catch (const out_of_range&) { } try { - enabled_expression = make_shared(metadata_json.get_string("EnabledIf")); + vq->enabled_expression = make_shared(metadata_json.get_string("EnabledIf")); } catch (const out_of_range&) { } try { - allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand"); + vq->allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand"); } catch (const out_of_range&) { } try { - force_joinable = metadata_json.get_bool("Joinable"); + vq->joinable = metadata_json.get_bool("Joinable"); } catch (const out_of_range&) { } try { - lock_status_register = metadata_json.get_int("LockStatusRegister"); + vq->lock_status_register = metadata_json.get_int("LockStatusRegister"); } catch (const out_of_range&) { } } - auto vq = make_shared( - quest_number, - category_id, - version, - language, - bin_filedata->data, - dat_filedata ? dat_filedata->data : nullptr, - dat_filedata ? dat_filedata->map_file : nullptr, - pvr_filedata ? pvr_filedata->data : nullptr, - battle_rules, - challenge_template_index, - description_flag, - available_expression, - enabled_expression, - allow_start_from_chat_command, - force_joinable, - lock_status_register); + vq->assert_valid(); auto category_name = this->category_index->at(vq->category_id)->name; - string filenames_str = bin_filedata->filename; + string filenames_str = entry.filename; if (dat_filedata) { filenames_str += phosg::string_printf("/%s", dat_filedata->filename.c_str()); } @@ -891,7 +953,7 @@ QuestIndex::QuestIndex( vq->joinable ? "joinable" : "not joinable"); } } catch (const exception& e) { - static_game_data_log.warning("(%s) Failed to index quest file: (%s)", basename.c_str(), e.what()); + static_game_data_log.warning("(%s) Failed to index quest file: %s", basename.c_str(), e.what()); } } } diff --git a/src/Quest.hh b/src/Quest.hh index bc8fcdfb..1f94bdaa 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -64,16 +64,16 @@ struct QuestCategoryIndex { }; struct VersionedQuest { - uint32_t quest_number = 0; - uint32_t category_id = 0; - Episode episode = Episode::NONE; - bool allow_start_from_chat_command = false; - bool joinable = false; - int16_t lock_status_register = -1; - std::string name; + // Most of these default values are intentionally invalid; we use these + // values to check if each field was parsed during quest indexing. + uint32_t category_id = 0xFFFFFFFF; + uint32_t quest_number = 0xFFFFFFFF; Version version = Version::UNKNOWN; - uint8_t language = 1; - bool is_dlq_encoded = false; + uint8_t language = 0xFF; + Episode episode = Episode::NONE; + bool joinable = false; + uint8_t max_players = 0x00; + std::string name; std::string short_description; std::string long_description; std::shared_ptr bin_contents; @@ -82,27 +82,14 @@ struct VersionedQuest { std::shared_ptr pvr_contents; std::shared_ptr battle_rules; ssize_t challenge_template_index = -1; - uint8_t description_flag = 0; + uint8_t description_flag = 0x00; std::shared_ptr available_expression; std::shared_ptr enabled_expression; + bool allow_start_from_chat_command = false; + int16_t lock_status_register = -1; + bool is_dlq_encoded = false; - VersionedQuest( - uint32_t quest_number, - uint32_t category_id, - Version version, - uint8_t language, - std::shared_ptr bin_contents, - std::shared_ptr dat_contents, - std::shared_ptr map_file, - std::shared_ptr pvr_contents, - std::shared_ptr battle_rules = nullptr, - ssize_t challenge_template_index = -1, - uint8_t description_flag = 0, - std::shared_ptr available_expression = nullptr, - std::shared_ptr enabled_expression = nullptr, - bool allow_start_from_chat_command = false, - bool force_joinable = false, - int16_t lock_status_register = -1); + void assert_valid() const; std::string bin_filename() const; std::string dat_filename() const; @@ -119,6 +106,7 @@ struct Quest { Episode episode; bool allow_start_from_chat_command; bool joinable; + uint8_t max_players; int16_t lock_status_register; std::string name; mutable std::shared_ptr supermap; diff --git a/src/QuestScript.cc b/src/QuestScript.cc index eb374539..eb5cba03 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -2996,9 +2996,8 @@ std::string disassemble_quest_script( } else { language = 1; } - lines.emplace_back(phosg::string_printf(".quest_num %hhu", header.quest_number)); + lines.emplace_back(phosg::string_printf(".quest_num %hu", header.quest_number.load())); lines.emplace_back(phosg::string_printf(".language %hhu", header.language)); - lines.emplace_back(phosg::string_printf(".episode %s", name_for_header_episode_number(header.episode))); lines.emplace_back(".name " + escape_string(header.name.decode(language))); lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language))); lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language))); @@ -3675,7 +3674,6 @@ Episode find_quest_episode_from_script(const void* data, size_t size, Version ve const auto& header = r.get(); code_offset = header.code_offset; function_table_offset = header.function_table_offset; - header_episode = episode_for_quest_episode_number(header.episode); break; } case Version::BB_V4: { @@ -3691,7 +3689,6 @@ Episode find_quest_episode_from_script(const void* data, size_t size, Version ve } unordered_set found_episodes; - try { const auto& opcodes = opcodes_for_version(version); // The set_episode opcode should always be in the first function (0) @@ -3794,8 +3791,10 @@ Episode find_quest_episode_from_script(const void* data, size_t size, Version ve throw runtime_error("multiple episodes found"); } else if (found_episodes.size() == 1) { return *found_episodes.begin(); - } else { + } else if (header_episode != Episode::NONE) { return header_episode; + } else { + return Episode::EP1; } } @@ -3997,7 +3996,7 @@ struct RegisterAssigner { array, 0x100> numbered_regs; }; -std::string assemble_quest_script( +AssembledQuestScript assemble_quest_script( const std::string& text, const vector& script_include_directories, const vector& native_include_directories) { @@ -4654,7 +4653,6 @@ std::string assemble_quest_script( header.code_offset = sizeof(header); header.function_table_offset = sizeof(header) + code_w.size(); header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]); - header.unused = 0; header.name.encode(quest_name, 0); w.put(header); break; @@ -4666,9 +4664,7 @@ std::string assemble_quest_script( header.code_offset = sizeof(header); header.function_table_offset = sizeof(header) + code_w.size(); header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]); - header.unused = 0; header.language = quest_language; - header.unknown1 = 0; header.quest_number = quest_num; header.name.encode(quest_name, quest_language); header.short_description.encode(quest_short_desc, quest_language); @@ -4682,9 +4678,7 @@ std::string assemble_quest_script( header.code_offset = sizeof(header); header.function_table_offset = sizeof(header) + code_w.size(); header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]); - header.unused = 0; header.language = quest_language; - header.unknown1 = 0; header.quest_number = quest_num; header.name.encode(quest_name, quest_language); header.short_description.encode(quest_short_desc, quest_language); @@ -4701,11 +4695,8 @@ std::string assemble_quest_script( header.code_offset = sizeof(header); header.function_table_offset = sizeof(header) + code_w.size(); header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]); - header.unused = 0; header.language = quest_language; - header.unknown1 = 0; header.quest_number = quest_num; - header.episode = (quest_episode == Episode::EP2) ? 1 : 0; header.name.encode(quest_name, quest_language); header.short_description.encode(quest_short_desc, quest_language); header.long_description.encode(quest_long_desc, quest_language); @@ -4717,9 +4708,7 @@ std::string assemble_quest_script( header.code_offset = sizeof(header); header.function_table_offset = sizeof(header) + code_w.size(); header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]); - header.unused = 0; header.quest_number = quest_num; - header.unused2 = 0; if (quest_episode == Episode::EP4) { header.episode = 2; } else if (quest_episode == Episode::EP2) { @@ -4729,7 +4718,6 @@ std::string assemble_quest_script( } header.max_players = quest_max_players; header.joinable = quest_joinable ? 1 : 0; - header.unknown = 0; header.name.encode(quest_name, quest_language); header.short_description.encode(quest_short_desc, quest_language); header.long_description.encode(quest_long_desc, quest_language); @@ -4741,5 +4729,18 @@ std::string assemble_quest_script( } w.write(code_w.str()); w.write(function_table.data(), function_table.size() * sizeof(function_table[0])); - return std::move(w.str()); + return AssembledQuestScript{ + .data = std::move(w.str()), + .metadata = QuestMetadata{ + .episode = quest_episode, + .version = quest_version, + .language = quest_language, + .max_players = quest_max_players, + .name = quest_name, + .short_description = quest_short_desc, + .long_description = quest_long_desc, + .quest_number = quest_num, + .joinable = quest_joinable, + }, + }; } diff --git a/src/QuestScript.hh b/src/QuestScript.hh index a245cf76..7567e76a 100644 --- a/src/QuestScript.hh +++ b/src/QuestScript.hh @@ -10,22 +10,24 @@ #include "Version.hh" struct PSOQuestHeaderDCNTE { - /* 0000 */ le_uint32_t code_offset; - /* 0004 */ le_uint32_t function_table_offset; - /* 0008 */ le_uint32_t size; - /* 000C */ le_uint32_t unused; + /* 0000 */ le_uint32_t code_offset = 0; + /* 0004 */ le_uint32_t function_table_offset = 0; + /* 0008 */ le_uint32_t size = 0; + /* 000C */ le_uint16_t unknown_a1 = 0; + /* 000E */ le_uint16_t unknown_a2 = 0; /* 0010 */ pstring name; /* 0020 */ } __packed_ws__(PSOQuestHeaderDCNTE, 0x20); struct PSOQuestHeaderDC { // Same format for DC v1 and v2 - /* 0000 */ le_uint32_t code_offset; - /* 0004 */ le_uint32_t function_table_offset; - /* 0008 */ le_uint32_t size; - /* 000C */ le_uint32_t unused; - /* 0010 */ uint8_t language; - /* 0011 */ uint8_t unknown1; - /* 0012 */ le_uint16_t quest_number; // 0xFFFF for challenge quests + /* 0000 */ le_uint32_t code_offset = 0; + /* 0004 */ le_uint32_t function_table_offset = 0; + /* 0008 */ le_uint32_t size = 0; + /* 000C */ le_uint16_t unknown_a1 = 0; + /* 000E */ le_uint16_t unknown_a2 = 0; + /* 0010 */ uint8_t language = 0; + /* 0011 */ uint8_t unknown_a3 = 0; + /* 0012 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests /* 0014 */ pstring name; /* 0034 */ pstring short_description; /* 00B4 */ pstring long_description; @@ -35,11 +37,12 @@ struct PSOQuestHeaderDC { // Same format for DC v1 and v2 struct PSOQuestHeaderPC { /* 0000 */ le_uint32_t code_offset; /* 0004 */ le_uint32_t function_table_offset; - /* 0008 */ le_uint32_t size; - /* 000C */ le_uint32_t unused; - /* 0010 */ uint8_t language; - /* 0011 */ uint8_t unknown1; - /* 0012 */ le_uint16_t quest_number; // 0xFFFF for challenge quests + /* 0008 */ le_uint32_t size = 0; + /* 000C */ le_uint16_t unknown_a1 = 0; + /* 000E */ le_uint16_t unknown_a2 = 0; + /* 0010 */ uint8_t language = 0; + /* 0011 */ uint8_t unknown_a3 = 0; + /* 0012 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests /* 0014 */ pstring name; /* 0054 */ pstring short_description; /* 0154 */ pstring long_description; @@ -49,15 +52,18 @@ struct PSOQuestHeaderPC { // TODO: Is the XB quest header format the same as on GC? If not, make a // separate struct; if so, rename this struct to V3. struct PSOQuestHeaderGC { - /* 0000 */ le_uint32_t code_offset; - /* 0004 */ le_uint32_t function_table_offset; - /* 0008 */ le_uint32_t size; - /* 000C */ le_uint16_t unknown_a1; - /* 000E */ le_uint16_t unknown_a2; - /* 0010 */ uint8_t language; - /* 0011 */ uint8_t unknown_a3; - /* 0012 */ uint8_t quest_number; - /* 0013 */ uint8_t episode; // 1 = Ep2. Apparently some quests have 0xFF here, which means ep1 (?) + /* 0000 */ le_uint32_t code_offset = 0; + /* 0004 */ le_uint32_t function_table_offset = 0; + /* 0008 */ le_uint32_t size = 0; + /* 000C */ le_uint16_t unknown_a1 = 0; + /* 000E */ le_uint16_t unknown_a2 = 0; + /* 0010 */ uint8_t language = 0; + /* 0011 */ uint8_t unknown_a3 = 0; + // Note: The GC client byteswaps this field, then loads it as a byte, so + // technically the high byte of this is what the client uses as the quest + // number. In practice, this only matters if the quest runs send_statistic + // without running prepare_statistic first, which is not the intended usage. + /* 0012 */ le_uint16_t quest_number = 0; /* 0014 */ pstring name; /* 0034 */ pstring short_description; /* 00B4 */ pstring long_description; @@ -65,16 +71,17 @@ struct PSOQuestHeaderGC { } __packed_ws__(PSOQuestHeaderGC, 0x1D4); struct PSOQuestHeaderBB { - /* 0000 */ le_uint32_t code_offset; - /* 0004 */ le_uint32_t function_table_offset; - /* 0008 */ le_uint32_t size; - /* 000C */ le_uint32_t unused; - /* 0010 */ le_uint16_t quest_number; // 0xFFFF for challenge quests - /* 0012 */ le_uint16_t unused2; - /* 0014 */ uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4 - /* 0015 */ uint8_t max_players; - /* 0016 */ uint8_t joinable; - /* 0017 */ uint8_t unknown; + /* 0000 */ le_uint32_t code_offset = 0; + /* 0004 */ le_uint32_t function_table_offset = 0; + /* 0008 */ le_uint32_t size = 0; + /* 000C */ le_uint16_t unknown_a1 = 0; + /* 000E */ le_uint16_t unknown_a2 = 0; + /* 0010 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests + /* 0012 */ le_uint16_t unknown_a3 = 0; + /* 0014 */ uint8_t episode = 0; // 0 = Ep1, 1 = Ep2, 2 = Ep4 + /* 0015 */ uint8_t max_players = 0; + /* 0016 */ uint8_t joinable = 0; + /* 0017 */ uint8_t unknown_a4 = 0; /* 0018 */ pstring name; /* 0058 */ pstring short_description; /* 0158 */ pstring long_description; @@ -92,7 +99,23 @@ std::string disassemble_quest_script( uint8_t override_language = 0xFF, bool reassembly_mode = false, bool use_qedit_names = false); -std::string assemble_quest_script( + +struct QuestMetadata { + int64_t quest_number = -1; + Version version = Version::UNKNOWN; + uint8_t language = 0xFF; + Episode episode = Episode::NONE; + bool joinable = false; + uint8_t max_players = 0x00; + std::string name; + std::string short_description; + std::string long_description; +}; +struct AssembledQuestScript { + std::string data; + QuestMetadata metadata; +}; +AssembledQuestScript assemble_quest_script( const std::string& text, const std::vector& script_include_directories, const std::vector& native_include_directories); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index fdef78c6..bcaa8ce3 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2785,15 +2785,15 @@ DropReconcileResult reconcile_drop_request_with_map( if (is_v1_or_v2(version) && (version != Version::GC_NTE)) { // V1 and V2 don't have 6xA2, so we can't get ignore_def or the object // parameters from the client on those versions - cmd.param3 = set_entry->param3; - cmd.param4 = set_entry->param4; - cmd.param5 = set_entry->param5; - cmd.param6 = set_entry->param6; + cmd.fparam3 = set_entry->fparam3; + cmd.iparam4 = set_entry->iparam4; + cmd.iparam5 = set_entry->iparam5; + cmd.iparam6 = set_entry->iparam6; } - bool object_ignore_def = (set_entry->param1 > 0.0); + bool object_ignore_def = (set_entry->fparam1 > 0.0); if (res.ignore_def != object_ignore_def) { log.warning("ignore_def value %s from command does not match object\'s expected ignore_def %s (from p1=%g)", - res.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", set_entry->param1.load()); + res.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", set_entry->fparam1.load()); } if (config.check_flag(Client::Flag::DEBUG_ENABLED)) { send_text_message_printf(client_channel, "$C5K-%03zX %c %s", @@ -2907,9 +2907,9 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u } else { l->log.info("Creating item from box %04hX => K-%03zX (area %02hX; specialized with %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ")", cmd.entity_index.load(), rec.obj_st->k_id, cmd.effective_area, - cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load()); + cmd.fparam3.load(), cmd.iparam4.load(), cmd.iparam5.load(), cmd.iparam6.load()); return l->item_creator->on_specialized_box_item_drop( - cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6); + cmd.effective_area, cmd.fparam3, cmd.iparam4, cmd.iparam5, cmd.iparam6); } } else if (rec.ene_st) { l->log.info("Creating item from enemy %04hX => E-%03zX (area %02hX)", diff --git a/system/quests/download/q050-d1-e.bin.txt b/system/quests/download/q050-d1-e.bin.txt index 14ca1505..b6817366 100755 --- a/system/quests/download/q050-d1-e.bin.txt +++ b/system/quests/download/q050-d1-e.bin.txt @@ -1,5 +1,5 @@ .version DC_V1 -.quest_num 15 +.quest_num 50 .language 1 .name "Soul of a Blacksmith" .short_desc "A blacksmith\nwants to make\na weapon using\nRagol\'s unknown\nmaterials." diff --git a/system/quests/download/q052-d1-e.bin.txt b/system/quests/download/q052-d1-e.bin.txt index 5a76b200..6afcc2a5 100755 --- a/system/quests/download/q052-d1-e.bin.txt +++ b/system/quests/download/q052-d1-e.bin.txt @@ -1,5 +1,5 @@ .version DC_V1 -.quest_num 19 +.quest_num 52 .language 1 .name "The Retired Hunter" .short_desc "I will kill\n10000 monsters\nbefore I die!" diff --git a/tests/GC-Episode3BattleVirusAndCreatureTech.rdtest.txt b/tests/GC-Episode3BattleVirusAndCreatureTech.rdtest.txt index 8c7cb057..aa1ca15f 100644 --- a/tests/GC-Episode3BattleVirusAndCreatureTech.rdtest.txt +++ b/tests/GC-Episode3BattleVirusAndCreatureTech.rdtest.txt @@ -78,7 +78,7 @@ I 94811 2024-09-01 16:33:07 - [C-1] Sending quest file chunk quest88532.bin:0 I 94811 2024-09-01 16:33:07 - [Commands] Sending to C-1 @ ipss:N-1:127.0.0.1:51929 (version=GC_EP3 command=13 flag=00) 0000 | 13 00 18 04 71 75 65 73 74 38 38 35 33 32 2E 62 | quest88532.b 0010 | 69 6E 00 00 3F D4 01 00 00 8C 04 04 FC A0 FC 86 | in ? -0020 | FF F1 D4 FF 00 47 43 20 45 70 33 20 FF 55 53 41 | GC Ep3 USA +0020 | FF F1 D4 FF 59 47 43 20 45 70 33 20 FF 55 53 41 | GC Ep3 USA 0030 | 20 70 61 74 63 FF 68 20 65 6E 61 62 6C 65 EB 72 | patc h enable r 0040 | 00 F8 FF A6 F8 FF FF 09 03 FF 00 40 00 80 49 4C | @ IL 0050 | 2A 45 F0 FB 28 9B 10 80 4F F8 B5 49 50 F4 A7 24 | *E ( O IP $ diff --git a/tests/GC-Episode3BattleWithSpectatorTeam.rdtest.txt b/tests/GC-Episode3BattleWithSpectatorTeam.rdtest.txt index 9195e545..ab6a81ed 100644 --- a/tests/GC-Episode3BattleWithSpectatorTeam.rdtest.txt +++ b/tests/GC-Episode3BattleWithSpectatorTeam.rdtest.txt @@ -78,7 +78,7 @@ I 64538 2024-09-01 15:24:00 - [C-7] Sending quest file chunk quest88532.bin:0 I 64538 2024-09-01 15:24:00 - [Commands] Sending to C-7 @ ipss:N-4: (version=GC_EP3 command=13 flag=00) 0000 | 13 00 18 04 71 75 65 73 74 38 38 35 33 32 2E 62 | quest88532.b 0010 | 69 6E 00 00 3F D4 01 00 00 8C 04 04 FC A0 FC 86 | in ? -0020 | FF F1 D4 FF 00 47 43 20 45 70 33 20 FF 55 53 41 | GC Ep3 USA +0020 | FF F1 D4 FF 59 47 43 20 45 70 33 20 FF 55 53 41 | GC Ep3 USA 0030 | 20 70 61 74 63 FF 68 20 65 6E 61 62 6C 65 EB 72 | patc h enable r 0040 | 00 F8 FF A6 F8 FF FF 09 03 FF 00 40 00 80 49 4C | @ IL 0050 | 2A 45 F0 FB 28 9B 10 80 4F F8 B5 49 50 F4 A7 24 | *E ( O IP $ @@ -4657,7 +4657,7 @@ I 64538 2024-09-01 15:24:43 - [C-9] Sending quest file chunk quest88533.bin:0 I 64538 2024-09-01 15:24:43 - [Commands] Sending to C-9 @ ipss:N-6:127.0.0.1:59366 (version=GC_EP3 command=13 flag=00) 0000 | 13 00 18 04 71 75 65 73 74 38 38 35 33 33 2E 62 | quest88533.b 0010 | 69 6E 00 00 3F D4 01 00 00 8C 04 04 FC A0 FC 86 | in ? -0020 | FF F1 D5 7F 00 47 43 20 45 70 33 F8 FC 55 20 70 | GC Ep3 U p +0020 | FF F1 D5 7F 59 47 43 20 45 70 33 F8 FC 55 20 70 | GC Ep3 U p 0030 | 61 74 FF 63 68 20 65 6E 61 62 6C D7 65 72 00 F8 | at ch enabl er 0040 | FF A7 F8 FF FF 09 FF 03 00 40 00 80 49 04 4E E1 | @ I N 0050 | 45 FB B4 9F 10 9F 80 F8 B5 49 08 4E F4 24 C3 00 | E I N $ diff --git a/tests/GC-Episode3CardTrade.rdtest.txt b/tests/GC-Episode3CardTrade.rdtest.txt index d6cf78fb..f20f2162 100644 --- a/tests/GC-Episode3CardTrade.rdtest.txt +++ b/tests/GC-Episode3CardTrade.rdtest.txt @@ -78,7 +78,7 @@ I 54825 2024-11-05 21:17:56 - [C-1] Sending quest file chunk quest88532.bin:0 I 54825 2024-11-05 21:17:56 - [Commands] Sending to C-1 @ ipss:N-1:127.0.0.1:62373 (version=GC_EP3 command=13 flag=00) 0000 | 13 00 18 04 71 75 65 73 74 38 38 35 33 32 2E 62 | quest88532.b 0010 | 69 6E 00 00 3F D4 01 00 00 8C 04 04 FC A0 FC 86 | in ? -0020 | FF F1 D4 FF 00 47 43 20 45 70 33 20 FF 55 53 41 | GC Ep3 USA +0020 | FF F1 D4 FF 59 47 43 20 45 70 33 20 FF 55 53 41 | GC Ep3 USA 0030 | 20 70 61 74 63 FF 68 20 65 6E 61 62 6C 65 EB 72 | patc h enable r 0040 | 00 F8 FF A6 F8 FF FF 09 03 FF 00 40 00 80 49 4C | @ IL 0050 | 2A 45 F0 FB 28 9B 10 80 4F F8 B5 49 50 F4 A7 24 | *E ( O IP $ @@ -5199,7 +5199,7 @@ I 54825 2024-11-05 21:18:16 - [C-3] Sending quest file chunk quest88532.bin:0 I 54825 2024-11-05 21:18:16 - [Commands] Sending to C-3 @ ipss:N-2: (version=GC_EP3 command=13 flag=00) 0000 | 13 00 18 04 71 75 65 73 74 38 38 35 33 32 2E 62 | quest88532.b 0010 | 69 6E 00 00 3F D4 01 00 00 8C 04 04 FC A0 FC 86 | in ? -0020 | FF F1 D4 FF 00 47 43 20 45 70 33 20 FF 55 53 41 | GC Ep3 USA +0020 | FF F1 D4 FF 59 47 43 20 45 70 33 20 FF 55 53 41 | GC Ep3 USA 0030 | 20 70 61 74 63 FF 68 20 65 6E 61 62 6C 65 EB 72 | patc h enable r 0040 | 00 F8 FF A6 F8 FF FF 09 03 FF 00 40 00 80 49 4C | @ IL 0050 | 2A 45 F0 FB 28 9B 10 80 4F F8 B5 49 50 F4 A7 24 | *E ( O IP $