add learnings from Ep3 Trial Edition download quest
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,7 +34,6 @@ struct MapAndRulesState {
|
||||
le_uint32_t map_number;
|
||||
uint32_t unused4;
|
||||
Rules rules;
|
||||
uint32_t unused5;
|
||||
|
||||
MapAndRulesState();
|
||||
void clear();
|
||||
|
||||
+3
-3
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user