From 4f16243e41ed2f42dfea75b335e73f0ba7ed558b Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Tue, 19 Sep 2023 09:16:10 -0700 Subject: [PATCH] fix incorrect type in Ep3 PlayerConfig --- CMakeLists.txt | 1 + src/Episode3/DataIndexes.hh | 6 +- src/Player.cc | 519 ----------------------------------- src/Player.hh | 448 +----------------------------- src/PlayerSubordinates.cc | 533 ++++++++++++++++++++++++++++++++++++ src/PlayerSubordinates.hh | 462 +++++++++++++++++++++++++++++++ 6 files changed, 999 insertions(+), 970 deletions(-) create mode 100644 src/PlayerSubordinates.cc create mode 100644 src/PlayerSubordinates.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index c5c24666..62338cef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,7 @@ add_executable(newserv src/NetworkAddresses.cc src/PatchFileIndex.cc src/Player.cc + src/PlayerSubordinates.cc src/Product.cc src/ProxyCommands.cc src/ProxyServer.cc diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index 925604c8..cf450c27 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -13,6 +13,7 @@ #include #include +#include "../PlayerSubordinates.hh" #include "../Text.hh" namespace Episode3 { @@ -761,10 +762,7 @@ struct PlayerConfig { // This visual config is copied to the player's main visual config when the // player's name or proportions have changed, or when certain buttons on the // controller (L, R, X, Y) are held at game start time. - // This field's type is incorrect because Player.hh depends on this file, so - // we cannot #include "Player.hh" to use the PlayerVisualConfig struct here. - // TODO: Break the dependency cycle and use the correct type here. - /* 2274 */ parray backup_visual; + /* 2274 */ PlayerVisualConfig backup_visual; /* 22C4 */ parray unknown_a14; /* 2350 */ diff --git a/src/Player.cc b/src/Player.cc index a625c44f..26876ad9 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -24,174 +24,6 @@ using namespace std; static const string ACCOUNT_FILE_SIGNATURE = "newserv account file format; 7 sections present; sequential;"; -static FileContentsCache player_files_cache(300 * 1000 * 1000); - -PlayerStats::PlayerStats() noexcept - : level(0), - experience(0), - meseta(0) {} - -PlayerVisualConfig::PlayerVisualConfig() noexcept - : unknown_a2(0), - name_color(0), - extra_model(0), - unknown_a3(0), - section_id(0), - char_class(0), - v2_flags(0), - version(0), - v1_flags(0), - costume(0), - skin(0), - face(0), - head(0), - hair(0), - hair_r(0), - hair_g(0), - hair_b(0), - proportion_x(0), - proportion_y(0) {} - -void PlayerDispDataDCPCV3::enforce_v2_limits() { - // V1/V2 have fewer classes, so we'll substitute some here - if (this->visual.char_class == 11) { - this->visual.char_class = 0; // FOmar -> HUmar - } else if (this->visual.char_class == 10) { - this->visual.char_class = 1; // RAmarl -> HUnewearl - } else if (this->visual.char_class == 9) { - this->visual.char_class = 5; // HUcaseal -> RAcaseal - } - - // If the player is somehow still not a valid class, make them appear as the - // "ninja" NPC - if (this->visual.char_class > 8) { - this->visual.extra_model = 0; - this->visual.v2_flags |= 2; - } - this->visual.version = 2; -} - -PlayerDispDataBB PlayerDispDataDCPCV3::to_bb() const { - PlayerDispDataBB bb; - bb.stats = this->stats; - bb.visual = this->visual; - bb.visual.name = " 0"; - bb.name = this->visual.name; - bb.config = this->config; - bb.technique_levels = this->v1_technique_levels; - return bb; -} - -PlayerDispDataBB::PlayerDispDataBB() noexcept - : play_time(0), - unknown_a3(0) {} - -PlayerDispDataDCPCV3 PlayerDispDataBB::to_dcpcv3() const { - PlayerDispDataDCPCV3 ret; - ret.stats = this->stats; - ret.visual = this->visual; - ret.visual.name = this->name; - remove_language_marker_inplace(ret.visual.name); - ret.config = this->config; - ret.v1_technique_levels = this->technique_levels; - return ret; -} - -PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { - PlayerDispDataBBPreview pre; - pre.level = this->stats.level; - pre.experience = this->stats.experience; - pre.visual = this->visual; - pre.name = this->name; - pre.play_time = this->play_time; - return pre; -} - -void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) { - this->stats.level = pre.level; - this->stats.experience = pre.experience; - this->visual = pre.visual; - this->name = pre.name; -} - -void PlayerDispDataBB::apply_dressing_room(const PlayerDispDataBBPreview& pre) { - this->visual.name_color = pre.visual.name_color; - this->visual.extra_model = pre.visual.extra_model; - this->visual.unknown_a3 = pre.visual.unknown_a3; - this->visual.section_id = pre.visual.section_id; - this->visual.char_class = pre.visual.char_class; - this->visual.v2_flags = pre.visual.v2_flags; - this->visual.version = pre.visual.version; - this->visual.v1_flags = pre.visual.v1_flags; - this->visual.costume = pre.visual.costume; - this->visual.skin = pre.visual.skin; - this->visual.face = pre.visual.face; - this->visual.head = pre.visual.head; - this->visual.hair = pre.visual.hair; - this->visual.hair_r = pre.visual.hair_r; - this->visual.hair_g = pre.visual.hair_g; - this->visual.hair_b = pre.visual.hair_b; - this->visual.proportion_x = pre.visual.proportion_x; - this->visual.proportion_y = pre.visual.proportion_y; - this->name = pre.name; -} - -PlayerDispDataBBPreview::PlayerDispDataBBPreview() noexcept - : experience(0), - level(0), - play_time(0) {} - -GuildCardV3::GuildCardV3() noexcept - : player_tag(0), - guild_card_number(0), - present(0), - language(0), - section_id(0), - char_class(0) {} - -GuildCardBB::GuildCardBB() noexcept - : guild_card_number(0), - present(0), - language(0), - section_id(0), - char_class(0) {} - -void GuildCardBB::clear() { - this->guild_card_number = 0; - this->name.clear(0); - this->team_name.clear(0); - this->description.clear(0); - this->present = 0; - this->language = 0; - this->section_id = 0; - this->char_class = 0; -} - -void GuildCardEntryBB::clear() { - this->data.clear(); - this->unknown_a1.clear(0); -} - -uint32_t GuildCardFileBB::checksum() const { - return crc32(this, sizeof(*this)); -} - -void PlayerBank::load(const string& filename) { - *this = player_files_cache.get_obj_or_load(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)); - } -} - -//////////////////////////////////////////////////////////////////////////////// - ClientGameData::ClientGameData() : last_play_time_update(0), guild_card_number(0), @@ -367,193 +199,6 @@ void ClientGameData::save_player_data() { } } -void PlayerLobbyDataPC::clear() { - this->player_tag = 0; - this->guild_card = 0; - this->ip_address = 0; - this->client_id = 0; - ptext name; -} - -void PlayerLobbyDataDCGC::clear() { - this->player_tag = 0; - this->guild_card = 0; - this->ip_address = 0; - this->client_id = 0; - ptext name; -} - -void XBNetworkLocation::clear() { - this->internal_ipv4_address = 0; - this->external_ipv4_address = 0; - this->port = 0; - this->mac_address.clear(0); - this->unknown_a1.clear(0); - this->account_id = 0; - this->unknown_a2.clear(0); -} - -void PlayerLobbyDataXB::clear() { - this->player_tag = 0; - this->guild_card = 0; - this->netloc.clear(); - this->client_id = 0; - this->name.clear(0); -} - -void PlayerLobbyDataBB::clear() { - this->player_tag = 0; - this->guild_card = 0; - this->ip_address = 0; - this->unknown_a1.clear(0); - this->client_id = 0; - this->name.clear(0); - this->unknown_a2 = 0; -} - -PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec) - : title_color(rec.title_color), - unknown_u0(rec.unknown_u0), - times_ep1_online(rec.times_ep1_online), - times_ep2_online(0), - times_ep1_offline(0), - unknown_g3(rec.unknown_g3), - grave_deaths(rec.grave_deaths), - unknown_u4(0), - grave_coords_time(rec.grave_coords_time), - grave_team(rec.grave_team), - grave_message(rec.grave_message), - unknown_m5(0), - unknown_t6(0), - rank_title(encrypt_challenge_rank_text(decode_sjis(decrypt_challenge_rank_text(rec.rank_title)))), - unknown_l7(0) {} - -PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec) - : title_color(rec.title_color), - unknown_u0(rec.unknown_u0), - times_ep1_online(rec.times_ep1_online), - times_ep2_online(0), - times_ep1_offline(0), - unknown_g3(rec.unknown_g3), - grave_deaths(rec.grave_deaths), - unknown_u4(0), - grave_coords_time(rec.grave_coords_time), - grave_team(rec.grave_team), - grave_message(rec.grave_message), - unknown_m5(0), - unknown_t6(0), - rank_title(rec.rank_title), - unknown_l7(0) {} - -PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge& rec) - : title_color(rec.stats.title_color), - unknown_u0(rec.stats.unknown_u0), - 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_deaths(rec.stats.grave_deaths), - unknown_u4(rec.stats.unknown_u4), - grave_coords_time(rec.stats.grave_coords_time), - grave_team(rec.stats.grave_team), - grave_message(rec.stats.grave_message), - unknown_m5(rec.stats.unknown_m5), - unknown_t6(rec.stats.unknown_t6), - rank_title(encrypt_challenge_rank_text(decode_sjis(decrypt_challenge_rank_text(rec.rank_title)))), - unknown_l7(rec.unknown_l7) {} - -PlayerRecordsBB_Challenge::operator PlayerRecordsDC_Challenge() const { - PlayerRecordsDC_Challenge ret; - ret.title_color = this->title_color; - ret.unknown_u0 = this->unknown_u0; - ret.rank_title = encrypt_challenge_rank_text(encode_sjis(decrypt_challenge_rank_text(this->rank_title))); - ret.times_ep1_online = this->times_ep1_online; - ret.unknown_g3 = 0; - ret.grave_deaths = this->grave_deaths; - ret.grave_coords_time = this->grave_coords_time; - ret.grave_team = this->grave_team; - ret.grave_message = this->grave_message; - ret.times_ep1_offline = this->times_ep1_offline; - ret.unknown_l4.clear(0); - return ret; -} - -PlayerRecordsBB_Challenge::operator PlayerRecordsPC_Challenge() const { - PlayerRecordsPC_Challenge ret; - ret.title_color = this->title_color; - ret.unknown_u0 = this->unknown_u0; - ret.rank_title = this->rank_title; - ret.times_ep1_online = this->times_ep1_online; - ret.unknown_g3 = 0; - ret.grave_deaths = this->grave_deaths; - ret.grave_coords_time = this->grave_coords_time; - ret.grave_team = this->grave_team; - ret.grave_message = this->grave_message; - ret.times_ep1_offline = this->times_ep1_offline; - ret.unknown_l4.clear(0); - return ret; -} - -PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge() const { - PlayerRecordsV3_Challenge ret; - ret.stats.title_color = this->title_color; - ret.stats.unknown_u0 = this->unknown_u0; - 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_deaths = this->grave_deaths; - ret.stats.unknown_u4 = this->unknown_u4; - ret.stats.grave_coords_time = this->grave_coords_time; - ret.stats.grave_team = this->grave_team; - ret.stats.grave_message = this->grave_message; - ret.stats.unknown_m5 = this->unknown_m5; - ret.stats.unknown_t6 = this->unknown_t6; - ret.rank_title = encrypt_challenge_rank_text(encode_sjis(decrypt_challenge_rank_text(this->rank_title))); - ret.unknown_l7 = this->unknown_l7; - return ret; -} - -PlayerInventoryItem::PlayerInventoryItem() { - this->clear(); -} - -PlayerInventoryItem::PlayerInventoryItem(const PlayerBankItem& src) - : present(1), - extension_data1(0), - extension_data2(0), - flags(0), - data(src.data) {} - -void PlayerInventoryItem::clear() { - this->present = 0x0000; - this->extension_data1 = 0x00; - this->extension_data2 = 0x00; - this->flags = 0x00000000; - this->data.clear(); -} - -PlayerBankItem::PlayerBankItem() { - this->clear(); -} - -PlayerBankItem::PlayerBankItem(const PlayerInventoryItem& src) - : data(src.data), - amount(this->data.stack_size()), - show_flags(1) {} - -void PlayerBankItem::clear() { - this->data.clear(); - this->amount = 0; - this->show_flags = 0; -} - -PlayerInventory::PlayerInventory() - : num_items(0), - hp_materials_used(0), - tp_materials_used(0), - language(0) {} - void SavedPlayerDataBB::update_to_latest_version() { if (this->signature == PLAYER_FILE_SIGNATURE_V0) { this->signature = PLAYER_FILE_SIGNATURE_V1; @@ -609,43 +254,6 @@ void SavedPlayerDataBB::add_item(const PlayerInventoryItem& item) { this->inventory.num_items++; } -void PlayerBank::add_item(const PlayerBankItem& item) { - uint32_t pid = item.data.primary_identifier(); - - if (pid == MESETA_IDENTIFIER) { - this->meseta += item.data.data2d; - if (this->meseta > 999999) { - this->meseta = 999999; - } - return; - } - - size_t combine_max = item.data.max_stack_size(); - if (combine_max > 1) { - size_t y; - for (y = 0; y < this->num_items; y++) { - if (this->items[y].data.primary_identifier() == item.data.primary_identifier()) { - break; - } - } - - if (y < this->num_items) { - this->items[y].data.data1[5] += item.data.data1[5]; - if (this->items[y].data.data1[5] > combine_max) { - this->items[y].data.data1[5] = combine_max; - } - this->items[y].amount = this->items[y].data.data1[5]; - return; - } - } - - if (this->num_items >= 200) { - throw runtime_error("bank is full"); - } - this->items[this->num_items] = item; - this->num_items++; -} - // TODO: Eliminate code duplication between this function and the parallel // function in PlayerBank PlayerInventoryItem SavedPlayerDataBB::remove_item( @@ -702,130 +310,3 @@ void SavedPlayerDataBB::remove_meseta(uint32_t amount, bool allow_overdraft) { throw out_of_range("player does not have enough meseta"); } } - -PlayerBankItem PlayerBank::remove_item(uint32_t item_id, uint32_t amount) { - PlayerBankItem ret; - - if (item_id == 0xFFFFFFFF) { - if (amount > this->meseta) { - throw out_of_range("player does not have enough meseta"); - } - ret.data.data1[0] = 0x04; - ret.data.data2d = amount; - this->meseta -= amount; - return ret; - } - - size_t index = this->find_item(item_id); - auto& bank_item = this->items[index]; - - if (amount && (bank_item.data.stack_size() > 1) && - (amount < bank_item.data.data1[5])) { - ret = bank_item; - ret.data.data1[5] = amount; - ret.amount = amount; - bank_item.data.data1[5] -= amount; - bank_item.amount -= amount; - return ret; - } - - ret = bank_item; - this->num_items--; - for (size_t x = index; x < this->num_items; x++) { - this->items[x] = this->items[x + 1]; - } - this->items[this->num_items] = PlayerBankItem(); - return ret; -} - -size_t PlayerInventory::find_item(uint32_t item_id) const { - for (size_t x = 0; x < this->num_items; x++) { - if (this->items[x].data.id == item_id) { - return x; - } - } - throw out_of_range("item not present"); -} - -size_t PlayerInventory::find_equipped_weapon() const { - ssize_t ret = -1; - for (size_t y = 0; y < this->num_items; y++) { - if (!(this->items[y].flags & 0x00000008)) { - continue; - } - if (this->items[y].data.data1[0] != 0) { - continue; - } - if (ret < 0) { - ret = y; - } else { - throw runtime_error("multiple weapons are equipped"); - } - } - if (ret < 0) { - throw out_of_range("no weapon is equipped"); - } - return ret; -} - -size_t PlayerInventory::find_equipped_armor() const { - ssize_t ret = -1; - for (size_t y = 0; y < this->num_items; y++) { - if (!(this->items[y].flags & 0x00000008)) { - continue; - } - if (this->items[y].data.data1[0] != 1 || this->items[y].data.data1[1] != 1) { - continue; - } - if (ret < 0) { - ret = y; - } else { - throw runtime_error("multiple armors are equipped"); - } - } - if (ret < 0) { - throw out_of_range("no armor is equipped"); - } - return ret; -} - -size_t PlayerInventory::find_equipped_mag() const { - ssize_t ret = -1; - for (size_t y = 0; y < this->num_items; y++) { - if (!(this->items[y].flags & 0x00000008)) { - continue; - } - if (this->items[y].data.data1[0] != 2) { - continue; - } - if (ret < 0) { - ret = y; - } else { - throw runtime_error("multiple mags are equipped"); - } - } - if (ret < 0) { - throw out_of_range("no mag is equipped"); - } - return ret; -} - -size_t PlayerBank::find_item(uint32_t item_id) { - for (size_t x = 0; x < this->num_items; x++) { - if (this->items[x].data.id == item_id) { - return x; - } - } - throw out_of_range("item not present"); -} - -void SavedPlayerDataBB::print_inventory(FILE* stream) 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 = item.data.name(false); - auto hex = item.data.hex(); - fprintf(stream, "[PlayerInventory] %zu: %s (%s)\n", x, hex.c_str(), name.c_str()); - } -} diff --git a/src/Player.hh b/src/Player.hh index df199dc1..3dbbff63 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -10,92 +10,10 @@ #include #include "Episode3/DataIndexes.hh" -#include "ItemData.hh" -#include "LevelTable.hh" +#include "PlayerSubordinates.hh" #include "Text.hh" #include "Version.hh" -struct PlayerBankItem; - -// 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 -// the InventoryItem struct to store some things not present in V1. The game -// stores arrays of bytes striped across these structures. In newserv, we call -// those fields extension_data. They contain: -// items[0].extension_data1 through items[19].extension_data1: -// Extended technique levels. The values in the v1_technique_levels array -// only go up to 14 (tech level 15); if the player has a technique above -// level 15, the corresponding extension_data1 field holds the remaining -// levels (so a level 20 tech would have 14 in v1_technique_levels and 5 -// in the corresponding item's extension_data1 field). -// items[0].extension_data2 through items[3].extension_data2: -// The value known as unknown_a1 in the PSOGCCharacterFile::Character -// struct. See SaveFileFormats.hh. -// items[4].extension_data2 through items[7].extension_data2: -// The timestamp when the character was last saved, in seconds since -// January 1, 2000. Stored little-endian, so items[4] contains the LSB. -// items[8].extension_data2 through items[12].extension_data2: -// Number of power materials, mind materials, evade materials, def -// materials, and luck materials (respectively) used by the player. -// items[13].extension_data2 through items[15].extension_data2: -// Unknown. These are not an array, but do appear to be related. - -struct PlayerInventoryItem { // 0x1C bytes - le_uint16_t present; - // See note above about these fields - uint8_t extension_data1; - uint8_t extension_data2; - le_uint32_t flags; // 8 = equipped - ItemData data; - - PlayerInventoryItem(); - PlayerInventoryItem(const PlayerBankItem&); - void clear(); -} __attribute__((packed)); - -struct PlayerBankItem { // 0x18 bytes - ItemData data; - le_uint16_t amount; - le_uint16_t show_flags; - - PlayerBankItem(); - PlayerBankItem(const PlayerInventoryItem&); - void clear(); -} __attribute__((packed)); - -struct PlayerInventory { // 0x34C bytes - uint8_t num_items; - uint8_t hp_materials_used; - uint8_t tp_materials_used; - uint8_t language; - PlayerInventoryItem items[30]; - - PlayerInventory(); - - size_t find_item(uint32_t item_id) const; - - size_t find_equipped_weapon() const; - size_t find_equipped_armor() const; - size_t find_equipped_mag() const; -} __attribute__((packed)); - -struct PlayerBank { // 0x12C8 bytes - le_uint32_t num_items; - le_uint32_t meseta; - PlayerBankItem items[200]; - - 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 PlayerBankItem& item); - PlayerBankItem remove_item(uint32_t item_id, uint32_t amount); - size_t find_item(uint32_t item_id); -} __attribute__((packed)); - struct PendingItemTrade { uint8_t other_client_id; bool confirmed; // true if client has sent a D2 command @@ -108,338 +26,6 @@ struct PendingCardTrade { std::vector> card_to_count; }; -struct PlayerDispDataBB; - -struct PlayerStats { - /* 00 */ CharacterStats char_stats; - /* 0E */ le_uint16_t unknown_a1 = 0; - /* 10 */ le_float unknown_a2 = 0.0; - /* 14 */ le_float unknown_a3 = 0.0; - /* 18 */ le_uint32_t level = 0; - /* 1C */ le_uint32_t experience = 0; - /* 20 */ le_uint32_t meseta = 0; - /* 24 */ - - PlayerStats() noexcept; -} __attribute__((packed)); - -struct PlayerVisualConfig { - /* 00 */ ptext name; - /* 10 */ le_uint64_t unknown_a2 = 0; // Note: This is probably not actually a 64-bit int. - /* 18 */ le_uint32_t name_color = 0xFFFFFFFF; // RGBA - /* 1C */ uint8_t extra_model = 0; - /* 1D */ parray unused; - /* 2C */ le_uint32_t unknown_a3 = 0; - /* 30 */ uint8_t section_id = 0; - /* 31 */ uint8_t char_class = 0; - /* 32 */ uint8_t v2_flags = 0; - /* 33 */ uint8_t version = 0; - /* 34 */ le_uint32_t v1_flags = 0; - /* 38 */ le_uint16_t costume = 0; - /* 3A */ le_uint16_t skin = 0; - /* 3C */ le_uint16_t face = 0; - /* 3E */ le_uint16_t head = 0; - /* 40 */ le_uint16_t hair = 0; - /* 42 */ le_uint16_t hair_r = 0; - /* 44 */ le_uint16_t hair_g = 0; - /* 46 */ le_uint16_t hair_b = 0; - /* 48 */ le_float proportion_x = 0.0; - /* 4C */ le_float proportion_y = 0.0; - /* 50 */ - - PlayerVisualConfig() noexcept; -} __attribute__((packed)); - -struct PlayerDispDataDCPCV3 { - /* 00 */ PlayerStats stats; - /* 24 */ PlayerVisualConfig visual; - /* 74 */ parray config; - /* BC */ parray v1_technique_levels; - /* D0 */ - - // Note: This struct has a default constructor because it's used in a command - // that has a fixed-size array. If we didn't define this constructor, the - // trivial fields in that array's members would be uninitialized, and we could - // send uninitialized memory to the client. - PlayerDispDataDCPCV3() noexcept = default; - - void enforce_v2_limits(); - PlayerDispDataBB to_bb() const; -} __attribute__((packed)); - -struct PlayerDispDataBBPreview { - /* 00 */ le_uint32_t experience; - /* 04 */ le_uint32_t level; - // The name field in this structure is used for the player's Guild Card - // number, apparently (possibly because it's a char array and this is BB) - /* 08 */ PlayerVisualConfig visual; - /* 58 */ ptext name; - /* 78 */ uint32_t play_time; - /* 7C */ - - PlayerDispDataBBPreview() noexcept; -} __attribute__((packed)); - -// BB player appearance and stats data -struct PlayerDispDataBB { - /* 0000 */ PlayerStats stats; - /* 0024 */ PlayerVisualConfig visual; - /* 0074 */ ptext name; - /* 008C */ le_uint32_t play_time; - /* 0090 */ uint32_t unknown_a3; - /* 0094 */ parray config; - /* 017C */ parray technique_levels; - /* 0190 */ - - PlayerDispDataBB() noexcept; - - inline void enforce_v2_limits() {} - PlayerDispDataDCPCV3 to_dcpcv3() const; - PlayerDispDataBBPreview to_preview() const; - void apply_preview(const PlayerDispDataBBPreview&); - void apply_dressing_room(const PlayerDispDataBBPreview&); -} __attribute__((packed)); - -// TODO: Is this the same for XB as it is for GC? (This struct is based on the -// GC format) -struct GuildCardV3 { - /* 00 */ le_uint32_t player_tag; - /* 04 */ le_uint32_t guild_card_number; - /* 08 */ ptext name; - /* 20 */ ptext description; - /* 8C */ uint8_t present; // should be 1 - /* 8D */ uint8_t language; - /* 8E */ uint8_t section_id; - /* 8F */ uint8_t char_class; - /* 90 */ - - GuildCardV3() noexcept; -} __attribute__((packed)); - -// BB guild card format -struct GuildCardBB { - /* 0000 */ le_uint32_t guild_card_number; - /* 0004 */ ptext name; - /* 0034 */ ptext team_name; - /* 0054 */ ptext description; - /* 0104 */ uint8_t present; // should be 1 if guild card entry exists - /* 0105 */ uint8_t language; - /* 0106 */ uint8_t section_id; - /* 0107 */ uint8_t char_class; - /* 0108 */ - - GuildCardBB() noexcept; - void clear(); -} __attribute__((packed)); - -// an entry in the BB guild card file -struct GuildCardEntryBB { - GuildCardBB data; - ptext comment; - parray unknown_a1; - - void clear(); -} __attribute__((packed)); - -// the format of the BB guild card file -struct GuildCardFileBB { - parray unknown_a1; - GuildCardBB blocked[0x1C]; - parray unknown_a2; - GuildCardEntryBB entries[0x69]; - - uint32_t checksum() const; -} __attribute__((packed)); - -struct KeyAndTeamConfigBB { - parray unknown_a1; // 0000 - parray key_config; // 0114 - parray joystick_config; // 0280 - le_uint32_t guild_card_number; // 02B8 - le_uint32_t team_id; // 02BC - le_uint64_t team_info; // 02C0 - le_uint16_t team_privilege_level; // 02C8 - le_uint16_t reserved; // 02CA - ptext team_name; // 02CC - parray team_flag; // 02EC - le_uint32_t team_rewards; // 0AEC -} __attribute__((packed)); - -struct PlayerLobbyDataPC { - le_uint32_t player_tag = 0; - le_uint32_t guild_card = 0; - // There's a strange behavior (bug? "feature"?) in Episode 3 where the start - // button does nothing in the lobby (hence you can't "quit game") if the - // client's IP address is zero. So, we fill it in with a fake nonzero value to - // avoid this behavior, and to be consistent, we make IP addresses fake and - // nonzero on all other versions too. - be_uint32_t ip_address = 0x7F000001; - le_uint32_t client_id = 0; - ptext name; - - void clear(); -} __attribute__((packed)); - -struct PlayerLobbyDataDCGC { - le_uint32_t player_tag = 0; - le_uint32_t guild_card = 0; - be_uint32_t ip_address = 0x7F000001; - le_uint32_t client_id = 0; - ptext name; - - void clear(); -} __attribute__((packed)); - -struct XBNetworkLocation { - le_uint32_t internal_ipv4_address = 0x0A0A0A0A; - le_uint32_t external_ipv4_address = 0x23232323; - le_uint16_t port = 9100; - parray mac_address = 0x77; - parray unknown_a1; - le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF; - parray unknown_a2; - - void clear(); -} __attribute__((packed)); - -struct PlayerLobbyDataXB { - le_uint32_t player_tag = 0; - le_uint32_t guild_card = 0; - XBNetworkLocation netloc; - le_uint32_t client_id = 0; - ptext name; - - void clear(); -} __attribute__((packed)); - -struct PlayerLobbyDataBB { - le_uint32_t player_tag = 0; - le_uint32_t guild_card = 0; - // This field is a guess; the official builds didn't use this, but all other - // versions have it - be_uint32_t ip_address = 0x7F000001; - parray unknown_a1; - le_uint32_t client_id = 0; - ptext name; - le_uint32_t unknown_a2 = 0; - - void clear(); -} __attribute__((packed)); - -template -struct PlayerRecordsDCPC_Challenge { - using CharT = typename std::conditional::type; - - /* 00 */ le_uint16_t title_color = 0x7FFF; - /* 02 */ parray unknown_u0; - /* 04 */ ptext rank_title; // Encrypted; see decrypt_challenge_rank_text - /* 10 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times - /* 34 */ le_uint16_t unknown_g3 = 0; - /* 36 */ le_uint16_t grave_deaths = 0; - /* 38 */ parray grave_coords_time; - /* 4C */ ptext grave_team; - /* 60 */ ptext grave_message; - /* 78 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times - /* 9C */ parray unknown_l4; - /* A0 */ -} __attribute__((packed)); - -struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge { -} __attribute__((packed)); - -struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge { -} __attribute__((packed)); - -template -struct PlayerRecordsV3_Challenge { - using U16T = typename std::conditional::type; - using U32T = typename std::conditional::type; - - // Offsets are (1) relative to start of C5 entry, and (2) relative to start - // of save file structure - struct Stats { - /* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555 - /* 02:1E */ parray unknown_u0; - /* 04:20 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time - /* 28:44 */ parray times_ep2_online; // Encrypted; see decrypt_challenge_time - /* 3C:58 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time - /* 60:7C */ parray unknown_g3; - /* 64:80 */ U16T grave_deaths = 0; - /* 66:82 */ parray unknown_u4; - /* 68:84 */ parray grave_coords_time; - /* 7C:98 */ ptext grave_team; - /* 90:AC */ ptext grave_message; - /* B0:CC */ parray unknown_m5; - /* B4:D0 */ parray unknown_t6; - /* D8:F4 */ - } __attribute__((packed)); - /* 0000:001C */ Stats stats; - // On Episode 3, there are special cases that apply to this field - if the - // text ends with certain strings (after decrypt_challenge_rank_text), the - // player will have particle effects emanate from their character in the - // lobby every 2 seconds. These effects are: - // Ends with ":GOD" => blue circle - // Ends with ":KING" => white particles - // Ends with ":LORD" => rising yellow sparkles - // Ends with ":CHAMP" => green circle - /* 00D8:00F4 */ ptext rank_title; - /* 00E4:0100 */ parray unknown_l7; - /* 0100:011C */ -} __attribute__((packed)); - -struct PlayerRecordsBB_Challenge { - /* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555 - /* 0002 */ parray unknown_u0; - /* 0004 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time - /* 0028 */ parray times_ep2_online; // Encrypted; see decrypt_challenge_time - /* 003C */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time - /* 0060 */ parray unknown_g3; - /* 0064 */ le_uint16_t grave_deaths = 0; - /* 0066 */ parray unknown_u4; - /* 0068 */ parray grave_coords_time; - /* 007C */ ptext grave_team; - /* 00A4 */ ptext grave_message; - /* 00E4 */ parray unknown_m5; - /* 00E8 */ parray unknown_t6; - /* 010C */ ptext rank_title; // Encrypted; see decrypt_challenge_rank_text - /* 0124 */ parray unknown_l7; - /* 0140 */ - - PlayerRecordsBB_Challenge() = default; - PlayerRecordsBB_Challenge(const PlayerRecordsBB_Challenge& other) = default; - PlayerRecordsBB_Challenge& operator=(const PlayerRecordsBB_Challenge& other) = default; - - PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec); - PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec); - PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge& rec); - - operator PlayerRecordsDC_Challenge() const; - operator PlayerRecordsPC_Challenge() const; - operator PlayerRecordsV3_Challenge() const; -} __attribute__((packed)); - -template -struct PlayerRecords_Battle { - using U16T = typename std::conditional::type; - // On Episode 3, place_counts[0] is win count and [1] is loss count - /* 00 */ parray place_counts; - /* 08 */ U16T disconnect_count; - /* 0A */ parray unknown_a1; - /* 10 */ parray unknown_a2; - /* 18 */ -} __attribute__((packed)); - -template -struct ChoiceSearchConfig { - // 0 = enabled, 1 = disabled. Unused for command C3 - le_uint32_t choice_search_disabled = 0; - struct Entry { - ItemIDT parent_category_id = 0; - ItemIDT category_id = 0; - } __attribute__((packed)); - parray entries; -} __attribute__((packed)); - constexpr uint64_t PLAYER_FILE_SIGNATURE_V0 = 0x6E65777365727620; constexpr uint64_t PLAYER_FILE_SIGNATURE_V1 = 0xA904332D5CEF0296; @@ -535,35 +121,3 @@ public: // Note: This function is not const because it updates the player's play time. void save_player_data(); }; - -uint32_t compute_guild_card_checksum(const void* data, size_t size); - -template -DestT convert_player_disp_data(const SrcT&) { - static_assert(always_false::v, - "unspecialized strcpy_t should never be called"); -} - -template <> -inline PlayerDispDataDCPCV3 convert_player_disp_data( - const PlayerDispDataDCPCV3& src) { - return src; -} - -template <> -inline PlayerDispDataDCPCV3 convert_player_disp_data( - const PlayerDispDataBB& src) { - return src.to_dcpcv3(); -} - -template <> -inline PlayerDispDataBB convert_player_disp_data( - const PlayerDispDataDCPCV3& src) { - return src.to_bb(); -} - -template <> -inline PlayerDispDataBB convert_player_disp_data( - const PlayerDispDataBB& src) { - return src; -} diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc new file mode 100644 index 00000000..a907cdbc --- /dev/null +++ b/src/PlayerSubordinates.cc @@ -0,0 +1,533 @@ +#include "PlayerSubordinates.hh" + +#include +#include +#include + +#include +#include +#include + +#include "ItemData.hh" +#include "Loggers.hh" +#include "PSOEncryption.hh" +#include "StaticGameData.hh" +#include "Text.hh" +#include "Version.hh" + +FileContentsCache player_files_cache(300 * 1000 * 1000); + +PlayerStats::PlayerStats() noexcept + : level(0), + experience(0), + meseta(0) {} + +PlayerVisualConfig::PlayerVisualConfig() noexcept + : unknown_a2(0), + name_color(0), + extra_model(0), + unknown_a3(0), + section_id(0), + char_class(0), + v2_flags(0), + version(0), + v1_flags(0), + costume(0), + skin(0), + face(0), + head(0), + hair(0), + hair_r(0), + hair_g(0), + hair_b(0), + proportion_x(0), + proportion_y(0) {} + +void PlayerDispDataDCPCV3::enforce_v2_limits() { + // V1/V2 have fewer classes, so we'll substitute some here + if (this->visual.char_class == 11) { + this->visual.char_class = 0; // FOmar -> HUmar + } else if (this->visual.char_class == 10) { + this->visual.char_class = 1; // RAmarl -> HUnewearl + } else if (this->visual.char_class == 9) { + this->visual.char_class = 5; // HUcaseal -> RAcaseal + } + + // If the player is somehow still not a valid class, make them appear as the + // "ninja" NPC + if (this->visual.char_class > 8) { + this->visual.extra_model = 0; + this->visual.v2_flags |= 2; + } + this->visual.version = 2; +} + +PlayerDispDataBB PlayerDispDataDCPCV3::to_bb() const { + PlayerDispDataBB bb; + bb.stats = this->stats; + bb.visual = this->visual; + bb.visual.name = " 0"; + bb.name = this->visual.name; + bb.config = this->config; + bb.technique_levels = this->v1_technique_levels; + return bb; +} + +PlayerDispDataBB::PlayerDispDataBB() noexcept + : play_time(0), + unknown_a3(0) {} + +PlayerDispDataDCPCV3 PlayerDispDataBB::to_dcpcv3() const { + PlayerDispDataDCPCV3 ret; + ret.stats = this->stats; + ret.visual = this->visual; + ret.visual.name = this->name; + remove_language_marker_inplace(ret.visual.name); + ret.config = this->config; + ret.v1_technique_levels = this->technique_levels; + return ret; +} + +PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { + PlayerDispDataBBPreview pre; + pre.level = this->stats.level; + pre.experience = this->stats.experience; + pre.visual = this->visual; + pre.name = this->name; + pre.play_time = this->play_time; + return pre; +} + +void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) { + this->stats.level = pre.level; + this->stats.experience = pre.experience; + this->visual = pre.visual; + this->name = pre.name; +} + +void PlayerDispDataBB::apply_dressing_room(const PlayerDispDataBBPreview& pre) { + this->visual.name_color = pre.visual.name_color; + this->visual.extra_model = pre.visual.extra_model; + this->visual.unknown_a3 = pre.visual.unknown_a3; + this->visual.section_id = pre.visual.section_id; + this->visual.char_class = pre.visual.char_class; + this->visual.v2_flags = pre.visual.v2_flags; + this->visual.version = pre.visual.version; + this->visual.v1_flags = pre.visual.v1_flags; + this->visual.costume = pre.visual.costume; + this->visual.skin = pre.visual.skin; + this->visual.face = pre.visual.face; + this->visual.head = pre.visual.head; + this->visual.hair = pre.visual.hair; + this->visual.hair_r = pre.visual.hair_r; + this->visual.hair_g = pre.visual.hair_g; + this->visual.hair_b = pre.visual.hair_b; + this->visual.proportion_x = pre.visual.proportion_x; + this->visual.proportion_y = pre.visual.proportion_y; + this->name = pre.name; +} + +PlayerDispDataBBPreview::PlayerDispDataBBPreview() noexcept + : experience(0), + level(0), + play_time(0) {} + +GuildCardV3::GuildCardV3() noexcept + : player_tag(0), + guild_card_number(0), + present(0), + language(0), + section_id(0), + char_class(0) {} + +GuildCardBB::GuildCardBB() noexcept + : guild_card_number(0), + present(0), + language(0), + section_id(0), + char_class(0) {} + +void GuildCardBB::clear() { + this->guild_card_number = 0; + this->name.clear(0); + this->team_name.clear(0); + this->description.clear(0); + this->present = 0; + this->language = 0; + this->section_id = 0; + this->char_class = 0; +} + +void GuildCardEntryBB::clear() { + this->data.clear(); + this->unknown_a1.clear(0); +} + +uint32_t GuildCardFileBB::checksum() const { + return crc32(this, sizeof(*this)); +} + +void PlayerBank::load(const string& filename) { + *this = player_files_cache.get_obj_or_load(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 = 0; + this->ip_address = 0; + this->client_id = 0; + ptext name; +} + +void PlayerLobbyDataDCGC::clear() { + this->player_tag = 0; + this->guild_card = 0; + this->ip_address = 0; + this->client_id = 0; + ptext name; +} + +void XBNetworkLocation::clear() { + this->internal_ipv4_address = 0; + this->external_ipv4_address = 0; + this->port = 0; + this->mac_address.clear(0); + this->unknown_a1.clear(0); + this->account_id = 0; + this->unknown_a2.clear(0); +} + +void PlayerLobbyDataXB::clear() { + this->player_tag = 0; + this->guild_card = 0; + this->netloc.clear(); + this->client_id = 0; + this->name.clear(0); +} + +void PlayerLobbyDataBB::clear() { + this->player_tag = 0; + this->guild_card = 0; + this->ip_address = 0; + this->unknown_a1.clear(0); + this->client_id = 0; + this->name.clear(0); + this->unknown_a2 = 0; +} + +PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec) + : title_color(rec.title_color), + unknown_u0(rec.unknown_u0), + times_ep1_online(rec.times_ep1_online), + times_ep2_online(0), + times_ep1_offline(0), + unknown_g3(rec.unknown_g3), + grave_deaths(rec.grave_deaths), + unknown_u4(0), + grave_coords_time(rec.grave_coords_time), + grave_team(rec.grave_team), + grave_message(rec.grave_message), + unknown_m5(0), + unknown_t6(0), + rank_title(encrypt_challenge_rank_text(decode_sjis(decrypt_challenge_rank_text(rec.rank_title)))), + unknown_l7(0) {} + +PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec) + : title_color(rec.title_color), + unknown_u0(rec.unknown_u0), + times_ep1_online(rec.times_ep1_online), + times_ep2_online(0), + times_ep1_offline(0), + unknown_g3(rec.unknown_g3), + grave_deaths(rec.grave_deaths), + unknown_u4(0), + grave_coords_time(rec.grave_coords_time), + grave_team(rec.grave_team), + grave_message(rec.grave_message), + unknown_m5(0), + unknown_t6(0), + rank_title(rec.rank_title), + unknown_l7(0) {} + +PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge& rec) + : title_color(rec.stats.title_color), + unknown_u0(rec.stats.unknown_u0), + 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_deaths(rec.stats.grave_deaths), + unknown_u4(rec.stats.unknown_u4), + grave_coords_time(rec.stats.grave_coords_time), + grave_team(rec.stats.grave_team), + grave_message(rec.stats.grave_message), + unknown_m5(rec.stats.unknown_m5), + unknown_t6(rec.stats.unknown_t6), + rank_title(encrypt_challenge_rank_text(decode_sjis(decrypt_challenge_rank_text(rec.rank_title)))), + unknown_l7(rec.unknown_l7) {} + +PlayerRecordsBB_Challenge::operator PlayerRecordsDC_Challenge() const { + PlayerRecordsDC_Challenge ret; + ret.title_color = this->title_color; + ret.unknown_u0 = this->unknown_u0; + ret.rank_title = encrypt_challenge_rank_text(encode_sjis(decrypt_challenge_rank_text(this->rank_title))); + ret.times_ep1_online = this->times_ep1_online; + ret.unknown_g3 = 0; + ret.grave_deaths = this->grave_deaths; + ret.grave_coords_time = this->grave_coords_time; + ret.grave_team = this->grave_team; + ret.grave_message = this->grave_message; + ret.times_ep1_offline = this->times_ep1_offline; + ret.unknown_l4.clear(0); + return ret; +} + +PlayerRecordsBB_Challenge::operator PlayerRecordsPC_Challenge() const { + PlayerRecordsPC_Challenge ret; + ret.title_color = this->title_color; + ret.unknown_u0 = this->unknown_u0; + ret.rank_title = this->rank_title; + ret.times_ep1_online = this->times_ep1_online; + ret.unknown_g3 = 0; + ret.grave_deaths = this->grave_deaths; + ret.grave_coords_time = this->grave_coords_time; + ret.grave_team = this->grave_team; + ret.grave_message = this->grave_message; + ret.times_ep1_offline = this->times_ep1_offline; + ret.unknown_l4.clear(0); + return ret; +} + +PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge() const { + PlayerRecordsV3_Challenge ret; + ret.stats.title_color = this->title_color; + ret.stats.unknown_u0 = this->unknown_u0; + 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_deaths = this->grave_deaths; + ret.stats.unknown_u4 = this->unknown_u4; + ret.stats.grave_coords_time = this->grave_coords_time; + ret.stats.grave_team = this->grave_team; + ret.stats.grave_message = this->grave_message; + ret.stats.unknown_m5 = this->unknown_m5; + ret.stats.unknown_t6 = this->unknown_t6; + ret.rank_title = encrypt_challenge_rank_text(encode_sjis(decrypt_challenge_rank_text(this->rank_title))); + ret.unknown_l7 = this->unknown_l7; + return ret; +} + +PlayerInventoryItem::PlayerInventoryItem() { + this->clear(); +} + +PlayerInventoryItem::PlayerInventoryItem(const PlayerBankItem& src) + : present(1), + extension_data1(0), + extension_data2(0), + flags(0), + data(src.data) {} + +void PlayerInventoryItem::clear() { + this->present = 0x0000; + this->extension_data1 = 0x00; + this->extension_data2 = 0x00; + this->flags = 0x00000000; + this->data.clear(); +} + +PlayerBankItem::PlayerBankItem() { + this->clear(); +} + +PlayerBankItem::PlayerBankItem(const PlayerInventoryItem& src) + : data(src.data), + amount(this->data.stack_size()), + show_flags(1) {} + +void PlayerBankItem::clear() { + this->data.clear(); + this->amount = 0; + this->show_flags = 0; +} + +PlayerInventory::PlayerInventory() + : num_items(0), + hp_materials_used(0), + tp_materials_used(0), + language(0) {} + +void PlayerBank::add_item(const PlayerBankItem& item) { + uint32_t pid = item.data.primary_identifier(); + + if (pid == MESETA_IDENTIFIER) { + this->meseta += item.data.data2d; + if (this->meseta > 999999) { + this->meseta = 999999; + } + return; + } + + size_t combine_max = item.data.max_stack_size(); + if (combine_max > 1) { + size_t y; + for (y = 0; y < this->num_items; y++) { + if (this->items[y].data.primary_identifier() == item.data.primary_identifier()) { + break; + } + } + + if (y < this->num_items) { + this->items[y].data.data1[5] += item.data.data1[5]; + if (this->items[y].data.data1[5] > combine_max) { + this->items[y].data.data1[5] = combine_max; + } + this->items[y].amount = this->items[y].data.data1[5]; + return; + } + } + + if (this->num_items >= 200) { + throw runtime_error("bank is full"); + } + this->items[this->num_items] = item; + this->num_items++; +} + +PlayerBankItem PlayerBank::remove_item(uint32_t item_id, uint32_t amount) { + PlayerBankItem ret; + + if (item_id == 0xFFFFFFFF) { + if (amount > this->meseta) { + throw out_of_range("player does not have enough meseta"); + } + ret.data.data1[0] = 0x04; + ret.data.data2d = amount; + this->meseta -= amount; + return ret; + } + + size_t index = this->find_item(item_id); + auto& bank_item = this->items[index]; + + if (amount && (bank_item.data.stack_size() > 1) && + (amount < bank_item.data.data1[5])) { + ret = bank_item; + ret.data.data1[5] = amount; + ret.amount = amount; + bank_item.data.data1[5] -= amount; + bank_item.amount -= amount; + return ret; + } + + ret = bank_item; + this->num_items--; + for (size_t x = index; x < this->num_items; x++) { + this->items[x] = this->items[x + 1]; + } + this->items[this->num_items] = PlayerBankItem(); + return ret; +} + +size_t PlayerInventory::find_item(uint32_t item_id) const { + for (size_t x = 0; x < this->num_items; x++) { + if (this->items[x].data.id == item_id) { + return x; + } + } + throw out_of_range("item not present"); +} + +size_t PlayerInventory::find_equipped_weapon() const { + ssize_t ret = -1; + for (size_t y = 0; y < this->num_items; y++) { + if (!(this->items[y].flags & 0x00000008)) { + continue; + } + if (this->items[y].data.data1[0] != 0) { + continue; + } + if (ret < 0) { + ret = y; + } else { + throw runtime_error("multiple weapons are equipped"); + } + } + if (ret < 0) { + throw out_of_range("no weapon is equipped"); + } + return ret; +} + +size_t PlayerInventory::find_equipped_armor() const { + ssize_t ret = -1; + for (size_t y = 0; y < this->num_items; y++) { + if (!(this->items[y].flags & 0x00000008)) { + continue; + } + if (this->items[y].data.data1[0] != 1 || this->items[y].data.data1[1] != 1) { + continue; + } + if (ret < 0) { + ret = y; + } else { + throw runtime_error("multiple armors are equipped"); + } + } + if (ret < 0) { + throw out_of_range("no armor is equipped"); + } + return ret; +} + +size_t PlayerInventory::find_equipped_mag() const { + ssize_t ret = -1; + for (size_t y = 0; y < this->num_items; y++) { + if (!(this->items[y].flags & 0x00000008)) { + continue; + } + if (this->items[y].data.data1[0] != 2) { + continue; + } + if (ret < 0) { + ret = y; + } else { + throw runtime_error("multiple mags are equipped"); + } + } + if (ret < 0) { + throw out_of_range("no mag is equipped"); + } + return ret; +} + +size_t PlayerBank::find_item(uint32_t item_id) { + for (size_t x = 0; x < this->num_items; x++) { + if (this->items[x].data.id == item_id) { + return x; + } + } + throw out_of_range("item not present"); +} + +void SavedPlayerDataBB::print_inventory(FILE* stream) 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 = item.data.name(false); + auto hex = item.data.hex(); + fprintf(stream, "[PlayerInventory] %zu: %s (%s)\n", x, hex.c_str(), name.c_str()); + } +} diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh new file mode 100644 index 00000000..f1c0a242 --- /dev/null +++ b/src/PlayerSubordinates.hh @@ -0,0 +1,462 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include "FileContentsCache.hh" +#include "ItemData.hh" +#include "LevelTable.hh" +#include "Text.hh" + +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 +// the InventoryItem struct to store some things not present in V1. The game +// stores arrays of bytes striped across these structures. In newserv, we call +// those fields extension_data. They contain: +// items[0].extension_data1 through items[19].extension_data1: +// Extended technique levels. The values in the v1_technique_levels array +// only go up to 14 (tech level 15); if the player has a technique above +// level 15, the corresponding extension_data1 field holds the remaining +// levels (so a level 20 tech would have 14 in v1_technique_levels and 5 +// in the corresponding item's extension_data1 field). +// items[0].extension_data2 through items[3].extension_data2: +// The value known as unknown_a1 in the PSOGCCharacterFile::Character +// struct. See SaveFileFormats.hh. +// items[4].extension_data2 through items[7].extension_data2: +// The timestamp when the character was last saved, in seconds since +// January 1, 2000. Stored little-endian, so items[4] contains the LSB. +// items[8].extension_data2 through items[12].extension_data2: +// Number of power materials, mind materials, evade materials, def +// materials, and luck materials (respectively) used by the player. +// items[13].extension_data2 through items[15].extension_data2: +// Unknown. These are not an array, but do appear to be related. + +struct PlayerBankItem; + +struct PlayerInventoryItem { // 0x1C bytes + le_uint16_t present; + // See note above about these fields + uint8_t extension_data1; + uint8_t extension_data2; + le_uint32_t flags; // 8 = equipped + ItemData data; + + PlayerInventoryItem(); + PlayerInventoryItem(const PlayerBankItem&); + void clear(); +} __attribute__((packed)); + +struct PlayerBankItem { // 0x18 bytes + ItemData data; + le_uint16_t amount; + le_uint16_t show_flags; + + PlayerBankItem(); + PlayerBankItem(const PlayerInventoryItem&); + void clear(); +} __attribute__((packed)); + +struct PlayerInventory { // 0x34C bytes + uint8_t num_items; + uint8_t hp_materials_used; + uint8_t tp_materials_used; + uint8_t language; + PlayerInventoryItem items[30]; + + PlayerInventory(); + + size_t find_item(uint32_t item_id) const; + + size_t find_equipped_weapon() const; + size_t find_equipped_armor() const; + size_t find_equipped_mag() const; +} __attribute__((packed)); + +struct PlayerBank { // 0x12C8 bytes + le_uint32_t num_items; + le_uint32_t meseta; + PlayerBankItem items[200]; + + 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 PlayerBankItem& item); + PlayerBankItem remove_item(uint32_t item_id, uint32_t amount); + size_t find_item(uint32_t item_id); +} __attribute__((packed)); + +struct PlayerDispDataBB; + +struct PlayerStats { + /* 00 */ CharacterStats char_stats; + /* 0E */ le_uint16_t unknown_a1 = 0; + /* 10 */ le_float unknown_a2 = 0.0; + /* 14 */ le_float unknown_a3 = 0.0; + /* 18 */ le_uint32_t level = 0; + /* 1C */ le_uint32_t experience = 0; + /* 20 */ le_uint32_t meseta = 0; + /* 24 */ + + PlayerStats() noexcept; +} __attribute__((packed)); + +struct PlayerVisualConfig { + /* 00 */ ptext name; + /* 10 */ le_uint64_t unknown_a2 = 0; // Note: This is probably not actually a 64-bit int. + /* 18 */ le_uint32_t name_color = 0xFFFFFFFF; // RGBA + /* 1C */ uint8_t extra_model = 0; + /* 1D */ parray unused; + /* 2C */ le_uint32_t unknown_a3 = 0; + /* 30 */ uint8_t section_id = 0; + /* 31 */ uint8_t char_class = 0; + /* 32 */ uint8_t v2_flags = 0; + /* 33 */ uint8_t version = 0; + /* 34 */ le_uint32_t v1_flags = 0; + /* 38 */ le_uint16_t costume = 0; + /* 3A */ le_uint16_t skin = 0; + /* 3C */ le_uint16_t face = 0; + /* 3E */ le_uint16_t head = 0; + /* 40 */ le_uint16_t hair = 0; + /* 42 */ le_uint16_t hair_r = 0; + /* 44 */ le_uint16_t hair_g = 0; + /* 46 */ le_uint16_t hair_b = 0; + /* 48 */ le_float proportion_x = 0.0; + /* 4C */ le_float proportion_y = 0.0; + /* 50 */ + + PlayerVisualConfig() noexcept; +} __attribute__((packed)); + +struct PlayerDispDataDCPCV3 { + /* 00 */ PlayerStats stats; + /* 24 */ PlayerVisualConfig visual; + /* 74 */ parray config; + /* BC */ parray v1_technique_levels; + /* D0 */ + + // Note: This struct has a default constructor because it's used in a command + // that has a fixed-size array. If we didn't define this constructor, the + // trivial fields in that array's members would be uninitialized, and we could + // send uninitialized memory to the client. + PlayerDispDataDCPCV3() noexcept = default; + + void enforce_v2_limits(); + PlayerDispDataBB to_bb() const; +} __attribute__((packed)); + +struct PlayerDispDataBBPreview { + /* 00 */ le_uint32_t experience; + /* 04 */ le_uint32_t level; + // The name field in this structure is used for the player's Guild Card + // number, apparently (possibly because it's a char array and this is BB) + /* 08 */ PlayerVisualConfig visual; + /* 58 */ ptext name; + /* 78 */ uint32_t play_time; + /* 7C */ + + PlayerDispDataBBPreview() noexcept; +} __attribute__((packed)); + +// BB player appearance and stats data +struct PlayerDispDataBB { + /* 0000 */ PlayerStats stats; + /* 0024 */ PlayerVisualConfig visual; + /* 0074 */ ptext name; + /* 008C */ le_uint32_t play_time; + /* 0090 */ uint32_t unknown_a3; + /* 0094 */ parray config; + /* 017C */ parray technique_levels; + /* 0190 */ + + PlayerDispDataBB() noexcept; + + inline void enforce_v2_limits() {} + PlayerDispDataDCPCV3 to_dcpcv3() const; + PlayerDispDataBBPreview to_preview() const; + void apply_preview(const PlayerDispDataBBPreview&); + void apply_dressing_room(const PlayerDispDataBBPreview&); +} __attribute__((packed)); + +// TODO: Is this the same for XB as it is for GC? (This struct is based on the +// GC format) +struct GuildCardV3 { + /* 00 */ le_uint32_t player_tag; + /* 04 */ le_uint32_t guild_card_number; + /* 08 */ ptext name; + /* 20 */ ptext description; + /* 8C */ uint8_t present; // should be 1 + /* 8D */ uint8_t language; + /* 8E */ uint8_t section_id; + /* 8F */ uint8_t char_class; + /* 90 */ + + GuildCardV3() noexcept; +} __attribute__((packed)); + +// BB guild card format +struct GuildCardBB { + /* 0000 */ le_uint32_t guild_card_number; + /* 0004 */ ptext name; + /* 0034 */ ptext team_name; + /* 0054 */ ptext description; + /* 0104 */ uint8_t present; // should be 1 if guild card entry exists + /* 0105 */ uint8_t language; + /* 0106 */ uint8_t section_id; + /* 0107 */ uint8_t char_class; + /* 0108 */ + + GuildCardBB() noexcept; + void clear(); +} __attribute__((packed)); + +// an entry in the BB guild card file +struct GuildCardEntryBB { + GuildCardBB data; + ptext comment; + parray unknown_a1; + + void clear(); +} __attribute__((packed)); + +// the format of the BB guild card file +struct GuildCardFileBB { + parray unknown_a1; + GuildCardBB blocked[0x1C]; + parray unknown_a2; + GuildCardEntryBB entries[0x69]; + + uint32_t checksum() const; +} __attribute__((packed)); + +struct KeyAndTeamConfigBB { + parray unknown_a1; // 0000 + parray key_config; // 0114 + parray joystick_config; // 0280 + le_uint32_t guild_card_number; // 02B8 + le_uint32_t team_id; // 02BC + le_uint64_t team_info; // 02C0 + le_uint16_t team_privilege_level; // 02C8 + le_uint16_t reserved; // 02CA + ptext team_name; // 02CC + parray team_flag; // 02EC + le_uint32_t team_rewards; // 0AEC +} __attribute__((packed)); + +struct PlayerLobbyDataPC { + le_uint32_t player_tag = 0; + le_uint32_t guild_card = 0; + // There's a strange behavior (bug? "feature"?) in Episode 3 where the start + // button does nothing in the lobby (hence you can't "quit game") if the + // client's IP address is zero. So, we fill it in with a fake nonzero value to + // avoid this behavior, and to be consistent, we make IP addresses fake and + // nonzero on all other versions too. + be_uint32_t ip_address = 0x7F000001; + le_uint32_t client_id = 0; + ptext name; + + void clear(); +} __attribute__((packed)); + +struct PlayerLobbyDataDCGC { + le_uint32_t player_tag = 0; + le_uint32_t guild_card = 0; + be_uint32_t ip_address = 0x7F000001; + le_uint32_t client_id = 0; + ptext name; + + void clear(); +} __attribute__((packed)); + +struct XBNetworkLocation { + le_uint32_t internal_ipv4_address = 0x0A0A0A0A; + le_uint32_t external_ipv4_address = 0x23232323; + le_uint16_t port = 9100; + parray mac_address = 0x77; + parray unknown_a1; + le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF; + parray unknown_a2; + + void clear(); +} __attribute__((packed)); + +struct PlayerLobbyDataXB { + le_uint32_t player_tag = 0; + le_uint32_t guild_card = 0; + XBNetworkLocation netloc; + le_uint32_t client_id = 0; + ptext name; + + void clear(); +} __attribute__((packed)); + +struct PlayerLobbyDataBB { + le_uint32_t player_tag = 0; + le_uint32_t guild_card = 0; + // This field is a guess; the official builds didn't use this, but all other + // versions have it + be_uint32_t ip_address = 0x7F000001; + parray unknown_a1; + le_uint32_t client_id = 0; + ptext name; + le_uint32_t unknown_a2 = 0; + + void clear(); +} __attribute__((packed)); + +template +struct PlayerRecordsDCPC_Challenge { + using CharT = typename std::conditional::type; + + /* 00 */ le_uint16_t title_color = 0x7FFF; + /* 02 */ parray unknown_u0; + /* 04 */ ptext rank_title; // Encrypted; see decrypt_challenge_rank_text + /* 10 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times + /* 34 */ le_uint16_t unknown_g3 = 0; + /* 36 */ le_uint16_t grave_deaths = 0; + /* 38 */ parray grave_coords_time; + /* 4C */ ptext grave_team; + /* 60 */ ptext grave_message; + /* 78 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times + /* 9C */ parray unknown_l4; + /* A0 */ +} __attribute__((packed)); + +struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge { +} __attribute__((packed)); + +struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge { +} __attribute__((packed)); + +template +struct PlayerRecordsV3_Challenge { + using U16T = typename std::conditional::type; + using U32T = typename std::conditional::type; + + // Offsets are (1) relative to start of C5 entry, and (2) relative to start + // of save file structure + struct Stats { + /* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555 + /* 02:1E */ parray unknown_u0; + /* 04:20 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time + /* 28:44 */ parray times_ep2_online; // Encrypted; see decrypt_challenge_time + /* 3C:58 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time + /* 60:7C */ parray unknown_g3; + /* 64:80 */ U16T grave_deaths = 0; + /* 66:82 */ parray unknown_u4; + /* 68:84 */ parray grave_coords_time; + /* 7C:98 */ ptext grave_team; + /* 90:AC */ ptext grave_message; + /* B0:CC */ parray unknown_m5; + /* B4:D0 */ parray unknown_t6; + /* D8:F4 */ + } __attribute__((packed)); + /* 0000:001C */ Stats stats; + // On Episode 3, there are special cases that apply to this field - if the + // text ends with certain strings (after decrypt_challenge_rank_text), the + // player will have particle effects emanate from their character in the + // lobby every 2 seconds. These effects are: + // Ends with ":GOD" => blue circle + // Ends with ":KING" => white particles + // Ends with ":LORD" => rising yellow sparkles + // Ends with ":CHAMP" => green circle + /* 00D8:00F4 */ ptext rank_title; + /* 00E4:0100 */ parray unknown_l7; + /* 0100:011C */ +} __attribute__((packed)); + +struct PlayerRecordsBB_Challenge { + /* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555 + /* 0002 */ parray unknown_u0; + /* 0004 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time + /* 0028 */ parray times_ep2_online; // Encrypted; see decrypt_challenge_time + /* 003C */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time + /* 0060 */ parray unknown_g3; + /* 0064 */ le_uint16_t grave_deaths = 0; + /* 0066 */ parray unknown_u4; + /* 0068 */ parray grave_coords_time; + /* 007C */ ptext grave_team; + /* 00A4 */ ptext grave_message; + /* 00E4 */ parray unknown_m5; + /* 00E8 */ parray unknown_t6; + /* 010C */ ptext rank_title; // Encrypted; see decrypt_challenge_rank_text + /* 0124 */ parray unknown_l7; + /* 0140 */ + + PlayerRecordsBB_Challenge() = default; + PlayerRecordsBB_Challenge(const PlayerRecordsBB_Challenge& other) = default; + PlayerRecordsBB_Challenge& operator=(const PlayerRecordsBB_Challenge& other) = default; + + PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec); + PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec); + PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge& rec); + + operator PlayerRecordsDC_Challenge() const; + operator PlayerRecordsPC_Challenge() const; + operator PlayerRecordsV3_Challenge() const; +} __attribute__((packed)); + +template +struct PlayerRecords_Battle { + using U16T = typename std::conditional::type; + // On Episode 3, place_counts[0] is win count and [1] is loss count + /* 00 */ parray place_counts; + /* 08 */ U16T disconnect_count; + /* 0A */ parray unknown_a1; + /* 10 */ parray unknown_a2; + /* 18 */ +} __attribute__((packed)); + +template +struct ChoiceSearchConfig { + // 0 = enabled, 1 = disabled. Unused for command C3 + le_uint32_t choice_search_disabled = 0; + struct Entry { + ItemIDT parent_category_id = 0; + ItemIDT category_id = 0; + } __attribute__((packed)); + parray entries; +} __attribute__((packed)); + +uint32_t compute_guild_card_checksum(const void* data, size_t size); + +template +DestT convert_player_disp_data(const SrcT&) { + static_assert(always_false::v, + "unspecialized strcpy_t should never be called"); +} + +template <> +inline PlayerDispDataDCPCV3 convert_player_disp_data( + const PlayerDispDataDCPCV3& src) { + return src; +} + +template <> +inline PlayerDispDataDCPCV3 convert_player_disp_data( + const PlayerDispDataBB& src) { + return src.to_dcpcv3(); +} + +template <> +inline PlayerDispDataBB convert_player_disp_data( + const PlayerDispDataDCPCV3& src) { + return src.to_bb(); +} + +template <> +inline PlayerDispDataBB convert_player_disp_data( + const PlayerDispDataBB& src) { + return src; +}