refine quest header format; use metadata from .bin.txt file if present

This commit is contained in:
Martin Michelsen
2025-02-27 00:17:41 -08:00
parent 78fe4ebf98
commit 4d7a3395ba
16 changed files with 489 additions and 407 deletions
+3
View File
@@ -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);
}
+4 -4
View File
@@ -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);
+10 -5
View File
@@ -1441,7 +1441,11 @@ Action a_encode_qst(
} catch (const phosg::cannot_open_file&) {
}
auto vq = make_shared<VersionedQuest>(0, 0, version, 0, bin_data, dat_data, nullptr, pvr_data);
auto vq = make_shared<VersionedQuest>();
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<bool>("decompressed");
if (compress) {
if (args.get<bool>("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(
+52 -52
View File
@@ -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> 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<Object> 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<Object> 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<size_t>(set_entry->param5, 0, 4));
num_switch_flags = (4 - clamp<size_t>(set_entry->iparam5, 0, 4));
break;
case 0x014A: // TODoorAncient08
num_switch_flags = 4;
@@ -2078,13 +2078,13 @@ shared_ptr<SuperMap::Enemy> 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::Enemy> SuperMap::add_enemy_and_children(
break;
case 0x0044: { // TObjEneBeast
static const EnemyType types[3] = {EnemyType::BOOMA, EnemyType::GOBOOMA, EnemyType::GIGOBOOMA};
add(types[clamp<int16_t>(set_entry->iparam1, 0, 2)]);
add(types[clamp<int16_t>(set_entry->iparam6, 0, 2)]);
break;
}
case 0x0060: // TObjGrass
@@ -2122,13 +2122,13 @@ shared_ptr<SuperMap::Enemy> 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<int16_t>(set_entry->iparam1, 0, 2)]);
add(types[clamp<int16_t>(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::Enemy> 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::Enemy> 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<int16_t>(set_entry->iparam1, 0, 2)]);
add(types[clamp<int16_t>(set_entry->iparam6, 0, 2)]);
break;
}
case 0x00A7: // TObjEneBalClawBody
@@ -2277,14 +2277,14 @@ shared_ptr<SuperMap::Enemy> 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::Enemy> 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::Enemy> 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::Enemy> 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::Enemy> 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::Enemy> 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<int16_t>(set_entry->iparam1, 0, 2)]);
add(types[clamp<int16_t>(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<int16_t>(set_entry->iparam1, 0, 2)]);
add(types[clamp<int16_t>(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<PSOLFGEncryptio
}
if (type == EnemyType::MERICARAND) {
// On v3, Mericarols that have uparam1 > 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.)
+10 -10
View File
@@ -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;
+2 -2
View File
@@ -1043,8 +1043,8 @@ static HandlerResult SC_6x60_6xA2(shared_ptr<ProxyServer::LinkedSession> 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);
+300 -238
View File
@@ -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<const std::string> bin_contents,
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const MapFile> map_file,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules,
ssize_t challenge_template_index,
uint8_t description_flag,
std::shared_ptr<const IntegralExpression> available_expression,
std::shared_ptr<const IntegralExpression> 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<const PSOQuestHeaderDCNTE*>(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<const PSOQuestHeaderDC*>(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<const PSOQuestHeaderPC*>(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<const Episode3::MapDefinition*>(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<const PSOQuestHeaderGC*>(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<const PSOQuestHeaderBB*>(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<const VersionedQuest> 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<const VersionedQuest> 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<const string> data;
};
struct BINFileData {
string filename;
unique_ptr<QuestMetadata> metadata;
shared_ptr<const string> data;
};
struct DATFileData {
string filename;
shared_ptr<const string> data;
shared_ptr<const MapFile> map_file;
};
map<string, FileData> bin_files;
map<string, BINFileData> bin_files;
map<string, DATFileData> dat_files;
map<string, FileData> pvr_files;
map<string, FileData> 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<string>(std::move(value));
auto data_ptr = make_shared<string>(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<QuestMetadata>(*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<string>(std::move(data));
auto map_file = make_shared<MapFile>(make_shared<string>(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<AssembledQuestScript> 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<AssembledQuestScript>(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<VersionedQuest>();
// 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<string, Version> 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<const PSOQuestHeaderDCNTE*>(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<const PSOQuestHeaderDC*>(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<const PSOQuestHeaderPC*>(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<const Episode3::MapDefinition*>(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<const PSOQuestHeaderGC*>(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<const PSOQuestHeaderBB*>(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<string, Version> 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<BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
uint8_t description_flag = 0;
shared_ptr<const IntegralExpression> available_expression;
shared_ptr<const IntegralExpression> 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<BattleRules>(metadata_json.at("BattleRules"));
vq->battle_rules = make_shared<BattleRules>(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<IntegralExpression>(metadata_json.get_string("AvailableIf"));
vq->available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
} catch (const out_of_range&) {
}
try {
enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
vq->enabled_expression = make_shared<IntegralExpression>(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<VersionedQuest>(
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());
}
}
}
+15 -27
View File
@@ -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<const std::string> bin_contents;
@@ -82,27 +82,14 @@ struct VersionedQuest {
std::shared_ptr<const std::string> pvr_contents;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
uint8_t description_flag = 0;
uint8_t description_flag = 0x00;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> 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<const std::string> bin_contents,
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const MapFile> map_file,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules = nullptr,
ssize_t challenge_template_index = -1,
uint8_t description_flag = 0,
std::shared_ptr<const IntegralExpression> available_expression = nullptr,
std::shared_ptr<const IntegralExpression> 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<const SuperMap> supermap;
+19 -18
View File
@@ -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<PSOQuestHeaderGC>();
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<Episode> 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<shared_ptr<Register>, 0x100> numbered_regs;
};
std::string assemble_quest_script(
AssembledQuestScript assemble_quest_script(
const std::string& text,
const vector<string>& script_include_directories,
const vector<string>& 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,
},
};
}
+59 -36
View File
@@ -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<TextEncoding::SJIS, 0x10> 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<TextEncoding::MARKED, 0x20> name;
/* 0034 */ pstring<TextEncoding::MARKED, 0x80> short_description;
/* 00B4 */ pstring<TextEncoding::MARKED, 0x120> 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<TextEncoding::UTF16, 0x20> name;
/* 0054 */ pstring<TextEncoding::UTF16, 0x80> short_description;
/* 0154 */ pstring<TextEncoding::UTF16, 0x120> 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<TextEncoding::MARKED, 0x20> name;
/* 0034 */ pstring<TextEncoding::MARKED, 0x80> short_description;
/* 00B4 */ pstring<TextEncoding::MARKED, 0x120> 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<TextEncoding::UTF16, 0x20> name;
/* 0058 */ pstring<TextEncoding::UTF16, 0x80> short_description;
/* 0158 */ pstring<TextEncoding::UTF16, 0x120> 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<std::string>& script_include_directories,
const std::vector<std::string>& native_include_directories);
+8 -8
View File
@@ -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<Client> 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)",
+1 -1
View File
@@ -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."
+1 -1
View File
@@ -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!"
@@ -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 $
@@ -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 $
+2 -2
View File
@@ -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 $