fix incorrect type in Ep3 PlayerConfig
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user