support joinable quests on all versions

This commit is contained in:
Martin Michelsen
2024-04-27 18:14:52 -07:00
parent c7dd98ccc0
commit ddbb922b95
12 changed files with 655 additions and 396 deletions
+1
View File
@@ -1051,6 +1051,7 @@ static void server_command_playrec(shared_ptr<Client> c, const std::string& args
}
s->change_client_lobby(c, game);
c->config.set_flag(Client::Flag::LOADING);
c->log.info("LOADING flag set");
}
} else {
send_text_message(c, "$C4This command cannot\nbe used in a game");
+1
View File
@@ -13,6 +13,7 @@
#include "IPStackSimulator.hh"
#include "Loggers.hh"
#include "SendCommands.hh"
#include "Server.hh"
#include "Version.hh"
+3
View File
@@ -24,6 +24,7 @@ extern const uint64_t CLIENT_CONFIG_MAGIC;
class Server;
struct Lobby;
class Parsed6x70Data;
class Client : public std::enable_shared_from_this<Client> {
public:
@@ -67,6 +68,7 @@ public:
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_ARTIFICIAL_PLAYER_STATES = 0x0200000000000000, // Server-side only
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
IS_CLIENT_CUSTOMIZATION = 0x0100000000000000,
@@ -241,6 +243,7 @@ public:
bool should_update_play_time;
std::unordered_set<uint32_t> blocked_senders;
std::unique_ptr<PlayerDispDataDCPCV3> v1_v2_last_reported_disp;
std::shared_ptr<Parsed6x70Data> last_reported_6x70;
// These are null unless the client is within the trade sequence (D0-D4 or EE commands)
std::unique_ptr<PendingItemTrade> pending_item_trade;
std::unique_ptr<PendingCardTrade> pending_card_trade;
+3 -3
View File
@@ -4974,11 +4974,11 @@ struct G_SyncPlayerDispAndInventory_BB_6x70 {
/* 04C8 */
} __packed_ws__(G_SyncPlayerDispAndInventory_BB_6x70, 0x4C0);
// 6x71: Unknown (used while loading into game)
// 6x71: Unblock game join (used while loading into game)
struct G_Unknown_6x71 {
struct G_UnblockGameJoin_6x71 {
G_UnusedHeader header;
} __packed_ws__(G_Unknown_6x71, 4);
} __packed_ws__(G_UnblockGameJoin_6x71, 4);
// 6x72: Player done loading into game
+15 -8
View File
@@ -205,7 +205,8 @@ VersionedQuest::VersionedQuest(
std::shared_ptr<const BattleRules> battle_rules,
ssize_t challenge_template_index,
std::shared_ptr<const IntegralExpression> available_expression,
std::shared_ptr<const IntegralExpression> enabled_expression)
std::shared_ptr<const IntegralExpression> enabled_expression,
bool force_joinable)
: quest_number(quest_number),
category_id(category_id),
episode(Episode::NONE),
@@ -233,7 +234,7 @@ VersionedQuest::VersionedQuest(
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDCNTE*>(bin_decompressed.data());
this->joinable = false;
this->joinable = force_joinable;
this->episode = Episode::EP1;
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = fnv1a32(header, sizeof(header)) & 0xFFFF;
@@ -249,7 +250,7 @@ VersionedQuest::VersionedQuest(
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDC*>(bin_decompressed.data());
this->joinable = false;
this->joinable = force_joinable;
this->episode = Episode::EP1;
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
@@ -266,7 +267,7 @@ VersionedQuest::VersionedQuest(
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderPC*>(bin_decompressed.data());
this->joinable = false;
this->joinable = force_joinable;
this->episode = Episode::EP1;
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
@@ -289,7 +290,7 @@ VersionedQuest::VersionedQuest(
throw invalid_argument("file is incorrect size");
}
auto* map = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
this->joinable = false;
this->joinable = force_joinable;
this->episode = Episode::EP3;
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = map->map_number;
@@ -307,7 +308,7 @@ VersionedQuest::VersionedQuest(
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderGC*>(bin_decompressed.data());
this->joinable = false;
this->joinable = force_joinable;
this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version);
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
@@ -323,7 +324,7 @@ VersionedQuest::VersionedQuest(
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
this->joinable = header->joinable;
this->joinable = header->joinable || force_joinable;
this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version);
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
@@ -674,6 +675,7 @@ QuestIndex::QuestIndex(
ssize_t challenge_template_index = -1;
shared_ptr<const IntegralExpression> available_expression;
shared_ptr<const IntegralExpression> enabled_expression;
bool force_joinable = false;
try {
json_filedata = &json_files.at(basename);
} catch (const out_of_range&) {
@@ -704,6 +706,10 @@ QuestIndex::QuestIndex(
enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
} catch (const out_of_range&) {
}
try {
force_joinable = metadata_json.get_bool("Joinable");
} catch (const out_of_range&) {
}
}
auto vq = make_shared<VersionedQuest>(
@@ -717,7 +723,8 @@ QuestIndex::QuestIndex(
battle_rules,
challenge_template_index,
available_expression,
enabled_expression);
enabled_expression,
force_joinable);
auto category_name = this->category_index->at(vq->category_id)->name;
string filenames_str = bin_filedata->filename;
+2 -1
View File
@@ -90,7 +90,8 @@ struct VersionedQuest {
std::shared_ptr<const BattleRules> battle_rules = nullptr,
ssize_t challenge_template_index = -1,
std::shared_ptr<const IntegralExpression> available_expression = nullptr,
std::shared_ptr<const IntegralExpression> enabled_expression = nullptr);
std::shared_ptr<const IntegralExpression> enabled_expression = nullptr,
bool force_joinable = false);
std::string bin_filename() const;
std::string dat_filename() const;
+91 -17
View File
@@ -465,6 +465,20 @@ static void on_1D(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
send_game_flag_state(c); // 6x6F
}
if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_PLAYER_STATES)) {
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_PLAYER_STATES);
auto l = c->require_lobby();
if (l->is_game()) {
for (auto lc : l->clients) {
// If we haven't received a 6x70 from this client, maybe they're VERY
// far behind and wil lstll send it, or maybe they'll time out and be
// disconnected soon - either way, we shouldn't fail if it's missing.
if (lc && (lc != c) && lc->last_reported_6x70) {
send_game_player_state(c, lc, true); // 6x70
}
}
}
}
}
static void on_05_XB(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
@@ -1229,6 +1243,7 @@ static bool add_next_game_client(shared_ptr<Lobby> l) {
s->change_client_lobby(c, l, true, target_client_id);
c->config.set_flag(Client::Flag::LOADING);
c->log.info("LOADING flag set");
if (tourn) {
c->config.set_flag(Client::Flag::LOADING_TOURNAMENT);
}
@@ -2045,6 +2060,7 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
if (use_loading_flag) {
lc->config.set_flag(Client::Flag::LOADING_QUEST);
lc->log.info("LOADING_QUEST flag set");
lc->disconnect_hooks.emplace(QUEST_BARRIER_DISCONNECT_HOOK_NAME, [l]() -> void {
send_quest_barrier_if_all_clients_ready(l);
});
@@ -2385,6 +2401,8 @@ 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);
c->log.info("LOADING flag set");
// 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)
@@ -2834,30 +2852,49 @@ static void on_A2(shared_ptr<Client> c, uint16_t, uint32_t flag, string& data) {
}
}
static void on_joinable_quest_loaded(shared_ptr<Client> c) {
auto l = c->require_lobby();
if (!l->is_game() || !l->quest) {
throw runtime_error("joinable quest load completed in non-game");
}
auto leader_c = l->clients.at(l->leader_id);
if (!leader_c) {
throw logic_error("lobby leader is missing");
}
// On BB, ask the leader to send the quest state to the joining player (and
// we'll need to use the game join command queue to avoid any item ID races).
// On other versions, the server will have to generate the state commands;
// this happens when the response to the ping (1D) is received, so we don't
// need the game join command queue in that case.
if (leader_c->version() == Version::BB_V4) {
send_command(leader_c, 0xDD, c->lobby_client_id);
c->log.info("Creating game join command queue");
c->game_join_command_queue = make_unique<deque<Client::JoinCommand>>();
} else {
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE);
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE);
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_PLAYER_STATES);
}
send_command(c, 0x1D, 0x00);
if (!is_v1_or_v2(c->version())) {
send_command(c, 0xAC, 0x00);
}
}
static void on_AC_V3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
check_size_v(data.size(), 0);
auto l = c->require_lobby();
if (c->config.check_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST)) {
if (l->base_version != Version::BB_V4) {
throw logic_error("joinable quest started on non-BB version");
}
auto leader_c = l->clients.at(l->leader_id);
if (!leader_c) {
throw logic_error("lobby leader is missing");
}
send_command(leader_c, 0xDD, c->lobby_client_id);
send_command(c, 0xAC, 0x00);
c->log.info("Creating game join command queue");
c->game_join_command_queue = make_unique<deque<Client::JoinCommand>>();
send_command(c, 0x1D, 0x00);
on_joinable_quest_loaded(c);
} else if (c->config.check_flag(Client::Flag::LOADING_QUEST)) {
c->config.clear_flag(Client::Flag::LOADING_QUEST);
c->log.info("LOADING_QUEST flag cleared");
auto l = c->require_lobby();
if (l->quest && send_quest_barrier_if_all_clients_ready(l)) {
on_quest_loaded(l);
}
@@ -4343,6 +4380,7 @@ static void on_C1_PC(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
if (game) {
s->change_client_lobby(c, game);
c->config.set_flag(Client::Flag::LOADING);
c->log.info("LOADING flag set");
}
}
@@ -4415,6 +4453,7 @@ static void on_0C_C1_E7_EC(shared_ptr<Client> c, uint16_t command, uint32_t, str
if (game) {
s->change_client_lobby(c, game);
c->config.set_flag(Client::Flag::LOADING);
c->log.info("LOADING flag set");
// There is a bug in DC NTE and 11/2000 that causes them to assign item IDs
// twice when joining a game. If there are other players in the game, this
@@ -4472,6 +4511,7 @@ static void on_C1_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
if (game) {
s->change_client_lobby(c, game);
c->config.set_flag(Client::Flag::LOADING);
c->log.info("LOADING flag set");
}
}
@@ -4501,6 +4541,7 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
// don't matter for Ep3)
if (c->config.check_flag(Client::Flag::LOADING)) {
c->config.clear_flag(Client::Flag::LOADING);
c->log.info("LOADING flag cleared");
// The client sends 6F when it has created its TObjPlayer and assigned its
// item IDs. For the leader, however, this happens before any inbound commands
@@ -4533,6 +4574,9 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
send_update_team_reward_flags(c);
send_all_nearby_team_metadatas_to_client(c, false);
// On BB, send the joinable quest file as soon as the client is ready (6F).
// On other versions, we send joinable quests in the 99 handler instead,
// since we need to wait for the client's save to complete.
// BB sends 016F when the client is done loading a quest. In that case, we
// shouldn't send the quest to them again!
if ((command == 0x006F) && l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
@@ -4549,10 +4593,12 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
c->config.set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag set");
should_resume_game = false;
} else if ((command == 0x016F) && c->config.check_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST)) {
c->config.clear_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag cleared");
}
if (l->map) {
send_rare_enemy_index_list(c, l->map->rare_enemy_indexes);
@@ -4616,6 +4662,34 @@ static void on_99(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
} else if (c->should_send_to_proxy_server) {
send_client_to_proxy_server(c);
}
// See the comment in on_6F about why we do this here, but only for non-BB
// versions.
if (l && l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) && (c->version() != Version::BB_V4)) {
if (!l->quest) {
throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest");
}
auto vq = l->quest->version(c->version(), c->language());
if (!vq) {
throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest for client version");
}
string bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename();
send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
c->config.set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag set");
// On v1 and v2, there is no confirmation when the client is done
// downloading the quest file, so just set the in-quest state immediately.
// On v3 and later, we do this when we receive the AC command.
// TODO: This might not work for GC NTE, since we wait for file chunk
// confirmations (13 commands) but there is no AC command.
if (is_v1_or_v2(c->version())) {
on_joinable_quest_loaded(c);
}
}
}
static void on_D0_V3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
+361 -363
View File
@@ -27,7 +27,6 @@ struct SubcommandDefinition {
enum Flag {
ALWAYS_FORWARD_TO_WATCHERS = 0x01,
ALLOW_FORWARD_TO_WATCHED_LOBBY = 0x02,
USE_JOIN_COMMAND_QUEUE = 0x04,
};
uint8_t nte_subcommand;
uint8_t proto_subcommand;
@@ -189,7 +188,7 @@ static void forward_subcommand(shared_ptr<Client> c, uint8_t command, uint8_t fl
if ((command == 0xCB) && (lc->version() == Version::GC_EP3_NTE)) {
command = 0xC9;
}
if ((def_flags & SDF::USE_JOIN_COMMAND_QUEUE) && lc->game_join_command_queue) {
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 = command;
@@ -652,316 +651,350 @@ static void on_sync_joining_player_quest_flags(shared_ptr<Client> c, uint8_t com
}
}
class Parsed6x70Data {
public:
G_SyncPlayerDispAndInventory_BaseDCNTE base;
uint32_t unknown_a5_nte = 0;
uint32_t unknown_a6_nte = 0;
uint16_t bonus_hp_from_materials = 0;
uint16_t bonus_tp_from_materials = 0;
parray<uint8_t, 0x10> unknown_a5_112000;
parray<G_Unknown_6x70_SubA2, 5> unknown_a4_final;
uint32_t language = 0;
uint32_t player_tag = 0;
uint32_t guild_card_number = 0;
uint32_t unknown_a6 = 0;
uint32_t battle_team_number = 0;
Telepipe telepipe;
uint32_t unknown_a8 = 0;
parray<uint8_t, 0x10> unknown_a9_nte_112000;
G_Unknown_6x70_SubA1 unknown_a9_final;
uint32_t area = 0;
uint32_t flags2 = 0;
parray<uint8_t, 0x14> technique_levels_v1 = 0xFF;
PlayerVisualConfig visual;
std::string name;
PlayerStats stats;
uint32_t num_items = 0;
parray<PlayerInventoryItem, 0x1E> items;
uint32_t floor = 0;
uint64_t xb_user_id = 0;
uint32_t xb_unknown_a16 = 0;
Parsed6x70Data(const G_SyncPlayerDispAndInventory_DCNTE_6x70& cmd, uint32_t guild_card_number)
: base(cmd.base),
unknown_a5_nte(cmd.unknown_a5),
unknown_a6_nte(cmd.unknown_a6),
bonus_hp_from_materials(0),
bonus_tp_from_materials(0),
language(0),
player_tag(0x00010000),
guild_card_number(guild_card_number),
unknown_a6(0),
battle_team_number(0),
telepipe(cmd.telepipe),
unknown_a8(cmd.unknown_a8),
unknown_a9_nte_112000(cmd.unknown_a9),
area(cmd.area),
flags2(cmd.flags2),
visual(cmd.visual),
stats(cmd.stats),
num_items(cmd.num_items),
items(cmd.items),
floor(cmd.area),
xb_user_id(this->default_xb_user_id()),
xb_unknown_a16(0) {
this->name = this->visual.name.decode(this->language);
static void transcode_inventory_items(
parray<PlayerInventoryItem, 30>& items,
size_t num_items,
Version from_version,
Version to_version,
shared_ptr<const ItemParameterTable> to_item_parameter_table) {
if (num_items > 30) {
throw runtime_error("invalid inventory item count");
}
Parsed6x70Data(const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd, uint32_t guild_card_number, uint8_t language)
: base(cmd.base),
unknown_a5_nte(0),
unknown_a6_nte(0),
bonus_hp_from_materials(cmd.bonus_hp_from_materials),
bonus_tp_from_materials(cmd.bonus_tp_from_materials),
unknown_a5_112000(cmd.unknown_a5),
language(language),
player_tag(0x00010000),
guild_card_number(guild_card_number),
unknown_a6(0),
battle_team_number(0),
telepipe(cmd.telepipe),
unknown_a8(cmd.unknown_a8),
unknown_a9_nte_112000(cmd.unknown_a9),
area(cmd.area),
flags2(cmd.flags2),
visual(cmd.visual),
stats(cmd.stats),
num_items(cmd.num_items),
items(cmd.items),
floor(cmd.area),
xb_user_id(this->default_xb_user_id()),
xb_unknown_a16(0) {
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data(const G_SyncPlayerDispAndInventory_DC_PC_6x70& cmd, uint32_t guild_card_number)
: Parsed6x70Data(cmd.base, guild_card_number) {
this->stats = cmd.stats;
this->num_items = cmd.num_items;
this->items = cmd.items;
this->floor = cmd.base.area;
this->xb_user_id = this->default_xb_user_id();
this->xb_unknown_a16 = 0;
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data(const G_SyncPlayerDispAndInventory_GC_6x70& cmd, uint32_t guild_card_number)
: Parsed6x70Data(cmd.base, guild_card_number) {
this->stats = cmd.stats;
this->num_items = cmd.num_items;
this->items = cmd.items;
this->floor = cmd.floor;
this->xb_user_id = this->default_xb_user_id();
this->xb_unknown_a16 = 0;
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data(const G_SyncPlayerDispAndInventory_XB_6x70& cmd, uint32_t guild_card_number)
: Parsed6x70Data(cmd.base, guild_card_number) {
this->stats = cmd.stats;
this->num_items = cmd.num_items;
this->items = cmd.items;
this->floor = cmd.floor;
this->xb_user_id = (static_cast<uint64_t>(cmd.xb_user_id_high) << 32) | cmd.xb_user_id_low;
this->xb_unknown_a16 = cmd.unknown_a16;
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data(const G_SyncPlayerDispAndInventory_BB_6x70& cmd, uint32_t guild_card_number)
: Parsed6x70Data(cmd.base, guild_card_number) {
this->stats = cmd.stats;
this->num_items = cmd.num_items;
this->items = cmd.items;
this->floor = cmd.floor;
this->xb_user_id = this->default_xb_user_id();
this->xb_unknown_a16 = cmd.unknown_a16;
this->name = cmd.name.decode(cmd.base.language);
this->visual.name.encode(this->name, cmd.base.language);
}
G_SyncPlayerDispAndInventory_DCNTE_6x70 as_dc_nte() const {
G_SyncPlayerDispAndInventory_DCNTE_6x70 ret;
ret.base = this->base;
ret.unknown_a5 = this->unknown_a5_nte;
ret.unknown_a6 = this->unknown_a6;
ret.telepipe = this->telepipe;
ret.unknown_a8 = this->unknown_a8;
ret.unknown_a9 = this->unknown_a9_nte_112000;
ret.area = this->area;
ret.flags2 = this->flags2;
ret.visual = this->visual;
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
return ret;
}
G_SyncPlayerDispAndInventory_DC112000_6x70 as_dc_112000() const {
G_SyncPlayerDispAndInventory_DC112000_6x70 ret;
ret.base = this->base;
ret.bonus_hp_from_materials = this->bonus_hp_from_materials;
ret.bonus_tp_from_materials = this->bonus_tp_from_materials;
ret.unknown_a5 = this->unknown_a5_112000;
ret.telepipe = this->telepipe;
ret.unknown_a8 = this->unknown_a8;
ret.unknown_a9 = this->unknown_a9_nte_112000;
ret.area = this->area;
ret.flags2 = this->flags2;
ret.visual = this->visual;
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
return ret;
}
G_SyncPlayerDispAndInventory_DC_PC_6x70 as_dc_pc() const {
G_SyncPlayerDispAndInventory_DC_PC_6x70 ret;
ret.base = this->base_v1();
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
return ret;
}
G_SyncPlayerDispAndInventory_GC_6x70 as_gc() const {
G_SyncPlayerDispAndInventory_GC_6x70 ret;
ret.base = this->base_v1();
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
ret.floor = this->floor;
return ret;
}
G_SyncPlayerDispAndInventory_XB_6x70 as_xb() const {
G_SyncPlayerDispAndInventory_XB_6x70 ret;
ret.base = this->base_v1();
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
ret.floor = this->floor;
ret.xb_user_id_high = this->xb_user_id >> 32;
ret.xb_user_id_low = this->xb_user_id;
ret.unknown_a16 = this->xb_unknown_a16;
return ret;
}
G_SyncPlayerDispAndInventory_BB_6x70 as_bb(uint8_t language) const {
G_SyncPlayerDispAndInventory_BB_6x70 ret;
ret.base = this->base_v1();
ret.name.encode(this->name, language);
ret.base.visual.name.encode(string_printf("%10" PRId32, this->guild_card_number), language);
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
ret.floor = this->floor;
ret.xb_user_id_high = this->xb_user_id >> 32;
ret.xb_user_id_low = this->xb_user_id;
ret.unknown_a16 = this->xb_unknown_a16;
return ret;
}
uint64_t default_xb_user_id() const {
return (0xAE00000000000000 | this->guild_card_number);
}
void clear_v1_unused_item_fields() {
for (size_t z = 0; z < min<uint32_t>(this->num_items, 30); z++) {
auto& item = this->items[z];
item.unknown_a1 = 0;
item.extension_data1 = 0;
item.extension_data2 = 0;
if (from_version != to_version) {
for (size_t z = 0; z < num_items; z++) {
items[z].data.decode_for_version(from_version);
items[z].data.encode_for_version(to_version, to_item_parameter_table);
}
}
void clear_dc_protos_unused_item_fields() {
for (size_t z = 0; z < min<uint32_t>(this->num_items, 30); z++) {
auto& item = this->items[z];
item.unknown_a1 = 0;
item.extension_data1 = 0;
item.extension_data2 = 0;
item.data.data2d = 0;
for (size_t z = num_items; z < 30; z++) {
auto& item = items[z];
item.present = 0;
item.unknown_a1 = 0;
item.flags = 0;
item.data.clear();
}
if (is_v1(to_version)) {
for (size_t z = 0; z < 30; z++) {
auto& item = items[z];
item.extension_data1 = 0x00;
item.extension_data2 = 0x00;
}
} else {
for (size_t z = 20; z < 30; z++) {
items[z].extension_data1 = 0x00;
}
for (size_t z = 16; z < 30; z++) {
items[z].extension_data2 = 0x00;
}
}
}
void transcode_inventory_items(
Version from_version,
Version to_version,
shared_ptr<const ItemParameterTable> to_item_parameter_table) {
if (this->num_items > 30) {
throw runtime_error("invalid inventory item count");
}
if (from_version != to_version) {
for (size_t z = 0; z < this->num_items; z++) {
this->items[z].data.decode_for_version(from_version);
this->items[z].data.encode_for_version(to_version, to_item_parameter_table);
}
}
for (size_t z = this->num_items; z < 30; z++) {
auto& item = this->items[z];
item.present = 0;
item.unknown_a1 = 0;
item.flags = 0;
item.data.clear();
}
if (is_v1(to_version)) {
for (size_t z = 0; z < 30; z++) {
auto& item = this->items[z];
item.extension_data1 = 0x00;
item.extension_data2 = 0x00;
}
Parsed6x70Data::Parsed6x70Data(
const G_SyncPlayerDispAndInventory_DCNTE_6x70& cmd, uint32_t guild_card_number, Version from_version)
: from_version(from_version),
item_version(from_version),
base(cmd.base),
unknown_a5_nte(cmd.unknown_a5),
unknown_a6_nte(cmd.unknown_a6),
bonus_hp_from_materials(0),
bonus_tp_from_materials(0),
language(0),
player_tag(0x00010000),
guild_card_number(guild_card_number),
unknown_a6(0),
battle_team_number(0),
telepipe(cmd.telepipe),
unknown_a8(cmd.unknown_a8),
unknown_a9_nte_112000(cmd.unknown_a9),
area(cmd.area),
flags2(cmd.flags2),
visual(cmd.visual),
stats(cmd.stats),
num_items(cmd.num_items),
items(cmd.items),
floor(cmd.area),
xb_user_id(this->default_xb_user_id()),
xb_unknown_a16(0) {
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data::Parsed6x70Data(
const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd, uint32_t guild_card_number, uint8_t language, Version from_version)
: from_version(from_version),
item_version(from_version),
base(cmd.base),
unknown_a5_nte(0),
unknown_a6_nte(0),
bonus_hp_from_materials(cmd.bonus_hp_from_materials),
bonus_tp_from_materials(cmd.bonus_tp_from_materials),
unknown_a5_112000(cmd.unknown_a5),
language(language),
player_tag(0x00010000),
guild_card_number(guild_card_number),
unknown_a6(0),
battle_team_number(0),
telepipe(cmd.telepipe),
unknown_a8(cmd.unknown_a8),
unknown_a9_nte_112000(cmd.unknown_a9),
area(cmd.area),
flags2(cmd.flags2),
visual(cmd.visual),
stats(cmd.stats),
num_items(cmd.num_items),
items(cmd.items),
floor(cmd.area),
xb_user_id(this->default_xb_user_id()),
xb_unknown_a16(0) {
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data::Parsed6x70Data(
const G_SyncPlayerDispAndInventory_DC_PC_6x70& cmd, uint32_t guild_card_number, Version from_version)
: Parsed6x70Data(cmd.base, guild_card_number, from_version) {
this->stats = cmd.stats;
this->num_items = cmd.num_items;
this->items = cmd.items;
this->floor = cmd.base.area;
this->xb_user_id = this->default_xb_user_id();
this->xb_unknown_a16 = 0;
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data::Parsed6x70Data(
const G_SyncPlayerDispAndInventory_GC_6x70& cmd, uint32_t guild_card_number, Version from_version)
: Parsed6x70Data(cmd.base, guild_card_number, from_version) {
this->stats = cmd.stats;
this->num_items = cmd.num_items;
this->items = cmd.items;
this->floor = cmd.floor;
this->xb_user_id = this->default_xb_user_id();
this->xb_unknown_a16 = 0;
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data::Parsed6x70Data(
const G_SyncPlayerDispAndInventory_XB_6x70& cmd, uint32_t guild_card_number, Version from_version)
: Parsed6x70Data(cmd.base, guild_card_number, from_version) {
this->stats = cmd.stats;
this->num_items = cmd.num_items;
this->items = cmd.items;
this->floor = cmd.floor;
this->xb_user_id = (static_cast<uint64_t>(cmd.xb_user_id_high) << 32) | cmd.xb_user_id_low;
this->xb_unknown_a16 = cmd.unknown_a16;
this->name = this->visual.name.decode(this->language);
}
Parsed6x70Data::Parsed6x70Data(
const G_SyncPlayerDispAndInventory_BB_6x70& cmd, uint32_t guild_card_number, Version from_version)
: Parsed6x70Data(cmd.base, guild_card_number, from_version) {
this->stats = cmd.stats;
this->num_items = cmd.num_items;
this->items = cmd.items;
this->floor = cmd.floor;
this->xb_user_id = this->default_xb_user_id();
this->xb_unknown_a16 = cmd.unknown_a16;
this->name = cmd.name.decode(cmd.base.language);
this->visual.name.encode(this->name, cmd.base.language);
}
G_SyncPlayerDispAndInventory_DCNTE_6x70 Parsed6x70Data::as_dc_nte(shared_ptr<ServerState> s) const {
G_SyncPlayerDispAndInventory_DCNTE_6x70 ret;
ret.base = this->base;
ret.unknown_a5 = this->unknown_a5_nte;
ret.unknown_a6 = this->unknown_a6;
ret.telepipe = this->telepipe;
ret.unknown_a8 = this->unknown_a8;
ret.unknown_a9 = this->unknown_a9_nte_112000;
ret.area = this->area;
ret.flags2 = this->flags2;
ret.visual = this->visual;
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
transcode_inventory_items(
ret.items, ret.num_items, this->item_version, Version::DC_NTE, s->item_parameter_table_for_encode(Version::DC_NTE));
ret.visual.enforce_lobby_join_limits_for_version(Version::DC_NTE);
if (s->version_name_colors) {
ret.visual.name_color = s->name_color_for_version(this->from_version);
ret.visual.compute_name_color_checksum();
}
return ret;
}
G_SyncPlayerDispAndInventory_DC112000_6x70 Parsed6x70Data::as_dc_112000(shared_ptr<ServerState> s) const {
G_SyncPlayerDispAndInventory_DC112000_6x70 ret;
ret.base = this->base;
ret.bonus_hp_from_materials = this->bonus_hp_from_materials;
ret.bonus_tp_from_materials = this->bonus_tp_from_materials;
ret.unknown_a5 = this->unknown_a5_112000;
ret.telepipe = this->telepipe;
ret.unknown_a8 = this->unknown_a8;
ret.unknown_a9 = this->unknown_a9_nte_112000;
ret.area = this->area;
ret.flags2 = this->flags2;
ret.visual = this->visual;
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
transcode_inventory_items(
ret.items, ret.num_items, this->item_version, Version::DC_V1_11_2000_PROTOTYPE, s->item_parameter_table_for_encode(Version::DC_V1_11_2000_PROTOTYPE));
ret.visual.enforce_lobby_join_limits_for_version(Version::DC_V1_11_2000_PROTOTYPE);
if (s->version_name_colors) {
ret.visual.name_color = s->name_color_for_version(this->from_version);
ret.visual.compute_name_color_checksum();
}
return ret;
}
G_SyncPlayerDispAndInventory_DC_PC_6x70 Parsed6x70Data::as_dc_pc(shared_ptr<ServerState> s, Version to_version) const {
G_SyncPlayerDispAndInventory_DC_PC_6x70 ret;
ret.base = this->base_v1();
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
transcode_inventory_items(
ret.items, ret.num_items, this->item_version, to_version, s->item_parameter_table_for_encode(to_version));
ret.base.visual.enforce_lobby_join_limits_for_version(to_version);
if (s->version_name_colors) {
ret.base.visual.name_color = s->name_color_for_version(this->from_version);
ret.base.visual.compute_name_color_checksum();
}
return ret;
}
G_SyncPlayerDispAndInventory_GC_6x70 Parsed6x70Data::as_gc_gcnte(shared_ptr<ServerState> s, Version to_version) const {
G_SyncPlayerDispAndInventory_GC_6x70 ret;
ret.base = this->base_v1();
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
ret.floor = this->floor;
transcode_inventory_items(
ret.items, ret.num_items, this->item_version, to_version, s->item_parameter_table_for_encode(to_version));
ret.base.visual.enforce_lobby_join_limits_for_version(to_version);
if (s->version_name_colors) {
ret.base.visual.name_color = s->name_color_for_version(this->from_version);
if (is_v1_or_v2(to_version)) {
ret.base.visual.compute_name_color_checksum();
} else {
for (size_t z = 20; z < 30; z++) {
this->items[z].extension_data1 = 0x00;
}
for (size_t z = 16; z < 30; z++) {
this->items[z].extension_data2 = 0x00;
}
ret.base.visual.name_color_checksum = 0;
}
}
return ret;
}
protected:
Parsed6x70Data(const G_SyncPlayerDispAndInventory_BaseV1& base, uint32_t guild_card_number) {
this->base = base.base;
this->bonus_hp_from_materials = base.bonus_hp_from_materials;
this->bonus_tp_from_materials = base.bonus_tp_from_materials;
this->unknown_a4_final = base.unknown_a4;
this->language = base.language;
this->player_tag = base.player_tag;
this->guild_card_number = guild_card_number; // Ignore the client's GC#
this->unknown_a6 = base.unknown_a6;
this->battle_team_number = base.battle_team_number;
this->telepipe = base.telepipe;
this->unknown_a8 = base.unknown_a8;
this->unknown_a9_final = base.unknown_a9;
this->area = base.area;
this->flags2 = base.flags2;
this->technique_levels_v1 = base.technique_levels_v1;
this->visual = base.visual;
}
G_SyncPlayerDispAndInventory_XB_6x70 Parsed6x70Data::as_xb(shared_ptr<ServerState> s) const {
G_SyncPlayerDispAndInventory_XB_6x70 ret;
ret.base = this->base_v1();
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
ret.floor = this->floor;
ret.xb_user_id_high = this->xb_user_id >> 32;
ret.xb_user_id_low = this->xb_user_id;
ret.unknown_a16 = this->xb_unknown_a16;
G_SyncPlayerDispAndInventory_BaseV1 base_v1() const {
G_SyncPlayerDispAndInventory_BaseV1 ret;
ret.base = this->base;
ret.bonus_hp_from_materials = this->bonus_hp_from_materials;
ret.bonus_tp_from_materials = this->bonus_tp_from_materials;
ret.unknown_a4 = this->unknown_a4_final;
ret.language = this->language;
ret.player_tag = this->player_tag;
ret.guild_card_number = this->guild_card_number;
ret.unknown_a6 = this->unknown_a6;
ret.battle_team_number = this->battle_team_number;
ret.telepipe = this->telepipe;
ret.unknown_a8 = this->unknown_a8;
ret.unknown_a9 = this->unknown_a9_final;
ret.area = this->area;
ret.flags2 = this->flags2;
ret.technique_levels_v1 = this->technique_levels_v1;
ret.visual = this->visual;
return ret;
transcode_inventory_items(
ret.items, ret.num_items, this->item_version, Version::XB_V3, s->item_parameter_table_for_encode(Version::XB_V3));
ret.base.visual.enforce_lobby_join_limits_for_version(Version::XB_V3);
if (s->version_name_colors) {
ret.base.visual.name_color = s->name_color_for_version(this->from_version);
ret.base.visual.name_color_checksum = 0;
}
};
return ret;
}
G_SyncPlayerDispAndInventory_BB_6x70 Parsed6x70Data::as_bb(shared_ptr<ServerState> s, uint8_t language) const {
G_SyncPlayerDispAndInventory_BB_6x70 ret;
ret.base = this->base_v1();
ret.name.encode(this->name, language);
ret.base.visual.name.encode(string_printf("%10" PRId32, this->guild_card_number), language);
ret.stats = this->stats;
ret.num_items = this->num_items;
ret.items = this->items;
ret.floor = this->floor;
ret.xb_user_id_high = this->xb_user_id >> 32;
ret.xb_user_id_low = this->xb_user_id;
ret.unknown_a16 = this->xb_unknown_a16;
transcode_inventory_items(
ret.items, ret.num_items, this->item_version, Version::BB_V4, s->item_parameter_table_for_encode(Version::BB_V4));
ret.base.visual.enforce_lobby_join_limits_for_version(Version::BB_V4);
if (s->version_name_colors) {
ret.base.visual.name_color = s->name_color_for_version(this->from_version);
ret.base.visual.name_color_checksum = 0;
}
return ret;
}
uint64_t Parsed6x70Data::default_xb_user_id() const {
return (0xAE00000000000000 | this->guild_card_number);
}
void Parsed6x70Data::clear_v1_unused_item_fields() {
for (size_t z = 0; z < min<uint32_t>(this->num_items, 30); z++) {
auto& item = this->items[z];
item.unknown_a1 = 0;
item.extension_data1 = 0;
item.extension_data2 = 0;
}
}
void Parsed6x70Data::clear_dc_protos_unused_item_fields() {
for (size_t z = 0; z < min<uint32_t>(this->num_items, 30); z++) {
auto& item = this->items[z];
item.unknown_a1 = 0;
item.extension_data1 = 0;
item.extension_data2 = 0;
item.data.data2d = 0;
}
}
Parsed6x70Data::Parsed6x70Data(
const G_SyncPlayerDispAndInventory_BaseV1& base, uint32_t guild_card_number, Version from_version)
: from_version(from_version),
item_version(this->from_version),
base(base.base),
bonus_hp_from_materials(base.bonus_hp_from_materials),
bonus_tp_from_materials(base.bonus_tp_from_materials),
unknown_a4_final(base.unknown_a4),
language(base.language),
player_tag(base.player_tag),
guild_card_number(guild_card_number), // Ignore the client's GC#
unknown_a6(base.unknown_a6),
battle_team_number(base.battle_team_number),
telepipe(base.telepipe),
unknown_a8(base.unknown_a8),
unknown_a9_final(base.unknown_a9),
area(base.area),
flags2(base.flags2),
technique_levels_v1(base.technique_levels_v1),
visual(base.visual) {}
G_SyncPlayerDispAndInventory_BaseV1 Parsed6x70Data::base_v1() const {
G_SyncPlayerDispAndInventory_BaseV1 ret;
ret.base = this->base;
ret.bonus_hp_from_materials = this->bonus_hp_from_materials;
ret.bonus_tp_from_materials = this->bonus_tp_from_materials;
ret.unknown_a4 = this->unknown_a4_final;
ret.language = this->language;
ret.player_tag = this->player_tag;
ret.guild_card_number = this->guild_card_number;
ret.unknown_a6 = this->unknown_a6;
ret.battle_team_number = this->battle_team_number;
ret.telepipe = this->telepipe;
ret.unknown_a8 = this->unknown_a8;
ret.unknown_a9 = this->unknown_a9_final;
ret.area = this->area;
ret.flags2 = this->flags2;
ret.technique_levels_v1 = this->technique_levels_v1;
ret.visual = this->visual;
return ret;
}
static void on_sync_joining_player_disp_and_inventory(
shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
@@ -985,92 +1018,53 @@ static void on_sync_joining_player_disp_and_inventory(
send_command(target, 0x62, target->lobby_client_id, &data, sizeof(data));
}
unique_ptr<Parsed6x70Data> parsed;
switch (c_v) {
case Version::DC_NTE:
parsed = make_unique<Parsed6x70Data>(
c->last_reported_6x70.reset(new Parsed6x70Data(
check_size_t<G_SyncPlayerDispAndInventory_DCNTE_6x70>(data, size),
c->login->account->account_id);
parsed->clear_dc_protos_unused_item_fields();
c->login->account->account_id, c_v));
c->last_reported_6x70->clear_dc_protos_unused_item_fields();
break;
case Version::DC_V1_11_2000_PROTOTYPE:
parsed = make_unique<Parsed6x70Data>(
c->last_reported_6x70.reset(new Parsed6x70Data(
check_size_t<G_SyncPlayerDispAndInventory_DC112000_6x70>(data, size),
c->login->account->account_id,
c->language());
parsed->clear_dc_protos_unused_item_fields();
c->login->account->account_id, c->language(), c_v));
c->last_reported_6x70->clear_dc_protos_unused_item_fields();
break;
case Version::DC_V1:
case Version::DC_V2:
case Version::PC_NTE:
case Version::PC_V2:
parsed = make_unique<Parsed6x70Data>(
c->last_reported_6x70.reset(new Parsed6x70Data(
check_size_t<G_SyncPlayerDispAndInventory_DC_PC_6x70>(data, size),
c->login->account->account_id);
c->login->account->account_id, c_v));
if (c_v == Version::DC_V1) {
parsed->clear_v1_unused_item_fields();
c->last_reported_6x70->clear_v1_unused_item_fields();
}
break;
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
parsed = make_unique<Parsed6x70Data>(
c->last_reported_6x70.reset(new Parsed6x70Data(
check_size_t<G_SyncPlayerDispAndInventory_GC_6x70>(data, size),
c->login->account->account_id);
c->login->account->account_id, c_v));
break;
case Version::XB_V3:
parsed = make_unique<Parsed6x70Data>(
c->last_reported_6x70.reset(new Parsed6x70Data(
check_size_t<G_SyncPlayerDispAndInventory_XB_6x70>(data, size),
c->login->account->account_id);
c->login->account->account_id, c_v));
break;
case Version::BB_V4:
parsed = make_unique<Parsed6x70Data>(
c->last_reported_6x70.reset(new Parsed6x70Data(
check_size_t<G_SyncPlayerDispAndInventory_BB_6x70>(data, size),
c->login->account->account_id);
c->login->account->account_id, c_v));
break;
default:
throw logic_error("6x70 command from unknown game version");
}
parsed->transcode_inventory_items(c_v, target_v, s->item_parameter_table_for_encode(target_v));
parsed->visual.enforce_lobby_join_limits_for_version(target_v);
if (s->version_name_colors) {
parsed->visual.name_color = s->name_color_for_version(c_v);
if (is_v1_or_v2(target_v)) {
parsed->visual.compute_name_color_checksum();
}
}
switch (target_v) {
case Version::DC_NTE:
forward_subcommand_t(target, command, flag, parsed->as_dc_nte());
break;
case Version::DC_V1_11_2000_PROTOTYPE:
forward_subcommand_t(target, command, flag, parsed->as_dc_112000());
break;
case Version::DC_V1:
case Version::DC_V2:
case Version::PC_NTE:
case Version::PC_V2:
forward_subcommand_t(target, command, flag, parsed->as_dc_pc());
break;
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
forward_subcommand_t(target, command, flag, parsed->as_gc());
break;
case Version::XB_V3:
forward_subcommand_t(target, command, flag, parsed->as_xb());
break;
case Version::BB_V4:
forward_subcommand_t(target, command, flag, parsed->as_bb(target->language()));
break;
default:
throw logic_error("6x70 command from unknown game version");
}
send_game_player_state(target, c, false);
}
static void on_forward_check_client(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
@@ -1346,6 +1340,9 @@ static void on_set_player_visible(shared_ptr<Client> c, uint8_t command, uint8_t
send_update_team_reward_flags(c);
send_all_nearby_team_metadatas_to_client(c, false);
}
} else if (c->config.check_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST) && (c->version() != Version::BB_V4)) {
c->config.clear_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag cleared");
}
}
}
@@ -1360,6 +1357,7 @@ static void on_change_floor_6x1F(shared_ptr<Client> c, uint8_t command, uint8_t
// the loading flag here instead.
if (c->config.check_flag(Client::Flag::LOADING)) {
c->config.clear_flag(Client::Flag::LOADING);
c->log.info("LOADING flag cleared");
send_resume_game(c->require_lobby(), c);
c->require_lobby()->assign_inventory_and_bank_item_ids(c, true);
}
@@ -4470,14 +4468,14 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
/* 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_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},
/* 6x6B */ {0x5C, 0x63, 0x6B, on_sync_joining_player_compressed_state},
/* 6x6C */ {0x5D, 0x64, 0x6C, on_sync_joining_player_compressed_state},
/* 6x6D */ {0x5E, 0x65, 0x6D, on_sync_joining_player_compressed_state},
/* 6x6E */ {0x5F, 0x66, 0x6E, on_sync_joining_player_compressed_state},
/* 6x6F */ {0x00, 0x00, 0x6F, on_sync_joining_player_quest_flags},
/* 6x70 */ {0x60, 0x67, 0x70, on_sync_joining_player_disp_and_inventory},
/* 6x71 */ {0x00, 0x00, 0x71, on_forward_check_game_loading},
/* 6x72 */ {0x61, 0x68, 0x72, on_forward_check_game_loading},
/* 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},
+75
View File
@@ -36,3 +36,78 @@ DropReconcileResult reconcile_drop_request_with_map(
const Client::Config& config,
std::shared_ptr<Map> map,
bool mark_drop);
class Parsed6x70Data {
public:
Version from_version;
Version item_version;
G_SyncPlayerDispAndInventory_BaseDCNTE base;
uint32_t unknown_a5_nte = 0;
uint32_t unknown_a6_nte = 0;
uint16_t bonus_hp_from_materials = 0;
uint16_t bonus_tp_from_materials = 0;
parray<uint8_t, 0x10> unknown_a5_112000;
parray<G_Unknown_6x70_SubA2, 5> unknown_a4_final;
uint32_t language = 0;
uint32_t player_tag = 0;
uint32_t guild_card_number = 0;
uint32_t unknown_a6 = 0;
uint32_t battle_team_number = 0;
Telepipe telepipe;
uint32_t unknown_a8 = 0;
parray<uint8_t, 0x10> unknown_a9_nte_112000;
G_Unknown_6x70_SubA1 unknown_a9_final;
uint32_t area = 0;
uint32_t flags2 = 0;
parray<uint8_t, 0x14> technique_levels_v1 = 0xFF;
PlayerVisualConfig visual;
std::string name;
PlayerStats stats;
uint32_t num_items = 0;
parray<PlayerInventoryItem, 0x1E> items;
uint32_t floor = 0;
uint64_t xb_user_id = 0;
uint32_t xb_unknown_a16 = 0;
Parsed6x70Data(
const G_SyncPlayerDispAndInventory_DCNTE_6x70& cmd,
uint32_t guild_card_number,
Version from_version);
Parsed6x70Data(
const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd,
uint32_t guild_card_number,
uint8_t language,
Version from_version);
Parsed6x70Data(
const G_SyncPlayerDispAndInventory_DC_PC_6x70& cmd,
uint32_t guild_card_number,
Version from_version);
Parsed6x70Data(
const G_SyncPlayerDispAndInventory_GC_6x70& cmd,
uint32_t guild_card_number,
Version from_version);
Parsed6x70Data(
const G_SyncPlayerDispAndInventory_XB_6x70& cmd,
uint32_t guild_card_number,
Version from_version);
Parsed6x70Data(
const G_SyncPlayerDispAndInventory_BB_6x70& cmd,
uint32_t guild_card_number,
Version from_version);
G_SyncPlayerDispAndInventory_DCNTE_6x70 as_dc_nte(std::shared_ptr<ServerState> s) const;
G_SyncPlayerDispAndInventory_DC112000_6x70 as_dc_112000(std::shared_ptr<ServerState> s) const;
G_SyncPlayerDispAndInventory_DC_PC_6x70 as_dc_pc(std::shared_ptr<ServerState> s, Version to_version) const;
G_SyncPlayerDispAndInventory_GC_6x70 as_gc_gcnte(std::shared_ptr<ServerState> s, Version to_version) const;
G_SyncPlayerDispAndInventory_XB_6x70 as_xb(std::shared_ptr<ServerState> s) const;
G_SyncPlayerDispAndInventory_BB_6x70 as_bb(std::shared_ptr<ServerState> s, uint8_t language) const;
uint64_t default_xb_user_id() const;
void clear_v1_unused_item_fields();
void clear_dc_protos_unused_item_fields();
protected:
Parsed6x70Data(const G_SyncPlayerDispAndInventory_BaseV1& base, uint32_t guild_card_number, Version from_version);
G_SyncPlayerDispAndInventory_BaseV1 base_v1() const;
};
+77 -2
View File
@@ -19,6 +19,7 @@
#include "FileContentsCache.hh"
#include "PSOProtocol.hh"
#include "ProxyServer.hh"
#include "ReceiveSubcommands.hh"
#include "StaticGameData.hh"
#include "Text.hh"
@@ -2436,7 +2437,13 @@ void send_arrow_update(shared_ptr<Lobby> l) {
}
}
// tells the player that the joining player is done joining, and the game can resume
void send_unblock_join(shared_ptr<Client> c) {
if (!is_pre_v1(c->version())) {
static const be_uint32_t data = 0x71010000;
send_command(c, 0x60, 0x00, &data, sizeof(be_uint32_t));
}
}
void send_resume_game(shared_ptr<Lobby> l, shared_ptr<Client> ready_client) {
for (auto lc : l->clients) {
if (lc && (lc != ready_client) && !is_pre_v1(lc->version())) {
@@ -2567,7 +2574,7 @@ void send_game_join_sync_command_compressed(
}
if (c->game_join_command_queue) {
c->log.info("Client not ready to receive join commands; adding to queue");
c->log.info("Client not ready to receive game commands; adding to queue");
auto& cmd = c->game_join_command_queue->emplace_back();
cmd.command = 0x6D;
cmd.flag = c->lobby_client_id;
@@ -2789,6 +2796,74 @@ void send_game_flag_state(shared_ptr<Client> c) {
}
}
void send_game_player_state(shared_ptr<Client> to_c, shared_ptr<Client> from_c, bool apply_overrides) {
if (!from_c->last_reported_6x70) {
throw runtime_error("source client did not send a 6x70 command");
}
if (!from_c->login) {
throw logic_error("source client is not logged in");
}
auto s = to_c->require_server_state();
Parsed6x70Data to_send = *from_c->last_reported_6x70;
to_send.base.client_id = from_c->lobby_client_id;
to_send.player_tag = 0x00010000;
to_send.guild_card_number = from_c->login->account->account_id;
if (apply_overrides) {
auto from_p = from_c->character();
to_send.base.x = from_c->x;
to_send.base.z = from_c->z;
to_send.bonus_hp_from_materials = from_p->inventory.hp_from_materials;
to_send.bonus_tp_from_materials = from_p->inventory.tp_from_materials;
to_send.language = from_c->language();
// TODO: Deal with telepipes. Probably we should track their state via the
// subcommands sent when they're created/destroyed, but currently we don't.
to_send.area = from_c->floor;
to_send.technique_levels_v1 = from_p->disp.technique_levels_v1;
to_send.visual = from_p->disp.visual;
to_send.name = from_p->disp.name.decode(from_c->language());
if (to_c->version() != Version::BB_V4) {
to_send.visual.name.encode(to_send.name, to_c->language());
}
to_send.stats = from_p->disp.stats;
to_send.num_items = from_p->inventory.num_items;
to_send.items = from_p->inventory.items;
to_send.item_version = Version::BB_V4; // Server-side items are stored in BB encoding
to_send.floor = from_c->floor;
}
switch (to_c->version()) {
case Version::DC_NTE:
send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_dc_nte(s));
break;
case Version::DC_V1_11_2000_PROTOTYPE:
send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_dc_112000(s));
break;
case Version::DC_V1:
case Version::DC_V2:
case Version::PC_NTE:
case Version::PC_V2:
send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_dc_pc(s, to_c->version()));
break;
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_gc_gcnte(s, to_c->version()));
break;
case Version::XB_V3:
send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_xb(s));
break;
case Version::BB_V4:
send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_bb(s, to_c->language()));
break;
default:
throw logic_error("attempting to send 6x70 command to unknown game version");
}
}
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);
+17
View File
@@ -32,6 +32,21 @@ extern const std::unordered_set<std::string> bb_crypt_initial_client_commands;
// pointer is given but size is accidentally not given (e.g. if the type of
// data in the calling function is changed from string to void*).
template <typename CmdT>
void send_or_enqueue_command(std::shared_ptr<Client> c, uint16_t command, uint32_t flag, const CmdT& cmd) {
if (c->game_join_command_queue) {
c->log.info("Client not ready to receive game commands; adding to queue");
auto& q_cmd = c->game_join_command_queue->emplace_back();
q_cmd.command = command;
q_cmd.flag = flag;
// TODO: It'd be nice to avoid this copy. Maybe take in a pointer to cmd
// and move it into q_cmd somehow, so q_cmd can free it when needed?
q_cmd.data.assign(reinterpret_cast<const char*>(&cmd), sizeof(cmd));
} else {
send_command(c, command, flag, &cmd, sizeof(cmd));
}
}
void send_command(std::shared_ptr<Client> c, uint16_t command,
uint32_t flag, const std::vector<std::pair<const void*, size_t>>& blocks);
@@ -293,6 +308,7 @@ void send_execute_item_trade(std::shared_ptr<Client> c, const std::vector<ItemDa
void send_execute_card_trade(std::shared_ptr<Client> c, const std::vector<std::pair<uint32_t, uint32_t>>& card_to_count);
void send_arrow_update(std::shared_ptr<Lobby> l);
void send_unblock_join(std::shared_ptr<Client> c);
void send_resume_game(std::shared_ptr<Lobby> l, std::shared_ptr<Client> ready_client);
enum PlayerStatsChange {
@@ -331,6 +347,7 @@ 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_game_player_state(std::shared_ptr<Client> to_c, std::shared_ptr<Client> from_c, bool apply_overrides);
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);
void send_drop_item_to_lobby(std::shared_ptr<Lobby> l, const ItemData& item,
+9 -2
View File
@@ -35,8 +35,8 @@
// "ChallengeTemplateIndex": 0,
// Quests may be set to be unavailable until a preceding quest has been
// cleared or a team reward has been purchased. To enable this feature, set a
// value for AvailableIf in the quest's JSON file. (This is ignored if the
// cleared or a team reward has been purchased. To enable this behavior, set
// a value for AvailableIf in the quest's JSON file. (This is ignored if the
// player has the DISABLE_QUEST_REQUIREMENTS flag in their account.) This
// field's value should be an expression that tests any of the following:
// F_XXXX: Quest flag specified in hex (e.g. F_014D)
@@ -54,4 +54,11 @@
// to false, this is ignored. This field is also ignored if the player has
// the DISABLE_QUEST_REQUIREMENTS flag in their account.
// "EnabledIf": "!F_0169",
// On BB, a quest's joinability flag is part of the quest file header, but
// other versions don't natively support joining quests in progress. This
// flag, if present, enables non-BB quests to be joined when already in
// progress. Note that this will likely not work properly unless the quest
// script is designed to support joining players.
// "Joinable": true,
}