implement quest version separation

This commit is contained in:
Martin Michelsen
2023-10-15 23:15:30 -07:00
parent 7005b573f5
commit 5d71b66f84
671 changed files with 928 additions and 619 deletions
+5 -2
View File
@@ -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.
+1
View File
@@ -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
+1 -1
View File
@@ -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);
+4 -1
View File
@@ -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
View File
@@ -2,6 +2,8 @@
#include "StaticGameData.hh"
using namespace std;
CommonItemSet::CommonItemSet(shared_ptr<const string> data)
: gsl(data, true) {}
+6 -6
View File
@@ -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 {
+249 -62
View File
@@ -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");
if (base_filename.size() < 2) {
throw runtime_error("filename too short for language code");
}
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[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);
}
+42 -12
View File
@@ -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 {
+76 -41
View File
@@ -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()) {
// 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(out_cmd));
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");
+29 -28
View File
@@ -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;
+11 -6
View File
@@ -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) {
+4 -4
View File
@@ -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);
+9 -15
View File
@@ -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);
+1 -3
View File
@@ -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);
+21
View File
@@ -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) {
+2 -1
View File
@@ -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;
+20 -8
View File
@@ -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;
}
+8 -1
View File
@@ -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;
}
+2
View File
@@ -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) {
+1 -1
View File
@@ -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,
+253 -240
View File
@@ -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));
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");
}
string ascii_name = encode_sjis(vq->name);
// 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 version of quest %" PRIu32 " \"%s\"",
filename.c_str(),
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());
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 quest %" PRIu32 " \"%s\" (%s, %s (%" PRIu32 "), %s)",
filename.c_str(),
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");
vq->joinable ? "joinable" : "not joinable",
dat_str.c_str());
}
} catch (const exception& e) {
static_game_data_log.warning("Failed to index quest file %s (%s)", filename.c_str(), e.what());
}
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");
}
+26 -42
View File
@@ -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,
+17 -39
View File
@@ -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);
}
+7 -6
View File
@@ -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;
}
}
+3 -3
View File
@@ -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());
+8 -7
View File
@@ -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);
+7 -7
View File
@@ -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,
+1 -1
View File
@@ -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();
+8 -2
View File
@@ -584,7 +584,8 @@ 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()) {
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()),
@@ -592,8 +593,11 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
.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();
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");
@@ -606,6 +610,8 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
}
}
}
} catch (const out_of_range&) {
}
if (!this->is_replay) {
for (const auto& it : json.get("Episode3LobbyBanners", JSON::list()).as_list()) {
+24 -2
View File
@@ -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;
+6 -3
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
../../quests/e765-dlt-gc3-e.mnm
-1
View File
@@ -1 +0,0 @@
../../quests/e765-dlt-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e901-dl-gc3-e.mnm
-1
View File
@@ -1 +0,0 @@
../../quests/e901-dl-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e903-dl-gc3-e.mnm
-1
View File
@@ -1 +0,0 @@
../../quests/e903-dl-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e904-dl-gc3-e.mnm
-1
View File
@@ -1 +0,0 @@
../../quests/e904-dl-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e905-dl-gc3-e.mnm
-1
View File
@@ -1 +0,0 @@
../../quests/e905-dl-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e906-dl-gc3-e.mnm
-1
View File
@@ -1 +0,0 @@
../../quests/e906-dl-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e907-dl-gc3-e.mnm
-1
View File
@@ -1 +0,0 @@
../../quests/e907-dl-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e908-dl-gc3-e.mnm
-1
View File
@@ -1 +0,0 @@
../../quests/e908-dl-gc3.mnm
View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Some files were not shown because too many files have changed in this diff Show More