fix incorrect type in Ep3 PlayerConfig

This commit is contained in:
Martin Michelsen
2023-09-19 09:16:10 -07:00
parent 7706adc7cb
commit 4f16243e41
6 changed files with 999 additions and 970 deletions
+2 -4
View File
@@ -13,6 +13,7 @@
#include <string>
#include <unordered_map>
#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<uint8_t, 0x50> backup_visual;
/* 2274 */ PlayerVisualConfig backup_visual;
/* 22C4 */ parray<uint8_t, 0x8C> unknown_a14;
/* 2350 */
-519
View File
@@ -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<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));
}
}
////////////////////////////////////////////////////////////////////////////////
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<char16_t, 0x10> name;
}
void PlayerLobbyDataDCGC::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->ip_address = 0;
this->client_id = 0;
ptext<char, 0x10> 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<false>& 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<false>() const {
PlayerRecordsV3_Challenge<false> 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());
}
}
+1 -447
View File
@@ -10,92 +10,10 @@
#include <vector>
#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<std::pair<uint32_t, uint32_t>> 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<char, 0x10> 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<uint8_t, 0x0F> 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<uint8_t, 0x48> config;
/* BC */ parray<uint8_t, 0x14> 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<char16_t, 0x10> 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<char16_t, 0x0C> name;
/* 008C */ le_uint32_t play_time;
/* 0090 */ uint32_t unknown_a3;
/* 0094 */ parray<uint8_t, 0xE8> config;
/* 017C */ parray<uint8_t, 0x14> 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<char, 0x18> name;
/* 20 */ ptext<char, 0x6C> 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<char16_t, 0x18> name;
/* 0034 */ ptext<char16_t, 0x10> team_name;
/* 0054 */ ptext<char16_t, 0x58> 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<char16_t, 0x58> comment;
parray<uint8_t, 0x4> unknown_a1;
void clear();
} __attribute__((packed));
// the format of the BB guild card file
struct GuildCardFileBB {
parray<uint8_t, 0x114> unknown_a1;
GuildCardBB blocked[0x1C];
parray<uint8_t, 0x180> unknown_a2;
GuildCardEntryBB entries[0x69];
uint32_t checksum() const;
} __attribute__((packed));
struct KeyAndTeamConfigBB {
parray<uint8_t, 0x0114> unknown_a1; // 0000
parray<uint8_t, 0x016C> key_config; // 0114
parray<uint8_t, 0x0038> 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<char16_t, 0x0010> team_name; // 02CC
parray<uint8_t, 0x0800> 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<char16_t, 0x10> 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<char, 0x10> 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<uint8_t, 6> mac_address = 0x77;
parray<le_uint32_t, 2> unknown_a1;
le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
parray<le_uint32_t, 4> 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<char, 0x10> 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<uint8_t, 0x10> unknown_a1;
le_uint32_t client_id = 0;
ptext<char16_t, 0x10> name;
le_uint32_t unknown_a2 = 0;
void clear();
} __attribute__((packed));
template <bool IsWideChar>
struct PlayerRecordsDCPC_Challenge {
using CharT = typename std::conditional<IsWideChar, char16_t, char>::type;
/* 00 */ le_uint16_t title_color = 0x7FFF;
/* 02 */ parray<uint8_t, 2> unknown_u0;
/* 04 */ ptext<CharT, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 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;
/* 36 */ le_uint16_t grave_deaths = 0;
/* 38 */ parray<le_uint32_t, 5> grave_coords_time;
/* 4C */ ptext<CharT, 0x14> grave_team;
/* 60 */ ptext<CharT, 0x18> grave_message;
/* 78 */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times
/* 9C */ parray<uint8_t, 4> unknown_l4;
/* A0 */
} __attribute__((packed));
struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge<false> {
} __attribute__((packed));
struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge<true> {
} __attribute__((packed));
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;
// 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<uint8_t, 2> unknown_u0;
/* 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;
/* 64:80 */ U16T grave_deaths = 0;
/* 66:82 */ parray<uint8_t, 2> unknown_u4;
/* 68:84 */ parray<U32T, 5> grave_coords_time;
/* 7C:98 */ ptext<char, 0x14> grave_team;
/* 90:AC */ ptext<char, 0x20> grave_message;
/* B0:CC */ parray<uint8_t, 4> unknown_m5;
/* B4:D0 */ parray<U32T, 9> 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<char, 0x0C> rank_title;
/* 00E4:0100 */ parray<uint8_t, 0x1C> unknown_l7;
/* 0100:011C */
} __attribute__((packed));
struct PlayerRecordsBB_Challenge {
/* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555
/* 0002 */ parray<uint8_t, 2> unknown_u0;
/* 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;
/* 0064 */ le_uint16_t grave_deaths = 0;
/* 0066 */ parray<uint8_t, 2> unknown_u4;
/* 0068 */ parray<le_uint32_t, 5> grave_coords_time;
/* 007C */ ptext<char16_t, 0x14> grave_team;
/* 00A4 */ ptext<char16_t, 0x20> grave_message;
/* 00E4 */ parray<uint8_t, 4> unknown_m5;
/* 00E8 */ parray<le_uint32_t, 9> unknown_t6;
/* 010C */ ptext<char16_t, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 0124 */ parray<uint8_t, 0x1C> 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<false>& rec);
operator PlayerRecordsDC_Challenge() const;
operator PlayerRecordsPC_Challenge() const;
operator PlayerRecordsV3_Challenge<false>() const;
} __attribute__((packed));
template <bool IsBigEndian>
struct PlayerRecords_Battle {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
// On Episode 3, place_counts[0] is win count and [1] is loss count
/* 00 */ parray<U16T, 4> place_counts;
/* 08 */ U16T disconnect_count;
/* 0A */ parray<uint16_t, 3> unknown_a1;
/* 10 */ parray<uint32_t, 2> unknown_a2;
/* 18 */
} __attribute__((packed));
template <typename ItemIDT>
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<Entry, 5> 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 <typename DestT, typename SrcT = DestT>
DestT convert_player_disp_data(const SrcT&) {
static_assert(always_false<DestT, SrcT>::v,
"unspecialized strcpy_t should never be called");
}
template <>
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3>(
const PlayerDispDataDCPCV3& src) {
return src;
}
template <>
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src.to_dcpcv3();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB, PlayerDispDataDCPCV3>(
const PlayerDispDataDCPCV3& src) {
return src.to_bb();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src;
}
+533
View File
@@ -0,0 +1,533 @@
#include "PlayerSubordinates.hh"
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
#include <stdexcept>
#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<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 = 0;
this->ip_address = 0;
this->client_id = 0;
ptext<char16_t, 0x10> name;
}
void PlayerLobbyDataDCGC::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->ip_address = 0;
this->client_id = 0;
ptext<char, 0x10> 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<false>& 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<false>() const {
PlayerRecordsV3_Challenge<false> 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());
}
}
+462
View File
@@ -0,0 +1,462 @@
#pragma once
#include <inttypes.h>
#include <stddef.h>
#include <array>
#include <phosg/Encoding.hh>
#include <string>
#include <utility>
#include <vector>
#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<char, 0x10> 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<uint8_t, 0x0F> 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<uint8_t, 0x48> config;
/* BC */ parray<uint8_t, 0x14> 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<char16_t, 0x10> 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<char16_t, 0x0C> name;
/* 008C */ le_uint32_t play_time;
/* 0090 */ uint32_t unknown_a3;
/* 0094 */ parray<uint8_t, 0xE8> config;
/* 017C */ parray<uint8_t, 0x14> 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<char, 0x18> name;
/* 20 */ ptext<char, 0x6C> 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<char16_t, 0x18> name;
/* 0034 */ ptext<char16_t, 0x10> team_name;
/* 0054 */ ptext<char16_t, 0x58> 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<char16_t, 0x58> comment;
parray<uint8_t, 0x4> unknown_a1;
void clear();
} __attribute__((packed));
// the format of the BB guild card file
struct GuildCardFileBB {
parray<uint8_t, 0x114> unknown_a1;
GuildCardBB blocked[0x1C];
parray<uint8_t, 0x180> unknown_a2;
GuildCardEntryBB entries[0x69];
uint32_t checksum() const;
} __attribute__((packed));
struct KeyAndTeamConfigBB {
parray<uint8_t, 0x0114> unknown_a1; // 0000
parray<uint8_t, 0x016C> key_config; // 0114
parray<uint8_t, 0x0038> 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<char16_t, 0x0010> team_name; // 02CC
parray<uint8_t, 0x0800> 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<char16_t, 0x10> 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<char, 0x10> 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<uint8_t, 6> mac_address = 0x77;
parray<le_uint32_t, 2> unknown_a1;
le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
parray<le_uint32_t, 4> 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<char, 0x10> 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<uint8_t, 0x10> unknown_a1;
le_uint32_t client_id = 0;
ptext<char16_t, 0x10> name;
le_uint32_t unknown_a2 = 0;
void clear();
} __attribute__((packed));
template <bool IsWideChar>
struct PlayerRecordsDCPC_Challenge {
using CharT = typename std::conditional<IsWideChar, char16_t, char>::type;
/* 00 */ le_uint16_t title_color = 0x7FFF;
/* 02 */ parray<uint8_t, 2> unknown_u0;
/* 04 */ ptext<CharT, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 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;
/* 36 */ le_uint16_t grave_deaths = 0;
/* 38 */ parray<le_uint32_t, 5> grave_coords_time;
/* 4C */ ptext<CharT, 0x14> grave_team;
/* 60 */ ptext<CharT, 0x18> grave_message;
/* 78 */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times
/* 9C */ parray<uint8_t, 4> unknown_l4;
/* A0 */
} __attribute__((packed));
struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge<false> {
} __attribute__((packed));
struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge<true> {
} __attribute__((packed));
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;
// 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<uint8_t, 2> unknown_u0;
/* 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;
/* 64:80 */ U16T grave_deaths = 0;
/* 66:82 */ parray<uint8_t, 2> unknown_u4;
/* 68:84 */ parray<U32T, 5> grave_coords_time;
/* 7C:98 */ ptext<char, 0x14> grave_team;
/* 90:AC */ ptext<char, 0x20> grave_message;
/* B0:CC */ parray<uint8_t, 4> unknown_m5;
/* B4:D0 */ parray<U32T, 9> 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<char, 0x0C> rank_title;
/* 00E4:0100 */ parray<uint8_t, 0x1C> unknown_l7;
/* 0100:011C */
} __attribute__((packed));
struct PlayerRecordsBB_Challenge {
/* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555
/* 0002 */ parray<uint8_t, 2> unknown_u0;
/* 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;
/* 0064 */ le_uint16_t grave_deaths = 0;
/* 0066 */ parray<uint8_t, 2> unknown_u4;
/* 0068 */ parray<le_uint32_t, 5> grave_coords_time;
/* 007C */ ptext<char16_t, 0x14> grave_team;
/* 00A4 */ ptext<char16_t, 0x20> grave_message;
/* 00E4 */ parray<uint8_t, 4> unknown_m5;
/* 00E8 */ parray<le_uint32_t, 9> unknown_t6;
/* 010C */ ptext<char16_t, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 0124 */ parray<uint8_t, 0x1C> 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<false>& rec);
operator PlayerRecordsDC_Challenge() const;
operator PlayerRecordsPC_Challenge() const;
operator PlayerRecordsV3_Challenge<false>() const;
} __attribute__((packed));
template <bool IsBigEndian>
struct PlayerRecords_Battle {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
// On Episode 3, place_counts[0] is win count and [1] is loss count
/* 00 */ parray<U16T, 4> place_counts;
/* 08 */ U16T disconnect_count;
/* 0A */ parray<uint16_t, 3> unknown_a1;
/* 10 */ parray<uint32_t, 2> unknown_a2;
/* 18 */
} __attribute__((packed));
template <typename ItemIDT>
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<Entry, 5> entries;
} __attribute__((packed));
uint32_t compute_guild_card_checksum(const void* data, size_t size);
template <typename DestT, typename SrcT = DestT>
DestT convert_player_disp_data(const SrcT&) {
static_assert(always_false<DestT, SrcT>::v,
"unspecialized strcpy_t should never be called");
}
template <>
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3>(
const PlayerDispDataDCPCV3& src) {
return src;
}
template <>
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src.to_dcpcv3();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB, PlayerDispDataDCPCV3>(
const PlayerDispDataDCPCV3& src) {
return src.to_bb();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src;
}