implement quest version separation
@@ -107,13 +107,16 @@ To use newserv in other ways (e.g. for translating data), see the end of this do
|
||||
|
||||
newserv automatically finds quests in the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's "save files" option, just put them in that directory and name them appropriately.
|
||||
|
||||
Standard quest files should be named like `q###-CATEGORY-VERSION.EXT`, battle quests should be named like `b###-VERSION.EXT`, challenge quests should be named like `c###-VERSION.EXT` for Episode 1 or `d###-VERSION.EXT` for Episode 2, and Episode 3 download quests should be named like `e###-gc3.EXT`. The fields in each filename are:
|
||||
Standard quest files should be named like `q###-CATEGORY-VERSION-LANGUAGE.EXT`, battle quests should be named like `b###-VERSION-LANGUAGE.EXT`, challenge quests should be named like `c###-VERSION-LANGUAGE.EXT` for Episode 1 or `d###-VERSION-LANGUAGE.EXT` for Episode 2, and Episode 3 download quests should be named like `e###-gc3-LANGUAGE.EXT`. The fields in each filename are:
|
||||
- `###`: quest number (this doesn't really matter; it should just be unique across the PSO version)
|
||||
- `CATEGORY`: ret = Retrieval, ext = Extermination, evt = Events, shp = Shops, vr = VR, twr = Tower, gv1/gv2/gv4 = Government (BB only), dl = Download (these don't appear during online play), 1p = Solo (BB only)
|
||||
- `VERSION`: dn = Dreamcast NTE, d1 = Dreamcast v1, dc = Dreamcast v2, pc = PC, gcn = GameCube Trial Edition, gc = GameCube Episodes 1 & 2, gc3 = Episode 3, xb = Xbox, bb = Blue Burst
|
||||
- `LANGUAGE`: j = Japanese, e = English, g = German, f = French, s = Spanish
|
||||
- `EXT`: file extension (see table below)
|
||||
|
||||
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-ret-gc.bin` and `q058-ret-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, and it puts them in the Retrieval category because the filenames contain `-ret`.
|
||||
On .dat files, the `LANGUAGE` token may be omitted. If it's present, then that .dat file will only be used for that version of the quest; if omitted, then that .dat file will be used for all versions of the quest.
|
||||
|
||||
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-ret-gc-e.bin` and `q058-ret-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the filenames contain `-ret`.
|
||||
|
||||
The type identifiers (`b`, `c`, `d`, `e`, or `q`) and categories are configurable. See QuestCategories in config.example.json for more information on how to make new categories or edit the existing categories.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- Make reloading happen on separate threads so compression doesn't block active clients
|
||||
- Implement decrypt/encrypt actions for VMS files
|
||||
- Fix Word Select mapping across versions
|
||||
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
|
||||
|
||||
## Episode 3
|
||||
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
#include "ServerState.hh"
|
||||
|
||||
void on_chat_command(std::shared_ptr<Client> c, const std::u16string& text);
|
||||
void on_chat_command(shared_ptr<ProxyServer::LinkedSession> ses, const std::u16string& text);
|
||||
void on_chat_command(std::shared_ptr<ProxyServer::LinkedSession> ses, const std::u16string& text);
|
||||
|
||||
@@ -182,7 +182,7 @@ struct Client : public std::enable_shared_from_this<Client> {
|
||||
std::unordered_map<std::string, std::shared_ptr<const std::string>> sending_files;
|
||||
|
||||
Client(
|
||||
shared_ptr<Server> server,
|
||||
std::shared_ptr<Server> server,
|
||||
struct bufferevent* bev,
|
||||
GameVersion version,
|
||||
ServerBehavior server_behavior);
|
||||
@@ -190,6 +190,9 @@ struct Client : public std::enable_shared_from_this<Client> {
|
||||
|
||||
void reschedule_ping_and_timeout_events();
|
||||
|
||||
inline uint8_t language() const {
|
||||
return this->game_data.player()->inventory.language;
|
||||
}
|
||||
inline GameVersion version() const {
|
||||
return this->channel.version;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
CommonItemSet::CommonItemSet(shared_ptr<const string> data)
|
||||
: gsl(data, true) {}
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ struct ProbabilityTable {
|
||||
|
||||
void push(ItemT item) {
|
||||
if (this->count == MaxCount) {
|
||||
throw runtime_error("push to full probability table");
|
||||
throw std::runtime_error("push to full probability table");
|
||||
}
|
||||
this->items[this->count++] = item;
|
||||
}
|
||||
|
||||
ItemT pop() {
|
||||
if (this->count == 0) {
|
||||
throw runtime_error("pop from empty probability table");
|
||||
throw std::runtime_error("pop from empty probability table");
|
||||
}
|
||||
return this->items[--this->count];
|
||||
}
|
||||
@@ -41,7 +41,7 @@ struct ProbabilityTable {
|
||||
|
||||
ItemT sample(PSOLFGEncryption& random_crypt) const {
|
||||
if (this->count == 0) {
|
||||
throw runtime_error("pop from empty probability table");
|
||||
throw std::runtime_error("pop from empty probability table");
|
||||
} else if (this->count == 1) {
|
||||
return this->items[0];
|
||||
} else {
|
||||
@@ -303,7 +303,7 @@ public:
|
||||
using WeightTableEntry32 = WeightTableEntry<be_uint32_t>;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<const string> data;
|
||||
std::shared_ptr<const std::string> data;
|
||||
StringReader r;
|
||||
|
||||
struct TableSpec {
|
||||
@@ -320,7 +320,7 @@ protected:
|
||||
const T* entries = &r.pget<T>(
|
||||
spec.offset + index * spec.entries_per_table * sizeof(T),
|
||||
spec.entries_per_table * sizeof(T));
|
||||
return make_pair(entries, spec.entries_per_table);
|
||||
return std::make_pair(entries, spec.entries_per_table);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -418,7 +418,7 @@ private:
|
||||
uint8_t section_id) const;
|
||||
int8_t get_luck(uint32_t start_offset, uint8_t delta_index) const;
|
||||
|
||||
std::shared_ptr<const string> data;
|
||||
std::shared_ptr<const std::string> data;
|
||||
StringReader r;
|
||||
|
||||
struct DeltaProbabilityEntry {
|
||||
|
||||
@@ -1536,6 +1536,91 @@ void StateFlags::clear_FF() {
|
||||
this->client_sc_card_types.clear(CardType::INVALID_FF);
|
||||
}
|
||||
|
||||
void MapDefinition::assert_semantically_equivalent(const MapDefinition& other) const {
|
||||
if (this->map_number != other.map_number) {
|
||||
throw runtime_error("map number not equal");
|
||||
}
|
||||
if (this->width != other.width) {
|
||||
throw runtime_error("width not equal");
|
||||
}
|
||||
if (this->height != other.height) {
|
||||
throw runtime_error("width not equal");
|
||||
}
|
||||
if (this->environment_number != other.environment_number) {
|
||||
throw runtime_error("environment number not equal");
|
||||
}
|
||||
if (this->map_tiles != other.map_tiles) {
|
||||
throw runtime_error("tiles not equal");
|
||||
}
|
||||
if (this->start_tile_definitions != other.start_tile_definitions) {
|
||||
throw runtime_error("start tile definitions not equal");
|
||||
}
|
||||
if (this->modification_tiles != other.modification_tiles) {
|
||||
throw runtime_error("modification tiles not equal");
|
||||
}
|
||||
if (this->unknown_a5 != other.unknown_a5) {
|
||||
throw runtime_error("unknown_a5 not equal");
|
||||
}
|
||||
if (this->default_rules != other.default_rules) {
|
||||
throw runtime_error("default rules not equal");
|
||||
}
|
||||
for (size_t z = 0; z < this->npc_decks.size(); z++) {
|
||||
if (this->npc_decks[z].card_ids != other.npc_decks[z].card_ids) {
|
||||
throw runtime_error("npc deck card IDs not equal");
|
||||
}
|
||||
const auto& this_ai_params = this->npc_ai_params[z];
|
||||
const auto& other_ai_params = other.npc_ai_params[z];
|
||||
if (this_ai_params.unknown_a1 != other_ai_params.unknown_a1) {
|
||||
throw runtime_error("npc AI params unknown_a1 not equal");
|
||||
}
|
||||
if (this_ai_params.is_arkz != other_ai_params.is_arkz) {
|
||||
throw runtime_error("npc AI params is_arkz not equal");
|
||||
}
|
||||
if (this_ai_params.unknown_a2 != other_ai_params.unknown_a2) {
|
||||
throw runtime_error("npc AI params unknown_a2 not equal");
|
||||
}
|
||||
if (this_ai_params.params != other_ai_params.params) {
|
||||
throw runtime_error("npc AI params not equal");
|
||||
}
|
||||
}
|
||||
if (this->unknown_a7 != other.unknown_a7) {
|
||||
throw runtime_error("unknown_a7 not equal");
|
||||
}
|
||||
if (this->npc_ai_params_entry_index != other.npc_ai_params_entry_index) {
|
||||
throw runtime_error("npc AI params entry indexes not equal");
|
||||
}
|
||||
if (this->reward_card_ids != other.reward_card_ids) {
|
||||
throw runtime_error("reward card IDs not equal");
|
||||
}
|
||||
if (this->win_level_override != other.win_level_override) {
|
||||
throw runtime_error("win level override not equal");
|
||||
}
|
||||
if (this->loss_level_override != other.loss_level_override) {
|
||||
throw runtime_error("loss level override not equal");
|
||||
}
|
||||
if (this->field_offset_x != other.field_offset_x) {
|
||||
throw runtime_error("field x offset not equal");
|
||||
}
|
||||
if (this->field_offset_y != other.field_offset_y) {
|
||||
throw runtime_error("field y offset not equal");
|
||||
}
|
||||
if (this->map_category != other.map_category) {
|
||||
throw runtime_error("map category not equal");
|
||||
}
|
||||
if (this->cyber_block_type != other.cyber_block_type) {
|
||||
throw runtime_error("cyber block type not equal");
|
||||
}
|
||||
if (this->unknown_a11 != other.unknown_a11) {
|
||||
throw runtime_error("unknown_a11 not equal");
|
||||
}
|
||||
if (this->unavailable_sc_cards != other.unavailable_sc_cards) {
|
||||
throw runtime_error("unavailable SC cards not equal");
|
||||
}
|
||||
if (this->entry_states != other.entry_states) {
|
||||
throw runtime_error("entry states not equal");
|
||||
}
|
||||
}
|
||||
|
||||
string MapDefinition::CameraSpec::str() const {
|
||||
return string_printf(
|
||||
"CameraSpec[a1=(%g %g %g %g %g %g %g %g %g) camera=(%g %g %g) focus=(%g %g %g) a2=(%g %g %g)]",
|
||||
@@ -2294,31 +2379,157 @@ string CardIndex::normalize_card_name(const string& name) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
MapIndex::VersionedMap::VersionedMap(shared_ptr<const MapDefinition> map, uint8_t language)
|
||||
: map(map),
|
||||
language(language) {}
|
||||
|
||||
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
|
||||
: language(language),
|
||||
compressed_data(std::move(compressed_data)) {
|
||||
string decompressed = prs_decompress(this->compressed_data);
|
||||
if (decompressed.size() != sizeof(MapDefinition)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed data size is incorrect (expected %zu bytes, read %zu bytes)",
|
||||
sizeof(MapDefinition), decompressed.size()));
|
||||
}
|
||||
this->map.reset(new MapDefinition(*reinterpret_cast<const MapDefinition*>(decompressed.data())));
|
||||
}
|
||||
|
||||
shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
|
||||
if (!this->trial_map) {
|
||||
this->trial_map.reset(new MapDefinitionTrial(*this->map));
|
||||
}
|
||||
return this->trial_map;
|
||||
}
|
||||
|
||||
const std::string& MapIndex::VersionedMap::compressed(bool is_trial) const {
|
||||
if (is_trial) {
|
||||
if (this->compressed_trial_data.empty()) {
|
||||
auto md = this->trial();
|
||||
this->compressed_trial_data = prs_compress(md.get(), sizeof(*md));
|
||||
}
|
||||
return this->compressed_trial_data;
|
||||
} else {
|
||||
if (this->compressed_data.empty()) {
|
||||
this->compressed_data = prs_compress(this->map.get(), sizeof(*this->map));
|
||||
}
|
||||
return this->compressed_data;
|
||||
}
|
||||
}
|
||||
|
||||
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
|
||||
: map_number(initial_version->map->map_number),
|
||||
initial_version(initial_version) {
|
||||
this->versions.resize(this->initial_version->language + 1);
|
||||
this->versions[this->initial_version->language] = initial_version;
|
||||
}
|
||||
|
||||
void MapIndex::Map::add_version(std::shared_ptr<const VersionedMap> vm) {
|
||||
if (this->versions.size() <= vm->language) {
|
||||
this->versions.resize(vm->language + 1);
|
||||
}
|
||||
if (this->versions[vm->language]) {
|
||||
throw runtime_error("map version already exists");
|
||||
}
|
||||
this->initial_version->map->assert_semantically_equivalent(*vm->map);
|
||||
this->versions[vm->language] = vm;
|
||||
}
|
||||
|
||||
bool MapIndex::Map::has_version(uint8_t language) const {
|
||||
return (this->versions.size() > language) && !!this->versions[language];
|
||||
}
|
||||
|
||||
shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language) const {
|
||||
// If the requested language exists, return it
|
||||
if ((language < this->versions.size()) && this->versions[language]) {
|
||||
return this->versions[language];
|
||||
}
|
||||
// If English exists, return it
|
||||
if ((1 < this->versions.size()) && this->versions[1]) {
|
||||
return this->versions[1];
|
||||
}
|
||||
// Return the first version that exists
|
||||
for (const auto& vm : this->versions) {
|
||||
if (vm) {
|
||||
return vm;
|
||||
}
|
||||
}
|
||||
// This should never happen because Map cannot be constructed without an
|
||||
// initial_version
|
||||
throw logic_error("no map versions exist");
|
||||
}
|
||||
|
||||
MapIndex::MapIndex(const string& directory) {
|
||||
for (const auto& filename : list_directory(directory)) {
|
||||
for (const auto& filename : list_directory_sorted(directory)) {
|
||||
try {
|
||||
shared_ptr<MapEntry> entry;
|
||||
|
||||
string base_filename;
|
||||
string compressed_data;
|
||||
shared_ptr<MapDefinition> decompressed_data;
|
||||
if (ends_with(filename, ".mnmd") || ends_with(filename, ".bind")) {
|
||||
entry.reset(new MapEntry(load_object_file<MapDefinition>(directory + "/" + filename)));
|
||||
decompressed_data.reset(new MapDefinition(load_object_file<MapDefinition>(directory + "/" + filename)));
|
||||
base_filename = filename.substr(0, filename.size() - 5);
|
||||
} else if (ends_with(filename, ".mnm") || ends_with(filename, ".bin")) {
|
||||
entry.reset(new MapEntry(load_file(directory + "/" + filename)));
|
||||
compressed_data = load_file(directory + "/" + filename);
|
||||
base_filename = filename.substr(0, filename.size() - 4);
|
||||
} else if (ends_with(filename, ".bin.gci") || ends_with(filename, ".mnm.gci")) {
|
||||
compressed_data = decode_gci_data(load_file(directory + "/" + filename));
|
||||
base_filename = filename.substr(0, filename.size() - 8);
|
||||
} else if (ends_with(filename, ".gci")) {
|
||||
entry.reset(new MapEntry(decode_gci_file(directory + "/" + filename)));
|
||||
compressed_data = decode_gci_data(load_file(directory + "/" + filename));
|
||||
base_filename = filename.substr(0, filename.size() - 4);
|
||||
} else if (ends_with(filename, ".bin.vms") || ends_with(filename, ".mnm.vms")) {
|
||||
compressed_data = decode_vms_data(load_file(directory + "/" + filename));
|
||||
base_filename = filename.substr(0, filename.size() - 8);
|
||||
} else if (ends_with(filename, ".vms")) {
|
||||
compressed_data = decode_vms_data(load_file(directory + "/" + filename));
|
||||
base_filename = filename.substr(0, filename.size() - 4);
|
||||
} else if (ends_with(filename, ".bin.dlq") || ends_with(filename, ".mnm.dlq")) {
|
||||
compressed_data = decode_dlq_data(load_file(directory + "/" + filename));
|
||||
base_filename = filename.substr(0, filename.size() - 8);
|
||||
} else if (ends_with(filename, ".dlq")) {
|
||||
entry.reset(new MapEntry(decode_dlq_file(directory + "/" + filename)));
|
||||
compressed_data = decode_dlq_data(load_file(directory + "/" + filename));
|
||||
base_filename = filename.substr(0, filename.size() - 4);
|
||||
} else {
|
||||
continue; // Silently skip file
|
||||
}
|
||||
|
||||
if (entry.get()) {
|
||||
if (!this->maps.emplace(entry->map.map_number, entry).second) {
|
||||
throw runtime_error("duplicate map number");
|
||||
}
|
||||
this->maps_by_name.emplace(entry->map.name, entry);
|
||||
string name = entry->map.name;
|
||||
static_game_data_log.info("Indexed Episode 3 %s %s (%08" PRIX32 "; %s)",
|
||||
entry->map.is_quest() ? "online quest" : "free battle map",
|
||||
filename.c_str(), entry->map.map_number.load(), name.c_str());
|
||||
if (base_filename.size() < 2) {
|
||||
throw runtime_error("filename too short for language code");
|
||||
}
|
||||
if (base_filename[base_filename.size() - 2] != '-') {
|
||||
throw runtime_error("language code not present");
|
||||
}
|
||||
uint8_t language = language_code_for_char(base_filename[base_filename.size() - 1]);
|
||||
|
||||
shared_ptr<VersionedMap> vm;
|
||||
if (decompressed_data) {
|
||||
vm.reset(new VersionedMap(decompressed_data, language));
|
||||
} else if (!compressed_data.empty()) {
|
||||
vm.reset(new VersionedMap(std::move(compressed_data), language));
|
||||
} else {
|
||||
throw runtime_error("unknown map file format");
|
||||
}
|
||||
|
||||
string name = format_data_string(vm->map->name);
|
||||
auto map_it = this->maps.find(vm->map->map_number);
|
||||
if (map_it == this->maps.end()) {
|
||||
map_it = this->maps.emplace(vm->map->map_number, new Map(vm)).first;
|
||||
static_game_data_log.info("(%s) Created Episode 3 map %08" PRIX32 " %c (%s; %s)",
|
||||
filename.c_str(),
|
||||
vm->map->map_number.load(),
|
||||
char_for_language_code(vm->language),
|
||||
vm->map->is_quest() ? "quest" : "free",
|
||||
name.c_str());
|
||||
} else {
|
||||
map_it->second->add_version(vm);
|
||||
static_game_data_log.info("(%s) Added Episode 3 map version %08" PRIX32 " %c (%s; %s)",
|
||||
filename.c_str(),
|
||||
vm->map->map_number.load(),
|
||||
char_for_language_code(vm->language),
|
||||
vm->map->is_quest() ? "quest" : "free",
|
||||
name.c_str());
|
||||
}
|
||||
this->maps_by_name.emplace(vm->map->name, map_it->second);
|
||||
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("Failed to index Episode 3 map %s: %s",
|
||||
@@ -2327,51 +2538,28 @@ MapIndex::MapIndex(const string& directory) {
|
||||
}
|
||||
}
|
||||
|
||||
MapIndex::MapEntry::MapEntry(const MapDefinition& map) : map(map) {}
|
||||
|
||||
MapIndex::MapEntry::MapEntry(const string& compressed)
|
||||
: compressed_data(compressed) {
|
||||
string decompressed = prs_decompress(this->compressed_data);
|
||||
if (decompressed.size() != sizeof(MapDefinition)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed data size is incorrect (expected %zu bytes, read %zu bytes)",
|
||||
sizeof(MapDefinition), decompressed.size()));
|
||||
}
|
||||
this->map = *reinterpret_cast<const MapDefinition*>(decompressed.data());
|
||||
}
|
||||
|
||||
const string& MapIndex::MapEntry::compressed(bool is_trial) const {
|
||||
if (is_trial) {
|
||||
if (this->compressed_trial_data.empty()) {
|
||||
MapDefinitionTrial mdt(this->map);
|
||||
this->compressed_trial_data = prs_compress(&mdt, sizeof(mdt));
|
||||
}
|
||||
return this->compressed_trial_data;
|
||||
} else {
|
||||
if (this->compressed_data.empty()) {
|
||||
this->compressed_data = prs_compress(&this->map, sizeof(this->map));
|
||||
}
|
||||
return this->compressed_data;
|
||||
}
|
||||
}
|
||||
|
||||
const string& MapIndex::get_compressed_list(size_t num_players) const {
|
||||
const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language) const {
|
||||
if (num_players == 0) {
|
||||
throw runtime_error("cannot generate map list for no players");
|
||||
}
|
||||
if (num_players > 4) {
|
||||
throw logic_error("player count is too high in map list generation");
|
||||
}
|
||||
string& compressed_map_list = this->compressed_map_lists.at(num_players - 1);
|
||||
|
||||
if (language >= this->compressed_map_lists.size()) {
|
||||
this->compressed_map_lists.resize(language + 1);
|
||||
}
|
||||
string& compressed_map_list = this->compressed_map_lists[language].at(num_players - 1);
|
||||
if (compressed_map_list.empty()) {
|
||||
StringWriter entries_w;
|
||||
StringWriter strings_w;
|
||||
|
||||
size_t num_maps = 0;
|
||||
for (const auto& map_it : this->maps) {
|
||||
auto vm = map_it.second->version(language);
|
||||
size_t map_num_players = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
uint8_t player_type = map_it.second->map.entry_states[z].player_type;
|
||||
uint8_t player_type = vm->map->entry_states[z].player_type;
|
||||
if (player_type == 0x00 || player_type == 0x01 || player_type == 0xFF) {
|
||||
map_num_players++;
|
||||
}
|
||||
@@ -2381,29 +2569,28 @@ const string& MapIndex::get_compressed_list(size_t num_players) const {
|
||||
}
|
||||
|
||||
MapList::Entry e;
|
||||
const auto& map = map_it.second->map;
|
||||
e.map_x = map.map_x;
|
||||
e.map_y = map.map_y;
|
||||
e.environment_number = map.environment_number;
|
||||
e.map_number = map.map_number.load();
|
||||
e.width = map.width;
|
||||
e.height = map.height;
|
||||
e.map_tiles = map.map_tiles;
|
||||
e.modification_tiles = map.modification_tiles;
|
||||
e.map_x = vm->map->map_x;
|
||||
e.map_y = vm->map->map_y;
|
||||
e.environment_number = vm->map->environment_number;
|
||||
e.map_number = vm->map->map_number.load();
|
||||
e.width = vm->map->width;
|
||||
e.height = vm->map->height;
|
||||
e.map_tiles = vm->map->map_tiles;
|
||||
e.modification_tiles = vm->map->modification_tiles;
|
||||
|
||||
e.name_offset = strings_w.size();
|
||||
strings_w.write(map.name.data(), map.name.len());
|
||||
strings_w.write(vm->map->name.data(), vm->map->name.len());
|
||||
strings_w.put_u8(0);
|
||||
e.location_name_offset = strings_w.size();
|
||||
strings_w.write(map.location_name.data(), map.location_name.len());
|
||||
strings_w.write(vm->map->location_name.data(), vm->map->location_name.len());
|
||||
strings_w.put_u8(0);
|
||||
e.quest_name_offset = strings_w.size();
|
||||
strings_w.write(map.quest_name.data(), map.quest_name.len());
|
||||
strings_w.write(vm->map->quest_name.data(), vm->map->quest_name.len());
|
||||
strings_w.put_u8(0);
|
||||
e.description_offset = strings_w.size();
|
||||
strings_w.write(map.description.data(), map.description.len());
|
||||
strings_w.write(vm->map->description.data(), vm->map->description.len());
|
||||
strings_w.put_u8(0);
|
||||
e.map_category = map_it.second->map.map_category;
|
||||
e.map_category = vm->map->map_category;
|
||||
|
||||
entries_w.put(e);
|
||||
num_maps++;
|
||||
@@ -2434,11 +2621,11 @@ const string& MapIndex::get_compressed_list(size_t num_players) const {
|
||||
return compressed_map_list;
|
||||
}
|
||||
|
||||
shared_ptr<const MapIndex::MapEntry> MapIndex::definition_for_number(uint32_t id) const {
|
||||
shared_ptr<const MapIndex::Map> MapIndex::for_number(uint32_t id) const {
|
||||
return this->maps.at(id);
|
||||
}
|
||||
|
||||
shared_ptr<const MapIndex::MapEntry> MapIndex::definition_for_name(const string& name) const {
|
||||
shared_ptr<const MapIndex::Map> MapIndex::for_name(const string& name) const {
|
||||
return this->maps_by_name.at(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -902,8 +902,8 @@ struct Rules {
|
||||
Rules() = default;
|
||||
explicit Rules(const JSON& json);
|
||||
JSON json() const;
|
||||
bool operator==(const Rules& other) const;
|
||||
bool operator!=(const Rules& other) const;
|
||||
bool operator==(const Rules& other) const = default;
|
||||
bool operator!=(const Rules& other) const = default;
|
||||
void clear();
|
||||
void set_defaults();
|
||||
|
||||
@@ -1284,6 +1284,9 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// 01 = DARK ONLY
|
||||
// FF = any deck allowed
|
||||
uint8_t deck_type;
|
||||
|
||||
bool operator==(const EntryState& other) const = default;
|
||||
bool operator!=(const EntryState& other) const = default;
|
||||
} __attribute__((packed));
|
||||
/* 5A10 */ parray<EntryState, 4> entry_states;
|
||||
/* 5A18 */
|
||||
@@ -1292,6 +1295,12 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
return (this->map_category <= 2);
|
||||
}
|
||||
|
||||
// This function throws runtime_error if the passed-in map is not semantically
|
||||
// equivalent to *this. Semantic equivalence means all fields that affect
|
||||
// gameplay and visuals are equivalent, but dialogue, names, and description
|
||||
// text may differ.
|
||||
void assert_semantically_equivalent(const MapDefinition& other) const;
|
||||
|
||||
std::string str(const CardIndex* card_index = nullptr) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
@@ -1390,30 +1399,51 @@ class MapIndex {
|
||||
public:
|
||||
MapIndex(const std::string& directory);
|
||||
|
||||
class MapEntry {
|
||||
class VersionedMap {
|
||||
public:
|
||||
MapDefinition map;
|
||||
std::shared_ptr<const MapDefinition> map;
|
||||
uint8_t language;
|
||||
|
||||
explicit MapEntry(const MapDefinition& map);
|
||||
explicit MapEntry(const std::string& compressed_data);
|
||||
VersionedMap(std::shared_ptr<const MapDefinition> map, uint8_t language);
|
||||
VersionedMap(std::string&& compressed_data, uint8_t language);
|
||||
|
||||
std::shared_ptr<const MapDefinitionTrial> trial() const;
|
||||
const std::string& compressed(bool is_trial) const;
|
||||
|
||||
private:
|
||||
mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
|
||||
mutable std::string compressed_data;
|
||||
mutable std::string compressed_trial_data;
|
||||
};
|
||||
|
||||
const std::string& get_compressed_list(size_t num_players) const;
|
||||
std::shared_ptr<const MapEntry> definition_for_number(uint32_t id) const;
|
||||
std::shared_ptr<const MapEntry> definition_for_name(const std::string& name) const;
|
||||
class Map {
|
||||
public:
|
||||
uint32_t map_number;
|
||||
std::shared_ptr<const VersionedMap> initial_version;
|
||||
|
||||
explicit Map(std::shared_ptr<const VersionedMap> initial_version);
|
||||
|
||||
void add_version(std::shared_ptr<const VersionedMap> vm);
|
||||
bool has_version(uint8_t language) const;
|
||||
std::shared_ptr<const VersionedMap> version(uint8_t language) const;
|
||||
inline const std::vector<std::shared_ptr<const VersionedMap>>& all_versions() const {
|
||||
return this->versions;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::shared_ptr<const VersionedMap>> versions;
|
||||
};
|
||||
|
||||
const std::string& get_compressed_list(size_t num_players, uint8_t language) const;
|
||||
std::shared_ptr<const Map> for_number(uint32_t id) const;
|
||||
std::shared_ptr<const Map> for_name(const std::string& name) const;
|
||||
std::set<uint32_t> all_numbers() const;
|
||||
|
||||
private:
|
||||
// The compressed map lists are generated on demand from the maps map below
|
||||
mutable std::array<std::string, 4> compressed_map_lists;
|
||||
std::map<uint32_t, std::shared_ptr<MapEntry>> maps;
|
||||
std::unordered_map<std::string, std::shared_ptr<MapEntry>> maps_by_name;
|
||||
mutable std::vector<std::array<std::string, 4>> compressed_map_lists;
|
||||
std::map<uint32_t, std::shared_ptr<Map>> maps;
|
||||
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
|
||||
};
|
||||
|
||||
class COMDeckIndex {
|
||||
|
||||
@@ -236,24 +236,26 @@ void Server::send_6xB4x46() const {
|
||||
this->options.random_crypt->seed(),
|
||||
this->options.random_crypt->absolute_offset());
|
||||
if (this->last_chosen_map) {
|
||||
date_str2 += string_printf(" Map:%08" PRIX32, this->last_chosen_map->map.map_number.load());
|
||||
date_str2 += string_printf(" Map:%08" PRIX32, this->last_chosen_map->map_number);
|
||||
}
|
||||
cmd46.date_str2 = date_str2;
|
||||
this->send(cmd46);
|
||||
}
|
||||
|
||||
string Server::prepare_6xB6x41_map_definition(
|
||||
shared_ptr<const MapIndex::MapEntry> map, bool is_trial) {
|
||||
const auto& compressed = map->compressed(is_trial);
|
||||
shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_trial) {
|
||||
auto vm = map->version(language);
|
||||
|
||||
const auto& compressed = vm->compressed(is_trial);
|
||||
|
||||
StringWriter w;
|
||||
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3);
|
||||
w.put<G_MapData_GC_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, map->map.map_number.load(), compressed.size(), 0});
|
||||
w.put<G_MapData_GC_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed.size(), 0});
|
||||
w.write(compressed);
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
void Server::send_commands_for_joining_spectator(Channel& c, bool is_trial) const {
|
||||
void Server::send_commands_for_joining_spectator(Channel& c, uint8_t language, bool is_trial) const {
|
||||
bool should_send_state = true;
|
||||
if (this->setup_phase == SetupPhase::REGISTRATION) {
|
||||
// If registration is still in progress, we only need to send the map data
|
||||
@@ -265,7 +267,8 @@ void Server::send_commands_for_joining_spectator(Channel& c, bool is_trial) cons
|
||||
}
|
||||
|
||||
if (this->last_chosen_map) {
|
||||
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, is_trial);
|
||||
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, language, is_trial);
|
||||
this->log().info("Sending %c version of map %08" PRIX32, char_for_language_code(language), this->last_chosen_map->map_number);
|
||||
c.send(0x6C, 0x00, data);
|
||||
}
|
||||
|
||||
@@ -1631,7 +1634,7 @@ const unordered_map<uint8_t, Server::handler_t> Server::subcommand_handlers({
|
||||
{0x49, &Server::handle_CAx49_card_counts},
|
||||
});
|
||||
|
||||
void Server::on_server_data_input(const string& data) {
|
||||
void Server::on_server_data_input(shared_ptr<Client> sender_c, const string& data) {
|
||||
auto header = check_size_t<G_CardBattleCommandHeader>(data, 0xFFFF);
|
||||
if (header.size * 4 < data.size()) {
|
||||
throw runtime_error("command is incomplete");
|
||||
@@ -1650,10 +1653,10 @@ void Server::on_server_data_input(const string& data) {
|
||||
string unmasked_data = data;
|
||||
set_mask_for_ep3_game_command(unmasked_data.data(), unmasked_data.size(), 0);
|
||||
|
||||
(this->*handler)(unmasked_data);
|
||||
(this->*handler)(sender_c, unmasked_data);
|
||||
}
|
||||
|
||||
void Server::handle_CAx0B_mulligan_hand(const string& data) {
|
||||
void Server::handle_CAx0B_mulligan_hand(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_RedrawInitialHand_GC_Ep3_6xB3x0B_CAx0B>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "REDRAW");
|
||||
@@ -1684,7 +1687,7 @@ void Server::handle_CAx0B_mulligan_hand(const string& data) {
|
||||
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
|
||||
}
|
||||
|
||||
void Server::handle_CAx0C_end_mulligan_phase(const string& data) {
|
||||
void Server::handle_CAx0C_end_mulligan_phase(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndInitialRedrawPhase_GC_Ep3_6xB3x0C_CAx0C>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 2");
|
||||
@@ -1739,7 +1742,7 @@ void Server::handle_CAx0C_end_mulligan_phase(const string& data) {
|
||||
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd_fin.error_code);
|
||||
}
|
||||
|
||||
void Server::handle_CAx0D_end_non_action_phase(const string& data) {
|
||||
void Server::handle_CAx0D_end_non_action_phase(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndNonAttackPhase_GC_Ep3_6xB3x0D_CAx0D>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "END PHASE");
|
||||
@@ -1760,7 +1763,7 @@ void Server::handle_CAx0D_end_non_action_phase(const string& data) {
|
||||
this->send(out_cmd_fin);
|
||||
}
|
||||
|
||||
void Server::handle_CAx0E_discard_card_from_hand(const string& data) {
|
||||
void Server::handle_CAx0E_discard_card_from_hand(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_DiscardCardFromHand_GC_Ep3_6xB3x0E_CAx0E>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "DISCARD");
|
||||
@@ -1799,7 +1802,7 @@ void Server::handle_CAx0E_discard_card_from_hand(const string& data) {
|
||||
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
|
||||
}
|
||||
|
||||
void Server::handle_CAx0F_set_card_from_hand(const string& data) {
|
||||
void Server::handle_CAx0F_set_card_from_hand(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_SetCardFromHand_GC_Ep3_6xB3x0F_CAx0F>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "SET FC");
|
||||
@@ -1841,7 +1844,7 @@ void Server::handle_CAx0F_set_card_from_hand(const string& data) {
|
||||
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
|
||||
}
|
||||
|
||||
void Server::handle_CAx10_move_fc_to_location(const string& data) {
|
||||
void Server::handle_CAx10_move_fc_to_location(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_MoveFieldCharacter_GC_Ep3_6xB3x10_CAx10>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "MOVE");
|
||||
@@ -1879,7 +1882,7 @@ void Server::handle_CAx10_move_fc_to_location(const string& data) {
|
||||
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
|
||||
}
|
||||
|
||||
void Server::handle_CAx11_enqueue_attack_or_defense(const string& data) {
|
||||
void Server::handle_CAx11_enqueue_attack_or_defense(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EnqueueAttackOrDefense_GC_Ep3_6xB3x11_CAx11>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "ENQUEUE ACT");
|
||||
@@ -1915,7 +1918,7 @@ void Server::handle_CAx11_enqueue_attack_or_defense(const string& data) {
|
||||
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
|
||||
}
|
||||
|
||||
void Server::handle_CAx12_end_attack_list(const string& data) {
|
||||
void Server::handle_CAx12_end_attack_list(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndAttackList_GC_Ep3_6xB3x12_CAx12>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "END ATK LIST");
|
||||
@@ -1938,7 +1941,7 @@ void Server::handle_CAx12_end_attack_list(const string& data) {
|
||||
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, error_code);
|
||||
}
|
||||
|
||||
void Server::handle_CAx13_update_map_during_setup(const string& data) {
|
||||
void Server::handle_CAx13_update_map_during_setup(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_SetMapState_GC_Ep3_6xB3x13_CAx13>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "UPDATE MAP");
|
||||
@@ -1980,7 +1983,7 @@ void Server::handle_CAx13_update_map_during_setup(const string& data) {
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx14_update_deck_during_setup(const string& data) {
|
||||
void Server::handle_CAx14_update_deck_during_setup(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_SetPlayerDeck_GC_Ep3_6xB3x14_CAx14>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "UPDATE DECK");
|
||||
@@ -2027,7 +2030,7 @@ void Server::handle_CAx14_update_deck_during_setup(const string& data) {
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx15_unused_hard_reset_server_state(const string& data) {
|
||||
void Server::handle_CAx15_unused_hard_reset_server_state(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_HardResetServerState_GC_Ep3_6xB3x15_CAx15>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "HARD RESET");
|
||||
@@ -2045,7 +2048,7 @@ void Server::handle_CAx15_unused_hard_reset_server_state(const string& data) {
|
||||
throw runtime_error("hard reset command received");
|
||||
}
|
||||
|
||||
void Server::handle_CAx1B_update_player_name(const string& data) {
|
||||
void Server::handle_CAx1B_update_player_name(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_SetPlayerName_GC_Ep3_6xB3x1B_CAx1B>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.entry.client_id, in_cmd.header.subsubcommand, "UPDATE NAME");
|
||||
@@ -2077,7 +2080,7 @@ void Server::handle_CAx1B_update_player_name(const string& data) {
|
||||
this->send(out_cmd);
|
||||
}
|
||||
|
||||
void Server::handle_CAx1D_start_battle(const string& data) {
|
||||
void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_StartBattle_GC_Ep3_6xB3x1D_CAx1D>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "START BATTLE");
|
||||
@@ -2116,7 +2119,7 @@ void Server::handle_CAx1D_start_battle(const string& data) {
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx21_end_battle(const string& data) {
|
||||
void Server::handle_CAx21_end_battle(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndBattle_GC_Ep3_6xB3x21_CAx21>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "END BATTLE");
|
||||
@@ -2131,7 +2134,7 @@ void Server::handle_CAx21_end_battle(const string& data) {
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx28_end_defense_list(const string& data) {
|
||||
void Server::handle_CAx28_end_defense_list(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndDefenseList_GC_Ep3_6xB3x28_CAx28>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "END DEF LIST");
|
||||
@@ -2184,13 +2187,13 @@ void Server::handle_CAx28_end_defense_list(const string& data) {
|
||||
this->send(out_cmd_fin);
|
||||
}
|
||||
|
||||
void Server::handle_CAx2B_legacy_set_card(const string& data) {
|
||||
void Server::handle_CAx2B_legacy_set_card(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_ExecLegacyCard_GC_Ep3_6xB3x2B_CAx2B>(data);
|
||||
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "EXEC LEGACY");
|
||||
// Sega's original implementation does nothing here, so we do nothing as well.
|
||||
}
|
||||
|
||||
void Server::handle_CAx34_subtract_ally_atk_points(const string& data) {
|
||||
void Server::handle_CAx34_subtract_ally_atk_points(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_PhotonBlastRequest_GC_Ep3_6xB3x34_CAx34>(data);
|
||||
|
||||
uint8_t card_ref_client_id = client_id_for_card_ref(in_cmd.card_ref);
|
||||
@@ -2267,7 +2270,7 @@ void Server::handle_CAx34_subtract_ally_atk_points(const string& data) {
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase(const string& data) {
|
||||
void Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_AdvanceFromStartingRollsPhase_GC_Ep3_6xB3x37_CAx37>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 1");
|
||||
@@ -2297,14 +2300,14 @@ void Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase(const
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx3A_time_limit_expired(const string& data) {
|
||||
void Server::handle_CAx3A_time_limit_expired(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_OverallTimeLimitExpired_GC_Ep3_6xB3x3A_CAx3A>(data);
|
||||
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "TIME EXPIRED");
|
||||
// We don't need to do anything here because the overall time limit is tracked
|
||||
// server-side instead.
|
||||
}
|
||||
|
||||
void Server::handle_CAx40_map_list_request(const string& data) {
|
||||
void Server::handle_CAx40_map_list_request(shared_ptr<Client> sender_c, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_MapListRequest_GC_Ep3_6xB3x40_CAx40>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "MAP LIST");
|
||||
@@ -2314,7 +2317,8 @@ void Server::handle_CAx40_map_list_request(const string& data) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
|
||||
const auto& list_data = this->options.map_index->get_compressed_list(l->count_clients());
|
||||
const auto& list_data = this->options.map_index->get_compressed_list(
|
||||
l->count_clients(), sender_c->language());
|
||||
|
||||
StringWriter w;
|
||||
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3);
|
||||
@@ -2332,30 +2336,61 @@ void Server::handle_CAx40_map_list_request(const string& data) {
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx41_map_request(const string& data) {
|
||||
const auto& cmd = check_size_t<G_MapDataRequest_GC_Ep3_6xB3x41_CAx41>(data);
|
||||
this->send_debug_command_received_message(
|
||||
cmd.header.subsubcommand, "MAP DATA");
|
||||
|
||||
void Server::send_6xB6x41_to_all_clients() const {
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
throw runtime_error("lobby is deleted");
|
||||
}
|
||||
|
||||
this->last_chosen_map = this->options.map_index->definition_for_number(cmd.map_number);
|
||||
auto out_cmd = this->prepare_6xB6x41_map_definition(this->last_chosen_map, l->flags & Lobby::Flag::IS_EP3_TRIAL);
|
||||
send_command(l, 0x6C, 0x00, out_cmd);
|
||||
vector<string> map_commands_by_language;
|
||||
auto send_to_client = [&](shared_ptr<Client> c) -> void {
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
uint8_t language = c->language();
|
||||
if (map_commands_by_language.size() <= language) {
|
||||
map_commands_by_language.resize(language + 1);
|
||||
}
|
||||
if (map_commands_by_language[language].empty()) {
|
||||
map_commands_by_language[language] = this->prepare_6xB6x41_map_definition(
|
||||
this->last_chosen_map, language, l->flags & Lobby::Flag::IS_EP3_TRIAL);
|
||||
}
|
||||
this->log().info("Sending %c version of map %08" PRIX32, char_for_language_code(language), this->last_chosen_map->map_number);
|
||||
send_command(c, 0x6C, 0x00, map_commands_by_language[language]);
|
||||
};
|
||||
for (const auto& c : l->clients) {
|
||||
send_to_client(c);
|
||||
}
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
send_command_if_not_loading(watcher_l, 0x6C, 0x00, out_cmd);
|
||||
for (const auto& c : watcher_l->clients) {
|
||||
send_to_client(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(out_cmd));
|
||||
// TODO: It's not great that we just pick the first one; ideally we'd put
|
||||
// all of them in the recording and send the appropriate one to the client
|
||||
// in the playback lobby
|
||||
for (string& data : map_commands_by_language) {
|
||||
if (!data.empty()) {
|
||||
l->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx48_end_turn(const string& data) {
|
||||
void Server::handle_CAx41_map_request(shared_ptr<Client>, const string& data) {
|
||||
const auto& cmd = check_size_t<G_MapDataRequest_GC_Ep3_6xB3x41_CAx41>(data);
|
||||
this->send_debug_command_received_message(
|
||||
cmd.header.subsubcommand, "MAP DATA");
|
||||
|
||||
this->last_chosen_map = this->options.map_index->for_number(cmd.map_number);
|
||||
this->send_6xB6x41_to_all_clients();
|
||||
}
|
||||
|
||||
void Server::handle_CAx48_end_turn(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndTurn_GC_Ep3_6xB3x48_CAx48>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "END TURN");
|
||||
@@ -2373,7 +2408,7 @@ void Server::handle_CAx48_end_turn(const string& data) {
|
||||
this->send(out_cmd);
|
||||
}
|
||||
|
||||
void Server::handle_CAx49_card_counts(const string& data) {
|
||||
void Server::handle_CAx49_card_counts(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_CardCounts_GC_Ep3_6xB3x49_CAx49>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.sender_client_id, in_cmd.header.subsubcommand, "CARD COUNTS");
|
||||
|
||||
@@ -111,7 +111,7 @@ public:
|
||||
this->send(&cmd, cmd.header.size * 4);
|
||||
}
|
||||
void send(const void* data, size_t size) const;
|
||||
void send_commands_for_joining_spectator(Channel& ch, bool is_trial) const;
|
||||
void send_commands_for_joining_spectator(Channel& ch, uint8_t language, bool is_trial) const;
|
||||
|
||||
void force_battle_result(uint8_t surrendered_client_id, bool set_winner);
|
||||
void force_destroy_field_character(uint8_t client_id, size_t set_index);
|
||||
@@ -181,30 +181,30 @@ public:
|
||||
void update_battle_state_flags_and_send_6xB4x03_if_needed(
|
||||
bool always_send = false);
|
||||
bool update_registration_phase();
|
||||
void on_server_data_input(const std::string& data);
|
||||
void handle_CAx0B_mulligan_hand(const std::string& data);
|
||||
void handle_CAx0C_end_mulligan_phase(const std::string& data);
|
||||
void handle_CAx0D_end_non_action_phase(const std::string& data);
|
||||
void handle_CAx0E_discard_card_from_hand(const std::string& data);
|
||||
void handle_CAx0F_set_card_from_hand(const std::string& data);
|
||||
void handle_CAx10_move_fc_to_location(const std::string& data);
|
||||
void handle_CAx11_enqueue_attack_or_defense(const std::string& data);
|
||||
void handle_CAx12_end_attack_list(const std::string& data);
|
||||
void handle_CAx13_update_map_during_setup(const std::string& data);
|
||||
void handle_CAx14_update_deck_during_setup(const std::string& data);
|
||||
void handle_CAx15_unused_hard_reset_server_state(const std::string& data);
|
||||
void handle_CAx1B_update_player_name(const std::string& data);
|
||||
void handle_CAx1D_start_battle(const std::string& data);
|
||||
void handle_CAx21_end_battle(const std::string& data);
|
||||
void handle_CAx28_end_defense_list(const std::string& data);
|
||||
void handle_CAx2B_legacy_set_card(const std::string&);
|
||||
void handle_CAx34_subtract_ally_atk_points(const std::string& data);
|
||||
void handle_CAx37_client_ready_to_advance_from_starter_roll_phase(const std::string& data);
|
||||
void handle_CAx3A_time_limit_expired(const std::string& data);
|
||||
void handle_CAx40_map_list_request(const std::string& data);
|
||||
void handle_CAx41_map_request(const std::string& data);
|
||||
void handle_CAx48_end_turn(const std::string& data);
|
||||
void handle_CAx49_card_counts(const std::string& data);
|
||||
void on_server_data_input(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx0B_mulligan_hand(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx0C_end_mulligan_phase(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx0D_end_non_action_phase(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx0E_discard_card_from_hand(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx0F_set_card_from_hand(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx10_move_fc_to_location(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx11_enqueue_attack_or_defense(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx12_end_attack_list(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx13_update_map_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx14_update_deck_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx15_unused_hard_reset_server_state(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx1B_update_player_name(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx1D_start_battle(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx21_end_battle(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx28_end_defense_list(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx2B_legacy_set_card(std::shared_ptr<Client> sender_c, const std::string&);
|
||||
void handle_CAx34_subtract_ally_atk_points(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx37_client_ready_to_advance_from_starter_roll_phase(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx3A_time_limit_expired(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx40_map_list_request(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx41_map_request(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx48_end_turn(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx49_card_counts(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void compute_losing_team_id_and_add_winner_flags(uint32_t flags);
|
||||
uint32_t get_team_exp(uint8_t team_id) const;
|
||||
uint32_t send_6xB4x06_if_card_ref_invalid(
|
||||
@@ -226,21 +226,22 @@ public:
|
||||
G_UpdateDecks_GC_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
|
||||
G_SetPlayerNames_GC_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
|
||||
static std::string prepare_6xB6x41_map_definition(
|
||||
std::shared_ptr<const MapIndex::MapEntry> map, bool is_trial);
|
||||
std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_trial);
|
||||
void send_6xB6x41_to_all_clients() const;
|
||||
G_SetTrapTileLocations_GC_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
||||
|
||||
std::vector<std::shared_ptr<Card>> const_cast_set_cards_v(
|
||||
const std::vector<std::shared_ptr<const Card>>& cards);
|
||||
|
||||
private:
|
||||
typedef void (Server::*handler_t)(const std::string&);
|
||||
typedef void (Server::*handler_t)(std::shared_ptr<Client>, const std::string&);
|
||||
static const std::unordered_map<uint8_t, handler_t> subcommand_handlers;
|
||||
|
||||
public:
|
||||
// These fields are not part of the original implementation
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
Options options;
|
||||
std::shared_ptr<const MapIndex::MapEntry> last_chosen_map;
|
||||
std::shared_ptr<const MapIndex::Map> last_chosen_map;
|
||||
bool tournament_match_result_sent;
|
||||
uint8_t override_environment_number;
|
||||
mutable std::deque<StackLogger*> logger_stack;
|
||||
|
||||
@@ -314,7 +314,7 @@ Tournament::Tournament(
|
||||
shared_ptr<const MapIndex> map_index,
|
||||
shared_ptr<const COMDeckIndex> com_deck_index,
|
||||
const string& name,
|
||||
shared_ptr<const MapIndex::MapEntry> map,
|
||||
shared_ptr<const MapIndex::Map> map,
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
uint8_t flags)
|
||||
@@ -355,7 +355,7 @@ void Tournament::init() {
|
||||
bool is_registration_complete;
|
||||
if (!this->source_json.is_null()) {
|
||||
this->name = this->source_json.get_string("name");
|
||||
this->map = this->map_index->definition_for_number(this->source_json.get_int("map_number"));
|
||||
this->map = this->map_index->for_number(this->source_json.get_int("map_number"));
|
||||
this->rules = Rules(this->source_json.at("rules"));
|
||||
this->flags = this->source_json.get_int("flags", 0x02);
|
||||
if (this->source_json.get_bool("is_2v2", false)) {
|
||||
@@ -531,7 +531,7 @@ JSON Tournament::json() const {
|
||||
}
|
||||
return JSON::dict({
|
||||
{"name", this->name},
|
||||
{"map_number", this->map->map.map_number.load()},
|
||||
{"map_number", this->map->map_number},
|
||||
{"rules", this->rules.json()},
|
||||
{"flags", this->flags},
|
||||
{"is_registration_complete", (this->current_state != State::REGISTRATION)},
|
||||
@@ -741,8 +741,13 @@ void Tournament::print_bracket(FILE* stream) const {
|
||||
}
|
||||
};
|
||||
fprintf(stream, "Tournament \"%s\"\n", this->name.c_str());
|
||||
string map_name = this->map->map.name;
|
||||
fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map.map_number.load(), map_name.c_str());
|
||||
auto en_vm = this->map->version(1);
|
||||
if (en_vm) {
|
||||
string map_name = en_vm->map->name;
|
||||
fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map_number, map_name.c_str());
|
||||
} else {
|
||||
fprintf(stream, " Map: %08" PRIX32 "\n", this->map->map_number);
|
||||
}
|
||||
string rules_str = this->rules.str();
|
||||
fprintf(stream, " Rules: %s\n", rules_str.c_str());
|
||||
fprintf(stream, " Structure: %s, %zu entries\n", (this->flags & Flag::IS_2V2) ? "2v2" : "1v1", this->num_teams);
|
||||
@@ -850,7 +855,7 @@ void TournamentIndex::save() const {
|
||||
|
||||
shared_ptr<Tournament> TournamentIndex::create_tournament(
|
||||
const string& name,
|
||||
shared_ptr<const MapIndex::MapEntry> map,
|
||||
shared_ptr<const MapIndex::Map> map,
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
uint8_t flags) {
|
||||
|
||||
@@ -108,7 +108,7 @@ public:
|
||||
std::shared_ptr<const MapIndex> map_index,
|
||||
std::shared_ptr<const COMDeckIndex> com_deck_index,
|
||||
const std::string& name,
|
||||
std::shared_ptr<const MapIndex::MapEntry> map,
|
||||
std::shared_ptr<const MapIndex::Map> map,
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
uint8_t flags);
|
||||
@@ -124,7 +124,7 @@ public:
|
||||
inline const std::string& get_name() const {
|
||||
return this->name;
|
||||
}
|
||||
inline std::shared_ptr<const MapIndex::MapEntry> get_map() const {
|
||||
inline std::shared_ptr<const MapIndex::Map> get_map() const {
|
||||
return this->map;
|
||||
}
|
||||
inline const Rules& get_rules() const {
|
||||
@@ -171,7 +171,7 @@ private:
|
||||
std::shared_ptr<const COMDeckIndex> com_deck_index;
|
||||
JSON source_json;
|
||||
std::string name;
|
||||
std::shared_ptr<const MapIndex::MapEntry> map;
|
||||
std::shared_ptr<const MapIndex::Map> map;
|
||||
Rules rules;
|
||||
size_t num_teams;
|
||||
uint8_t flags;
|
||||
@@ -225,7 +225,7 @@ public:
|
||||
|
||||
std::shared_ptr<Tournament> create_tournament(
|
||||
const std::string& name,
|
||||
std::shared_ptr<const MapIndex::MapEntry> map,
|
||||
std::shared_ptr<const MapIndex::Map> map,
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
uint8_t flags);
|
||||
|
||||
@@ -7,13 +7,11 @@
|
||||
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class FileContentsCache {
|
||||
public:
|
||||
struct File {
|
||||
std::string name;
|
||||
shared_ptr<const std::string> data;
|
||||
std::shared_ptr<const std::string> data;
|
||||
uint64_t load_time;
|
||||
|
||||
File() = delete;
|
||||
@@ -37,10 +35,8 @@ public:
|
||||
return this->name_to_file.erase(key);
|
||||
}
|
||||
|
||||
std::shared_ptr<const File> replace(
|
||||
const std::string& name, std::string&& data, uint64_t t = 0);
|
||||
std::shared_ptr<const File> replace(
|
||||
const std::string& name, const void* data, size_t size, uint64_t t = 0);
|
||||
std::shared_ptr<const File> replace(const std::string& name, std::string&& data, uint64_t t = 0);
|
||||
std::shared_ptr<const File> replace(const std::string& name, const void* data, size_t size, uint64_t t = 0);
|
||||
|
||||
struct GetResult {
|
||||
std::shared_ptr<const File> file;
|
||||
@@ -52,10 +48,8 @@ public:
|
||||
std::shared_ptr<const File> get_or_throw(const std::string& name);
|
||||
std::shared_ptr<const File> get_or_throw(const char* name);
|
||||
|
||||
GetResult get(
|
||||
const std::string& name, std::function<std::string(const std::string&)> generate);
|
||||
GetResult get(
|
||||
const char* name, std::function<std::string(const std::string&)> generate);
|
||||
GetResult get(const std::string& name, std::function<std::string(const std::string&)> generate);
|
||||
GetResult get(const char* name, std::function<std::string(const std::string&)> generate);
|
||||
|
||||
template <typename T>
|
||||
struct GetObjResult {
|
||||
@@ -68,7 +62,7 @@ public:
|
||||
GetObjResult<T> get_obj_or_load(NameT name) {
|
||||
auto res = this->get_or_load(name);
|
||||
if (res.file->data->size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
throw std::runtime_error("cached string size is incorrect");
|
||||
}
|
||||
return {*reinterpret_cast<const T*>(res.file->data->data()), res.file, res.generate_called};
|
||||
}
|
||||
@@ -76,7 +70,7 @@ public:
|
||||
GetObjResult<T> get_obj_or_throw(NameT name) {
|
||||
auto res = this->get_or_throw(name);
|
||||
if (res.file->data->size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
throw std::runtime_error("cached string size is incorrect");
|
||||
}
|
||||
return {*reinterpret_cast<const T*>(res.file->data->data()), res.file, res.generate_called};
|
||||
}
|
||||
@@ -86,12 +80,12 @@ public:
|
||||
try {
|
||||
auto& f = this->name_to_file.at(name);
|
||||
if (f->data->size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
throw std::runtime_error("cached string size is incorrect");
|
||||
}
|
||||
if (this->ttl_usecs && (t - f->load_time < this->ttl_usecs)) {
|
||||
return {*reinterpret_cast<const T*>(f->data->data()), f, false};
|
||||
}
|
||||
} catch (const out_of_range& e) {
|
||||
} catch (const std::out_of_range& e) {
|
||||
}
|
||||
T value = generate(name);
|
||||
auto ret = this->replace_obj(name, value);
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
enum class GVRDataFormat : uint8_t {
|
||||
INTENSITY_4 = 0x00,
|
||||
INTENSITY_8 = 0x01,
|
||||
@@ -21,4 +19,4 @@ enum class GVRDataFormat : uint8_t {
|
||||
DXT1 = 0x0E,
|
||||
};
|
||||
|
||||
string encode_gvm(const Image& img, GVRDataFormat data_format);
|
||||
std::string encode_gvm(const Image& img, GVRDataFormat data_format);
|
||||
|
||||
@@ -43,6 +43,27 @@ shared_ptr<ServerState> Lobby::require_server_state() const {
|
||||
return s;
|
||||
}
|
||||
|
||||
void Lobby::create_ep3_server() {
|
||||
auto s = this->require_server_state();
|
||||
if (!this->ep3_server) {
|
||||
this->log.info("Creating Episode 3 server state");
|
||||
} else {
|
||||
this->log.info("Recreating Episode 3 server state");
|
||||
}
|
||||
auto tourn = this->tournament_match ? this->tournament_match->tournament.lock() : nullptr;
|
||||
bool is_trial = (this->flags & Lobby::Flag::IS_EP3_TRIAL);
|
||||
Episode3::Server::Options options = {
|
||||
.card_index = is_trial ? s->ep3_card_index_trial : s->ep3_card_index,
|
||||
.map_index = s->ep3_map_index,
|
||||
.behavior_flags = s->ep3_behavior_flags,
|
||||
.random_crypt = this->random_crypt,
|
||||
.tournament = tourn,
|
||||
.trap_card_ids = s->ep3_trap_card_ids,
|
||||
};
|
||||
this->ep3_server = make_shared<Episode3::Server>(this->shared_from_this(), std::move(options));
|
||||
this->ep3_server->init();
|
||||
}
|
||||
|
||||
void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) {
|
||||
for (size_t x = 0; x < this->max_clients; x++) {
|
||||
if (x == leaving_client_index) {
|
||||
|
||||
@@ -100,7 +100,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
// absent.
|
||||
std::shared_ptr<Episode3::Server> ep3_server; // Only used in primary games
|
||||
std::weak_ptr<Lobby> watched_lobby; // Only used in watcher games
|
||||
std::unordered_set<shared_ptr<Lobby>> watcher_lobbies; // Only used in primary games
|
||||
std::unordered_set<std::shared_ptr<Lobby>> watcher_lobbies; // Only used in primary games
|
||||
std::shared_ptr<Episode3::BattleRecord> battle_record; // Not used in watcher games
|
||||
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player; // Only used in replay games
|
||||
std::shared_ptr<Episode3::Tournament::Match> tournament_match;
|
||||
@@ -124,6 +124,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
Lobby& operator=(Lobby&&) = delete;
|
||||
|
||||
std::shared_ptr<ServerState> require_server_state() const;
|
||||
void create_ep3_server();
|
||||
|
||||
inline bool is_game() const {
|
||||
return this->flags & Flag::GAME;
|
||||
|
||||
@@ -1329,17 +1329,17 @@ int main(int argc, char** argv) {
|
||||
string output_filename_base = input_filename;
|
||||
if (quest_file_type == QuestFileFormat::BIN_DAT_GCI) {
|
||||
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
|
||||
auto decoded = decode_gci_file(input_filename, num_threads, dec_seed, skip_checksum);
|
||||
auto decoded = decode_gci_data(read_input_data(), num_threads, dec_seed, skip_checksum);
|
||||
save_file(output_filename_base + ".dec", decoded);
|
||||
} else if (quest_file_type == QuestFileFormat::BIN_DAT_VMS) {
|
||||
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
|
||||
auto decoded = decode_vms_file(input_filename, num_threads, dec_seed, skip_checksum);
|
||||
auto decoded = decode_vms_data(read_input_data(), num_threads, dec_seed, skip_checksum);
|
||||
save_file(output_filename_base + ".dec", decoded);
|
||||
} else if (quest_file_type == QuestFileFormat::BIN_DAT_DLQ) {
|
||||
auto decoded = decode_dlq_file(input_filename);
|
||||
auto decoded = decode_dlq_data(read_input_data());
|
||||
save_file(output_filename_base + ".dec", decoded);
|
||||
} else if (quest_file_type == QuestFileFormat::QST) {
|
||||
auto data = decode_qst_file(input_filename);
|
||||
auto data = decode_qst_data(read_input_data());
|
||||
save_file(output_filename_base + ".bin", data.first);
|
||||
save_file(output_filename_base + ".dat", data.second);
|
||||
} else {
|
||||
@@ -1353,7 +1353,13 @@ int main(int argc, char** argv) {
|
||||
throw invalid_argument("an input filename is required");
|
||||
}
|
||||
|
||||
shared_ptr<VersionedQuest> vq(new VersionedQuest(input_filename, cli_quest_version, nullptr));
|
||||
string bin_filename = input_filename;
|
||||
string dat_filename = ends_with(bin_filename, ".bin")
|
||||
? (bin_filename.substr(0, bin_filename.size() - 3) + "dat")
|
||||
: (bin_filename + ".dat");
|
||||
shared_ptr<string> bin_data(new string(load_file(bin_filename)));
|
||||
shared_ptr<string> dat_data(new string(load_file(dat_filename)));
|
||||
shared_ptr<VersionedQuest> vq(new VersionedQuest(0, 0, cli_quest_version, 0, bin_data, dat_data));
|
||||
if (download) {
|
||||
vq = vq->create_download_quest();
|
||||
}
|
||||
@@ -1872,9 +1878,15 @@ int main(int argc, char** argv) {
|
||||
auto map_ids = map_index.all_numbers();
|
||||
log_info("%zu maps", map_ids.size());
|
||||
for (uint32_t map_id : map_ids) {
|
||||
auto map = map_index.definition_for_number(map_id);
|
||||
string s = map->map.str(&card_index);
|
||||
fprintf(stdout, "%s\n", s.c_str());
|
||||
auto map = map_index.for_number(map_id);
|
||||
const auto& vms = map->all_versions();
|
||||
for (size_t language = 0; language < vms.size(); language++) {
|
||||
if (!vms[language]) {
|
||||
continue;
|
||||
}
|
||||
string s = vms[language]->map->str(&card_index);
|
||||
fprintf(stdout, "(%c) %s\n", char_for_language_code(language), s.c_str());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -480,6 +480,13 @@ void Map::add_enemies_from_map_data(
|
||||
}
|
||||
}
|
||||
|
||||
struct DATSectionHeader {
|
||||
le_uint32_t type; // 1 = objects, 2 = enemies. There are other types too
|
||||
le_uint32_t section_size; // Includes this header
|
||||
le_uint32_t area;
|
||||
le_uint32_t data_size;
|
||||
} __attribute__((packed));
|
||||
|
||||
void Map::add_enemies_from_quest_data(
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
@@ -488,7 +495,7 @@ void Map::add_enemies_from_quest_data(
|
||||
size_t size) {
|
||||
StringReader r(data, size);
|
||||
while (!r.eof()) {
|
||||
const auto& header = r.get<VersionedQuest::DATSectionHeader>();
|
||||
const auto& header = r.get<DATSectionHeader>();
|
||||
if (header.type == 0 && header.section_size == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
FileContentsCache player_files_cache(300 * 1000 * 1000);
|
||||
|
||||
void PlayerDispDataDCPCV3::enforce_lobby_join_limits(GameVersion target_version) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "ServerState.hh"
|
||||
|
||||
void on_proxy_command(
|
||||
shared_ptr<ProxyServer::LinkedSession> ses,
|
||||
std::shared_ptr<ProxyServer::LinkedSession> ses,
|
||||
bool from_server,
|
||||
uint16_t command,
|
||||
uint32_t flag,
|
||||
|
||||
@@ -217,99 +217,23 @@ struct PSODownloadQuestHeader {
|
||||
} __attribute__((packed));
|
||||
|
||||
VersionedQuest::VersionedQuest(
|
||||
const string& bin_filename,
|
||||
uint32_t quest_number,
|
||||
uint32_t category_id,
|
||||
QuestScriptVersion version,
|
||||
shared_ptr<const QuestCategoryIndex> category_index)
|
||||
: quest_number(0xFFFFFFFF),
|
||||
category_id(0xFFFFFFFF),
|
||||
uint8_t language,
|
||||
std::shared_ptr<const std::string> bin_contents,
|
||||
std::shared_ptr<const std::string> dat_contents)
|
||||
: quest_number(quest_number),
|
||||
category_id(category_id),
|
||||
episode(Episode::NONE),
|
||||
joinable(false),
|
||||
version(version),
|
||||
file_format(QuestFileFormat::BIN_DAT),
|
||||
has_mnm_extension(false),
|
||||
is_dlq_encoded(false) {
|
||||
language(language),
|
||||
is_dlq_encoded(false),
|
||||
bin_contents(bin_contents),
|
||||
dat_contents(dat_contents) {
|
||||
|
||||
if (ends_with(bin_filename, ".bin.gci") || ends_with(bin_filename, ".mnm.gci")) {
|
||||
this->file_format = QuestFileFormat::BIN_DAT_GCI;
|
||||
this->has_mnm_extension = ends_with(bin_filename, ".mnm.gci");
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".bin.vms")) {
|
||||
this->file_format = QuestFileFormat::BIN_DAT_VMS;
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".bin.dlq") || ends_with(bin_filename, ".mnm.dlq")) {
|
||||
this->file_format = QuestFileFormat::BIN_DAT_DLQ;
|
||||
this->has_mnm_extension = ends_with(bin_filename, ".mnm.dlq");
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".qst")) {
|
||||
this->file_format = QuestFileFormat::QST;
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 4);
|
||||
} else if (ends_with(bin_filename, ".bin") || ends_with(bin_filename, ".mnm")) {
|
||||
this->file_format = QuestFileFormat::BIN_DAT;
|
||||
this->has_mnm_extension = ends_with(bin_filename, ".mnm");
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 4);
|
||||
} else if (ends_with(bin_filename, ".bind") || ends_with(bin_filename, ".mnmd")) {
|
||||
this->file_format = QuestFileFormat::BIN_DAT_UNCOMPRESSED;
|
||||
this->has_mnm_extension = ends_with(bin_filename, ".mnmd");
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 5);
|
||||
} else {
|
||||
throw runtime_error("quest does not have a valid .bin or .mnm file");
|
||||
}
|
||||
|
||||
string basename;
|
||||
{
|
||||
size_t slash_pos = this->file_basename.rfind('/');
|
||||
if (slash_pos != string::npos) {
|
||||
basename = this->file_basename.substr(slash_pos + 1);
|
||||
} else {
|
||||
basename = this->file_basename;
|
||||
}
|
||||
}
|
||||
|
||||
if (basename.empty()) {
|
||||
throw invalid_argument("empty filename");
|
||||
}
|
||||
|
||||
if ((version == QuestScriptVersion::UNKNOWN) || category_index) {
|
||||
vector<string> tokens = split(basename, '-');
|
||||
|
||||
string category_token;
|
||||
if (tokens.size() == 3) {
|
||||
category_token = std::move(tokens[1]);
|
||||
tokens.erase(tokens.begin() + 1);
|
||||
} else if (tokens.size() != 2) {
|
||||
throw invalid_argument("incorrect filename format");
|
||||
}
|
||||
|
||||
if (category_index) {
|
||||
auto& category = category_index->find(basename[0], category_token);
|
||||
this->category_id = category.category_id;
|
||||
} else {
|
||||
this->category_id = 0;
|
||||
}
|
||||
|
||||
// Parse the number out of the first token
|
||||
this->quest_number = strtoull(tokens[0].c_str() + 1, nullptr, 10);
|
||||
|
||||
// Get the version from the second (or previously third) token
|
||||
static const unordered_map<string, QuestScriptVersion> name_to_version({
|
||||
{"dn", QuestScriptVersion::DC_NTE},
|
||||
{"d1", QuestScriptVersion::DC_V1},
|
||||
{"dc", QuestScriptVersion::DC_V2},
|
||||
{"pc", QuestScriptVersion::PC_V2},
|
||||
{"gcn", QuestScriptVersion::GC_NTE},
|
||||
{"gc", QuestScriptVersion::GC_V3},
|
||||
{"gc3", QuestScriptVersion::GC_EP3},
|
||||
{"xb", QuestScriptVersion::XB_V3},
|
||||
{"bb", QuestScriptVersion::BB_V4},
|
||||
});
|
||||
this->version = name_to_version.at(tokens[1]);
|
||||
}
|
||||
|
||||
// The rest of the information needs to be fetched from the .bin file's
|
||||
// contents
|
||||
|
||||
auto bin_compressed = this->bin_contents();
|
||||
auto bin_decompressed = prs_decompress(*bin_compressed);
|
||||
auto bin_decompressed = prs_decompress(*this->bin_contents);
|
||||
|
||||
switch (this->version) {
|
||||
case QuestScriptVersion::DC_NTE:
|
||||
@@ -418,10 +342,6 @@ VersionedQuest::VersionedQuest(
|
||||
default:
|
||||
throw logic_error("invalid quest game version");
|
||||
}
|
||||
|
||||
if (this->has_mnm_extension && this->episode != Episode::EP3) {
|
||||
throw runtime_error("non-Episode 3 quest has .mnm extension");
|
||||
}
|
||||
}
|
||||
|
||||
string VersionedQuest::bin_filename() const {
|
||||
@@ -440,80 +360,10 @@ string VersionedQuest::dat_filename() const {
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const string> VersionedQuest::bin_contents() const {
|
||||
if (!this->bin_contents_ptr) {
|
||||
switch (this->file_format) {
|
||||
case QuestFileFormat::BIN_DAT:
|
||||
this->bin_contents_ptr.reset(new string(load_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnm" : ".bin"))));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_UNCOMPRESSED:
|
||||
this->bin_contents_ptr.reset(new string(prs_compress(load_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnmd" : ".bind")))));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_GCI:
|
||||
this->bin_contents_ptr.reset(new string(decode_gci_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnm.gci" : ".bin.gci"))));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_VMS:
|
||||
this->bin_contents_ptr.reset(new string(decode_vms_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnm.vms" : ".bin.vms"))));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_DLQ:
|
||||
this->bin_contents_ptr.reset(new string(decode_dlq_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnm.dlq" : ".bin.dlq"))));
|
||||
break;
|
||||
case QuestFileFormat::QST: {
|
||||
auto result = decode_qst_file(this->file_basename + ".qst");
|
||||
this->bin_contents_ptr.reset(new string(std::move(result.first)));
|
||||
this->dat_contents_ptr.reset(new string(std::move(result.second)));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw logic_error("invalid quest file format");
|
||||
}
|
||||
}
|
||||
return this->bin_contents_ptr;
|
||||
}
|
||||
|
||||
shared_ptr<const string> VersionedQuest::dat_contents() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
throw logic_error("Episode 3 quests do not have .dat files");
|
||||
}
|
||||
if (!this->dat_contents_ptr) {
|
||||
switch (this->file_format) {
|
||||
case QuestFileFormat::BIN_DAT:
|
||||
this->dat_contents_ptr.reset(new string(load_file(this->file_basename + ".dat")));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_UNCOMPRESSED:
|
||||
this->dat_contents_ptr.reset(new string(prs_compress(load_file(this->file_basename + ".datd"))));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_GCI:
|
||||
this->dat_contents_ptr.reset(new string(decode_gci_file(this->file_basename + ".dat.gci")));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_VMS:
|
||||
this->dat_contents_ptr.reset(new string(decode_vms_file(this->file_basename + ".dat.vms")));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_DLQ:
|
||||
this->dat_contents_ptr.reset(new string(decode_dlq_file(this->file_basename + ".dat.dlq")));
|
||||
break;
|
||||
case QuestFileFormat::QST: {
|
||||
auto result = decode_qst_file(this->file_basename + ".qst");
|
||||
this->bin_contents_ptr.reset(new string(std::move(result.first)));
|
||||
this->dat_contents_ptr.reset(new string(std::move(result.second)));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw logic_error("invalid quest file format");
|
||||
}
|
||||
}
|
||||
return this->dat_contents_ptr;
|
||||
}
|
||||
|
||||
string VersionedQuest::encode_qst() const {
|
||||
return encode_qst_file(
|
||||
*this->bin_contents(),
|
||||
*this->dat_contents(),
|
||||
*this->bin_contents,
|
||||
*this->dat_contents,
|
||||
this->name,
|
||||
this->quest_number,
|
||||
this->version,
|
||||
@@ -525,9 +375,12 @@ Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
|
||||
category_id(initial_version->category_id),
|
||||
episode(initial_version->episode),
|
||||
joinable(initial_version->joinable),
|
||||
name(initial_version->name),
|
||||
versions_present(1 << static_cast<size_t>(initial_version->version)) {
|
||||
versions.emplace(initial_version->version, initial_version);
|
||||
name(initial_version->name) {
|
||||
this->versions.emplace(this->versions_key(initial_version->version, initial_version->language), initial_version);
|
||||
}
|
||||
|
||||
uint16_t Quest::versions_key(QuestScriptVersion v, uint8_t language) {
|
||||
return (static_cast<uint16_t>(v) << 8) | language;
|
||||
}
|
||||
|
||||
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
|
||||
@@ -544,24 +397,30 @@ void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
|
||||
throw runtime_error("quest version has a different joinability state");
|
||||
}
|
||||
|
||||
uint16_t presence_mask = 1 << static_cast<size_t>(vq->version);
|
||||
if (this->versions_present & presence_mask) {
|
||||
throw runtime_error("quest version is already present");
|
||||
}
|
||||
this->versions_present |= presence_mask;
|
||||
this->versions.emplace(vq->version, vq);
|
||||
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
|
||||
}
|
||||
|
||||
bool Quest::has_version(QuestScriptVersion v) const {
|
||||
return !!(this->versions_present & (1 << static_cast<size_t>(v)));
|
||||
bool Quest::has_version(QuestScriptVersion v, uint8_t language) const {
|
||||
return this->versions.count(this->versions_key(v, language));
|
||||
}
|
||||
|
||||
shared_ptr<const VersionedQuest> Quest::version(QuestScriptVersion v) const {
|
||||
shared_ptr<const VersionedQuest> Quest::version(QuestScriptVersion v, uint8_t language) const {
|
||||
// Return the requested version, if it exists
|
||||
try {
|
||||
return this->versions.at(v);
|
||||
return this->versions.at(this->versions_key(v, language));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
// Return the English version, if it exists
|
||||
try {
|
||||
return this->versions.at(this->versions_key(v, 1));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
// Return the first language, if it exists
|
||||
auto it = this->versions.lower_bound(this->versions_key(v, 0));
|
||||
if ((it == this->versions.end()) || ((it->first & 0xFF00) != this->versions_key(v, 0))) {
|
||||
return nullptr;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
QuestIndex::QuestIndex(
|
||||
@@ -570,54 +429,220 @@ QuestIndex::QuestIndex(
|
||||
: directory(directory),
|
||||
category_index(category_index) {
|
||||
|
||||
for (const auto& filename : list_directory_sorted(this->directory)) {
|
||||
string full_path = this->directory + "/" + filename;
|
||||
unordered_map<string, shared_ptr<const string>> dat_cache;
|
||||
|
||||
if (ends_with(filename, ".gba")) {
|
||||
shared_ptr<string> contents(new string(load_file(full_path)));
|
||||
this->gba_file_contents.emplace(make_pair(filename, contents));
|
||||
for (const auto& bin_filename : list_directory_sorted(directory)) {
|
||||
string bin_path = this->directory + "/" + bin_filename;
|
||||
|
||||
if (ends_with(bin_filename, ".gba")) {
|
||||
shared_ptr<string> contents(new string(load_file(bin_path)));
|
||||
this->gba_file_contents.emplace(make_pair(bin_filename, contents));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ends_with(filename, ".bin") ||
|
||||
ends_with(filename, ".bind") ||
|
||||
ends_with(filename, ".bin.gci") ||
|
||||
ends_with(filename, ".bin.vms") ||
|
||||
ends_with(filename, ".bin.dlq") ||
|
||||
ends_with(filename, ".mnm") ||
|
||||
ends_with(filename, ".mnmd") ||
|
||||
ends_with(filename, ".mnm.gci") ||
|
||||
ends_with(filename, ".mnm.dlq") ||
|
||||
ends_with(filename, ".qst")) {
|
||||
try {
|
||||
shared_ptr<VersionedQuest> vq(new VersionedQuest(full_path, QuestScriptVersion::UNKNOWN, this->category_index));
|
||||
|
||||
string ascii_name = encode_sjis(vq->name);
|
||||
auto category_name = encode_sjis(this->category_index->at(vq->category_id).name);
|
||||
|
||||
auto q_it = this->quests_by_number.find(vq->quest_number);
|
||||
if (q_it != this->quests_by_number.end()) {
|
||||
q_it->second->add_version(vq);
|
||||
static_game_data_log.info("(%s) Added %s version of quest %" PRIu32 " \"%s\"",
|
||||
filename.c_str(),
|
||||
name_for_enum(vq->version),
|
||||
vq->quest_number,
|
||||
ascii_name.c_str());
|
||||
} else {
|
||||
this->quests_by_number.emplace(vq->quest_number, new Quest(vq));
|
||||
static_game_data_log.info("(%s) Created %s quest %" PRIu32 " \"%s\" (%s, %s (%" PRIu32 "), %s)",
|
||||
filename.c_str(),
|
||||
name_for_enum(vq->version),
|
||||
vq->quest_number,
|
||||
ascii_name.c_str(),
|
||||
name_for_episode(vq->episode),
|
||||
category_name.c_str(),
|
||||
vq->category_id,
|
||||
vq->joinable ? "joinable" : "not joinable");
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("Failed to index quest file %s (%s)", filename.c_str(), e.what());
|
||||
try {
|
||||
QuestFileFormat format;
|
||||
string basename;
|
||||
if (ends_with(bin_filename, ".bin.gci") || ends_with(bin_filename, ".mnm.gci")) {
|
||||
format = QuestFileFormat::BIN_DAT_GCI;
|
||||
basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".bin.vms")) {
|
||||
format = QuestFileFormat::BIN_DAT_VMS;
|
||||
basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".bin.dlq") || ends_with(bin_filename, ".mnm.dlq")) {
|
||||
format = QuestFileFormat::BIN_DAT_DLQ;
|
||||
basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".qst")) {
|
||||
format = QuestFileFormat::QST;
|
||||
basename = bin_filename.substr(0, bin_filename.size() - 4);
|
||||
} else if (ends_with(bin_filename, ".bin") || ends_with(bin_filename, ".mnm")) {
|
||||
format = QuestFileFormat::BIN_DAT;
|
||||
basename = bin_filename.substr(0, bin_filename.size() - 4);
|
||||
} else if (ends_with(bin_filename, ".bind") || ends_with(bin_filename, ".mnmd")) {
|
||||
format = QuestFileFormat::BIN_DAT_UNCOMPRESSED;
|
||||
basename = bin_filename.substr(0, bin_filename.size() - 5);
|
||||
} else {
|
||||
continue; // Silently skip file
|
||||
}
|
||||
if (basename.empty()) {
|
||||
throw invalid_argument("empty filename");
|
||||
}
|
||||
|
||||
// Quest .bin filenames are like K###-CAT-VERS-LANG.EXT, where:
|
||||
// K = class (quest, battle, challenge, etc.)
|
||||
// # = quest number (does not have to match the internal quest number)
|
||||
// CAT = menu category in which quest should appear (optional)
|
||||
// VERS = PSO version that the quest is for
|
||||
// LANG = client language (j, e, g, f, s)
|
||||
// EXT = file type (bin, bind, bin.dlq, qst, etc.)
|
||||
// Quest .dat filenames are like K###-CAT-VERS.EXT (same as for .bin except
|
||||
// the LANG token is omitted)
|
||||
vector<string> filename_tokens = split(basename, '-');
|
||||
|
||||
string category_token;
|
||||
if (filename_tokens.size() == 4) {
|
||||
category_token = std::move(filename_tokens[1]);
|
||||
filename_tokens.erase(filename_tokens.begin() + 1);
|
||||
} else if (filename_tokens.size() != 3) {
|
||||
throw invalid_argument("incorrect filename format");
|
||||
}
|
||||
|
||||
uint32_t category_id = this->category_index
|
||||
? this->category_index->find(basename[0], category_token).category_id
|
||||
: 0;
|
||||
|
||||
// Parse the number out of the first token
|
||||
uint32_t quest_number = strtoull(filename_tokens[0].c_str() + 1, nullptr, 10);
|
||||
|
||||
// Get the version from the second (or previously third) token
|
||||
static const unordered_map<string, QuestScriptVersion> name_to_version({
|
||||
{"dn", QuestScriptVersion::DC_NTE},
|
||||
{"d1", QuestScriptVersion::DC_V1},
|
||||
{"dc", QuestScriptVersion::DC_V2},
|
||||
{"pc", QuestScriptVersion::PC_V2},
|
||||
{"gcn", QuestScriptVersion::GC_NTE},
|
||||
{"gc", QuestScriptVersion::GC_V3},
|
||||
{"gc3", QuestScriptVersion::GC_EP3},
|
||||
{"xb", QuestScriptVersion::XB_V3},
|
||||
{"bb", QuestScriptVersion::BB_V4},
|
||||
});
|
||||
auto version = name_to_version.at(filename_tokens[1]);
|
||||
|
||||
// Get the language from the last token
|
||||
if (filename_tokens[2].size() != 1) {
|
||||
throw runtime_error("language token is not a single character");
|
||||
}
|
||||
uint8_t language = language_code_for_char(filename_tokens[2][0]);
|
||||
|
||||
shared_ptr<const string> bin_contents;
|
||||
shared_ptr<const string> dat_contents;
|
||||
string bin_data = load_file(bin_path);
|
||||
switch (format) {
|
||||
case QuestFileFormat::BIN_DAT:
|
||||
bin_contents.reset(new string(std::move(bin_data)));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_UNCOMPRESSED:
|
||||
bin_contents.reset(new string(prs_compress(bin_data)));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_GCI:
|
||||
bin_contents.reset(new string(decode_gci_data(bin_data)));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_VMS:
|
||||
bin_contents.reset(new string(decode_vms_data(bin_data)));
|
||||
break;
|
||||
case QuestFileFormat::BIN_DAT_DLQ:
|
||||
bin_contents.reset(new string(decode_dlq_data(bin_data)));
|
||||
break;
|
||||
case QuestFileFormat::QST: {
|
||||
auto result = decode_qst_data(bin_data);
|
||||
bin_contents.reset(new string(std::move(result.first)));
|
||||
dat_contents.reset(new string(std::move(result.second)));
|
||||
dat_cache.emplace(basename, dat_contents);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw logic_error("invalid quest file format");
|
||||
}
|
||||
|
||||
string dat_filename;
|
||||
if (!dat_contents && (version != QuestScriptVersion::GC_EP3)) {
|
||||
if (basename.size() < 2) {
|
||||
throw logic_error("basename too short for language trim");
|
||||
}
|
||||
// Look for dat file with the same basename as the bin file; if not
|
||||
// found, look for a dat file without the language suffix
|
||||
string dat_basename;
|
||||
for (size_t z = 0; z < 2; z++) {
|
||||
dat_basename = z ? basename.substr(0, basename.size() - 2) : basename;
|
||||
try {
|
||||
dat_contents = dat_cache.at(dat_basename);
|
||||
break;
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
dat_filename = dat_basename + ".dat";
|
||||
string dat_path = this->directory + "/" + dat_filename;
|
||||
if (isfile(dat_path)) {
|
||||
dat_contents.reset(new string(load_file(dat_path)));
|
||||
break;
|
||||
}
|
||||
|
||||
dat_filename = dat_basename + ".datd";
|
||||
dat_path = this->directory + "/" + dat_filename;
|
||||
if (isfile(dat_path)) {
|
||||
string decompressed = load_file(dat_path);
|
||||
dat_contents.reset(new string(prs_compress_optimal(decompressed.data(), decompressed.size())));
|
||||
break;
|
||||
}
|
||||
|
||||
dat_filename = dat_basename + ".dat.gci";
|
||||
dat_path = this->directory + "/" + dat_filename;
|
||||
if (isfile(dat_path)) {
|
||||
dat_contents.reset(new string(decode_gci_data(load_file(dat_path))));
|
||||
break;
|
||||
}
|
||||
|
||||
dat_filename = dat_basename + ".dat.vms";
|
||||
dat_path = this->directory + "/" + dat_filename;
|
||||
if (isfile(dat_path)) {
|
||||
dat_contents.reset(new string(decode_vms_data(load_file(dat_path))));
|
||||
break;
|
||||
}
|
||||
|
||||
dat_filename = dat_basename + ".dat.dlq";
|
||||
dat_path = this->directory + "/" + dat_filename;
|
||||
if (isfile(dat_path)) {
|
||||
dat_contents.reset(new string(decode_dlq_data(load_file(dat_path))));
|
||||
break;
|
||||
}
|
||||
|
||||
dat_filename = dat_basename + ".qst";
|
||||
dat_path = this->directory + "/" + dat_filename;
|
||||
if (isfile(dat_basename + ".qst")) {
|
||||
dat_contents.reset(new string(decode_dlq_data(load_file(dat_basename + ".dat.dlq"))));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dat_contents) {
|
||||
dat_cache.emplace(dat_basename, dat_contents);
|
||||
} else {
|
||||
throw runtime_error("no dat file found");
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<VersionedQuest> vq(new VersionedQuest(
|
||||
quest_number, category_id, version, language, bin_contents, dat_contents));
|
||||
|
||||
string ascii_name = format_data_string(encode_sjis(vq->name));
|
||||
auto category_name = encode_sjis(this->category_index->at(vq->category_id).name);
|
||||
|
||||
string dat_str = dat_filename.empty() ? "" : (" with layout file " + dat_filename);
|
||||
auto q_it = this->quests_by_number.find(vq->quest_number);
|
||||
if (q_it != this->quests_by_number.end()) {
|
||||
q_it->second->add_version(vq);
|
||||
static_game_data_log.info("(%s) Added %s %c version of quest %" PRIu32 " %s%s",
|
||||
bin_filename.c_str(),
|
||||
name_for_enum(vq->version),
|
||||
char_for_language_code(vq->language),
|
||||
vq->quest_number,
|
||||
ascii_name.c_str(),
|
||||
dat_str.c_str());
|
||||
} else {
|
||||
this->quests_by_number.emplace(vq->quest_number, new Quest(vq));
|
||||
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " %s (%s, %s (%" PRIu32 "), %s)%s",
|
||||
bin_filename.c_str(),
|
||||
name_for_enum(vq->version),
|
||||
char_for_language_code(vq->language),
|
||||
vq->quest_number,
|
||||
ascii_name.c_str(),
|
||||
name_for_episode(vq->episode),
|
||||
category_name.c_str(),
|
||||
vq->category_id,
|
||||
vq->joinable ? "joinable" : "not joinable",
|
||||
dat_str.c_str());
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("(%s) Failed to index quest file: (%s)", bin_filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,17 +663,17 @@ shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
|
||||
}
|
||||
}
|
||||
|
||||
vector<shared_ptr<const Quest>> QuestIndex::filter(uint32_t category_id, QuestScriptVersion version) const {
|
||||
vector<shared_ptr<const Quest>> QuestIndex::filter(uint32_t category_id, QuestScriptVersion version, uint8_t language) const {
|
||||
vector<shared_ptr<const Quest>> ret;
|
||||
for (auto it : this->quests_by_number) {
|
||||
if (it.second->category_id == category_id && it.second->has_version(version)) {
|
||||
if (it.second->category_id == category_id && it.second->has_version(version, language)) {
|
||||
ret.emplace_back(it.second);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
string encode_download_quest_file(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) {
|
||||
string encode_download_quest_data(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) {
|
||||
// Download quest files are like normal (PRS-compressed) quest files, but they
|
||||
// are encrypted with PSO V2 encryption (even on V3 / PSO GC), and a small
|
||||
// header (PSODownloadQuestHeader) is prepended to the encrypted data.
|
||||
@@ -691,7 +716,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest() const {
|
||||
throw logic_error("Episode 3 quests cannot be converted to download quests");
|
||||
}
|
||||
|
||||
string decompressed_bin = prs_decompress(*this->bin_contents());
|
||||
string decompressed_bin = prs_decompress(*this->bin_contents);
|
||||
|
||||
void* data_ptr = decompressed_bin.data();
|
||||
switch (this->version) {
|
||||
@@ -730,19 +755,17 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest() const {
|
||||
// Return a new VersionedQuest object with appropriately-processed .bin and
|
||||
// .dat file contents
|
||||
shared_ptr<VersionedQuest> dlq(new VersionedQuest(*this));
|
||||
dlq->bin_contents_ptr.reset(new string(encode_download_quest_file(compressed_bin, decompressed_bin.size())));
|
||||
dlq->dat_contents_ptr.reset(new string(encode_download_quest_file(*this->dat_contents())));
|
||||
dlq->bin_contents.reset(new string(encode_download_quest_data(compressed_bin, decompressed_bin.size())));
|
||||
dlq->dat_contents.reset(new string(encode_download_quest_data(*this->dat_contents)));
|
||||
dlq->is_dlq_encoded = true;
|
||||
return dlq;
|
||||
}
|
||||
|
||||
string decode_gci_file(
|
||||
const string& filename,
|
||||
string decode_gci_data(
|
||||
const string& data,
|
||||
ssize_t find_seed_num_threads,
|
||||
int64_t known_seed,
|
||||
bool skip_checksum) {
|
||||
string data = load_file(filename);
|
||||
|
||||
StringReader r(data);
|
||||
const auto& header = r.get<PSOGCIFileHeader>();
|
||||
header.check();
|
||||
@@ -817,22 +840,22 @@ string decode_gci_file(
|
||||
}
|
||||
r.skip(9);
|
||||
|
||||
data = r.readx(header.data_size - 40);
|
||||
string decrypted = r.readx(header.data_size - 40);
|
||||
|
||||
// For some reason, Sega decided not to encrypt Episode 3 quest files in the
|
||||
// same way as Episodes 1&2 quest files (see above). Instead, they just
|
||||
// wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the
|
||||
// remaining bytes completely unencrypted (but still compressed).
|
||||
size_t unscramble_size = min<size_t>(0x100, data.size());
|
||||
decrypt_trivial_gci_data(data.data(), unscramble_size, 0);
|
||||
decrypt_trivial_gci_data(decrypted.data(), unscramble_size, 0);
|
||||
|
||||
size_t decompressed_size = prs_decompress_size(data);
|
||||
size_t decompressed_size = prs_decompress_size(decrypted);
|
||||
if (decompressed_size != sizeof(Episode3::MapDefinition)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed quest is 0x%zX bytes; expected 0x%zX bytes",
|
||||
decompressed_size, sizeof(Episode3::MapDefinition)));
|
||||
}
|
||||
return data;
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -840,13 +863,11 @@ string decode_gci_file(
|
||||
}
|
||||
}
|
||||
|
||||
string decode_vms_file(
|
||||
const string& filename,
|
||||
string decode_vms_data(
|
||||
const string& data,
|
||||
ssize_t find_seed_num_threads,
|
||||
int64_t known_seed,
|
||||
bool skip_checksum) {
|
||||
string data = load_file(filename);
|
||||
|
||||
StringReader r(data);
|
||||
const auto& header = r.get<PSOVMSFileHeader>();
|
||||
if (!header.checksum_correct()) {
|
||||
@@ -899,15 +920,9 @@ string decode_dlq_data(const string& data) {
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
string decode_dlq_file(const string& filename) {
|
||||
auto f = fopen_unique(filename, "rb");
|
||||
return decode_dlq_data(read_all(f.get()));
|
||||
}
|
||||
|
||||
template <typename HeaderT, typename OpenFileT>
|
||||
static pair<string, string> decode_qst_t(FILE* f) {
|
||||
string qst_data = read_all(f);
|
||||
StringReader r(qst_data);
|
||||
static pair<string, string> decode_qst_data_t(const string& data) {
|
||||
StringReader r(data);
|
||||
|
||||
string bin_contents;
|
||||
string dat_contents;
|
||||
@@ -945,7 +960,7 @@ static pair<string, string> decode_qst_t(FILE* f) {
|
||||
if (header.size != sizeof(HeaderT) + sizeof(OpenFileT)) {
|
||||
throw runtime_error("qst open file command has incorrect size");
|
||||
}
|
||||
const auto& cmd = r.get<OpenFileT>(f);
|
||||
const auto& cmd = r.get<OpenFileT>();
|
||||
string internal_filename = cmd.filename;
|
||||
|
||||
if (ends_with(internal_filename, ".bin")) {
|
||||
@@ -1011,32 +1026,30 @@ static pair<string, string> decode_qst_t(FILE* f) {
|
||||
}
|
||||
|
||||
if (subformat == QuestFileFormat::BIN_DAT_DLQ) {
|
||||
bin_contents = decode_dlq_file(bin_contents);
|
||||
dat_contents = decode_dlq_file(dat_contents);
|
||||
bin_contents = decode_dlq_data(bin_contents);
|
||||
dat_contents = decode_dlq_data(dat_contents);
|
||||
}
|
||||
|
||||
return make_pair(bin_contents, dat_contents);
|
||||
}
|
||||
|
||||
pair<string, string> decode_qst_file(const string& filename) {
|
||||
auto f = fopen_unique(filename, "rb");
|
||||
|
||||
pair<string, string> decode_qst_data(const string& data) {
|
||||
// QST files start with an open file command, but the format differs depending
|
||||
// on the PSO version that the qst file is for. We can detect the format from
|
||||
// the first 4 bytes in the file:
|
||||
// - BB: 58 00 44 00 or 58 00 A6 00
|
||||
// - PC: 3C 00 44 ?? or 3C 00 A6 ??
|
||||
// - DC/V3: 44 ?? 3C 00 or A6 ?? 3C 00
|
||||
uint32_t signature = freadx<be_uint32_t>(f.get());
|
||||
fseek(f.get(), 0, SEEK_SET);
|
||||
StringReader r(data);
|
||||
uint32_t signature = r.get_u32b();
|
||||
if (signature == 0x58004400 || signature == 0x5800A600) {
|
||||
return decode_qst_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(f.get());
|
||||
return decode_qst_data_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(data);
|
||||
} else if ((signature & 0xFFFFFF00) == 0x3C004400 || (signature & 0xFFFFFF00) == 0x3C00A600) {
|
||||
return decode_qst_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(f.get());
|
||||
return decode_qst_data_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(data);
|
||||
} else if ((signature & 0xFF00FFFF) == 0x44003C00 || (signature & 0xFF00FFFF) == 0xA6003C00) {
|
||||
return decode_qst_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(f.get());
|
||||
return decode_qst_data_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(data);
|
||||
} else if ((signature & 0xFF00FFFF) == 0x44005400 || (signature & 0xFF00FFFF) == 0xA6005400) {
|
||||
return decode_qst_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(f.get());
|
||||
return decode_qst_data_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(data);
|
||||
} else {
|
||||
throw runtime_error("invalid qst file format");
|
||||
}
|
||||
|
||||
@@ -52,47 +52,33 @@ struct QuestCategoryIndex {
|
||||
const Category& at(uint32_t category_id) const;
|
||||
};
|
||||
|
||||
class VersionedQuest {
|
||||
public:
|
||||
struct DATSectionHeader {
|
||||
le_uint32_t type; // 1 = objects, 2 = enemies. There are other types too
|
||||
le_uint32_t section_size; // Includes this header
|
||||
le_uint32_t area;
|
||||
le_uint32_t data_size;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct VersionedQuest {
|
||||
uint32_t quest_number;
|
||||
uint32_t category_id;
|
||||
Episode episode;
|
||||
bool joinable;
|
||||
QuestScriptVersion version;
|
||||
std::string file_basename; // we append -<version>.<bin/dat> when reading
|
||||
QuestFileFormat file_format;
|
||||
bool has_mnm_extension;
|
||||
bool is_dlq_encoded;
|
||||
std::u16string name;
|
||||
QuestScriptVersion version;
|
||||
uint8_t language;
|
||||
bool is_dlq_encoded;
|
||||
std::u16string short_description;
|
||||
std::u16string long_description;
|
||||
std::shared_ptr<const std::string> bin_contents;
|
||||
std::shared_ptr<const std::string> dat_contents;
|
||||
|
||||
VersionedQuest(const std::string& file_basename, QuestScriptVersion version, std::shared_ptr<const QuestCategoryIndex> category_index);
|
||||
VersionedQuest(const VersionedQuest&) = default;
|
||||
VersionedQuest(VersionedQuest&&) = default;
|
||||
VersionedQuest& operator=(const VersionedQuest&) = default;
|
||||
VersionedQuest& operator=(VersionedQuest&&) = default;
|
||||
VersionedQuest(
|
||||
uint32_t quest_number,
|
||||
uint32_t category_id,
|
||||
QuestScriptVersion version,
|
||||
uint8_t language,
|
||||
std::shared_ptr<const std::string> bin_contents,
|
||||
std::shared_ptr<const std::string> dat_contents);
|
||||
|
||||
std::string bin_filename() const;
|
||||
std::string dat_filename() const;
|
||||
|
||||
std::shared_ptr<const std::string> bin_contents() const;
|
||||
std::shared_ptr<const std::string> dat_contents() const;
|
||||
|
||||
std::shared_ptr<VersionedQuest> create_download_quest() const;
|
||||
std::string encode_qst() const;
|
||||
|
||||
private:
|
||||
// these are populated when requested
|
||||
mutable std::shared_ptr<std::string> bin_contents_ptr;
|
||||
mutable std::shared_ptr<std::string> dat_contents_ptr;
|
||||
};
|
||||
|
||||
class Quest {
|
||||
@@ -104,18 +90,18 @@ public:
|
||||
Quest& operator=(const Quest&) = default;
|
||||
Quest& operator=(Quest&&) = default;
|
||||
|
||||
void add_version(shared_ptr<const VersionedQuest> vq);
|
||||
bool has_version(QuestScriptVersion v) const;
|
||||
shared_ptr<const VersionedQuest> version(QuestScriptVersion v) const;
|
||||
void add_version(std::shared_ptr<const VersionedQuest> vq);
|
||||
bool has_version(QuestScriptVersion v, uint8_t language) const;
|
||||
std::shared_ptr<const VersionedQuest> version(QuestScriptVersion v, uint8_t language) const;
|
||||
|
||||
static uint16_t versions_key(QuestScriptVersion v, uint8_t language);
|
||||
|
||||
uint32_t quest_number;
|
||||
uint32_t category_id;
|
||||
Episode episode;
|
||||
bool joinable;
|
||||
std::u16string name;
|
||||
|
||||
uint16_t versions_present;
|
||||
std::unordered_map<QuestScriptVersion, std::shared_ptr<const VersionedQuest>> versions;
|
||||
std::map<uint16_t, std::shared_ptr<const VersionedQuest>> versions;
|
||||
};
|
||||
|
||||
struct QuestIndex {
|
||||
@@ -130,29 +116,27 @@ struct QuestIndex {
|
||||
|
||||
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
|
||||
std::shared_ptr<const std::string> get_gba(const std::string& name) const;
|
||||
std::vector<std::shared_ptr<const Quest>> filter(uint32_t category_id, QuestScriptVersion version) const;
|
||||
std::vector<std::shared_ptr<const Quest>> filter(uint32_t category_id, QuestScriptVersion version, uint8_t language) const;
|
||||
};
|
||||
|
||||
std::string encode_download_quest_file(
|
||||
std::string encode_download_quest_data(
|
||||
const std::string& compressed_data,
|
||||
size_t decompressed_size = 0,
|
||||
uint32_t encryption_seed = 0);
|
||||
|
||||
std::string decode_gci_file(
|
||||
const std::string& filename,
|
||||
std::string decode_gci_data(
|
||||
const std::string& data,
|
||||
ssize_t find_seed_num_threads = -1,
|
||||
int64_t known_seed = -1,
|
||||
bool skip_checksum = false);
|
||||
std::string decode_vms_file(
|
||||
const std::string& filename,
|
||||
std::string decode_vms_data(
|
||||
const std::string& data,
|
||||
ssize_t find_seed_num_threads = -1,
|
||||
int64_t known_seed = -1,
|
||||
bool skip_checksum = false);
|
||||
|
||||
std::string decode_dlq_file(const std::string& filename);
|
||||
std::string decode_dlq_data(const std::string& data);
|
||||
std::pair<std::string, std::string> decode_qst_data(const std::string& data);
|
||||
|
||||
std::pair<std::string, std::string> decode_qst_file(const std::string& filename);
|
||||
std::string encode_qst_file(
|
||||
const std::string& bin_data,
|
||||
const std::string& dat_data,
|
||||
|
||||
@@ -1296,23 +1296,7 @@ static void on_CA_Ep3(shared_ptr<Client> c, uint16_t, uint32_t, const string& da
|
||||
if (!l->ep3_server || l->ep3_server->battle_finished) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
if (!l->ep3_server) {
|
||||
l->log.info("Creating Episode 3 server state");
|
||||
} else {
|
||||
l->log.info("Recreating Episode 3 server state");
|
||||
}
|
||||
auto tourn = l->tournament_match ? l->tournament_match->tournament.lock() : nullptr;
|
||||
bool is_trial = (l->flags & Lobby::Flag::IS_EP3_TRIAL);
|
||||
Episode3::Server::Options options = {
|
||||
.card_index = is_trial ? s->ep3_card_index_trial : s->ep3_card_index,
|
||||
.map_index = s->ep3_map_index,
|
||||
.behavior_flags = s->ep3_behavior_flags,
|
||||
.random_crypt = l->random_crypt,
|
||||
.tournament = tourn,
|
||||
.trap_card_ids = s->ep3_trap_card_ids,
|
||||
};
|
||||
l->ep3_server = make_shared<Episode3::Server>(l, std::move(options));
|
||||
l->ep3_server->init();
|
||||
l->create_ep3_server();
|
||||
|
||||
if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) {
|
||||
for (size_t z = 0; z < l->max_clients; z++) {
|
||||
@@ -1353,7 +1337,7 @@ static void on_CA_Ep3(shared_ptr<Client> c, uint16_t, uint32_t, const string& da
|
||||
}
|
||||
}
|
||||
bool battle_finished_before = l->ep3_server->battle_finished;
|
||||
l->ep3_server->on_server_data_input(data);
|
||||
l->ep3_server->on_server_data_input(c, data);
|
||||
if (!battle_finished_before && l->ep3_server->battle_finished && l->battle_record) {
|
||||
l->battle_record->set_battle_end_timestamp();
|
||||
}
|
||||
@@ -1485,7 +1469,7 @@ static void on_09(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
if (!q) {
|
||||
send_quest_info(c, u"$C4Quest does not\nexist.", is_download_quest);
|
||||
} else {
|
||||
auto vq = q->version(c->quest_version());
|
||||
auto vq = q->version(c->quest_version(), c->language());
|
||||
if (!vq) {
|
||||
send_quest_info(c, u"$C4Quest does not\nexist for this game\nversion.", is_download_quest);
|
||||
} else {
|
||||
@@ -1732,7 +1716,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
}
|
||||
}
|
||||
if (num_ep3_categories == 1) {
|
||||
auto quests = s->quest_index->filter(ep3_category_id, c->quest_version());
|
||||
auto quests = s->quest_index->filter(ep3_category_id, c->quest_version(), c->language());
|
||||
send_quest_menu(c, MenuID::QUEST, quests, true);
|
||||
break;
|
||||
}
|
||||
@@ -1983,7 +1967,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
break;
|
||||
}
|
||||
shared_ptr<Lobby> l = c->lobby.lock();
|
||||
auto quests = s->quest_index->filter(item_id, c->quest_version());
|
||||
auto quests = s->quest_index->filter(item_id, c->quest_version(), c->language());
|
||||
|
||||
// Hack: Assume the menu to be sent is the download quest menu if the
|
||||
// client is not in any lobby
|
||||
@@ -2033,7 +2017,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
continue;
|
||||
}
|
||||
|
||||
auto vq = q->version(lc->quest_version());
|
||||
auto vq = q->version(lc->quest_version(), c->language());
|
||||
if (!vq) {
|
||||
send_lobby_message_box(lc, u"$C6Quest does not exist\nfor this game version.");
|
||||
lc->should_disconnect = true;
|
||||
@@ -2041,10 +2025,8 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
}
|
||||
string bin_filename = vq->bin_filename();
|
||||
string dat_filename = vq->dat_filename();
|
||||
shared_ptr<const string> bin_contents = vq->bin_contents();
|
||||
shared_ptr<const string> dat_contents = vq->dat_contents();
|
||||
send_open_quest_file(lc, bin_filename, bin_filename, bin_contents, QuestFileType::ONLINE);
|
||||
send_open_quest_file(lc, dat_filename, dat_filename, dat_contents, QuestFileType::ONLINE);
|
||||
send_open_quest_file(lc, bin_filename, bin_filename, vq->bin_contents, QuestFileType::ONLINE);
|
||||
send_open_quest_file(lc, dat_filename, dat_filename, vq->dat_contents, QuestFileType::ONLINE);
|
||||
|
||||
// There is no such thing as command AC on PSO V1 and V2 - quests just
|
||||
// start immediately when they're done downloading. (This is also the
|
||||
@@ -2064,7 +2046,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
|
||||
} else {
|
||||
string quest_name = encode_sjis(q->name);
|
||||
auto vq = q->version(c->quest_version());
|
||||
auto vq = q->version(c->quest_version(), c->language());
|
||||
if (!vq) {
|
||||
send_lobby_message_box(c, u"$C6Quest does not exist\nfor this game version.");
|
||||
break;
|
||||
@@ -2075,11 +2057,11 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
// TODO: This is not true for Episode 3 Trial Edition. We also would
|
||||
// have to convert the map to a MapDefinitionTrial, though.
|
||||
if (vq->version == QuestScriptVersion::GC_EP3) {
|
||||
send_open_quest_file(c, quest_name, vq->bin_filename(), vq->bin_contents(), QuestFileType::EPISODE_3);
|
||||
send_open_quest_file(c, quest_name, vq->bin_filename(), vq->bin_contents, QuestFileType::EPISODE_3);
|
||||
} else {
|
||||
vq = vq->create_download_quest();
|
||||
send_open_quest_file(c, quest_name, vq->bin_filename(), vq->bin_contents(), QuestFileType::DOWNLOAD);
|
||||
send_open_quest_file(c, quest_name, vq->dat_filename(), vq->dat_contents(), QuestFileType::DOWNLOAD);
|
||||
send_open_quest_file(c, quest_name, vq->bin_filename(), vq->bin_contents, QuestFileType::DOWNLOAD);
|
||||
send_open_quest_file(c, quest_name, vq->dat_filename(), vq->dat_contents, QuestFileType::DOWNLOAD);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -2423,7 +2405,7 @@ static void on_AC_V3_BB(shared_ptr<Client> c, uint16_t, uint32_t, const string&
|
||||
(l->base_version == GameVersion::BB) &&
|
||||
l->map &&
|
||||
l->quest) {
|
||||
auto dat_contents = prs_decompress(*l->quest->version(QuestScriptVersion::BB_V4)->dat_contents());
|
||||
auto dat_contents = prs_decompress(*l->quest->version(QuestScriptVersion::BB_V4, c->language())->dat_contents);
|
||||
l->map->clear();
|
||||
l->map->add_enemies_from_quest_data(l->episode, l->difficulty, l->event, dat_contents.data(), dat_contents.size());
|
||||
c->log.info("Replaced enemies list with quest layout (%zu entries)",
|
||||
@@ -3616,19 +3598,15 @@ static void on_6F(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
if (!l->quest) {
|
||||
throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest");
|
||||
}
|
||||
auto vq = l->quest->version(c->quest_version());
|
||||
auto vq = l->quest->version(c->quest_version(), c->language());
|
||||
if (!vq) {
|
||||
throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest for client version");
|
||||
}
|
||||
string bin_basename = vq->bin_filename();
|
||||
shared_ptr<const string> bin_contents = vq->bin_contents();
|
||||
string dat_basename = vq->dat_filename();
|
||||
shared_ptr<const string> dat_contents = vq->dat_contents();
|
||||
|
||||
send_open_quest_file(c, bin_basename + ".bin",
|
||||
bin_basename, bin_contents, QuestFileType::ONLINE);
|
||||
send_open_quest_file(c, dat_basename + ".dat",
|
||||
dat_basename, dat_contents, QuestFileType::ONLINE);
|
||||
send_open_quest_file(c, bin_basename + ".bin", bin_basename, vq->bin_contents, QuestFileType::ONLINE);
|
||||
send_open_quest_file(c, dat_basename + ".dat", dat_basename, vq->dat_contents, QuestFileType::ONLINE);
|
||||
c->flags |= Client::Flag::LOADING_RUNNING_QUEST;
|
||||
} else if (l->map) {
|
||||
send_rare_enemy_index_list(c, l->map->rare_enemy_indexes);
|
||||
@@ -3642,7 +3620,7 @@ static void on_6F(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
} else if (watched_lobby && watched_lobby->ep3_server) {
|
||||
if (!watched_lobby->ep3_server->battle_finished) {
|
||||
watched_lobby->ep3_server->send_commands_for_joining_spectator(
|
||||
c->channel, c->flags & Client::Flag::IS_EP3_TRIAL_EDITION);
|
||||
c->channel, c->language(), c->flags & Client::Flag::IS_EP3_TRIAL_EDITION);
|
||||
}
|
||||
send_ep3_update_game_metadata(watched_lobby);
|
||||
}
|
||||
|
||||
@@ -1158,19 +1158,20 @@ static void on_sort_inventory_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
|
||||
|
||||
PlayerInventory sorted;
|
||||
|
||||
const auto& inv = c->game_data.player()->inventory;
|
||||
for (size_t x = 0; x < 30; x++) {
|
||||
if (cmd.item_ids[x] == 0xFFFFFFFF) {
|
||||
sorted.items[x].data.id = 0xFFFFFFFF;
|
||||
} else {
|
||||
size_t index = c->game_data.player()->inventory.find_item(cmd.item_ids[x]);
|
||||
sorted.items[x] = c->game_data.player()->inventory.items[index];
|
||||
size_t index = inv.find_item(cmd.item_ids[x]);
|
||||
sorted.items[x] = inv.items[index];
|
||||
}
|
||||
}
|
||||
|
||||
sorted.num_items = c->game_data.player()->inventory.num_items;
|
||||
sorted.hp_materials_used = c->game_data.player()->inventory.hp_materials_used;
|
||||
sorted.tp_materials_used = c->game_data.player()->inventory.tp_materials_used;
|
||||
sorted.language = c->game_data.player()->inventory.language;
|
||||
sorted.num_items = inv.num_items;
|
||||
sorted.hp_materials_used = inv.hp_materials_used;
|
||||
sorted.tp_materials_used = inv.tp_materials_used;
|
||||
sorted.language = inv.language;
|
||||
c->game_data.player()->inventory = sorted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ std::string decrypt_fixed_size_data_section_s(
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
|
||||
if (size < 2 * sizeof(U32T)) {
|
||||
throw runtime_error("data size is too small");
|
||||
throw std::runtime_error("data size is too small");
|
||||
}
|
||||
std::string decrypted = decrypt_data_section<IsBigEndian>(data_section, size, round1_seed);
|
||||
|
||||
@@ -476,12 +476,12 @@ std::string encrypt_fixed_size_data_section_s(const void* data, size_t size, uin
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
|
||||
if (size < 2 * sizeof(U32T)) {
|
||||
throw runtime_error("data size is too small");
|
||||
throw std::runtime_error("data size is too small");
|
||||
}
|
||||
|
||||
uint32_t round2_seed = random_object<uint32_t>();
|
||||
|
||||
string encrypted(reinterpret_cast<const char*>(data), size);
|
||||
std::string encrypted(reinterpret_cast<const char*>(data), size);
|
||||
*reinterpret_cast<U32T*>(encrypted.data()) = 0;
|
||||
*reinterpret_cast<U32T*>(encrypted.data() + encrypted.size() - sizeof(U32T)) = round2_seed;
|
||||
*reinterpret_cast<U32T*>(encrypted.data()) = crc32(encrypted.data(), encrypted.size());
|
||||
|
||||
@@ -1276,7 +1276,7 @@ void send_quest_menu_t(
|
||||
auto v = c->quest_version();
|
||||
vector<EntryT> entries;
|
||||
for (const auto& quest : quests) {
|
||||
auto vq = quest->version(v);
|
||||
auto vq = quest->version(v, c->language());
|
||||
if (!vq) {
|
||||
continue;
|
||||
}
|
||||
@@ -2389,7 +2389,7 @@ void send_ep3_tournament_details(
|
||||
shared_ptr<const Episode3::Tournament> tourn) {
|
||||
S_TournamentGameDetails_GC_Ep3_E3 cmd;
|
||||
cmd.name = tourn->get_name();
|
||||
cmd.map_name = tourn->get_map()->map.name;
|
||||
cmd.map_name = tourn->get_map()->version(c->language())->map->name;
|
||||
cmd.rules = tourn->get_rules();
|
||||
const auto& teams = tourn->all_teams();
|
||||
for (size_t z = 0; z < min<size_t>(teams.size(), 0x20); z++) {
|
||||
@@ -2430,7 +2430,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
S_TournamentGameDetails_GC_Ep3_E3 cmd;
|
||||
cmd.name = encode_sjis(l->name);
|
||||
|
||||
cmd.map_name = tourn->get_map()->map.name;
|
||||
cmd.map_name = tourn->get_map()->version(c->language())->map->name;
|
||||
cmd.rules = tourn->get_rules();
|
||||
|
||||
const auto& teams = tourn->all_teams();
|
||||
@@ -2546,7 +2546,7 @@ void send_ep3_set_tournament_player_decks(shared_ptr<Client> c) {
|
||||
|
||||
G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D cmd;
|
||||
cmd.rules = tourn->get_rules();
|
||||
cmd.map_number = tourn->get_map()->map.map_number.load();
|
||||
cmd.map_number = tourn->get_map()->map_number;
|
||||
cmd.player_slot = 0xFF;
|
||||
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
@@ -2862,9 +2862,10 @@ bool send_ep3_start_tournament_deck_select_if_all_clients_ready(shared_ptr<Lobby
|
||||
|
||||
// If they're all done, start deck selection
|
||||
if (x == l->max_clients) {
|
||||
string data = Episode3::Server::prepare_6xB6x41_map_definition(
|
||||
tourn->get_map(), l->flags & Lobby::Flag::IS_EP3_TRIAL);
|
||||
send_command(l, 0x6C, 0x00, data);
|
||||
if (!l->ep3_server) {
|
||||
l->create_ep3_server();
|
||||
}
|
||||
l->ep3_server->send_6xB6x41_to_all_clients();
|
||||
for (auto c : l->clients) {
|
||||
if (c) {
|
||||
send_ep3_set_tournament_player_decks(c);
|
||||
|
||||
@@ -55,7 +55,7 @@ inline void send_command_excluding_client(std::shared_ptr<Lobby> l,
|
||||
void send_command_if_not_loading(std::shared_ptr<Lobby> l,
|
||||
uint16_t command, uint32_t flag, const void* data, size_t size);
|
||||
inline void send_command_if_not_loading(std::shared_ptr<Lobby> l,
|
||||
uint16_t command, uint32_t flag, const string& data) {
|
||||
uint16_t command, uint32_t flag, const std::string& data) {
|
||||
send_command_if_not_loading(l, command, flag, data.data(), data.size());
|
||||
}
|
||||
template <typename StructT>
|
||||
@@ -208,7 +208,7 @@ void send_chat_message(
|
||||
std::shared_ptr<Client> c,
|
||||
uint32_t from_guild_card_number,
|
||||
const std::u16string& from_name,
|
||||
const u16string& text,
|
||||
const std::u16string& text,
|
||||
char private_flags);
|
||||
void send_simple_mail(
|
||||
std::shared_ptr<Client> c,
|
||||
@@ -240,9 +240,9 @@ void send_card_search_result(
|
||||
void send_guild_card(
|
||||
Channel& ch,
|
||||
uint32_t guild_card_number,
|
||||
const u16string& name,
|
||||
const u16string& team_name,
|
||||
const u16string& description,
|
||||
const std::u16string& name,
|
||||
const std::u16string& team_name,
|
||||
const std::u16string& description,
|
||||
uint8_t section_id,
|
||||
uint8_t char_class);
|
||||
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
|
||||
@@ -359,8 +359,8 @@ void send_open_quest_file(
|
||||
std::shared_ptr<const std::string> contents,
|
||||
QuestFileType type);
|
||||
void send_quest_file_chunk(
|
||||
shared_ptr<Client> c,
|
||||
const string& filename,
|
||||
std::shared_ptr<Client> c,
|
||||
const std::string& filename,
|
||||
size_t chunk_index,
|
||||
const void* data,
|
||||
size_t size,
|
||||
|
||||
@@ -471,7 +471,7 @@ Proxy session commands:\n\
|
||||
} else if (command_name == "create-tournament") {
|
||||
string name = get_quoted_string(command_args);
|
||||
string map_name = get_quoted_string(command_args);
|
||||
auto map = this->state->ep3_map_index->definition_for_name(map_name);
|
||||
auto map = this->state->ep3_map_index->for_name(map_name);
|
||||
uint32_t num_teams = stoul(get_quoted_string(command_args), nullptr, 0);
|
||||
Episode3::Rules rules;
|
||||
rules.set_defaults();
|
||||
|
||||
@@ -584,27 +584,33 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
|
||||
this->ep3_card_auction_max_size = 0;
|
||||
}
|
||||
|
||||
for (const auto& it : json.get("CardAuctionPool", JSON::dict()).as_dict()) {
|
||||
this->ep3_card_auction_pool.emplace_back(
|
||||
CardAuctionPoolEntry{
|
||||
.probability = static_cast<uint64_t>(it.second->at(0).as_int()),
|
||||
.card_id = 0,
|
||||
.min_price = static_cast<uint16_t>(it.second->at(1).as_int()),
|
||||
.card_name = it.first});
|
||||
try {
|
||||
for (const auto& it : json.get_dict("CardAuctionPool")) {
|
||||
this->ep3_card_auction_pool.emplace_back(
|
||||
CardAuctionPoolEntry{
|
||||
.probability = static_cast<uint64_t>(it.second->at(0).as_int()),
|
||||
.card_id = 0,
|
||||
.min_price = static_cast<uint16_t>(it.second->at(1).as_int()),
|
||||
.card_name = it.first});
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
const auto& ep3_trap_cards_json = json.get("Episode3TrapCards", JSON::list()).as_list();
|
||||
if (!ep3_trap_cards_json.empty()) {
|
||||
if (ep3_trap_cards_json.size() != 5) {
|
||||
throw runtime_error("Episode3TrapCards must be a list of 5 lists");
|
||||
}
|
||||
this->ep3_trap_card_names.clear();
|
||||
for (const auto& trap_type_it : ep3_trap_cards_json) {
|
||||
auto& names = this->ep3_trap_card_names.emplace_back();
|
||||
for (const auto& card_it : trap_type_it->as_list()) {
|
||||
names.emplace_back(card_it->as_string());
|
||||
try {
|
||||
const auto& ep3_trap_cards_json = json.get_list("Episode3TrapCards");
|
||||
if (!ep3_trap_cards_json.empty()) {
|
||||
if (ep3_trap_cards_json.size() != 5) {
|
||||
throw runtime_error("Episode3TrapCards must be a list of 5 lists");
|
||||
}
|
||||
this->ep3_trap_card_names.clear();
|
||||
for (const auto& trap_type_it : ep3_trap_cards_json) {
|
||||
auto& names = this->ep3_trap_card_names.emplace_back();
|
||||
for (const auto& card_it : trap_type_it->as_list()) {
|
||||
names.emplace_back(card_it->as_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
if (!this->is_replay) {
|
||||
|
||||
@@ -462,8 +462,8 @@ char abbreviation_for_difficulty(uint8_t difficulty) {
|
||||
}
|
||||
}
|
||||
|
||||
char char_for_language_code(uint8_t language) {
|
||||
switch (language) {
|
||||
char char_for_language_code(uint8_t language_code) {
|
||||
switch (language_code) {
|
||||
case 0:
|
||||
return 'J';
|
||||
case 1:
|
||||
@@ -479,6 +479,28 @@ char char_for_language_code(uint8_t language) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t language_code_for_char(char language_char) {
|
||||
switch (language_char) {
|
||||
case 'J':
|
||||
case 'j':
|
||||
return 0;
|
||||
case 'E':
|
||||
case 'e':
|
||||
return 1;
|
||||
case 'G':
|
||||
case 'g':
|
||||
return 2;
|
||||
case 'F':
|
||||
case 'f':
|
||||
return 3;
|
||||
case 'S':
|
||||
case 's':
|
||||
return 4;
|
||||
default:
|
||||
throw runtime_error("unknown language");
|
||||
}
|
||||
}
|
||||
|
||||
size_t max_stack_size_for_item(uint8_t data0, uint8_t data1) {
|
||||
if (data0 == 4) {
|
||||
return 999999;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "FileContentsCache.hh"
|
||||
#include "Player.hh"
|
||||
@@ -32,8 +34,8 @@ const char* abbreviation_for_mode(GameMode mode);
|
||||
|
||||
size_t max_stack_size_for_item(uint8_t data0, uint8_t data1);
|
||||
|
||||
extern const vector<string> tech_id_to_name;
|
||||
extern const unordered_map<string, uint8_t> name_to_tech_id;
|
||||
extern const std::vector<std::string> tech_id_to_name;
|
||||
extern const std::unordered_map<std::string, uint8_t> name_to_tech_id;
|
||||
|
||||
const std::string& name_for_technique(uint8_t tech);
|
||||
std::u16string u16name_for_technique(uint8_t tech);
|
||||
@@ -74,7 +76,8 @@ const char* name_for_difficulty(uint8_t difficulty);
|
||||
const char* token_name_for_difficulty(uint8_t difficulty);
|
||||
char abbreviation_for_difficulty(uint8_t difficulty);
|
||||
|
||||
char char_for_language_code(uint8_t language);
|
||||
char char_for_language_code(uint8_t language_code);
|
||||
uint8_t language_code_for_char(char language_char);
|
||||
|
||||
extern const std::vector<const char*> name_for_mag_color;
|
||||
extern const std::unordered_map<std::string, uint8_t> mag_color_for_name;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../quests/e765-dlt-gc3-e.mnm
|
||||
@@ -1 +0,0 @@
|
||||
../../quests/e765-dlt-gc3.mnm
|
||||
@@ -0,0 +1 @@
|
||||
../../quests/e901-dl-gc3-e.mnm
|
||||
@@ -1 +0,0 @@
|
||||
../../quests/e901-dl-gc3.mnm
|
||||
@@ -0,0 +1 @@
|
||||
../../quests/e903-dl-gc3-e.mnm
|
||||
@@ -1 +0,0 @@
|
||||
../../quests/e903-dl-gc3.mnm
|
||||
@@ -0,0 +1 @@
|
||||
../../quests/e904-dl-gc3-e.mnm
|
||||
@@ -1 +0,0 @@
|
||||
../../quests/e904-dl-gc3.mnm
|
||||
@@ -0,0 +1 @@
|
||||
../../quests/e905-dl-gc3-e.mnm
|
||||
@@ -1 +0,0 @@
|
||||
../../quests/e905-dl-gc3.mnm
|
||||
@@ -0,0 +1 @@
|
||||
../../quests/e906-dl-gc3-e.mnm
|
||||
@@ -1 +0,0 @@
|
||||
../../quests/e906-dl-gc3.mnm
|
||||
@@ -0,0 +1 @@
|
||||
../../quests/e907-dl-gc3-e.mnm
|
||||
@@ -1 +0,0 @@
|
||||
../../quests/e907-dl-gc3.mnm
|
||||
@@ -0,0 +1 @@
|
||||
../../quests/e908-dl-gc3-e.mnm
|
||||
@@ -1 +0,0 @@
|
||||
../../quests/e908-dl-gc3.mnm
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |