add learnings from Ep3 Trial Edition download quest

This commit is contained in:
Martin Michelsen
2023-09-28 14:48:26 -07:00
parent 5c5da8e10b
commit fbdfdb085a
11 changed files with 269 additions and 90 deletions
+2 -1
View File
@@ -131,6 +131,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
| GCI (with key) | .bin.gci and .dat.gci | Yes | decode-gci |
| GCI (no key) | .bin.gci and .dat.gci | Decode (3) | decode-gci (3) |
| GCI (Ep3) | .bin.gci or .mnm.gci | Yes | decode-gci |
| GCI (Ep3 Trial) | .bin.gci or .mnm.gci | Decode (3) | decode-gci (3) |
| DLQ | .bin.dlq and .dat.dlq | Yes | decode-dlq |
| DLQ (Ep3) | .bin.dlq or .mnm.dlq | Yes | decode-dlq |
| QST (online) | .qst | Yes | decode-qst |
@@ -142,7 +143,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
3. *Use the decode action to convert these quests to .bin/.dat format before putting them into the server's quests directory. If you know the encryption seed (serial number), pass it in as a hex string with the `--seed=` option. If you don't know the encryption seed, newserv will find it for you, which will likely take a long time.*
4. *Episode 3 online quests don't go in the system/quests directory; they instead go in the system/ep3/maps-free or system/ep3/maps-quest directories. If you want an Episode 3 quest to be available for both online play and for downloading, the file must exist in both system/quests and in one of the map directories in system/ep3.*
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst. There are no encrypted Episode 3 GCI formats because the game doesn't encrypt quests saved to the memory card, unlike Episodes 1&2.
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst.
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
-4
View File
@@ -2831,7 +2831,6 @@ struct S_GameInformation_GC_Ep3_E1 {
/* 0024 */ parray<PlayerEntry, 4> player_entries;
/* 00E4 */ parray<uint8_t, 0x20> unknown_a3;
/* 0104 */ Episode3::Rules rules;
/* 0114 */ parray<uint8_t, 4> unknown_a4;
/* 0118 */ parray<PlayerEntry, 8> spectator_entries;
} __packed__;
@@ -2927,8 +2926,6 @@ struct S_TournamentGameDetails_GC_Ep3_E3 {
/* 0024/034C */ ptext<char, 0x20> map_name;
/* 0044/036C */ Episode3::Rules rules;
/* 0054/037C */ parray<uint8_t, 4> unknown_a1;
// This field is used only if the bracket pane is shown
struct BracketEntry {
le_uint16_t win_count = 0;
@@ -6252,7 +6249,6 @@ struct G_SetPlayerSubstatus_GC_Ep3_6xB5x3C {
struct G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D {
G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0};
Episode3::Rules rules;
parray<uint8_t, 4> unknown_a1;
struct Entry {
uint8_t type = 0; // 0 = no player, 1 = human, 2 = COM
ptext<char, 0x10> player_name;
+26 -13
View File
@@ -794,7 +794,8 @@ string prs_compress_indexed(const string& data, ProgressCallback progress_fn) {
return prs_compress_indexed(data.data(), data.size(), progress_fn);
}
PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size) {
PRSDecompressResult prs_decompress_with_meta(
const void* data, size_t size, size_t max_output_size, bool allow_unterminated) {
// PRS is an LZ77-based compression algorithm. Compressed data is split into
// two streams: a control stream and a data stream. The control stream is read
// one bit at a time, and the data stream is read one byte at a time. The
@@ -839,7 +840,11 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size
// Control 1 = literal byte
if (cr.read()) {
if (max_output_size && w.size() == max_output_size) {
throw runtime_error("maximum output size exceeded");
if (allow_unterminated) {
return {std::move(w.str()), r.where()};
} else {
throw runtime_error("maximum output size exceeded");
}
}
w.put_u8(r.get_u8());
@@ -882,7 +887,11 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size
}
for (size_t z = 0; z < count; z++) {
if (max_output_size && w.size() == max_output_size) {
throw runtime_error("maximum output size exceeded");
if (allow_unterminated) {
return {std::move(w.str()), r.where()};
} else {
throw out_of_range("maximum output size exceeded");
}
}
w.put_u8(w.str()[read_offset + z]);
}
@@ -892,21 +901,21 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size
return {std::move(w.str()), r.where()};
}
PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size) {
return prs_decompress_with_meta(data.data(), data.size(), max_output_size);
PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size, bool allow_unterminated) {
return prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated);
}
string prs_decompress(const void* data, size_t size, size_t max_output_size) {
auto ret = prs_decompress_with_meta(data, size, max_output_size);
string prs_decompress(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) {
auto ret = prs_decompress_with_meta(data, size, max_output_size, allow_unterminated);
return std::move(ret.data);
}
string prs_decompress(const string& data, size_t max_output_size) {
auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size);
string prs_decompress(const string& data, size_t max_output_size, bool allow_unterminated) {
auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated);
return std::move(ret.data);
}
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size) {
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) {
size_t ret = 0;
StringReader r(data, size);
ControlStreamReader cr(r);
@@ -943,15 +952,19 @@ size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size
}
if (max_output_size && ret > max_output_size) {
throw runtime_error("maximum output size exceeded");
if (allow_unterminated) {
return max_output_size;
} else {
throw out_of_range("maximum output size exceeded");
}
}
}
return ret;
}
size_t prs_decompress_size(const string& data, size_t max_output_size) {
return prs_decompress_size(data.data(), data.size(), max_output_size);
size_t prs_decompress_size(const string& data, size_t max_output_size, bool allow_unterminated) {
return prs_decompress_size(data.data(), data.size(), max_output_size, allow_unterminated);
}
void prs_disassemble(FILE* stream, const void* data, size_t size) {
+6 -6
View File
@@ -184,15 +184,15 @@ struct PRSDecompressResult {
std::string data;
size_t input_bytes_used;
};
PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0);
PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0);
std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0);
std::string prs_decompress(const std::string& data, size_t max_output_size = 0);
PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
std::string prs_decompress(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
// Returns the decompressed size of PRS-compressed data, without actually
// decompressing it.
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0);
size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0);
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
// Prints the command stream from a PRS-compressed buffer.
void prs_disassemble(FILE* stream, const void* data, size_t size);
+127 -6
View File
@@ -1256,8 +1256,7 @@ void Rules::clear() {
this->dice_exchange_mode = DiceExchangeMode::HIGH_ATK;
this->disable_dice_boost = 0;
this->def_dice_range = 0;
this->unused1 = 0;
this->unused2 = 0;
this->unused.clear(0);
}
string Rules::str() const {
@@ -1396,6 +1395,40 @@ string Rules::str() const {
return "Rules[" + join(tokens, ", ") + "]";
}
RulesTrial::RulesTrial(const Rules& r)
: overall_time_limit(r.overall_time_limit),
phase_time_limit(r.phase_time_limit),
allowed_cards(r.allowed_cards),
atk_dice_max(r.max_dice),
def_dice_max(r.max_dice),
disable_deck_shuffle(r.disable_deck_shuffle),
disable_deck_loop(r.disable_deck_loop),
char_hp(r.char_hp),
hp_type(r.hp_type),
no_assist_cards(r.no_assist_cards),
disable_dialogue(r.disable_dialogue),
dice_exchange_mode(r.dice_exchange_mode) {}
RulesTrial::operator Rules() const {
Rules ret;
ret.overall_time_limit = this->overall_time_limit;
ret.phase_time_limit = this->phase_time_limit;
ret.allowed_cards = this->allowed_cards;
ret.min_dice = 1;
ret.max_dice = this->atk_dice_max;
ret.disable_deck_shuffle = this->disable_deck_shuffle;
ret.disable_deck_loop = this->disable_deck_loop;
ret.char_hp = this->char_hp;
ret.hp_type = this->hp_type;
ret.no_assist_cards = this->no_assist_cards;
ret.disable_dialogue = this->disable_dialogue;
ret.dice_exchange_mode = this->dice_exchange_mode;
ret.disable_dice_boost = 0;
ret.def_dice_range = 0x10 | (this->def_dice_max ? this->def_dice_max : 0x06);
ret.unused.clear(0);
return ret;
}
StateFlags::StateFlags() {
this->clear();
}
@@ -1518,9 +1551,6 @@ string MapDefinition::str(const CardIndex* card_index) const {
" a5[0x70:0x74]=%02hhX %02hhX %02hhX %02hhX",
this->unknown_a5[0x70], this->unknown_a5[0x71], this->unknown_a5[0x72], this->unknown_a5[0x73]));
lines.emplace_back(" default_rules: " + this->default_rules.str());
lines.emplace_back(string_printf(
" a6=%02hhX %02hhX %02hhX %02hhX",
this->unknown_a6[0], this->unknown_a6[1], this->unknown_a6[2], this->unknown_a6[3]));
lines.emplace_back(" name: " + string(this->name));
lines.emplace_back(" location_name: " + string(this->location_name));
lines.emplace_back(" quest_name: " + string(this->quest_name));
@@ -1715,7 +1745,6 @@ MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map)
modification_tiles(map.modification_tiles),
unknown_a5(map.unknown_a5),
default_rules(map.default_rules),
unknown_a6(map.unknown_a6),
name(map.name),
location_name(map.location_name),
quest_name(map.quest_name),
@@ -1744,6 +1773,98 @@ MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map)
}
}
MapDefinitionTrial::operator MapDefinition() const {
MapDefinition ret;
ret.unknown_a1 = this->unknown_a1;
ret.map_number = this->map_number;
ret.width = this->width;
ret.height = this->height;
ret.environment_number = this->environment_number;
ret.num_camera_zones = this->num_camera_zones;
ret.map_tiles = this->map_tiles;
ret.start_tile_definitions = this->start_tile_definitions;
ret.camera_zone_maps = this->camera_zone_maps;
ret.camera_zone_specs = this->camera_zone_specs;
ret.overview_specs = this->overview_specs;
ret.modification_tiles = this->modification_tiles;
ret.unknown_a5.clear(0xFF);
ret.unknown_a5 = this->unknown_a5;
ret.default_rules = this->default_rules;
ret.name = this->name;
ret.location_name = this->location_name;
ret.quest_name = this->quest_name;
ret.description = this->description;
ret.map_x = this->map_x;
ret.map_y = this->map_y;
ret.npc_decks = this->npc_decks;
ret.npc_ai_params = this->npc_ai_params;
ret.unknown_a7 = this->unknown_a7;
ret.npc_ai_params_entry_index = this->npc_ai_params_entry_index;
ret.before_message = this->before_message;
ret.after_message = this->after_message;
ret.dispatch_message = this->dispatch_message;
for (size_t z = 0; z < ret.dialogue_sets.size(); z++) {
ret.dialogue_sets[z].sub<8>(0) = this->dialogue_sets[z];
for (size_t x = 8; x < ret.dialogue_sets[z].size(); x++) {
ret.dialogue_sets[z][x].unknown_a1 = 0xFFFF;
ret.dialogue_sets[z][x].unknown_a2 = 0xFFFF;
for (size_t w = 0; w < 4; w++) {
ret.dialogue_sets[z][x].strings[w].clear(0xFF);
ret.dialogue_sets[z][x].strings[w][0] = 0x00;
}
}
}
ret.reward_card_ids = this->reward_card_ids;
ret.win_level_override = this->win_level_override;
ret.loss_level_override = this->loss_level_override;
ret.field_offset_x = this->field_offset_x;
ret.field_offset_y = this->field_offset_y;
ret.map_category = this->map_category;
ret.cyber_block_type = this->cyber_block_type;
ret.unknown_a11 = this->unknown_a11;
ret.unavailable_sc_cards.clear(0xFFFF);
// The trial edition doesn't seem to have entry_states at all, so we have to
// guess and fill in the field appropriately here.
size_t num_npc_decks = 0;
for (size_t z = 0; z < ret.npc_decks.size(); z++) {
if (ret.npc_decks[z].name[0]) {
num_npc_decks++;
}
}
for (size_t z = 0; z < 4; z++) {
ret.entry_states[z].deck_type = 0xFF;
}
switch (num_npc_decks) {
case 0: // No NPCs; it's a free battle map
ret.entry_states[0].player_type = 0xFF;
ret.entry_states[1].player_type = 0xFF;
ret.entry_states[2].player_type = 0xFF;
ret.entry_states[3].player_type = 0xFF;
break;
case 1: // One NPC; assume it's a 1v1 quest (Player vs. COM)
ret.entry_states[0].player_type = 0x00;
ret.entry_states[1].player_type = 0x04;
ret.entry_states[2].player_type = 0x03;
ret.entry_states[3].player_type = 0x04;
break;
case 2: // Two NPCs; assume it's a 2v2 quest (Player+Player/COM vs. COM+COM)
ret.entry_states[0].player_type = 0x00;
ret.entry_states[1].player_type = 0x02;
ret.entry_states[2].player_type = 0x03;
ret.entry_states[3].player_type = 0x03;
break;
case 3: // Three NPCs; assume it's a 2v2 quest (Player+COM vs. COM+COM)
ret.entry_states[0].player_type = 0x00;
ret.entry_states[1].player_type = 0x03;
ret.entry_states[2].player_type = 0x03;
ret.entry_states[3].player_type = 0x03;
break;
default: // Should be impossible
throw logic_error("too many NPC decks in trial map definition");
}
return ret;
}
bool Rules::check_invalid_fields() const {
Rules t = *this;
return t.check_and_reset_invalid_fields();
+27 -7
View File
@@ -879,9 +879,8 @@ struct Rules {
// NOTE: The following fields are unused in PSO's implementation, but newserv
// uses them to implement extended rules.
/* 0D */ uint8_t def_dice_range = 0; // High 4 bits = min, low 4 = max
/* 0E */ uint8_t unused1 = 0;
/* 0F */ uint8_t unused2 = 0;
/* 10 */
/* 0E */ parray<uint8_t, 6> unused;
/* 14 */
// Annoyingly, this structure is a different size in Episode 3 Trial Edition.
// This means that many command formats, as well as the map format, are
@@ -906,6 +905,28 @@ struct Rules {
std::string str() const;
} __attribute__((packed));
struct RulesTrial {
// The fields here have the same meaning as in the final version. The only
// difference is that Dice Boost does not exist in the trial version.
/* 00 */ uint8_t overall_time_limit = 0;
/* 01 */ uint8_t phase_time_limit = 0;
/* 02 */ AllowedCards allowed_cards = AllowedCards::ALL;
/* 03 */ uint8_t atk_dice_max = 1;
/* 04 */ uint8_t def_dice_max = 6;
/* 05 */ uint8_t disable_deck_shuffle = 0;
/* 06 */ uint8_t disable_deck_loop = 0;
/* 07 */ uint8_t char_hp = 15;
/* 08 */ HPType hp_type = HPType::DEFEAT_PLAYER;
/* 09 */ uint8_t no_assist_cards = 0;
/* 0A */ uint8_t disable_dialogue = 0;
/* 0B */ DiceExchangeMode dice_exchange_mode = DiceExchangeMode::HIGH_ATK;
/* 0C */
RulesTrial() = default;
explicit RulesTrial(const Rules&);
operator Rules() const;
} __attribute__((packed));
struct StateFlags {
/* 00 */ le_uint16_t turn_num;
/* 02 */ BattlePhase battle_phase;
@@ -1090,7 +1111,6 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
/* 1D68 */ parray<uint8_t, 0x74> unknown_a5;
/* 1DDC */ Rules default_rules;
/* 1DEC */ parray<uint8_t, 4> unknown_a6;
/* 1DF0 */ ptext<char, 0x14> name;
/* 1E04 */ ptext<char, 0x14> location_name;
@@ -1261,9 +1281,8 @@ struct MapDefinitionTrial {
/* 1518 */ parray<parray<MapDefinition::CameraSpec, 10>, 2> camera_zone_specs;
/* 1AB8 */ parray<parray<MapDefinition::CameraSpec, 2>, 3> overview_specs;
/* 1C68 */ parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
/* 1D68 */ parray<uint8_t, 0x6C> unknown_a5;
/* 1DD4 */ Rules default_rules;
/* 1DE4 */ parray<uint8_t, 4> unknown_a6;
/* 1D68 */ parray<uint8_t, 0x74> unknown_a5;
/* 1DD4 */ RulesTrial default_rules;
/* 1DE8 */ ptext<char, 0x14> name;
/* 1DFC */ ptext<char, 0x14> location_name;
/* 1E10 */ ptext<char, 0x3C> quest_name;
@@ -1292,6 +1311,7 @@ struct MapDefinitionTrial {
/* 41A0 */
MapDefinitionTrial(const MapDefinition& map);
operator MapDefinition() const;
} __attribute__((packed));
struct COMDeckDefinition {
-1
View File
@@ -47,7 +47,6 @@ void MapAndRulesState::clear() {
this->map_number = 0;
this->unused4 = 0;
this->rules.clear();
this->unused5 = 0;
}
bool MapAndRulesState::loc_is_within_bounds(uint8_t x, uint8_t y) const {
-1
View File
@@ -34,7 +34,6 @@ struct MapAndRulesState {
le_uint32_t map_number;
uint32_t unused4;
Rules rules;
uint32_t unused5;
MapAndRulesState();
void clear();
+3 -3
View File
@@ -756,7 +756,7 @@ int main(int argc, char** argv) {
data = prs_compress(data, compression_level, progress_fn);
}
} else if ((behavior == Behavior::DECOMPRESS_PRS) || (behavior == Behavior::DECOMPRESS_PR2)) {
data = prs_decompress(data);
data = prs_decompress(data, bytes, (bytes != 0));
} else if (behavior == Behavior::COMPRESS_BC0) {
if (compress_optimal) {
data = bc0_compress_optimal(data.data(), data.size(), optimal_progress_fn);
@@ -1323,11 +1323,11 @@ int main(int argc, char** argv) {
string output_filename_base = input_filename;
if (quest_file_type == Quest::FileFormat::BIN_DAT_GCI) {
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
auto decoded = Quest::decode_gci_file(input_filename, num_threads, dec_seed);
auto decoded = Quest::decode_gci_file(input_filename, num_threads, dec_seed, skip_checksum);
save_file(output_filename_base + ".dec", decoded);
} else if (quest_file_type == Quest::FileFormat::BIN_DAT_VMS) {
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
auto decoded = Quest::decode_vms_file(input_filename, num_threads, dec_seed);
auto decoded = Quest::decode_vms_file(input_filename, num_threads, dec_seed, skip_checksum);
save_file(output_filename_base + ".dec", decoded);
} else if (quest_file_type == Quest::FileFormat::BIN_DAT_DLQ) {
auto decoded = Quest::decode_dlq_file(input_filename);
+74 -46
View File
@@ -83,7 +83,8 @@ struct PSOGCIDLQFileEncryptedHeader : PSOMemCardDLQFileEncryptedHeader<true> {
} __attribute__((packed));
template <bool IsBigEndian>
string decrypt_download_quest_data_section(const void* data_section, size_t size, uint32_t seed) {
string decrypt_download_quest_data_section(
const void* data_section, size_t size, uint32_t seed, bool skip_checksum = false, bool is_ep3_trial = false) {
string decrypted = decrypt_data_section<IsBigEndian>(data_section, size, seed);
size_t orig_size = decrypted.size();
@@ -99,41 +100,67 @@ string decrypt_download_quest_data_section(const void* data_section, size_t size
round2_crypt.encrypt_t<IsBigEndian>(
decrypted.data() + 4, (decrypted.size() - 4));
if (header->decompressed_size & 0xFFF00000) {
throw runtime_error(string_printf(
"decompressed_size too large (%08" PRIX32 ")", header->decompressed_size.load()));
if (is_ep3_trial) {
StringReader r(decrypted);
r.skip(16);
if (r.readx(15) != "SONICTEAM,SEGA.") {
throw runtime_error("Episode 3 GCI file is not a quest");
}
r.skip(9);
// Some Ep3 trial download quests don't have a stop opcode in the PRS
// stream; it seems the client just automatically stops when the correct
// amount of data has been produced. To handle this, we allow the PRS stream
// to be unterminated here.
size_t decompressed_size = prs_decompress_size(
r.getv(r.remaining(), false), r.remaining(), sizeof(Episode3::MapDefinitionTrial), true);
if (decompressed_size < sizeof(Episode3::MapDefinitionTrial)) {
throw runtime_error(string_printf(
"decompressed size (%zu) does not match expected size (%zu)",
decompressed_size, sizeof(Episode3::MapDefinitionTrial)));
}
return decrypted.substr(0x28);
} else {
if (header->decompressed_size & 0xFFF00000) {
throw runtime_error(string_printf(
"decompressed_size too large (%08" PRIX32 ")", header->decompressed_size.load()));
}
if (!skip_checksum) {
uint32_t expected_crc = header->checksum;
header->checksum = 0;
uint32_t actual_crc = crc32(decrypted.data(), orig_size);
header->checksum = expected_crc;
if (expected_crc != actual_crc && expected_crc != bswap32(actual_crc)) {
throw runtime_error(string_printf(
"incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32,
expected_crc, actual_crc));
}
}
// Unlike the above rounds, round 3 is always little-endian (it corresponds to
// the round of encryption done on the server before sending the file to the
// client in the first place)
PSOV2Encryption(header->round3_seed).decrypt(decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT));
decrypted.resize(orig_size);
// Some download quest GCI files have decompressed_size fields that are 8
// bytes smaller than the actual decompressed size of the data. They seem to
// work fine, so we accept both cases as correct.
size_t decompressed_size = prs_decompress_size(
decrypted.data() + sizeof(HeaderT),
decrypted.size() - sizeof(HeaderT));
size_t expected_decompressed_size = header->decompressed_size.load();
if ((decompressed_size != expected_decompressed_size) &&
(decompressed_size != expected_decompressed_size - 8)) {
throw runtime_error(string_printf(
"decompressed size (%zu) does not match expected size (%zu)",
decompressed_size, expected_decompressed_size));
}
return decrypted.substr(sizeof(HeaderT));
}
uint32_t expected_crc = header->checksum;
header->checksum = 0;
uint32_t actual_crc = crc32(decrypted.data(), orig_size);
header->checksum = expected_crc;
if (expected_crc != actual_crc && expected_crc != bswap32(actual_crc)) {
throw runtime_error(string_printf(
"incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32,
expected_crc, actual_crc));
}
// Unlike the above rounds, round 3 is always little-endian (it corresponds to
// the round of encryption done on the server before sending the file to the
// client in the first place)
PSOV2Encryption(header->round3_seed).decrypt(decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT));
decrypted.resize(orig_size);
// Some download quest GCI files have decompressed_size fields that are 8
// bytes smaller than the actual decompressed size of the data. They seem to
// work fine, so we accept both cases as correct.
size_t decompressed_size = prs_decompress_size(
decrypted.data() + sizeof(HeaderT),
decrypted.size() - sizeof(HeaderT));
if ((decompressed_size != header->decompressed_size) &&
(decompressed_size != header->decompressed_size - 8)) {
throw runtime_error(string_printf(
"decompressed size (%zu) does not match size in header (%" PRId32 ")",
decompressed_size, header->decompressed_size.load()));
}
return decrypted.substr(sizeof(HeaderT));
}
string decrypt_vms_v1_data_section(const void* data_section, size_t size) {
@@ -160,16 +187,17 @@ string decrypt_vms_v1_data_section(const void* data_section, size_t size) {
template <bool IsBigEndian>
string find_seed_and_decrypt_download_quest_data_section(
const void* data_section, size_t size, size_t num_threads) {
const void* data_section, size_t size, bool skip_checksum, bool is_ep3_trial, size_t num_threads) {
mutex result_lock;
string result;
uint64_t result_seed = parallel_range<uint64_t>([&](uint64_t seed, size_t) {
try {
string ret = decrypt_download_quest_data_section<IsBigEndian>(data_section, size, seed);
string ret = decrypt_download_quest_data_section<IsBigEndian>(
data_section, size, seed, skip_checksum, is_ep3_trial);
lock_guard<mutex> g(result_lock);
result = std::move(ret);
return true;
} catch (const runtime_error&) {
} catch (const runtime_error& e) {
return false;
}
},
@@ -474,7 +502,7 @@ shared_ptr<const string> Quest::dat_contents() const {
}
string Quest::decode_gci_file(
const string& filename, ssize_t find_seed_num_threads, int64_t known_seed) {
const string& filename, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) {
string data = load_file(filename);
StringReader r(data);
@@ -489,11 +517,11 @@ string Quest::decode_gci_file(
if (dlq_header.round2_seed || dlq_header.checksum || dlq_header.round3_seed) {
if (known_seed >= 0) {
return decrypt_download_quest_data_section<true>(
r.getv(header.data_size), header.data_size, known_seed);
r.getv(header.data_size), header.data_size, known_seed, skip_checksum, false);
} else if (header.embedded_seed != 0) {
return decrypt_download_quest_data_section<true>(
r.getv(header.data_size), header.data_size, header.embedded_seed);
r.getv(header.data_size), header.data_size, header.embedded_seed, skip_checksum, false);
} else {
if (find_seed_num_threads < 0) {
@@ -503,7 +531,7 @@ string Quest::decode_gci_file(
find_seed_num_threads = thread::hardware_concurrency();
}
return find_seed_and_decrypt_download_quest_data_section<true>(
r.getv(header.data_size), header.data_size, find_seed_num_threads);
r.getv(header.data_size), header.data_size, skip_checksum, false, find_seed_num_threads);
}
} else { // Unencrypted GCI format
@@ -525,7 +553,7 @@ string Quest::decode_gci_file(
if (header.is_trial()) {
if (known_seed >= 0) {
return decrypt_download_quest_data_section<true>(
r.getv(header.data_size), header.data_size, known_seed);
r.getv(header.data_size), header.data_size, known_seed, true, true);
} else {
if (find_seed_num_threads < 0) {
throw runtime_error("file is encrypted");
@@ -534,7 +562,7 @@ string Quest::decode_gci_file(
find_seed_num_threads = thread::hardware_concurrency();
}
return find_seed_and_decrypt_download_quest_data_section<true>(
r.getv(header.data_size), header.data_size, find_seed_num_threads);
r.getv(header.data_size), header.data_size, true, true, find_seed_num_threads);
}
} else {
@@ -575,7 +603,7 @@ string Quest::decode_gci_file(
}
string Quest::decode_vms_file(
const string& filename, ssize_t find_seed_num_threads, int64_t known_seed) {
const string& filename, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) {
string data = load_file(filename);
StringReader r(data);
@@ -603,7 +631,7 @@ string Quest::decode_vms_file(
find_seed_num_threads = thread::hardware_concurrency();
}
return find_seed_and_decrypt_download_quest_data_section<false>(
data_section, header.data_size, find_seed_num_threads);
data_section, header.data_size, skip_checksum, 0, find_seed_num_threads);
}
}
+4 -2
View File
@@ -93,11 +93,13 @@ public:
static std::string decode_gci_file(
const std::string& filename,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1);
int64_t known_seed = -1,
bool skip_checksum = false);
static std::string decode_vms_file(
const std::string& filename,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1);
int64_t known_seed = -1,
bool skip_checksum = false);
static std::string decode_dlq_file(const std::string& filename);
static std::string decode_dlq_data(const std::string& filename);
static std::pair<std::string, std::string> decode_qst_file(const std::string& filename);