implement battle rules and character replacement
This commit is contained in:
+20
-19
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "ItemData.hh"
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <phosg/Random.hh>
|
||||
|
||||
#include "BattleParamsIndex.hh"
|
||||
#include "ItemData.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,6 +2,8 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
bool episode_has_arpg_semantics(Episode ep) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "FileContentsCache.hh"
|
||||
#include "Player.hh"
|
||||
|
||||
enum class Episode {
|
||||
NONE = 0,
|
||||
|
||||
Reference in New Issue
Block a user