Files
psopeeps-newserv/src/PlayerInventory.hh
T

386 lines
13 KiB
C++

#pragma once
#include <inttypes.h>
#include <stddef.h>
#include <algorithm>
#include <array>
#include <phosg/Encoding.hh>
#include <phosg/JSON.hh>
#include <string>
#include <utility>
#include <vector>
#include "ChoiceSearch.hh"
#include "FileContentsCache.hh"
#include "ItemData.hh"
#include "PSOEncryption.hh"
#include "Text.hh"
#include "Version.hh"
class Client;
class ItemParameterTable;
// 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 technique_levels_v1 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 technique_levels_v1 and 5
// in the corresponding item's extension_data1 field).
// items[0].extension_data2 through items[3].extension_data2:
// The flags field from the PSOGCCharacterFile::Character struct; see
// SaveFileFormats.hh for details.
// 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.
template <bool BE>
struct PlayerInventoryItemT {
/* 00 */ uint8_t present = 0;
/* 01 */ uint8_t unknown_a1 = 0;
// See note above about these fields
/* 02 */ uint8_t extension_data1 = 0;
/* 03 */ uint8_t extension_data2 = 0;
/* 04 */ U32T<BE> flags = 0; // 8 = equipped
/* 08 */ ItemData data;
/* 1C */
PlayerInventoryItemT() = default;
PlayerInventoryItemT(const ItemData& item, bool equipped)
: present(1),
unknown_a1(0),
extension_data1(0),
extension_data2(0),
flags(equipped ? 8 : 0),
data(item) {}
operator PlayerInventoryItemT<!BE>() const {
PlayerInventoryItemT<!BE> ret;
ret.present = this->present;
ret.unknown_a1 = this->unknown_a1;
ret.extension_data1 = this->extension_data1;
ret.extension_data2 = this->extension_data2;
ret.flags = this->flags;
ret.data = this->data;
ret.data.id.store_raw(phosg::bswap32(ret.data.id.load_raw()));
return ret;
}
bool is_equipped() const {
return (this->flags & 8);
}
} __attribute__((packed));
using PlayerInventoryItem = PlayerInventoryItemT<false>;
using PlayerInventoryItemBE = PlayerInventoryItemT<true>;
check_struct_size(PlayerInventoryItem, 0x1C);
check_struct_size(PlayerInventoryItemBE, 0x1C);
template <bool BE>
struct PlayerBankItemT {
/* 00 */ ItemData data;
/* 14 */ U16T<BE> amount = 0;
/* 16 */ U16T<BE> present = 0;
/* 18 */
inline bool operator<(const PlayerBankItemT<BE>& other) const {
return this->data < other.data;
}
operator PlayerBankItemT<!BE>() const {
PlayerBankItemT<!BE> ret;
ret.data = this->data;
ret.amount = this->amount;
ret.present = this->present;
return ret;
}
} __attribute__((packed));
using PlayerBankItem = PlayerBankItemT<false>;
using PlayerBankItemBE = PlayerBankItemT<true>;
check_struct_size(PlayerBankItem, 0x18);
check_struct_size(PlayerBankItemBE, 0x18);
template <bool BE>
struct PlayerInventoryT {
/* 0000 */ uint8_t num_items = 0;
/* 0001 */ uint8_t hp_from_materials = 0;
/* 0002 */ uint8_t tp_from_materials = 0;
/* 0003 */ Language language = Language::JAPANESE;
/* 0004 */ parray<PlayerInventoryItemT<BE>, 30> items;
/* 034C */
size_t 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 std::out_of_range("item not present");
}
size_t find_item_by_primary_identifier(uint32_t primary_identifier) const {
for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.primary_identifier() == primary_identifier) {
return x;
}
}
throw std::out_of_range("item not present");
}
size_t find_equipped_item(EquipSlot slot) const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
const auto& i = this->items[y];
if (!(i.flags & 0x00000008)) {
continue;
}
if (!i.data.can_be_equipped_in_slot(slot)) {
continue;
}
// Units can be equipped in multiple slots, so the currently-equipped slot
// is stored in the item data itself.
if (((slot == EquipSlot::UNIT_1) && (i.data.data1[4] != 0x00)) ||
((slot == EquipSlot::UNIT_2) && (i.data.data1[4] != 0x01)) ||
((slot == EquipSlot::UNIT_3) && (i.data.data1[4] != 0x02)) ||
((slot == EquipSlot::UNIT_4) && (i.data.data1[4] != 0x03))) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw std::runtime_error("multiple items are equipped in the same slot");
}
}
if (ret < 0) {
throw std::out_of_range("no item is equipped in this slot");
}
return ret;
}
bool has_equipped_item(EquipSlot slot) const {
try {
this->find_equipped_item(slot);
return true;
} catch (const std::out_of_range&) {
return false;
}
}
void equip_item_id(uint32_t item_id, EquipSlot slot) {
this->equip_item_index(this->find_item(item_id), slot);
}
void equip_item_index(size_t index, EquipSlot slot) {
auto& item = this->items[index];
if ((slot == EquipSlot::UNKNOWN) || !item.data.can_be_equipped_in_slot(slot)) {
slot = item.data.default_equip_slot();
}
if (this->has_equipped_item(slot)) {
this->unequip_item_slot(slot);
}
item.flags |= 0x00000008;
// Units store which slot they're equipped in within the item data itself
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x03)) {
item.data.data1[4] = static_cast<uint8_t>(slot) - 9;
}
}
void unequip_item_id(uint32_t item_id) {
this->unequip_item_index(this->find_item(item_id));
}
void unequip_item_slot(EquipSlot slot) {
this->unequip_item_index(this->find_equipped_item(slot));
}
void unequip_item_index(size_t index) {
auto& item = this->items[index];
item.flags &= (~0x00000008);
// Units store which slot they're equipped in within the item data itself
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x03)) {
item.data.data1[4] = 0x00;
}
// If the item is an armor, remove all units too
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x01)) {
for (size_t z = 0; z < 30; z++) {
auto& unit = this->items[z];
if ((unit.data.data1[0] == 0x01) && (unit.data.data1[1] == 0x03)) {
unit.flags &= (~0x00000008);
unit.data.data1[4] = 0x00;
}
}
}
}
size_t remove_all_items_of_type(uint8_t data1_0, int16_t data1_1 = -1) {
size_t write_offset = 0;
for (size_t read_offset = 0; read_offset < this->num_items; read_offset++) {
bool should_delete = ((this->items[read_offset].data.data1[0] == data1_0) &&
((data1_1 < 0) || (this->items[read_offset].data.data1[1] == static_cast<uint8_t>(data1_1))));
if (!should_delete) {
if (read_offset != write_offset) {
this->items[write_offset].present = this->items[read_offset].present;
this->items[write_offset].unknown_a1 = this->items[read_offset].unknown_a1;
this->items[write_offset].flags = this->items[read_offset].flags;
this->items[write_offset].data = this->items[read_offset].data;
}
write_offset++;
}
}
size_t ret = this->num_items - write_offset;
this->num_items = write_offset;
return ret;
}
void decode_from_client(Version v) {
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.decode_for_version(v);
}
}
void encode_for_client(Version v, std::shared_ptr<const ItemParameterTable> item_parameter_table) {
if (v == Version::DC_NTE) {
// DC NTE has the item count as a 32-bit value here, whereas every other
// version uses a single byte. To stop DC NTE from crashing by trying to
// construct far more than 30 TItem objects, we clear the fields DC NTE
// doesn't know about. Note that the 11/2000 prototype does not have this
// issue - its inventory format matches the rest of the versions.
this->hp_from_materials = 0;
this->tp_from_materials = 0;
this->language = Language::JAPANESE;
} else if ((v != Version::PC_NTE) && (v != Version::PC_V2)) {
if (static_cast<size_t>(this->language) > 4) {
this->language = Language::JAPANESE;
}
} else {
if (static_cast<size_t>(this->language) > 7) {
this->language = Language::JAPANESE;
}
}
// For pre-V2 clients, use the V2 parameter table, since the V1 table
// doesn't have correct encodings for backward-compatible V2 items.
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.encode_for_version(v, item_parameter_table);
}
}
void enforce_stack_limits(std::shared_ptr<const ItemData::StackLimits> stack_limits) {
for (size_t z = 0; z < std::min<uint8_t>(this->num_items, this->items.size()); z++) {
this->items[z].data.enforce_stack_size_limits(*stack_limits);
}
}
operator PlayerInventoryT<!BE>() const {
PlayerInventoryT<!BE> ret;
ret.num_items = this->num_items;
ret.hp_from_materials = this->hp_from_materials;
ret.tp_from_materials = this->tp_from_materials;
ret.language = this->language;
ret.items = this->items;
return ret;
}
} __attribute__((packed));
using PlayerInventory = PlayerInventoryT<false>;
using PlayerInventoryBE = PlayerInventoryT<true>;
check_struct_size(PlayerInventory, 0x34C);
check_struct_size(PlayerInventoryBE, 0x34C);
template <size_t SlotCount, bool BE>
struct PlayerBankT {
/* 0000 */ U32T<BE> num_items = 0;
/* 0004 */ U32T<BE> meseta = 0;
/* 0008 */ parray<PlayerBankItemT<BE>, SlotCount> items;
/* 05A8 for 60 items (v1/v2), 12C8 for 200 items (v3/v4) */
void decode_from_client(Version v) {
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.decode_for_version(v);
}
}
void encode_for_client(Version v) {
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.encode_for_version(v, nullptr);
}
}
template <size_t DestSlotCount, bool DestBE>
operator PlayerBankT<DestSlotCount, DestBE>() const {
PlayerBankT<DestSlotCount, DestBE> ret;
ret.num_items = std::min<size_t>(ret.items.size(), this->num_items);
ret.meseta = this->meseta;
for (size_t z = 0; z < std::min<size_t>(ret.items.size(), this->items.size()); z++) {
ret.items[z] = this->items[z];
}
return ret;
}
} __attribute__((packed));
using PlayerBank60 = PlayerBankT<60, false>;
using PlayerBank200 = PlayerBankT<200, false>;
using PlayerBank200BE = PlayerBankT<200, true>;
check_struct_size(PlayerBank60, 0x05A8);
check_struct_size(PlayerBank200, 0x12C8);
check_struct_size(PlayerBank200BE, 0x12C8);
struct PlayerBank {
uint32_t max_meseta = 999999;
uint32_t max_items = 200;
uint32_t meseta = 0;
std::vector<PlayerBankItem> items;
PlayerBank() = default;
template <size_t SrcSlotCount, bool SrcBE>
PlayerBank(const PlayerBankT<SrcSlotCount, SrcBE>& src)
: max_meseta(999999), max_items(SrcSlotCount), meseta(src.meseta) {
this->items.reserve(src.num_items);
for (size_t z = 0; z < src.num_items; z++) {
this->items.emplace_back(src.items[z]);
}
}
template <size_t DestSlotCount, bool DestBE>
operator PlayerBankT<DestSlotCount, DestBE>() const {
PlayerBankT<DestSlotCount, DestBE> ret;
ret.num_items = std::min<size_t>(ret.items.size(), this->items.size());
ret.meseta = this->meseta;
for (size_t z = 0; z < ret.num_items; z++) {
ret.items[z] = this->items[z];
}
return ret;
}
void load(FILE* f);
void save(FILE* f) const;
uint32_t bb_checksum() const;
void add_item(const ItemData& item, const ItemData::StackLimits& limits);
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits);
size_t find_item(uint32_t item_id);
void sort();
void assign_ids(uint32_t base_id);
void enforce_stack_limits(std::shared_ptr<const ItemData::StackLimits> stack_limits);
};