implement battle rules and character replacement

This commit is contained in:
Martin Michelsen
2023-10-18 11:55:31 -07:00
parent 13dacc013a
commit 8c2ce5210d
24 changed files with 699 additions and 305 deletions
+20 -19
View File
@@ -800,58 +800,59 @@ static void server_command_edit(shared_ptr<Client> c, const std::u16string& args
vector<string> tokens = split(encoded_args, ' '); vector<string> tokens = split(encoded_args, ' ');
try { try {
auto p = c->game_data.player();
if (tokens.at(0) == "atp") { if (tokens.at(0) == "atp") {
c->game_data.player()->disp.stats.char_stats.atp = stoul(tokens.at(1)); p->disp.stats.char_stats.atp = stoul(tokens.at(1));
} else if (tokens.at(0) == "mst") { } else if (tokens.at(0) == "mst") {
c->game_data.player()->disp.stats.char_stats.mst = stoul(tokens.at(1)); p->disp.stats.char_stats.mst = stoul(tokens.at(1));
} else if (tokens.at(0) == "evp") { } else if (tokens.at(0) == "evp") {
c->game_data.player()->disp.stats.char_stats.evp = stoul(tokens.at(1)); p->disp.stats.char_stats.evp = stoul(tokens.at(1));
} else if (tokens.at(0) == "hp") { } else if (tokens.at(0) == "hp") {
c->game_data.player()->disp.stats.char_stats.hp = stoul(tokens.at(1)); p->disp.stats.char_stats.hp = stoul(tokens.at(1));
} else if (tokens.at(0) == "dfp") { } else if (tokens.at(0) == "dfp") {
c->game_data.player()->disp.stats.char_stats.dfp = stoul(tokens.at(1)); p->disp.stats.char_stats.dfp = stoul(tokens.at(1));
} else if (tokens.at(0) == "ata") { } else if (tokens.at(0) == "ata") {
c->game_data.player()->disp.stats.char_stats.ata = stoul(tokens.at(1)); p->disp.stats.char_stats.ata = stoul(tokens.at(1));
} else if (tokens.at(0) == "lck") { } else if (tokens.at(0) == "lck") {
c->game_data.player()->disp.stats.char_stats.lck = stoul(tokens.at(1)); p->disp.stats.char_stats.lck = stoul(tokens.at(1));
} else if (tokens.at(0) == "meseta") { } else if (tokens.at(0) == "meseta") {
c->game_data.player()->disp.stats.meseta = stoul(tokens.at(1)); p->disp.stats.meseta = stoul(tokens.at(1));
} else if (tokens.at(0) == "exp") { } else if (tokens.at(0) == "exp") {
c->game_data.player()->disp.stats.experience = stoul(tokens.at(1)); p->disp.stats.experience = stoul(tokens.at(1));
} else if (tokens.at(0) == "level") { } else if (tokens.at(0) == "level") {
c->game_data.player()->disp.stats.level = stoul(tokens.at(1)) - 1; p->disp.stats.level = stoul(tokens.at(1)) - 1;
} else if (tokens.at(0) == "namecolor") { } else if (tokens.at(0) == "namecolor") {
uint32_t new_color; uint32_t new_color;
sscanf(tokens.at(1).c_str(), "%8X", &new_color); sscanf(tokens.at(1).c_str(), "%8X", &new_color);
c->game_data.player()->disp.visual.name_color = new_color; p->disp.visual.name_color = new_color;
} else if (tokens.at(0) == "secid") { } else if (tokens.at(0) == "secid") {
uint8_t secid = section_id_for_name(decode_sjis(tokens.at(1))); uint8_t secid = section_id_for_name(decode_sjis(tokens.at(1)));
if (secid == 0xFF) { if (secid == 0xFF) {
send_text_message(c, u"$C6No such section ID"); send_text_message(c, u"$C6No such section ID");
return; return;
} else { } else {
c->game_data.player()->disp.visual.section_id = secid; p->disp.visual.section_id = secid;
} }
} else if (tokens.at(0) == "name") { } else if (tokens.at(0) == "name") {
c->game_data.player()->disp.name = add_language_marker(tokens.at(1), 'J'); p->disp.name = add_language_marker(tokens.at(1), 'J');
} else if (tokens.at(0) == "npc") { } else if (tokens.at(0) == "npc") {
if (tokens.at(1) == "none") { if (tokens.at(1) == "none") {
c->game_data.player()->disp.visual.extra_model = 0; p->disp.visual.extra_model = 0;
c->game_data.player()->disp.visual.v2_flags &= 0xFD; p->disp.visual.v2_flags &= 0xFD;
} else { } else {
uint8_t npc = npc_for_name(decode_sjis(tokens.at(1))); uint8_t npc = npc_for_name(decode_sjis(tokens.at(1)));
if (npc == 0xFF) { if (npc == 0xFF) {
send_text_message(c, u"$C6No such NPC"); send_text_message(c, u"$C6No such NPC");
return; return;
} }
c->game_data.player()->disp.visual.extra_model = npc; p->disp.visual.extra_model = npc;
c->game_data.player()->disp.visual.v2_flags |= 0x02; p->disp.visual.v2_flags |= 0x02;
} }
} else if (tokens.at(0) == "tech") { } else if (tokens.at(0) == "tech") {
uint8_t level = stoul(tokens.at(2)) - 1; uint8_t level = stoul(tokens.at(2)) - 1;
if (tokens.at(1) == "all") { if (tokens.at(1) == "all") {
for (size_t x = 0; x < 0x14; x++) { for (size_t x = 0; x < 0x14; x++) {
c->game_data.player()->set_technique_level(x, level); p->set_technique_level(x, level);
} }
} else { } else {
uint8_t tech_id = technique_for_name(decode_sjis(tokens.at(1))); uint8_t tech_id = technique_for_name(decode_sjis(tokens.at(1)));
@@ -860,7 +861,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::u16string& args
return; return;
} }
try { try {
c->game_data.player()->set_technique_level(tech_id, level); p->set_technique_level(tech_id, level);
} catch (const out_of_range&) { } catch (const out_of_range&) {
send_text_message(c, u"$C6Invalid technique"); send_text_message(c, u"$C6Invalid technique");
return; return;
+2 -1
View File
@@ -192,7 +192,8 @@ struct Client : public std::enable_shared_from_this<Client> {
void reschedule_ping_and_timeout_events(); void reschedule_ping_and_timeout_events();
inline uint8_t language() const { inline uint8_t language() const {
return this->game_data.player()->inventory.language; auto p = this->game_data.player(true, false);
return p ? p->inventory.language : 1; // English by default
} }
inline GameVersion version() const { inline GameVersion version() const {
return this->channel.version; return this->channel.version;
+1 -1
View File
@@ -3906,7 +3906,7 @@ struct G_SetPlayerVisibility_6x22_6x23 {
// 6x24: Teleport player // 6x24: Teleport player
struct G_Unknown_6x24 { struct G_TeleportPlayer_6x24 {
G_ClientIDHeader header; G_ClientIDHeader header;
le_uint32_t unknown_a1; le_uint32_t unknown_a1;
le_float x; le_float x;
+12 -12
View File
@@ -21,7 +21,7 @@ ItemCreator::ItemCreator(
uint8_t difficulty, uint8_t difficulty,
uint8_t section_id, uint8_t section_id,
uint32_t random_seed, uint32_t random_seed,
shared_ptr<const Restrictions> restrictions) shared_ptr<const BattleRules> restrictions)
: log("[ItemCreator] "), : log("[ItemCreator] "),
episode(episode), episode(episode),
mode(mode), mode(mode),
@@ -50,7 +50,7 @@ bool ItemCreator::are_rare_drops_allowed() const {
} }
uint8_t ItemCreator::normalize_area_number(uint8_t area) const { uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
if (!this->item_drop_sub || (area < 0x10) || (area > 0x11)) { if (!this->restrictions || (this->restrictions->box_drop_area == 0) || (area < 0x10) || (area > 0x11)) {
switch (this->episode) { switch (this->episode) {
case Episode::EP1: case Episode::EP1:
if (area >= 15) { if (area >= 15) {
@@ -102,7 +102,7 @@ uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
} }
} else { } else {
return this->item_drop_sub->override_area; return this->restrictions->box_drop_area;
} }
} }
@@ -452,16 +452,16 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
case 0: case 0:
case 1: case 1:
switch (this->restrictions->weapon_and_armor_mode) { switch (this->restrictions->weapon_and_armor_mode) {
case Restrictions::WeaponAndArmorMode::ALL_ON: case BattleRules::WeaponAndArmorMode::ALLOW:
case Restrictions::WeaponAndArmorMode::ONLY_PICKING: case BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW:
break; break;
case Restrictions::WeaponAndArmorMode::NO_RARE: case BattleRules::WeaponAndArmorMode::FORBID_RARES:
if (this->item_parameter_table->is_item_rare(item)) { if (this->item_parameter_table->is_item_rare(item)) {
this->log.info("Restricted: rare items not allowed"); this->log.info("Restricted: rare items not allowed");
item.clear(); item.clear();
} }
break; break;
case Restrictions::WeaponAndArmorMode::ALL_OFF: case BattleRules::WeaponAndArmorMode::FORBID_ALL:
this->log.info("Restricted: weapons and armors not allowed"); this->log.info("Restricted: weapons and armors not allowed");
item.clear(); item.clear();
break; break;
@@ -476,18 +476,18 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
} }
break; break;
case 3: case 3:
if (this->restrictions->tool_mode == Restrictions::ToolMode::ALL_OFF) { if (this->restrictions->tool_mode == BattleRules::ToolMode::FORBID_ALL) {
this->log.info("Restricted: tools not allowed"); this->log.info("Restricted: tools not allowed");
item.clear(); item.clear();
} else if (item.data1[1] == 2) { } else if (item.data1[1] == 2) {
switch (this->restrictions->tech_disk_mode) { switch (this->restrictions->tech_disk_mode) {
case Restrictions::TechDiskMode::ON: case BattleRules::TechDiskMode::ALLOW:
break; break;
case Restrictions::TechDiskMode::OFF: case BattleRules::TechDiskMode::FORBID_ALL:
this->log.info("Restricted: tech disks not allowed"); this->log.info("Restricted: tech disks not allowed");
item.clear(); item.clear();
break; break;
case Restrictions::TechDiskMode::LIMIT_LEVEL: case BattleRules::TechDiskMode::LIMIT_LEVEL:
this->log.info("Restricted: tech disk level limited to %hhu", this->log.info("Restricted: tech disk level limited to %hhu",
static_cast<uint8_t>(this->restrictions->max_tech_disk_level + 1)); static_cast<uint8_t>(this->restrictions->max_tech_disk_level + 1));
if (this->restrictions->max_tech_disk_level == 0) { if (this->restrictions->max_tech_disk_level == 0) {
@@ -505,7 +505,7 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
} }
break; break;
case 4: case 4:
if (this->restrictions->meseta_drop_mode == Restrictions::MesetaDropMode::OFF) { if (this->restrictions->meseta_drop_mode == BattleRules::MesetaDropMode::FORBID_ALL) {
this->log.info("Restricted: meseta not allowed"); this->log.info("Restricted: meseta not allowed");
item.clear(); item.clear();
} }
+7 -43
View File
@@ -5,51 +5,12 @@
#include "CommonItemSet.hh" #include "CommonItemSet.hh"
#include "ItemParameterTable.hh" #include "ItemParameterTable.hh"
#include "PSOEncryption.hh" #include "PSOEncryption.hh"
#include "PlayerSubordinates.hh"
#include "RareItemSet.hh" #include "RareItemSet.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
struct ItemDropSub {
uint8_t override_area;
};
class ItemCreator { class ItemCreator {
public: public:
struct Restrictions {
// Note: In the original code, this is actually the battle rules structure.
// We omit some fields here because the item creator doesn't need them.
enum class TechDiskMode {
ON = 0,
OFF = 1,
LIMIT_LEVEL = 2,
};
enum class WeaponAndArmorMode {
// Note: These names match the value names in TPlyPKEditor
ALL_ON = 0,
ONLY_PICKING = 1,
ALL_OFF = 2,
NO_RARE = 3,
};
enum class ToolMode {
// Note: These names match the value names in TPlyPKEditor
ALL_ON = 0,
ONLY_PICKING = 1,
ALL_OFF = 2,
};
enum class MesetaDropMode {
// Note: These names match the value names in TPlyPKEditor
ON = 0,
OFF = 1,
ONLY_PICKING = 2,
};
TechDiskMode tech_disk_mode;
WeaponAndArmorMode weapon_and_armor_mode;
bool forbid_mags;
ToolMode tool_mode;
MesetaDropMode meseta_drop_mode;
bool forbid_scape_dolls;
uint8_t max_tech_disk_level; // 0xFF = no maximum
};
ItemCreator( ItemCreator(
std::shared_ptr<const CommonItemSet> common_item_set, std::shared_ptr<const CommonItemSet> common_item_set,
std::shared_ptr<const RareItemSet> rare_item_set, std::shared_ptr<const RareItemSet> rare_item_set,
@@ -63,7 +24,7 @@ public:
uint8_t difficulty, uint8_t difficulty,
uint8_t section_id, uint8_t section_id,
uint32_t random_seed, uint32_t random_seed,
std::shared_ptr<const Restrictions> restrictions = nullptr); std::shared_ptr<const BattleRules> restrictions = nullptr);
~ItemCreator() = default; ~ItemCreator() = default;
ItemData on_monster_item_drop(uint32_t enemy_type, uint8_t area); ItemData on_monster_item_drop(uint32_t enemy_type, uint8_t area);
@@ -78,6 +39,10 @@ public:
// See the comments in TekkerAdjustmentSet for what this value means. // See the comments in TekkerAdjustmentSet for what this value means.
ssize_t apply_tekker_deltas(ItemData& item, uint8_t section_id); ssize_t apply_tekker_deltas(ItemData& item, uint8_t section_id);
inline void set_restrictions(std::shared_ptr<const BattleRules> restrictions) {
this->restrictions = restrictions;
}
private: private:
PrefixedLogger log; PrefixedLogger log;
Episode episode; Episode episode;
@@ -92,9 +57,8 @@ private:
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set; std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
std::shared_ptr<const ItemParameterTable> item_parameter_table; std::shared_ptr<const ItemParameterTable> item_parameter_table;
const CommonItemSet::Table<true>* pt; const CommonItemSet::Table<true>* pt;
std::shared_ptr<const Restrictions> restrictions; std::shared_ptr<const BattleRules> restrictions;
std::shared_ptr<ItemDropSub> item_drop_sub;
parray<uint8_t, 0x88> unit_weights_table1; parray<uint8_t, 0x88> unit_weights_table1;
parray<int8_t, 0x0D> unit_weights_table2; parray<int8_t, 0x0D> unit_weights_table2;
+2
View File
@@ -1,5 +1,7 @@
#include "ItemData.hh" #include "ItemData.hh"
#include <map>
#include "StaticGameData.hh" #include "StaticGameData.hh"
using namespace std; using namespace std;
+1 -1
View File
@@ -28,7 +28,7 @@ const CharacterStats& LevelTable::base_stats_for_class(uint8_t char_class) const
return this->table->base_stats[char_class]; return this->table->base_stats[char_class];
} }
const LevelTable::LevelStats& LevelTable::stats_for_level( const LevelTable::LevelStats& LevelTable::stats_delta_for_level(
uint8_t char_class, uint8_t level) const { uint8_t char_class, uint8_t level) const {
if (char_class >= 12) { if (char_class >= 12) {
throw invalid_argument("invalid character class"); throw invalid_argument("invalid character class");
+44 -1
View File
@@ -16,6 +16,17 @@ struct CharacterStats {
le_uint16_t lck = 0; le_uint16_t lck = 0;
} __attribute__((packed)); } __attribute__((packed));
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 */
} __attribute__((packed));
class LevelTable { // from PlyLevelTbl.prs class LevelTable { // from PlyLevelTbl.prs
public: public:
struct LevelStats { struct LevelStats {
@@ -41,9 +52,41 @@ public:
LevelTable(std::shared_ptr<const std::string> data, bool compressed); LevelTable(std::shared_ptr<const std::string> data, bool compressed);
const CharacterStats& base_stats_for_class(uint8_t char_class) const; const CharacterStats& base_stats_for_class(uint8_t char_class) const;
const LevelStats& stats_for_level(uint8_t char_class, uint8_t level) const; const LevelStats& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
private: private:
// TODO: Currently we only support the BB version of this file. It'd be nice
// to support non-BB versions, but their formats are very different:
//
// BB:
// root:
// u32 offset:
// u32[12] unknown
// u32 offset:
// u32[12] offsets:
// LevelStats[200] level_stats
// u32 offset:
// CharacterStats[12] base_stats
// GC:
// root:
// u32 offset:
// u32[12] offsets:
// LevelStats[200] level_stats
// PC:
// root:
// u32 offset:
// u32 offset[9]:
// LevelStats[200] level_stats
// u32 offset:
// (0x18 bytes)
// u32 offset:
// PlayerStats[9] max_stats
// u32 offset:
// PlayerStats[9] level100_stats
// u32 offset:
// u32 offset[9]:
// CharacterStats level1_stats
// (11 more pointers)
std::shared_ptr<const std::string> data; std::shared_ptr<const std::string> data;
const Table* table; const Table* table;
}; };
+7 -6
View File
@@ -176,24 +176,26 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
// If the lobby is a game and item tracking is enabled, assign the inventory's // If the lobby is a game and item tracking is enabled, assign the inventory's
// item IDs // item IDs
if (this->is_game() && (this->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { if (this->is_game() && (this->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto& inv = c->game_data.player()->inventory; auto p = c->game_data.player();
auto& inv = p->inventory;
size_t count = min<uint8_t>(inv.num_items, 30); size_t count = min<uint8_t>(inv.num_items, 30);
for (size_t x = 0; x < count; x++) { for (size_t x = 0; x < count; x++) {
inv.items[x].data.id = this->generate_item_id(c->lobby_client_id); inv.items[x].data.id = this->generate_item_id(c->lobby_client_id);
} }
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
} }
// If the lobby is recording a battle record, add the player join event // If the lobby is recording a battle record, add the player join event
if (this->battle_record) { if (this->battle_record) {
auto p = c->game_data.player();
PlayerLobbyDataDCGC lobby_data; PlayerLobbyDataDCGC lobby_data;
lobby_data.player_tag = 0x00010000; lobby_data.player_tag = 0x00010000;
lobby_data.guild_card = c->license->serial_number; lobby_data.guild_card = c->license->serial_number;
lobby_data.name = encode_sjis(c->game_data.player()->disp.name); lobby_data.name = encode_sjis(p->disp.name);
this->battle_record->add_player( this->battle_record->add_player(
lobby_data, lobby_data,
c->game_data.player()->inventory, p->inventory,
c->game_data.player()->disp.to_dcpcv3(), p->disp.to_dcpcv3(),
c->game_data.ep3_config ? (c->game_data.ep3_config->online_clv_exp / 100) : 0); c->game_data.ep3_config ? (c->game_data.ep3_config->online_clv_exp / 100) : 0);
} }
@@ -218,7 +220,6 @@ void Lobby::remove_client(shared_ptr<Client> c) {
c->lobby_client_id, c->lobby_client_id,
static_cast<uint8_t>(other_c ? other_c->lobby_client_id : 0xFF))); static_cast<uint8_t>(other_c ? other_c->lobby_client_id : 0xFF)));
} }
this->clients[c->lobby_client_id] = nullptr; this->clients[c->lobby_client_id] = nullptr;
// Unassign the client's lobby if it matches the current lobby (it may not // Unassign the client's lobby if it matches the current lobby (it may not
+93 -8
View File
@@ -42,8 +42,76 @@ ClientGameData::~ClientGameData() {
} }
} }
shared_ptr<SavedAccountDataBB> ClientGameData::account(bool should_load) { void ClientGameData::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_ptr<const LevelTable> level_table) {
if (!this->account_data.get() && should_load) { this->overlay_player_data.reset(new SavedPlayerDataBB(*this->player(true, false)));
if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) {
this->overlay_player_data->inventory.remove_all_items_of_type(0);
this->overlay_player_data->inventory.remove_all_items_of_type(1);
}
if (rules->forbid_mags) {
this->overlay_player_data->inventory.remove_all_items_of_type(2);
}
if (rules->tool_mode != BattleRules::ToolMode::ALLOW) {
this->overlay_player_data->inventory.remove_all_items_of_type(3);
}
if (rules->replace_char) {
this->overlay_player_data->inventory.hp_materials_used = 0;
this->overlay_player_data->inventory.tp_materials_used = 0;
uint32_t target_level = clamp<uint32_t>(rules->char_level, 0, 199);
uint8_t char_class = this->overlay_player_data->disp.visual.char_class;
const auto& base_stats = level_table->base_stats_for_class(char_class);
auto& stats = this->overlay_player_data->disp.stats;
stats.char_stats.atp = base_stats.atp;
stats.char_stats.mst = base_stats.mst;
stats.char_stats.evp = base_stats.evp;
stats.char_stats.hp = base_stats.hp;
stats.char_stats.dfp = base_stats.dfp;
stats.char_stats.ata = base_stats.ata;
stats.char_stats.lck = base_stats.lck;
for (this->overlay_player_data->disp.stats.level = 0;
this->overlay_player_data->disp.stats.level < target_level;
this->overlay_player_data->disp.stats.level++) {
const auto& level_stats = level_table->stats_delta_for_level(char_class, this->overlay_player_data->disp.stats.level + 1);
// The original code clamps the resulting stat values to [0, max_stat];
// we don't have max_stat handy so we just allow them to be unbounded
stats.char_stats.atp += level_stats.atp;
stats.char_stats.mst += level_stats.mst;
stats.char_stats.evp += level_stats.evp;
stats.char_stats.hp += level_stats.hp;
stats.char_stats.dfp += level_stats.dfp;
stats.char_stats.ata += level_stats.ata;
// Note: It is not a bug that lck is ignored here; the original code
// ignores it too.
}
stats.unknown_a1 = 40;
stats.experience = level_table->stats_delta_for_level(char_class, stats.level).experience;
stats.meseta = 300;
}
if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) {
// TODO: Verify this is what the game actually does.
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
uint8_t existing_level = this->overlay_player_data->get_technique_level(tech_num);
if ((existing_level != 0xFF) && (existing_level > rules->max_tech_disk_level)) {
this->overlay_player_data->set_technique_level(tech_num, rules->max_tech_disk_level);
}
}
} else if (rules->tech_disk_mode == BattleRules::TechDiskMode::FORBID_ALL) {
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
this->overlay_player_data->set_technique_level(tech_num, 0xFF);
}
}
if (rules->meseta_drop_mode != BattleRules::MesetaDropMode::ALLOW) {
this->overlay_player_data->disp.stats.meseta = 0;
}
if (rules->forbid_scape_dolls) {
this->overlay_player_data->inventory.remove_all_items_of_type(3, 9);
}
}
shared_ptr<SavedAccountDataBB> ClientGameData::account(bool allow_load) {
if (!this->account_data.get() && allow_load) {
if (this->bb_username.empty()) { if (this->bb_username.empty()) {
this->account_data.reset(new SavedAccountDataBB()); this->account_data.reset(new SavedAccountDataBB());
this->account_data->signature = ACCOUNT_FILE_SIGNATURE; this->account_data->signature = ACCOUNT_FILE_SIGNATURE;
@@ -54,8 +122,11 @@ shared_ptr<SavedAccountDataBB> ClientGameData::account(bool should_load) {
return this->account_data; return this->account_data;
} }
shared_ptr<SavedPlayerDataBB> ClientGameData::player(bool should_load) { shared_ptr<SavedPlayerDataBB> ClientGameData::player(bool allow_load, bool allow_overlay) {
if (!this->player_data.get() && should_load) { if (this->overlay_player_data && allow_overlay) {
return this->overlay_player_data;
}
if (!this->player_data.get() && allow_load) {
if (this->bb_username.empty()) { if (this->bb_username.empty()) {
this->player_data.reset(new SavedPlayerDataBB()); this->player_data.reset(new SavedPlayerDataBB());
} else { } else {
@@ -65,15 +136,18 @@ shared_ptr<SavedPlayerDataBB> ClientGameData::player(bool should_load) {
return this->player_data; return this->player_data;
} }
shared_ptr<const SavedAccountDataBB> ClientGameData::account() const { shared_ptr<const SavedAccountDataBB> ClientGameData::account(bool allow_load) const {
if (!this->account_data.get()) { if (!this->account_data.get() && allow_load) {
throw runtime_error("account data is not loaded"); throw runtime_error("account data is not loaded");
} }
return this->account_data; return this->account_data;
} }
shared_ptr<const SavedPlayerDataBB> ClientGameData::player() const { shared_ptr<const SavedPlayerDataBB> ClientGameData::player(bool allow_load, bool allow_overlay) const {
if (!this->player_data.get()) { if (allow_overlay && this->overlay_player_data) {
return this->overlay_player_data;
}
if (!this->player_data.get() && allow_load) {
throw runtime_error("player data is not loaded"); throw runtime_error("player data is not loaded");
} }
return this->player_data; return this->player_data;
@@ -371,3 +445,14 @@ void SavedPlayerDataBB::set_material_usage(MaterialType which, uint8_t usage) {
throw logic_error("invalid material type"); throw logic_error("invalid material type");
} }
} }
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());
}
}
+15 -4
View File
@@ -10,6 +10,8 @@
#include <vector> #include <vector>
#include "Episode3/DataIndexes.hh" #include "Episode3/DataIndexes.hh"
#include "ItemCreator.hh"
#include "LevelTable.hh"
#include "PlayerSubordinates.hh" #include "PlayerSubordinates.hh"
#include "Text.hh" #include "Text.hh"
#include "Version.hh" #include "Version.hh"
@@ -91,6 +93,10 @@ struct SavedAccountDataBB { // .nsa file format
class ClientGameData { class ClientGameData {
private: private:
std::shared_ptr<SavedAccountDataBB> account_data; std::shared_ptr<SavedAccountDataBB> account_data;
// The overlay player data is used in battle and challenge modes, when player
// data is temporarily replaced in-game. In other play modes and in lobbies,
// overlay_player_data is null.
std::shared_ptr<SavedPlayerDataBB> overlay_player_data;
std::shared_ptr<SavedPlayerDataBB> player_data; std::shared_ptr<SavedPlayerDataBB> player_data;
uint64_t last_play_time_update; uint64_t last_play_time_update;
@@ -117,10 +123,15 @@ public:
ClientGameData(); ClientGameData();
~ClientGameData(); ~ClientGameData();
std::shared_ptr<SavedAccountDataBB> account(bool should_load = true); void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
std::shared_ptr<SavedPlayerDataBB> player(bool should_load = true); inline void delete_overlay() {
std::shared_ptr<const SavedAccountDataBB> account() const; this->overlay_player_data.reset();
std::shared_ptr<const SavedPlayerDataBB> player() const; }
std::shared_ptr<SavedAccountDataBB> account(bool allow_load = true);
std::shared_ptr<SavedPlayerDataBB> player(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<const SavedAccountDataBB> account(bool allow_load = true) const;
std::shared_ptr<const SavedPlayerDataBB> player(bool allow_load = true, bool allow_overlay = true) const;
std::string account_data_filename() const; std::string account_data_filename() const;
std::string player_data_filename() const; std::string player_data_filename() const;
+152 -8
View File
@@ -443,6 +443,25 @@ size_t PlayerInventory::find_equipped_mag() const {
return ret; return ret;
} }
size_t PlayerInventory::remove_all_items_of_type(uint8_t data1_0, int16_t data1_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].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;
}
size_t PlayerBank::find_item(uint32_t item_id) { size_t PlayerBank::find_item(uint32_t item_id) {
for (size_t x = 0; x < this->num_items; x++) { for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.id == item_id) { if (this->items[x].data.id == item_id) {
@@ -452,13 +471,138 @@ size_t PlayerBank::find_item(uint32_t item_id) {
throw out_of_range("item not present"); throw out_of_range("item not present");
} }
void SavedPlayerDataBB::print_inventory(FILE* stream) const { BattleRules::BattleRules(const JSON& json) {
fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", this->disp.stats.meseta.load()); this->tech_disk_mode = json.get_enum("tech_disk_mode", this->tech_disk_mode);
fprintf(stream, "[PlayerInventory] %hhu items\n", this->inventory.num_items); this->weapon_and_armor_mode = json.get_enum("weapon_and_armor_mode", this->weapon_and_armor_mode);
for (size_t x = 0; x < this->inventory.num_items; x++) { this->forbid_mags = json.get_bool("forbid_mags", this->forbid_mags);
const auto& item = this->inventory.items[x]; this->tool_mode = json.get_enum("tool_mode", this->tool_mode);
auto name = item.data.name(false); this->meseta_drop_mode = json.get_enum("meseta_drop_mode", this->meseta_drop_mode);
auto hex = item.data.hex(); this->forbid_scape_dolls = json.get_bool("forbid_scape_dolls", this->forbid_scape_dolls);
fprintf(stream, "[PlayerInventory] %zu: %s (%s)\n", x, hex.c_str(), name.c_str()); this->max_tech_disk_level = json.get_int("max_tech_disk_level", this->max_tech_disk_level);
this->replace_char = json.get_bool("replace_char", this->replace_char);
this->char_level = json.get_int("char_level", this->char_level);
this->box_drop_area = json.get_int("box_drop_area", this->box_drop_area);
}
JSON BattleRules::json() const {
return JSON::dict({
{"tech_disk_mode", this->tech_disk_mode},
{"weapon_and_armor_mode", this->weapon_and_armor_mode},
{"forbid_mags", this->forbid_mags},
{"tool_mode", this->tool_mode},
{"meseta_drop_mode", this->meseta_drop_mode},
{"forbid_scape_dolls", this->forbid_scape_dolls},
{"max_tech_disk_level", this->max_tech_disk_level},
{"replace_char", this->replace_char},
{"char_level", this->char_level},
{"box_drop_area", this->box_drop_area},
});
}
template <>
const char* name_for_enum<BattleRules::TechDiskMode>(BattleRules::TechDiskMode v) {
switch (v) {
case BattleRules::TechDiskMode::ALLOW:
return "ALLOW";
case BattleRules::TechDiskMode::FORBID_ALL:
return "FORBID_ALL";
case BattleRules::TechDiskMode::LIMIT_LEVEL:
return "LIMIT_LEVEL";
default:
throw invalid_argument("invalid BattleRules::TechDiskMode value");
}
}
template <>
BattleRules::TechDiskMode enum_for_name<BattleRules::TechDiskMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::TechDiskMode::ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::TechDiskMode::FORBID_ALL;
} else if (!strcmp(name, "LIMIT_LEVEL")) {
return BattleRules::TechDiskMode::LIMIT_LEVEL;
} else {
throw invalid_argument("invalid BattleRules::TechDiskMode name");
}
}
template <>
const char* name_for_enum<BattleRules::WeaponAndArmorMode>(BattleRules::WeaponAndArmorMode v) {
switch (v) {
case BattleRules::WeaponAndArmorMode::ALLOW:
return "ALLOW";
case BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW:
return "CLEAR_AND_ALLOW";
case BattleRules::WeaponAndArmorMode::FORBID_ALL:
return "FORBID_ALL";
case BattleRules::WeaponAndArmorMode::FORBID_RARES:
return "FORBID_RARES";
default:
throw invalid_argument("invalid BattleRules::WeaponAndArmorMode value");
}
}
template <>
BattleRules::WeaponAndArmorMode enum_for_name<BattleRules::WeaponAndArmorMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::WeaponAndArmorMode::ALLOW;
} else if (!strcmp(name, "CLEAR_AND_ALLOW")) {
return BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::WeaponAndArmorMode::FORBID_ALL;
} else if (!strcmp(name, "FORBID_RARES")) {
return BattleRules::WeaponAndArmorMode::FORBID_RARES;
} else {
throw invalid_argument("invalid BattleRules::WeaponAndArmorMode name");
}
}
template <>
const char* name_for_enum<BattleRules::ToolMode>(BattleRules::ToolMode v) {
switch (v) {
case BattleRules::ToolMode::ALLOW:
return "ALLOW";
case BattleRules::ToolMode::CLEAR_AND_ALLOW:
return "CLEAR_AND_ALLOW";
case BattleRules::ToolMode::FORBID_ALL:
return "FORBID_ALL";
default:
throw invalid_argument("invalid BattleRules::ToolMode value");
}
}
template <>
BattleRules::ToolMode enum_for_name<BattleRules::ToolMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::ToolMode::ALLOW;
} else if (!strcmp(name, "CLEAR_AND_ALLOW")) {
return BattleRules::ToolMode::CLEAR_AND_ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::ToolMode::FORBID_ALL;
} else {
throw invalid_argument("invalid BattleRules::ToolMode name");
}
}
template <>
const char* name_for_enum<BattleRules::MesetaDropMode>(BattleRules::MesetaDropMode v) {
switch (v) {
case BattleRules::MesetaDropMode::ALLOW:
return "ALLOW";
case BattleRules::MesetaDropMode::FORBID_ALL:
return "FORBID_ALL";
case BattleRules::MesetaDropMode::CLEAR_AND_ALLOW:
return "CLEAR_AND_ALLOW";
default:
throw invalid_argument("invalid BattleRules::MesetaDropMode value");
}
}
template <>
BattleRules::MesetaDropMode enum_for_name<BattleRules::MesetaDropMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::MesetaDropMode::ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::MesetaDropMode::FORBID_ALL;
} else if (!strcmp(name, "CLEAR_AND_ALLOW")) {
return BattleRules::MesetaDropMode::CLEAR_AND_ALLOW;
} else {
throw invalid_argument("invalid BattleRules::MesetaDropMode name");
} }
} }
+46 -11
View File
@@ -5,6 +5,7 @@
#include <array> #include <array>
#include <phosg/Encoding.hh> #include <phosg/Encoding.hh>
#include <phosg/JSON.hh>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -73,6 +74,8 @@ struct PlayerInventory {
size_t find_equipped_weapon() const; size_t find_equipped_weapon() const;
size_t find_equipped_armor() const; size_t find_equipped_armor() const;
size_t find_equipped_mag() const; size_t find_equipped_mag() const;
size_t remove_all_items_of_type(uint8_t data0, int16_t data1 = -1);
} __attribute__((packed)); } __attribute__((packed));
struct PlayerBank { struct PlayerBank {
@@ -94,17 +97,6 @@ struct PlayerBank {
struct PlayerDispDataBB; 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 */
} __attribute__((packed));
struct PlayerVisualConfig { struct PlayerVisualConfig {
/* 00 */ ptext<char, 0x10> name; /* 00 */ ptext<char, 0x10> name;
/* 10 */ parray<uint8_t, 8> unknown_a2; /* 10 */ parray<uint8_t, 8> unknown_a2;
@@ -448,3 +440,46 @@ inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
const PlayerDispDataBB& src) { const PlayerDispDataBB& src) {
return src; return src;
} }
struct BattleRules {
enum class TechDiskMode {
ALLOW = 0,
FORBID_ALL = 1,
LIMIT_LEVEL = 2,
};
enum class WeaponAndArmorMode {
ALLOW = 0,
CLEAR_AND_ALLOW = 1,
FORBID_ALL = 2,
FORBID_RARES = 3,
};
enum class ToolMode {
ALLOW = 0,
CLEAR_AND_ALLOW = 1,
FORBID_ALL = 2,
};
enum class MesetaDropMode {
ALLOW = 0,
FORBID_ALL = 1,
CLEAR_AND_ALLOW = 2,
};
TechDiskMode tech_disk_mode = TechDiskMode::ALLOW;
WeaponAndArmorMode weapon_and_armor_mode = WeaponAndArmorMode::ALLOW;
bool forbid_mags = false;
ToolMode tool_mode = ToolMode::ALLOW;
MesetaDropMode meseta_drop_mode = MesetaDropMode::ALLOW;
bool forbid_scape_dolls = false;
uint8_t max_tech_disk_level = 0xFF; // 0xFF = no maximum
bool replace_char = false; // char_type in quest opcodes
uint16_t char_level = 0; // Only used if replace_char is true
uint8_t box_drop_area = 0;
BattleRules() = default;
explicit BattleRules(const JSON& json);
JSON json() const;
bool operator==(const BattleRules& other) const = default;
bool operator!=(const BattleRules& other) const = default;
};
+61 -8
View File
@@ -222,7 +222,8 @@ VersionedQuest::VersionedQuest(
QuestScriptVersion version, QuestScriptVersion version,
uint8_t language, uint8_t language,
std::shared_ptr<const std::string> bin_contents, std::shared_ptr<const std::string> bin_contents,
std::shared_ptr<const std::string> dat_contents) std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const BattleRules> battle_rules)
: quest_number(quest_number), : quest_number(quest_number),
category_id(category_id), category_id(category_id),
episode(Episode::NONE), episode(Episode::NONE),
@@ -231,7 +232,8 @@ VersionedQuest::VersionedQuest(
language(language), language(language),
is_dlq_encoded(false), is_dlq_encoded(false),
bin_contents(bin_contents), bin_contents(bin_contents),
dat_contents(dat_contents) { dat_contents(dat_contents),
battle_rules(battle_rules) {
auto bin_decompressed = prs_decompress(*this->bin_contents); auto bin_decompressed = prs_decompress(*this->bin_contents);
@@ -375,7 +377,8 @@ Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
category_id(initial_version->category_id), category_id(initial_version->category_id),
episode(initial_version->episode), episode(initial_version->episode),
joinable(initial_version->joinable), joinable(initial_version->joinable),
name(initial_version->name) { name(initial_version->name),
battle_rules(initial_version->battle_rules) {
this->versions.emplace(this->versions_key(initial_version->version, initial_version->language), initial_version); this->versions.emplace(this->versions_key(initial_version->version, initial_version->language), initial_version);
} }
@@ -396,6 +399,12 @@ void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
if (this->joinable != vq->joinable) { if (this->joinable != vq->joinable) {
throw runtime_error("quest version has a different joinability state"); throw runtime_error("quest version has a different joinability state");
} }
if (!this->battle_rules != !vq->battle_rules) {
throw runtime_error("quest version has a different battle rules presence state");
}
if (this->battle_rules && (*this->battle_rules != *vq->battle_rules)) {
throw runtime_error("quest version has different battle rules");
}
this->versions.emplace(this->versions_key(vq->version, vq->language), vq); this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
} }
@@ -430,6 +439,7 @@ QuestIndex::QuestIndex(
category_index(category_index) { category_index(category_index) {
unordered_map<string, shared_ptr<const string>> dat_cache; unordered_map<string, shared_ptr<const string>> dat_cache;
unordered_map<string, shared_ptr<const JSON>> metadata_json_cache;
for (const auto& bin_filename : list_directory_sorted(directory)) { for (const auto& bin_filename : list_directory_sorted(directory)) {
string bin_path = this->directory + "/" + bin_filename; string bin_path = this->directory + "/" + bin_filename;
@@ -467,6 +477,9 @@ QuestIndex::QuestIndex(
if (basename.empty()) { if (basename.empty()) {
throw invalid_argument("empty filename"); throw invalid_argument("empty filename");
} }
if (basename.size() < 2) {
throw logic_error("basename too short for language trim");
}
// Quest .bin filenames are like K###-CAT-VERS-LANG.EXT, where: // Quest .bin filenames are like K###-CAT-VERS-LANG.EXT, where:
// K = class (quest, battle, challenge, etc.) // K = class (quest, battle, challenge, etc.)
@@ -549,6 +562,7 @@ QuestIndex::QuestIndex(
if (basename.size() < 2) { if (basename.size() < 2) {
throw logic_error("basename too short for language trim"); throw logic_error("basename too short for language trim");
} }
// Look for dat file with the same basename as the bin file; if not // Look for dat file with the same basename as the bin file; if not
// found, look for a dat file without the language suffix // found, look for a dat file without the language suffix
string dat_basename; string dat_basename;
@@ -611,26 +625,64 @@ QuestIndex::QuestIndex(
} }
} }
// Look for a JSON file with the same basename as the bin file; if not
// found, look for a JSON file without the language suffix
shared_ptr<const JSON> metadata_json;
string json_filename;
for (size_t z = 0; z < 3; z++) {
string json_basename;
if (z == 0) {
json_filename = basename + ".json";
} else if (z == 1) {
json_filename = basename.substr(0, basename.size() - 2) + ".json"; // Strip off language prefix
} else if (z == 2) {
json_filename = basename.substr(0, basename.find('-')) + ".json"; // Look only at base token (e.g. "b88001")
}
try {
metadata_json = metadata_json_cache.at(json_filename);
break;
} catch (const out_of_range&) {
}
string json_path = this->directory + "/" + json_filename;
if (isfile(json_path)) {
metadata_json.reset(new JSON(JSON::parse(load_file(json_path))));
break;
}
}
metadata_json_cache.emplace(json_filename, metadata_json);
shared_ptr<BattleRules> battle_rules;
if (metadata_json) {
try {
battle_rules.reset(new BattleRules(metadata_json->at("battle_rules")));
} catch (const out_of_range&) {
}
}
shared_ptr<VersionedQuest> vq(new VersionedQuest( shared_ptr<VersionedQuest> vq(new VersionedQuest(
quest_number, category_id, version, language, bin_contents, dat_contents)); quest_number, category_id, version, language, bin_contents, dat_contents, battle_rules));
string ascii_name = format_data_string(encode_sjis(vq->name)); string ascii_name = format_data_string(encode_sjis(vq->name));
auto category_name = encode_sjis(this->category_index->at(vq->category_id).name); auto category_name = encode_sjis(this->category_index->at(vq->category_id).name);
string dat_str = dat_filename.empty() ? "" : (" with layout " + dat_filename); string dat_str = dat_filename.empty() ? "" : (" with layout " + dat_filename);
string metadata_json_str = battle_rules ? (" with battle rules from " + json_filename) : "";
auto q_it = this->quests_by_number.find(vq->quest_number); auto q_it = this->quests_by_number.find(vq->quest_number);
if (q_it != this->quests_by_number.end()) { if (q_it != this->quests_by_number.end()) {
q_it->second->add_version(vq); q_it->second->add_version(vq);
static_game_data_log.info("(%s) Added %s %c version of quest %" PRIu32 " %s%s", static_game_data_log.info("(%s) Added %s %c version of quest %" PRIu32 " %s%s%s",
bin_filename.c_str(), bin_filename.c_str(),
name_for_enum(vq->version), name_for_enum(vq->version),
char_for_language_code(vq->language), char_for_language_code(vq->language),
vq->quest_number, vq->quest_number,
ascii_name.c_str(), ascii_name.c_str(),
dat_str.c_str()); dat_str.c_str(),
metadata_json_str.c_str());
} else { } else {
this->quests_by_number.emplace(vq->quest_number, new Quest(vq)); this->quests_by_number.emplace(vq->quest_number, new Quest(vq));
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " %s (%s, %s (%" PRIu32 "), %s)%s", static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " %s (%s, %s (%" PRIu32 "), %s)%s%s",
bin_filename.c_str(), bin_filename.c_str(),
name_for_enum(vq->version), name_for_enum(vq->version),
char_for_language_code(vq->language), char_for_language_code(vq->language),
@@ -640,7 +692,8 @@ QuestIndex::QuestIndex(
category_name.c_str(), category_name.c_str(),
vq->category_id, vq->category_id,
vq->joinable ? "joinable" : "not joinable", vq->joinable ? "joinable" : "not joinable",
dat_str.c_str()); dat_str.c_str(),
metadata_json_str.c_str());
} }
} catch (const exception& e) { } catch (const exception& e) {
static_game_data_log.warning("(%s) Failed to index quest file: (%s)", bin_filename.c_str(), e.what()); static_game_data_log.warning("(%s) Failed to index quest file: (%s)", bin_filename.c_str(), e.what());
+5 -1
View File
@@ -7,6 +7,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "PlayerSubordinates.hh"
#include "QuestScript.hh" #include "QuestScript.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
@@ -65,6 +66,7 @@ struct VersionedQuest {
std::u16string long_description; std::u16string long_description;
std::shared_ptr<const std::string> bin_contents; std::shared_ptr<const std::string> bin_contents;
std::shared_ptr<const std::string> dat_contents; std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const BattleRules> battle_rules;
VersionedQuest( VersionedQuest(
uint32_t quest_number, uint32_t quest_number,
@@ -72,7 +74,8 @@ struct VersionedQuest {
QuestScriptVersion version, QuestScriptVersion version,
uint8_t language, uint8_t language,
std::shared_ptr<const std::string> bin_contents, std::shared_ptr<const std::string> bin_contents,
std::shared_ptr<const std::string> dat_contents); std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const BattleRules> battle_rules = nullptr);
std::string bin_filename() const; std::string bin_filename() const;
std::string dat_filename() const; std::string dat_filename() const;
@@ -101,6 +104,7 @@ public:
Episode episode; Episode episode;
bool joinable; bool joinable;
std::u16string name; std::u16string name;
std::shared_ptr<const BattleRules> battle_rules;
std::map<uint16_t, std::shared_ptr<const VersionedQuest>> versions; std::map<uint16_t, std::shared_ptr<const VersionedQuest>> versions;
}; };
+11 -11
View File
@@ -513,19 +513,19 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF80F, "enable_weapon_drop", {CLIENT_ID}, F_V2_V4 | F_ARGS}, {0xF80F, "enable_weapon_drop", {CLIENT_ID}, F_V2_V4 | F_ARGS},
{0xF810, "ba_initial_floor", {AREA}, F_V2_V4 | F_ARGS}, {0xF810, "ba_initial_floor", {AREA}, F_V2_V4 | F_ARGS},
{0xF811, "set_ba_rules", {}, F_V2_V4}, {0xF811, "set_ba_rules", {}, F_V2_V4},
{0xF812, "ba_set_tech", {INT32}, F_V2_V4 | F_ARGS}, {0xF812, "ba_set_tech_disk_mode", {INT32}, F_V2_V4 | F_ARGS},
{0xF813, "ba_set_equip", {INT32}, F_V2_V4 | F_ARGS}, {0xF813, "ba_set_weapon_and_armor_mode", {INT32}, F_V2_V4 | F_ARGS},
{0xF814, "ba_set_mag", {INT32}, F_V2_V4 | F_ARGS}, {0xF814, "ba_set_forbid_mags", {INT32}, F_V2_V4 | F_ARGS},
{0xF815, "ba_set_item", {INT32}, F_V2_V4 | F_ARGS}, {0xF815, "ba_set_tool_mode", {INT32}, F_V2_V4 | F_ARGS},
{0xF816, "ba_set_trapmenu", {INT32}, F_V2_V4 | F_ARGS}, {0xF816, "ba_set_trap_mode", {INT32}, F_V2_V4 | F_ARGS},
{0xF817, "ba_set_unused_F817", {INT32}, F_V2_V4 | F_ARGS}, // This appears to be unused - the value is copied into the main battle rules struct, but then the field appears never to be read {0xF817, "ba_set_unused_F817", {INT32}, F_V2_V4 | F_ARGS}, // This appears to be unused - the value is copied into the main battle rules struct, but then the field appears never to be read
{0xF818, "ba_set_respawn", {INT32}, F_V2_V4 | F_ARGS}, {0xF818, "ba_set_respawn", {INT32}, F_V2_V4 | F_ARGS},
{0xF819, "ba_set_char", {INT32}, F_V2_V4 | F_ARGS}, {0xF819, "ba_set_replace_char", {INT32}, F_V2_V4 | F_ARGS},
{0xF81A, "ba_dropwep", {INT32}, F_V2_V4 | F_ARGS}, {0xF81A, "ba_dropwep", {INT32}, F_V2_V4 | F_ARGS},
{0xF81B, "ba_teams", {INT32}, F_V2_V4 | F_ARGS}, {0xF81B, "ba_teams", {INT32}, F_V2_V4 | F_ARGS},
{0xF81C, "ba_disp_msg", {CSTRING}, F_V2_V4 | F_ARGS}, {0xF81C, "ba_start", {CSTRING}, F_V2_V4 | F_ARGS},
{0xF81D, "death_lvl_up", {INT32}, F_V2_V4 | F_ARGS}, {0xF81D, "death_lvl_up", {INT32}, F_V2_V4 | F_ARGS},
{0xF81E, "ba_set_meseta", {INT32}, F_V2_V4 | F_ARGS}, {0xF81E, "ba_set_meseta_drop_mode", {INT32}, F_V2_V4 | F_ARGS},
{0xF820, "cmode_stage", {INT32}, F_V2_V4 | F_ARGS}, {0xF820, "cmode_stage", {INT32}, F_V2_V4 | F_ARGS},
{0xF821, "nop_F821", {{REG_SET_FIXED, 9}}, F_V2_V4}, // regsA[3-8] specify first 6 bytes of an ItemData. This opcode consumes an item ID, but does nothing else. {0xF821, "nop_F821", {{REG_SET_FIXED, 9}}, F_V2_V4}, // regsA[3-8] specify first 6 bytes of an ItemData. This opcode consumes an item ID, but does nothing else.
{0xF822, "nop_F822", {REG}, F_V2_V4}, {0xF822, "nop_F822", {REG}, F_V2_V4},
@@ -590,13 +590,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF868, "set_cmode_rank", {REG, REG}, F_V2_V4}, {0xF868, "set_cmode_rank", {REG, REG}, F_V2_V4},
{0xF869, "check_rank_time", {REG, REG}, F_V2_V4}, {0xF869, "check_rank_time", {REG, REG}, F_V2_V4},
{0xF86A, "item_create_cmode", {{REG_SET_FIXED, 6}, REG}, F_V2_V4}, // regsA specifies item.data1[0-5] {0xF86A, "item_create_cmode", {{REG_SET_FIXED, 6}, REG}, F_V2_V4}, // regsA specifies item.data1[0-5]
{0xF86B, "ba_box_drops", {REG}, F_V2_V4}, // TODO: This sets override_area in TItemDropSub; use this in ItemCreator {0xF86B, "ba_set_box_drop_area", {REG}, F_V2_V4}, // TODO: This sets override_area in TItemDropSub; use this in ItemCreator
{0xF86C, "award_item_ok", {REG}, F_V2_V4}, {0xF86C, "award_item_ok", {REG}, F_V2_V4},
{0xF86D, "ba_set_trapself", {}, F_V2_V4}, {0xF86D, "ba_set_trapself", {}, F_V2_V4},
{0xF86E, "ba_clear_trapself", {}, F_V2_V4}, {0xF86E, "ba_clear_trapself", {}, F_V2_V4},
{0xF86F, "ba_set_lives", {INT32}, F_V2_V4 | F_ARGS}, {0xF86F, "ba_set_lives", {INT32}, F_V2_V4 | F_ARGS},
{0xF870, "ba_set_tech_lvl", {INT32}, F_V2_V4 | F_ARGS}, {0xF870, "ba_set_max_tech_level", {INT32}, F_V2_V4 | F_ARGS},
{0xF871, "ba_set_lvl", {INT32}, F_V2_V4 | F_ARGS}, {0xF871, "ba_set_char_level", {INT32}, F_V2_V4 | F_ARGS},
{0xF872, "ba_set_time_limit", {INT32}, F_V2_V4 | F_ARGS}, {0xF872, "ba_set_time_limit", {INT32}, F_V2_V4 | F_ARGS},
{0xF873, "dark_falz_is_dead", {REG}, F_V2_V4}, {0xF873, "dark_falz_is_dead", {REG}, F_V2_V4},
{0xF874, "set_cmode_rank_override", {INT32, CSTRING}, F_V2_V4 | F_ARGS}, // argA is an XRGB8888 color, argB is two strings separated by \t or \n: the rank text to check for, and the rank text that should replace it if found {0xF874, "set_cmode_rank_override", {INT32, CSTRING}, F_V2_V4 | F_ARGS}, // argA is an XRGB8888 color, argB is two strings separated by \t or \n: the rank text to check for, and the rank text that should replace it if found
+1
View File
@@ -4,6 +4,7 @@
#include <phosg/Random.hh> #include <phosg/Random.hh>
#include "BattleParamsIndex.hh" #include "BattleParamsIndex.hh"
#include "ItemData.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
using namespace std; using namespace std;
+2
View File
@@ -3,12 +3,14 @@
#include <stdint.h> #include <stdint.h>
#include <memory> #include <memory>
#include <phosg/JSON.hh>
#include <random> #include <random>
#include <string> #include <string>
#include "AFSArchive.hh" #include "AFSArchive.hh"
#include "GSLArchive.hh" #include "GSLArchive.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
#include "Text.hh"
class RareItemSet { class RareItemSet {
public: public:
+48 -44
View File
@@ -1317,14 +1317,15 @@ static void on_CA_Ep3(shared_ptr<Client> c, uint16_t, uint32_t, const string& da
l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags)); l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags));
for (auto existing_c : l->clients) { for (auto existing_c : l->clients) {
if (existing_c) { if (existing_c) {
auto existing_p = existing_c->game_data.player();
PlayerLobbyDataDCGC lobby_data; PlayerLobbyDataDCGC lobby_data;
lobby_data.name = encode_sjis(existing_c->game_data.player()->disp.name); lobby_data.name = encode_sjis(existing_p->disp.name);
lobby_data.player_tag = 0x00010000; lobby_data.player_tag = 0x00010000;
lobby_data.guild_card = existing_c->license->serial_number; lobby_data.guild_card = existing_c->license->serial_number;
l->battle_record->add_player( l->battle_record->add_player(
lobby_data, lobby_data,
existing_c->game_data.player()->inventory, existing_p->inventory,
existing_c->game_data.player()->disp.to_dcpcv3(), existing_p->disp.to_dcpcv3(),
c->game_data.ep3_config ? (c->game_data.ep3_config->online_clv_exp / 100) : 0); c->game_data.ep3_config ? (c->game_data.ep3_config->online_clv_exp / 100) : 0);
} }
} }
@@ -1949,11 +1950,12 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
send_lobby_message_box(c, u"$C6Incorrect password."); send_lobby_message_box(c, u"$C6Incorrect password.");
break; break;
} }
if (c->game_data.player()->disp.stats.level < game->min_level) { auto p = c->game_data.player();
if (p->disp.stats.level < game->min_level) {
send_lobby_message_box(c, u"$C6Your level is too\nlow to join this\ngame."); send_lobby_message_box(c, u"$C6Your level is too\nlow to join this\ngame.");
break; break;
} }
if (c->game_data.player()->disp.stats.level > game->max_level) { if (p->disp.stats.level > game->max_level) {
send_lobby_message_box(c, u"$C6Your level is too\nhigh to join this\ngame."); send_lobby_message_box(c, u"$C6Your level is too\nhigh to join this\ngame.");
break; break;
} }
@@ -2019,6 +2021,11 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
} }
l->quest = q; l->quest = q;
l->episode = q->episode; l->episode = q->episode;
if (q->battle_rules && l->item_creator) {
l->item_creator->set_restrictions(q->battle_rules);
}
for (size_t x = 0; x < l->max_clients; x++) { for (size_t x = 0; x < l->max_clients; x++) {
auto lc = l->clients[x]; auto lc = l->clients[x];
if (!lc) { if (!lc) {
@@ -2031,6 +2038,13 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
lc->should_disconnect = true; lc->should_disconnect = true;
break; break;
} }
if (vq->battle_rules) {
lc->game_data.create_battle_overlay(vq->battle_rules, s->level_table);
lc->log.info("Created battle overlay");
lc->game_data.player()->print_inventory(stderr);
}
string bin_filename = vq->bin_filename(); string bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename(); string dat_filename = vq->dat_filename();
send_open_quest_file(lc, bin_filename, bin_filename, vq->bin_contents, QuestFileType::ONLINE); send_open_quest_file(lc, bin_filename, bin_filename, vq->bin_contents, QuestFileType::ONLINE);
@@ -2494,16 +2508,18 @@ static void on_13_A7_V3_BB(shared_ptr<Client> c, uint16_t command, uint32_t flag
} }
static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, const string& data) { static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, const string& data) {
auto s = c->require_server_state();
auto player = c->game_data.player();
auto account = c->game_data.account();
switch (c->version()) { switch (c->version()) {
case GameVersion::DC: { case GameVersion::DC: {
if (c->flags & Client::Flag::IS_DC_V1) { if (c->flags & Client::Flag::IS_DC_V1) {
const auto& pd = check_size_t<C_CharacterData_DCv1_61_98>(data); const auto& pd = check_size_t<C_CharacterData_DCv1_61_98>(data);
auto player = c->game_data.player();
player->inventory = pd.inventory; player->inventory = pd.inventory;
player->disp = pd.disp.to_bb(); player->disp = pd.disp.to_bb();
} else { } else {
const auto& pd = check_size_t<C_CharacterData_DCv2_61_98>(data, 0xFFFF); const auto& pd = check_size_t<C_CharacterData_DCv2_61_98>(data, 0xFFFF);
auto player = c->game_data.player();
player->inventory = pd.inventory; player->inventory = pd.inventory;
player->disp = pd.disp.to_bb(); player->disp = pd.disp.to_bb();
player->battle_records = pd.records.battle; player->battle_records = pd.records.battle;
@@ -2514,8 +2530,6 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, cons
} }
case GameVersion::PC: { case GameVersion::PC: {
const auto& pd = check_size_t<C_CharacterData_PC_61_98>(data, 0xFFFF); const auto& pd = check_size_t<C_CharacterData_PC_61_98>(data, 0xFFFF);
auto player = c->game_data.player();
auto account = c->game_data.account();
player->inventory = pd.inventory; player->inventory = pd.inventory;
player->disp = pd.disp.to_bb(); player->disp = pd.disp.to_bb();
player->battle_records = pd.records.battle; player->battle_records = pd.records.battle;
@@ -2574,8 +2588,6 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, cons
} }
} }
auto account = c->game_data.account();
auto player = c->game_data.player();
player->inventory = cmd->inventory; player->inventory = cmd->inventory;
if (c->version() == GameVersion::GC) { if (c->version() == GameVersion::GC) {
for (size_t z = 0; z < 30; z++) { for (size_t z = 0; z < 30; z++) {
@@ -2597,8 +2609,6 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, cons
} }
case GameVersion::BB: { case GameVersion::BB: {
const auto& cmd = check_size_t<C_CharacterData_BB_61_98>(data, 0xFFFF); const auto& cmd = check_size_t<C_CharacterData_BB_61_98>(data, 0xFFFF);
auto account = c->game_data.account();
auto player = c->game_data.player();
// Note: we don't copy the inventory and disp here because we already have // Note: we don't copy the inventory and disp here because we already have
// them (we sent the player data to the client in the first place) // them (we sent the player data to the client in the first place)
player->battle_records = cmd.records.battle; player->battle_records = cmd.records.battle;
@@ -2617,17 +2627,15 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, cons
throw logic_error("player data command not implemented for version"); throw logic_error("player data command not implemented for version");
} }
auto s = c->require_server_state(); string name_str = remove_language_marker(encode_sjis(player->disp.name));
auto player = c->game_data.player(false); c->channel.name = string_printf("C-%" PRIX64 " (%s)", c->id, name_str.c_str());
if (player) {
string name_str = remove_language_marker(encode_sjis(player->disp.name));
c->channel.name = string_printf("C-%" PRIX64 " (%s)",
c->id, name_str.c_str());
}
// 98 should only be sent when leaving a game, and we should leave the client // 98 should only be sent when leaving a game, and we should leave the client
// in no lobby (they will send an 84 soon afterward to choose a lobby). // in no lobby (they will send an 84 soon afterward to choose a lobby).
if (command == 0x98) { if (command == 0x98) {
// If the client had an overlay (for battle/challenge modes), delete it
c->game_data.delete_overlay();
s->remove_client_from_lobby(c); s->remove_client_from_lobby(c);
} else if (command == 0x61) { } else if (command == 0x61) {
@@ -2711,7 +2719,8 @@ static void on_chat_generic(shared_ptr<Client> c, const u16string& text) {
return; return;
} }
u16string from_name = c->game_data.player()->disp.name; auto p = c->game_data.player();
u16string from_name = p->disp.name;
for (size_t x = 0; x < l->max_clients; x++) { for (size_t x = 0; x < l->max_clients; x++) {
if (l->clients[x]) { if (l->clients[x]) {
send_chat_message(l->clients[x], c->license->serial_number, send_chat_message(l->clients[x], c->license->serial_number,
@@ -2729,7 +2738,7 @@ static void on_chat_generic(shared_ptr<Client> c, const u16string& text) {
if (l->battle_record && l->battle_record->battle_in_progress()) { if (l->battle_record && l->battle_record->battle_in_progress()) {
auto prepared_message = prepare_chat_message( auto prepared_message = prepare_chat_message(
c->version(), c->game_data.player()->disp.name.data(), c->version(), p->disp.name.data(),
processed_text.c_str(), private_flags); processed_text.c_str(), private_flags);
string prepared_message_sjis = encode_sjis(prepared_message); string prepared_message_sjis = encode_sjis(prepared_message);
l->battle_record->add_chat_message(c->license->serial_number, std::move(prepared_message_sjis)); l->battle_record->add_chat_message(c->license->serial_number, std::move(prepared_message_sjis));
@@ -3049,9 +3058,10 @@ static void on_00E7_BB(shared_ptr<Client> c, uint16_t, uint32_t, const string& d
// should instead verify our copy of the player against what the client sent, // should instead verify our copy of the player against what the client sent,
// and alert on anything that's out of sync. // and alert on anything that's out of sync.
// TODO: In the future, we should save battle records here too. // TODO: In the future, we should save battle records here too.
c->game_data.player()->challenge_records = cmd.challenge_records; auto p = c->game_data.player();
c->game_data.player()->quest_data1 = cmd.quest_data1; p->challenge_records = cmd.challenge_records;
c->game_data.player()->quest_data2 = cmd.quest_data2; p->quest_data1 = cmd.quest_data1;
p->quest_data2 = cmd.quest_data2;
} }
static void on_00E2_BB(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) { static void on_00E2_BB(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) {
@@ -3139,10 +3149,9 @@ static void on_81(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
// If the target has auto-reply enabled, send the autoreply. Note that we also // If the target has auto-reply enabled, send the autoreply. Note that we also
// forward the message in this case. // forward the message in this case.
if (!target->game_data.player()->auto_reply.empty()) { auto target_p = target->game_data.player();
send_simple_mail(c, target->license->serial_number, if (!target_p->auto_reply.empty()) {
target->game_data.player()->disp.name, send_simple_mail(c, target->license->serial_number, target_p->disp.name, target_p->auto_reply);
target->game_data.player()->auto_reply);
} }
// Forward the message // Forward the message
@@ -3161,10 +3170,9 @@ static void on_D8(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
template <typename CharT> template <typename CharT>
void on_D9_t(shared_ptr<Client> c, const string& data) { void on_D9_t(shared_ptr<Client> c, const string& data) {
check_size_v(data.size(), 0, c->game_data.player()->info_board.size() * sizeof(CharT)); auto p = c->game_data.player(true, false);
c->game_data.player()->info_board.assign( check_size_v(data.size(), 0, p->info_board.size() * sizeof(CharT));
reinterpret_cast<const CharT*>(data.data()), p->info_board.assign(reinterpret_cast<const CharT*>(data.data()), data.size() / sizeof(CharT));
data.size() / sizeof(CharT));
} }
void on_D9_a(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) { void on_D9_a(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) {
@@ -3176,8 +3184,9 @@ void on_D9_w(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) {
template <typename CharT> template <typename CharT>
void on_C7_t(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) { void on_C7_t(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) {
check_size_v(data.size(), 0, c->game_data.player()->auto_reply.size() * sizeof(CharT)); auto p = c->game_data.player(true, false);
c->game_data.player()->auto_reply.assign( check_size_v(data.size(), 0, p->auto_reply.size() * sizeof(CharT));
p->auto_reply.assign(
reinterpret_cast<const CharT*>(data.data()), reinterpret_cast<const CharT*>(data.data()),
data.size() / sizeof(CharT)); data.size() / sizeof(CharT));
} }
@@ -3191,7 +3200,7 @@ void on_C7_w(shared_ptr<Client> c, uint16_t cmd, uint32_t flag, const string& da
static void on_C8(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) { static void on_C8(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) {
check_size_v(data.size(), 0); check_size_v(data.size(), 0);
c->game_data.player()->auto_reply.clear(0); c->game_data.player(true, false)->auto_reply.clear(0);
} }
static void on_C6(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) { static void on_C6(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) {
@@ -3256,8 +3265,9 @@ shared_ptr<Lobby> create_game_generic(
throw runtime_error("invalid episode"); throw runtime_error("invalid episode");
} }
auto p = c->game_data.player();
if (!(c->license->flags & License::Flag::FREE_JOIN_GAMES) && if (!(c->license->flags & License::Flag::FREE_JOIN_GAMES) &&
(min_level > c->game_data.player()->disp.stats.level)) { (min_level > p->disp.stats.level)) {
// Note: We don't throw here because this is a situation players might // Note: We don't throw here because this is a situation players might
// actually encounter while playing the game normally // actually encounter while playing the game normally
send_lobby_message_box(c, u"Your level is too\nlow for this\ndifficulty"); send_lobby_message_box(c, u"Your level is too\nlow for this\ndifficulty");
@@ -3337,7 +3347,7 @@ shared_ptr<Lobby> create_game_generic(
game->section_id = c->options.override_section_id >= 0 game->section_id = c->options.override_section_id >= 0
? c->options.override_section_id ? c->options.override_section_id
: c->game_data.player()->disp.visual.section_id; : p->disp.visual.section_id;
game->episode = episode; game->episode = episode;
game->mode = mode; game->mode = mode;
game->difficulty = difficulty; game->difficulty = difficulty;
@@ -4351,12 +4361,6 @@ void on_command(
uint16_t command, uint16_t command,
uint32_t flag, uint32_t flag,
const string& data) { const string& data) {
string encoded_name;
auto player = c->game_data.player(false);
if (player) {
encoded_name = remove_language_marker(encode_sjis(player->disp.name));
}
c->reschedule_ping_and_timeout_events(); c->reschedule_ping_and_timeout_events();
// Most of the command handlers assume the client is registered, logged in, // Most of the command handlers assume the client is registered, logged in,
+106 -71
View File
@@ -402,18 +402,18 @@ static void on_send_guild_card(shared_ptr<Client> c, uint8_t command, uint8_t fl
switch (c->version()) { switch (c->version()) {
case GameVersion::DC: { case GameVersion::DC: {
const auto& cmd = check_size_t<G_SendGuildCard_DC_6x06>(data, size); const auto& cmd = check_size_t<G_SendGuildCard_DC_6x06>(data, size);
c->game_data.player()->guild_card_description = cmd.description; c->game_data.player(true, false)->guild_card_description = cmd.description;
break; break;
} }
case GameVersion::PC: { case GameVersion::PC: {
const auto& cmd = check_size_t<G_SendGuildCard_PC_6x06>(data, size); const auto& cmd = check_size_t<G_SendGuildCard_PC_6x06>(data, size);
c->game_data.player()->guild_card_description = cmd.description; c->game_data.player(true, false)->guild_card_description = cmd.description;
break; break;
} }
case GameVersion::GC: case GameVersion::GC:
case GameVersion::XB: { case GameVersion::XB: {
const auto& cmd = check_size_t<G_SendGuildCard_V3_6x06>(data, size); const auto& cmd = check_size_t<G_SendGuildCard_V3_6x06>(data, size);
c->game_data.player()->guild_card_description = cmd.description; c->game_data.player(true, false)->guild_card_description = cmd.description;
break; break;
} }
case GameVersion::BB: case GameVersion::BB:
@@ -611,7 +611,8 @@ static void on_player_drop_item(shared_ptr<Client> c, uint8_t command, uint8_t f
auto l = c->require_lobby(); auto l = c->require_lobby();
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
auto item = c->game_data.player()->remove_item(cmd.item_id, 0, c->version() != GameVersion::BB); auto p = c->game_data.player();
auto item = p->remove_item(cmd.item_id, 0, c->version() != GameVersion::BB);
l->add_item(item, cmd.area, cmd.x, cmd.z); l->add_item(item, cmd.area, cmd.x, cmd.z);
auto name = item.name(false); auto name = item.name(false);
@@ -623,7 +624,7 @@ static void on_player_drop_item(shared_ptr<Client> c, uint8_t command, uint8_t f
send_text_message_printf(c, "$C5DROP %08" PRIX32 "\n%s", send_text_message_printf(c, "$C5DROP %08" PRIX32 "\n%s",
cmd.item_id.load(), name.c_str()); cmd.item_id.load(), name.c_str());
} }
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
} }
forward_subcommand(c, command, flag, data, size); forward_subcommand(c, command, flag, data, size);
@@ -665,12 +666,13 @@ static void on_create_inventory_item_t(shared_ptr<Client> c, uint8_t command, ui
auto l = c->require_lobby(); auto l = c->require_lobby();
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
auto p = c->game_data.player();
{ {
ItemData item = cmd.item_data; ItemData item = cmd.item_data;
if (c->version() == GameVersion::GC) { if (c->version() == GameVersion::GC) {
item.bswap_data2_if_mag(); item.bswap_data2_if_mag();
} }
c->game_data.player()->add_item(item); p->add_item(item);
} }
auto name = cmd.item_data.name(false); auto name = cmd.item_data.name(false);
@@ -680,7 +682,7 @@ static void on_create_inventory_item_t(shared_ptr<Client> c, uint8_t command, ui
string name = cmd.item_data.name(true); string name = cmd.item_data.name(true);
send_text_message_printf(c, "$C5CREATE %08" PRIX32 "\n%s", cmd.item_data.id.load(), name.c_str()); send_text_message_printf(c, "$C5CREATE %08" PRIX32 "\n%s", cmd.item_data.id.load(), name.c_str());
} }
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
} }
forward_subcommand_with_mag_bswap_t(c, command, flag, cmd); forward_subcommand_with_mag_bswap_t(c, command, flag, cmd);
@@ -757,8 +759,8 @@ static void on_drop_partial_stack_bb(shared_ptr<Client> c, uint8_t command, uint
throw logic_error("item tracking not enabled in BB game"); throw logic_error("item tracking not enabled in BB game");
} }
auto item = c->game_data.player()->remove_item( auto p = c->game_data.player();
cmd.item_id, cmd.amount, c->version() != GameVersion::BB); auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB);
// if a stack was split, the original item still exists, so the dropped item // if a stack was split, the original item still exists, so the dropped item
// needs a new ID. remove_item signals this by returning an item with id=-1 // needs a new ID. remove_item signals this by returning an item with id=-1
@@ -769,7 +771,7 @@ static void on_drop_partial_stack_bb(shared_ptr<Client> c, uint8_t command, uint
// PSOBB sends a 6x29 command after it receives the 6x5D, so we need to add // PSOBB sends a 6x29 command after it receives the 6x5D, so we need to add
// the item back to the player's inventory to correct for this (it will get // the item back to the player's inventory to correct for this (it will get
// removed again by the 6x29 handler) // removed again by the 6x29 handler)
c->game_data.player()->add_item(item); p->add_item(item);
l->add_item(item, cmd.area, cmd.x, cmd.z); l->add_item(item, cmd.area, cmd.x, cmd.z);
@@ -782,7 +784,7 @@ static void on_drop_partial_stack_bb(shared_ptr<Client> c, uint8_t command, uint
send_text_message_printf(c, "$C5SPLIT/BB %08" PRIX32 "\n%s", send_text_message_printf(c, "$C5SPLIT/BB %08" PRIX32 "\n%s",
cmd.item_id.load(), name.c_str()); cmd.item_id.load(), name.c_str());
} }
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
send_drop_stacked_item(l, item, cmd.area, cmd.x, cmd.z); send_drop_stacked_item(l, item, cmd.area, cmd.x, cmd.z);
@@ -889,8 +891,9 @@ static void on_pick_up_item(shared_ptr<Client> c, uint8_t command, uint8_t flag,
} }
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
auto effective_p = effective_c->game_data.player();
auto item = l->remove_item(cmd.item_id); auto item = l->remove_item(cmd.item_id);
effective_c->game_data.player()->add_item(item); effective_p->add_item(item);
auto name = item.name(false); auto name = item.name(false);
l->log.info("Player %hu picked up %08" PRIX32 " (%s)", l->log.info("Player %hu picked up %08" PRIX32 " (%s)",
@@ -899,7 +902,7 @@ static void on_pick_up_item(shared_ptr<Client> c, uint8_t command, uint8_t flag,
string name = item.name(true); string name = item.name(true);
send_text_message_printf(c, "$C5PICK %08" PRIX32 "\n%s", cmd.item_id.load(), name.c_str()); send_text_message_printf(c, "$C5PICK %08" PRIX32 "\n%s", cmd.item_id.load(), name.c_str());
} }
effective_c->game_data.player()->print_inventory(stderr); effective_p->print_inventory(stderr);
} }
forward_subcommand(c, command, flag, data, size); forward_subcommand(c, command, flag, data, size);
@@ -919,8 +922,9 @@ static void on_pick_up_item_request(shared_ptr<Client> c, uint8_t command, uint8
throw logic_error("item tracking not enabled in BB game"); throw logic_error("item tracking not enabled in BB game");
} }
auto p = c->game_data.player();
auto item = l->remove_item(cmd.item_id); auto item = l->remove_item(cmd.item_id);
c->game_data.player()->add_item(item); p->add_item(item);
auto name = item.name(false); auto name = item.name(false);
l->log.info("Player %hu picked up %08" PRIX32 " (%s)", l->log.info("Player %hu picked up %08" PRIX32 " (%s)",
@@ -930,7 +934,7 @@ static void on_pick_up_item_request(shared_ptr<Client> c, uint8_t command, uint8
send_text_message_printf(c, "$C5PICK/BB %08" PRIX32 "\n%s", send_text_message_printf(c, "$C5PICK/BB %08" PRIX32 "\n%s",
cmd.item_id.load(), name.c_str()); cmd.item_id.load(), name.c_str());
} }
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
send_pick_up_item(c, cmd.item_id, cmd.area); send_pick_up_item(c, cmd.item_id, cmd.area);
@@ -948,11 +952,12 @@ static void on_equip_unequip_item(shared_ptr<Client> c, uint8_t command, uint8_t
auto l = c->require_lobby(); auto l = c->require_lobby();
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
size_t index = c->game_data.player()->inventory.find_item(cmd.item_id); auto p = c->game_data.player();
size_t index = p->inventory.find_item(cmd.item_id);
if (cmd.header.subcommand == 0x25) { // Equip if (cmd.header.subcommand == 0x25) { // Equip
c->game_data.player()->inventory.items[index].flags |= 0x00000008; p->inventory.items[index].flags |= 0x00000008;
} else { // Unequip } else { // Unequip
c->game_data.player()->inventory.items[index].flags &= 0xFFFFFFF7; p->inventory.items[index].flags &= 0xFFFFFFF7;
} }
} else if (l->base_version == GameVersion::BB) { } else if (l->base_version == GameVersion::BB) {
throw logic_error("item tracking not enabled in BB game"); throw logic_error("item tracking not enabled in BB game");
@@ -974,12 +979,13 @@ static void on_use_item(
auto l = c->require_lobby(); auto l = c->require_lobby();
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
size_t index = c->game_data.player()->inventory.find_item(cmd.item_id); auto p = c->game_data.player();
size_t index = p->inventory.find_item(cmd.item_id);
string name, colored_name; string name, colored_name;
{ {
// Note: We do this weird scoping thing because player_use_item will // Note: We do this weird scoping thing because player_use_item will
// likely delete the item, which will break the reference here. // likely delete the item, which will break the reference here.
const auto& item = c->game_data.player()->inventory.items[index].data; const auto& item = p->inventory.items[index].data;
name = item.name(false); name = item.name(false);
colored_name = item.name(true); colored_name = item.name(true);
} }
@@ -991,7 +997,7 @@ static void on_use_item(
send_text_message_printf(c, "$C5USE %08" PRIX32 "\n%s", send_text_message_printf(c, "$C5USE %08" PRIX32 "\n%s",
cmd.item_id.load(), colored_name.c_str()); cmd.item_id.load(), colored_name.c_str());
} }
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
} }
forward_subcommand(c, command, flag, data, size); forward_subcommand(c, command, flag, data, size);
@@ -1010,16 +1016,18 @@ static void on_feed_mag(
auto l = c->require_lobby(); auto l = c->require_lobby();
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
size_t mag_index = c->game_data.player()->inventory.find_item(cmd.mag_item_id); auto p = c->game_data.player();
size_t fed_index = c->game_data.player()->inventory.find_item(cmd.fed_item_id);
size_t mag_index = p->inventory.find_item(cmd.mag_item_id);
size_t fed_index = p->inventory.find_item(cmd.fed_item_id);
string mag_name, mag_colored_name, fed_name, fed_colored_name; string mag_name, mag_colored_name, fed_name, fed_colored_name;
{ {
// Note: We do this weird scoping thing because player_use_item will // Note: We do this weird scoping thing because player_use_item will
// likely delete the item, which will break the reference here. // likely delete the item, which will break the reference here.
const auto& fed_item = c->game_data.player()->inventory.items[fed_index].data; const auto& fed_item = p->inventory.items[fed_index].data;
fed_name = fed_item.name(false); fed_name = fed_item.name(false);
fed_colored_name = fed_item.name(true); fed_colored_name = fed_item.name(true);
const auto& mag_item = c->game_data.player()->inventory.items[mag_index].data; const auto& mag_item = p->inventory.items[mag_index].data;
mag_name = mag_item.name(false); mag_name = mag_item.name(false);
mag_colored_name = mag_item.name(true); mag_colored_name = mag_item.name(true);
} }
@@ -1030,7 +1038,7 @@ static void on_feed_mag(
// remove the fed item here, but on other versions, we allow the following // remove the fed item here, but on other versions, we allow the following
// 6x29 command to do that. // 6x29 command to do that.
if (l->base_version == GameVersion::BB) { if (l->base_version == GameVersion::BB) {
c->game_data.player()->remove_item(cmd.fed_item_id, 1, false); p->remove_item(cmd.fed_item_id, 1, false);
} }
l->log.info("Player fed item %hu:%08" PRIX32 " (%s) to mag %hu:%08" PRIX32 " (%s)", l->log.info("Player fed item %hu:%08" PRIX32 " (%s) to mag %hu:%08" PRIX32 " (%s)",
@@ -1041,7 +1049,7 @@ static void on_feed_mag(
cmd.fed_item_id.load(), fed_colored_name.c_str(), cmd.fed_item_id.load(), fed_colored_name.c_str(),
cmd.mag_item_id.load(), mag_colored_name.c_str()); cmd.mag_item_id.load(), mag_colored_name.c_str());
} }
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
} }
forward_subcommand(c, command, flag, data, size); forward_subcommand(c, command, flag, data, size);
@@ -1109,35 +1117,36 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
throw logic_error("item tracking not enabled in BB game"); throw logic_error("item tracking not enabled in BB game");
} }
auto p = c->game_data.player();
if (cmd.action == 0) { // deposit if (cmd.action == 0) { // deposit
if (cmd.item_id == 0xFFFFFFFF) { // meseta if (cmd.item_id == 0xFFFFFFFF) { // meseta
if (cmd.meseta_amount > c->game_data.player()->disp.stats.meseta) { if (cmd.meseta_amount > p->disp.stats.meseta) {
return; return;
} }
if ((c->game_data.player()->bank.meseta + cmd.meseta_amount) > 999999) { if ((p->bank.meseta + cmd.meseta_amount) > 999999) {
return; return;
} }
c->game_data.player()->bank.meseta += cmd.meseta_amount; p->bank.meseta += cmd.meseta_amount;
c->game_data.player()->disp.stats.meseta -= cmd.meseta_amount; p->disp.stats.meseta -= cmd.meseta_amount;
} else { // item } else { // item
auto item = c->game_data.player()->remove_item(cmd.item_id, cmd.item_amount, c->version() != GameVersion::BB); auto item = p->remove_item(cmd.item_id, cmd.item_amount, c->version() != GameVersion::BB);
c->game_data.player()->bank.add_item(item); p->bank.add_item(item);
send_destroy_item(c, cmd.item_id, cmd.item_amount); send_destroy_item(c, cmd.item_id, cmd.item_amount);
} }
} else if (cmd.action == 1) { // take } else if (cmd.action == 1) { // take
if (cmd.item_id == 0xFFFFFFFF) { // meseta if (cmd.item_id == 0xFFFFFFFF) { // meseta
if (cmd.meseta_amount > c->game_data.player()->bank.meseta) { if (cmd.meseta_amount > p->bank.meseta) {
return; return;
} }
if ((c->game_data.player()->disp.stats.meseta + cmd.meseta_amount) > 999999) { if ((p->disp.stats.meseta + cmd.meseta_amount) > 999999) {
return; return;
} }
c->game_data.player()->bank.meseta -= cmd.meseta_amount; p->bank.meseta -= cmd.meseta_amount;
c->game_data.player()->disp.stats.meseta += cmd.meseta_amount; p->disp.stats.meseta += cmd.meseta_amount;
} else { // item } else { // item
auto item = c->game_data.player()->bank.remove_item(cmd.item_id, cmd.item_amount); auto item = p->bank.remove_item(cmd.item_id, cmd.item_amount);
item.id = l->generate_item_id(0xFF); item.id = l->generate_item_id(0xFF);
c->game_data.player()->add_item(item); p->add_item(item);
send_create_inventory_item(c, item); send_create_inventory_item(c, item);
} }
} }
@@ -1156,23 +1165,43 @@ static void on_sort_inventory_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
throw logic_error("item tracking not enabled in BB game"); throw logic_error("item tracking not enabled in BB game");
} }
PlayerInventory sorted; auto p = c->game_data.player();
const auto& inv = c->game_data.player()->inventory; // Make sure the set of item IDs passed in by the client exactly matches the
// set of item IDs present in the inventory
unordered_set<uint32_t> sorted_item_ids;
size_t expected_count = 0;
for (size_t x = 0; x < 30; x++) { for (size_t x = 0; x < 30; x++) {
if (cmd.item_ids[x] == 0xFFFFFFFF) { if (cmd.item_ids[x] != 0xFFFFFFFF) {
sorted.items[x].data.id = 0xFFFFFFFF; sorted_item_ids.emplace(cmd.item_ids[x]);
} else { expected_count++;
size_t index = inv.find_item(cmd.item_ids[x]);
sorted.items[x] = inv.items[index];
} }
} }
if (sorted_item_ids.size() != expected_count) {
throw runtime_error("sorted array contains duplicate item IDs");
}
if (sorted_item_ids.size() != p->inventory.num_items) {
throw runtime_error("sorted array contains a different number of items than the inventory contains");
}
for (size_t x = 0; x < p->inventory.num_items; x++) {
if (!sorted_item_ids.erase(cmd.item_ids[x])) {
throw runtime_error("inventory contains item ID not present in sorted array");
}
}
if (!sorted_item_ids.empty()) {
throw runtime_error("sorted array contains item ID not present in inventory");
}
sorted.num_items = inv.num_items; parray<PlayerInventoryItem, 30> sorted;
sorted.hp_materials_used = inv.hp_materials_used; for (size_t x = 0; x < 30; x++) {
sorted.tp_materials_used = inv.tp_materials_used; if (cmd.item_ids[x] == 0xFFFFFFFF) {
sorted.language = inv.language; sorted[x].data.id = 0xFFFFFFFF;
c->game_data.player()->inventory = sorted; } else {
size_t index = p->inventory.find_item(cmd.item_ids[x]);
sorted[x] = p->inventory.items[index];
}
}
p->inventory.items = sorted;
} }
} }
@@ -1256,15 +1285,19 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
if (flag_index >= 0x400) { if (flag_index >= 0x400) {
return; return;
} }
// TODO: Should we allow overlays here?
auto p = c->game_data.player(true, false);
// The client explicitly checks for both 0 and 1 - any other value means no // The client explicitly checks for both 0 and 1 - any other value means no
// operation is performed. // operation is performed.
size_t bit_index = (difficulty << 10) + flag_index; size_t bit_index = (difficulty << 10) + flag_index;
size_t byte_index = bit_index >> 3; size_t byte_index = bit_index >> 3;
uint8_t mask = 0x80 >> (bit_index & 7); uint8_t mask = 0x80 >> (bit_index & 7);
if (action == 0) { if (action == 0) {
c->game_data.player()->quest_data1[byte_index] |= mask; p->quest_data1[byte_index] |= mask;
} else if (action == 1) { } else if (action == 1) {
c->game_data.player()->quest_data1[byte_index] &= (~mask); p->quest_data1[byte_index] &= (~mask);
} }
forward_subcommand(c, command, flag, data, size); forward_subcommand(c, command, flag, data, size);
@@ -1354,22 +1387,23 @@ static void on_charge_attack_bb(shared_ptr<Client> c, uint8_t command, uint8_t f
static void add_player_exp(shared_ptr<Client> c, uint32_t exp) { static void add_player_exp(shared_ptr<Client> c, uint32_t exp) {
auto s = c->require_server_state(); auto s = c->require_server_state();
auto p = c->game_data.player();
c->game_data.player()->disp.stats.experience += exp; p->disp.stats.experience += exp;
send_give_experience(c, exp); send_give_experience(c, exp);
bool leveled_up = false; bool leveled_up = false;
do { do {
const auto& level = s->level_table->stats_for_level( const auto& level = s->level_table->stats_delta_for_level(
c->game_data.player()->disp.visual.char_class, c->game_data.player()->disp.stats.level + 1); p->disp.visual.char_class, p->disp.stats.level + 1);
if (c->game_data.player()->disp.stats.experience >= level.experience) { if (p->disp.stats.experience >= level.experience) {
leveled_up = true; leveled_up = true;
level.apply(c->game_data.player()->disp.stats.char_stats); level.apply(p->disp.stats.char_stats);
c->game_data.player()->disp.stats.level++; p->disp.stats.level++;
} else { } else {
break; break;
} }
} while (c->game_data.player()->disp.stats.level < 199); } while (p->disp.stats.level < 199);
if (leveled_up) { if (leveled_up) {
send_level_up(c); send_level_up(c);
} }
@@ -1388,8 +1422,9 @@ static void on_steal_exp_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void*
const auto& cmd = check_size_t<G_StealEXP_BB_6xC6>(data, size); const auto& cmd = check_size_t<G_StealEXP_BB_6xC6>(data, size);
auto p = c->game_data.player();
const auto& enemy = l->map->enemies.at(cmd.enemy_id); const auto& enemy = l->map->enemies.at(cmd.enemy_id);
const auto& inventory = c->game_data.player()->inventory; const auto& inventory = p->inventory;
const auto& weapon = inventory.items[inventory.find_equipped_weapon()]; const auto& weapon = inventory.items[inventory.find_equipped_weapon()];
uint8_t special = 0; uint8_t special = 0;
@@ -1402,7 +1437,7 @@ static void on_steal_exp_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void*
if (special >= 0x09 && special <= 0x0B) { if (special >= 0x09 && special <= 0x0B) {
// Master's = 8, Lord's = 10, King's = 12 // Master's = 8, Lord's = 10, King's = 12
uint32_t percent = 8 + ((special - 9) << 1) + (char_class_is_android(c->game_data.player()->disp.visual.char_class) ? 30 : 0); uint32_t percent = 8 + ((special - 9) << 1) + (char_class_is_android(p->disp.visual.char_class) ? 30 : 0);
uint32_t enemy_exp = s->battle_params->get(l->mode == GameMode::SOLO, l->episode, l->difficulty, enemy.type).experience; uint32_t enemy_exp = s->battle_params->get(l->mode == GameMode::SOLO, l->episode, l->difficulty, enemy.type).experience;
uint32_t stolen_exp = min<uint32_t>((enemy_exp * percent) / 100, 80); uint32_t stolen_exp = min<uint32_t>((enemy_exp * percent) / 100, 80);
if (c->options.debug) { if (c->options.debug) {
@@ -1511,7 +1546,7 @@ void on_meseta_reward_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
item.data1[0] = 0x04; item.data1[0] = 0x04;
item.data2d = cmd.amount.load(); item.data2d = cmd.amount.load();
item.id = l->generate_item_id(0xFF); item.id = l->generate_item_id(0xFF);
c->game_data.player()->add_item(item); p->add_item(item);
send_create_inventory_item(c, item); send_create_inventory_item(c, item);
} }
} }
@@ -1539,8 +1574,8 @@ static void on_destroy_inventory_item(shared_ptr<Client> c, uint8_t command, uin
} }
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
auto item = c->game_data.player()->remove_item( auto p = c->game_data.player();
cmd.item_id, cmd.amount, c->version() != GameVersion::BB); auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB);
auto name = item.name(false); auto name = item.name(false);
l->log.info("Inventory item %hu:%08" PRIX32 " destroyed (%s)", l->log.info("Inventory item %hu:%08" PRIX32 " destroyed (%s)",
cmd.header.client_id.load(), cmd.item_id.load(), name.c_str()); cmd.header.client_id.load(), cmd.item_id.load(), name.c_str());
@@ -1549,7 +1584,7 @@ static void on_destroy_inventory_item(shared_ptr<Client> c, uint8_t command, uin
send_text_message_printf(c, "$C5DESTROY %08" PRIX32 "\n%s", send_text_message_printf(c, "$C5DESTROY %08" PRIX32 "\n%s",
cmd.item_id.load(), name.c_str()); cmd.item_id.load(), name.c_str());
} }
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
forward_subcommand(c, command, flag, data, size); forward_subcommand(c, command, flag, data, size);
} }
} }
@@ -1590,14 +1625,14 @@ static void on_identify_item_bb(shared_ptr<Client> c, uint8_t command, uint8_t f
throw logic_error("received item identify subcommand without item creator present"); throw logic_error("received item identify subcommand without item creator present");
} }
size_t x = c->game_data.player()->inventory.find_item(cmd.item_id); auto p = c->game_data.player();
if (c->game_data.player()->inventory.items[x].data.data1[0] != 0) { size_t x = p->inventory.find_item(cmd.item_id);
if (p->inventory.items[x].data.data1[0] != 0) {
return; // Only weapons can be identified return; // Only weapons can be identified
} }
auto p = c->game_data.player();
p->disp.stats.meseta -= 100; p->disp.stats.meseta -= 100;
c->game_data.identify_result = c->game_data.player()->inventory.items[x].data; c->game_data.identify_result = p->inventory.items[x].data;
c->game_data.identify_result.data1[4] &= 0x7F; c->game_data.identify_result.data1[4] &= 0x7F;
l->item_creator->apply_tekker_deltas(c->game_data.identify_result, p->disp.visual.section_id); l->item_creator->apply_tekker_deltas(c->game_data.identify_result, p->disp.visual.section_id);
send_item_identify_result(c); send_item_identify_result(c);
@@ -1649,7 +1684,7 @@ static void on_sell_item_at_shop_bb(shared_ptr<Client> c, uint8_t command, uint8
auto name = item.name(false); auto name = item.name(false);
l->log.info("Inventory item %hu:%08" PRIX32 " (%s) destroyed via sale (%zu Meseta)", l->log.info("Inventory item %hu:%08" PRIX32 " (%s) destroyed via sale (%zu Meseta)",
c->lobby_client_id, cmd.item_id.load(), name.c_str(), price); c->lobby_client_id, cmd.item_id.load(), name.c_str(), price);
c->game_data.player()->print_inventory(stderr); p->print_inventory(stderr);
if (c->options.debug) { if (c->options.debug) {
string name = item.name(true); string name = item.name(true);
send_text_message_printf(c, "$C5DESTROY/SELL %08" PRIX32 "\n+%zu Meseta\n%s", send_text_message_printf(c, "$C5DESTROY/SELL %08" PRIX32 "\n+%zu Meseta\n%s",
+60 -53
View File
@@ -613,7 +613,7 @@ void send_approve_player_choice_bb(shared_ptr<Client> c) {
void send_complete_player_bb(shared_ptr<Client> c) { void send_complete_player_bb(shared_ptr<Client> c) {
auto account = c->game_data.account(); auto account = c->game_data.account();
auto player = c->game_data.player(); auto player = c->game_data.player(true, false);
SC_SyncCharacterSaveFile_BB_00E7 cmd; SC_SyncCharacterSaveFile_BB_00E7 cmd;
cmd.inventory = player->inventory; cmd.inventory = player->inventory;
@@ -922,13 +922,14 @@ template <typename CharT>
void send_info_board_t(shared_ptr<Client> c) { void send_info_board_t(shared_ptr<Client> c) {
vector<S_InfoBoardEntry_D8<CharT>> entries; vector<S_InfoBoardEntry_D8<CharT>> entries;
auto l = c->require_lobby(); auto l = c->require_lobby();
for (const auto& c : l->clients) { for (const auto& other_c : l->clients) {
if (!c.get()) { if (!other_c.get()) {
continue; continue;
} }
auto other_p = other_c->game_data.player(true, false);
auto& e = entries.emplace_back(); auto& e = entries.emplace_back();
e.name = c->game_data.player()->disp.name; e.name = other_p->disp.name;
e.message = c->game_data.player()->info_board; e.message = other_p->info_board;
add_color_inplace(e.message); add_color_inplace(e.message);
} }
send_command_vt(c, 0xD8, entries.size(), entries); send_command_vt(c, 0xD8, entries.size(), entries);
@@ -979,7 +980,7 @@ void send_card_search_result_t(
cmd.location_string = location_string; cmd.location_string = location_string;
cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY; cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY;
cmd.extension.lobby_refs[0].item_id = result_lobby->lobby_id; cmd.extension.lobby_refs[0].item_id = result_lobby->lobby_id;
cmd.extension.player_name = result->game_data.player()->disp.name; cmd.extension.player_name = result->game_data.player(true, false)->disp.name;
send_command_t(c, 0x41, 0x00, cmd); send_command_t(c, 0x41, 0x00, cmd);
} }
@@ -1079,11 +1080,12 @@ void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
throw runtime_error("source player does not have a license"); throw runtime_error("source player does not have a license");
} }
auto source_p = source->game_data.player(true, false);
uint32_t guild_card_number = source->license->serial_number; uint32_t guild_card_number = source->license->serial_number;
u16string name = source->game_data.player()->disp.name; u16string name = source_p->disp.name;
u16string description = source->game_data.player()->guild_card_description; u16string description = source_p->guild_card_description;
uint8_t section_id = source->game_data.player()->disp.visual.section_id; uint8_t section_id = source_p->disp.visual.section_id;
uint8_t char_class = source->game_data.player()->disp.visual.char_class; uint8_t char_class = source_p->disp.visual.char_class;
send_guild_card( send_guild_card(
c->channel, guild_card_number, name, u"", description, section_id, char_class); c->channel, guild_card_number, name, u"", description, section_id, char_class);
@@ -1388,7 +1390,7 @@ template <typename EntryT>
void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) { void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
vector<EntryT> entries; vector<EntryT> entries;
auto add_client = [&](shared_ptr<Client> lc) -> void { auto add_client = [&](shared_ptr<Client> lc) -> void {
auto lp = lc->game_data.player(); auto lp = lc->game_data.player(true, false);
auto& e = entries.emplace_back(); auto& e = entries.emplace_back();
e.client_id = lc->lobby_client_id; e.client_id = lc->lobby_client_id;
e.challenge = lp->challenge_records; e.challenge = lp->challenge_records;
@@ -1512,27 +1514,28 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
for (size_t z = 4; z < 12; z++) { for (size_t z = 4; z < 12; z++) {
if (l->clients[z]) { if (l->clients[z]) {
auto& gd = l->clients[z]->game_data; auto other_c = l->clients[z];
auto& p = cmd.spectator_players[z - 4]; auto other_p = other_c->game_data.player();
auto& e = cmd.entries[z]; auto& cmd_p = cmd.spectator_players[z - 4];
p.lobby_data.player_tag = 0x00010000; auto& cmd_e = cmd.entries[z];
p.lobby_data.guild_card = l->clients[z]->license->serial_number; cmd_p.lobby_data.player_tag = 0x00010000;
p.lobby_data.client_id = l->clients[z]->lobby_client_id; cmd_p.lobby_data.guild_card = other_c->license->serial_number;
p.lobby_data.name = gd.player()->disp.name; cmd_p.lobby_data.client_id = other_c->lobby_client_id;
remove_language_marker_inplace(p.lobby_data.name); cmd_p.lobby_data.name = other_p->disp.name;
p.inventory = gd.player()->inventory; remove_language_marker_inplace(cmd_p.lobby_data.name);
p.disp = gd.player()->disp.to_dcpcv3(); cmd_p.inventory = other_p->inventory;
remove_language_marker_inplace(p.disp.visual.name); cmd_p.disp = other_p->disp.to_dcpcv3();
remove_language_marker_inplace(cmd_p.disp.visual.name);
e.player_tag = 0x00010000; cmd_e.player_tag = 0x00010000;
e.guild_card_number = l->clients[z]->license->serial_number; cmd_e.guild_card_number = other_c->license->serial_number;
e.name = gd.player()->disp.name; cmd_e.name = other_p->disp.name;
remove_language_marker_inplace(e.name); remove_language_marker_inplace(cmd_e.name);
e.present = 1; cmd_e.present = 1;
e.level = gd.ep3_config cmd_e.level = other_c->game_data.ep3_config
? (gd.ep3_config->online_clv_exp / 100) ? (other_c->game_data.ep3_config->online_clv_exp / 100)
: gd.player()->disp.stats.level.load(); : other_p->disp.stats.level.load();
e.name_color = gd.player()->disp.visual.name_color; cmd_e.name_color = other_p->disp.visual.name_color;
player_count++; player_count++;
} }
@@ -1625,12 +1628,12 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
size_t player_count = populate_v3_cmd(cmd); size_t player_count = populate_v3_cmd(cmd);
for (size_t x = 0; x < 4; x++) { for (size_t x = 0; x < 4; x++) {
if (l->clients[x]) { if (l->clients[x]) {
cmd.players_ep3[x].inventory = l->clients[x]->game_data.player()->inventory; auto other_p = l->clients[x]->game_data.player();
cmd.players_ep3[x].inventory = other_p->inventory;
for (size_t z = 0; z < 30; z++) { for (size_t z = 0; z < 30; z++) {
cmd.players_ep3[x].inventory.items[z].data.bswap_data2_if_mag(); cmd.players_ep3[x].inventory.items[z].data.bswap_data2_if_mag();
} }
cmd.players_ep3[x].disp = convert_player_disp_data<PlayerDispDataDCPCV3>( cmd.players_ep3[x].disp = convert_player_disp_data<PlayerDispDataDCPCV3>(other_p->disp);
l->clients[x]->game_data.player()->disp);
} }
} }
send_command_t(c, 0x64, player_count, cmd); send_command_t(c, 0x64, player_count, cmd);
@@ -1732,22 +1735,23 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l,
size_t used_entries = 0; size_t used_entries = 0;
for (const auto& lc : lobby_clients) { for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.player();
auto& e = cmd.entries[used_entries++]; auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000; e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card = lc->license->serial_number; e.lobby_data.guild_card = lc->license->serial_number;
e.lobby_data.client_id = lc->lobby_client_id; e.lobby_data.client_id = lc->lobby_client_id;
e.lobby_data.name = lc->game_data.player()->disp.name; e.lobby_data.name = lp->disp.name;
remove_language_marker_inplace(e.lobby_data.name); remove_language_marker_inplace(e.lobby_data.name);
if (UseLanguageMarkerInName) { if (UseLanguageMarkerInName) {
add_language_marker_inplace(e.lobby_data.name, 'J'); add_language_marker_inplace(e.lobby_data.name, 'J');
} }
e.inventory = lc->game_data.player()->inventory; e.inventory = lp->inventory;
if (c->version() == GameVersion::GC) { if (c->version() == GameVersion::GC) {
for (size_t z = 0; z < 30; z++) { for (size_t z = 0; z < 30; z++) {
e.inventory.items[z].data.bswap_data2_if_mag(); e.inventory.items[z].data.bswap_data2_if_mag();
} }
} }
e.disp = convert_player_disp_data<DispDataT>(lc->game_data.player()->disp); e.disp = convert_player_disp_data<DispDataT>(lp->disp);
e.disp.enforce_lobby_join_limits(c->version()); e.disp.enforce_lobby_join_limits(c->version());
} }
@@ -1785,13 +1789,14 @@ void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
size_t used_entries = 0; size_t used_entries = 0;
for (const auto& lc : lobby_clients) { for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.player();
auto& e = cmd.entries[used_entries++]; auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000; e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card = lc->license->serial_number; e.lobby_data.guild_card = lc->license->serial_number;
e.lobby_data.client_id = lc->lobby_client_id; e.lobby_data.client_id = lc->lobby_client_id;
e.lobby_data.name = lc->game_data.player()->disp.name; e.lobby_data.name = lp->disp.name;
e.inventory = lc->game_data.player()->inventory; e.inventory = lp->inventory;
e.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(lc->game_data.player()->disp); e.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(lp->disp);
e.disp.enforce_lobby_join_limits(c->version()); e.disp.enforce_lobby_join_limits(c->version());
} }
@@ -2112,14 +2117,15 @@ void send_bank(shared_ptr<Client> c) {
throw logic_error("6xBC can only be sent to BB clients"); throw logic_error("6xBC can only be sent to BB clients");
} }
const auto* items_it = c->game_data.player()->bank.items.data(); auto p = c->game_data.player();
vector<PlayerBankItem> items(items_it, items_it + c->game_data.player()->bank.num_items); const auto* items_it = p->bank.items.data();
vector<PlayerBankItem> items(items_it, items_it + p->bank.num_items);
G_BankContentsHeader_BB_6xBC cmd = { G_BankContentsHeader_BB_6xBC cmd = {
{{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + items.size() * sizeof(PlayerBankItem)}, {{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + items.size() * sizeof(PlayerBankItem)},
random_object<uint32_t>(), random_object<uint32_t>(),
c->game_data.player()->bank.num_items, p->bank.num_items,
c->game_data.player()->bank.meseta}; p->bank.meseta};
send_command_t_vt(c, 0x6C, 0x00, cmd, items); send_command_t_vt(c, 0x6C, 0x00, cmd, items);
} }
@@ -2148,15 +2154,16 @@ void send_shop(shared_ptr<Client> c, uint8_t shop_type) {
// notifies players about a level up // notifies players about a level up
void send_level_up(shared_ptr<Client> c) { void send_level_up(shared_ptr<Client> c) {
auto l = c->require_lobby(); auto l = c->require_lobby();
CharacterStats stats = c->game_data.player()->disp.stats.char_stats; auto p = c->game_data.player();
CharacterStats stats = p->disp.stats.char_stats;
for (size_t x = 0; x < c->game_data.player()->inventory.num_items; x++) { for (size_t x = 0; x < p->inventory.num_items; x++) {
if ((c->game_data.player()->inventory.items[x].flags & 0x08) && if ((p->inventory.items[x].flags & 0x08) &&
(c->game_data.player()->inventory.items[x].data.data1[0] == 0x02)) { (p->inventory.items[x].data.data1[0] == 0x02)) {
stats.dfp += (c->game_data.player()->inventory.items[x].data.data1w[2] / 100); stats.dfp += (p->inventory.items[x].data.data1w[2] / 100);
stats.atp += (c->game_data.player()->inventory.items[x].data.data1w[3] / 50); stats.atp += (p->inventory.items[x].data.data1w[3] / 50);
stats.ata += (c->game_data.player()->inventory.items[x].data.data1w[4] / 200); stats.ata += (p->inventory.items[x].data.data1w[4] / 200);
stats.mst += (c->game_data.player()->inventory.items[x].data.data1w[5] / 50); stats.mst += (p->inventory.items[x].data.data1w[5] / 50);
} }
} }
@@ -2168,7 +2175,7 @@ void send_level_up(shared_ptr<Client> c) {
stats.hp, stats.hp,
stats.dfp, stats.dfp,
stats.ata, stats.ata,
c->game_data.player()->disp.stats.level.load(), p->disp.stats.level.load(),
0}; 0};
send_command_t(l, 0x60, 0x00, cmd); send_command_t(l, 0x60, 0x00, cmd);
} }
+1 -1
View File
@@ -315,7 +315,7 @@ vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident
continue; continue;
} }
auto p = c->game_data.player(false); auto p = c->game_data.player(false, false);
if (p && p->disp.name == u16name) { if (p && p->disp.name == u16name) {
results.emplace_back(std::move(c)); results.emplace_back(std::move(c));
continue; continue;
+2
View File
@@ -2,6 +2,8 @@
#include <array> #include <array>
#include "Text.hh"
using namespace std; using namespace std;
bool episode_has_arpg_semantics(Episode ep) { bool episode_has_arpg_semantics(Episode ep) {
-1
View File
@@ -7,7 +7,6 @@
#include <vector> #include <vector>
#include "FileContentsCache.hh" #include "FileContentsCache.hh"
#include "Player.hh"
enum class Episode { enum class Episode {
NONE = 0, NONE = 0,