From 8c2ce5210d31aa7dc4fa7c59ce4f54f6f84e02e1 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 18 Oct 2023 11:55:31 -0700 Subject: [PATCH] implement battle rules and character replacement --- src/ChatCommands.cc | 39 +++++---- src/Client.hh | 3 +- src/CommandFormats.hh | 2 +- src/ItemCreator.cc | 24 +++--- src/ItemCreator.hh | 50 ++--------- src/ItemData.cc | 2 + src/LevelTable.cc | 2 +- src/LevelTable.hh | 45 +++++++++- src/Lobby.cc | 13 +-- src/Player.cc | 101 ++++++++++++++++++++-- src/Player.hh | 19 +++- src/PlayerSubordinates.cc | 160 ++++++++++++++++++++++++++++++++-- src/PlayerSubordinates.hh | 57 +++++++++--- src/Quest.cc | 69 +++++++++++++-- src/Quest.hh | 6 +- src/QuestScript.cc | 22 ++--- src/RareItemSet.cc | 1 + src/RareItemSet.hh | 2 + src/ReceiveCommands.cc | 92 ++++++++++---------- src/ReceiveSubcommands.cc | 177 +++++++++++++++++++++++--------------- src/SendCommands.cc | 113 ++++++++++++------------ src/Server.cc | 2 +- src/StaticGameData.cc | 2 + src/StaticGameData.hh | 1 - 24 files changed, 699 insertions(+), 305 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 34210a03..a16794e4 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -800,58 +800,59 @@ static void server_command_edit(shared_ptr c, const std::u16string& args vector tokens = split(encoded_args, ' '); try { + auto p = c->game_data.player(); 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") { - 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") { - 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") { - 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") { - 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") { - 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") { - 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") { - 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") { - 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") { - 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") { uint32_t 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") { uint8_t secid = section_id_for_name(decode_sjis(tokens.at(1))); if (secid == 0xFF) { send_text_message(c, u"$C6No such section ID"); return; } else { - c->game_data.player()->disp.visual.section_id = secid; + p->disp.visual.section_id = secid; } } 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") { if (tokens.at(1) == "none") { - c->game_data.player()->disp.visual.extra_model = 0; - c->game_data.player()->disp.visual.v2_flags &= 0xFD; + p->disp.visual.extra_model = 0; + p->disp.visual.v2_flags &= 0xFD; } else { uint8_t npc = npc_for_name(decode_sjis(tokens.at(1))); if (npc == 0xFF) { send_text_message(c, u"$C6No such NPC"); return; } - c->game_data.player()->disp.visual.extra_model = npc; - c->game_data.player()->disp.visual.v2_flags |= 0x02; + p->disp.visual.extra_model = npc; + p->disp.visual.v2_flags |= 0x02; } } else if (tokens.at(0) == "tech") { uint8_t level = stoul(tokens.at(2)) - 1; if (tokens.at(1) == "all") { for (size_t x = 0; x < 0x14; x++) { - c->game_data.player()->set_technique_level(x, level); + p->set_technique_level(x, level); } } else { uint8_t tech_id = technique_for_name(decode_sjis(tokens.at(1))); @@ -860,7 +861,7 @@ static void server_command_edit(shared_ptr c, const std::u16string& args return; } try { - c->game_data.player()->set_technique_level(tech_id, level); + p->set_technique_level(tech_id, level); } catch (const out_of_range&) { send_text_message(c, u"$C6Invalid technique"); return; diff --git a/src/Client.hh b/src/Client.hh index bb88d560..da775502 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -192,7 +192,8 @@ struct Client : public std::enable_shared_from_this { void reschedule_ping_and_timeout_events(); 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 { return this->channel.version; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 30907e49..3270d065 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -3906,7 +3906,7 @@ struct G_SetPlayerVisibility_6x22_6x23 { // 6x24: Teleport player -struct G_Unknown_6x24 { +struct G_TeleportPlayer_6x24 { G_ClientIDHeader header; le_uint32_t unknown_a1; le_float x; diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index 89a8f32b..6d2c38ff 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -21,7 +21,7 @@ ItemCreator::ItemCreator( uint8_t difficulty, uint8_t section_id, uint32_t random_seed, - shared_ptr restrictions) + shared_ptr restrictions) : log("[ItemCreator] "), episode(episode), mode(mode), @@ -50,7 +50,7 @@ bool ItemCreator::are_rare_drops_allowed() 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) { case Episode::EP1: if (area >= 15) { @@ -102,7 +102,7 @@ uint8_t ItemCreator::normalize_area_number(uint8_t area) const { } } 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 1: switch (this->restrictions->weapon_and_armor_mode) { - case Restrictions::WeaponAndArmorMode::ALL_ON: - case Restrictions::WeaponAndArmorMode::ONLY_PICKING: + case BattleRules::WeaponAndArmorMode::ALLOW: + case BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW: break; - case Restrictions::WeaponAndArmorMode::NO_RARE: + case BattleRules::WeaponAndArmorMode::FORBID_RARES: if (this->item_parameter_table->is_item_rare(item)) { this->log.info("Restricted: rare items not allowed"); item.clear(); } break; - case Restrictions::WeaponAndArmorMode::ALL_OFF: + case BattleRules::WeaponAndArmorMode::FORBID_ALL: this->log.info("Restricted: weapons and armors not allowed"); item.clear(); break; @@ -476,18 +476,18 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const { } break; 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"); item.clear(); } else if (item.data1[1] == 2) { switch (this->restrictions->tech_disk_mode) { - case Restrictions::TechDiskMode::ON: + case BattleRules::TechDiskMode::ALLOW: break; - case Restrictions::TechDiskMode::OFF: + case BattleRules::TechDiskMode::FORBID_ALL: this->log.info("Restricted: tech disks not allowed"); item.clear(); break; - case Restrictions::TechDiskMode::LIMIT_LEVEL: + case BattleRules::TechDiskMode::LIMIT_LEVEL: this->log.info("Restricted: tech disk level limited to %hhu", static_cast(this->restrictions->max_tech_disk_level + 1)); if (this->restrictions->max_tech_disk_level == 0) { @@ -505,7 +505,7 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const { } break; 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"); item.clear(); } diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index cfabf312..f13773cd 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -5,51 +5,12 @@ #include "CommonItemSet.hh" #include "ItemParameterTable.hh" #include "PSOEncryption.hh" +#include "PlayerSubordinates.hh" #include "RareItemSet.hh" #include "StaticGameData.hh" -struct ItemDropSub { - uint8_t override_area; -}; - class ItemCreator { 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( std::shared_ptr common_item_set, std::shared_ptr rare_item_set, @@ -63,7 +24,7 @@ public: uint8_t difficulty, uint8_t section_id, uint32_t random_seed, - std::shared_ptr restrictions = nullptr); + std::shared_ptr restrictions = nullptr); ~ItemCreator() = default; 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. ssize_t apply_tekker_deltas(ItemData& item, uint8_t section_id); + inline void set_restrictions(std::shared_ptr restrictions) { + this->restrictions = restrictions; + } + private: PrefixedLogger log; Episode episode; @@ -92,9 +57,8 @@ private: std::shared_ptr tekker_adjustment_set; std::shared_ptr item_parameter_table; const CommonItemSet::Table* pt; - std::shared_ptr restrictions; + std::shared_ptr restrictions; - std::shared_ptr item_drop_sub; parray unit_weights_table1; parray unit_weights_table2; diff --git a/src/ItemData.cc b/src/ItemData.cc index 40e5f318..5ca9c46b 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -1,5 +1,7 @@ #include "ItemData.hh" +#include + #include "StaticGameData.hh" using namespace std; diff --git a/src/LevelTable.cc b/src/LevelTable.cc index b5ca1cb4..456baf0a 100644 --- a/src/LevelTable.cc +++ b/src/LevelTable.cc @@ -28,7 +28,7 @@ const CharacterStats& LevelTable::base_stats_for_class(uint8_t char_class) const 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 { if (char_class >= 12) { throw invalid_argument("invalid character class"); diff --git a/src/LevelTable.hh b/src/LevelTable.hh index dfb3e450..af686d1a 100644 --- a/src/LevelTable.hh +++ b/src/LevelTable.hh @@ -16,6 +16,17 @@ struct CharacterStats { le_uint16_t lck = 0; } __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 public: struct LevelStats { @@ -41,9 +52,41 @@ public: LevelTable(std::shared_ptr data, bool compressed); 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: + // 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 data; const Table* table; }; diff --git a/src/Lobby.cc b/src/Lobby.cc index b79aa81b..ab9373f8 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -176,24 +176,26 @@ void Lobby::add_client(shared_ptr c, ssize_t required_client_id) { // If the lobby is a game and item tracking is enabled, assign the inventory's // item IDs 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(inv.num_items, 30); for (size_t x = 0; x < count; x++) { 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 (this->battle_record) { + auto p = c->game_data.player(); PlayerLobbyDataDCGC lobby_data; lobby_data.player_tag = 0x00010000; 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( lobby_data, - c->game_data.player()->inventory, - c->game_data.player()->disp.to_dcpcv3(), + p->inventory, + p->disp.to_dcpcv3(), 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 c) { c->lobby_client_id, static_cast(other_c ? other_c->lobby_client_id : 0xFF))); } - this->clients[c->lobby_client_id] = nullptr; // Unassign the client's lobby if it matches the current lobby (it may not diff --git a/src/Player.cc b/src/Player.cc index 8f76c25a..569c8f07 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -42,8 +42,76 @@ ClientGameData::~ClientGameData() { } } -shared_ptr ClientGameData::account(bool should_load) { - if (!this->account_data.get() && should_load) { +void ClientGameData::create_battle_overlay(shared_ptr rules, shared_ptr level_table) { + 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(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 ClientGameData::account(bool allow_load) { + if (!this->account_data.get() && allow_load) { if (this->bb_username.empty()) { this->account_data.reset(new SavedAccountDataBB()); this->account_data->signature = ACCOUNT_FILE_SIGNATURE; @@ -54,8 +122,11 @@ shared_ptr ClientGameData::account(bool should_load) { return this->account_data; } -shared_ptr ClientGameData::player(bool should_load) { - if (!this->player_data.get() && should_load) { +shared_ptr ClientGameData::player(bool allow_load, bool allow_overlay) { + if (this->overlay_player_data && allow_overlay) { + return this->overlay_player_data; + } + if (!this->player_data.get() && allow_load) { if (this->bb_username.empty()) { this->player_data.reset(new SavedPlayerDataBB()); } else { @@ -65,15 +136,18 @@ shared_ptr ClientGameData::player(bool should_load) { return this->player_data; } -shared_ptr ClientGameData::account() const { - if (!this->account_data.get()) { +shared_ptr ClientGameData::account(bool allow_load) const { + if (!this->account_data.get() && allow_load) { throw runtime_error("account data is not loaded"); } return this->account_data; } -shared_ptr ClientGameData::player() const { - if (!this->player_data.get()) { +shared_ptr ClientGameData::player(bool allow_load, bool allow_overlay) const { + 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"); } return this->player_data; @@ -371,3 +445,14 @@ void SavedPlayerDataBB::set_material_usage(MaterialType which, uint8_t usage) { 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()); + } +} diff --git a/src/Player.hh b/src/Player.hh index ddd5c165..b4d77293 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -10,6 +10,8 @@ #include #include "Episode3/DataIndexes.hh" +#include "ItemCreator.hh" +#include "LevelTable.hh" #include "PlayerSubordinates.hh" #include "Text.hh" #include "Version.hh" @@ -91,6 +93,10 @@ struct SavedAccountDataBB { // .nsa file format class ClientGameData { private: std::shared_ptr 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 overlay_player_data; std::shared_ptr player_data; uint64_t last_play_time_update; @@ -117,10 +123,15 @@ public: ClientGameData(); ~ClientGameData(); - std::shared_ptr account(bool should_load = true); - std::shared_ptr player(bool should_load = true); - std::shared_ptr account() const; - std::shared_ptr player() const; + void create_battle_overlay(std::shared_ptr rules, std::shared_ptr level_table); + inline void delete_overlay() { + this->overlay_player_data.reset(); + } + + std::shared_ptr account(bool allow_load = true); + std::shared_ptr player(bool allow_load = true, bool allow_overlay = true); + std::shared_ptr account(bool allow_load = true) const; + std::shared_ptr player(bool allow_load = true, bool allow_overlay = true) const; std::string account_data_filename() const; std::string player_data_filename() const; diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index 28aeaec6..432a502a 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -443,6 +443,25 @@ size_t PlayerInventory::find_equipped_mag() const { 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(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) { for (size_t x = 0; x < this->num_items; x++) { 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"); } -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()); +BattleRules::BattleRules(const JSON& json) { + this->tech_disk_mode = json.get_enum("tech_disk_mode", this->tech_disk_mode); + this->weapon_and_armor_mode = json.get_enum("weapon_and_armor_mode", this->weapon_and_armor_mode); + this->forbid_mags = json.get_bool("forbid_mags", this->forbid_mags); + this->tool_mode = json.get_enum("tool_mode", this->tool_mode); + this->meseta_drop_mode = json.get_enum("meseta_drop_mode", this->meseta_drop_mode); + this->forbid_scape_dolls = json.get_bool("forbid_scape_dolls", this->forbid_scape_dolls); + 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 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(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 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(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 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(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 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(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"); } } diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index 76d62e09..12f2bacf 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -73,6 +74,8 @@ struct PlayerInventory { size_t find_equipped_weapon() const; size_t find_equipped_armor() const; size_t find_equipped_mag() const; + + size_t remove_all_items_of_type(uint8_t data0, int16_t data1 = -1); } __attribute__((packed)); struct PlayerBank { @@ -94,17 +97,6 @@ struct PlayerBank { 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 { /* 00 */ ptext name; /* 10 */ parray unknown_a2; @@ -448,3 +440,46 @@ inline PlayerDispDataBB convert_player_disp_data( const PlayerDispDataBB& 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; +}; diff --git a/src/Quest.cc b/src/Quest.cc index 686bc73b..ca474319 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -222,7 +222,8 @@ VersionedQuest::VersionedQuest( QuestScriptVersion version, uint8_t language, std::shared_ptr bin_contents, - std::shared_ptr dat_contents) + std::shared_ptr dat_contents, + std::shared_ptr battle_rules) : quest_number(quest_number), category_id(category_id), episode(Episode::NONE), @@ -231,7 +232,8 @@ VersionedQuest::VersionedQuest( language(language), is_dlq_encoded(false), bin_contents(bin_contents), - dat_contents(dat_contents) { + dat_contents(dat_contents), + battle_rules(battle_rules) { auto bin_decompressed = prs_decompress(*this->bin_contents); @@ -375,7 +377,8 @@ Quest::Quest(shared_ptr initial_version) category_id(initial_version->category_id), episode(initial_version->episode), 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); } @@ -396,6 +399,12 @@ void Quest::add_version(shared_ptr vq) { if (this->joinable != vq->joinable) { 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); } @@ -430,6 +439,7 @@ QuestIndex::QuestIndex( category_index(category_index) { unordered_map> dat_cache; + unordered_map> metadata_json_cache; for (const auto& bin_filename : list_directory_sorted(directory)) { string bin_path = this->directory + "/" + bin_filename; @@ -467,6 +477,9 @@ QuestIndex::QuestIndex( if (basename.empty()) { 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: // K = class (quest, battle, challenge, etc.) @@ -549,6 +562,7 @@ QuestIndex::QuestIndex( if (basename.size() < 2) { throw logic_error("basename too short for language trim"); } + // Look for dat file with the same basename as the bin file; if not // found, look for a dat file without the language suffix 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 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 battle_rules; + if (metadata_json) { + try { + battle_rules.reset(new BattleRules(metadata_json->at("battle_rules"))); + } catch (const out_of_range&) { + } + } + shared_ptr 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)); auto category_name = encode_sjis(this->category_index->at(vq->category_id).name); 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); if (q_it != this->quests_by_number.end()) { 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(), name_for_enum(vq->version), char_for_language_code(vq->language), vq->quest_number, ascii_name.c_str(), - dat_str.c_str()); + dat_str.c_str(), + metadata_json_str.c_str()); } else { 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(), name_for_enum(vq->version), char_for_language_code(vq->language), @@ -640,7 +692,8 @@ QuestIndex::QuestIndex( category_name.c_str(), vq->category_id, vq->joinable ? "joinable" : "not joinable", - dat_str.c_str()); + dat_str.c_str(), + metadata_json_str.c_str()); } } catch (const exception& e) { static_game_data_log.warning("(%s) Failed to index quest file: (%s)", bin_filename.c_str(), e.what()); diff --git a/src/Quest.hh b/src/Quest.hh index b9ca60a9..72503f49 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -7,6 +7,7 @@ #include #include +#include "PlayerSubordinates.hh" #include "QuestScript.hh" #include "StaticGameData.hh" @@ -65,6 +66,7 @@ struct VersionedQuest { std::u16string long_description; std::shared_ptr bin_contents; std::shared_ptr dat_contents; + std::shared_ptr battle_rules; VersionedQuest( uint32_t quest_number, @@ -72,7 +74,8 @@ struct VersionedQuest { QuestScriptVersion version, uint8_t language, std::shared_ptr bin_contents, - std::shared_ptr dat_contents); + std::shared_ptr dat_contents, + std::shared_ptr battle_rules = nullptr); std::string bin_filename() const; std::string dat_filename() const; @@ -101,6 +104,7 @@ public: Episode episode; bool joinable; std::u16string name; + std::shared_ptr battle_rules; std::map> versions; }; diff --git a/src/QuestScript.cc b/src/QuestScript.cc index 15a6b821..f28d595d 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -513,19 +513,19 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF80F, "enable_weapon_drop", {CLIENT_ID}, F_V2_V4 | F_ARGS}, {0xF810, "ba_initial_floor", {AREA}, F_V2_V4 | F_ARGS}, {0xF811, "set_ba_rules", {}, F_V2_V4}, - {0xF812, "ba_set_tech", {INT32}, F_V2_V4 | F_ARGS}, - {0xF813, "ba_set_equip", {INT32}, F_V2_V4 | F_ARGS}, - {0xF814, "ba_set_mag", {INT32}, F_V2_V4 | F_ARGS}, - {0xF815, "ba_set_item", {INT32}, F_V2_V4 | F_ARGS}, - {0xF816, "ba_set_trapmenu", {INT32}, F_V2_V4 | F_ARGS}, + {0xF812, "ba_set_tech_disk_mode", {INT32}, F_V2_V4 | F_ARGS}, + {0xF813, "ba_set_weapon_and_armor_mode", {INT32}, F_V2_V4 | F_ARGS}, + {0xF814, "ba_set_forbid_mags", {INT32}, F_V2_V4 | F_ARGS}, + {0xF815, "ba_set_tool_mode", {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 {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}, {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}, - {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}, {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}, @@ -590,13 +590,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF868, "set_cmode_rank", {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] - {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}, {0xF86D, "ba_set_trapself", {}, F_V2_V4}, {0xF86E, "ba_clear_trapself", {}, F_V2_V4}, {0xF86F, "ba_set_lives", {INT32}, F_V2_V4 | F_ARGS}, - {0xF870, "ba_set_tech_lvl", {INT32}, F_V2_V4 | F_ARGS}, - {0xF871, "ba_set_lvl", {INT32}, F_V2_V4 | F_ARGS}, + {0xF870, "ba_set_max_tech_level", {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}, {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 diff --git a/src/RareItemSet.cc b/src/RareItemSet.cc index 5b2057ac..dda1265c 100644 --- a/src/RareItemSet.cc +++ b/src/RareItemSet.cc @@ -4,6 +4,7 @@ #include #include "BattleParamsIndex.hh" +#include "ItemData.hh" #include "StaticGameData.hh" using namespace std; diff --git a/src/RareItemSet.hh b/src/RareItemSet.hh index 45b82043..ece18fba 100644 --- a/src/RareItemSet.hh +++ b/src/RareItemSet.hh @@ -3,12 +3,14 @@ #include #include +#include #include #include #include "AFSArchive.hh" #include "GSLArchive.hh" #include "StaticGameData.hh" +#include "Text.hh" class RareItemSet { public: diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 80c09180..0c591f86 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1317,14 +1317,15 @@ static void on_CA_Ep3(shared_ptr c, uint16_t, uint32_t, const string& da l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags)); for (auto existing_c : l->clients) { if (existing_c) { + auto existing_p = existing_c->game_data.player(); 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.guild_card = existing_c->license->serial_number; l->battle_record->add_player( lobby_data, - existing_c->game_data.player()->inventory, - existing_c->game_data.player()->disp.to_dcpcv3(), + existing_p->inventory, + existing_p->disp.to_dcpcv3(), 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 c, uint16_t, uint32_t, const string& data) send_lobby_message_box(c, u"$C6Incorrect password."); 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."); 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."); break; } @@ -2019,6 +2021,11 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, const string& data) } l->quest = q; 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++) { auto lc = l->clients[x]; if (!lc) { @@ -2031,6 +2038,13 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, const string& data) lc->should_disconnect = true; 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 dat_filename = vq->dat_filename(); 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 c, uint16_t command, uint32_t flag } static void on_61_98(shared_ptr 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()) { case GameVersion::DC: { if (c->flags & Client::Flag::IS_DC_V1) { const auto& pd = check_size_t(data); - auto player = c->game_data.player(); player->inventory = pd.inventory; player->disp = pd.disp.to_bb(); } else { const auto& pd = check_size_t(data, 0xFFFF); - auto player = c->game_data.player(); player->inventory = pd.inventory; player->disp = pd.disp.to_bb(); player->battle_records = pd.records.battle; @@ -2514,8 +2530,6 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, cons } case GameVersion::PC: { const auto& pd = check_size_t(data, 0xFFFF); - auto player = c->game_data.player(); - auto account = c->game_data.account(); player->inventory = pd.inventory; player->disp = pd.disp.to_bb(); player->battle_records = pd.records.battle; @@ -2574,8 +2588,6 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, cons } } - auto account = c->game_data.account(); - auto player = c->game_data.player(); player->inventory = cmd->inventory; if (c->version() == GameVersion::GC) { for (size_t z = 0; z < 30; z++) { @@ -2597,8 +2609,6 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, cons } case GameVersion::BB: { const auto& cmd = check_size_t(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 // them (we sent the player data to the client in the first place) player->battle_records = cmd.records.battle; @@ -2617,17 +2627,15 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, cons throw logic_error("player data command not implemented for version"); } - auto s = c->require_server_state(); - auto player = c->game_data.player(false); - 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()); - } + 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 // in no lobby (they will send an 84 soon afterward to choose a lobby). 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); } else if (command == 0x61) { @@ -2711,7 +2719,8 @@ static void on_chat_generic(shared_ptr c, const u16string& text) { 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++) { if (l->clients[x]) { send_chat_message(l->clients[x], c->license->serial_number, @@ -2729,7 +2738,7 @@ static void on_chat_generic(shared_ptr c, const u16string& text) { if (l->battle_record && l->battle_record->battle_in_progress()) { 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); string prepared_message_sjis = encode_sjis(prepared_message); 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 c, uint16_t, uint32_t, const string& d // should instead verify our copy of the player against what the client sent, // and alert on anything that's out of sync. // TODO: In the future, we should save battle records here too. - c->game_data.player()->challenge_records = cmd.challenge_records; - c->game_data.player()->quest_data1 = cmd.quest_data1; - c->game_data.player()->quest_data2 = cmd.quest_data2; + auto p = c->game_data.player(); + p->challenge_records = cmd.challenge_records; + p->quest_data1 = cmd.quest_data1; + p->quest_data2 = cmd.quest_data2; } static void on_00E2_BB(shared_ptr c, uint16_t, uint32_t, const string& data) { @@ -3139,10 +3149,9 @@ static void on_81(shared_ptr c, uint16_t, uint32_t, const string& data) // If the target has auto-reply enabled, send the autoreply. Note that we also // forward the message in this case. - if (!target->game_data.player()->auto_reply.empty()) { - send_simple_mail(c, target->license->serial_number, - target->game_data.player()->disp.name, - target->game_data.player()->auto_reply); + auto target_p = target->game_data.player(); + if (!target_p->auto_reply.empty()) { + send_simple_mail(c, target->license->serial_number, target_p->disp.name, target_p->auto_reply); } // Forward the message @@ -3161,10 +3170,9 @@ static void on_D8(shared_ptr c, uint16_t, uint32_t, const string& data) template void on_D9_t(shared_ptr c, const string& data) { - check_size_v(data.size(), 0, c->game_data.player()->info_board.size() * sizeof(CharT)); - c->game_data.player()->info_board.assign( - reinterpret_cast(data.data()), - data.size() / sizeof(CharT)); + auto p = c->game_data.player(true, false); + check_size_v(data.size(), 0, p->info_board.size() * sizeof(CharT)); + p->info_board.assign(reinterpret_cast(data.data()), data.size() / sizeof(CharT)); } void on_D9_a(shared_ptr c, uint16_t, uint32_t, const string& data) { @@ -3176,8 +3184,9 @@ void on_D9_w(shared_ptr c, uint16_t, uint32_t, const string& data) { template void on_C7_t(shared_ptr c, uint16_t, uint32_t, const string& data) { - check_size_v(data.size(), 0, c->game_data.player()->auto_reply.size() * sizeof(CharT)); - c->game_data.player()->auto_reply.assign( + auto p = c->game_data.player(true, false); + check_size_v(data.size(), 0, p->auto_reply.size() * sizeof(CharT)); + p->auto_reply.assign( reinterpret_cast(data.data()), data.size() / sizeof(CharT)); } @@ -3191,7 +3200,7 @@ void on_C7_w(shared_ptr c, uint16_t cmd, uint32_t flag, const string& da static void on_C8(shared_ptr c, uint16_t, uint32_t, const string& data) { 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 c, uint16_t, uint32_t, const string& data) { @@ -3256,8 +3265,9 @@ shared_ptr create_game_generic( throw runtime_error("invalid episode"); } + auto p = c->game_data.player(); 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 // actually encounter while playing the game normally send_lobby_message_box(c, u"Your level is too\nlow for this\ndifficulty"); @@ -3337,7 +3347,7 @@ shared_ptr create_game_generic( game->section_id = c->options.override_section_id >= 0 ? c->options.override_section_id - : c->game_data.player()->disp.visual.section_id; + : p->disp.visual.section_id; game->episode = episode; game->mode = mode; game->difficulty = difficulty; @@ -4351,12 +4361,6 @@ void on_command( uint16_t command, uint32_t flag, 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(); // Most of the command handlers assume the client is registered, logged in, diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index e878e175..a1821e56 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -402,18 +402,18 @@ static void on_send_guild_card(shared_ptr c, uint8_t command, uint8_t fl switch (c->version()) { case GameVersion::DC: { const auto& cmd = check_size_t(data, size); - c->game_data.player()->guild_card_description = cmd.description; + c->game_data.player(true, false)->guild_card_description = cmd.description; break; } case GameVersion::PC: { const auto& cmd = check_size_t(data, size); - c->game_data.player()->guild_card_description = cmd.description; + c->game_data.player(true, false)->guild_card_description = cmd.description; break; } case GameVersion::GC: case GameVersion::XB: { const auto& cmd = check_size_t(data, size); - c->game_data.player()->guild_card_description = cmd.description; + c->game_data.player(true, false)->guild_card_description = cmd.description; break; } case GameVersion::BB: @@ -611,7 +611,8 @@ static void on_player_drop_item(shared_ptr c, uint8_t command, uint8_t f auto l = c->require_lobby(); 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); auto name = item.name(false); @@ -623,7 +624,7 @@ static void on_player_drop_item(shared_ptr c, uint8_t command, uint8_t f send_text_message_printf(c, "$C5DROP %08" PRIX32 "\n%s", 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); @@ -665,12 +666,13 @@ static void on_create_inventory_item_t(shared_ptr c, uint8_t command, ui auto l = c->require_lobby(); if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { + auto p = c->game_data.player(); { ItemData item = cmd.item_data; if (c->version() == GameVersion::GC) { item.bswap_data2_if_mag(); } - c->game_data.player()->add_item(item); + p->add_item(item); } auto name = cmd.item_data.name(false); @@ -680,7 +682,7 @@ static void on_create_inventory_item_t(shared_ptr c, uint8_t command, ui 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()); } - c->game_data.player()->print_inventory(stderr); + p->print_inventory(stderr); } forward_subcommand_with_mag_bswap_t(c, command, flag, cmd); @@ -757,8 +759,8 @@ static void on_drop_partial_stack_bb(shared_ptr c, uint8_t command, uint throw logic_error("item tracking not enabled in BB game"); } - auto item = c->game_data.player()->remove_item( - cmd.item_id, cmd.amount, c->version() != GameVersion::BB); + auto p = c->game_data.player(); + 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 // 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 c, uint8_t command, uint // 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 // 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); @@ -782,7 +784,7 @@ static void on_drop_partial_stack_bb(shared_ptr c, uint8_t command, uint send_text_message_printf(c, "$C5SPLIT/BB %08" PRIX32 "\n%s", 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); @@ -889,8 +891,9 @@ static void on_pick_up_item(shared_ptr c, uint8_t command, uint8_t flag, } if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { + auto effective_p = effective_c->game_data.player(); 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); l->log.info("Player %hu picked up %08" PRIX32 " (%s)", @@ -899,7 +902,7 @@ static void on_pick_up_item(shared_ptr c, uint8_t command, uint8_t flag, string name = item.name(true); 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); @@ -919,8 +922,9 @@ static void on_pick_up_item_request(shared_ptr c, uint8_t command, uint8 throw logic_error("item tracking not enabled in BB game"); } + auto p = c->game_data.player(); auto item = l->remove_item(cmd.item_id); - c->game_data.player()->add_item(item); + p->add_item(item); auto name = item.name(false); l->log.info("Player %hu picked up %08" PRIX32 " (%s)", @@ -930,7 +934,7 @@ static void on_pick_up_item_request(shared_ptr c, uint8_t command, uint8 send_text_message_printf(c, "$C5PICK/BB %08" PRIX32 "\n%s", 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); @@ -948,11 +952,12 @@ static void on_equip_unequip_item(shared_ptr c, uint8_t command, uint8_t auto l = c->require_lobby(); 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 - c->game_data.player()->inventory.items[index].flags |= 0x00000008; + p->inventory.items[index].flags |= 0x00000008; } else { // Unequip - c->game_data.player()->inventory.items[index].flags &= 0xFFFFFFF7; + p->inventory.items[index].flags &= 0xFFFFFFF7; } } else if (l->base_version == GameVersion::BB) { throw logic_error("item tracking not enabled in BB game"); @@ -974,12 +979,13 @@ static void on_use_item( auto l = c->require_lobby(); 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; { // Note: We do this weird scoping thing because player_use_item will // 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); colored_name = item.name(true); } @@ -991,7 +997,7 @@ static void on_use_item( send_text_message_printf(c, "$C5USE %08" PRIX32 "\n%s", 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); @@ -1010,16 +1016,18 @@ static void on_feed_mag( auto l = c->require_lobby(); if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { - size_t mag_index = c->game_data.player()->inventory.find_item(cmd.mag_item_id); - size_t fed_index = c->game_data.player()->inventory.find_item(cmd.fed_item_id); + auto p = c->game_data.player(); + + 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; { // Note: We do this weird scoping thing because player_use_item will // 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_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_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 // 6x29 command to do that. 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)", @@ -1041,7 +1049,7 @@ static void on_feed_mag( cmd.fed_item_id.load(), fed_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); @@ -1109,35 +1117,36 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint throw logic_error("item tracking not enabled in BB game"); } + auto p = c->game_data.player(); if (cmd.action == 0) { // deposit 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; } - if ((c->game_data.player()->bank.meseta + cmd.meseta_amount) > 999999) { + if ((p->bank.meseta + cmd.meseta_amount) > 999999) { return; } - c->game_data.player()->bank.meseta += cmd.meseta_amount; - c->game_data.player()->disp.stats.meseta -= cmd.meseta_amount; + p->bank.meseta += cmd.meseta_amount; + p->disp.stats.meseta -= cmd.meseta_amount; } else { // item - auto item = c->game_data.player()->remove_item(cmd.item_id, cmd.item_amount, c->version() != GameVersion::BB); - c->game_data.player()->bank.add_item(item); + auto item = p->remove_item(cmd.item_id, cmd.item_amount, c->version() != GameVersion::BB); + p->bank.add_item(item); send_destroy_item(c, cmd.item_id, cmd.item_amount); } } else if (cmd.action == 1) { // take if (cmd.item_id == 0xFFFFFFFF) { // meseta - if (cmd.meseta_amount > c->game_data.player()->bank.meseta) { + if (cmd.meseta_amount > p->bank.meseta) { return; } - if ((c->game_data.player()->disp.stats.meseta + cmd.meseta_amount) > 999999) { + if ((p->disp.stats.meseta + cmd.meseta_amount) > 999999) { return; } - c->game_data.player()->bank.meseta -= cmd.meseta_amount; - c->game_data.player()->disp.stats.meseta += cmd.meseta_amount; + p->bank.meseta -= cmd.meseta_amount; + p->disp.stats.meseta += cmd.meseta_amount; } 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); - c->game_data.player()->add_item(item); + p->add_item(item); send_create_inventory_item(c, item); } } @@ -1156,23 +1165,43 @@ static void on_sort_inventory_bb(shared_ptr c, uint8_t, uint8_t, const v 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 sorted_item_ids; + size_t expected_count = 0; for (size_t x = 0; x < 30; x++) { - if (cmd.item_ids[x] == 0xFFFFFFFF) { - sorted.items[x].data.id = 0xFFFFFFFF; - } else { - size_t index = inv.find_item(cmd.item_ids[x]); - sorted.items[x] = inv.items[index]; + if (cmd.item_ids[x] != 0xFFFFFFFF) { + sorted_item_ids.emplace(cmd.item_ids[x]); + expected_count++; } } + 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; - sorted.hp_materials_used = inv.hp_materials_used; - sorted.tp_materials_used = inv.tp_materials_used; - sorted.language = inv.language; - c->game_data.player()->inventory = sorted; + parray sorted; + for (size_t x = 0; x < 30; x++) { + if (cmd.item_ids[x] == 0xFFFFFFFF) { + sorted[x].data.id = 0xFFFFFFFF; + } 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 c, uint8_t command, uint8_t fla if (flag_index >= 0x400) { 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 // operation is performed. size_t bit_index = (difficulty << 10) + flag_index; size_t byte_index = bit_index >> 3; uint8_t mask = 0x80 >> (bit_index & 7); if (action == 0) { - c->game_data.player()->quest_data1[byte_index] |= mask; + p->quest_data1[byte_index] |= mask; } 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); @@ -1354,22 +1387,23 @@ static void on_charge_attack_bb(shared_ptr c, uint8_t command, uint8_t f static void add_player_exp(shared_ptr c, uint32_t exp) { 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); bool leveled_up = false; do { - const auto& level = s->level_table->stats_for_level( - c->game_data.player()->disp.visual.char_class, c->game_data.player()->disp.stats.level + 1); - if (c->game_data.player()->disp.stats.experience >= level.experience) { + const auto& level = s->level_table->stats_delta_for_level( + p->disp.visual.char_class, p->disp.stats.level + 1); + if (p->disp.stats.experience >= level.experience) { leveled_up = true; - level.apply(c->game_data.player()->disp.stats.char_stats); - c->game_data.player()->disp.stats.level++; + level.apply(p->disp.stats.char_stats); + p->disp.stats.level++; } else { break; } - } while (c->game_data.player()->disp.stats.level < 199); + } while (p->disp.stats.level < 199); if (leveled_up) { send_level_up(c); } @@ -1388,8 +1422,9 @@ static void on_steal_exp_bb(shared_ptr c, uint8_t, uint8_t, const void* const auto& cmd = check_size_t(data, size); + auto p = c->game_data.player(); 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()]; uint8_t special = 0; @@ -1402,7 +1437,7 @@ static void on_steal_exp_bb(shared_ptr c, uint8_t, uint8_t, const void* if (special >= 0x09 && special <= 0x0B) { // 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 stolen_exp = min((enemy_exp * percent) / 100, 80); if (c->options.debug) { @@ -1511,7 +1546,7 @@ void on_meseta_reward_request_bb(shared_ptr c, uint8_t, uint8_t, const v item.data1[0] = 0x04; item.data2d = cmd.amount.load(); item.id = l->generate_item_id(0xFF); - c->game_data.player()->add_item(item); + p->add_item(item); send_create_inventory_item(c, item); } } @@ -1539,8 +1574,8 @@ static void on_destroy_inventory_item(shared_ptr c, uint8_t command, uin } if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { - auto item = c->game_data.player()->remove_item( - cmd.item_id, cmd.amount, c->version() != GameVersion::BB); + auto p = c->game_data.player(); + auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB); auto name = item.name(false); l->log.info("Inventory item %hu:%08" PRIX32 " destroyed (%s)", cmd.header.client_id.load(), cmd.item_id.load(), name.c_str()); @@ -1549,7 +1584,7 @@ static void on_destroy_inventory_item(shared_ptr c, uint8_t command, uin send_text_message_printf(c, "$C5DESTROY %08" PRIX32 "\n%s", 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); } } @@ -1590,14 +1625,14 @@ static void on_identify_item_bb(shared_ptr c, uint8_t command, uint8_t f throw logic_error("received item identify subcommand without item creator present"); } - size_t x = c->game_data.player()->inventory.find_item(cmd.item_id); - if (c->game_data.player()->inventory.items[x].data.data1[0] != 0) { + auto p = c->game_data.player(); + 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 } - auto p = c->game_data.player(); 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; l->item_creator->apply_tekker_deltas(c->game_data.identify_result, p->disp.visual.section_id); send_item_identify_result(c); @@ -1649,7 +1684,7 @@ static void on_sell_item_at_shop_bb(shared_ptr c, uint8_t command, uint8 auto name = item.name(false); 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->game_data.player()->print_inventory(stderr); + p->print_inventory(stderr); if (c->options.debug) { string name = item.name(true); send_text_message_printf(c, "$C5DESTROY/SELL %08" PRIX32 "\n+%zu Meseta\n%s", diff --git a/src/SendCommands.cc b/src/SendCommands.cc index f4651e2e..2b51b3da 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -613,7 +613,7 @@ void send_approve_player_choice_bb(shared_ptr c) { void send_complete_player_bb(shared_ptr c) { 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; cmd.inventory = player->inventory; @@ -922,13 +922,14 @@ template void send_info_board_t(shared_ptr c) { vector> entries; auto l = c->require_lobby(); - for (const auto& c : l->clients) { - if (!c.get()) { + for (const auto& other_c : l->clients) { + if (!other_c.get()) { continue; } + auto other_p = other_c->game_data.player(true, false); auto& e = entries.emplace_back(); - e.name = c->game_data.player()->disp.name; - e.message = c->game_data.player()->info_board; + e.name = other_p->disp.name; + e.message = other_p->info_board; add_color_inplace(e.message); } send_command_vt(c, 0xD8, entries.size(), entries); @@ -979,7 +980,7 @@ void send_card_search_result_t( cmd.location_string = location_string; cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY; 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); } @@ -1079,11 +1080,12 @@ void send_guild_card(shared_ptr c, shared_ptr source) { 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; - u16string name = source->game_data.player()->disp.name; - u16string description = source->game_data.player()->guild_card_description; - uint8_t section_id = source->game_data.player()->disp.visual.section_id; - uint8_t char_class = source->game_data.player()->disp.visual.char_class; + u16string name = source_p->disp.name; + u16string description = source_p->guild_card_description; + uint8_t section_id = source_p->disp.visual.section_id; + uint8_t char_class = source_p->disp.visual.char_class; send_guild_card( c->channel, guild_card_number, name, u"", description, section_id, char_class); @@ -1388,7 +1390,7 @@ template void send_player_records_t(shared_ptr c, shared_ptr l, shared_ptr joining_client) { vector entries; auto add_client = [&](shared_ptr lc) -> void { - auto lp = lc->game_data.player(); + auto lp = lc->game_data.player(true, false); auto& e = entries.emplace_back(); e.client_id = lc->lobby_client_id; e.challenge = lp->challenge_records; @@ -1512,27 +1514,28 @@ static void send_join_spectator_team(shared_ptr c, shared_ptr l) for (size_t z = 4; z < 12; z++) { if (l->clients[z]) { - auto& gd = l->clients[z]->game_data; - auto& p = cmd.spectator_players[z - 4]; - auto& e = cmd.entries[z]; - p.lobby_data.player_tag = 0x00010000; - p.lobby_data.guild_card = l->clients[z]->license->serial_number; - p.lobby_data.client_id = l->clients[z]->lobby_client_id; - p.lobby_data.name = gd.player()->disp.name; - remove_language_marker_inplace(p.lobby_data.name); - p.inventory = gd.player()->inventory; - p.disp = gd.player()->disp.to_dcpcv3(); - remove_language_marker_inplace(p.disp.visual.name); + auto other_c = l->clients[z]; + auto other_p = other_c->game_data.player(); + auto& cmd_p = cmd.spectator_players[z - 4]; + auto& cmd_e = cmd.entries[z]; + cmd_p.lobby_data.player_tag = 0x00010000; + cmd_p.lobby_data.guild_card = other_c->license->serial_number; + cmd_p.lobby_data.client_id = other_c->lobby_client_id; + cmd_p.lobby_data.name = other_p->disp.name; + remove_language_marker_inplace(cmd_p.lobby_data.name); + cmd_p.inventory = other_p->inventory; + cmd_p.disp = other_p->disp.to_dcpcv3(); + remove_language_marker_inplace(cmd_p.disp.visual.name); - e.player_tag = 0x00010000; - e.guild_card_number = l->clients[z]->license->serial_number; - e.name = gd.player()->disp.name; - remove_language_marker_inplace(e.name); - e.present = 1; - e.level = gd.ep3_config - ? (gd.ep3_config->online_clv_exp / 100) - : gd.player()->disp.stats.level.load(); - e.name_color = gd.player()->disp.visual.name_color; + cmd_e.player_tag = 0x00010000; + cmd_e.guild_card_number = other_c->license->serial_number; + cmd_e.name = other_p->disp.name; + remove_language_marker_inplace(cmd_e.name); + cmd_e.present = 1; + cmd_e.level = other_c->game_data.ep3_config + ? (other_c->game_data.ep3_config->online_clv_exp / 100) + : other_p->disp.stats.level.load(); + cmd_e.name_color = other_p->disp.visual.name_color; player_count++; } @@ -1625,12 +1628,12 @@ void send_join_game(shared_ptr c, shared_ptr l) { size_t player_count = populate_v3_cmd(cmd); for (size_t x = 0; x < 4; 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++) { cmd.players_ep3[x].inventory.items[z].data.bswap_data2_if_mag(); } - cmd.players_ep3[x].disp = convert_player_disp_data( - l->clients[x]->game_data.player()->disp); + cmd.players_ep3[x].disp = convert_player_disp_data(other_p->disp); } } send_command_t(c, 0x64, player_count, cmd); @@ -1732,22 +1735,23 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, size_t used_entries = 0; for (const auto& lc : lobby_clients) { + auto lp = lc->game_data.player(); auto& e = cmd.entries[used_entries++]; e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card = lc->license->serial_number; 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); if (UseLanguageMarkerInName) { 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) { for (size_t z = 0; z < 30; z++) { e.inventory.items[z].data.bswap_data2_if_mag(); } } - e.disp = convert_player_disp_data(lc->game_data.player()->disp); + e.disp = convert_player_disp_data(lp->disp); e.disp.enforce_lobby_join_limits(c->version()); } @@ -1785,13 +1789,14 @@ void send_join_lobby_dc_nte(shared_ptr c, shared_ptr l, size_t used_entries = 0; for (const auto& lc : lobby_clients) { + auto lp = lc->game_data.player(); auto& e = cmd.entries[used_entries++]; e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card = lc->license->serial_number; e.lobby_data.client_id = lc->lobby_client_id; - e.lobby_data.name = lc->game_data.player()->disp.name; - e.inventory = lc->game_data.player()->inventory; - e.disp = convert_player_disp_data(lc->game_data.player()->disp); + e.lobby_data.name = lp->disp.name; + e.inventory = lp->inventory; + e.disp = convert_player_disp_data(lp->disp); e.disp.enforce_lobby_join_limits(c->version()); } @@ -2112,14 +2117,15 @@ void send_bank(shared_ptr c) { throw logic_error("6xBC can only be sent to BB clients"); } - const auto* items_it = c->game_data.player()->bank.items.data(); - vector items(items_it, items_it + c->game_data.player()->bank.num_items); + auto p = c->game_data.player(); + const auto* items_it = p->bank.items.data(); + vector items(items_it, items_it + p->bank.num_items); G_BankContentsHeader_BB_6xBC cmd = { {{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + items.size() * sizeof(PlayerBankItem)}, random_object(), - c->game_data.player()->bank.num_items, - c->game_data.player()->bank.meseta}; + p->bank.num_items, + p->bank.meseta}; send_command_t_vt(c, 0x6C, 0x00, cmd, items); } @@ -2148,15 +2154,16 @@ void send_shop(shared_ptr c, uint8_t shop_type) { // notifies players about a level up void send_level_up(shared_ptr c) { 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++) { - if ((c->game_data.player()->inventory.items[x].flags & 0x08) && - (c->game_data.player()->inventory.items[x].data.data1[0] == 0x02)) { - stats.dfp += (c->game_data.player()->inventory.items[x].data.data1w[2] / 100); - stats.atp += (c->game_data.player()->inventory.items[x].data.data1w[3] / 50); - stats.ata += (c->game_data.player()->inventory.items[x].data.data1w[4] / 200); - stats.mst += (c->game_data.player()->inventory.items[x].data.data1w[5] / 50); + for (size_t x = 0; x < p->inventory.num_items; x++) { + if ((p->inventory.items[x].flags & 0x08) && + (p->inventory.items[x].data.data1[0] == 0x02)) { + stats.dfp += (p->inventory.items[x].data.data1w[2] / 100); + stats.atp += (p->inventory.items[x].data.data1w[3] / 50); + stats.ata += (p->inventory.items[x].data.data1w[4] / 200); + stats.mst += (p->inventory.items[x].data.data1w[5] / 50); } } @@ -2168,7 +2175,7 @@ void send_level_up(shared_ptr c) { stats.hp, stats.dfp, stats.ata, - c->game_data.player()->disp.stats.level.load(), + p->disp.stats.level.load(), 0}; send_command_t(l, 0x60, 0x00, cmd); } diff --git a/src/Server.cc b/src/Server.cc index 54be9350..9277ec08 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -315,7 +315,7 @@ vector> Server::get_clients_by_identifier(const string& ident continue; } - auto p = c->game_data.player(false); + auto p = c->game_data.player(false, false); if (p && p->disp.name == u16name) { results.emplace_back(std::move(c)); continue; diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index f3d37403..76d817c1 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -2,6 +2,8 @@ #include +#include "Text.hh" + using namespace std; bool episode_has_arpg_semantics(Episode ep) { diff --git a/src/StaticGameData.hh b/src/StaticGameData.hh index 92c579d5..e3de82cd 100644 --- a/src/StaticGameData.hh +++ b/src/StaticGameData.hh @@ -7,7 +7,6 @@ #include #include "FileContentsCache.hh" -#include "Player.hh" enum class Episode { NONE = 0,