use .psochar format for BB characters

This commit is contained in:
Martin Michelsen
2023-11-13 13:00:22 -08:00
parent f5bfd4a3c6
commit 18ddfa4ef4
20 changed files with 1138 additions and 842 deletions
+15 -15
View File
@@ -270,20 +270,20 @@ static void server_command_quest(shared_ptr<Client> c, const std::string& args)
}
static void server_command_show_material_counts(shared_ptr<Client> c, const std::string&) {
auto p = c->game_data.player();
auto p = c->game_data.character();
if ((c->version() == GameVersion::DC) || (c->version() == GameVersion::PC)) {
send_text_message_printf(c, "%hhu HP, %hhu TP",
p->get_material_usage(SavedPlayerDataBB::MaterialType::HP),
p->get_material_usage(SavedPlayerDataBB::MaterialType::TP));
p->get_material_usage(PSOBBCharacterFile::MaterialType::HP),
p->get_material_usage(PSOBBCharacterFile::MaterialType::TP));
} else {
send_text_message_printf(c, "%hhu HP, %hhu TP, %hhu POW\n%hhu MIND, %hhu EVADE\n%hhu DEF, %hhu LUCK",
p->get_material_usage(SavedPlayerDataBB::MaterialType::HP),
p->get_material_usage(SavedPlayerDataBB::MaterialType::TP),
p->get_material_usage(SavedPlayerDataBB::MaterialType::POWER),
p->get_material_usage(SavedPlayerDataBB::MaterialType::MIND),
p->get_material_usage(SavedPlayerDataBB::MaterialType::EVADE),
p->get_material_usage(SavedPlayerDataBB::MaterialType::DEF),
p->get_material_usage(SavedPlayerDataBB::MaterialType::LUCK));
p->get_material_usage(PSOBBCharacterFile::MaterialType::HP),
p->get_material_usage(PSOBBCharacterFile::MaterialType::TP),
p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER),
p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND),
p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE),
p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF),
p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK));
}
}
@@ -832,7 +832,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
vector<string> tokens = split(encoded_args, ' ');
try {
auto p = c->game_data.player();
auto p = c->game_data.character();
if (tokens.at(0) == "atp") {
p->disp.stats.char_stats.atp = stoul(tokens.at(1));
} else if (tokens.at(0) == "mst") {
@@ -939,8 +939,8 @@ static void server_command_convert_char_to_bb(shared_ptr<Client> c, const std::s
}
// username/password are tokens[0] and [1]
c->pending_bb_save_player_index = stoul(tokens[2]) - 1;
if (c->pending_bb_save_player_index > 3) {
c->pending_bb_save_character_index = stoul(tokens[2]) - 1;
if (c->pending_bb_save_character_index > 3) {
send_text_message(c, "$C6Player index must be 1-4");
return;
}
@@ -963,7 +963,7 @@ static void server_command_convert_char_to_bb(shared_ptr<Client> c, const std::s
// Administration commands
static string name_for_client(shared_ptr<Client> c) {
auto player = c->game_data.player(false);
auto player = c->game_data.character(false);
if (player.get()) {
return player->disp.name.decode(player->inventory.language);
}
@@ -1486,7 +1486,7 @@ static void server_command_surrender(shared_ptr<Client> c, const std::string&) {
send_text_message(c, "$C6Battle has not\nyet started");
return;
}
const string& name = c->game_data.player()->disp.name.decode(c->language());
const string& name = c->game_data.character()->disp.name.decode(c->language());
send_text_message_printf(l, "$C6%s has\nsurrendered", name.c_str());
for (const auto& watcher_l : l->watcher_lobbies) {
send_text_message_printf(watcher_l, "$C6%s has\nsurrendered", name.c_str());
+3 -6
View File
@@ -170,7 +170,7 @@ Client::Client(
card_battle_table_seat_state(0),
next_exp_value(0),
can_chat(true),
pending_bb_save_player_index(0),
pending_bb_save_character_index(0),
dol_base_addr(0) {
this->config.set_flags_for_version(version, -1);
@@ -267,11 +267,8 @@ void Client::save_game_data() {
if (this->version() != GameVersion::BB) {
throw logic_error("save_game_data called for non-BB client");
}
if (this->game_data.account(false)) {
this->game_data.save_account_data();
}
if (this->game_data.player(false)) {
this->game_data.save_player_data();
if (this->game_data.character(false)) {
this->game_data.save_character_file();
}
}
+1 -1
View File
@@ -197,7 +197,7 @@ struct Client : public std::enable_shared_from_this<Client> {
G_SwitchStateChanged_6x05 last_switch_enabled_command;
bool can_chat;
std::string pending_bb_save_username;
uint8_t pending_bb_save_player_index;
uint8_t pending_bb_save_character_index;
std::deque<std::function<void(uint32_t, uint32_t)>> function_call_response_queue;
// File loading state
+18 -61
View File
@@ -1671,7 +1671,7 @@ struct C_Login_BB_93 {
le_uint32_t guild_card_number = 0;
le_uint32_t sub_version = 0;
uint8_t language;
uint8_t character_slot;
int8_t character_slot;
// Values for connection_phase:
// 00 - initial connection (client will request system file, characters, etc.)
// 01 - choose character
@@ -2829,16 +2829,16 @@ struct S_GameInformation_GC_Ep3_E1 {
/* 0298 */
} __packed__;
// E1 (S->C): Create system file (BB)
// E1 (S->C): System file created (BB)
// This seems to take the place of 00E2 in certain cases. Perhaps it was used
// when a client hadn't logged in before and didn't have a system file, so the
// client should use appropriate defaults.
struct S_TeamAndKeyConfigMissing_00E1_BB {
struct S_SystemFileCreated_00E1_BB {
// If success is not equal to 1, the client shows a message saying "Forced
// server disconnect (907)" and disconnects. Otherwise, the client proceeeds
// as if it had received an 00E2 command, and sends its first 00E3.
le_uint32_t success = 0;
le_uint32_t success = 1;
} __packed__;
// E2 (C->S): Tournament control (Episode 3)
@@ -2954,7 +2954,7 @@ struct S_TournamentGameDetails_GC_Ep3_E3 {
// E3 (C->S): Player preview request (BB)
struct C_PlayerPreviewRequest_BB_E3 {
le_uint32_t player_index = 0;
le_int32_t character_index = 0;
le_uint32_t unused = 0;
} __packed__;
@@ -2990,12 +2990,12 @@ struct S_CardBattleTableState_GC_Ep3_E4 {
// E4 (S->C): Player choice or no player present (BB)
struct S_ApprovePlayerChoice_BB_00E4 {
le_uint32_t player_index = 0;
le_int32_t character_index = 0;
le_uint32_t result = 0; // 1 = approved
} __packed__;
struct S_PlayerPreview_NoPlayer_BB_00E4 {
le_uint32_t player_index = 0;
le_int32_t character_index = 0;
le_uint32_t error = 0; // 2 = no player present
} __packed__;
@@ -3011,7 +3011,7 @@ struct S_CardBattleTableConfirmation_GC_Ep3_E5 {
// E5 (C->S): Create character (BB)
struct SC_PlayerPreview_CreateCharacter_BB_00E5 {
le_uint32_t player_index = 0;
le_int32_t character_index = 0;
PlayerDispDataBBPreview preview;
} __packed__;
@@ -3063,34 +3063,13 @@ struct C_CreateSpectatorTeam_GC_Ep3_E7 {
// E7 (S->C): Tournament entry list for spectating (Episode 3)
// Same format as E2 command.
// E7: Save or load full character data (BB)
// TODO: Verify full breakdown from send_E7 in BB disassembly.
// E7: Sync save files (BB)
struct SC_SyncCharacterSaveFile_BB_00E7 {
/* 0000 */ PlayerInventory inventory; // From player data
/* 034C */ PlayerDispDataBB disp; // From player data
/* 04DC */ le_uint32_t unknown_a1 = 0;
/* 04E0 */ le_uint32_t creation_timestamp = 0;
/* 04E4 */ le_uint32_t signature = 0xA205B064;
/* 04E8 */ le_uint32_t play_time_seconds = 0;
/* 04EC */ le_uint32_t option_flags = 0; // account
/* 04F0 */ parray<uint8_t, 0x0208> quest_data1; // player
/* 06F8 */ PlayerBank bank; // player
/* 19C0 */ GuildCardBB guild_card;
/* 1AC8 */ le_uint32_t unknown_a3 = 0;
/* 1ACC */ parray<uint8_t, 0x04E0> symbol_chats; // account
/* 1FAC */ parray<uint8_t, 0x0A40> shortcuts; // account
/* 29EC */ pstring<TextEncoding::UTF16, 0x00AC> auto_reply; // player
/* 2B44 */ pstring<TextEncoding::UTF16, 0x00AC> info_board; // player
/* 2C9C */ PlayerRecords_Battle<false> battle_records;
/* 2CB4 */ parray<uint8_t, 4> unknown_a4;
/* 2CB8 */ PlayerRecordsBB_Challenge challenge_records;
/* 2DF8 */ parray<uint8_t, 0x0028> tech_menu_config; // player
/* 2E20 */ parray<uint8_t, 0x002C> unknown_a6;
/* 2E4C */ parray<le_uint32_t, 0x0016> quest_data2; // player
/* 2EA4 */ PSOBBSystemFile system_file; // account
struct SC_SyncSaveFiles_BB_E7 {
/* 0000 */ PSOBBCharacterFile char_file;
/* 2EA4 */ PSOBBSystemFile system_file;
/* 3994 */
} __attribute__((packed));
} __packed__;
// E8 (S->C): Join spectator team (Episode 3)
// header.flag = player count (including spectators)
@@ -3471,7 +3450,7 @@ struct C_UpdateOptionFlags_BB_01ED {
le_uint32_t option_flags = 0;
} __packed__;
struct C_UpdateSymbolChats_BB_02ED {
parray<uint8_t, 0x4E0> symbol_chats;
parray<PSOBBCharacterFile::SymbolChatEntry, 12> symbol_chats;
} __packed__;
struct C_UpdateChatShortcuts_BB_03ED {
parray<uint8_t, 0xA40> chat_shortcuts;
@@ -3483,7 +3462,7 @@ struct C_UpdatePadConfig_BB_05ED {
parray<uint8_t, 0x38> pad_config;
} __packed__;
struct C_UpdateTechMenu_BB_06ED {
parray<uint8_t, 0x28> tech_menu;
parray<le_uint16_t, 0x14> tech_menu;
} __packed__;
struct C_UpdateCustomizeMenu_BB_07ED {
parray<uint8_t, 0xE8> customize;
@@ -3734,28 +3713,6 @@ struct G_SendGuildCard_BB_6x06 {
// 6x07: Symbol chat
struct SymbolChat {
// Bits: ----------------------DMSSSCCCFF
// S = sound, C = face color, F = face shape, D = capture, M = mute sound
/* 00 */ le_uint32_t spec = 0;
// Corner objects are specified in reading order ([0] is the top-left one).
// Bits (each entry): ---VHCCCZZZZZZZZ
// V = reverse vertical, H = reverse horizontal, C = color, Z = object
// If Z is all 1 bits (0xFF), no corner object is rendered.
/* 04 */ parray<le_uint16_t, 4> corner_objects;
struct FacePart {
uint8_t type = 0; // FF = no part in this slot
uint8_t x = 0;
uint8_t y = 0;
// Bits: ------VH (V = reverse vertical, H = reverse horizontal)
uint8_t flags = 0;
} __packed__;
/* 0C */ parray<FacePart, 12> face_parts;
/* 3C */
} __packed__;
struct G_SymbolChat_6x07 {
G_UnusedHeader header;
le_uint32_t client_id = 0;
@@ -5580,12 +5537,12 @@ struct G_ChallengeModeGrave_BB_6xD1 {
le_uint32_t unknown_a5 = 0;
} __packed__;
// 6xD2: Set quest data 2 (BB)
// 6xD2: Set quest global flag (BB)
// Writes 4 bytes to the 32-bit field specified by index.
struct G_SetQuestData2_BB_6xD2 {
struct G_SetQuestGlobalFlag_BB_6xD2 {
G_ClientIDHeader header;
le_uint32_t index = 0;
le_uint32_t index = 0; // There are 0x10 of them (0x00-0x0F)
le_uint32_t value = 0;
} __packed__;
+1 -1
View File
@@ -16,7 +16,7 @@ Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number, const string& playe
Tournament::PlayerEntry::PlayerEntry(shared_ptr<Client> c)
: serial_number(c->license->serial_number),
client(c),
player_name(c->game_data.player()->disp.name.decode(c->language())) {}
player_name(c->game_data.character()->disp.name.decode(c->language())) {}
Tournament::PlayerEntry::PlayerEntry(
shared_ptr<const COMDeckDefinition> com_deck)
+4 -4
View File
@@ -16,7 +16,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
// delete the item here.
bool should_delete_item = (c->version() != GameVersion::DC) && (c->version() != GameVersion::PC);
auto player = c->game_data.player();
auto player = c->game_data.character();
auto& item = player->inventory.items[item_index];
uint32_t item_identifier = item.data.primary_identifier();
@@ -53,10 +53,10 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
weapon.data.data1[3] += (item.data.data1[2] + 1);
} else if ((item_identifier & 0xFFFF00) == 0x030B00) { // Material
auto p = c->game_data.player();
auto p = c->game_data.character();
bool track_non_hp_tp_materials = (c->version() != GameVersion::DC) && (c->version() != GameVersion::PC);
using Type = SavedPlayerDataBB::MaterialType;
using Type = PSOBBCharacterFile::MaterialType;
Type type;
switch (item.data.data1[2]) {
case 0: // Power Material
@@ -276,7 +276,7 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
});
auto s = c->require_server_state();
auto player = c->game_data.player();
auto player = c->game_data.character();
auto& fed_item = player->inventory.items[fed_item_index];
auto& mag_item = player->inventory.items[mag_item_index];
+3 -3
View File
@@ -193,7 +193,7 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
// item IDs
if (this->is_game() && this->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto s = this->require_server_state();
auto p = c->game_data.player();
auto p = c->game_data.character();
auto& inv = p->inventory;
size_t count = min<uint8_t>(inv.num_items, 30);
for (size_t x = 0; x < count; x++) {
@@ -205,7 +205,7 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
// If the lobby is recording a battle record, add the player join event
if (this->battle_record) {
auto p = c->game_data.player();
auto p = c->game_data.character();
PlayerLobbyDataDCGC lobby_data;
lobby_data.player_tag = 0x00010000;
lobby_data.guild_card_number = c->license->serial_number;
@@ -304,7 +304,7 @@ shared_ptr<Client> Lobby::find_client(const string* identifier, uint64_t serial_
(lc->license->serial_number == serial_number)) {
return lc;
}
if (identifier && (lc->game_data.player()->disp.name.eq(*identifier, lc->language()))) {
if (identifier && (lc->game_data.character()->disp.name.eq(*identifier, lc->language()))) {
return lc;
}
}
+262 -342
View File
@@ -12,58 +12,51 @@
#include "ItemData.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
using namespace std;
// Originally there was going to be a language-based header, but then I decided
// against it. This string was already in use for that parser, so I didn't
// bother changing it.
static const string ACCOUNT_FILE_SIGNATURE =
"newserv account file format; 7 sections present; sequential;";
ClientGameData::ClientGameData()
: last_play_time_update(0),
guild_card_number(0),
: guild_card_number(0),
should_update_play_time(false),
bb_player_index(0),
should_save(true) {}
bb_character_index(-1),
last_play_time_update(0) {
for (size_t z = 0; z < this->blocked_senders.size(); z++) {
this->blocked_senders[z] = 0;
}
}
ClientGameData::~ClientGameData() {
if (!this->bb_username.empty()) {
if (this->account_data.get()) {
this->save_account_data();
}
if (this->player_data.get()) {
this->save_player_data();
}
if (!this->bb_username.empty() && this->character_data.get()) {
this->save_character_file();
}
}
void ClientGameData::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_ptr<const LevelTable> level_table) {
this->overlay_player_data.reset(new SavedPlayerDataBB(*this->player(true, false)));
this->overlay_character_data.reset(new PSOBBCharacterFile(*this->character(true, false)));
if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) {
this->overlay_player_data->inventory.remove_all_items_of_type(0);
this->overlay_player_data->inventory.remove_all_items_of_type(1);
this->overlay_character_data->inventory.remove_all_items_of_type(0);
this->overlay_character_data->inventory.remove_all_items_of_type(1);
}
if (rules->mag_mode == BattleRules::MagMode::FORBID_ALL) {
this->overlay_player_data->inventory.remove_all_items_of_type(2);
this->overlay_character_data->inventory.remove_all_items_of_type(2);
}
if (rules->tool_mode != BattleRules::ToolMode::ALLOW) {
this->overlay_player_data->inventory.remove_all_items_of_type(3);
this->overlay_character_data->inventory.remove_all_items_of_type(3);
}
if (rules->replace_char) {
// TODO: Shouldn't we clear other material usage here? It looks like the
// original code doesn't, but that seems wrong.
this->overlay_player_data->inventory.hp_from_materials = 0;
this->overlay_player_data->inventory.tp_from_materials = 0;
this->overlay_character_data->inventory.hp_from_materials = 0;
this->overlay_character_data->inventory.tp_from_materials = 0;
uint32_t target_level = clamp<uint32_t>(rules->char_level, 0, 199);
uint8_t char_class = this->overlay_player_data->disp.visual.char_class;
auto& stats = this->overlay_player_data->disp.stats;
uint8_t char_class = this->overlay_character_data->disp.visual.char_class;
auto& stats = this->overlay_character_data->disp.stats;
stats.reset_to_base(char_class, level_table);
stats.advance_to_level(char_class, target_level, level_table);
@@ -74,30 +67,30 @@ void ClientGameData::create_battle_overlay(shared_ptr<const BattleRules> rules,
if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) {
// TODO: Verify this is what the game actually does.
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
uint8_t existing_level = this->overlay_player_data->get_technique_level(tech_num);
uint8_t existing_level = this->overlay_character_data->get_technique_level(tech_num);
if ((existing_level != 0xFF) && (existing_level > rules->max_tech_level)) {
this->overlay_player_data->set_technique_level(tech_num, rules->max_tech_level);
this->overlay_character_data->set_technique_level(tech_num, rules->max_tech_level);
}
}
} else if (rules->tech_disk_mode == BattleRules::TechDiskMode::FORBID_ALL) {
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
this->overlay_player_data->set_technique_level(tech_num, 0xFF);
this->overlay_character_data->set_technique_level(tech_num, 0xFF);
}
}
if (rules->meseta_mode != BattleRules::MesetaMode::ALLOW) {
this->overlay_player_data->disp.stats.meseta = 0;
this->overlay_character_data->disp.stats.meseta = 0;
}
if (rules->forbid_scape_dolls) {
this->overlay_player_data->inventory.remove_all_items_of_type(3, 9);
this->overlay_character_data->inventory.remove_all_items_of_type(3, 9);
}
}
void ClientGameData::create_challenge_overlay(GameVersion version, size_t template_index, shared_ptr<const LevelTable> level_table) {
const auto& tpl = get_challenge_template_definition(
version, this->player(true, false)->disp.visual.class_flags, template_index);
auto p = this->character(true, false);
const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index);
this->overlay_player_data.reset(new SavedPlayerDataBB(*this->player(true, false)));
auto overlay = this->overlay_player_data;
this->overlay_character_data.reset(new PSOBBCharacterFile(*p));
auto overlay = this->overlay_character_data;
for (size_t z = 0; z < overlay->inventory.items.size(); z++) {
auto& i = overlay->inventory.items[z];
@@ -137,360 +130,287 @@ void ClientGameData::create_challenge_overlay(GameVersion version, size_t templa
}
}
shared_ptr<SavedAccountDataBB> ClientGameData::account(bool allow_load) {
if (!this->account_data.get() && allow_load) {
if (this->bb_username.empty()) {
this->account_data.reset(new SavedAccountDataBB());
this->account_data->signature.encode(ACCOUNT_FILE_SIGNATURE);
} else {
this->load_account_data();
}
shared_ptr<PSOBBSystemFile> ClientGameData::system(bool allow_load) {
if (!this->system_data && allow_load) {
this->load_all_files();
}
return this->account_data;
return this->system_data;
}
shared_ptr<SavedPlayerDataBB> ClientGameData::player(bool allow_load, bool allow_overlay) {
if (this->overlay_player_data && allow_overlay) {
return this->overlay_player_data;
shared_ptr<const PSOBBSystemFile> ClientGameData::system(bool allow_load) const {
if (!this->system_data.get() && allow_load) {
throw runtime_error("system data is not loaded");
}
if (!this->player_data.get() && allow_load) {
if (this->bb_username.empty()) {
this->player_data.reset(new SavedPlayerDataBB());
} else {
this->load_player_data();
}
}
return this->player_data;
return this->system_data;
}
shared_ptr<const SavedAccountDataBB> ClientGameData::account(bool allow_load) const {
if (!this->account_data.get() && allow_load) {
shared_ptr<PSOBBCharacterFile> ClientGameData::character(bool allow_load, bool allow_overlay) {
if (this->overlay_character_data && allow_overlay) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
if (this->bb_character_index < 0) {
throw runtime_error("character index not specified");
}
this->load_all_files();
}
return this->character_data;
}
shared_ptr<const PSOBBCharacterFile> ClientGameData::character(bool allow_load, bool allow_overlay) const {
if (allow_overlay && this->overlay_character_data) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
throw runtime_error("character data is not loaded");
}
return this->character_data;
}
shared_ptr<PSOBBGuildCardFile> ClientGameData::guild_cards(bool allow_load) {
if (!this->guild_card_data && allow_load) {
this->load_all_files();
}
return this->guild_card_data;
}
shared_ptr<const PSOBBGuildCardFile> ClientGameData::guild_cards(bool allow_load) const {
if (!this->guild_card_data && allow_load) {
throw runtime_error("account data is not loaded");
}
return this->account_data;
return this->guild_card_data;
}
shared_ptr<const SavedPlayerDataBB> ClientGameData::player(bool allow_load, bool allow_overlay) const {
if (allow_overlay && this->overlay_player_data) {
return this->overlay_player_data;
}
if (!this->player_data.get() && allow_load) {
throw runtime_error("player data is not loaded");
}
return this->player_data;
}
string ClientGameData::account_data_filename() const {
string ClientGameData::system_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have account data");
throw logic_error("non-BB players do not have system data");
}
return string_printf("system/players/account_%s.nsa",
this->bb_username.c_str());
return string_printf("system/players/system_%s.psosys", this->bb_username.c_str());
}
string ClientGameData::player_data_filename() const {
string ClientGameData::character_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have account data");
throw logic_error("non-BB players do not have character data");
}
return string_printf("system/players/player_%s_%zu.nsc",
this->bb_username.c_str(), this->bb_player_index + 1);
if (this->bb_character_index < 0) {
throw logic_error("character index is not set");
}
return string_printf("system/players/player_%s_%hhd.psochar", this->bb_username.c_str(), this->bb_character_index);
}
string ClientGameData::player_template_filename(uint8_t char_class) {
return string_printf("system/players/default_player_%hhu.nsc", char_class);
string ClientGameData::guild_card_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have Guild Card files");
}
return string_printf("system/players/guild_cards_%s.psocard", this->bb_username.c_str());
}
void ClientGameData::create_player(
string ClientGameData::legacy_account_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have legacy account data");
}
return string_printf("system/players/account_%s.nsa", this->bb_username.c_str());
}
string ClientGameData::legacy_player_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have legacy player data");
}
if (this->bb_character_index < 0) {
throw logic_error("character index is not set");
}
return string_printf(
"system/players/player_%s_%hhd.nsc",
this->bb_username.c_str(),
static_cast<int8_t>(this->bb_character_index + 1));
}
void ClientGameData::create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> level_table) {
shared_ptr<SavedPlayerDataBB> data(new SavedPlayerDataBB(
load_object_file<SavedPlayerDataBB>(player_template_filename(preview.visual.char_class))));
data->update_to_latest_version();
try {
data->disp.apply_preview(preview);
data->disp.stats.char_stats = level_table->base_stats_for_class(data->disp.visual.char_class);
} catch (const exception& e) {
throw runtime_error(string_printf("template application failed: %s", e.what()));
}
this->player_data = data;
this->save_player_data();
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
this->save_character_file();
}
void ClientGameData::load_account_data() {
string filename = this->account_data_filename();
void ClientGameData::load_all_files() {
if (this->bb_username.empty()) {
this->system_data.reset(new PSOBBSystemFile());
this->character_data.reset(new PSOBBCharacterFile());
this->guild_card_data.reset(new PSOBBGuildCardFile());
return;
}
shared_ptr<SavedAccountDataBB> data;
try {
data.reset(new SavedAccountDataBB(
player_files_cache.get_obj_or_load<SavedAccountDataBB>(filename).obj));
if (!data->signature.eq(ACCOUNT_FILE_SIGNATURE)) {
throw runtime_error("account data header is incorrect");
this->system_data.reset();
this->character_data.reset();
this->guild_card_data.reset();
string sys_filename = this->system_filename();
if (isfile(sys_filename)) {
this->system_data.reset(new PSOBBSystemFile(load_object_file<PSOBBSystemFile>(sys_filename)));
player_data_log.info("Loaded system data from %s", sys_filename.c_str());
}
if (this->bb_character_index >= 0) {
string char_filename = this->character_filename();
if (isfile(char_filename)) {
auto f = fopen_unique(char_filename, "rb");
auto header = freadx<PSOCommandHeaderBB>(f.get());
if (header.size != 0x399C) {
throw runtime_error("incorrect size in character file header");
}
if (header.command != 0x00E7) {
throw runtime_error("incorrect command in character file header");
}
if (header.flag != 0x00000000) {
throw runtime_error("incorrect flag in character file header");
}
this->character_data.reset(new PSOBBCharacterFile(freadx<PSOBBCharacterFile>(f.get())));
player_data_log.info("Loaded character data from %s", char_filename.c_str());
// If there was no .psosys file, load the system file from the .psochar
// file instead
if (!this->system_data) {
this->system_data.reset(new PSOBBSystemFile(freadx<PSOBBSystemFile>(f.get())));
player_data_log.info("Loaded system data from %s", char_filename.c_str());
}
}
player_data_log.info("Loaded account data file %s", filename.c_str());
}
} catch (const exception& e) {
player_data_log.info("Cannot load account data for %s (%s); using default",
this->bb_username.c_str(), e.what());
player_files_cache.delete_key(filename);
data.reset(new SavedAccountDataBB(
player_files_cache.get_obj_or_load<SavedAccountDataBB>(
"system/players/default.nsa")
.obj));
if (!data->signature.eq(ACCOUNT_FILE_SIGNATURE)) {
throw runtime_error("default account data header is incorrect");
string card_filename = this->guild_card_filename();
if (isfile(card_filename)) {
this->guild_card_data.reset(new PSOBBGuildCardFile(load_object_file<PSOBBGuildCardFile>(card_filename)));
player_data_log.info("Loaded Guild Card data from %s", card_filename.c_str());
}
// If any of the above files were missing, try to load from .nsa/.nsc files instead
if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) {
string nsa_filename = this->legacy_account_filename();
shared_ptr<LegacySavedAccountDataBB> nsa_data;
if (isfile(nsa_filename)) {
nsa_data.reset(new LegacySavedAccountDataBB(load_object_file<LegacySavedAccountDataBB>(nsa_filename)));
if (!nsa_data->signature.eq(LegacySavedAccountDataBB::SIGNATURE)) {
throw runtime_error("account data header is incorrect");
}
if (!this->system_data) {
this->system_data.reset(new PSOBBSystemFile(nsa_data->system_file));
player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str());
}
if (!this->guild_card_data) {
this->guild_card_data.reset(new PSOBBGuildCardFile(nsa_data->guild_card_file));
player_data_log.info("Loaded legacy Guild Card data from %s", nsa_filename.c_str());
}
}
if (!this->system_data) {
this->system_data.reset(new PSOBBSystemFile());
player_data_log.info("Created new system data");
}
if (!this->guild_card_data) {
this->guild_card_data.reset(new PSOBBGuildCardFile());
player_data_log.info("Created new Guild Card data");
}
if (!this->character_data && (this->bb_character_index >= 0)) {
string nsc_filename = this->legacy_player_filename();
auto nsc_data = load_object_file<LegacySavedPlayerDataBB>(nsc_filename);
if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) {
nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0;
nsc_data.unused.clear();
nsc_data.battle_records.place_counts.clear(0);
nsc_data.battle_records.disconnect_count = 0;
nsc_data.battle_records.unknown_a1.clear(0);
} else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) {
throw runtime_error("legacy player data has incorrect signature");
}
this->character_data.reset(new PSOBBCharacterFile());
this->character_data->inventory = nsc_data.inventory;
this->character_data->disp = nsc_data.disp;
this->character_data->play_time_seconds = nsc_data.disp.play_time;
this->character_data->unknown_a2 = nsc_data.unknown_a2;
this->character_data->quest_flags = nsc_data.quest_flags;
this->character_data->death_count = nsc_data.death_count;
this->character_data->bank = nsc_data.bank;
this->character_data->guild_card.guild_card_number = this->guild_card_number;
this->character_data->guild_card.name = nsc_data.disp.name;
this->character_data->guild_card.team_name = this->system_data->team_name;
this->character_data->guild_card.description = nsc_data.guild_card_description;
this->character_data->guild_card.present = 1;
this->character_data->guild_card.language = nsc_data.inventory.language;
this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id;
this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class;
this->character_data->auto_reply = nsc_data.auto_reply;
this->character_data->info_board = nsc_data.info_board;
this->character_data->battle_records = nsc_data.battle_records;
this->character_data->challenge_records = nsc_data.challenge_records;
this->character_data->tech_menu_config = nsc_data.tech_menu_config;
this->character_data->quest_global_flags = nsc_data.quest_global_flags;
if (nsa_data) {
this->character_data->option_flags = nsa_data->option_flags;
this->character_data->symbol_chats = nsa_data->symbol_chats;
this->character_data->shortcuts = nsa_data->shortcuts;
player_data_log.info("Loaded legacy player data from %s and %s", nsa_filename.c_str(), nsc_filename.c_str());
} else {
player_data_log.info("Loaded legacy player data from %s", nsc_filename.c_str());
}
}
player_data_log.info("Loaded default account data file");
}
this->account_data = data;
}
void ClientGameData::save_account_data() const {
if (!this->account_data.get()) {
throw logic_error("save_account_data called when no account data loaded");
this->blocked_senders.fill(0);
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
if (this->guild_card_data->blocked[z].present) {
this->blocked_senders[z] = this->guild_card_data->blocked[z].guild_card_number;
}
}
string filename = this->account_data_filename();
player_files_cache.replace(filename, this->account_data.get(), sizeof(SavedAccountDataBB));
if (this->should_save) {
save_file(filename, this->account_data.get(), sizeof(SavedAccountDataBB));
player_data_log.info("Saved account data file %s to filesystem", filename.c_str());
} else {
player_data_log.info("Saved account data file %s to cache only", filename.c_str());
if (this->character_data) {
this->last_play_time_update = now();
}
}
void ClientGameData::load_player_data() {
this->last_play_time_update = now();
string filename = this->player_data_filename();
this->player_data.reset(new SavedPlayerDataBB(
player_files_cache.get_obj_or_load<SavedPlayerDataBB>(filename).obj));
try {
this->player_data->update_to_latest_version();
} catch (const exception&) {
this->player_data.reset();
player_files_cache.delete_key(filename);
throw;
void ClientGameData::save_system_file() const {
if (!this->system_data) {
throw logic_error("no system file loaded");
}
player_data_log.info("Loaded player data file %s", filename.c_str());
string filename = this->system_filename();
save_object_file(filename, *this->system_data);
player_data_log.info("Saved system file %s", filename.c_str());
}
void ClientGameData::save_player_data() {
if (!this->player_data.get()) {
throw logic_error("save_player_data called when no player data loaded");
void ClientGameData::save_character_file() {
if (!this->system_data.get()) {
throw logic_error("no system file loaded");
}
if (!this->character_data.get()) {
throw logic_error("no character file loaded");
}
if (this->should_update_play_time) {
// This is slightly inaccurate, since fractions of a second are truncated
// off each time we save. I'm lazy, so insert shrug emoji here.
uint64_t t = now();
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
this->player_data->disp.play_time += seconds;
this->character_data->disp.play_time += seconds;
this->character_data->play_time_seconds = this->character_data->disp.play_time;
player_data_log.info("Added %" PRIu64 " seconds to play time", seconds);
this->last_play_time_update = t;
}
string filename = this->player_data_filename();
player_files_cache.replace(filename, this->player_data.get(), sizeof(SavedPlayerDataBB));
if (this->should_save) {
save_file(filename, this->player_data.get(), sizeof(SavedPlayerDataBB));
player_data_log.info("Saved player data file %s to filesystem", filename.c_str());
} else {
player_data_log.info("Saved player data file %s to cache only", filename.c_str());
}
string filename = this->character_filename();
auto f = fopen_unique(filename, "wb");
PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBSystemFile), 0x00E7, 0x00000000};
fwritex(f.get(), header);
fwritex(f.get(), *this->character_data);
fwritex(f.get(), *this->system_data);
player_data_log.info("Saved character file %s", filename.c_str());
}
void SavedPlayerDataBB::update_to_latest_version() {
if (this->signature == PLAYER_FILE_SIGNATURE_V0) {
this->signature = PLAYER_FILE_SIGNATURE_V1;
this->unused.clear();
this->battle_records.place_counts.clear(0);
this->battle_records.disconnect_count = 0;
this->battle_records.unknown_a1.clear(0);
} else if (this->signature != PLAYER_FILE_SIGNATURE_V1) {
throw runtime_error("player data has incorrect signature");
}
}
// TODO: Eliminate duplication between this function and the parallel function
// in PlayerBank
void SavedPlayerDataBB::add_item(const ItemData& item) {
uint32_t pid = item.primary_identifier();
// Annoyingly, meseta is in the disp data, not in the inventory struct. If the
// item is meseta, we have to modify disp instead.
if (pid == MESETA_IDENTIFIER) {
this->add_meseta(item.data2d);
return;
}
// Handle combinable items
size_t combine_max = item.max_stack_size();
if (combine_max > 1) {
// Get the item index if there's already a stack of the same item in the
// player's inventory
size_t y;
for (y = 0; y < this->inventory.num_items; y++) {
if (this->inventory.items[y].data.primary_identifier() == item.primary_identifier()) {
break;
}
}
// If we found an existing stack, add it to the total and return
if (y < this->inventory.num_items) {
size_t new_stack_size = this->inventory.items[y].data.data1[5] + item.data1[5];
if (new_stack_size > combine_max) {
throw out_of_range("stack is too large");
}
this->inventory.items[y].data.data1[5] = new_stack_size;
return;
}
}
// If we get here, then it's not meseta and not a combine item, so it needs to
// go into an empty inventory slot
if (this->inventory.num_items >= 30) {
throw out_of_range("inventory is full");
}
auto& inv_item = this->inventory.items[this->inventory.num_items];
inv_item.present = 1;
inv_item.unknown_a1 = 0;
inv_item.flags = 0;
inv_item.data = item;
this->inventory.num_items++;
}
// TODO: Eliminate code duplication between this function and the parallel
// function in PlayerBank
ItemData SavedPlayerDataBB::remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft) {
ItemData ret;
// If we're removing meseta (signaled by an invalid item ID), then create a
// meseta item.
if (item_id == 0xFFFFFFFF) {
this->remove_meseta(amount, allow_meseta_overdraft);
ret.data1[0] = 0x04;
ret.data2d = amount;
return ret;
}
size_t index = this->inventory.find_item(item_id);
auto& inventory_item = this->inventory.items[index];
// If the item is a combine item and are we removing less than we have of it,
// then create a new item and reduce the amount of the existing stack. Note
// that passing amount == 0 means to remove the entire stack, so this only
// applies if amount is nonzero.
if (amount && (inventory_item.data.stack_size() > 1) &&
(amount < inventory_item.data.data1[5])) {
ret = inventory_item.data;
ret.data1[5] = amount;
ret.id = 0xFFFFFFFF;
inventory_item.data.data1[5] -= amount;
return ret;
}
// If we get here, then it's not meseta, and either it's not a combine item or
// we're removing the entire stack. Delete the item from the inventory slot
// and return the deleted item.
ret = inventory_item.data;
this->inventory.num_items--;
for (size_t x = index; x < this->inventory.num_items; x++) {
this->inventory.items[x] = this->inventory.items[x + 1];
}
auto& last_item = this->inventory.items[this->inventory.num_items];
last_item.present = 0;
last_item.unknown_a1 = 0;
last_item.flags = 0;
last_item.data.clear();
return ret;
}
void SavedPlayerDataBB::add_meseta(uint32_t amount) {
this->disp.stats.meseta = min<size_t>(static_cast<size_t>(this->disp.stats.meseta) + amount, 999999);
}
void SavedPlayerDataBB::remove_meseta(uint32_t amount, bool allow_overdraft) {
if (amount <= this->disp.stats.meseta) {
this->disp.stats.meseta -= amount;
} else if (allow_overdraft) {
this->disp.stats.meseta = 0;
} else {
throw out_of_range("player does not have enough meseta");
}
}
uint8_t SavedPlayerDataBB::get_technique_level(uint8_t which) const {
return (this->disp.technique_levels_v1[which] == 0xFF)
? 0xFF
: (this->disp.technique_levels_v1[which] + this->inventory.items[which].extension_data1);
}
void SavedPlayerDataBB::set_technique_level(uint8_t which, uint8_t level) {
if (level == 0xFF) {
this->disp.technique_levels_v1[which] = 0xFF;
this->inventory.items[which].extension_data1 = 0x00;
} else if (level <= 0x0E) {
this->disp.technique_levels_v1[which] = level;
this->inventory.items[which].extension_data1 = 0x00;
} else {
this->disp.technique_levels_v1[which] = 0x0E;
this->inventory.items[which].extension_data1 = level - 0x0E;
}
}
uint8_t SavedPlayerDataBB::get_material_usage(MaterialType which) const {
switch (which) {
case MaterialType::HP:
return this->inventory.hp_from_materials >> 1;
case MaterialType::TP:
return this->inventory.tp_from_materials >> 1;
case MaterialType::POWER:
case MaterialType::MIND:
case MaterialType::EVADE:
case MaterialType::DEF:
case MaterialType::LUCK:
return this->inventory.items[8 + static_cast<uint8_t>(which)].extension_data2;
default:
throw logic_error("invalid material type");
}
}
void SavedPlayerDataBB::set_material_usage(MaterialType which, uint8_t usage) {
switch (which) {
case MaterialType::HP:
this->inventory.hp_from_materials = usage << 1;
break;
case MaterialType::TP:
this->inventory.tp_from_materials = usage << 1;
break;
case MaterialType::POWER:
case MaterialType::MIND:
case MaterialType::EVADE:
case MaterialType::DEF:
case MaterialType::LUCK:
this->inventory.items[8 + static_cast<uint8_t>(which)].extension_data2 = usage;
break;
default:
throw logic_error("invalid material type");
}
}
void SavedPlayerDataBB::clear_all_material_usage() {
this->inventory.hp_from_materials = 0;
this->inventory.tp_from_materials = 0;
for (size_t z = 0; z < 5; z++) {
this->inventory.items[z + 8].extension_data2 = 0;
}
}
void SavedPlayerDataBB::print_inventory(FILE* stream, GameVersion version, shared_ptr<const ItemNameIndex> name_index) const {
fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", this->disp.stats.meseta.load());
fprintf(stream, "[PlayerInventory] %hhu items\n", this->inventory.num_items);
for (size_t x = 0; x < this->inventory.num_items; x++) {
const auto& item = this->inventory.items[x];
auto name = name_index->describe_item(version, item.data);
auto hex = item.data.hex();
fprintf(stream, "[PlayerInventory] %2zu: %s (%s)\n", x, hex.c_str(), name.c_str());
void ClientGameData::save_guild_card_file() const {
if (!this->guild_card_data.get()) {
throw logic_error("no Guild Card file loaded");
}
string filename = this->guild_card_filename();
save_object_file(filename, *this->guild_card_data);
player_data_log.info("Saved Guild Card file %s", filename.c_str());
}
+37 -88
View File
@@ -30,85 +30,15 @@ struct PendingCardTrade {
std::vector<std::pair<uint32_t, uint32_t>> card_to_count;
};
constexpr uint64_t PLAYER_FILE_SIGNATURE_V0 = 0x6E65777365727620;
constexpr uint64_t PLAYER_FILE_SIGNATURE_V1 = 0xA904332D5CEF0296;
struct SavedPlayerDataBB { // .nsc file format
/* 0000 */ be_uint64_t signature = PLAYER_FILE_SIGNATURE_V1;
/* 0008 */ parray<uint8_t, 0x20> unused;
/* 0028 */ PlayerRecords_Battle<false> battle_records;
/* 0040 */ PlayerDispDataBBPreview preview;
/* 00BC */ pstring<TextEncoding::UTF16, 0x00AC> auto_reply;
/* 0214 */ PlayerBank bank;
/* 14DC */ PlayerRecordsBB_Challenge challenge_records;
/* 161C */ PlayerDispDataBB disp;
/* 17AC */ pstring<TextEncoding::UTF16, 0x0058> guild_card_description;
/* 185C */ pstring<TextEncoding::UTF16, 0x00AC> info_board;
/* 19B4 */ PlayerInventory inventory;
/* 1D00 */ parray<uint8_t, 0x0208> quest_data1;
/* 1F08 */ parray<le_uint32_t, 0x0016> quest_data2;
/* 1F60 */ parray<uint8_t, 0x0028> tech_menu_config;
/* 1F88 */
void update_to_latest_version();
void add_item(const ItemData& item);
ItemData remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft);
void add_meseta(uint32_t amount);
void remove_meseta(uint32_t amount, bool allow_overdraft);
uint8_t get_technique_level(uint8_t which) const; // Returns FF or 00-1D
void set_technique_level(uint8_t which, uint8_t level);
enum class MaterialType : int8_t {
HP = -2,
TP = -1,
POWER = 0,
MIND = 1,
EVADE = 2,
DEF = 3,
LUCK = 4,
};
uint8_t get_material_usage(MaterialType which) const;
void set_material_usage(MaterialType which, uint8_t usage);
void clear_all_material_usage();
void print_inventory(FILE* stream, GameVersion version, std::shared_ptr<const ItemNameIndex> name_index) const;
} __attribute__((packed));
enum AccountFlag {
IN_DRESSING_ROOM = 0x00000001,
};
struct SavedAccountDataBB { // .nsa file format
pstring<TextEncoding::ASCII, 0x40> signature;
parray<le_uint32_t, 0x001E> blocked_senders;
PSOBBGuildCardFile guild_card_file;
PSOBBSystemFile system_file;
le_uint32_t unused;
le_uint32_t option_flags;
parray<uint8_t, 0x0A40> shortcuts;
parray<uint8_t, 0x04E0> symbol_chats;
pstring<TextEncoding::UTF16, 0x0010> team_name;
} __attribute__((packed));
class ClientGameData {
private:
std::shared_ptr<SavedAccountDataBB> account_data;
// The overlay player data is used in battle and challenge modes, when player
// data is temporarily replaced in-game. In other play modes and in lobbies,
// overlay_player_data is null.
std::shared_ptr<SavedPlayerDataBB> overlay_player_data;
std::shared_ptr<SavedPlayerDataBB> player_data;
uint64_t last_play_time_update;
public:
uint32_t guild_card_number;
bool should_update_play_time;
// The following fields are not saved, and are only used in certain situations
std::array<uint32_t, 30> blocked_senders;
// 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;
@@ -118,10 +48,9 @@ public:
// These are only used if the client is BB
std::string bb_username;
size_t bb_player_index;
int8_t bb_character_index;
ItemData identify_result;
std::array<std::vector<ItemData>, 3> shop_contents;
bool should_save;
ClientGameData();
~ClientGameData();
@@ -129,28 +58,48 @@ public:
void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
void create_challenge_overlay(GameVersion version, size_t template_index, std::shared_ptr<const LevelTable> level_table);
inline void delete_overlay() {
this->overlay_player_data.reset();
this->overlay_character_data.reset();
}
inline bool has_overlay() const {
return this->overlay_player_data.get() != nullptr;
return this->overlay_character_data.get() != nullptr;
}
std::shared_ptr<SavedAccountDataBB> account(bool allow_load = true);
std::shared_ptr<SavedPlayerDataBB> player(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<const SavedAccountDataBB> account(bool allow_load = true) const;
std::shared_ptr<const SavedPlayerDataBB> player(bool allow_load = true, bool allow_overlay = true) const;
std::shared_ptr<PSOBBSystemFile> system(bool allow_load = true);
std::shared_ptr<const PSOBBSystemFile> system(bool allow_load = true) const;
std::string account_data_filename() const;
std::string player_data_filename() const;
static std::string player_template_filename(uint8_t char_class);
std::shared_ptr<PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<const PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true) const;
void create_player(
std::shared_ptr<PSOBBGuildCardFile> guild_cards(bool allow_load = true);
std::shared_ptr<const PSOBBGuildCardFile> guild_cards(bool allow_load = true) const;
void create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
std::shared_ptr<const LevelTable> level_table);
void load_account_data();
void save_account_data() const;
void load_player_data();
void save_system_file() const;
// Note: This function is not const because it updates the player's play time.
void save_player_data();
void save_character_file();
void save_guild_card_file() const;
private:
// The overlay character data is used in battle and challenge modes, when
// character data is temporarily replaced in-game. In other play modes and in
// lobbies, overlay_character_data is null.
std::shared_ptr<PSOBBSystemFile> system_data;
std::shared_ptr<PSOBBCharacterFile> overlay_character_data;
std::shared_ptr<PSOBBCharacterFile> character_data;
std::shared_ptr<PSOBBGuildCardFile> guild_card_data;
uint64_t last_play_time_update;
void load_all_files();
std::string system_filename() const;
std::string character_filename() const;
std::string guild_card_filename() const;
std::string legacy_player_filename() const;
std::string legacy_account_filename() const;
};
+82 -31
View File
@@ -19,8 +19,6 @@
using namespace std;
FileContentsCache player_files_cache(300 * 1000 * 1000);
uint32_t PlayerVisualConfig::compute_name_color_checksum(uint32_t name_color) {
uint8_t x = (random_object<uint32_t>() % 0xFF) + 1;
uint8_t y = (random_object<uint32_t>() % 0xFF) + 1;
@@ -222,20 +220,6 @@ void GuildCardBB::clear() {
this->char_class = 0;
}
void PlayerBank::load(const string& filename) {
*this = player_files_cache.get_obj_or_load<PlayerBank>(filename).obj;
for (uint32_t x = 0; x < this->num_items; x++) {
this->items[x].data.id = 0x0F010000 + x;
}
}
void PlayerBank::save(const string& filename, bool save_to_filesystem) const {
player_files_cache.replace(filename, this, sizeof(*this));
if (save_to_filesystem) {
save_file(filename, this, sizeof(*this));
}
}
void PlayerLobbyDataPC::clear() {
this->player_tag = 0;
this->guild_card_number = 0;
@@ -288,10 +272,17 @@ PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsDC_Chall
times_ep1_online(rec.times_ep1_online),
times_ep2_online(0),
times_ep1_offline(0),
unknown_g3(rec.unknown_g3),
grave_is_ep2(0),
grave_stage_num(rec.grave_stage_num),
grave_floor(rec.grave_floor),
unknown_g0(0),
grave_deaths(rec.grave_deaths),
unknown_u4(0),
grave_coords_time(rec.grave_coords_time),
grave_time(rec.grave_time),
unknown_g1(rec.unknown_g1),
grave_x(rec.grave_x),
grave_y(rec.grave_y),
grave_z(rec.grave_z),
grave_team(rec.grave_team.decode(), 1),
grave_message(rec.grave_message.decode(), 1),
unknown_m5(0),
@@ -305,10 +296,17 @@ PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsPC_Chall
times_ep1_online(rec.times_ep1_online),
times_ep2_online(0),
times_ep1_offline(0),
unknown_g3(rec.unknown_g3),
grave_is_ep2(0),
grave_stage_num(rec.grave_stage_num),
grave_floor(rec.grave_floor),
unknown_g0(0),
grave_deaths(rec.grave_deaths),
unknown_u4(0),
grave_coords_time(rec.grave_coords_time),
grave_time(rec.grave_time),
unknown_g1(rec.unknown_g1),
grave_x(rec.grave_x),
grave_y(rec.grave_y),
grave_z(rec.grave_z),
grave_team(rec.grave_team.decode(), 1),
grave_message(rec.grave_message.decode(), 1),
unknown_m5(0),
@@ -322,14 +320,24 @@ PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsV3_Chall
times_ep1_online(rec.stats.times_ep1_online),
times_ep2_online(rec.stats.times_ep2_online),
times_ep1_offline(rec.stats.times_ep1_offline),
unknown_g3(rec.stats.unknown_g3),
grave_is_ep2(rec.stats.grave_is_ep2),
grave_stage_num(rec.stats.grave_stage_num),
grave_floor(rec.stats.grave_floor),
unknown_g0(rec.stats.unknown_g0),
grave_deaths(rec.stats.grave_deaths),
unknown_u4(rec.stats.unknown_u4),
grave_coords_time(rec.stats.grave_coords_time),
grave_time(rec.stats.grave_time),
unknown_g1(rec.stats.unknown_g1),
grave_x(rec.stats.grave_x),
grave_y(rec.stats.grave_y),
grave_z(rec.stats.grave_z),
grave_team(rec.stats.grave_team.decode(), 1),
grave_message(rec.stats.grave_message.decode(), 1),
unknown_m5(rec.stats.unknown_m5),
unknown_t6(rec.stats.unknown_t6),
ep1_online_award_state(rec.stats.ep1_online_award_state),
ep2_online_award_state(rec.stats.ep2_online_award_state),
ep1_offline_award_state(rec.stats.ep1_offline_award_state),
rank_title(rec.rank_title.decode(), 1),
unknown_l7(rec.unknown_l7) {}
@@ -339,9 +347,23 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsDC_Challenge() const {
ret.unknown_u0 = this->unknown_u0;
ret.rank_title.encode(this->rank_title.decode());
ret.times_ep1_online = this->times_ep1_online;
ret.unknown_g3 = 0;
if (this->grave_is_ep2) {
ret.grave_stage_num = 0;
ret.grave_floor = 0;
ret.unknown_g1 = 0;
ret.grave_x = 0;
ret.grave_y = 0;
ret.grave_z = 0;
} else {
ret.grave_stage_num = this->grave_stage_num;
ret.grave_floor = this->grave_floor;
ret.unknown_g1 = this->unknown_g1;
ret.grave_x = this->grave_x;
ret.grave_y = this->grave_y;
ret.grave_z = this->grave_z;
}
ret.grave_time = this->grave_time;
ret.grave_deaths = this->grave_deaths;
ret.grave_coords_time = this->grave_coords_time;
ret.grave_team.encode(this->grave_team.decode());
ret.grave_message.encode(this->grave_message.decode());
ret.times_ep1_offline = this->times_ep1_offline;
@@ -355,9 +377,23 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsPC_Challenge() const {
ret.unknown_u0 = this->unknown_u0;
ret.rank_title = this->rank_title;
ret.times_ep1_online = this->times_ep1_online;
ret.unknown_g3 = 0;
if (this->grave_is_ep2) {
ret.grave_stage_num = 0;
ret.grave_floor = 0;
ret.unknown_g1 = 0;
ret.grave_x = 0;
ret.grave_y = 0;
ret.grave_z = 0;
} else {
ret.grave_stage_num = this->grave_stage_num;
ret.grave_floor = this->grave_floor;
ret.unknown_g1 = this->unknown_g1;
ret.grave_x = this->grave_x;
ret.grave_y = this->grave_y;
ret.grave_z = this->grave_z;
}
ret.grave_time = this->grave_time;
ret.grave_deaths = this->grave_deaths;
ret.grave_coords_time = this->grave_coords_time;
ret.grave_team.encode(this->grave_team.decode());
ret.grave_message.encode(this->grave_message.decode());
ret.times_ep1_offline = this->times_ep1_offline;
@@ -372,15 +408,25 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge<false>() const {
ret.stats.times_ep1_online = this->times_ep1_online;
ret.stats.times_ep2_online = this->times_ep2_online;
ret.stats.times_ep1_offline = this->times_ep1_offline;
ret.stats.unknown_g3 = this->unknown_g3;
ret.stats.grave_is_ep2 = this->grave_is_ep2;
ret.stats.grave_stage_num = this->grave_stage_num;
ret.stats.grave_floor = this->grave_floor;
ret.stats.unknown_g0 = this->unknown_g0;
ret.stats.grave_deaths = this->grave_deaths;
ret.stats.unknown_u4 = this->unknown_u4;
ret.stats.grave_coords_time = this->grave_coords_time;
ret.stats.grave_team.encode(this->grave_team.decode());
ret.stats.grave_message.encode(this->grave_message.decode());
ret.stats.grave_time = this->grave_time;
ret.stats.unknown_g1 = this->unknown_g1;
ret.stats.grave_x = this->grave_x;
ret.stats.grave_y = this->grave_y;
ret.stats.grave_z = this->grave_z;
ret.stats.grave_team.encode(this->grave_team.decode(), 1);
ret.stats.grave_message.encode(this->grave_message.decode(), 1);
ret.stats.unknown_m5 = this->unknown_m5;
ret.stats.unknown_t6 = this->unknown_t6;
ret.rank_title.encode(this->rank_title.decode());
ret.stats.ep1_online_award_state = this->ep1_online_award_state;
ret.stats.ep2_online_award_state = this->ep2_online_award_state;
ret.stats.ep1_offline_award_state = this->ep1_offline_award_state;
ret.rank_title.encode(this->rank_title.decode(), 1);
ret.unknown_l7 = this->unknown_l7;
return ret;
}
@@ -956,3 +1002,8 @@ const ChallengeTemplateDefinition& get_challenge_template_definition(GameVersion
throw runtime_error("invalid class flags on original player");
}
}
SymbolChat::SymbolChat()
: spec(0),
corner_objects(0x00FF),
face_parts() {}
+58 -15
View File
@@ -18,8 +18,6 @@
class ItemParameterTable;
extern FileContentsCache player_files_cache;
// PSO V2 stored some extra data in the character structs in a format that I'm
// sure Sega thought was very clever for backward compatibility, but for us is
// just plain annoying. Specifically, they used the third and fourth bytes of
@@ -89,12 +87,6 @@ struct PlayerBank {
/* 0008 */ parray<PlayerBankItem, 200> items;
/* 12C8 */
void load(const std::string& filename);
void save(const std::string& filename, bool save_to_filesystem) const;
bool switch_with_file(const std::string& save_filename,
const std::string& load_filename);
void add_item(const ItemData& item);
ItemData remove_item(uint32_t item_id, uint32_t amount);
size_t find_item(uint32_t item_id);
@@ -325,9 +317,21 @@ struct PlayerRecordsDCPC_Challenge {
/* 02 */ parray<uint8_t, 2> unknown_u0;
/* 04 */ pstring<EncryptedEncoding, 0x0C> rank_title;
/* 10 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times
/* 34 */ le_uint16_t unknown_g3 = 0;
/* 34 */ uint8_t grave_stage_num = 0;
/* 35 */ uint8_t grave_floor = 0;
/* 36 */ le_uint16_t grave_deaths = 0;
/* 38 */ parray<le_uint32_t, 5> grave_coords_time;
// grave_time is encoded with the following bit fields:
// YYYYMMMM DDDDDDDD HHHHHHHH mmmmmmmm
// Y = year after 2000 (clamped to [0, 15])
// M = month
// D = day
// H = hour
// m = minute
/* 38 */ le_uint32_t grave_time = 0;
/* 3C */ le_uint32_t unknown_g1 = 0;
/* 40 */ le_float grave_x = 0.0f;
/* 44 */ le_float grave_y = 0.0f;
/* 48 */ le_float grave_z = 0.0f;
/* 4C */ pstring<UnencryptedEncoding, 0x14> grave_team;
/* 60 */ pstring<UnencryptedEncoding, 0x18> grave_message;
/* 78 */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times
@@ -345,6 +349,7 @@ template <bool IsBigEndian>
struct PlayerRecordsV3_Challenge {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
using FloatT = typename std::conditional<IsBigEndian, be_float, le_float>::type;
// Offsets are (1) relative to start of C5 entry, and (2) relative to start
// of save file structure
@@ -354,10 +359,17 @@ struct PlayerRecordsV3_Challenge {
/* 04:20 */ parray<U32T, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
/* 28:44 */ parray<U32T, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
/* 3C:58 */ parray<U32T, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
/* 60:7C */ parray<uint8_t, 4> unknown_g3;
/* 60:7C */ uint8_t grave_is_ep2 = 0;
/* 61:7D */ uint8_t grave_stage_num = 0;
/* 62:7E */ uint8_t grave_floor = 0;
/* 63:7F */ uint8_t unknown_g0 = 0;
/* 64:80 */ U16T grave_deaths = 0;
/* 66:82 */ parray<uint8_t, 2> unknown_u4;
/* 68:84 */ parray<U32T, 5> grave_coords_time;
/* 68:84 */ U32T grave_time = 0; // Encoded as in PlayerRecordsDCPC_Challenge
/* 6C:88 */ U32T unknown_g1 = 0;
/* 70:8C */ FloatT grave_x = 0.0f;
/* 74:90 */ FloatT grave_y = 0.0f;
/* 78:94 */ FloatT grave_z = 0.0f;
/* 7C:98 */ pstring<TextEncoding::ASCII, 0x14> grave_team;
/* 90:AC */ pstring<TextEncoding::ASCII, 0x20> grave_message;
/* B0:CC */ parray<uint8_t, 4> unknown_m5;
@@ -386,10 +398,17 @@ struct PlayerRecordsBB_Challenge {
/* 0004 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
/* 0028 */ parray<le_uint32_t, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
/* 003C */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
/* 0060 */ parray<uint8_t, 4> unknown_g3;
/* 0060 */ uint8_t grave_is_ep2 = 0;
/* 0061 */ uint8_t grave_stage_num = 0;
/* 0062 */ uint8_t grave_floor = 0;
/* 0063 */ uint8_t unknown_g0 = 0;
/* 0064 */ le_uint16_t grave_deaths = 0;
/* 0066 */ parray<uint8_t, 2> unknown_u4;
/* 0068 */ parray<le_uint32_t, 5> grave_coords_time;
/* 0068 */ le_uint32_t grave_time = 0; // Encoded as in PlayerRecordsDCPC_Challenge
/* 006C */ le_uint32_t unknown_g1 = 0;
/* 0070 */ le_float grave_x = 0.0f;
/* 0074 */ le_float grave_y = 0.0f;
/* 0078 */ le_float grave_z = 0.0f;
/* 007C */ pstring<TextEncoding::UTF16, 0x14> grave_team;
/* 00A4 */ pstring<TextEncoding::UTF16, 0x20> grave_message;
/* 00E4 */ parray<uint8_t, 4> unknown_m5;
@@ -428,7 +447,7 @@ struct PlayerRecords_Battle {
template <typename ItemIDT>
struct ChoiceSearchConfig {
// 0 = enabled, 1 = disabled. Unused for command C3
le_uint32_t choice_search_disabled = 0;
le_uint32_t disabled = 1;
struct Entry {
ItemIDT parent_category_id = 0;
ItemIDT category_id = 0;
@@ -592,3 +611,27 @@ struct ChallengeTemplateDefinition {
};
const ChallengeTemplateDefinition& get_challenge_template_definition(GameVersion version, uint32_t class_flags, size_t index);
struct SymbolChat {
// Bits: ----------------------DMSSSCCCFF
// S = sound, C = face color, F = face shape, D = capture, M = mute sound
/* 00 */ le_uint32_t spec = 0;
// Corner objects are specified in reading order ([0] is the top-left one).
// Bits (each entry): ---VHCCCZZZZZZZZ
// V = reverse vertical, H = reverse horizontal, C = color, Z = object
// If Z is all 1 bits (0xFF), no corner object is rendered.
/* 04 */ parray<le_uint16_t, 4> corner_objects;
struct FacePart {
uint8_t type = 0xFF; // FF = no part in this slot
uint8_t x = 0;
uint8_t y = 0;
// Bits: ------VH (V = reverse vertical, H = reverse horizontal)
uint8_t flags = 0;
} __attribute__((packed));
/* 0C */ parray<FacePart, 12> face_parts;
/* 3C */
SymbolChat();
} __attribute__((packed));
+138 -118
View File
@@ -1005,9 +1005,7 @@ static void on_93_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
string version_string = is_old_format
? cmd.var.old_client_config.as_string()
: cmd.var.new_clients.client_config.as_string();
print_data(stderr, version_string);
strip_trailing_zeroes(version_string);
// Note: Tethealla PSOBB is actually Japanese PSOBB, but with most of the
// files replaced with English text/graphics/etc. For this reason, it still
// reports its language as Japanese, so we have to account for that
@@ -1019,7 +1017,7 @@ static void on_93_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
}
c->channel.language = c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB) ? 1 : cmd.language;
c->bb_connection_phase = cmd.connection_phase;
c->game_data.bb_player_index = cmd.character_slot;
c->game_data.bb_character_index = cmd.character_slot;
if (cmd.menu_id == MenuID::LOBBY) {
c->preferred_lobby_id = cmd.preferred_lobby_id;
@@ -1492,7 +1490,7 @@ static void on_CA_Ep3(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags));
for (auto existing_c : l->clients) {
if (existing_c) {
auto existing_p = existing_c->game_data.player();
auto existing_p = existing_c->game_data.character();
PlayerLobbyDataDCGC lobby_data;
lobby_data.name.encode(existing_p->disp.name.decode(existing_c->language()), c->language());
lobby_data.player_tag = 0x00010000;
@@ -1677,7 +1675,7 @@ static void on_09(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
for (size_t x = 0; x < game->max_clients; x++) {
const auto& game_c = game->clients[x];
if (game_c.get()) {
auto player = game_c->game_data.player();
auto player = game_c->game_data.character();
string name = player->disp.name.decode(game_c->language());
if (game->is_ep3()) {
info += string_printf("%zu: $C6%s$C7 L%" PRIu32 "\n",
@@ -1860,12 +1858,12 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q) {
// If an overlay was created, item IDs need to be assigned
if (lc->game_data.has_overlay()) {
auto overlay = lc->game_data.player();
auto overlay = lc->game_data.character();
for (size_t z = 0; z < overlay->inventory.num_items; z++) {
overlay->inventory.items[z].data.id = l->generate_item_id(client_id);
}
lc->log.info("Assigned overlay item IDs");
lc->game_data.player()->print_inventory(stderr, lc->version(), s->item_name_index);
overlay->print_inventory(stderr, lc->version(), s->item_name_index);
}
string bin_filename = vq->bin_filename();
@@ -1955,7 +1953,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
}
case MainMenuItemID::PROXY_DESTINATIONS:
if (!c->game_data.player(false, false)) {
if (!c->game_data.character(false, false)) {
send_get_player_info(c);
}
send_proxy_destinations_menu(c);
@@ -2207,7 +2205,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
send_lobby_message_box(c, "$C6Incorrect password.");
break;
}
auto p = c->game_data.player();
auto p = c->game_data.character();
if (p->disp.stats.level < game->min_level) {
send_lobby_message_box(c, "$C6Your level is too\nlow to join this\ngame.");
break;
@@ -2361,7 +2359,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
break;
}
if (team_name.empty()) {
team_name = c->game_data.player()->disp.name.decode(c->language());
team_name = c->game_data.character()->disp.name.decode(c->language());
team_name += string_printf("/%" PRIX32, c->license->serial_number);
}
auto s = c->require_server_state();
@@ -2726,35 +2724,36 @@ static void on_13_A7_V3_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag
static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, string& data) {
auto s = c->require_server_state();
auto player = c->game_data.player();
auto account = c->game_data.account();
auto player = c->game_data.character();
switch (c->version()) {
case GameVersion::DC: {
if (c->config.check_flag(Client::Flag::IS_DC_V1)) {
const auto& pd = check_size_t<C_CharacterData_DCv1_61_98>(data);
player->inventory = pd.inventory;
player->disp = pd.disp.to_bb(player->inventory.language, player->inventory.language);
const auto& cmd = check_size_t<C_CharacterData_DCv1_61_98>(data);
player->inventory = cmd.inventory;
player->disp = cmd.disp.to_bb(player->inventory.language, player->inventory.language);
} else {
const auto& pd = check_size_t<C_CharacterData_DCv2_61_98>(data, 0xFFFF);
player->inventory = pd.inventory;
player->disp = pd.disp.to_bb(player->inventory.language, player->inventory.language);
player->battle_records = pd.records.battle;
player->challenge_records = pd.records.challenge;
// TODO: Parse choice search config
const auto& cmd = check_size_t<C_CharacterData_DCv2_61_98>(data, 0xFFFF);
player->inventory = cmd.inventory;
player->disp = cmd.disp.to_bb(player->inventory.language, player->inventory.language);
player->battle_records = cmd.records.battle;
player->challenge_records = cmd.records.challenge;
player->choice_search_config = cmd.choice_search_config;
}
break;
}
case GameVersion::PC: {
const auto& pd = check_size_t<C_CharacterData_PC_61_98>(data, 0xFFFF);
player->inventory = pd.inventory;
player->disp = pd.disp.to_bb(player->inventory.language, player->inventory.language);
player->battle_records = pd.records.battle;
player->challenge_records = pd.records.challenge;
// TODO: Parse choice search config
account->blocked_senders = pd.blocked_senders;
if (pd.auto_reply_enabled) {
string auto_reply = data.substr(sizeof(pd));
const auto& cmd = check_size_t<C_CharacterData_PC_61_98>(data, 0xFFFF);
player->inventory = cmd.inventory;
player->disp = cmd.disp.to_bb(player->inventory.language, player->inventory.language);
player->battle_records = cmd.records.battle;
player->challenge_records = cmd.records.challenge;
player->choice_search_config = cmd.choice_search_config;
for (size_t z = 0; z < cmd.blocked_senders.size(); z++) {
c->game_data.blocked_senders.at(z) = cmd.blocked_senders[z];
}
if (cmd.auto_reply_enabled) {
string auto_reply = data.substr(sizeof(cmd));
strip_trailing_zeroes(auto_reply);
if (auto_reply.size() & 1) {
auto_reply.push_back(0);
@@ -2772,9 +2771,9 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
if (!c->config.check_flag(Client::Flag::IS_EPISODE_3)) {
throw runtime_error("non-Episode 3 client sent Episode 3 player data");
}
const auto* pd3 = &check_size_t<C_CharacterData_GC_Ep3_61_98>(data);
c->game_data.ep3_config.reset(new Episode3::PlayerConfig(pd3->ep3_config));
cmd = reinterpret_cast<const C_CharacterData_V3_61_98*>(pd3);
const auto* cmd3 = &check_size_t<C_CharacterData_GC_Ep3_61_98>(data);
c->game_data.ep3_config.reset(new Episode3::PlayerConfig(cmd3->ep3_config));
cmd = reinterpret_cast<const C_CharacterData_V3_61_98*>(cmd3);
} else {
if (c->config.check_flag(Client::Flag::IS_EPISODE_3)) {
c->config.set_flag(Client::Flag::IS_EP3_TRIAL_EDITION);
@@ -2817,7 +2816,9 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
player->challenge_records = cmd->records.challenge;
// TODO: Parse choice search config
player->info_board.encode(cmd->info_board.decode(player->inventory.language), player->inventory.language);
account->blocked_senders = cmd->blocked_senders;
for (size_t z = 0; z < cmd->blocked_senders.size(); z++) {
c->game_data.blocked_senders.at(z) = cmd->blocked_senders[z];
}
if (cmd->auto_reply_enabled) {
string auto_reply = data.substr(sizeof(cmd), 0xAC);
strip_trailing_zeroes(auto_reply);
@@ -2836,7 +2837,9 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
player->challenge_records = cmd.records.challenge;
// TODO: Parse choice search config
player->info_board = cmd.info_board;
account->blocked_senders = cmd.blocked_senders;
for (size_t z = 0; z < cmd.blocked_senders.size(); z++) {
c->game_data.blocked_senders.at(z) = cmd.blocked_senders[z];
}
if (cmd.auto_reply_enabled) {
string auto_reply = data.substr(sizeof(cmd), 0xAC);
strip_trailing_zeroes(auto_reply);
@@ -2872,14 +2875,14 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
} else if (command == 0x61) {
if (!c->pending_bb_save_username.empty()) {
string prev_bb_username = c->game_data.bb_username;
size_t prev_bb_player_index = c->game_data.bb_player_index;
int8_t prev_bb_character_index = c->game_data.bb_character_index;
c->game_data.bb_username = c->pending_bb_save_username;
c->game_data.bb_player_index = c->pending_bb_save_player_index;
c->game_data.bb_character_index = c->pending_bb_save_character_index;
bool failure = false;
try {
c->game_data.save_player_data();
c->game_data.save_character_file();
} catch (const exception& e) {
send_text_message_printf(c, "$C6PSOBB player data could\nnot be saved:\n%s", e.what());
failure = true;
@@ -2888,12 +2891,12 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
if (!failure) {
send_text_message_printf(c,
"$C6BB player data saved\nas player %hhu for user\n%s",
static_cast<uint8_t>(c->pending_bb_save_player_index + 1),
static_cast<uint8_t>(c->pending_bb_save_character_index + 1),
c->pending_bb_save_username.c_str());
}
c->game_data.bb_username = prev_bb_username;
c->game_data.bb_player_index = prev_bb_player_index;
c->game_data.bb_character_index = prev_bb_character_index;
c->pending_bb_save_username.clear();
}
@@ -2951,7 +2954,7 @@ static void on_06(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
return;
}
auto p = c->game_data.player();
auto p = c->game_data.character();
string from_name = p->disp.name.decode(c->language());
if (from_name.size() >= 2 && from_name[0] == '\t' && (from_name[1] == 'E' || from_name[1] == 'J')) {
from_name = from_name.substr(2);
@@ -2990,6 +2993,8 @@ static void on_E0_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
static void on_E3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
const auto& cmd = check_size_t<C_PlayerPreviewRequest_BB_E3>(data);
c->game_data.bb_character_index = cmd.character_index;
if (c->bb_connection_phase != 0x00) {
send_approve_player_choice_bb(c);
@@ -3002,15 +3007,16 @@ static void on_E3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
ClientGameData temp_gd;
temp_gd.guild_card_number = c->license->serial_number;
temp_gd.bb_username = c->license->bb_username;
temp_gd.bb_player_index = cmd.player_index;
temp_gd.bb_character_index = cmd.character_index;
try {
auto preview = temp_gd.player()->disp.to_preview();
send_player_preview_bb(c, cmd.player_index, &preview);
auto preview = temp_gd.character()->disp.to_preview();
send_player_preview_bb(c, cmd.character_index, &preview);
} catch (const exception& e) {
// Player doesn't exist
send_player_preview_bb(c, cmd.player_index, nullptr);
c->log.warning("Can\'t load character data: %s", e.what());
send_player_preview_bb(c, cmd.character_index, nullptr);
}
}
}
@@ -3018,10 +3024,12 @@ static void on_E3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
static void on_E8_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& data) {
constexpr size_t max_count = sizeof(PSOBBGuildCardFile::entries) / sizeof(PSOBBGuildCardFile::Entry);
constexpr size_t max_blocked = sizeof(PSOBBGuildCardFile::blocked) / sizeof(GuildCardBB);
auto gcf = c->game_data.guild_cards();
bool should_save = false;
switch (command) {
case 0x01E8: { // Check guild card file checksum
const auto& cmd = check_size_t<C_GuildCardChecksum_01E8>(data);
uint32_t checksum = c->game_data.account()->guild_card_file.checksum();
uint32_t checksum = gcf->checksum();
c->log.info("(Guild card file) Server checksum = %08" PRIX32 ", client checksum = %08" PRIX32,
checksum, cmd.checksum.load());
S_GuildCardChecksumResponse_BB_02E8 response = {
@@ -3035,13 +3043,12 @@ static void on_E8_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
break;
case 0x04E8: { // Add guild card
auto& new_gc = check_size_t<GuildCardBB>(data);
auto& gcf = c->game_data.account()->guild_card_file;
for (size_t z = 0; z < max_count; z++) {
if (!gcf.entries[z].data.present) {
gcf.entries[z].data = new_gc;
gcf.entries[z].unknown_a1.clear(0);
c->log.info("Added guild card %" PRIu32 " at position %zu",
new_gc.guild_card_number.load(), z);
if (!gcf->entries[z].data.present) {
gcf->entries[z].data = new_gc;
gcf->entries[z].unknown_a1.clear(0);
c->log.info("Added guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z);
should_save = true;
break;
}
}
@@ -3049,15 +3056,14 @@ static void on_E8_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
}
case 0x05E8: { // Delete guild card
auto& cmd = check_size_t<C_DeleteGuildCard_BB_05E8_08E8>(data);
auto& gcf = c->game_data.account()->guild_card_file;
for (size_t z = 0; z < max_count; z++) {
if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) {
c->log.info("Deleted guild card %" PRIu32 " at position %zu",
cmd.guild_card_number.load(), z);
if (gcf->entries[z].data.guild_card_number == cmd.guild_card_number) {
c->log.info("Deleted guild card %" PRIu32 " at position %zu", cmd.guild_card_number.load(), z);
for (z = 0; z < max_count - 1; z++) {
gcf.entries[z] = gcf.entries[z + 1];
gcf->entries[z] = gcf->entries[z + 1];
}
gcf.entries[max_count - 1].clear();
gcf->entries[max_count - 1].clear();
should_save = true;
break;
}
}
@@ -3065,26 +3071,24 @@ static void on_E8_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
}
case 0x06E8: { // Update guild card
auto& new_gc = check_size_t<GuildCardBB>(data);
auto& gcf = c->game_data.account()->guild_card_file;
for (size_t z = 0; z < max_count; z++) {
if (gcf.entries[z].data.guild_card_number == new_gc.guild_card_number) {
gcf.entries[z].data = new_gc;
c->log.info("Updated guild card %" PRIu32 " at position %zu",
new_gc.guild_card_number.load(), z);
if (gcf->entries[z].data.guild_card_number == new_gc.guild_card_number) {
gcf->entries[z].data = new_gc;
c->log.info("Updated guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z);
should_save = true;
}
}
break;
}
case 0x07E8: { // Add blocked user
auto& new_gc = check_size_t<GuildCardBB>(data);
auto& gcf = c->game_data.account()->guild_card_file;
for (size_t z = 0; z < max_blocked; z++) {
if (!gcf.blocked[z].present) {
gcf.blocked[z] = new_gc;
c->log.info("Added blocked guild card %" PRIu32 " at position %zu",
new_gc.guild_card_number.load(), z);
if (!gcf->blocked[z].present) {
gcf->blocked[z] = new_gc;
c->log.info("Added blocked guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z);
// Note: The client also sends a C6 command, so we don't have to
// manually sync the actual blocked senders list here
should_save = true;
break;
}
}
@@ -3092,17 +3096,17 @@ static void on_E8_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
}
case 0x08E8: { // Delete blocked user
auto& cmd = check_size_t<C_DeleteGuildCard_BB_05E8_08E8>(data);
auto& gcf = c->game_data.account()->guild_card_file;
for (size_t z = 0; z < max_blocked; z++) {
if (gcf.blocked[z].guild_card_number == cmd.guild_card_number) {
if (gcf->blocked[z].guild_card_number == cmd.guild_card_number) {
c->log.info("Deleted blocked guild card %" PRIu32 " at position %zu",
cmd.guild_card_number.load(), z);
for (z = 0; z < max_blocked - 1; z++) {
gcf.blocked[z] = gcf.blocked[z + 1];
gcf->blocked[z] = gcf->blocked[z + 1];
}
gcf.blocked[max_blocked - 1].clear();
gcf->blocked[max_blocked - 1].clear();
// Note: The client also sends a C6 command, so we don't have to
// manually sync the actual blocked senders list here
should_save = true;
break;
}
}
@@ -3110,12 +3114,11 @@ static void on_E8_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
}
case 0x09E8: { // Write comment
auto& cmd = check_size_t<C_WriteGuildCardComment_BB_09E8>(data);
auto& gcf = c->game_data.account()->guild_card_file;
for (size_t z = 0; z < max_count; z++) {
if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) {
gcf.entries[z].comment = cmd.comment;
c->log.info("Updated comment on guild card %" PRIu32 " at position %zu",
cmd.guild_card_number.load(), z);
if (gcf->entries[z].data.guild_card_number == cmd.guild_card_number) {
gcf->entries[z].comment = cmd.comment;
c->log.info("Updated comment on guild card %" PRIu32 " at position %zu", cmd.guild_card_number.load(), z);
should_save = true;
break;
}
}
@@ -3123,34 +3126,36 @@ static void on_E8_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
}
case 0x0AE8: { // Move guild card in list
auto& cmd = check_size_t<C_MoveGuildCard_BB_0AE8>(data);
auto& gcf = c->game_data.account()->guild_card_file;
if (cmd.position >= max_count) {
throw invalid_argument("invalid new position");
}
size_t index;
for (index = 0; index < max_count; index++) {
if (gcf.entries[index].data.guild_card_number == cmd.guild_card_number) {
if (gcf->entries[index].data.guild_card_number == cmd.guild_card_number) {
break;
}
}
if (index >= max_count) {
throw invalid_argument("player does not have requested guild card");
}
auto moved_gc = gcf.entries[index];
auto moved_gc = gcf->entries[index];
for (; index < cmd.position; index++) {
gcf.entries[index] = gcf.entries[index + 1];
gcf->entries[index] = gcf->entries[index + 1];
}
for (; index > cmd.position; index--) {
gcf.entries[index] = gcf.entries[index - 1];
gcf->entries[index] = gcf->entries[index - 1];
}
gcf.entries[index] = moved_gc;
c->log.info("Moved guild card %" PRIu32 " to position %zu",
cmd.guild_card_number.load(), index);
gcf->entries[index] = moved_gc;
c->log.info("Moved guild card %" PRIu32 " to position %zu", cmd.guild_card_number.load(), index);
should_save = true;
break;
}
default:
throw invalid_argument("invalid command");
}
if (should_save) {
c->game_data.save_guild_card_file();
}
}
static void on_DC_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
@@ -3184,15 +3189,17 @@ static void on_E5_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
return;
}
if (c->game_data.player(false).get()) {
if (c->game_data.character(false).get()) {
throw runtime_error("player already exists");
}
c->game_data.bb_player_index = cmd.player_index;
c->game_data.bb_character_index = -1;
c->game_data.system(); // Ensure system file is loaded
c->game_data.bb_character_index = cmd.character_index;
if (c->bb_connection_phase == 0x03) { // Dressing room
try {
c->game_data.player()->disp.apply_dressing_room(cmd.preview);
c->game_data.character()->disp.apply_dressing_room(cmd.preview);
} catch (const exception& e) {
send_message_box(c, string_printf("$C6Character could not be modified:\n%s", e.what()));
return;
@@ -3200,7 +3207,7 @@ static void on_E5_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
} else {
try {
auto s = c->require_server_state();
c->game_data.create_player(cmd.preview, s->level_table);
c->game_data.create_character_file(c->license->serial_number, c->language(), cmd.preview, s->level_table);
} catch (const exception& e) {
send_message_box(c, string_printf("$C6New character could not be created:\n%s", e.what()));
return;
@@ -3212,45 +3219,49 @@ static void on_E5_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
}
static void on_ED_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& data) {
auto p = c->game_data.character();
auto sys = c->game_data.system();
switch (command) {
case 0x01ED: {
const auto& cmd = check_size_t<C_UpdateOptionFlags_BB_01ED>(data);
c->game_data.account()->option_flags = cmd.option_flags;
p->option_flags = cmd.option_flags;
break;
}
case 0x02ED: {
const auto& cmd = check_size_t<C_UpdateSymbolChats_BB_02ED>(data);
c->game_data.account()->symbol_chats = cmd.symbol_chats;
p->symbol_chats = cmd.symbol_chats;
break;
}
case 0x03ED: {
const auto& cmd = check_size_t<C_UpdateChatShortcuts_BB_03ED>(data);
c->game_data.account()->shortcuts = cmd.chat_shortcuts;
p->shortcuts = cmd.chat_shortcuts;
break;
}
case 0x04ED: {
const auto& cmd = check_size_t<C_UpdateKeyConfig_BB_04ED>(data);
c->game_data.account()->system_file.key_config = cmd.key_config;
sys->key_config = cmd.key_config;
c->game_data.save_system_file();
break;
}
case 0x05ED: {
const auto& cmd = check_size_t<C_UpdatePadConfig_BB_05ED>(data);
c->game_data.account()->system_file.joystick_config = cmd.pad_config;
sys->joystick_config = cmd.pad_config;
c->game_data.save_system_file();
break;
}
case 0x06ED: {
const auto& cmd = check_size_t<C_UpdateTechMenu_BB_06ED>(data);
c->game_data.player()->tech_menu_config = cmd.tech_menu;
p->tech_menu_config = cmd.tech_menu;
break;
}
case 0x07ED: {
const auto& cmd = check_size_t<C_UpdateCustomizeMenu_BB_07ED>(data);
c->game_data.player()->disp.config = cmd.customize;
p->disp.config = cmd.customize;
break;
}
case 0x08ED: {
const auto& cmd = check_size_t<C_UpdateChallengeRecords_BB_08ED>(data);
c->game_data.player()->challenge_records = cmd.records;
p->challenge_records = cmd.records;
break;
}
default:
@@ -3259,22 +3270,26 @@ static void on_ED_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
}
static void on_E7_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
const auto& cmd = check_size_t<SC_SyncCharacterSaveFile_BB_00E7>(data);
const auto& cmd = check_size_t<SC_SyncSaveFiles_BB_E7>(data);
// We only trust the player's quest data and challenge data.
// TODO: In the future, we shouldn't even need to trust these fields. We
// should instead verify our copy of the player against what the client sent,
// and alert on anything that's out of sync.
// TODO: In the future, we should save battle records here too.
auto p = c->game_data.player();
p->challenge_records = cmd.challenge_records;
p->quest_data1 = cmd.quest_data1;
p->quest_data2 = cmd.quest_data2;
// TODO: In the future, we shouldn't need to trust any of the client's data
// here. We should instead verify our copy of the player against what the
// client sent, and alert on anything that's out of sync.
auto p = c->game_data.character();
p->challenge_records = cmd.char_file.challenge_records;
p->battle_records = cmd.char_file.battle_records;
p->death_count = cmd.char_file.death_count;
*c->game_data.system() = cmd.system_file;
}
static void on_E2_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
auto& cmd = check_size_t<PSOBBSystemFile>(data);
c->game_data.account()->system_file = cmd;
auto sys = c->game_data.system();
*sys = cmd;
c->game_data.save_system_file();
S_SystemFileCreated_00E1_BB out_cmd = {1};
send_command_t(c, 0x00E1, 0x00000000, out_cmd);
}
static void on_DF_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& data) {
@@ -3349,7 +3364,7 @@ static void on_DF_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
case 0x07DF: {
const auto& cmd = check_size_t<C_CreateChallengeModeAwardItem_BB_07DF>(data);
auto p = c->game_data.player(true, false);
auto p = c->game_data.character(true, false);
auto& award_state = (l->episode == Episode::EP2)
? p->challenge_records.ep2_online_award_state
: p->challenge_records.ep1_online_award_state;
@@ -3434,14 +3449,14 @@ static void on_81(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
} else {
// If the sender is blocked, don't forward the mail
for (size_t y = 0; y < 30; y++) {
if (target->game_data.account()->blocked_senders.data()[y] == c->license->serial_number) {
if (target->game_data.blocked_senders.data()[y] == c->license->serial_number) {
return;
}
}
// If the target has auto-reply enabled, send the autoreply. Note that we also
// forward the message in this case.
auto target_p = target->game_data.player();
auto target_p = target->game_data.character();
if (!target_p->auto_reply.empty()) {
send_simple_mail(
c,
@@ -3454,7 +3469,7 @@ static void on_81(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
send_simple_mail(
target,
c->license->serial_number,
c->game_data.player()->disp.name.decode(c->language()),
c->game_data.character()->disp.name.decode(c->language()),
message);
}
}
@@ -3470,7 +3485,7 @@ void on_D9(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
if (is_w && (data.size() & 1)) {
data.push_back(0);
}
c->game_data.player(true, false)->info_board.encode(tt_decode_marked(data, c->language(), is_w), c->language());
c->game_data.character(true, false)->info_board.encode(tt_decode_marked(data, c->language(), is_w), c->language());
}
void on_C7(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
@@ -3479,21 +3494,26 @@ void on_C7(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
if (is_w && (data.size() & 1)) {
data.push_back(0);
}
c->game_data.player(true, false)->auto_reply.encode(tt_decode_marked(data, c->language(), is_w), c->language());
c->game_data.character(true, false)->auto_reply.encode(tt_decode_marked(data, c->language(), is_w), c->language());
}
static void on_C8(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
check_size_v(data.size(), 0);
c->game_data.player(true, false)->auto_reply.clear();
c->game_data.character(true, false)->auto_reply.clear();
}
static void on_C6(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
c->game_data.blocked_senders.fill(0);
if (c->version() == GameVersion::BB) {
const auto& cmd = check_size_t<C_SetBlockedSenders_BB_C6>(data);
c->game_data.account()->blocked_senders = cmd.blocked_senders;
for (size_t z = 0; z < cmd.blocked_senders.size(); z++) {
c->game_data.blocked_senders[z] = cmd.blocked_senders[z];
}
} else {
const auto& cmd = check_size_t<C_SetBlockedSenders_V3_C6>(data);
c->game_data.account()->blocked_senders = cmd.blocked_senders;
for (size_t z = 0; z < cmd.blocked_senders.size(); z++) {
c->game_data.blocked_senders[z] = cmd.blocked_senders[z];
}
}
}
@@ -3553,7 +3573,7 @@ shared_ptr<Lobby> create_game_generic(
throw runtime_error("invalid episode");
}
auto p = c->game_data.player();
auto p = c->game_data.character();
if (!(c->license->flags & License::Flag::FREE_JOIN_GAMES) &&
(min_level > p->disp.stats.level)) {
// Note: We don't throw here because this is a situation players might
+51 -52
View File
@@ -430,22 +430,22 @@ static void on_send_guild_card(shared_ptr<Client> c, uint8_t command, uint8_t fl
switch (c->version()) {
case GameVersion::DC: {
const auto& cmd = check_size_t<G_SendGuildCard_DC_6x06>(data, size);
c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language());
c->game_data.character(true, false)->guild_card.description.encode(cmd.guild_card.description.decode(c->language()), c->language());
break;
}
case GameVersion::PC: {
const auto& cmd = check_size_t<G_SendGuildCard_PC_6x06>(data, size);
c->game_data.player(true, false)->guild_card_description = cmd.guild_card.description;
c->game_data.character(true, false)->guild_card.description = cmd.guild_card.description;
break;
}
case GameVersion::GC: {
const auto& cmd = check_size_t<G_SendGuildCard_GC_6x06>(data, size);
c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language());
c->game_data.character(true, false)->guild_card.description.encode(cmd.guild_card.description.decode(c->language()), c->language());
break;
}
case GameVersion::XB: {
const auto& cmd = check_size_t<G_SendGuildCard_XB_6x06>(data, size);
c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language());
c->game_data.character(true, false)->guild_card.description.encode(cmd.guild_card.description.decode(c->language()), c->language());
break;
}
case GameVersion::BB:
@@ -522,7 +522,7 @@ static void on_word_select_t(shared_ptr<Client> c, uint8_t command, uint8_t, con
}
} catch (const exception& e) {
string name = c->game_data.player()->disp.name.decode(c->language());
string name = c->game_data.character()->disp.name.decode(c->language());
lc->log.warning("Untranslatable Word Select message: %s", e.what());
send_text_message_printf(lc, "$C4Untranslatable Word\nSelect message from\n%s", name.c_str());
}
@@ -576,7 +576,7 @@ static void on_player_died(shared_ptr<Client> c, uint8_t command, uint8_t flag,
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
try {
auto& inventory = c->game_data.player()->inventory;
auto& inventory = c->game_data.character()->inventory;
size_t mag_index = inventory.find_equipped_mag();
auto& data = inventory.items[mag_index].data;
data.data2[0] = max<int8_t>(static_cast<int8_t>(data.data2[0] - 5), 0);
@@ -709,7 +709,7 @@ static void on_player_drop_item(shared_ptr<Client> c, uint8_t command, uint8_t f
auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto p = c->game_data.player();
auto p = c->game_data.character();
auto item = p->remove_item(cmd.item_id, 0, c->version() != GameVersion::BB);
l->add_item(item, cmd.area, cmd.x, cmd.z);
@@ -768,7 +768,7 @@ static void on_create_inventory_item_t(shared_ptr<Client> c, uint8_t command, ui
auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto p = c->game_data.player();
auto p = c->game_data.character();
ItemData item = cmd.item_data;
item.decode_for_version(c->version());
l->on_item_id_generated_externally(item.id);
@@ -827,7 +827,7 @@ static void on_drop_partial_stack_t(shared_ptr<Client> c, uint8_t command, uint8
string name = s->describe_item(c->version(), item, true);
send_text_message_printf(c, "$C5SPLIT %08" PRIX32 "\n%s", item.id.load(), name.c_str());
}
c->game_data.player()->print_inventory(stderr, c->version(), s->item_name_index);
c->game_data.character()->print_inventory(stderr, c->version(), s->item_name_index);
}
forward_subcommand_with_item_transcode_t(c, command, flag, cmd);
@@ -856,7 +856,7 @@ static void on_drop_partial_stack_bb(shared_ptr<Client> c, uint8_t command, uint
throw logic_error("item tracking not enabled in BB game");
}
auto p = c->game_data.player();
auto p = c->game_data.character();
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB);
// If a stack was split, the original item still exists, so the dropped item
@@ -905,7 +905,7 @@ static void on_buy_shop_item(shared_ptr<Client> c, uint8_t command, uint8_t flag
}
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto p = c->game_data.player();
auto p = c->game_data.character();
ItemData item = cmd.item_data;
item.data2d = 0; // Clear the price field
item.decode_for_version(c->version());
@@ -1004,7 +1004,7 @@ static void on_pick_up_item(shared_ptr<Client> c, uint8_t command, uint8_t flag,
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto s = c->require_server_state();
auto effective_p = effective_c->game_data.player();
auto effective_p = effective_c->game_data.character();
// It seems the client just plays it fast and loose with these commands.
// There can be multiple 6x5A (request to pick up item) commands in flight,
@@ -1072,7 +1072,7 @@ static void on_pick_up_item_request(shared_ptr<Client> c, uint8_t command, uint8
throw logic_error("item tracking not enabled in BB game");
}
auto p = c->game_data.player();
auto p = c->game_data.character();
auto item = l->remove_item(cmd.item_id);
p->add_item(item);
@@ -1107,7 +1107,7 @@ static void on_equip_unequip_item(shared_ptr<Client> c, uint8_t command, uint8_t
auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto p = c->game_data.player();
auto p = c->game_data.character();
size_t index = p->inventory.find_item(cmd.item_id);
if (cmd.header.subcommand == 0x25) { // Equip
p->inventory.items[index].flags |= 0x00000008;
@@ -1135,7 +1135,7 @@ static void on_use_item(
auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto s = c->require_server_state();
auto p = c->game_data.player();
auto p = c->game_data.character();
size_t index = p->inventory.find_item(cmd.item_id);
string name, colored_name;
{
@@ -1173,7 +1173,7 @@ static void on_feed_mag(
auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto s = c->require_server_state();
auto p = c->game_data.player();
auto p = c->game_data.character();
size_t mag_index = p->inventory.find_item(cmd.mag_item_id);
size_t fed_index = p->inventory.find_item(cmd.fed_item_id);
@@ -1228,7 +1228,7 @@ static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr<Client> c, uint8_t com
}
auto s = c->require_server_state();
size_t level = c->game_data.player()->disp.stats.level + 1;
size_t level = c->game_data.character()->disp.stats.level + 1;
switch (cmd.shop_type) {
case 0:
c->game_data.shop_contents[0] = l->item_creator->generate_tool_shop_contents(level);
@@ -1275,7 +1275,7 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
throw logic_error("item tracking not enabled in BB game");
}
auto p = c->game_data.player();
auto p = c->game_data.character();
if (cmd.action == 0) { // Deposit
if (cmd.item_id == 0xFFFFFFFF) { // Deposit Meseta
if (cmd.meseta_amount > p->disp.stats.meseta) {
@@ -1299,7 +1299,7 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
string name = s->item_name_index->describe_item(GameVersion::BB, item);
l->log.info("Player %hu deposited item %08" PRIX32 " (x%hhu) (%s) in the bank",
c->lobby_client_id, cmd.item_id.load(), cmd.item_amount, name.c_str());
c->game_data.player()->print_inventory(stderr, c->version(), s->item_name_index);
c->game_data.character()->print_inventory(stderr, c->version(), s->item_name_index);
}
} else if (cmd.action == 1) { // Take
@@ -1326,7 +1326,7 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
string name = s->item_name_index->describe_item(GameVersion::BB, item);
l->log.info("Player %hu withdrew item %08" PRIX32 " (x%hhu) (%s) from the bank",
c->lobby_client_id, cmd.item_id.load(), cmd.item_amount, name.c_str());
c->game_data.player()->print_inventory(stderr, c->version(), s->item_name_index);
c->game_data.character()->print_inventory(stderr, c->version(), s->item_name_index);
}
}
@@ -1344,7 +1344,7 @@ static void on_sort_inventory_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
throw logic_error("item tracking not enabled in BB game");
}
auto p = c->game_data.player();
auto p = c->game_data.character();
// Make sure the set of item IDs passed in by the client exactly matches the
// set of item IDs present in the inventory
@@ -1495,17 +1495,16 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
}
// TODO: Should we allow overlays here?
auto p = c->game_data.player(true, false);
auto p = c->game_data.character(true, false);
// The client explicitly checks for both 0 and 1 - any other value means no
// operation is performed.
size_t bit_index = (difficulty << 10) + flag_index;
size_t byte_index = bit_index >> 3;
uint8_t mask = 0x80 >> (bit_index & 7);
size_t byte_index = flag_index >> 3;
uint8_t mask = 0x80 >> (flag_index & 7);
if (action == 0) {
p->quest_data1[byte_index] |= mask;
p->quest_flags[difficulty][byte_index] |= mask;
} else if (action == 1) {
p->quest_data1[byte_index] &= (~mask);
p->quest_flags[difficulty][byte_index] &= (~mask);
}
forward_subcommand(c, command, flag, data, size);
@@ -1655,7 +1654,7 @@ static void on_charge_attack_bb(shared_ptr<Client> c, uint8_t command, uint8_t f
forward_subcommand(c, command, flag, data, size);
const auto& cmd = check_size_t<G_ChargeAttack_BB_6xC7>(data, size);
auto& disp = c->game_data.player()->disp;
auto& disp = c->game_data.character()->disp;
if (cmd.meseta_amount > disp.stats.meseta) {
disp.stats.meseta = 0;
} else {
@@ -1671,7 +1670,7 @@ static void on_level_up(shared_ptr<Client> c, uint8_t command, uint8_t flag, con
return;
}
auto p = c->game_data.player();
auto p = c->game_data.character();
p->disp.stats.char_stats.atp = cmd.atp;
p->disp.stats.char_stats.mst = cmd.mst;
p->disp.stats.char_stats.evp = cmd.evp;
@@ -1685,7 +1684,7 @@ static void on_level_up(shared_ptr<Client> c, uint8_t command, uint8_t flag, con
static void add_player_exp(shared_ptr<Client> c, uint32_t exp) {
auto s = c->require_server_state();
auto p = c->game_data.player();
auto p = c->game_data.character();
p->disp.stats.experience += exp;
send_give_experience(c, exp);
@@ -1720,7 +1719,7 @@ static void on_steal_exp_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void*
const auto& cmd = check_size_t<G_StealEXP_BB_6xC6>(data, size);
auto p = c->game_data.player();
auto p = c->game_data.character();
const auto& enemy = l->map->enemies.at(cmd.enemy_index);
const auto& inventory = p->inventory;
const auto& weapon = inventory.items[inventory.find_equipped_weapon()];
@@ -1821,14 +1820,14 @@ static void on_enemy_killed_bb(shared_ptr<Client> c, uint8_t command, uint8_t fl
send_text_message_printf(c, "$C5+%" PRIu32 " E-%hX %s",
player_exp, cmd.enemy_index.load(), name_for_enum(e.type));
}
if (other_c->game_data.player()->disp.stats.level < 199) {
if (other_c->game_data.character()->disp.stats.level < 199) {
add_player_exp(other_c, player_exp);
}
}
}
// Update kill counts on unsealable items
auto& inventory = c->game_data.player()->inventory;
auto& inventory = c->game_data.character()->inventory;
for (size_t z = 0; z < inventory.num_items; z++) {
auto& item = inventory.items[z];
if ((item.flags & 0x08) &&
@@ -1841,7 +1840,7 @@ static void on_enemy_killed_bb(shared_ptr<Client> c, uint8_t command, uint8_t fl
void on_meseta_reward_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
const auto& cmd = check_size_t<G_MesetaRewardRequest_BB_6xC9>(data, size);
auto p = c->game_data.player();
auto p = c->game_data.character();
if (cmd.amount < 0) {
if (-cmd.amount > static_cast<int32_t>(p->disp.stats.meseta.load())) {
p->disp.stats.meseta = 0;
@@ -1867,7 +1866,7 @@ void on_item_reward_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, const voi
ItemData item;
item = cmd.item_data;
item.id = l->generate_item_id(0xFF);
c->game_data.player()->add_item(item);
c->game_data.character()->add_item(item);
send_create_inventory_item(c, item);
}
@@ -1884,7 +1883,7 @@ static void on_destroy_inventory_item(shared_ptr<Client> c, uint8_t command, uin
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto s = c->require_server_state();
auto p = c->game_data.player();
auto p = c->game_data.character();
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB);
auto name = s->describe_item(c->version(), item, false);
l->log.info("Player %hhu destroyed inventory item %hu:%08" PRIX32 " (%s)",
@@ -1936,7 +1935,7 @@ static void on_identify_item_bb(shared_ptr<Client> c, uint8_t command, uint8_t f
throw logic_error("received item identify subcommand without item creator present");
}
auto p = c->game_data.player();
auto p = c->game_data.character();
size_t x = p->inventory.find_item(cmd.item_id);
if (p->inventory.items[x].data.data1[0] != 0) {
return; // Only weapons can be identified
@@ -1968,7 +1967,7 @@ static void on_accept_identify_item_bb(shared_ptr<Client> c, uint8_t command, ui
if (c->game_data.identify_result.id != cmd.item_id) {
throw runtime_error("accepted item ID does not match previous identify request");
}
c->game_data.player()->add_item(c->game_data.identify_result);
c->game_data.character()->add_item(c->game_data.identify_result);
send_create_inventory_item(c, c->game_data.identify_result);
c->game_data.identify_result.clear();
@@ -1988,7 +1987,7 @@ static void on_sell_item_at_shop_bb(shared_ptr<Client> c, uint8_t command, uint8
}
auto s = c->require_server_state();
auto p = c->game_data.player();
auto p = c->game_data.character();
auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB);
size_t price = (s->item_parameter_table_for_version(c->version())->price_for_item(item) >> 3) * cmd.amount;
p->add_meseta(price);
@@ -2025,7 +2024,7 @@ static void on_buy_shop_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, const vo
size_t price = item.data2d * cmd.amount;
item.data2d = 0;
auto p = c->game_data.player();
auto p = c->game_data.character();
p->remove_meseta(price, false);
item.id = cmd.inventory_item_id;
@@ -2048,7 +2047,7 @@ static void on_buy_shop_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, const vo
static void on_medical_center_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void*, size_t) {
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB)) {
c->game_data.player()->remove_meseta(10, false);
c->game_data.character()->remove_meseta(10, false);
}
}
@@ -2089,7 +2088,7 @@ static void on_battle_level_up_bb(shared_ptr<Client> c, uint8_t, uint8_t, const
auto lc = l->clients[cmd.header.client_id];
if (lc) {
auto s = c->require_server_state();
auto lp = lc->game_data.player();
auto lp = lc->game_data.character();
uint32_t target_level = lp->disp.stats.level + cmd.num_levels;
uint32_t before_exp = lp->disp.stats.experience;
lp->disp.stats.advance_to_level(lp->disp.visual.char_class, target_level, s->level_table);
@@ -2107,7 +2106,7 @@ static void on_quest_exchange_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, co
const auto& cmd = check_size_t<G_ExchangeItemInQuest_BB_6xD5>(data, size);
try {
auto p = c->game_data.player();
auto p = c->game_data.character();
size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier());
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false);
@@ -2134,7 +2133,7 @@ static void on_wrap_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void*
if (l->is_game() && (l->base_version == GameVersion::BB)) {
const auto& cmd = check_size_t<G_WrapItem_BB_6xD6>(data, size);
auto p = c->game_data.player();
auto p = c->game_data.character();
auto item = p->remove_item(cmd.item.id, 1, false);
send_destroy_item(c, item.id, 1);
item.wrap();
@@ -2149,7 +2148,7 @@ static void on_photon_drop_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, c
const auto& cmd = check_size_t<G_PaganiniPhotonDropExchange_BB_6xD7>(data, size);
try {
auto p = c->game_data.player();
auto p = c->game_data.character();
size_t found_index = p->inventory.find_item_by_primary_identifier(0x031000);
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, false);
@@ -2175,7 +2174,7 @@ static void on_photon_crystal_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) {
check_size_t<G_BlackPaperDealPhotonCrystalExchange_BB_6xDF>(data, size);
auto p = c->game_data.player();
auto p = c->game_data.character();
size_t index = p->inventory.find_item_by_primary_identifier(0x031002);
auto item = p->remove_item(p->inventory.items[index].data.id, 1, false);
send_destroy_item(c, item.id, 1);
@@ -2187,7 +2186,7 @@ static void on_momoka_item_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, c
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) {
const auto& cmd = check_size_t<G_MomokaItemExchange_BB_6xD9>(data, size);
auto p = c->game_data.player();
auto p = c->game_data.character();
try {
size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier());
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false);
@@ -2216,7 +2215,7 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr<Client> c, uint8_t, uint8_
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) {
const auto& cmd = check_size_t<G_UpgradeWeaponAttribute_BB_6xDA>(data, size);
auto p = c->game_data.player();
auto p = c->game_data.character();
try {
size_t item_index = p->inventory.find_item(cmd.item_id);
auto& item = p->inventory.items[item_index].data;
@@ -2266,9 +2265,9 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr<Client> c, uint8_t, uint8_
}
}
static void on_write_quest_data2_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
const auto& cmd = check_size_t<G_SetQuestData2_BB_6xD2>(data, size);
c->game_data.player()->quest_data2[cmd.index] = cmd.value;
static void on_write_quest_global_flag_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
const auto& cmd = check_size_t<G_SetQuestGlobalFlag_BB_6xD2>(data, size);
c->game_data.character()->quest_global_flags[cmd.index] = cmd.value;
}
////////////////////////////////////////////////////////////////////////////////
@@ -2486,7 +2485,7 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6xCF */ on_battle_restart_bb,
/* 6xD0 */ on_battle_level_up_bb,
/* 6xD1 */ nullptr,
/* 6xD2 */ on_write_quest_data2_bb,
/* 6xD2 */ on_write_quest_global_flag_bb,
/* 6xD3 */ nullptr,
/* 6xD4 */ nullptr,
/* 6xD5 */ on_quest_exchange_item_bb,
+285
View File
@@ -3,8 +3,15 @@
#include <stdexcept>
#include <string>
#include "PSOProtocol.hh"
using namespace std;
// Originally there was going to be a language-based header, but then I decided
// against it. This string was already in use for that parser, so I didn't
// bother changing it.
const char* LegacySavedAccountDataBB::SIGNATURE = "newserv account file format; 7 sections present; sequential;";
ShuffleTables::ShuffleTables(PSOV2Encryption& crypt) {
for (size_t x = 0; x < 0x100; x++) {
this->forward_table[x] = x;
@@ -189,3 +196,281 @@ void PSOBBGuildCardFile::Entry::clear() {
uint32_t PSOBBGuildCardFile::checksum() const {
return crc32(this, sizeof(*this));
}
PSOBBSystemFile::PSOBBSystemFile(uint32_t guild_card_number)
: PSOBBSystemFile() {
// This field is based on 1/1/2000, not 1/1/1970, so adjust appropriately
this->base.creation_timestamp = (now() - 946684800000000ULL) / 1000000;
for (size_t z = 0; z < PSOBBSystemFile::DEFAULT_KEY_CONFIG.size(); z++) {
this->key_config[z] = PSOBBSystemFile::DEFAULT_KEY_CONFIG[z];
}
for (size_t z = 0; z < PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG.size(); z++) {
this->joystick_config[z] = PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG[z];
}
this->guild_card_number = guild_card_number;
}
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> level_table) {
shared_ptr<PSOBBCharacterFile> ret(new PSOBBCharacterFile());
ret->disp.apply_preview(preview);
ret->disp.stats.reset_to_base(ret->disp.visual.char_class, level_table);
ret->inventory.language = language;
ret->guild_card.guild_card_number = guild_card_number;
ret->guild_card.name = ret->disp.name;
ret->guild_card.present = 1;
ret->guild_card.language = ret->inventory.language;
ret->guild_card.section_id = ret->disp.visual.section_id;
ret->guild_card.char_class = ret->disp.visual.char_class;
for (size_t z = 0; z < PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS.size(); z++) {
ret->symbol_chats[z] = PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS[z].to_entry();
}
for (size_t z = 0; z < PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG.size(); z++) {
ret->tech_menu_config[z] = PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG[z];
}
return ret;
}
PSOBBCharacterFile::SymbolChatEntry PSOBBCharacterFile::DefaultSymbolChatEntry::to_entry() const {
SymbolChatEntry ret;
ret.present = 1;
ret.name.encode(this->name, 1);
ret.data.spec = this->spec;
for (size_t z = 0; z < 4; z++) {
ret.data.corner_objects[z] = this->corner_objects[z];
}
for (size_t z = 0; z < 12; z++) {
ret.data.face_parts[z] = this->face_parts[z];
}
return ret;
}
// TODO: Eliminate duplication between this function and the parallel function
// in PlayerBank
void PSOBBCharacterFile::add_item(const ItemData& item) {
uint32_t pid = item.primary_identifier();
// Annoyingly, meseta is in the disp data, not in the inventory struct. If the
// item is meseta, we have to modify disp instead.
if (pid == MESETA_IDENTIFIER) {
this->add_meseta(item.data2d);
return;
}
// Handle combinable items
size_t combine_max = item.max_stack_size();
if (combine_max > 1) {
// Get the item index if there's already a stack of the same item in the
// player's inventory
size_t y;
for (y = 0; y < this->inventory.num_items; y++) {
if (this->inventory.items[y].data.primary_identifier() == item.primary_identifier()) {
break;
}
}
// If we found an existing stack, add it to the total and return
if (y < this->inventory.num_items) {
size_t new_stack_size = this->inventory.items[y].data.data1[5] + item.data1[5];
if (new_stack_size > combine_max) {
throw out_of_range("stack is too large");
}
this->inventory.items[y].data.data1[5] = new_stack_size;
return;
}
}
// If we get here, then it's not meseta and not a combine item, so it needs to
// go into an empty inventory slot
if (this->inventory.num_items >= 30) {
throw out_of_range("inventory is full");
}
auto& inv_item = this->inventory.items[this->inventory.num_items];
inv_item.present = 1;
inv_item.unknown_a1 = 0;
inv_item.flags = 0;
inv_item.data = item;
this->inventory.num_items++;
}
// TODO: Eliminate code duplication between this function and the parallel
// function in PlayerBank
ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft) {
ItemData ret;
// If we're removing meseta (signaled by an invalid item ID), then create a
// meseta item.
if (item_id == 0xFFFFFFFF) {
this->remove_meseta(amount, allow_meseta_overdraft);
ret.data1[0] = 0x04;
ret.data2d = amount;
return ret;
}
size_t index = this->inventory.find_item(item_id);
auto& inventory_item = this->inventory.items[index];
// If the item is a combine item and are we removing less than we have of it,
// then create a new item and reduce the amount of the existing stack. Note
// that passing amount == 0 means to remove the entire stack, so this only
// applies if amount is nonzero.
if (amount && (inventory_item.data.stack_size() > 1) &&
(amount < inventory_item.data.data1[5])) {
ret = inventory_item.data;
ret.data1[5] = amount;
ret.id = 0xFFFFFFFF;
inventory_item.data.data1[5] -= amount;
return ret;
}
// If we get here, then it's not meseta, and either it's not a combine item or
// we're removing the entire stack. Delete the item from the inventory slot
// and return the deleted item.
ret = inventory_item.data;
this->inventory.num_items--;
for (size_t x = index; x < this->inventory.num_items; x++) {
this->inventory.items[x] = this->inventory.items[x + 1];
}
auto& last_item = this->inventory.items[this->inventory.num_items];
last_item.present = 0;
last_item.unknown_a1 = 0;
last_item.flags = 0;
last_item.data.clear();
return ret;
}
void PSOBBCharacterFile::add_meseta(uint32_t amount) {
this->disp.stats.meseta = min<size_t>(static_cast<size_t>(this->disp.stats.meseta) + amount, 999999);
}
void PSOBBCharacterFile::remove_meseta(uint32_t amount, bool allow_overdraft) {
if (amount <= this->disp.stats.meseta) {
this->disp.stats.meseta -= amount;
} else if (allow_overdraft) {
this->disp.stats.meseta = 0;
} else {
throw out_of_range("player does not have enough meseta");
}
}
uint8_t PSOBBCharacterFile::get_technique_level(uint8_t which) const {
return (this->disp.technique_levels_v1[which] == 0xFF)
? 0xFF
: (this->disp.technique_levels_v1[which] + this->inventory.items[which].extension_data1);
}
void PSOBBCharacterFile::set_technique_level(uint8_t which, uint8_t level) {
if (level == 0xFF) {
this->disp.technique_levels_v1[which] = 0xFF;
this->inventory.items[which].extension_data1 = 0x00;
} else if (level <= 0x0E) {
this->disp.technique_levels_v1[which] = level;
this->inventory.items[which].extension_data1 = 0x00;
} else {
this->disp.technique_levels_v1[which] = 0x0E;
this->inventory.items[which].extension_data1 = level - 0x0E;
}
}
uint8_t PSOBBCharacterFile::get_material_usage(MaterialType which) const {
switch (which) {
case MaterialType::HP:
return this->inventory.hp_from_materials >> 1;
case MaterialType::TP:
return this->inventory.tp_from_materials >> 1;
case MaterialType::POWER:
case MaterialType::MIND:
case MaterialType::EVADE:
case MaterialType::DEF:
case MaterialType::LUCK:
return this->inventory.items[8 + static_cast<uint8_t>(which)].extension_data2;
default:
throw logic_error("invalid material type");
}
}
void PSOBBCharacterFile::set_material_usage(MaterialType which, uint8_t usage) {
switch (which) {
case MaterialType::HP:
this->inventory.hp_from_materials = usage << 1;
break;
case MaterialType::TP:
this->inventory.tp_from_materials = usage << 1;
break;
case MaterialType::POWER:
case MaterialType::MIND:
case MaterialType::EVADE:
case MaterialType::DEF:
case MaterialType::LUCK:
this->inventory.items[8 + static_cast<uint8_t>(which)].extension_data2 = usage;
break;
default:
throw logic_error("invalid material type");
}
}
void PSOBBCharacterFile::clear_all_material_usage() {
this->inventory.hp_from_materials = 0;
this->inventory.tp_from_materials = 0;
for (size_t z = 0; z < 5; z++) {
this->inventory.items[z + 8].extension_data2 = 0;
}
}
void PSOBBCharacterFile::print_inventory(FILE* stream, GameVersion version, shared_ptr<const ItemNameIndex> name_index) const {
fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", this->disp.stats.meseta.load());
fprintf(stream, "[PlayerInventory] %hhu items\n", this->inventory.num_items);
for (size_t x = 0; x < this->inventory.num_items; x++) {
const auto& item = this->inventory.items[x];
auto name = name_index->describe_item(version, item.data);
auto hex = item.data.hex();
fprintf(stream, "[PlayerInventory] %2zu: %s (%s)\n", x, hex.c_str(), name.c_str());
}
}
const std::array<PSOBBCharacterFile::DefaultSymbolChatEntry, 6> PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS = {
DefaultSymbolChatEntry{"\tEHello", 0x28, {0xFFFF, 0x000D, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x05, 0x18, 0x1D, 0x00}, {0x05, 0x28, 0x1D, 0x01}, {0x36, 0x20, 0x2A, 0x00}, {0x3C, 0x00, 0x32, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
DefaultSymbolChatEntry{"\tEGood-bye", 0x74, {0x0476, 0x000C, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x06, 0x15, 0x14, 0x00}, {0x06, 0x2B, 0x14, 0x01}, {0x05, 0x18, 0x1F, 0x00}, {0x05, 0x28, 0x1F, 0x01}, {0x36, 0x20, 0x2A, 0x00}, {0x3C, 0x00, 0x32, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
DefaultSymbolChatEntry{"\tEHurrah!", 0x28, {0x0362, 0x0362, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x09, 0x16, 0x1B, 0x00}, {0x09, 0x2B, 0x1B, 0x01}, {0x37, 0x20, 0x2C, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
DefaultSymbolChatEntry{"\tECrying", 0x74, {0x074F, 0xFFFF, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x06, 0x15, 0x14, 0x00}, {0x06, 0x2B, 0x14, 0x01}, {0x05, 0x18, 0x1F, 0x00}, {0x05, 0x28, 0x1F, 0x01}, {0x21, 0x20, 0x2E, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
DefaultSymbolChatEntry{"\tEI\'m angry!", 0x5C, {0x0116, 0x0001, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x0B, 0x18, 0x1B, 0x01}, {0x0B, 0x28, 0x1B, 0x00}, {0x33, 0x20, 0x2A, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
DefaultSymbolChatEntry{"\tEHelp me!", 0xEC, {0x065E, 0x0138, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x02, 0x17, 0x1B, 0x01}, {0x02, 0x2A, 0x1B, 0x00}, {0x31, 0x20, 0x2C, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
};
const std::array<uint16_t, 20> PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG = {
0x0000, 0x0006, 0x0003, 0x0001, 0x0007, 0x0004, 0x0002, 0x0008, 0x0005, 0x0009,
0x0012, 0x000F, 0x0010, 0x0011, 0x000D, 0x000A, 0x000B, 0x000C, 0x000E, 0x0000};
const std::array<uint8_t, 0x016C> PSOBBSystemFile::DEFAULT_KEY_CONFIG = {
0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4B, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00};
const std::array<uint8_t, 0x0038> PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG = {
0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00};
+139 -23
View File
@@ -11,6 +11,7 @@
#include <string>
#include "Episode3/DataIndexes.hh"
#include "ItemNameIndex.hh"
#include "PSOEncryption.hh"
#include "PlayerSubordinates.hh"
#include "Text.hh"
@@ -120,7 +121,7 @@ struct PSOGCEp3SystemFile {
struct PSOBBSystemFileBase {
/* 0000 */ be_uint32_t checksum = 0;
/* 0004 */ be_int16_t music_volume = -50;
/* 0004 */ be_int16_t music_volume = 0;
/* 0006 */ int8_t sound_volume = 0;
/* 0007 */ uint8_t language = 0;
/* 0008 */ be_uint32_t server_time_delta_frames = 1728000;
@@ -144,6 +145,92 @@ struct PSOBBSystemFile {
/* 02EC */ parray<uint8_t, 0x0800> team_flag;
/* 0AEC */ le_uint32_t team_rewards = 0;
/* 0AF0 */
static const std::array<uint8_t, 0x016C> DEFAULT_KEY_CONFIG;
static const std::array<uint8_t, 0x0038> DEFAULT_JOYSTICK_CONFIG;
PSOBBSystemFile() = default;
explicit PSOBBSystemFile(uint32_t guild_card_number);
} __attribute__((packed));
struct PSOBBCharacterFile {
struct SymbolChatEntry {
/* 00 */ le_uint32_t present = 0;
/* 04 */ pstring<TextEncoding::UTF16, 0x14> name;
/* 2C */ SymbolChat data;
/* 68 */
} __attribute__((packed));
struct DefaultSymbolChatEntry {
const char* name;
uint32_t spec;
std::array<uint16_t, 4> corner_objects;
std::array<SymbolChat::FacePart, 12> face_parts;
SymbolChatEntry to_entry() const;
};
/* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataBB disp;
/* 04DC */ le_uint32_t flags = 0;
/* 04E0 */ le_uint32_t creation_timestamp = 0;
/* 04E4 */ le_uint32_t signature = 0xA205B064;
/* 04E8 */ le_uint32_t play_time_seconds = 0;
/* 04EC */ le_uint32_t option_flags = 0;
/* 04F0 */ parray<uint8_t, 4> unknown_a2;
/* 04F4 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 06F4 */ le_uint32_t death_count = 0;
/* 06F8 */ PlayerBank bank;
/* 19C0 */ GuildCardBB guild_card;
/* 1AC8 */ le_uint32_t unknown_a3 = 0;
/* 1ACC */ parray<SymbolChatEntry, 12> symbol_chats;
/* 1FAC */ parray<uint8_t, 0x0A40> shortcuts;
/* 29EC */ pstring<TextEncoding::UTF16, 0x00AC> auto_reply;
/* 2B44 */ pstring<TextEncoding::UTF16, 0x00AC> info_board;
/* 2C9C */ PlayerRecords_Battle<false> battle_records;
/* 2CB4 */ parray<uint8_t, 4> unknown_a4;
/* 2CB8 */ PlayerRecordsBB_Challenge challenge_records;
/* 2DF8 */ parray<le_uint16_t, 0x0014> tech_menu_config;
/* 2E20 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
/* 2E38 */ parray<uint8_t, 0x0010> unknown_a6;
/* 2E48 */ parray<le_uint32_t, 0x0010> quest_global_flags;
/* 2E88 */ parray<uint8_t, 0x1C> unknown_a7;
/* 2EA4 */
static const std::array<DefaultSymbolChatEntry, 6> DEFAULT_SYMBOL_CHATS;
static const std::array<uint16_t, 20> DEFAULT_TECH_MENU_CONFIG;
PSOBBCharacterFile() = default;
static std::shared_ptr<PSOBBCharacterFile> create_from_preview(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
std::shared_ptr<const LevelTable> level_table);
void add_item(const ItemData& item);
ItemData remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft);
void add_meseta(uint32_t amount);
void remove_meseta(uint32_t amount, bool allow_overdraft);
uint8_t get_technique_level(uint8_t which) const; // Returns FF or 00-1D
void set_technique_level(uint8_t which, uint8_t level);
enum class MaterialType : int8_t {
HP = -2,
TP = -1,
POWER = 0,
MIND = 1,
EVADE = 2,
DEF = 3,
LUCK = 4,
};
uint8_t get_material_usage(MaterialType which) const;
void set_material_usage(MaterialType which, uint8_t usage);
void clear_all_material_usage();
void print_inventory(FILE* stream, GameVersion version, std::shared_ptr<const ItemNameIndex> name_index) const;
} __attribute__((packed));
struct PSOBBGuildCardFile {
@@ -156,10 +243,13 @@ struct PSOBBGuildCardFile {
void clear();
} __attribute__((packed));
PSOBBSystemFileBase system_file;
parray<GuildCardBB, 0x1C> blocked;
parray<uint8_t, 0x180> unknown_a2;
parray<Entry, 0x69> entries;
/* 0000 */ PSOBBSystemFileBase system_file;
/* 0114 */ parray<GuildCardBB, 0x1C> blocked;
/* 1DF4 */ parray<uint8_t, 0x180> unknown_a2;
/* 1F74 */ parray<Entry, 0x69> entries;
/* D590 */
PSOBBGuildCardFile() = default;
uint32_t checksum() const;
} __attribute__((packed));
@@ -167,9 +257,7 @@ struct PSOBBGuildCardFile {
struct PSOGCSaveFileSymbolChatEntry {
/* 00 */ be_uint32_t present;
/* 04 */ pstring<TextEncoding::SJIS, 0x18> name;
/* 1C */ be_uint16_t unused;
/* 1E */ uint8_t flags;
/* 1F */ uint8_t face_spec;
/* 1C */ be_uint32_t spec;
struct CornerObject {
uint8_t type;
uint8_t flags_color;
@@ -188,21 +276,7 @@ struct PSOGCSaveFileSymbolChatEntry {
struct PSOPCSaveFileSymbolChatEntry {
/* 00 */ le_uint32_t present;
/* 04 */ pstring<TextEncoding::UTF16, 0x18> name;
/* 34 */ uint8_t face_spec;
/* 35 */ uint8_t flags;
/* 36 */ be_uint16_t unused;
struct CornerObject {
uint8_t type;
uint8_t flags_color;
} __attribute__((packed));
/* 38 */ parray<CornerObject, 4> corner_objects;
struct FacePart {
uint8_t type;
uint8_t x;
uint8_t y;
uint8_t flags;
} __attribute__((packed));
/* 40 */ parray<FacePart, 12> face_parts;
/* 34 */ SymbolChat data;
/* 70 */
} __attribute__((packed));
@@ -651,3 +725,45 @@ struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD
/* 00440 */ parray<CharacterEntry, 0x80> entries;
/* ECE40 */
} __attribute__((packed));
// This format is specific to newserv and is no longer used, but remains here
// for backward compatibility.
struct LegacySavedPlayerDataBB { // .nsc file format
static constexpr uint64_t SIGNATURE_V0 = 0x6E65777365727620;
static constexpr uint64_t SIGNATURE_V1 = 0xA904332D5CEF0296;
/* 0000 */ be_uint64_t signature = SIGNATURE_V1;
/* 0008 */ parray<uint8_t, 0x20> unused;
/* 0028 */ PlayerRecords_Battle<false> battle_records;
/* 0040 */ PlayerDispDataBBPreview preview;
/* 00BC */ pstring<TextEncoding::UTF16, 0x00AC> auto_reply;
/* 0214 */ PlayerBank bank;
/* 14DC */ PlayerRecordsBB_Challenge challenge_records;
/* 161C */ PlayerDispDataBB disp;
/* 17AC */ pstring<TextEncoding::UTF16, 0x0058> guild_card_description;
/* 185C */ pstring<TextEncoding::UTF16, 0x00AC> info_board;
/* 19B4 */ PlayerInventory inventory;
/* 1D00 */ parray<uint8_t, 4> unknown_a2;
/* 1D04 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 1F04 */ le_uint32_t death_count;
/* 1F08 */ parray<le_uint32_t, 0x0016> quest_global_flags;
/* 1F60 */ parray<le_uint16_t, 0x0014> tech_menu_config;
/* 1F88 */
} __attribute__((packed));
// This format is specific to newserv and is no longer used, but remains here
// for backward compatibility.
struct LegacySavedAccountDataBB { // .nsa file format
static const char* SIGNATURE;
/* 0000 */ pstring<TextEncoding::ASCII, 0x40> signature;
/* 0040 */ parray<le_uint32_t, 0x001E> blocked_senders;
/* 00B8 */ PSOBBGuildCardFile guild_card_file;
/* D648 */ PSOBBSystemFile system_file;
/* E138 */ le_uint32_t unused;
/* E13C */ le_uint32_t option_flags;
/* E140 */ parray<uint8_t, 0x0A40> shortcuts;
/* EB80 */ parray<PSOBBCharacterFile::SymbolChatEntry, 12> symbol_chats;
/* F060 */ pstring<TextEncoding::UTF16, 0x0010> team_name;
/* F080 */
} __attribute__((packed));
+39 -70
View File
@@ -470,25 +470,23 @@ void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
}
void send_system_file_bb(shared_ptr<Client> c) {
send_command_t(c, 0x00E2, 0x00000000, c->game_data.account()->system_file);
send_command_t(c, 0x00E2, 0x00000000, *c->game_data.system());
}
void send_player_preview_bb(shared_ptr<Client> c, uint8_t player_index,
const PlayerDispDataBBPreview* preview) {
void send_player_preview_bb(shared_ptr<Client> c, int8_t character_index, const PlayerDispDataBBPreview* preview) {
if (!preview) {
// no player exists
S_PlayerPreview_NoPlayer_BB_00E4 cmd = {player_index, 0x00000002};
S_PlayerPreview_NoPlayer_BB_00E4 cmd = {character_index, 0x00000002};
send_command_t(c, 0x00E4, 0x00000000, cmd);
} else {
SC_PlayerPreview_CreateCharacter_BB_00E5 cmd = {player_index, *preview};
SC_PlayerPreview_CreateCharacter_BB_00E5 cmd = {character_index, *preview};
send_command_t(c, 0x00E5, 0x00000000, cmd);
}
}
void send_guild_card_header_bb(shared_ptr<Client> c) {
uint32_t checksum = c->game_data.account()->guild_card_file.checksum();
uint32_t checksum = c->game_data.guild_cards()->checksum();
S_GuildCardHeader_BB_01DC cmd = {1, sizeof(PSOBBGuildCardFile), checksum};
send_command_t(c, 0x01DC, 0x00000000, cmd);
}
@@ -505,7 +503,7 @@ void send_guild_card_chunk_bb(shared_ptr<Client> c, size_t chunk_index) {
cmd.unknown = 0;
cmd.chunk_index = chunk_index;
cmd.data.assign_range(
reinterpret_cast<const uint8_t*>(&c->game_data.account()->guild_card_file) + chunk_offset,
reinterpret_cast<const uint8_t*>(c->game_data.guild_cards().get()) + chunk_offset,
data_size, 0);
send_command(c, 0x02DC, 0x00000000, &cmd, sizeof(cmd) - sizeof(cmd.data) + data_size);
@@ -591,50 +589,19 @@ void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
}
void send_approve_player_choice_bb(shared_ptr<Client> c) {
S_ApprovePlayerChoice_BB_00E4 cmd = {c->game_data.bb_player_index, 1};
S_ApprovePlayerChoice_BB_00E4 cmd = {c->game_data.bb_character_index, 1};
send_command_t(c, 0x00E4, 0x00000000, cmd);
}
void send_complete_player_bb(shared_ptr<Client> c) {
auto account = c->game_data.account();
auto player = c->game_data.player(true, false);
auto p = c->game_data.character(true, false);
auto sys = c->game_data.system(true);
if (c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
player->inventory.language = 1;
p->inventory.language = 1;
p->guild_card.language = 1;
sys->base.language = 1;
}
SC_SyncCharacterSaveFile_BB_00E7 cmd;
cmd.inventory = player->inventory;
cmd.disp = player->disp;
cmd.disp.visual.compute_name_color_checksum();
cmd.disp.play_time = 0;
cmd.unknown_a1 = 0;
cmd.creation_timestamp = 0;
cmd.signature = 0xA205B064;
cmd.play_time_seconds = player->disp.play_time;
cmd.option_flags = account->option_flags;
cmd.quest_data1 = player->quest_data1;
cmd.bank = player->bank;
cmd.guild_card.guild_card_number = c->game_data.guild_card_number;
cmd.guild_card.name = player->disp.name;
cmd.guild_card.team_name = account->team_name;
cmd.guild_card.description = player->guild_card_description;
cmd.guild_card.present = 1;
cmd.guild_card.language = cmd.inventory.language;
cmd.guild_card.section_id = player->disp.visual.section_id;
cmd.guild_card.char_class = player->disp.visual.char_class;
cmd.unknown_a3 = 0;
cmd.symbol_chats = account->symbol_chats;
cmd.shortcuts = account->shortcuts;
cmd.auto_reply = player->auto_reply;
cmd.info_board = player->info_board;
cmd.battle_records = player->battle_records;
cmd.unknown_a4.clear(0);
cmd.challenge_records = player->challenge_records;
cmd.tech_menu_config = player->tech_menu_config;
cmd.unknown_a6.clear(0);
cmd.quest_data2 = player->quest_data2;
cmd.system_file = account->system_file;
SC_SyncSaveFiles_BB_E7 cmd = {*p, *sys};
send_command_t(c, 0x00E7, 0x00000000, cmd);
}
@@ -948,7 +915,7 @@ void send_info_board_t(shared_ptr<Client> c) {
if (!other_c.get()) {
continue;
}
auto other_p = other_c->game_data.player(true, false);
auto other_p = other_c->game_data.character(true, false);
auto& e = entries.emplace_back();
e.name.encode(other_p->disp.name.decode(other_p->inventory.language), c->language());
e.message.encode(add_color(other_p->info_board.decode(other_p->inventory.language)), c->language());
@@ -998,7 +965,7 @@ void send_card_search_result_t(
cmd.location_string.encode(location_string, c->language());
cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY;
cmd.extension.lobby_refs[0].item_id = result_lobby->lobby_id;
auto rp = result->game_data.player(true, false);
auto rp = result->game_data.character(true, false);
cmd.extension.player_name.encode(rp->disp.name.decode(rp->inventory.language), c->language());
send_command_t(c, 0x41, 0x00, cmd);
@@ -1135,14 +1102,14 @@ void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
throw runtime_error("source player does not have a license");
}
auto source_p = source->game_data.player(true, false);
auto source_p = source->game_data.character(true, false);
uint32_t guild_card_number = source->license->serial_number;
uint64_t xb_user_id = source->license->xb_user_id
? source->license->xb_user_id
: (0xAE00000000000000 | guild_card_number);
uint8_t language = source_p->inventory.language;
string name = source_p->disp.name.decode(language);
string description = source_p->guild_card_description.decode(language);
string description = source_p->guild_card.description.decode(language);
uint8_t section_id = source_p->disp.visual.section_id;
uint8_t char_class = source_p->disp.visual.char_class;
@@ -1301,7 +1268,9 @@ void send_game_menu_t(
e.flags |= 0x20;
break;
case GameMode::SOLO:
e.flags |= 0x34;
// These should only be visible to other BB clients
e.flags |= 0x04; // Grayed (but not disabled apparently)
e.episode = 0x10 | episode_num;
break;
default:
throw logic_error("invalid game mode");
@@ -1445,7 +1414,7 @@ template <typename EntryT>
void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
vector<EntryT> entries;
auto add_client = [&](shared_ptr<Client> lc) -> void {
auto lp = lc->game_data.player(true, false);
auto lp = lc->game_data.character(true, false);
auto& e = entries.emplace_back();
e.client_id = lc->lobby_client_id;
e.challenge = lp->challenge_records;
@@ -1497,7 +1466,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
if (!wc) {
continue;
}
auto wc_p = wc->game_data.player();
auto wc_p = wc->game_data.character();
auto& p = cmd.players[z];
p.lobby_data.player_tag = 0x00010000;
p.lobby_data.guild_card_number = wc->license->serial_number;
@@ -1564,7 +1533,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
for (size_t z = 4; z < 12; z++) {
if (l->clients[z]) {
auto other_c = l->clients[z];
auto other_p = other_c->game_data.player();
auto other_p = other_c->game_data.character();
auto& cmd_p = cmd.spectator_players[z - 4];
auto& cmd_e = cmd.entries[z];
cmd_p.lobby_data.player_tag = 0x00010000;
@@ -1606,7 +1575,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
cmd.lobby_data[x].player_tag = 0x00010000;
cmd.lobby_data[x].guild_card_number = lc->license->serial_number;
cmd.lobby_data[x].client_id = lc->lobby_client_id;
cmd.lobby_data[x].name.encode(lc->game_data.player()->disp.name.decode(lc->language()), c->language());
cmd.lobby_data[x].name.encode(lc->game_data.character()->disp.name.decode(lc->language()), c->language());
player_count++;
} else {
cmd.lobby_data[x].clear();
@@ -1677,7 +1646,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
auto s = c->require_server_state();
for (size_t x = 0; x < 4; x++) {
if (l->clients[x]) {
auto other_p = l->clients[x]->game_data.player();
auto other_p = l->clients[x]->game_data.character();
cmd.players_ep3[x].inventory = other_p->inventory;
cmd.players_ep3[x].inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
cmd.players_ep3[x].disp = convert_player_disp_data<PlayerDispDataDCPCV3>(other_p->disp, c->language(), other_p->inventory.language);
@@ -1794,7 +1763,7 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.player();
auto lp = lc->game_data.character();
auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card_number = lc->license->serial_number;
@@ -1866,7 +1835,7 @@ void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cl
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.player();
auto lp = lc->game_data.character();
auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card_number = lc->license->serial_number;
@@ -1919,7 +1888,7 @@ void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.player();
auto lp = lc->game_data.character();
auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card_number = lc->license->serial_number;
@@ -2242,7 +2211,7 @@ void send_bank(shared_ptr<Client> c) {
throw logic_error("6xBC can only be sent to BB clients");
}
auto p = c->game_data.player();
auto p = c->game_data.character();
const auto* items_it = p->bank.items.data();
vector<PlayerBankItem> items(items_it, items_it + p->bank.num_items);
@@ -2278,7 +2247,7 @@ void send_shop(shared_ptr<Client> c, uint8_t shop_type) {
void send_level_up(shared_ptr<Client> c) {
auto l = c->require_lobby();
auto p = c->game_data.player();
auto p = c->game_data.character();
CharacterStats stats = p->disp.stats.char_stats;
const ItemData* mag = nullptr;
@@ -2547,12 +2516,12 @@ string ep3_description_for_client(shared_ptr<Client> c) {
if (!c->config.check_flag(Client::Flag::IS_EPISODE_3)) {
throw runtime_error("client is not Episode 3");
}
auto player = c->game_data.player();
auto p = c->game_data.character();
return string_printf(
"%s CLv%" PRIu32 " %c",
name_for_char_class(player->disp.visual.char_class),
player->disp.stats.level + 1,
char_for_language_code(player->inventory.language));
name_for_char_class(p->disp.visual.char_class),
p->disp.stats.level + 1,
char_for_language_code(p->inventory.language));
}
void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
@@ -2595,7 +2564,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
if (player.is_human()) {
try {
auto other_c = serial_number_to_client.at(player.serial_number);
entry.name.encode(other_c->game_data.player()->disp.name.decode(other_c->language()), c->language());
entry.name.encode(other_c->game_data.character()->disp.name.decode(other_c->language()), c->language());
entry.description.encode(ep3_description_for_client(other_c), c->language());
} catch (const out_of_range&) {
entry.name.encode(player.player_name, c->language());
@@ -2616,7 +2585,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
for (auto spec_c : l->clients) {
if (spec_c) {
auto& entry = cmd.spectator_entries[cmd.num_spectators++];
entry.name.encode(spec_c->game_data.player()->disp.name.decode(spec_c->language()), c->language());
entry.name.encode(spec_c->game_data.character()->disp.name.decode(spec_c->language()), c->language());
entry.description.encode(ep3_description_for_client(spec_c), c->language());
}
}
@@ -2633,7 +2602,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
size_t num_players = 0;
for (const auto& opp_c : primary_lobby->clients) {
if (opp_c) {
cmd.player_entries[num_players].name.encode(opp_c->game_data.player()->disp.name.decode(opp_c->language()), c->language());
cmd.player_entries[num_players].name.encode(opp_c->game_data.character()->disp.name.decode(opp_c->language()), c->language());
cmd.player_entries[num_players].description.encode(ep3_description_for_client(opp_c), c->language());
num_players++;
}
@@ -2646,7 +2615,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
for (auto spec_c : l->clients) {
if (spec_c) {
auto& entry = cmd.spectator_entries[num_spectators++];
entry.name.encode(spec_c->game_data.player()->disp.name.decode(spec_c->language()), c->language());
entry.name.encode(spec_c->game_data.character()->disp.name.decode(spec_c->language()), c->language());
entry.description.encode(ep3_description_for_client(spec_c), c->language());
}
}
@@ -2757,7 +2726,7 @@ void send_ep3_tournament_match_result(shared_ptr<Lobby> l, uint32_t meseta_rewar
if (player.is_human()) {
try {
auto pc = serial_number_to_client.at(player.serial_number);
entry.player_names[z].encode(pc->game_data.player()->disp.name.decode(pc->language()), lc->language());
entry.player_names[z].encode(pc->game_data.character()->disp.name.decode(pc->language()), lc->language());
} catch (const out_of_range&) {
entry.player_names[z].encode(player.player_name, lc->language());
}
+1 -2
View File
@@ -161,8 +161,7 @@ void send_pc_console_split_reconnect(
void send_client_init_bb(std::shared_ptr<Client> c, uint32_t error);
void send_system_file_bb(std::shared_ptr<Client> c);
void send_player_preview_bb(std::shared_ptr<Client> c, uint8_t player_index,
const PlayerDispDataBBPreview* preview);
void send_player_preview_bb(std::shared_ptr<Client> c, int8_t character_index, const PlayerDispDataBBPreview* preview);
void send_accept_client_checksum_bb(std::shared_ptr<Client> c);
void send_guild_card_header_bb(std::shared_ptr<Client> c);
void send_guild_card_chunk_bb(std::shared_ptr<Client> c, size_t chunk_index);
+1 -3
View File
@@ -112,7 +112,6 @@ void Server::on_listen_accept(
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
shared_ptr<Client> c(new Client(
this->shared_from_this(), bev, listening_socket->version, listening_socket->behavior));
c->game_data.should_save = this->state->allow_saving;
c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error;
c->channel.context_obj = this;
@@ -133,7 +132,6 @@ void Server::connect_client(
struct bufferevent* bev, uint32_t address, uint16_t client_port,
uint16_t server_port, GameVersion version, ServerBehavior initial_state) {
shared_ptr<Client> c(new Client(this->shared_from_this(), bev, version, initial_state));
c->game_data.should_save = this->state->allow_saving;
c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error;
c->channel.context_obj = this;
@@ -318,7 +316,7 @@ vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident
continue;
}
auto p = c->game_data.player(false, false);
auto p = c->game_data.character(false, false);
if (p && p->disp.name.eq(ident, p->inventory.language)) {
results.emplace_back(std::move(c));
continue;
-6
View File
@@ -23,7 +23,6 @@ ServerState::ServerState(const char* config_filename, bool is_replay)
dns_server_port(0),
ip_stack_debug(false),
allow_unregistered_users(false),
allow_saving(true),
allow_dc_pc_games(false),
allow_gc_xb_games(true),
item_tracking_enabled(true),
@@ -108,11 +107,6 @@ void ServerState::init() {
this->load_quest_index();
this->compile_functions();
this->load_dol_files();
if (this->is_replay) {
this->allow_saving = false;
config_log.info("Saving disabled because this is a replay session");
}
}
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
-1
View File
@@ -66,7 +66,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::vector<std::string> ip_stack_addresses;
bool ip_stack_debug;
bool allow_unregistered_users;
bool allow_saving;
bool allow_dc_pc_games;
bool allow_gc_xb_games;
bool item_tracking_enabled;