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, ' ');
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<Client> 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;
+2 -1
View File
@@ -192,7 +192,8 @@ struct Client : public std::enable_shared_from_this<Client> {
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;
+1 -1
View File
@@ -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;
+12 -12
View File
@@ -21,7 +21,7 @@ ItemCreator::ItemCreator(
uint8_t difficulty,
uint8_t section_id,
uint32_t random_seed,
shared_ptr<const Restrictions> restrictions)
shared_ptr<const BattleRules> 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<uint8_t>(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();
}
+7 -43
View File
@@ -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<const CommonItemSet> common_item_set,
std::shared_ptr<const RareItemSet> rare_item_set,
@@ -63,7 +24,7 @@ public:
uint8_t difficulty,
uint8_t section_id,
uint32_t random_seed,
std::shared_ptr<const Restrictions> restrictions = nullptr);
std::shared_ptr<const BattleRules> 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<const BattleRules> restrictions) {
this->restrictions = restrictions;
}
private:
PrefixedLogger log;
Episode episode;
@@ -92,9 +57,8 @@ private:
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
std::shared_ptr<const ItemParameterTable> item_parameter_table;
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<int8_t, 0x0D> unit_weights_table2;
+2
View File
@@ -1,5 +1,7 @@
#include "ItemData.hh"
#include <map>
#include "StaticGameData.hh"
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];
}
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");
+44 -1
View File
@@ -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<const std::string> 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<const std::string> data;
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
// 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<uint8_t>(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<Client> c) {
c->lobby_client_id,
static_cast<uint8_t>(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
+93 -8
View File
@@ -42,8 +42,76 @@ ClientGameData::~ClientGameData() {
}
}
shared_ptr<SavedAccountDataBB> ClientGameData::account(bool should_load) {
if (!this->account_data.get() && should_load) {
void ClientGameData::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_ptr<const LevelTable> 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<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()) {
this->account_data.reset(new SavedAccountDataBB());
this->account_data->signature = ACCOUNT_FILE_SIGNATURE;
@@ -54,8 +122,11 @@ shared_ptr<SavedAccountDataBB> ClientGameData::account(bool should_load) {
return this->account_data;
}
shared_ptr<SavedPlayerDataBB> ClientGameData::player(bool should_load) {
if (!this->player_data.get() && should_load) {
shared_ptr<SavedPlayerDataBB> 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<SavedPlayerDataBB> ClientGameData::player(bool should_load) {
return this->player_data;
}
shared_ptr<const SavedAccountDataBB> ClientGameData::account() const {
if (!this->account_data.get()) {
shared_ptr<const SavedAccountDataBB> 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<const SavedPlayerDataBB> ClientGameData::player() const {
if (!this->player_data.get()) {
shared_ptr<const SavedPlayerDataBB> 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());
}
}
+15 -4
View File
@@ -10,6 +10,8 @@
#include <vector>
#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<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;
uint64_t last_play_time_update;
@@ -117,10 +123,15 @@ public:
ClientGameData();
~ClientGameData();
std::shared_ptr<SavedAccountDataBB> account(bool should_load = true);
std::shared_ptr<SavedPlayerDataBB> player(bool should_load = true);
std::shared_ptr<const SavedAccountDataBB> account() const;
std::shared_ptr<const SavedPlayerDataBB> player() const;
void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
inline void delete_overlay() {
this->overlay_player_data.reset();
}
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 player_data_filename() const;
+152 -8
View File
@@ -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<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) {
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>(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 <phosg/Encoding.hh>
#include <phosg/JSON.hh>
#include <string>
#include <utility>
#include <vector>
@@ -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<char, 0x10> name;
/* 10 */ parray<uint8_t, 8> unknown_a2;
@@ -448,3 +440,46 @@ inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
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;
};
+61 -8
View File
@@ -222,7 +222,8 @@ VersionedQuest::VersionedQuest(
QuestScriptVersion version,
uint8_t language,
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),
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<const VersionedQuest> 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<const VersionedQuest> 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<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)) {
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<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(
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());
+5 -1
View File
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
#include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
@@ -65,6 +66,7 @@ struct VersionedQuest {
std::u16string long_description;
std::shared_ptr<const std::string> bin_contents;
std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const BattleRules> battle_rules;
VersionedQuest(
uint32_t quest_number,
@@ -72,7 +74,8 @@ struct VersionedQuest {
QuestScriptVersion version,
uint8_t language,
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 dat_filename() const;
@@ -101,6 +104,7 @@ public:
Episode episode;
bool joinable;
std::u16string name;
std::shared_ptr<const BattleRules> battle_rules;
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},
{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
+1
View File
@@ -4,6 +4,7 @@
#include <phosg/Random.hh>
#include "BattleParamsIndex.hh"
#include "ItemData.hh"
#include "StaticGameData.hh"
using namespace std;
+2
View File
@@ -3,12 +3,14 @@
#include <stdint.h>
#include <memory>
#include <phosg/JSON.hh>
#include <random>
#include <string>
#include "AFSArchive.hh"
#include "GSLArchive.hh"
#include "StaticGameData.hh"
#include "Text.hh"
class RareItemSet {
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));
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<Client> 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<Client> 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<Client> 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<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) {
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<C_CharacterData_DCv1_61_98>(data);
auto player = c->game_data.player();
player->inventory = pd.inventory;
player->disp = pd.disp.to_bb();
} else {
const auto& pd = check_size_t<C_CharacterData_DCv2_61_98>(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<Client> c, uint16_t command, uint32_t flag, cons
}
case GameVersion::PC: {
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->disp = pd.disp.to_bb();
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;
if (c->version() == GameVersion::GC) {
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: {
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
// 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<Client> 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<Client> 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<Client> 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<Client> 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<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
// 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<Client> c, uint16_t, uint32_t, const string& data)
template <typename CharT>
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));
c->game_data.player()->info_board.assign(
reinterpret_cast<const CharT*>(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<const CharT*>(data.data()), data.size() / sizeof(CharT));
}
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>
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));
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<const CharT*>(data.data()),
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) {
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) {
@@ -3256,8 +3265,9 @@ shared_ptr<Lobby> 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<Lobby> 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,
+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()) {
case GameVersion::DC: {
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;
}
case GameVersion::PC: {
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;
}
case GameVersion::GC:
case GameVersion::XB: {
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;
}
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();
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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<uint32_t> 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<PlayerInventoryItem, 30> 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<Client> 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<Client> c, uint8_t command, uint8_t f
static void add_player_exp(shared_ptr<Client> 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<Client> c, uint8_t, uint8_t, const void*
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& 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<Client> 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<uint32_t>((enemy_exp * percent) / 100, 80);
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.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<Client> 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<Client> 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<Client> 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<Client> 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",
+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) {
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 <typename CharT>
void send_info_board_t(shared_ptr<Client> c) {
vector<S_InfoBoardEntry_D8<CharT>> 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<Client> c, shared_ptr<Client> 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 <typename EntryT>
void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
vector<EntryT> entries;
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();
e.client_id = lc->lobby_client_id;
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++) {
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<Client> c, shared_ptr<Lobby> 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<PlayerDispDataDCPCV3>(
l->clients[x]->game_data.player()->disp);
cmd.players_ep3[x].disp = convert_player_disp_data<PlayerDispDataDCPCV3>(other_p->disp);
}
}
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;
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<DispDataT>(lc->game_data.player()->disp);
e.disp = convert_player_disp_data<DispDataT>(lp->disp);
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;
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<PlayerDispDataDCPCV3>(lc->game_data.player()->disp);
e.lobby_data.name = lp->disp.name;
e.inventory = lp->inventory;
e.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(lp->disp);
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");
}
const auto* items_it = c->game_data.player()->bank.items.data();
vector<PlayerBankItem> 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<PlayerBankItem> 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<uint32_t>(),
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<Client> c, uint8_t shop_type) {
// notifies players about a level up
void send_level_up(shared_ptr<Client> 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<Client> 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);
}
+1 -1
View File
@@ -315,7 +315,7 @@ vector<shared_ptr<Client>> 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;
+2
View File
@@ -2,6 +2,8 @@
#include <array>
#include "Text.hh"
using namespace std;
bool episode_has_arpg_semantics(Episode ep) {
-1
View File
@@ -7,7 +7,6 @@
#include <vector>
#include "FileContentsCache.hh"
#include "Player.hh"
enum class Episode {
NONE = 0,