refine quest header format; use metadata from .bin.txt file if present
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,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,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 $
|
||||
|
||||
@@ -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 $
|
||||
|
||||
Reference in New Issue
Block a user