add enemy, object, and event tracking for persistence
This commit is contained in:
+32
-8
@@ -311,10 +311,22 @@ static void server_command_qcheck(shared_ptr<Client> c, const std::string& args)
|
||||
auto l = c->require_lobby();
|
||||
uint16_t flag_num = stoul(args, nullptr, 0);
|
||||
|
||||
send_text_message_printf(c, "$C7Quest flag 0x%hX (%hu)\nis %s on %s",
|
||||
flag_num, flag_num,
|
||||
c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
||||
name_for_difficulty(l->difficulty));
|
||||
if (l->is_game()) {
|
||||
if (!l->quest_flags_known || l->quest_flags_known->get(l->difficulty, flag_num)) {
|
||||
send_text_message_printf(c, "$C7Game: flag 0x%hX (%hu)\nis %s on %s",
|
||||
flag_num, flag_num,
|
||||
c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
||||
name_for_difficulty(l->difficulty));
|
||||
} else {
|
||||
send_text_message_printf(c, "$C7Game: flag 0x%hX (%hu)\nis unknown on %s",
|
||||
flag_num, flag_num, name_for_difficulty(l->difficulty));
|
||||
}
|
||||
} else if (c->version() == Version::BB_V4) {
|
||||
send_text_message_printf(c, "$C7Player: flag 0x%hX (%hu)\nis %s on %s",
|
||||
flag_num, flag_num,
|
||||
c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
||||
name_for_difficulty(l->difficulty));
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_qset_qclear(shared_ptr<Client> c, const std::string& args, bool should_set) {
|
||||
@@ -330,10 +342,22 @@ static void server_command_qset_qclear(shared_ptr<Client> c, const std::string&
|
||||
|
||||
uint16_t flag_num = stoul(args, nullptr, 0);
|
||||
|
||||
if (should_set) {
|
||||
c->character()->quest_flags.set(l->difficulty, flag_num);
|
||||
} else {
|
||||
c->character()->quest_flags.clear(l->difficulty, flag_num);
|
||||
if (l->is_game()) {
|
||||
if (l->quest_flags_known) {
|
||||
l->quest_flags_known->set(l->difficulty, flag_num);
|
||||
}
|
||||
if (should_set) {
|
||||
l->quest_flag_values->set(l->difficulty, flag_num);
|
||||
} else {
|
||||
l->quest_flag_values->clear(l->difficulty, flag_num);
|
||||
}
|
||||
} else if (c->version() == Version::BB_V4) {
|
||||
auto p = c->character();
|
||||
if (should_set) {
|
||||
p->quest_flags.set(l->difficulty, flag_num);
|
||||
} else {
|
||||
p->quest_flags.clear(l->difficulty, flag_num);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_v1_or_v2(c->version())) {
|
||||
|
||||
+2
-1
@@ -361,9 +361,10 @@ bool Client::evaluate_quest_availability_expression(
|
||||
if (!expr) {
|
||||
return true;
|
||||
}
|
||||
auto l = this->lobby.lock();
|
||||
auto p = this->character();
|
||||
QuestAvailabilityExpression::Env env = {
|
||||
.flags = &p->quest_flags.data.at(difficulty),
|
||||
.flags = (l && !l->quest_flags_known) ? &l->quest_flag_values->data.at(difficulty) : &p->quest_flags.data.at(difficulty),
|
||||
.challenge_records = &p->challenge_records,
|
||||
.team = this->team(),
|
||||
.num_players = num_players,
|
||||
|
||||
+46
-44
@@ -34,60 +34,62 @@ public:
|
||||
// TODO: It'd be nice to use a pattern here (e.g. all server-side flags are
|
||||
// in the high bits) but that would require re-recording or manually
|
||||
// rewriting all the tests
|
||||
CLIENT_SIDE_MASK = 0xFFFFFFFFFC0FFFFB,
|
||||
CLIENT_SIDE_MASK = 0xFF3CFFFF7C0FFFFB,
|
||||
|
||||
// Version-related flags
|
||||
CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002,
|
||||
LICENSE_WAS_CREATED = 0x0000000000000004, // Server-side only
|
||||
NO_D6_AFTER_LOBBY = 0x0000000000000100,
|
||||
NO_D6 = 0x0000000000000200,
|
||||
FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400,
|
||||
CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002,
|
||||
LICENSE_WAS_CREATED = 0x0000000000000004, // Server-side only
|
||||
NO_D6_AFTER_LOBBY = 0x0000000000000100,
|
||||
NO_D6 = 0x0000000000000200,
|
||||
FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400,
|
||||
|
||||
// Flags describing the behavior for send_function_call
|
||||
NO_SEND_FUNCTION_CALL = 0x0000000000001000,
|
||||
ENCRYPTED_SEND_FUNCTION_CALL = 0x0000000000002000,
|
||||
SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x0000000000004000,
|
||||
SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000,
|
||||
USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000,
|
||||
NO_SEND_FUNCTION_CALL = 0x0000000000001000,
|
||||
ENCRYPTED_SEND_FUNCTION_CALL = 0x0000000000002000,
|
||||
SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x0000000000004000,
|
||||
SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000,
|
||||
USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000,
|
||||
|
||||
// State flags
|
||||
LOADING = 0x0000000000100000, // Server-side only
|
||||
LOADING_QUEST = 0x0000000000200000, // Server-side only
|
||||
LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000, // Server-side only
|
||||
LOADING_TOURNAMENT = 0x0000000000800000, // Server-side only
|
||||
IN_INFORMATION_MENU = 0x0000000001000000, // Server-side only
|
||||
AT_WELCOME_MESSAGE = 0x0000000002000000, // Server-side only
|
||||
SAVE_ENABLED = 0x0000000004000000,
|
||||
HAS_EP3_CARD_DEFS = 0x0000000008000000,
|
||||
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
|
||||
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
|
||||
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
|
||||
AT_BANK_COUNTER = 0x0000000080000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only
|
||||
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
|
||||
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
|
||||
LOADING = 0x0000000000100000, // Server-side only
|
||||
LOADING_QUEST = 0x0000000000200000, // Server-side only
|
||||
LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000, // Server-side only
|
||||
LOADING_TOURNAMENT = 0x0000000000800000, // Server-side only
|
||||
IN_INFORMATION_MENU = 0x0000000001000000, // Server-side only
|
||||
AT_WELCOME_MESSAGE = 0x0000000002000000, // Server-side only
|
||||
SAVE_ENABLED = 0x0000000004000000,
|
||||
HAS_EP3_CARD_DEFS = 0x0000000008000000,
|
||||
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
|
||||
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
|
||||
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
|
||||
AT_BANK_COUNTER = 0x0000000080000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_OBJECT_STATE = 0x0080000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only
|
||||
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
|
||||
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
|
||||
|
||||
// Cheat mode and option flags
|
||||
INFINITE_HP_ENABLED = 0x0000000200000000,
|
||||
INFINITE_TP_ENABLED = 0x0000000400000000,
|
||||
DEBUG_ENABLED = 0x0000000800000000,
|
||||
ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000,
|
||||
ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000,
|
||||
INFINITE_HP_ENABLED = 0x0000000200000000,
|
||||
INFINITE_TP_ENABLED = 0x0000000400000000,
|
||||
DEBUG_ENABLED = 0x0000000800000000,
|
||||
ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000,
|
||||
ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000,
|
||||
|
||||
// Proxy option flags
|
||||
PROXY_SAVE_FILES = 0x0000001000000000,
|
||||
PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000,
|
||||
PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000,
|
||||
PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000,
|
||||
PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000,
|
||||
PROXY_ZERO_REMOTE_GUILD_CARD = 0x0000040000000000,
|
||||
PROXY_EP3_INFINITE_MESETA_ENABLED = 0x0000080000000000,
|
||||
PROXY_EP3_INFINITE_TIME_ENABLED = 0x0000100000000000,
|
||||
PROXY_RED_NAME_ENABLED = 0x0000200000000000,
|
||||
PROXY_BLANK_NAME_ENABLED = 0x0000400000000000,
|
||||
PROXY_BLOCK_FUNCTION_CALLS = 0x0000800000000000,
|
||||
PROXY_EP3_UNMASK_WHISPERS = 0x0008000000000000,
|
||||
PROXY_SAVE_FILES = 0x0000001000000000,
|
||||
PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000,
|
||||
PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000,
|
||||
PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000,
|
||||
PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000,
|
||||
PROXY_ZERO_REMOTE_GUILD_CARD = 0x0000040000000000,
|
||||
PROXY_EP3_INFINITE_MESETA_ENABLED = 0x0000080000000000,
|
||||
PROXY_EP3_INFINITE_TIME_ENABLED = 0x0000100000000000,
|
||||
PROXY_RED_NAME_ENABLED = 0x0000200000000000,
|
||||
PROXY_BLANK_NAME_ENABLED = 0x0000400000000000,
|
||||
PROXY_BLOCK_FUNCTION_CALLS = 0x0000800000000000,
|
||||
PROXY_EP3_UNMASK_WHISPERS = 0x0008000000000000,
|
||||
// clang-format on
|
||||
};
|
||||
enum class ItemDropNotificationMode {
|
||||
|
||||
+15
-8
@@ -4629,9 +4629,9 @@ struct G_UseStarAtomizer_6x66 {
|
||||
parray<le_uint16_t, 4> target_client_ids;
|
||||
} __packed__;
|
||||
|
||||
// 6x67: Trigger wave event
|
||||
// 6x67: Trigger set event
|
||||
|
||||
struct G_TriggerWaveEvent_6x67 {
|
||||
struct G_TriggerSetEvent_6x67 {
|
||||
G_UnusedHeader header;
|
||||
le_uint32_t floor = 0;
|
||||
le_uint32_t event_id = 0; // NOT event index
|
||||
@@ -4773,7 +4773,12 @@ struct G_SyncSetFlagState_6x6E_Decompressed {
|
||||
|
||||
// 6x6F: Set quest flags (used while loading into game)
|
||||
|
||||
struct G_SetQuestFlags_6x6F {
|
||||
struct G_SetQuestFlagsV1_6x6F {
|
||||
G_UnusedHeader header;
|
||||
QuestFlagsV1 quest_flags;
|
||||
} __packed__;
|
||||
|
||||
struct G_SetQuestFlagsV2V3V4_6x6F {
|
||||
G_UnusedHeader header;
|
||||
QuestFlags quest_flags;
|
||||
} __packed__;
|
||||
@@ -4988,12 +4993,14 @@ struct G_UpdateQuestFlag_V3_BB_6x75 : G_UpdateQuestFlag_DC_PC_6x75 {
|
||||
le_uint16_t unused = 0;
|
||||
} __packed__;
|
||||
|
||||
// 6x76: Enemy killed
|
||||
// 6x76: Set entity flags
|
||||
// This command can only be used to set flags, since the game performs a bitwise
|
||||
// OR operation instead of a simple assignment.
|
||||
|
||||
struct G_EnemyKilled_6x76 {
|
||||
G_EnemyIDHeader header;
|
||||
le_uint16_t unknown_a1 = 0;
|
||||
le_uint16_t unknown_a2 = 0; // Flags of some sort
|
||||
struct G_SetEntityFlags_6x76 {
|
||||
G_EnemyIDHeader header; // 1000-3FFF = enemy, 4000-FFFF = object
|
||||
le_uint16_t floor = 0;
|
||||
le_uint16_t flags = 0;
|
||||
} __packed__;
|
||||
|
||||
// 6x77: Sync quest data
|
||||
|
||||
+25
-2
@@ -325,11 +325,13 @@ shared_ptr<Map> Lobby::load_maps(
|
||||
shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
const parray<le_uint32_t, 0x20>& variations,
|
||||
const PrefixedLogger* log) {
|
||||
auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, true);
|
||||
auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, false);
|
||||
auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::ENEMIES);
|
||||
auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::OBJECTS);
|
||||
auto event_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::EVENTS);
|
||||
return Lobby::load_maps(
|
||||
enemy_filenames,
|
||||
object_filenames,
|
||||
event_filenames,
|
||||
version,
|
||||
episode,
|
||||
mode,
|
||||
@@ -346,6 +348,7 @@ shared_ptr<Map> Lobby::load_maps(
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
const vector<string>& enemy_filenames,
|
||||
const vector<string>& object_filenames,
|
||||
const vector<string>& event_filenames,
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
@@ -402,6 +405,21 @@ shared_ptr<Map> Lobby::load_maps(
|
||||
} else if (log) {
|
||||
log->info("No objects to load for floor %02zX", floor);
|
||||
}
|
||||
|
||||
const auto& floor_event_filename = event_filenames.at(floor);
|
||||
if (!floor_event_filename.empty()) {
|
||||
auto map_data = get_file_data(version, floor_event_filename);
|
||||
if (map_data) {
|
||||
map->add_events_from_map_data(floor, map_data->data(), map_data->size());
|
||||
if (log) {
|
||||
log->info("Loaded events map %s for floor %02zX", floor_event_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("Events map %s for floor %02zX cannot be used; skipping", floor_event_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("No events to load for floor %02zX", floor);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
@@ -464,6 +482,11 @@ void Lobby::load_maps() {
|
||||
string e_str = this->map->enemies[z].str();
|
||||
this->log.info("(E-%zX) %s", z, e_str.c_str());
|
||||
}
|
||||
this->log.info("Generated events list (%zu entries):", this->map->events.size());
|
||||
for (size_t z = 0; z < this->map->events.size(); z++) {
|
||||
string e_str = this->map->events[z].str();
|
||||
this->log.info("%s", e_str.c_str());
|
||||
}
|
||||
this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)",
|
||||
this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size());
|
||||
}
|
||||
|
||||
+4
-3
@@ -92,15 +92,15 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
uint32_t min_level;
|
||||
uint32_t max_level;
|
||||
|
||||
// Item state
|
||||
// Game state
|
||||
std::array<uint32_t, 12> next_item_id_for_client;
|
||||
uint32_t next_game_item_id;
|
||||
std::vector<FloorItemManager> floor_item_managers;
|
||||
|
||||
// Map state
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_enemy_rates;
|
||||
std::shared_ptr<Map> map;
|
||||
parray<le_uint32_t, 0x20> variations;
|
||||
std::unique_ptr<QuestFlags> quest_flags_known; // If null, ALL quest flags are known
|
||||
std::unique_ptr<QuestFlags> quest_flag_values;
|
||||
|
||||
// Game config
|
||||
Version base_version;
|
||||
@@ -226,6 +226,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
static std::shared_ptr<Map> load_maps(
|
||||
const std::vector<std::string>& enemy_filenames,
|
||||
const std::vector<std::string>& object_filenames,
|
||||
const std::vector<std::string>& event_filenames,
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
|
||||
+116
-181
@@ -1127,187 +1127,6 @@ Action a_disassemble_set_data_table(
|
||||
string str = sdt.str();
|
||||
write_output_data(args, str.data(), str.size(), "txt");
|
||||
});
|
||||
Action a_check_set_data_table(
|
||||
"check-set-data-tables", nullptr, +[](Arguments&) {
|
||||
auto s = make_shared<ServerState>();
|
||||
s->load_patch_indexes(false);
|
||||
s->load_set_data_tables(false);
|
||||
static_game_data_log.min_level = LogLevel::DISABLED;
|
||||
|
||||
auto get_file_data = [&](Version version, const string& filename) -> shared_ptr<const string> {
|
||||
try {
|
||||
return s->load_map_file(version, filename);
|
||||
} catch (const cannot_open_file&) {
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
auto check_filenames = [&](Version version, const string& sdt_filename, const vector<string>& ns_filenames) -> string {
|
||||
for (size_t z = 0; z < ns_filenames.size(); z++) {
|
||||
const auto& ns_filename = ns_filenames[z];
|
||||
auto data = get_file_data(version, ns_filename);
|
||||
if (data) {
|
||||
if (sdt_filename != ns_filename) {
|
||||
string ns_filenames_str = join(ns_filenames, ", ");
|
||||
return string_printf("SDT => %s, NS => [%s]", sdt_filename.c_str(), ns_filenames_str.c_str());
|
||||
}
|
||||
return "OK";
|
||||
}
|
||||
if (!data && (sdt_filename == ns_filename)) {
|
||||
string ns_filenames_str = join(ns_filenames, ", ");
|
||||
return string_printf("SDT => %s (missing)", sdt_filename.c_str());
|
||||
}
|
||||
}
|
||||
if (ns_filenames.empty() && sdt_filename.empty()) {
|
||||
return "OK (no files)";
|
||||
} else if (ns_filenames.empty()) {
|
||||
auto data = get_file_data(version, sdt_filename);
|
||||
if (data) {
|
||||
return string_printf("NS blank, SDT => %s", sdt_filename.c_str());
|
||||
} else {
|
||||
return string_printf("NS blank, SDT => %s (missing)", sdt_filename.c_str());
|
||||
}
|
||||
} else if (sdt_filename.empty()) {
|
||||
string ns_filenames_str = join(ns_filenames, ", ");
|
||||
return string_printf("SDT blank, NS => [%s] (all missing)", ns_filenames_str.c_str());
|
||||
} else {
|
||||
string ns_filenames_str = join(ns_filenames, ", ");
|
||||
return string_printf("SDT => %s (missing), NS => [%s] (all missing)", sdt_filename.c_str(), ns_filenames_str.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
size_t num_checks = 0;
|
||||
size_t num_errors = 0;
|
||||
auto check_table = [&](Version version) {
|
||||
vector<Episode> episodes({Episode::EP1});
|
||||
if (!is_v1_or_v2(version) || (version == Version::GC_NTE)) {
|
||||
episodes.emplace_back(Episode::EP2);
|
||||
if (is_v4(version)) {
|
||||
episodes.emplace_back(Episode::EP4);
|
||||
}
|
||||
}
|
||||
|
||||
vector<GameMode> modes({GameMode::NORMAL});
|
||||
if (!is_v1(version)) {
|
||||
modes.emplace_back(GameMode::BATTLE);
|
||||
modes.emplace_back(GameMode::CHALLENGE);
|
||||
}
|
||||
if (is_v4(version)) {
|
||||
modes.emplace_back(GameMode::SOLO);
|
||||
}
|
||||
|
||||
uint8_t max_difficulty = is_v1(version) ? 2 : 3;
|
||||
|
||||
for (Episode episode : episodes) {
|
||||
for (GameMode mode : modes) {
|
||||
for (uint8_t difficulty = 0; difficulty <= max_difficulty; difficulty++) {
|
||||
auto sdt = s->set_data_table(version, episode, mode, difficulty);
|
||||
auto ns_var_maxes = variation_maxes_deprecated(version, episode, (mode == GameMode::SOLO));
|
||||
size_t num_floors;
|
||||
if (episode == Episode::EP4) {
|
||||
num_floors = 0x0B;
|
||||
} else if (episode == Episode::EP2) {
|
||||
num_floors = 0x10;
|
||||
} else {
|
||||
num_floors = 0x0F;
|
||||
}
|
||||
for (size_t floor = 0; floor < num_floors; floor++) {
|
||||
auto sdt_var_avail = sdt->num_available_variations_for_floor(episode, floor);
|
||||
auto sdt_var_maxes = sdt->num_free_roam_variations_for_floor(episode, mode == GameMode::SOLO, floor);
|
||||
size_t sdt_var1_max_avail = sdt_var_avail.first - 1;
|
||||
size_t sdt_var2_max_avail = sdt_var_avail.second - 1;
|
||||
size_t sdt_var1_max = sdt_var_maxes.first - 1;
|
||||
size_t sdt_var2_max = sdt_var_maxes.second - 1;
|
||||
size_t ns_var1_max = ns_var_maxes[floor * 2];
|
||||
size_t ns_var2_max = ns_var_maxes[floor * 2 + 1];
|
||||
num_checks += 4;
|
||||
if (sdt_var1_max > sdt_var1_max_avail) {
|
||||
fprintf(stdout, "## %-8s %-10s %-10s %-10s %02zX VAR1:[SDT:%02zX SDTA:%02zX]\n",
|
||||
name_for_enum(version),
|
||||
name_for_episode(episode),
|
||||
name_for_mode(mode),
|
||||
name_for_difficulty(difficulty),
|
||||
floor,
|
||||
sdt_var1_max,
|
||||
sdt_var1_max_avail);
|
||||
num_errors++;
|
||||
}
|
||||
if (sdt_var2_max > sdt_var2_max_avail) {
|
||||
fprintf(stdout, "## %-8s %-10s %-10s %-10s %02zX VAR2:[SDT:%02zX SDTA:%02zX]\n",
|
||||
name_for_enum(version),
|
||||
name_for_episode(episode),
|
||||
name_for_mode(mode),
|
||||
name_for_difficulty(difficulty),
|
||||
floor,
|
||||
sdt_var2_max,
|
||||
sdt_var2_max_avail);
|
||||
num_errors++;
|
||||
}
|
||||
if (sdt_var1_max < ns_var1_max) {
|
||||
fprintf(stdout, "## %-8s %-10s %-10s %-10s %02zX VAR1:[SDT:%02zX NS:%02zX]\n",
|
||||
name_for_enum(version),
|
||||
name_for_episode(episode),
|
||||
name_for_mode(mode),
|
||||
name_for_difficulty(difficulty),
|
||||
floor,
|
||||
sdt_var1_max,
|
||||
ns_var1_max);
|
||||
num_errors++;
|
||||
}
|
||||
if (sdt_var2_max < ns_var2_max) {
|
||||
fprintf(stdout, "## %-8s %-10s %-10s %-10s %02zX VAR2:[SDT:%02zX NS:%02zX]\n",
|
||||
name_for_enum(version),
|
||||
name_for_episode(episode),
|
||||
name_for_mode(mode),
|
||||
name_for_difficulty(difficulty),
|
||||
floor,
|
||||
sdt_var2_max,
|
||||
ns_var2_max);
|
||||
num_errors++;
|
||||
}
|
||||
for (size_t var1 = 0; var1 <= ns_var1_max; var1++) {
|
||||
for (size_t var2 = 0; var2 <= ns_var2_max; var2++) {
|
||||
auto sdt_enemy_filename = sdt->map_filename_for_variation(floor, var1, var2, episode, mode, true);
|
||||
auto sdt_object_filename = sdt->map_filename_for_variation(floor, var1, var2, episode, mode, false);
|
||||
auto ns_enemy_filenames = map_filenames_for_variation_deprecated(floor, var1, var2, version, episode, mode, true);
|
||||
auto ns_object_filenames = map_filenames_for_variation_deprecated(floor, var1, var2, version, episode, mode, false);
|
||||
string enemies_error = check_filenames(version, sdt_enemy_filename, ns_enemy_filenames);
|
||||
string objects_error = check_filenames(version, sdt_object_filename, ns_object_filenames);
|
||||
num_checks += 2;
|
||||
num_errors += (enemies_error != "OK") + (objects_error != "OK");
|
||||
fprintf(stdout, "%s %-8s %-10s %-10s %-10s %02zX %02zX %02zX E:[%s] O:[%s] E:%-30s O:%-30s\n",
|
||||
((enemies_error != "OK") || (objects_error != "OK")) ? "##" : " ",
|
||||
name_for_enum(version),
|
||||
name_for_episode(episode),
|
||||
name_for_mode(mode),
|
||||
name_for_difficulty(difficulty),
|
||||
floor,
|
||||
var1,
|
||||
var2,
|
||||
enemies_error.c_str(),
|
||||
objects_error.c_str(),
|
||||
sdt_enemy_filename.c_str(),
|
||||
sdt_object_filename.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
check_table(Version::DC_NTE);
|
||||
check_table(Version::DC_V1_11_2000_PROTOTYPE);
|
||||
check_table(Version::DC_V1);
|
||||
check_table(Version::DC_V2);
|
||||
check_table(Version::PC_NTE);
|
||||
check_table(Version::PC_V2);
|
||||
check_table(Version::GC_NTE);
|
||||
check_table(Version::GC_V3);
|
||||
check_table(Version::XB_V3);
|
||||
check_table(Version::BB_V4);
|
||||
fprintf(stdout, "%zu/%zu errors\n", num_errors, num_checks);
|
||||
});
|
||||
|
||||
Action a_assemble_quest_script(
|
||||
"assemble-quest-script", "\
|
||||
@@ -2189,6 +2008,122 @@ Action a_find_rare_enemy_seeds(
|
||||
parallel_range<uint64_t>(thread_fn, 0, 0x100000000, num_threads, nullptr);
|
||||
});
|
||||
|
||||
Action a_load_maps_test(
|
||||
"load-maps-test", nullptr, +[](Arguments&) {
|
||||
using SDT = SetDataTable;
|
||||
auto s = make_shared<ServerState>("system/config.json");
|
||||
s->load_config_early();
|
||||
s->clear_map_file_caches();
|
||||
s->load_patch_indexes(false);
|
||||
s->load_set_data_tables(false);
|
||||
s->load_quest_index(false);
|
||||
for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) {
|
||||
Version v = static_cast<Version>(v_s);
|
||||
if (is_ep3(v)) {
|
||||
continue;
|
||||
}
|
||||
const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
||||
for (Episode episode : episodes) {
|
||||
if (episode == Episode::EP4 && !is_v4(v)) {
|
||||
continue;
|
||||
}
|
||||
if (episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) {
|
||||
continue;
|
||||
}
|
||||
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
||||
for (GameMode mode : modes) {
|
||||
if ((mode == GameMode::BATTLE || mode == GameMode::CHALLENGE) && is_v1(v)) {
|
||||
continue;
|
||||
}
|
||||
if (mode == GameMode::SOLO && !is_v4(v)) {
|
||||
continue;
|
||||
}
|
||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
if (difficulty == 3 && is_v1(v)) {
|
||||
continue;
|
||||
}
|
||||
auto sdt = s->set_data_table(v, episode, mode, difficulty);
|
||||
for (uint8_t floor = 0; floor < 0x12; floor++) {
|
||||
auto variation_maxes = sdt->num_free_roam_variations_for_floor(episode, mode == GameMode::SOLO, floor);
|
||||
for (size_t var1 = 0; var1 < variation_maxes.first; var1++) {
|
||||
for (size_t var2 = 0; var2 < variation_maxes.second; var2++) {
|
||||
auto enemies_filename = sdt->map_filename_for_variation(
|
||||
floor, var1, var2, episode, mode, SDT::FilenameType::ENEMIES);
|
||||
auto objects_filename = sdt->map_filename_for_variation(
|
||||
floor, var1, var2, episode, mode, SDT::FilenameType::OBJECTS);
|
||||
auto events_filename = sdt->map_filename_for_variation(
|
||||
floor, var1, var2, episode, mode, SDT::FilenameType::EVENTS);
|
||||
|
||||
fprintf(stderr, "... %s %s %s %s %02hhX %zX %zX",
|
||||
name_for_enum(v), name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), floor, var1, var2);
|
||||
auto map = make_shared<Map>(v, 0, 0, nullptr);
|
||||
if (!enemies_filename.empty()) {
|
||||
fprintf(stderr, " [%s => ", enemies_filename.c_str());
|
||||
auto map_data = s->load_map_file(v, enemies_filename);
|
||||
if (map_data) {
|
||||
map->add_enemies_from_map_data(
|
||||
episode, difficulty, 0, 0, map_data->data(), map_data->size(), Map::DEFAULT_RARE_ENEMIES);
|
||||
fprintf(stderr, "%zu enemies, %zu sets]", map->enemies.size(), map->enemy_set_flags.size());
|
||||
} else {
|
||||
fprintf(stderr, "__MISSING__]");
|
||||
}
|
||||
}
|
||||
if (!objects_filename.empty()) {
|
||||
fprintf(stderr, " [%s => ", objects_filename.c_str());
|
||||
auto map_data = s->load_map_file(v, objects_filename);
|
||||
if (map_data) {
|
||||
map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
||||
fprintf(stderr, "%zu objects]", map->objects.size());
|
||||
} else {
|
||||
fprintf(stderr, "__MISSING__]");
|
||||
}
|
||||
}
|
||||
if (!events_filename.empty()) {
|
||||
fprintf(stderr, " [%s => ", events_filename.c_str());
|
||||
auto map_data = s->load_map_file(v, events_filename);
|
||||
if (map_data) {
|
||||
map->add_events_from_map_data(floor, map_data->data(), map_data->size());
|
||||
fprintf(stderr, "%zu events, %zu action bytes]", map->events.size(), map->event_action_stream.size());
|
||||
} else {
|
||||
fprintf(stderr, "__MISSING__]");
|
||||
}
|
||||
}
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& q_it : s->default_quest_index->quests_by_number) {
|
||||
for (const auto& vq_it : q_it.second->versions) {
|
||||
auto vq = vq_it.second;
|
||||
shared_ptr<const Map> map = Lobby::load_maps(
|
||||
vq->version,
|
||||
vq->episode,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
Map::DEFAULT_RARE_ENEMIES,
|
||||
0,
|
||||
nullptr,
|
||||
vq->dat_contents_decompressed);
|
||||
fprintf(stderr, "... %" PRIu32 " (%s) %s %s %s => %zu enemies (%zu sets), %zu objects, %zu events\n",
|
||||
vq->quest_number,
|
||||
vq->name.c_str(),
|
||||
name_for_episode(vq->episode),
|
||||
name_for_enum(vq->version),
|
||||
name_for_language_code(vq->language),
|
||||
map->enemies.size(),
|
||||
map->enemy_set_flags.size(),
|
||||
map->objects.size(),
|
||||
map->events.size());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Action a_parse_object_graph(
|
||||
"parse-object-graph", nullptr, +[](Arguments& args) {
|
||||
uint32_t root_object_address = args.get<uint32_t>("root", Arguments::IntFormat::HEX);
|
||||
|
||||
+165
-49
@@ -675,6 +675,15 @@ string Map::Enemy::str() const {
|
||||
this->state_flags);
|
||||
}
|
||||
|
||||
string Map::Event::str() const {
|
||||
return string_printf("[Map::Event W-%02hhX-%" PRIX32 " flags=%04hX floor=%02hhX action_stream_offset=%" PRIX32 "]",
|
||||
this->floor,
|
||||
this->event_id,
|
||||
this->flags,
|
||||
this->floor,
|
||||
this->action_stream_offset);
|
||||
}
|
||||
|
||||
string Map::Object::str() const {
|
||||
return string_printf("[Map::Object source %zX %04hX(%s) @%04hX p1=%g p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]",
|
||||
this->source_index,
|
||||
@@ -1189,9 +1198,6 @@ void Map::add_enemy(
|
||||
case 0x00C7: // TBoss3VoloptHiraisin
|
||||
case 0x0118:
|
||||
add(EnemyType::UNKNOWN);
|
||||
this->log.warning(
|
||||
"(Entry %zu, offset %zX in file) Unknown enemy type %04hX",
|
||||
source_index, source_index * sizeof(EnemyEntry), e.base_type.load());
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -1320,6 +1326,10 @@ void Map::add_random_enemies_from_map_data(
|
||||
}
|
||||
wave_events_segment_r.go(wave_events_header.entries_offset);
|
||||
|
||||
size_t action_stream_base_offset = this->event_action_stream.size();
|
||||
this->event_action_stream += wave_events_segment_r.pread(
|
||||
wave_events_header.action_stream_offset, wave_events_segment_r.size() - wave_events_header.action_stream_offset);
|
||||
|
||||
const auto& locations_header = locations_segment_r.get<RandomEnemyLocationsHeader>();
|
||||
const auto& definitions_header = definitions_segment_r.get<RandomEnemyDefinitionsHeader>();
|
||||
auto definitions_r = definitions_segment_r.sub(
|
||||
@@ -1336,6 +1346,7 @@ void Map::add_random_enemies_from_map_data(
|
||||
size_t remaining_waves = random_state->rand_int_biased(1, entry.max_waves);
|
||||
// Trace: at 0080E125 EAX is wave count
|
||||
|
||||
le_uint32_t wave_next_event_id = entry.event_id;
|
||||
uint32_t wave_number = entry.wave_number;
|
||||
while (remaining_waves) {
|
||||
remaining_waves--;
|
||||
@@ -1422,17 +1433,60 @@ void Map::add_random_enemies_from_map_data(
|
||||
}
|
||||
}
|
||||
if (remaining_waves) {
|
||||
// We don't generate the event stream here, but the client does, and in
|
||||
// doing so, it uses one value from random to determine the delay
|
||||
// parameter of the event. To keep our state in sync with what the
|
||||
// client would do, we skip a random value here.
|
||||
random_state->random.next();
|
||||
/* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay);
|
||||
this->add_event(wave_next_event_id, entry.flags, floor, this->event_action_stream.size());
|
||||
this->event_action_stream.push_back(0x0C);
|
||||
wave_next_event_id = entry.event_id + wave_number + 10000;
|
||||
this->event_action_stream.append(reinterpret_cast<const char*>(&wave_next_event_id), sizeof(wave_next_event_id));
|
||||
this->event_action_stream.push_back(0x01);
|
||||
wave_number++;
|
||||
}
|
||||
}
|
||||
|
||||
// For the same reason as above, we need to skip another random value here.
|
||||
random_state->random.next();
|
||||
/* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay);
|
||||
this->add_event(wave_next_event_id, entry.flags, floor, action_stream_base_offset + entry.action_stream_offset);
|
||||
wave_number++;
|
||||
}
|
||||
}
|
||||
|
||||
void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t action_stream_offset) {
|
||||
size_t index = this->events.size();
|
||||
auto& ev = this->events.emplace_back();
|
||||
ev.event_id = event_id;
|
||||
ev.flags = flags;
|
||||
ev.floor = floor;
|
||||
ev.action_stream_offset = action_stream_offset;
|
||||
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
|
||||
if (!this->floor_and_event_id_to_index.emplace(k, index).second) {
|
||||
this->log.warning("Duplicate event ID: W-%02hhX-%" PRIX32, floor, event_id);
|
||||
}
|
||||
}
|
||||
|
||||
Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) {
|
||||
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
|
||||
return this->events.at(this->floor_and_event_id_to_index.at(k));
|
||||
}
|
||||
|
||||
const Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) const {
|
||||
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
|
||||
return this->events.at(this->floor_and_event_id_to_index.at(k));
|
||||
}
|
||||
|
||||
void Map::add_events_from_map_data(uint8_t floor, const void* data, size_t size) {
|
||||
StringReader r(data, size);
|
||||
const auto& header = r.get<EventsSectionHeader>();
|
||||
if (header.format != 0) {
|
||||
throw runtime_error("events section format is not zero");
|
||||
}
|
||||
|
||||
size_t action_stream_base_offset = this->event_action_stream.size();
|
||||
this->event_action_stream += r.pread(header.action_stream_offset, r.size() - header.action_stream_offset);
|
||||
|
||||
this->events.reserve(this->events.size() + header.entry_count);
|
||||
auto events_r = r.sub(header.entries_offset, sizeof(Event1Entry) * header.entry_count);
|
||||
while (!events_r.eof()) {
|
||||
const auto& entry = events_r.get<Event1Entry>();
|
||||
this->add_event(entry.event_id, entry.flags, floor, entry.action_stream_offset + action_stream_base_offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1519,23 +1573,10 @@ void Map::add_entities_from_quest_data(
|
||||
this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size);
|
||||
}
|
||||
|
||||
if (floor_sections.enemies != 0xFFFFFFFF) {
|
||||
const auto& header = r.pget<SectionHeader>(floor_sections.enemies);
|
||||
if (header.data_size % sizeof(EnemyEntry)) {
|
||||
throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size");
|
||||
}
|
||||
this->add_enemies_from_map_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
floor,
|
||||
r.pgetv(floor_sections.enemies + sizeof(header), header.data_size),
|
||||
header.data_size,
|
||||
rare_rates);
|
||||
|
||||
} else if ((floor_sections.wave_events != 0xFFFFFFFF) &&
|
||||
if ((floor_sections.wave_events != 0xFFFFFFFF) &&
|
||||
(floor_sections.random_enemy_locations != 0xFFFFFFFF) &&
|
||||
(floor_sections.random_enemy_definitions != 0xFFFFFFFF)) {
|
||||
// Challenge Mode random enemy waves
|
||||
const auto& wave_events_header = r.pget<SectionHeader>(floor_sections.wave_events);
|
||||
const auto& random_enemy_locations_header = r.pget<SectionHeader>(floor_sections.random_enemy_locations);
|
||||
const auto& random_enemy_definitions_header = r.pget<SectionHeader>(floor_sections.random_enemy_definitions);
|
||||
@@ -1552,6 +1593,29 @@ void Map::add_entities_from_quest_data(
|
||||
r.sub(floor_sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size),
|
||||
random_state,
|
||||
rare_rates);
|
||||
|
||||
} else {
|
||||
// Non-Challenge (standard) enemies
|
||||
if (floor_sections.enemies != 0xFFFFFFFF) {
|
||||
const auto& header = r.pget<SectionHeader>(floor_sections.enemies);
|
||||
if (header.data_size % sizeof(EnemyEntry)) {
|
||||
throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size");
|
||||
}
|
||||
this->add_enemies_from_map_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
floor,
|
||||
r.pgetv(floor_sections.enemies + sizeof(header), header.data_size),
|
||||
header.data_size,
|
||||
rare_rates);
|
||||
}
|
||||
|
||||
if (floor_sections.wave_events != 0xFFFFFFFF) {
|
||||
const auto& wave_events_header = r.pget<SectionHeader>(floor_sections.wave_events);
|
||||
const void* data = r.pgetv(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size);
|
||||
this->add_events_from_map_data(floor, data, wave_events_header.data_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1657,14 +1721,14 @@ parray<le_uint32_t, 0x20> SetDataTableBase::generate_variations(
|
||||
}
|
||||
|
||||
vector<string> SetDataTableBase::map_filenames_for_variations(
|
||||
const parray<le_uint32_t, 0x20>& variations, Episode episode, GameMode mode, bool is_enemies) const {
|
||||
const parray<le_uint32_t, 0x20>& variations, Episode episode, GameMode mode, FilenameType type) const {
|
||||
vector<string> ret;
|
||||
for (uint8_t floor = 0; floor < 0x10; floor++) {
|
||||
ret.emplace_back(this->map_filename_for_variation(
|
||||
floor, variations[floor * 2], variations[floor * 2 + 1], episode, mode, is_enemies));
|
||||
floor, variations[floor * 2], variations[floor * 2 + 1], episode, mode, type));
|
||||
}
|
||||
for (uint8_t floor = 0x10; floor < 0x12; floor++) {
|
||||
ret.emplace_back(this->map_filename_for_variation(floor, 0, 0, episode, mode, is_enemies));
|
||||
ret.emplace_back(this->map_filename_for_variation(floor, 0, 0, episode, mode, type));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -1738,8 +1802,8 @@ void SetDataTable::load_table_t(const string& data) {
|
||||
while (!var2_r.eof()) {
|
||||
auto& entry = var2_v.emplace_back();
|
||||
entry.object_list_basename = r.pget_cstr(var2_r.get<U32T>());
|
||||
entry.enemy_list_basename = r.pget_cstr(var2_r.get<U32T>());
|
||||
entry.event_list_basename = r.pget_cstr(var2_r.get<U32T>());
|
||||
entry.enemy_and_event_list_basename = r.pget_cstr(var2_r.get<U32T>());
|
||||
entry.area_setup_filename = r.pget_cstr(var2_r.get<U32T>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1750,7 +1814,10 @@ pair<uint32_t, uint32_t> SetDataTable::num_available_variations_for_floor(Episod
|
||||
if (area == 0xFF) {
|
||||
return make_pair(1, 1);
|
||||
} else {
|
||||
const auto& e = this->entries.at(area);
|
||||
if (area >= this->entries.size()) {
|
||||
return make_pair(1, 1);
|
||||
}
|
||||
const auto& e = this->entries[area];
|
||||
return make_pair(e.size(), e.at(0).size());
|
||||
}
|
||||
}
|
||||
@@ -1789,7 +1856,7 @@ pair<uint32_t, uint32_t> SetDataTable::num_free_roam_variations_for_floor(Episod
|
||||
}
|
||||
|
||||
string SetDataTable::map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const {
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const {
|
||||
uint8_t area = this->default_area_for_floor(episode, floor);
|
||||
if (area == 0xFF) {
|
||||
return "";
|
||||
@@ -1800,22 +1867,35 @@ string SetDataTable::map_filename_for_variation(
|
||||
}
|
||||
|
||||
const auto& entry = this->entries.at(area).at(var1).at(var2);
|
||||
string filename = is_enemies ? entry.enemy_list_basename : entry.object_list_basename;
|
||||
|
||||
filename += (is_enemies ? "e" : "o");
|
||||
string filename;
|
||||
switch (type) {
|
||||
case FilenameType::OBJECTS:
|
||||
filename = entry.object_list_basename + "o";
|
||||
break;
|
||||
case FilenameType::ENEMIES:
|
||||
filename = entry.enemy_and_event_list_basename + "e";
|
||||
break;
|
||||
case FilenameType::EVENTS:
|
||||
filename = entry.enemy_and_event_list_basename;
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid map filename type");
|
||||
}
|
||||
|
||||
bool is_events = (type == FilenameType::EVENTS);
|
||||
switch ((floor != 0) ? GameMode::NORMAL : mode) {
|
||||
case GameMode::NORMAL:
|
||||
filename += ".dat";
|
||||
filename += is_events ? ".evt" : ".dat";
|
||||
break;
|
||||
case GameMode::SOLO:
|
||||
filename += "_s.dat";
|
||||
filename += is_events ? "_s.evt" : "_s.dat";
|
||||
break;
|
||||
case GameMode::CHALLENGE:
|
||||
filename += "_c1.dat";
|
||||
filename += is_events ? "_c1.evt" : "_c1.dat";
|
||||
break;
|
||||
case GameMode::BATTLE:
|
||||
filename += "_d.dat";
|
||||
filename += is_events ? "_d.evt" : "_d.dat";
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid game mode");
|
||||
@@ -1826,14 +1906,15 @@ string SetDataTable::map_filename_for_variation(
|
||||
|
||||
string SetDataTable::str() const {
|
||||
vector<string> lines;
|
||||
lines.emplace_back(string_printf("FL/V1/V2 => ----------------------OBJECT -----------------------ENEMY -----------------------EVENT\n"));
|
||||
lines.emplace_back(string_printf("FL/V1/V2 => ----------------------OBJECT -----------------ENEMY+EVENT -----------------------SETUP\n"));
|
||||
for (size_t a = 0; a < this->entries.size(); a++) {
|
||||
const auto& v1_v = this->entries[a];
|
||||
for (size_t v1 = 0; v1 < v1_v.size(); v1++) {
|
||||
const auto& v2_v = v1_v[v1];
|
||||
for (size_t v2 = 0; v2 < v2_v.size(); v2++) {
|
||||
const auto& e = v2_v[v2];
|
||||
lines.emplace_back(string_printf("%02zX/%02zX/%02zX => %28s %28s %28s\n", a, v1, v2, e.object_list_basename.c_str(), e.enemy_list_basename.c_str(), e.event_list_basename.c_str()));
|
||||
lines.emplace_back(string_printf("%02zX/%02zX/%02zX => %28s %28s %28s\n",
|
||||
a, v1, v2, e.object_list_basename.c_str(), e.enemy_and_event_list_basename.c_str(), e.area_setup_filename.c_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1854,7 +1935,7 @@ struct AreaMapFileInfo {
|
||||
variation2_values(variation2_values) {}
|
||||
};
|
||||
|
||||
const array<vector<vector<string>>, 0x10> SetDataTableDCNTE::NAMES = {{
|
||||
const array<vector<vector<string>>, 0x12> SetDataTableDCNTE::NAMES = {{
|
||||
/* 00 */ {{"map_city00_00"}},
|
||||
/* 01 */ {{"map_forest01_00", "map_forest01_01"}},
|
||||
/* 02 */ {{"map_forest02_00", "map_forest02_03"}},
|
||||
@@ -1871,12 +1952,15 @@ const array<vector<vector<string>>, 0x10> SetDataTableDCNTE::NAMES = {{
|
||||
/* 0D */ {{"map_boss03"}},
|
||||
/* 0E */ {{"map_boss04"}},
|
||||
/* 0F */ {{"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}},
|
||||
/* 10 */ {},
|
||||
/* 11 */ {},
|
||||
}};
|
||||
|
||||
SetDataTableDCNTE::SetDataTableDCNTE() : SetDataTableBase(Version::DC_NTE) {}
|
||||
|
||||
pair<uint32_t, uint32_t> SetDataTableDCNTE::num_available_variations_for_floor(Episode, uint8_t floor) const {
|
||||
return make_pair(this->NAMES[floor].size(), this->NAMES[floor][0].size());
|
||||
const auto& floor_names = this->NAMES.at(floor);
|
||||
return make_pair(floor_names.size(), floor_names.empty() ? 0 : this->NAMES.at(floor)[0].size());
|
||||
}
|
||||
|
||||
pair<uint32_t, uint32_t> SetDataTableDCNTE::num_free_roam_variations_for_floor(Episode episode, bool, uint8_t floor) const {
|
||||
@@ -1884,14 +1968,29 @@ pair<uint32_t, uint32_t> SetDataTableDCNTE::num_free_roam_variations_for_floor(E
|
||||
}
|
||||
|
||||
string SetDataTableDCNTE::map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, bool is_enemies) const {
|
||||
if (floor >= this->NAMES.size()) {
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, FilenameType type) const {
|
||||
try {
|
||||
string basename = this->NAMES.at(floor).at(var1).at(var2);
|
||||
switch (type) {
|
||||
case FilenameType::ENEMIES:
|
||||
basename += "e.dat";
|
||||
break;
|
||||
case FilenameType::OBJECTS:
|
||||
basename += "o.dat";
|
||||
break;
|
||||
case FilenameType::EVENTS:
|
||||
basename += ".evt";
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid map filename type");
|
||||
}
|
||||
return basename;
|
||||
} catch (const out_of_range&) {
|
||||
return "";
|
||||
}
|
||||
return this->NAMES.at(floor).at(var1).at(var2) + (is_enemies ? "e.dat" : "o.dat");
|
||||
}
|
||||
|
||||
const array<vector<vector<string>>, 0x10> SetDataTableDC112000::NAMES = {{
|
||||
const array<vector<vector<string>>, 0x12> SetDataTableDC112000::NAMES = {{
|
||||
/* 00 */ {{"map_city00_00"}},
|
||||
/* 01 */ {{"map_forest01_00", "map_forest01_01", "map_forest01_02", "map_forest01_03", "map_forest01_04"}},
|
||||
/* 02 */ {{"map_forest02_00", "map_forest02_01", "map_forest02_02", "map_forest02_03", "map_forest02_04"}},
|
||||
@@ -1908,12 +2007,15 @@ const array<vector<vector<string>>, 0x10> SetDataTableDC112000::NAMES = {{
|
||||
/* 0D */ {{"map_boss03"}},
|
||||
/* 0E */ {{"map_boss04"}},
|
||||
/* 0F */ {{"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}},
|
||||
/* 10 */ {},
|
||||
/* 11 */ {},
|
||||
}};
|
||||
|
||||
SetDataTableDC112000::SetDataTableDC112000() : SetDataTableBase(Version::DC_V1_11_2000_PROTOTYPE) {}
|
||||
|
||||
pair<uint32_t, uint32_t> SetDataTableDC112000::num_available_variations_for_floor(Episode, uint8_t floor) const {
|
||||
return make_pair(this->NAMES[floor].size(), this->NAMES[floor][0].size());
|
||||
const auto& floor_names = this->NAMES.at(floor);
|
||||
return make_pair(floor_names.size(), floor_names.empty() ? 0 : this->NAMES.at(floor)[0].size());
|
||||
}
|
||||
|
||||
pair<uint32_t, uint32_t> SetDataTableDC112000::num_free_roam_variations_for_floor(Episode episode, bool, uint8_t floor) const {
|
||||
@@ -1921,11 +2023,25 @@ pair<uint32_t, uint32_t> SetDataTableDC112000::num_free_roam_variations_for_floo
|
||||
}
|
||||
|
||||
string SetDataTableDC112000::map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, bool is_enemies) const {
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, FilenameType type) const {
|
||||
if (floor >= this->NAMES.size()) {
|
||||
return "";
|
||||
}
|
||||
return this->NAMES.at(floor).at(var1).at(var2) + (is_enemies ? "e.dat" : "o.dat");
|
||||
string basename = this->NAMES.at(floor).at(var1).at(var2);
|
||||
switch (type) {
|
||||
case FilenameType::ENEMIES:
|
||||
basename += "e.dat";
|
||||
break;
|
||||
case FilenameType::OBJECTS:
|
||||
basename += "o.dat";
|
||||
break;
|
||||
case FilenameType::EVENTS:
|
||||
basename += ".evt";
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid map filename type");
|
||||
}
|
||||
return basename;
|
||||
}
|
||||
|
||||
static const vector<AreaMapFileInfo> map_file_info_dc_nte = {
|
||||
|
||||
+33
-10
@@ -94,7 +94,7 @@ struct Map {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct EventsSectionHeader { // Section type 3 (WAVE_EVENTS)
|
||||
/* 00 */ le_uint32_t footer_offset;
|
||||
/* 00 */ le_uint32_t action_stream_offset;
|
||||
/* 04 */ le_uint32_t entries_offset;
|
||||
/* 08 */ le_uint32_t entry_count;
|
||||
/* 0C */ be_uint32_t format; // 0 or 'evt2'
|
||||
@@ -248,6 +248,15 @@ struct Map {
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Event {
|
||||
uint32_t event_id;
|
||||
uint16_t flags;
|
||||
uint8_t floor;
|
||||
uint32_t action_stream_offset;
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct DATParserRandomState {
|
||||
PSOV2Encryption random;
|
||||
PSOV2Encryption location_table_random;
|
||||
@@ -297,6 +306,11 @@ struct Map {
|
||||
std::shared_ptr<DATParserRandomState> random_state,
|
||||
std::shared_ptr<const RareEnemyRates> rare_rates = DEFAULT_RARE_ENEMIES);
|
||||
|
||||
void add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t action_stream_offset);
|
||||
Event& get_event(uint8_t floor, uint32_t event_id);
|
||||
const Event& get_event(uint8_t floor, uint32_t event_id) const;
|
||||
void add_events_from_map_data(uint8_t floor, const void* data, size_t size);
|
||||
|
||||
struct DATSectionsForFloor {
|
||||
uint32_t objects = 0xFFFFFFFF;
|
||||
uint32_t enemies = 0xFFFFFFFF;
|
||||
@@ -327,6 +341,9 @@ struct Map {
|
||||
std::vector<Enemy> enemies;
|
||||
std::vector<uint16_t> enemy_set_flags;
|
||||
std::vector<size_t> rare_enemy_indexes;
|
||||
std::vector<Event> events;
|
||||
std::string event_action_stream;
|
||||
std::unordered_map<uint64_t, size_t> floor_and_event_id_to_index;
|
||||
};
|
||||
|
||||
class SetDataTableBase {
|
||||
@@ -340,10 +357,16 @@ public:
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const = 0;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const = 0;
|
||||
|
||||
enum class FilenameType {
|
||||
OBJECTS = 0,
|
||||
ENEMIES,
|
||||
EVENTS,
|
||||
};
|
||||
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const = 0;
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const = 0;
|
||||
std::vector<std::string> map_filenames_for_variations(
|
||||
const parray<le_uint32_t, 0x20>& variations, Episode episode, GameMode mode, bool is_enemies) const;
|
||||
const parray<le_uint32_t, 0x20>& variations, Episode episode, GameMode mode, FilenameType type) const;
|
||||
|
||||
uint8_t default_area_for_floor(Episode episode, uint8_t floor) const;
|
||||
|
||||
@@ -357,8 +380,8 @@ class SetDataTable : public SetDataTableBase {
|
||||
public:
|
||||
struct SetEntry {
|
||||
std::string object_list_basename;
|
||||
std::string enemy_list_basename;
|
||||
std::string event_list_basename;
|
||||
std::string enemy_and_event_list_basename;
|
||||
std::string area_setup_filename;
|
||||
};
|
||||
|
||||
SetDataTable(Version version, const std::string& data);
|
||||
@@ -367,7 +390,7 @@ public:
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const;
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const;
|
||||
|
||||
std::string str() const;
|
||||
|
||||
@@ -388,10 +411,10 @@ public:
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const;
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const;
|
||||
|
||||
private:
|
||||
static const std::array<std::vector<std::vector<std::string>>, 0x10> NAMES;
|
||||
static const std::array<std::vector<std::vector<std::string>>, 0x12> NAMES;
|
||||
};
|
||||
|
||||
class SetDataTableDC112000 : public SetDataTableBase {
|
||||
@@ -402,10 +425,10 @@ public:
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const;
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const;
|
||||
|
||||
private:
|
||||
static const std::array<std::vector<std::vector<std::string>>, 0x10> NAMES;
|
||||
static const std::array<std::vector<std::vector<std::string>>, 0x12> NAMES;
|
||||
};
|
||||
|
||||
void generate_variations_deprecated(
|
||||
|
||||
@@ -697,6 +697,21 @@ void PlayerBank::assign_ids(uint32_t base_id) {
|
||||
}
|
||||
}
|
||||
|
||||
QuestFlagsV1& QuestFlagsV1::operator=(const QuestFlags& other) {
|
||||
this->data[0] = other.data[0];
|
||||
this->data[1] = other.data[1];
|
||||
this->data[2] = other.data[2];
|
||||
return *this;
|
||||
}
|
||||
|
||||
QuestFlagsV1::operator QuestFlags() const {
|
||||
QuestFlags ret;
|
||||
ret.data[0] = this->data[0];
|
||||
ret.data[1] = this->data[1];
|
||||
ret.data[2] = this->data[2];
|
||||
return ret;
|
||||
}
|
||||
|
||||
BattleRules::BattleRules(const JSON& json) {
|
||||
static const JSON empty_list = JSON::list();
|
||||
|
||||
|
||||
@@ -558,6 +558,13 @@ struct QuestFlags {
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
struct QuestFlagsV1 {
|
||||
parray<QuestFlagsForDifficulty, 3> data;
|
||||
|
||||
QuestFlagsV1& operator=(const QuestFlags& other);
|
||||
operator QuestFlags() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct BattleRules {
|
||||
enum class TechDiskMode : uint8_t {
|
||||
ALLOW = 0,
|
||||
|
||||
+39
-10
@@ -365,15 +365,24 @@ static void on_1D(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
|
||||
c->game_join_command_queue.reset();
|
||||
}
|
||||
|
||||
if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE)) {
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
|
||||
if (!is_ep3(c->version())) {
|
||||
send_game_item_state(c);
|
||||
if (!is_ep3(c->version())) {
|
||||
if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE)) {
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
|
||||
send_game_item_state(c); // 6x6D
|
||||
}
|
||||
if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE)) {
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE);
|
||||
send_game_object_state(c); // 6x6C
|
||||
}
|
||||
if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE)) {
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE);
|
||||
send_game_enemy_state(c); // 6x6B
|
||||
send_game_set_state(c); // 6x6E
|
||||
}
|
||||
}
|
||||
if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE)) {
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
|
||||
send_game_flag_state(c);
|
||||
send_game_flag_state(c); // 6x6F
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2501,13 +2510,24 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
}
|
||||
if (game->is_game()) {
|
||||
c->config.set_flag(Client::Flag::LOADING);
|
||||
// If no one was in the game before, then there's no leader to send the
|
||||
// item state - send it to the joining player (who is now the leader)
|
||||
// If no one was in the game before, then there's no leader to send
|
||||
// the game state - send it to the joining player (who is now the
|
||||
// leader)
|
||||
if (game->count_clients() == 1) {
|
||||
// No one was in the game before, so the object and enemy state is lost;
|
||||
// regenerate it as if the game was just created
|
||||
game->load_maps();
|
||||
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
|
||||
// TODO: Eventually, we want to send the enemy and set states too,
|
||||
// but currently this doesn't work well. Instead, we reset their
|
||||
// flags so it's as if they were never defeated.
|
||||
// c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE);
|
||||
if (game->map) {
|
||||
for (auto& enemy : game->map->enemies) {
|
||||
enemy.game_flags = 0;
|
||||
enemy.total_damage = 0;
|
||||
enemy.state_flags = 0;
|
||||
}
|
||||
}
|
||||
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE);
|
||||
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -4301,6 +4321,15 @@ shared_ptr<Lobby> create_game_generic(
|
||||
game->map = make_shared<Map>(game->base_version, game->lobby_id, game->random_seed, game->opt_rand_crypt);
|
||||
}
|
||||
|
||||
// The game's quest flags are inherited from the creator, if known
|
||||
if (c->version() == Version::BB_V4) {
|
||||
game->quest_flag_values = make_unique<QuestFlags>(p->quest_flags);
|
||||
game->quest_flags_known = nullptr;
|
||||
} else {
|
||||
game->quest_flag_values = make_unique<QuestFlags>();
|
||||
game->quest_flags_known = make_unique<QuestFlags>();
|
||||
}
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
|
||||
+337
-121
@@ -328,19 +328,19 @@ static shared_ptr<Client> get_sync_target(shared_ptr<Client> sender_c, uint8_t c
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void on_forward_sync_joining_player_state(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
static void on_sync_joining_player_compressed_state(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
auto target = get_sync_target(c, command, flag, false);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t subcommand;
|
||||
uint8_t orig_subcommand_number;
|
||||
size_t decompressed_size;
|
||||
size_t compressed_size;
|
||||
const void* compressed_data;
|
||||
if (is_pre_v1(c->version())) {
|
||||
const auto& cmd = check_size_t<G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E>(data, size, 0xFFFF);
|
||||
subcommand = cmd.header.basic_header.subcommand;
|
||||
orig_subcommand_number = cmd.header.basic_header.subcommand;
|
||||
decompressed_size = cmd.decompressed_size;
|
||||
compressed_size = size - sizeof(cmd);
|
||||
compressed_data = reinterpret_cast<const char*>(data) + sizeof(cmd);
|
||||
@@ -349,117 +349,276 @@ static void on_forward_sync_joining_player_state(shared_ptr<Client> c, uint8_t c
|
||||
if (cmd.compressed_size > size - sizeof(cmd)) {
|
||||
throw runtime_error("compressed end offset is beyond end of command");
|
||||
}
|
||||
subcommand = cmd.header.basic_header.subcommand;
|
||||
orig_subcommand_number = cmd.header.basic_header.subcommand;
|
||||
decompressed_size = cmd.decompressed_size;
|
||||
compressed_size = cmd.compressed_size;
|
||||
compressed_data = reinterpret_cast<const char*>(data) + sizeof(cmd);
|
||||
}
|
||||
|
||||
const auto* subcommand_def = def_for_subcommand(c->version(), orig_subcommand_number);
|
||||
if (!subcommand_def) {
|
||||
throw runtime_error("unknown sync subcommand");
|
||||
}
|
||||
|
||||
string decompressed = bc0_decompress(compressed_data, compressed_size);
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
string decompressed = bc0_decompress(compressed_data, compressed_size);
|
||||
c->log.info("Decompressed sync data (%zX -> %zX bytes; expected %zX):",
|
||||
compressed_size, decompressed.size(), decompressed_size);
|
||||
print_data(stderr, decompressed);
|
||||
}
|
||||
|
||||
if (is_pre_v1(c->version()) == is_pre_v1(target->version())) {
|
||||
on_forward_check_game_loading(c, command, flag, data, size);
|
||||
switch (subcommand_def->final_subcommand) {
|
||||
case 0x6B: {
|
||||
auto l = c->require_lobby();
|
||||
if (l->map) {
|
||||
l->log.info("Checking client enemy state against server state");
|
||||
StringReader r(decompressed);
|
||||
size_t count = r.size() / sizeof(G_SyncEnemyState_6x6B_Entry_Decompressed);
|
||||
if (count != l->map->enemies.size()) {
|
||||
l->log.warning("Enemy count from client (%zu) does not match enemy count from map (%zu)",
|
||||
count, l->map->enemies.size());
|
||||
} else {
|
||||
l->log.info("Enemy count from client matches enemy count from map (%zu)", l->map->enemies.size());
|
||||
}
|
||||
|
||||
} else if (is_pre_v1(target->version())) {
|
||||
StringWriter w;
|
||||
uint32_t cmd_size = ((compressed_size + sizeof(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E)) + 3) & (~3);
|
||||
w.put(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E{
|
||||
.header = {{subcommand, 0x00, 0x0000}, cmd_size},
|
||||
.decompressed_size = decompressed_size,
|
||||
});
|
||||
w.write(compressed_data, compressed_size);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
const string& data_to_send = w.str();
|
||||
forward_subcommand(c, command, flag, data_to_send.data(), data_to_send.size());
|
||||
// TODO: We should UPDATE our view of the flags here, not just check them
|
||||
for (size_t z = 0; z < min<size_t>(count, l->map->enemies.size()); z++) {
|
||||
const auto& entry = r.get<G_SyncEnemyState_6x6B_Entry_Decompressed>();
|
||||
if (l->map->enemies[z].game_flags != entry.flags) {
|
||||
l->log.warning("(E-%zX) Flags from client (%08" PRIX32 ") do not match game flags from map (%08" PRIX32 ")",
|
||||
z, entry.flags.load(), l->map->enemies[z].game_flags);
|
||||
}
|
||||
if (l->map->enemies[z].total_damage != entry.total_damage) {
|
||||
l->log.warning("(E-%zX) Total damage from client (%hu) does not match total damage from map (%hu)",
|
||||
z, entry.total_damage.load(), l->map->enemies[z].total_damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
StringWriter w;
|
||||
uint32_t cmd_size = ((compressed_size + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E)) + 3) & (~3);
|
||||
w.put(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E{
|
||||
.header = {{subcommand, 0x00, 0x0000}, cmd_size},
|
||||
.decompressed_size = decompressed_size,
|
||||
.compressed_size = compressed_size,
|
||||
});
|
||||
w.write(compressed_data, compressed_size);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
send_game_join_sync_command_compressed(
|
||||
target,
|
||||
compressed_data,
|
||||
compressed_size,
|
||||
decompressed_size,
|
||||
subcommand_def->nte_subcommand,
|
||||
subcommand_def->proto_subcommand,
|
||||
subcommand_def->final_subcommand);
|
||||
break;
|
||||
}
|
||||
const string& data_to_send = w.str();
|
||||
forward_subcommand(c, command, flag, data_to_send.data(), data_to_send.size());
|
||||
|
||||
case 0x6C: {
|
||||
auto l = c->require_lobby();
|
||||
if (l->map) {
|
||||
l->log.info("Checking client object state against server state");
|
||||
StringReader r(decompressed);
|
||||
size_t count = r.size() / sizeof(G_SyncObjectState_6x6C_Entry_Decompressed);
|
||||
if (count > l->map->objects.size()) {
|
||||
l->log.warning("Object count from client (%zu) exceeds object count from map (%zu)",
|
||||
count, l->map->objects.size());
|
||||
} else if (count < l->map->objects.size()) {
|
||||
// This is normal because we load objects for inaccessible maps (e.g. lobby)
|
||||
l->log.info("Object count from client (%zu) is less than object count from map (%zu) (this is normal)",
|
||||
count, l->map->objects.size());
|
||||
} else {
|
||||
l->log.info("Object count from client matches object count from map (%zu)", l->map->objects.size());
|
||||
}
|
||||
|
||||
// TODO: We should UPDATE our view of the flags here, not just check them
|
||||
for (size_t z = 0; z < min<size_t>(count, l->map->enemies.size()); z++) {
|
||||
const auto& entry = r.get<G_SyncObjectState_6x6C_Entry_Decompressed>();
|
||||
if (l->map->objects[z].game_flags != entry.flags) {
|
||||
l->log.warning("(K-%zX) Flags from client (%04hX) do not match game flags from map (%04hX)",
|
||||
z, entry.flags.load(), l->map->objects[z].game_flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_game_join_sync_command_compressed(
|
||||
target,
|
||||
compressed_data,
|
||||
compressed_size,
|
||||
decompressed_size,
|
||||
subcommand_def->nte_subcommand,
|
||||
subcommand_def->proto_subcommand,
|
||||
subcommand_def->final_subcommand);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x6D: {
|
||||
if (decompressed.size() < sizeof(G_SyncItemState_6x6D_Decompressed)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed 6x6D data (0x%zX bytes) is too short for header (0x%zX bytes)",
|
||||
decompressed.size(), sizeof(G_SyncItemState_6x6D_Decompressed)));
|
||||
}
|
||||
auto* decompressed_cmd = reinterpret_cast<G_SyncItemState_6x6D_Decompressed*>(decompressed.data());
|
||||
|
||||
size_t num_floor_items = 0;
|
||||
for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_floor.size(); z++) {
|
||||
num_floor_items += decompressed_cmd->floor_item_count_per_floor[z];
|
||||
}
|
||||
|
||||
size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(FloorItem);
|
||||
if (decompressed.size() < required_size) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed 6x6D data (0x%zX bytes) is too short for all floor items (0x%zX bytes)",
|
||||
decompressed.size(), required_size));
|
||||
}
|
||||
|
||||
auto l = c->require_lobby();
|
||||
size_t target_num_items = target->character()->inventory.num_items;
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
uint32_t client_next_id = decompressed_cmd->next_item_id_per_player[z];
|
||||
uint32_t server_next_id = l->next_item_id_for_client[z];
|
||||
if (client_next_id == server_next_id) {
|
||||
l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value", z, l->next_item_id_for_client[z]);
|
||||
} else if ((z == target->lobby_client_id) && (client_next_id == server_next_id - target_num_items)) {
|
||||
l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value before inventory item ID assignment (%08" PRIX32 ")", z, l->next_item_id_for_client[z], static_cast<uint32_t>(server_next_id - target_num_items));
|
||||
} else {
|
||||
l->log.warning("Next item ID for player %zu (%08" PRIX32 ") does not match expected value (%08" PRIX32 ")",
|
||||
z, decompressed_cmd->next_item_id_per_player[z].load(), l->next_item_id_for_client[z]);
|
||||
}
|
||||
}
|
||||
|
||||
// The leader's item state is never forwarded since the leader may be able
|
||||
// to see items that the joining player should not see. We always generate
|
||||
// a new item state for the joining player instead.
|
||||
send_game_item_state(target);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x6E: {
|
||||
StringReader r(decompressed);
|
||||
const auto& dec_header = r.get<G_SyncSetFlagState_6x6E_Decompressed>();
|
||||
if (dec_header.total_size != dec_header.entity_set_flags_size + dec_header.event_set_flags_size + dec_header.unused_size) {
|
||||
throw runtime_error("incorrect size fields in 6x6E header");
|
||||
}
|
||||
|
||||
auto l = c->require_lobby();
|
||||
if (l->map) {
|
||||
l->log.info("Checking client set flag state against server state");
|
||||
|
||||
StringReader set_flags_r = r.sub(r.where(), dec_header.entity_set_flags_size);
|
||||
const auto& set_flags_header = set_flags_r.get<G_SyncSetFlagState_6x6E_Decompressed::EntitySetFlags>();
|
||||
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
c->log.info("Set flags data:");
|
||||
print_data(stderr, set_flags_r.all());
|
||||
}
|
||||
|
||||
if (set_flags_header.num_object_sets > l->map->objects.size()) {
|
||||
l->log.warning("Object set count from client (%hu) exceeds object count from map (%zu)",
|
||||
set_flags_header.num_object_sets.load(), l->map->objects.size());
|
||||
} else if (set_flags_header.num_object_sets < l->map->objects.size()) {
|
||||
// This is normal because we load objects for inaccessible maps (e.g. lobby)
|
||||
l->log.info("Object set count from client (%hu) is less than object count from map (%zu) (this is normal)",
|
||||
set_flags_header.num_object_sets.load(), l->map->objects.size());
|
||||
} else {
|
||||
l->log.info("Object set count from client matches object count from map (%zu)", l->map->objects.size());
|
||||
}
|
||||
for (size_t z = 0; z < min<size_t>(set_flags_header.num_object_sets, l->map->objects.size()); z++) {
|
||||
uint16_t flags = set_flags_r.get_u16l();
|
||||
if (flags != l->map->objects[z].set_flags) {
|
||||
l->log.warning("(K-%zX) Set flags from client (%04hX) do not match flags from map (%04hX)",
|
||||
z, flags, l->map->objects[z].set_flags);
|
||||
}
|
||||
}
|
||||
|
||||
set_flags_r.go(sizeof(set_flags_header) + set_flags_header.num_object_sets * sizeof(le_uint16_t));
|
||||
if (set_flags_header.num_enemy_sets != l->map->enemy_set_flags.size()) {
|
||||
l->log.warning("Enemy set count from client (%hu) does not match count from map (%zu)",
|
||||
set_flags_header.num_enemy_sets.load(), l->map->enemy_set_flags.size());
|
||||
} else {
|
||||
l->log.info("Enemy set count from client matches count from map (%zu)", l->map->enemy_set_flags.size());
|
||||
}
|
||||
for (size_t z = 0; z < min<size_t>(set_flags_header.num_enemy_sets, l->map->enemy_set_flags.size()); z++) {
|
||||
uint16_t flags = set_flags_r.get_u16l();
|
||||
if (flags != l->map->enemy_set_flags[z]) {
|
||||
l->log.warning("(S-%zX) Set flags from client (%04hX) do not match flags from map (%04hX)",
|
||||
z, flags, l->map->enemy_set_flags[z]);
|
||||
}
|
||||
}
|
||||
|
||||
StringReader event_set_flags_r = r.sub(r.where() + dec_header.entity_set_flags_size, dec_header.event_set_flags_size);
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
c->log.info("Event flags data:");
|
||||
print_data(stderr, event_set_flags_r.all());
|
||||
}
|
||||
size_t num_event_flags = event_set_flags_r.size() / sizeof(le_uint16_t);
|
||||
if (num_event_flags != l->map->events.size()) {
|
||||
l->log.warning("Event count from client (%zu) does not match count from map (%zu)",
|
||||
num_event_flags, l->map->events.size());
|
||||
} else {
|
||||
l->log.info("Event count from client matches count from map (%zu)", l->map->events.size());
|
||||
}
|
||||
for (size_t z = 0; z < min<size_t>(num_event_flags, l->map->events.size()); z++) {
|
||||
uint16_t flags = event_set_flags_r.get_u16l();
|
||||
const auto& event = l->map->events[z];
|
||||
if (flags != event.flags) {
|
||||
l->log.warning("(W-%02hhX-%" PRIX32 ") Event flags from client (%04hX) do not match flags from map (%04hX)",
|
||||
event.floor, event.event_id, flags, event.flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t expected_unused_size = is_v1(c->version()) ? 0x200 : 0x240;
|
||||
size_t target_unused_size = is_v1(target->version()) ? 0x200 : 0x240;
|
||||
if (dec_header.unused_size != expected_unused_size) {
|
||||
l->log.warning("Unused data size (0x%" PRIX32 ") does not match expected size (0x%zX)",
|
||||
dec_header.unused_size.load(), expected_unused_size);
|
||||
}
|
||||
if (dec_header.unused_size != target_unused_size) {
|
||||
l->log.info("Resizing unused data from 0x%" PRIX32 " bytes to 0x%zX bytes",
|
||||
dec_header.unused_size.load(), target_unused_size);
|
||||
if (dec_header.unused_size >= decompressed.size()) {
|
||||
throw runtime_error("unused size is too large");
|
||||
}
|
||||
decompressed.resize(decompressed.size() - dec_header.unused_size.load() + target_unused_size, '\0');
|
||||
auto* wdec_header = reinterpret_cast<G_SyncSetFlagState_6x6E_Decompressed*>(decompressed.data());
|
||||
wdec_header->unused_size = target_unused_size;
|
||||
wdec_header->total_size = wdec_header->entity_set_flags_size + wdec_header->event_set_flags_size + wdec_header->unused_size;
|
||||
}
|
||||
|
||||
send_game_join_sync_command_compressed(
|
||||
target,
|
||||
compressed_data,
|
||||
compressed_size,
|
||||
decompressed_size,
|
||||
subcommand_def->nte_subcommand,
|
||||
subcommand_def->proto_subcommand,
|
||||
subcommand_def->final_subcommand);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw logic_error("invalid compressed sync state subcommand");
|
||||
}
|
||||
}
|
||||
|
||||
static void on_sync_joining_player_item_state(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
auto target = get_sync_target(c, command, flag, false);
|
||||
if (!target) {
|
||||
template <typename CmdT>
|
||||
static void on_sync_joining_player_quest_flags_t(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
const auto& cmd = check_size_t<CmdT>(data, size);
|
||||
|
||||
if (!command_is_private(command)) {
|
||||
return;
|
||||
}
|
||||
const auto& l = c->require_lobby();
|
||||
|
||||
string decompressed;
|
||||
size_t compressed_size;
|
||||
size_t decompressed_size;
|
||||
if (is_pre_v1(c->version())) {
|
||||
const auto& cmd = check_size_t<G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E>(data, size, 0xFFFF);
|
||||
compressed_size = size - sizeof(cmd);
|
||||
decompressed_size = cmd.decompressed_size;
|
||||
decompressed = bc0_decompress(reinterpret_cast<const char*>(data) + sizeof(cmd), compressed_size);
|
||||
auto l = c->require_lobby();
|
||||
if (l->is_game() && l->any_client_loading() && (l->leader_id == c->lobby_client_id)) {
|
||||
l->quest_flags_known = nullptr; // All quest flags are now known
|
||||
l->quest_flag_values = make_unique<QuestFlags>(cmd.quest_flags);
|
||||
auto target = l->clients.at(flag);
|
||||
if (target) {
|
||||
send_game_flag_state(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void on_sync_joining_player_quest_flags(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
if (is_v1(c->version())) {
|
||||
on_sync_joining_player_quest_flags_t<G_SetQuestFlagsV1_6x6F>(c, command, flag, data, size);
|
||||
} else {
|
||||
const auto& cmd = check_size_t<G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E>(data, size, 0xFFFF);
|
||||
compressed_size = cmd.compressed_size;
|
||||
decompressed_size = cmd.decompressed_size;
|
||||
if (compressed_size > size - sizeof(cmd)) {
|
||||
throw runtime_error("compressed end offset is beyond end of command");
|
||||
}
|
||||
decompressed = bc0_decompress(reinterpret_cast<const char*>(data) + sizeof(cmd), cmd.compressed_size);
|
||||
on_sync_joining_player_quest_flags_t<G_SetQuestFlagsV2V3V4_6x6F>(c, command, flag, data, size);
|
||||
}
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
c->log.info("Decompressed item sync data (%zX -> %zX bytes; expected %zX):",
|
||||
compressed_size, decompressed.size(), decompressed_size);
|
||||
print_data(stderr, decompressed);
|
||||
}
|
||||
|
||||
if (decompressed.size() < sizeof(G_SyncItemState_6x6D_Decompressed)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed 6x6D data (0x%zX bytes) is too short for header (0x%zX bytes)",
|
||||
decompressed.size(), sizeof(G_SyncItemState_6x6D_Decompressed)));
|
||||
}
|
||||
auto* decompressed_cmd = reinterpret_cast<G_SyncItemState_6x6D_Decompressed*>(decompressed.data());
|
||||
|
||||
size_t num_floor_items = 0;
|
||||
for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_floor.size(); z++) {
|
||||
num_floor_items += decompressed_cmd->floor_item_count_per_floor[z];
|
||||
}
|
||||
|
||||
size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(FloorItem);
|
||||
if (decompressed.size() < required_size) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed 6x6D data (0x%zX bytes) is too short for all floor items (0x%zX bytes)",
|
||||
decompressed.size(), required_size));
|
||||
}
|
||||
|
||||
size_t target_num_items = target->character()->inventory.num_items;
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
uint32_t client_next_id = decompressed_cmd->next_item_id_per_player[z];
|
||||
uint32_t server_next_id = l->next_item_id_for_client[z];
|
||||
if (client_next_id == server_next_id) {
|
||||
l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value", z, l->next_item_id_for_client[z]);
|
||||
} else if ((z == target->lobby_client_id) && (client_next_id == server_next_id - target_num_items)) {
|
||||
l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value before inventory item ID assignment (%08" PRIX32 ")", z, l->next_item_id_for_client[z], static_cast<uint32_t>(server_next_id - target_num_items));
|
||||
} else {
|
||||
l->log.warning("Next item ID for player %zu (%08" PRIX32 ") does not match expected value (%08" PRIX32 ")",
|
||||
z, decompressed_cmd->next_item_id_per_player[z].load(), l->next_item_id_for_client[z]);
|
||||
}
|
||||
}
|
||||
|
||||
send_game_item_state(target);
|
||||
}
|
||||
|
||||
class Parsed6x70Data {
|
||||
@@ -2404,39 +2563,50 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t flag_index, difficulty, action;
|
||||
uint16_t flag_num, difficulty, action;
|
||||
if (is_v1_or_v2(c->version()) && (c->version() != Version::GC_NTE)) {
|
||||
const auto& cmd = check_size_t<G_UpdateQuestFlag_DC_PC_6x75>(data, size);
|
||||
flag_index = cmd.flag;
|
||||
flag_num = cmd.flag;
|
||||
action = cmd.action;
|
||||
difficulty = l->difficulty;
|
||||
} else {
|
||||
const auto& cmd = check_size_t<G_UpdateQuestFlag_V3_BB_6x75>(data, size);
|
||||
flag_index = cmd.flag;
|
||||
flag_num = cmd.flag;
|
||||
action = cmd.action;
|
||||
difficulty = cmd.difficulty;
|
||||
}
|
||||
|
||||
if ((flag_index >= 0x400) || (difficulty > 3) || (action > 1)) {
|
||||
// The client explicitly checks action for both 0 and 1 - any other value
|
||||
// means no operation is performed.
|
||||
if ((flag_num >= 0x400) || (difficulty > 3) || (action > 1)) {
|
||||
return;
|
||||
}
|
||||
bool should_set = (action == 0);
|
||||
|
||||
// TODO: Should we allow overlays here?
|
||||
auto p = c->character(true, false);
|
||||
|
||||
auto s = c->require_server_state();
|
||||
if (s->quest_flag_persist_mask.get(flag_index)) {
|
||||
// The client explicitly checks for both 0 and 1 - any other value means no
|
||||
// operation is performed.
|
||||
if (action == 0) {
|
||||
c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_index);
|
||||
p->quest_flags.set(difficulty, flag_index);
|
||||
} else if (action == 1) {
|
||||
c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_index);
|
||||
p->quest_flags.clear(difficulty, flag_index);
|
||||
}
|
||||
if (l->quest_flags_known) {
|
||||
l->quest_flags_known->set(difficulty, flag_num);
|
||||
}
|
||||
if (should_set) {
|
||||
l->quest_flag_values->set(difficulty, flag_num);
|
||||
} else {
|
||||
c->log.info("Quest flag %s:%03hX cannot be modified", name_for_difficulty(difficulty), flag_index);
|
||||
l->quest_flag_values->clear(difficulty, flag_num);
|
||||
}
|
||||
|
||||
if (c->version() == Version::BB_V4) {
|
||||
auto s = c->require_server_state();
|
||||
// TODO: Should we allow overlays here?
|
||||
auto p = c->character(true, false);
|
||||
if (s->quest_flag_persist_mask.get(flag_num)) {
|
||||
if (should_set) {
|
||||
c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num);
|
||||
p->quest_flags.set(difficulty, flag_num);
|
||||
} else {
|
||||
c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num);
|
||||
p->quest_flags.clear(difficulty, flag_num);
|
||||
}
|
||||
} else {
|
||||
c->log.info("Quest flag %s:%03hX cannot be modified", name_for_difficulty(difficulty), flag_num);
|
||||
}
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
@@ -2448,12 +2618,12 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
|
||||
// On Normal, Dark Falz does not have a third phase, so send the drop
|
||||
// request after the end of the second phase. On all other difficulty
|
||||
// levels, send it after the third phase.
|
||||
if ((difficulty == 0) && (flag_index == 0x0035)) {
|
||||
if ((difficulty == 0) && (flag_num == 0x0035)) {
|
||||
boss_enemy_type = EnemyType::DARK_FALZ_2;
|
||||
} else if ((difficulty != 0) && (flag_index == 0x0037)) {
|
||||
} else if ((difficulty != 0) && (flag_num == 0x0037)) {
|
||||
boss_enemy_type = EnemyType::DARK_FALZ_3;
|
||||
}
|
||||
} else if (is_ep2 && (flag_index == 0x0057) && (c->floor == 0x0D)) {
|
||||
} else if (is_ep2 && (flag_num == 0x0057) && (c->floor == 0x0D)) {
|
||||
boss_enemy_type = EnemyType::OLGA_FLOW_2;
|
||||
}
|
||||
|
||||
@@ -2494,6 +2664,52 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
|
||||
}
|
||||
}
|
||||
|
||||
static void on_set_entity_flag(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& cmd = check_size_t<G_SetEntityFlags_6x76>(data, size);
|
||||
if (l->map) {
|
||||
if (cmd.header.enemy_id >= 0x1000 && cmd.header.enemy_id < 0x4000) {
|
||||
try {
|
||||
l->map->enemies.at(cmd.header.enemy_id - 0x1000).game_flags |= cmd.flags;
|
||||
} catch (const out_of_range&) {
|
||||
l->log.warning("Flag update refers to missing enemy");
|
||||
}
|
||||
} else if (cmd.header.enemy_id >= 0x4000) {
|
||||
try {
|
||||
l->map->objects.at(cmd.header.enemy_id - 0x4000).game_flags |= cmd.flags;
|
||||
} catch (const out_of_range&) {
|
||||
l->log.warning("Flag update refers to missing object");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
|
||||
static void on_trigger_set_event(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& cmd = check_size_t<G_TriggerSetEvent_6x67>(data, size);
|
||||
if (l->map) {
|
||||
// TODO: The game's logic is significantly more complex than this. Do we
|
||||
// need to do anything fancy here?
|
||||
try {
|
||||
l->map->get_event(cmd.floor, cmd.event_id).flags |= 0x04;
|
||||
} catch (const out_of_range&) {
|
||||
l->log.warning("Client triggered missing event W-%02" PRIX32 "-%" PRIX32, cmd.floor.load(), cmd.event_id.load());
|
||||
}
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
|
||||
static void on_battle_scores(shared_ptr<Client> c, uint8_t command, uint8_t, void* data, size_t size) {
|
||||
const auto& cmd = check_size_t<G_BattleScores_6x7F<false>>(data, size);
|
||||
|
||||
@@ -3935,22 +4151,22 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x64 */ {0x56, 0x5D, 0x64, on_forward_check_game},
|
||||
/* 6x65 */ {0x57, 0x5E, 0x65, on_forward_check_game},
|
||||
/* 6x66 */ {0x00, 0x00, 0x66, on_forward_check_game},
|
||||
/* 6x67 */ {0x58, 0x5F, 0x67, on_forward_check_game},
|
||||
/* 6x67 */ {0x58, 0x5F, 0x67, on_trigger_set_event},
|
||||
/* 6x68 */ {0x59, 0x60, 0x68, on_forward_check_game},
|
||||
/* 6x69 */ {0x5A, 0x61, 0x69, on_npc_control},
|
||||
/* 6x6A */ {0x5B, 0x62, 0x6A, on_forward_check_game},
|
||||
/* 6x6B */ {0x5C, 0x63, 0x6B, on_forward_sync_joining_player_state, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6C */ {0x5D, 0x64, 0x6C, on_forward_sync_joining_player_state, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6D */ {0x5E, 0x65, 0x6D, on_sync_joining_player_item_state, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6E */ {0x5F, 0x66, 0x6E, on_forward_sync_joining_player_state, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6F */ {0x00, 0x00, 0x6F, on_forward_check_game_loading, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6B */ {0x5C, 0x63, 0x6B, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6C */ {0x5D, 0x64, 0x6C, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6D */ {0x5E, 0x65, 0x6D, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6E */ {0x5F, 0x66, 0x6E, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x6F */ {0x00, 0x00, 0x6F, on_sync_joining_player_quest_flags, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x70 */ {0x60, 0x67, 0x70, on_sync_joining_player_disp_and_inventory, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x71 */ {0x00, 0x00, 0x71, on_forward_check_game_loading, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x72 */ {0x61, 0x68, 0x72, on_forward_check_game_loading, SDF::USE_JOIN_COMMAND_QUEUE},
|
||||
/* 6x73 */ {0x00, 0x00, 0x73, on_forward_check_game_quest},
|
||||
/* 6x74 */ {0x62, 0x69, 0x74, on_word_select, SDF::ALWAYS_FORWARD_TO_WATCHERS},
|
||||
/* 6x75 */ {0x00, 0x00, 0x75, on_set_quest_flag},
|
||||
/* 6x76 */ {0x00, 0x00, 0x76, on_forward_check_game},
|
||||
/* 6x76 */ {0x00, 0x00, 0x76, on_set_entity_flag},
|
||||
/* 6x77 */ {0x00, 0x00, 0x77, on_forward_check_game},
|
||||
/* 6x78 */ {0x00, 0x00, 0x78, forward_subcommand_m},
|
||||
/* 6x79 */ {0x00, 0x00, 0x79, on_forward_check_lobby},
|
||||
|
||||
+125
-23
@@ -2396,30 +2396,44 @@ void send_ep3_change_music(Channel& ch, uint32_t song) {
|
||||
ch.send(0x60, 0x00, cmd);
|
||||
}
|
||||
|
||||
static void send_game_join_sync_command(
|
||||
void send_game_join_sync_command(
|
||||
shared_ptr<Client> c, const void* data, size_t size, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc) {
|
||||
string compressed_data = bc0_compress(data, size);
|
||||
send_game_join_sync_command_compressed(c, compressed_data.data(), compressed_data.size(), size, dc_nte_sc, dc_11_2000_sc, sc);
|
||||
}
|
||||
|
||||
void send_game_join_sync_command(shared_ptr<Client> c, const string& data, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc) {
|
||||
send_game_join_sync_command(c, data.data(), data.size(), dc_nte_sc, dc_11_2000_sc, sc);
|
||||
}
|
||||
|
||||
void send_game_join_sync_command_compressed(
|
||||
shared_ptr<Client> c,
|
||||
const void* data,
|
||||
size_t size,
|
||||
size_t decompressed_size,
|
||||
uint8_t dc_nte_sc,
|
||||
uint8_t dc_11_2000_sc,
|
||||
uint8_t sc) {
|
||||
StringWriter w;
|
||||
if (is_pre_v1(c->version())) {
|
||||
G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E compressed_header;
|
||||
compressed_header.header.basic_header.subcommand = (c->version() == Version::DC_NTE) ? dc_nte_sc : dc_11_2000_sc;
|
||||
compressed_header.header.basic_header.size = 0x00;
|
||||
compressed_header.header.basic_header.unused = 0x0000;
|
||||
compressed_header.header.size = (compressed_data.size() + sizeof(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E) + 3) & (~3);
|
||||
compressed_header.decompressed_size = size;
|
||||
compressed_header.header.size = (size + sizeof(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E) + 3) & (~3);
|
||||
compressed_header.decompressed_size = decompressed_size;
|
||||
w.put(compressed_header);
|
||||
} else {
|
||||
G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E compressed_header;
|
||||
compressed_header.header.basic_header.subcommand = sc;
|
||||
compressed_header.header.basic_header.size = 0x00;
|
||||
compressed_header.header.basic_header.unused = 0x0000;
|
||||
compressed_header.header.size = (compressed_data.size() + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E) + 3) & (~3);
|
||||
compressed_header.decompressed_size = size;
|
||||
compressed_header.compressed_size = compressed_data.size();
|
||||
compressed_header.header.size = (size + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E) + 3) & (~3);
|
||||
compressed_header.decompressed_size = decompressed_size;
|
||||
compressed_header.compressed_size = size;
|
||||
w.put(compressed_header);
|
||||
}
|
||||
w.write(compressed_data);
|
||||
w.write(data, size);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0x00);
|
||||
}
|
||||
@@ -2525,31 +2539,119 @@ void send_game_object_state(shared_ptr<Client> c) {
|
||||
send_game_join_sync_command(c, entries.data(), entries.size() * sizeof(entries[0]), 0x5D, 0x64, 0x6C);
|
||||
}
|
||||
|
||||
void send_game_flag_state(shared_ptr<Client> c) {
|
||||
void send_game_set_state(shared_ptr<Client> c) {
|
||||
auto l = c->require_lobby();
|
||||
if (!l->map) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t num_object_sets = l->map->objects.size();
|
||||
size_t num_enemy_sets = l->map->enemy_set_flags.size();
|
||||
|
||||
G_SyncSetFlagState_6x6E_Decompressed::EntitySetFlags entity_set_flags_header;
|
||||
entity_set_flags_header.object_set_flags_offset = sizeof(entity_set_flags_header);
|
||||
entity_set_flags_header.num_object_sets = num_object_sets;
|
||||
entity_set_flags_header.enemy_set_flags_offset = sizeof(entity_set_flags_header) + num_object_sets * sizeof(le_uint16_t);
|
||||
entity_set_flags_header.num_enemy_sets = num_enemy_sets;
|
||||
|
||||
G_SyncSetFlagState_6x6E_Decompressed header;
|
||||
header.entity_set_flags_size = sizeof(entity_set_flags_header) + (num_object_sets + num_enemy_sets) * sizeof(le_uint16_t);
|
||||
header.event_set_flags_size = sizeof(le_uint16_t) * l->map->events.size();
|
||||
header.unused_size = is_v1(c->version()) ? 0x200 : 0x240;
|
||||
header.total_size = header.entity_set_flags_size + header.event_set_flags_size + header.unused_size;
|
||||
|
||||
StringWriter w;
|
||||
w.put(header);
|
||||
w.put(entity_set_flags_header);
|
||||
for (const auto& obj : l->map->objects) {
|
||||
w.put_u16l(obj.set_flags);
|
||||
}
|
||||
for (uint16_t enemy_set_flags : l->map->enemy_set_flags) {
|
||||
w.put_u16l(enemy_set_flags);
|
||||
}
|
||||
for (const auto& event : l->map->events) {
|
||||
w.put_u16l(event.flags);
|
||||
}
|
||||
w.extend_by(header.unused_size, 0x00);
|
||||
|
||||
send_game_join_sync_command(c, w.str(), 0x5F, 0x66, 0x6E);
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
void send_game_flag_state_t(shared_ptr<Client> c) {
|
||||
auto l = c->require_lobby();
|
||||
|
||||
G_SetQuestFlags_6x6F cmd;
|
||||
cmd.header.subcommand = 0x6F;
|
||||
cmd.header.size = sizeof(G_SetQuestFlags_6x6F) >> 2;
|
||||
cmd.header.unused = 0x0000;
|
||||
cmd.quest_flags = c->character()->quest_flags;
|
||||
|
||||
for (const auto& lc : l->clients) {
|
||||
if (!lc) {
|
||||
continue;
|
||||
if (l->quest_flags_known) { // Not all flags known; send multiple 6x75s
|
||||
StringWriter w;
|
||||
bool use_v3_cmd = !is_v1_or_v2(c->version()) || (c->version() == Version::GC_NTE);
|
||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
if ((difficulty != l->difficulty) && !use_v3_cmd) {
|
||||
continue;
|
||||
}
|
||||
const auto& diff_flags = l->quest_flag_values->data.at(difficulty);
|
||||
const auto& diff_known_flags = l->quest_flags_known->data.at(difficulty);
|
||||
for (uint8_t z = 0; z < diff_known_flags.data.size(); z++) {
|
||||
uint8_t known_flags = diff_known_flags.data[z];
|
||||
if (!known_flags) {
|
||||
continue;
|
||||
}
|
||||
uint8_t flag_values = diff_flags.data[z];
|
||||
for (uint8_t sh = 0; sh < 8; sh++) {
|
||||
if ((known_flags << sh) & 0x80) {
|
||||
uint16_t flag_num = ((z << 3) | sh);
|
||||
if (use_v3_cmd) {
|
||||
w.put(G_UpdateQuestFlag_V3_BB_6x75{
|
||||
{{0x75, 0x03, 0x0000}, flag_num, (((flag_values << sh) & 0x80) ? 0 : 1)}, difficulty, 0});
|
||||
} else {
|
||||
w.put(G_UpdateQuestFlag_DC_PC_6x75{
|
||||
{0x75, 0x02, 0x0000}, flag_num, (((flag_values << sh) & 0x80) ? 0 : 1)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lc->game_join_command_queue) {
|
||||
lc->log.info("Client not ready to receive join commands; adding to queue");
|
||||
auto& cmd = lc->game_join_command_queue->emplace_back();
|
||||
cmd.command = 0x0060;
|
||||
cmd.flag = 0x00000000;
|
||||
|
||||
if (w.size() > 0) {
|
||||
if (c->game_join_command_queue) {
|
||||
c->log.info("Client not ready to receive join commands; adding to queue");
|
||||
auto& cmd = c->game_join_command_queue->emplace_back();
|
||||
cmd.command = 0x006D;
|
||||
cmd.flag = c->lobby_client_id;
|
||||
cmd.data = std::move(w.str());
|
||||
} else {
|
||||
send_command(c, 0x6D, c->lobby_client_id, w.str());
|
||||
}
|
||||
}
|
||||
|
||||
} else { // All flags known; send 6x6F
|
||||
CmdT cmd;
|
||||
cmd.header.subcommand = 0x6F;
|
||||
cmd.header.size = sizeof(CmdT) >> 2;
|
||||
cmd.header.unused = 0x0000;
|
||||
cmd.quest_flags = (l && !l->quest_flags_known) ? *l->quest_flag_values : c->character()->quest_flags;
|
||||
|
||||
if (c->game_join_command_queue) {
|
||||
c->log.info("Client not ready to receive join commands; adding to queue");
|
||||
auto& cmd = c->game_join_command_queue->emplace_back();
|
||||
cmd.command = 0x0062;
|
||||
cmd.flag = c->lobby_client_id;
|
||||
cmd.data.assign(reinterpret_cast<const char*>(&cmd), sizeof(cmd));
|
||||
} else {
|
||||
send_command_t(lc, 0x60, 0x00, cmd);
|
||||
send_command_t(c, 0x62, c->lobby_client_id, cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void send_game_flag_state(shared_ptr<Client> c) {
|
||||
// DC NTE and 11/2000 don't have this command at all; v1 has it but it doesn't
|
||||
// include flags for Ultimate.
|
||||
if (!is_v1(c->version())) {
|
||||
send_game_flag_state_t<G_SetQuestFlagsV2V3V4_6x6F>(c);
|
||||
} else if (!is_pre_v1(c->version())) {
|
||||
send_game_flag_state_t<G_SetQuestFlagsV1_6x6F>(c);
|
||||
}
|
||||
}
|
||||
|
||||
void send_drop_item_to_channel(shared_ptr<ServerState> s, Channel& ch, const ItemData& item,
|
||||
bool from_enemy, uint8_t floor, float x, float z, uint16_t entity_id) {
|
||||
uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x51, 0x58, 0x5F);
|
||||
|
||||
@@ -309,9 +309,22 @@ void send_warp(std::shared_ptr<Lobby> l, uint32_t floor, bool is_private);
|
||||
void send_ep3_change_music(Channel& ch, uint32_t song);
|
||||
void send_revive_player(std::shared_ptr<Client> c);
|
||||
|
||||
void send_game_join_sync_command(
|
||||
std::shared_ptr<Client> c, const void* data, size_t size, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc);
|
||||
void send_game_join_sync_command(
|
||||
std::shared_ptr<Client> c, const std::string& data, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc);
|
||||
void send_game_join_sync_command_compressed(
|
||||
std::shared_ptr<Client> c,
|
||||
const void* data,
|
||||
size_t size,
|
||||
size_t decompressed_size,
|
||||
uint8_t dc_nte_sc,
|
||||
uint8_t dc_11_2000_sc,
|
||||
uint8_t sc);
|
||||
void send_game_item_state(std::shared_ptr<Client> c);
|
||||
void send_game_enemy_state(std::shared_ptr<Client> c);
|
||||
void send_game_object_state(std::shared_ptr<Client> c);
|
||||
void send_game_set_state(std::shared_ptr<Client> c);
|
||||
void send_game_flag_state(std::shared_ptr<Client> c);
|
||||
void send_drop_item_to_channel(std::shared_ptr<ServerState> s, Channel& ch, const ItemData& item,
|
||||
bool from_enemy, uint8_t floor, float x, float z, uint16_t request_id);
|
||||
|
||||
+6
-2
@@ -35,7 +35,12 @@ TextTranscoder::Result TextTranscoder::operator()(
|
||||
const void* orig_src = src;
|
||||
while (src_bytes > 0) {
|
||||
size_t src_bytes_before = src_bytes;
|
||||
size_t ret = iconv(this->ic, reinterpret_cast<char**>(const_cast<void**>(&src)), &src_bytes, reinterpret_cast<char**>(&dest), &dest_bytes);
|
||||
size_t ret = iconv(
|
||||
this->ic,
|
||||
reinterpret_cast<char**>(const_cast<void**>(&src)),
|
||||
&src_bytes,
|
||||
reinterpret_cast<char**>(&dest),
|
||||
&dest_bytes);
|
||||
|
||||
size_t bytes_read = reinterpret_cast<const char*>(src) - reinterpret_cast<const char*>(orig_src);
|
||||
if (ret == this->FAILURE_RESULT) {
|
||||
@@ -64,7 +69,6 @@ TextTranscoder::Result TextTranscoder::operator()(
|
||||
default:
|
||||
throw runtime_error("transcoding failed: " + string_for_error(errno));
|
||||
}
|
||||
|
||||
} else if (src_bytes_before == src_bytes) {
|
||||
throw runtime_error("could not transcode any characters");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user