add enemy, object, and event tracking for persistence
This commit is contained in:
@@ -431,8 +431,8 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* The rest of the commands in this section are enabled on the game server. (They are always enabled on the proxy server.)
|
||||
* `$quest <number>` (game server only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present.
|
||||
* `$qcall <function-id>`: Call a quest function on your client.
|
||||
* `$qcheck <flag-num>` (game server only): Show the value of a quest flag. This command can be used without debug mode enabled.
|
||||
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a quest flag for everyone in the game.
|
||||
* `$qcheck <flag-num>` (game server only): Show the value of a quest flag. This command can be used without debug mode enabled. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only).
|
||||
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a quest flag for everyone in the game. If you're in the lobby and on BB, set or clear the saved value of a quest flag in your character file.
|
||||
* `$qgread <flag-num>` (game server only): Get the value of a quest counter ("global flag"). This command can be used without debug mode enabled.
|
||||
* `$qgwrite <flag-num> <value>` (game server only): Set the value of a quest counter ("global flag") for yourself.
|
||||
* `$qsync <reg-num> <value>`: Set a quest register's value for yourself only. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
@@ -468,7 +468,7 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$minlevel <level>`: Sets the minimum level for players to join the current game.
|
||||
* `$password <password>`: Sets the game's join password. To unlock the game, run `$password` with nothing after it.
|
||||
* `$dropmode [mode]`: Changes the way item drops behave in the current game. `mode` can be `none`, `client`, `shared`, `private`, or `duplicate`. If `mode` is not given, tells you the current drop mode without changing it. See the "Item tables and drop modes" section for more information.
|
||||
* `$persist`: Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The state of enemies and objects on the map will be reset when the last player leaves, but dropped items will not be deleted. If the game is empty for too long (15 minutes by default), it is then deleted.
|
||||
* `$persist`: Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The state of enemies on the map will be reset when the last player leaves, but dropped items will not be deleted. If the game is empty for too long (15 minutes by default), it is then deleted.
|
||||
|
||||
* Episode 3 commands (game server only)
|
||||
* `$spec`: Toggles the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they will be sent back to the lobby.
|
||||
|
||||
+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");
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3185,7 +3185,7 @@ I 94381 2023-12-29 15:36:50 - [Commands] Received from C-2 (Jess) (version=GC_V3
|
||||
01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0200 | 00 00 00 00 00 00 00 00 |
|
||||
I 94381 2023-12-29 15:36:50 - [Commands] Sending to C-5 (Jonah) (version=GC_V3 command=62 flag=01)
|
||||
0000 | 62 01 08 02 6F 81 88 98 00 12 CB C0 00 08 00 00 | b o
|
||||
0000 | 62 01 08 02 6F 81 00 00 00 12 CB C0 00 08 00 00 | b o
|
||||
0010 | 00 20 00 10 0A 20 00 00 00 00 00 00 00 00 00 00 |
|
||||
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
@@ -12489,7 +12489,7 @@ I 94381 2023-12-29 15:42:19 - [Commands] Received from C-2 (Jess) (version=GC_V3
|
||||
01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0200 | 00 00 00 00 00 00 00 00 |
|
||||
I 94381 2023-12-29 15:42:19 - [Commands] Sending to C-5 (Jonah) (version=GC_V3 command=62 flag=01)
|
||||
0000 | 62 01 08 02 6F 81 88 98 00 12 CB C0 00 08 00 00 | b o
|
||||
0000 | 62 01 08 02 6F 81 00 00 00 12 CB C0 00 08 00 00 | b o
|
||||
0010 | 00 30 00 10 0A 20 00 00 00 00 00 00 00 00 00 00 | 0
|
||||
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
|
||||
@@ -614,6 +614,11 @@ I 49108 2023-05-26 16:18:25 - [Commands] Received from C-2 (Jess) (version=GC co
|
||||
0010 | 00 00 00 00 |
|
||||
I 49108 2023-05-26 16:18:25 - [Commands] Received from C-2 (Jess) (version=GC command=60 flag=00)
|
||||
0000 | 60 00 10 00 52 03 00 00 00 00 00 00 00 80 00 00 | ` R
|
||||
I 49108 2023-05-26 16:18:25 - [Commands] Received from C-2 (Jess) (version=GC command=60 flag=00)
|
||||
0000 | 06 00 20 00 00 00 00 00 00 00 00 00 09 45 24 76 | E$v
|
||||
0010 | 61 72 69 61 74 69 6F 6E 73 20 30 30 30 31 30 34 | ariations 000104
|
||||
0020 | 32 31 30 31 32 30 32 31 31 30 32 30 32 31 32 31 | 2101202110202121
|
||||
0030 | 30 30 30 30 30 30 30 30 30 30 00 00 | 0000000000
|
||||
I 49108 2023-05-26 16:18:32 - [Commands] Received from C-2 (Jess) (version=GC command=C1 flag=03)
|
||||
0000 | C1 03 30 00 00 00 00 00 00 00 00 00 09 45 31 31 | 0 E11
|
||||
0010 | 31 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 11
|
||||
|
||||
@@ -25658,7 +25658,7 @@ I 56327 2024-03-03 23:56:43 - [Commands] Received from C-2 (Jess) (version=GC_V3
|
||||
01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0200 | 00 00 00 00 00 00 00 00 |
|
||||
I 56327 2024-03-03 23:56:43 - [Commands] Sending to C-5 (NO DATA ) (version=GC_V3 command=62 flag=01)
|
||||
0000 | 62 01 08 02 6F 81 88 98 00 12 CB C0 00 08 00 00 | b o
|
||||
0000 | 62 01 08 02 6F 81 00 00 00 12 CB C0 00 08 00 00 | b o
|
||||
0010 | 00 32 00 10 0A 20 00 00 00 00 00 00 00 00 00 00 | 2
|
||||
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
|
||||
@@ -2822,7 +2822,7 @@ I 97037 2023-12-29 15:57:06 - [Commands] Received from C-3 (Tali) (version=PC_V2
|
||||
01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0200 | 00 00 00 00 00 00 00 00 |
|
||||
I 97037 2023-12-29 15:57:06 - [Commands] Sending to C-5 (88888888) (version=DC_V1 command=62 flag=01)
|
||||
0000 | 62 01 08 02 6F 81 00 00 00 00 00 00 00 00 00 00 | b o
|
||||
0000 | 62 01 88 01 6F 61 00 00 00 00 00 00 00 00 00 00 | b o
|
||||
0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
@@ -2846,15 +2846,7 @@ I 97037 2023-12-29 15:57:06 - [Commands] Sending to C-5 (88888888) (version=DC_V
|
||||
0150 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0200 | 00 00 00 00 00 00 00 00 |
|
||||
0180 | 00 00 00 00 00 00 00 00 |
|
||||
I 97037 2023-12-29 15:57:06 - [Commands] Received from C-3 (Tali) (version=PC_V2 command=62 flag=01)
|
||||
0000 | 08 00 62 01 71 01 00 00 | b q
|
||||
I 97037 2023-12-29 15:57:06 - [Commands] Sending to C-5 (88888888) (version=DC_V1 command=62 flag=01)
|
||||
@@ -7036,7 +7028,7 @@ I 97037 2023-12-29 15:59:36 - [Commands] Sending to C-3 (Tali) (version=PC_V2 co
|
||||
1910 | B0 FD B0 B1 FD B0 B2 FD B0 B3 FD B0 55 B4 FD B0 | U
|
||||
1920 | B5 FD B0 B6 FD B0 B7 FD B0 55 B8 FD B0 B9 FD B0 | U
|
||||
1930 | BA FD B0 BB FD B0 55 BC FD B0 BD FD B0 BE FD B0 | U
|
||||
1940 | BF FD B0 03 C0 08 5F 73 | _s
|
||||
1940 | BF FD B0 03 C0 08 00 00 |
|
||||
I 97037 2023-12-29 15:59:37 - [Commands] Received from C-5 (88888888) (version=DC_V1 command=6D flag=00)
|
||||
0000 | 6D 00 B4 03 6E 00 00 00 B0 03 00 00 BA 1D 00 00 | m n
|
||||
0010 | 9F 03 00 00 FF BA 1D 0A 19 B0 02 00 02 FD 10 EB |
|
||||
@@ -7186,7 +7178,7 @@ I 97037 2023-12-29 15:59:37 - [Commands] Received from C-5 (88888888) (version=D
|
||||
0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0180 | 00 00 00 00 00 00 00 00 |
|
||||
I 97037 2023-12-29 15:59:37 - [Commands] Sending to C-3 (Tali) (version=PC_V2 command=62 flag=00)
|
||||
0000 | 88 01 62 00 6F 61 00 00 00 00 08 00 00 00 00 00 | b oa
|
||||
0000 | 08 02 62 00 6F 81 00 00 00 00 08 00 00 00 00 00 | b oa
|
||||
0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
@@ -7210,7 +7202,15 @@ I 97037 2023-12-29 15:59:37 - [Commands] Sending to C-3 (Tali) (version=PC_V2 co
|
||||
0150 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0180 | 00 00 00 00 00 00 00 00 |
|
||||
0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
|
||||
0200 | 00 00 00 00 00 00 00 00 |
|
||||
I 97037 2023-12-29 15:59:37 - [Commands] Received from C-5 (88888888) (version=DC_V1 command=62 flag=00)
|
||||
0000 | 62 00 08 00 71 01 00 00 | b q
|
||||
I 97037 2023-12-29 15:59:37 - [Commands] Sending to C-3 (Tali) (version=PC_V2 command=62 flag=00)
|
||||
|
||||
Reference in New Issue
Block a user