use .psochar format for BB characters
This commit is contained in:
+15
-15
@@ -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
@@ -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
@@ -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
@@ -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__;
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user