implement some BB quest commands

This commit is contained in:
Martin Michelsen
2023-10-19 15:34:06 -07:00
parent 4bc5f1b90f
commit 5a30272869
14 changed files with 307 additions and 97 deletions
+2 -6
View File
@@ -396,9 +396,7 @@ static void proxy_command_exit(shared_ptr<ProxyServer::LinkedSession> ses, const
static void server_command_call(shared_ptr<Client> c, const std::u16string& args) {
auto l = c->require_lobby();
if (l->is_game() && l->quest) {
S_ConfirmQuestStatistic_V3_BB_AB cmd;
cmd.function_id = stoul(encode_sjis(args), nullptr, 0);
send_command_t(c, 0xAB, 0x00, cmd);
send_quest_function_call(c, stoul(encode_sjis(args), nullptr, 0));
} else {
send_text_message(c, u"$C6You must be in\nquest to use this\ncommand");
}
@@ -406,9 +404,7 @@ static void server_command_call(shared_ptr<Client> c, const std::u16string& args
static void proxy_command_call(shared_ptr<ProxyServer::LinkedSession> ses, const std::u16string& args) {
if (ses->is_in_game && ses->is_in_quest) {
S_ConfirmQuestStatistic_V3_BB_AB cmd;
cmd.function_id = stoul(encode_sjis(args), nullptr, 0);
ses->client_channel.send(0xAB, 0x00, &cmd, sizeof(cmd));
send_quest_function_call(ses->client_channel, stoul(encode_sjis(args), nullptr, 0));
} else {
send_text_message(ses->client_channel, u"$C6You must be in\nquest to use this\ncommand");
}
+27 -22
View File
@@ -2063,14 +2063,14 @@ struct C_SendQuestStatistic_V3_BB_AA {
parray<le_uint32_t, 8> params;
} __packed__;
// AB (S->C): Confirm quest statistic (V3/BB)
// AB (S->C): Call quest function (V3/BB)
// This command is not valid on PSO GC Episodes 1&2 Trial Edition.
// Upon receipt, the client starts a quest thread running the given function.
// Probably this is supposed to be one of the function IDs previously sent in
// the AA command, but the client does not check for this. The server can
// presumably use this command to call any function at any time during a quest.
struct S_ConfirmQuestStatistic_V3_BB_AB {
struct S_CallQuestFunction_V3_BB_AB {
le_uint16_t function_id;
parray<uint8_t, 2> unused;
} __packed__;
@@ -5534,17 +5534,17 @@ struct G_Unknown_BB_6xD4 {
struct G_ExchangeItemInQuest_BB_6xD5 {
G_ClientIDHeader header;
ItemData unknown_a1; // Only data1[0]-[2] are used
ItemData unknown_a2; // Only data1[0]-[2] are used
le_uint16_t unknown_a3;
le_uint16_t unknown_a4;
ItemData find_item; // Only data1[0]-[2] are used
ItemData replace_item; // Only data1[0]-[2] are used
le_uint16_t success_function_id;
le_uint16_t failure_function_id;
} __packed__;
// 6xD6: Wrap item (BB; handled by server)
struct G_WrapItem_BB_6xD6 {
G_ClientIDHeader header;
ItemData item_data;
ItemData item;
uint8_t unknown_a1;
parray<uint8_t, 3> unused;
} __packed__;
@@ -5554,9 +5554,9 @@ struct G_WrapItem_BB_6xD6 {
struct G_PaganiniPhotonDropExchange_BB_6xD7 {
G_ClientIDHeader header;
ItemData unknown_a1; // Only data1[0]-[2] are used
le_uint16_t request_id;
le_uint16_t unknown_a3;
ItemData new_item; // Only data1[0]-[2] are used
le_uint16_t success_function_id;
le_uint16_t failure_function_id;
} __packed__;
// 6xD8: Add S-rank weapon special (BB; handled by server)
@@ -5576,8 +5576,8 @@ struct G_AddSRankWeaponSpecial_BB_6xD8 {
struct G_MomokaItemExchange_BB_6xD9 {
G_ClientIDHeader header;
ItemData unknown_a1;
ItemData unknown_a2;
ItemData find_item; // Only data1[0]-[2] are used
ItemData replace_item; // Only data1[0]-[2] are used
le_uint32_t unknown_a3;
le_uint32_t unknown_a4;
le_uint16_t unknown_a5;
@@ -5589,13 +5589,13 @@ struct G_MomokaItemExchange_BB_6xD9 {
struct G_UpgradeWeaponAttribute_BB_6xDA {
G_ClientIDHeader header;
ItemData unknown_a1; // Only data1[0]-[2] are used
le_uint32_t item_id;
le_uint32_t attribute;
le_uint32_t unknown_a4; // 0 or 1
le_uint32_t unknown_a5;
le_uint16_t request_id;
le_uint16_t unknown_a7;
ItemData item; // Only data1[0-2] are used (argsA[1-3])
le_uint32_t item_id; // argsA[0]
le_uint32_t attribute; // argsA[4]
le_uint32_t payment_count; // Number of PD or PS (argsA[5])
le_uint32_t payment_type; // 0 = Photon Drops, 1 = Photon Spheres
le_uint16_t success_function_id; // argsA[6]
le_uint16_t failure_function_id; // argsA[7]
} __packed__;
// 6xDB: Exchange item in quest (BB)
@@ -5633,10 +5633,10 @@ struct G_GoodLuckQuestActions_BB_6xDE {
le_uint16_t unknown_a3;
} __packed__;
// 6xDF: Black Paper's Deal Photon Drop exchange (BB; handled by server)
// 6xDF: Black Paper's Deal Photon Crystal exchange (BB; handled by server)
// The client sends this when it executes an F95D quest opcode.
struct G_BlackPaperDealPhotonDropExchange_BB_6xE0 {
struct G_BlackPaperDealPhotonCrystalExchange_BB_6xDF {
G_ClientIDHeader header;
} __packed__;
@@ -5645,7 +5645,12 @@ struct G_BlackPaperDealPhotonDropExchange_BB_6xE0 {
struct G_BlackPaperDealRewards_BB_6xE0 {
G_ClientIDHeader header;
parray<uint8_t, 12> unknown_a1; // TODO: There might be uint16_ts and uint32_ts in here.
uint8_t unknown_a1;
uint8_t unknown_a2;
uint8_t unknown_a3;
uint8_t unknown_a4;
le_uint32_t unknown_a5;
le_uint32_t unknown_a6;
} __packed__;
// 6xE1: Gallon's Plan quest (BB; handled by server)
+21
View File
@@ -125,6 +125,27 @@ bool ItemData::is_wrapped() const {
}
}
void ItemData::wrap() {
switch (this->data1[0]) {
case 0:
case 1:
this->data1[4] |= 0x40;
break;
case 2:
this->data2[2] |= 0x40;
break;
case 3:
if (!this->is_stackable()) {
this->data1[3] |= 0x40;
}
break;
case 4:
break;
default:
throw runtime_error("invalid item data");
}
}
void ItemData::unwrap() {
switch (this->data1[0]) {
case 0:
+1
View File
@@ -114,6 +114,7 @@ struct ItemData { // 0x14 bytes
uint32_t primary_identifier() const;
bool is_wrapped() const;
void wrap();
void unwrap();
bool is_stackable() const;
+22
View File
@@ -8,6 +8,28 @@
using namespace std;
void PlayerStats::reset_to_base(uint8_t char_class, shared_ptr<const LevelTable> level_table) {
this->level = 0;
this->char_stats = level_table->base_stats_for_class(char_class);
}
void PlayerStats::advance_to_level(uint8_t char_class, uint32_t level, shared_ptr<const LevelTable> level_table) {
for (; this->level < level; this->level++) {
const auto& level_stats = level_table->stats_delta_for_level(char_class, this->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
this->char_stats.atp += level_stats.atp;
this->char_stats.mst += level_stats.mst;
this->char_stats.evp += level_stats.evp;
this->char_stats.hp += level_stats.hp;
this->char_stats.dfp += level_stats.dfp;
this->char_stats.ata += level_stats.ata;
// Note: It is not a bug that lck is ignored here; the original code
// ignores it too.
this->experience = level_stats.experience;
}
}
LevelTable::LevelTable(shared_ptr<const string> data, bool compressed) {
if (compressed) {
this->data.reset(new string(prs_decompress(*data)));
+5
View File
@@ -6,6 +6,8 @@
#include <phosg/Encoding.hh>
#include <string>
class LevelTable;
struct CharacterStats {
le_uint16_t atp = 0;
le_uint16_t mst = 0;
@@ -25,6 +27,9 @@ struct PlayerStats {
/* 1C */ le_uint32_t experience = 0;
/* 20 */ le_uint32_t meseta = 0;
/* 24 */
void reset_to_base(uint8_t char_class, std::shared_ptr<const LevelTable> level_table);
void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr<const LevelTable> level_table);
} __attribute__((packed));
class LevelTable { // from PlyLevelTbl.prs
+8 -41
View File
@@ -56,37 +56,19 @@ void ClientGameData::create_battle_overlay(shared_ptr<const BattleRules> rules,
this->overlay_player_data->inventory.remove_all_items_of_type(3);
}
if (rules->replace_char) {
// TODO: Shouldn't we clear other material usage here? It looks like the
// original code doesn't, but that seems wrong.
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.reset_to_base(char_class, level_table);
stats.advance_to_level(char_class, target_level, level_table);
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) {
@@ -127,23 +109,8 @@ void ClientGameData::create_challenge_overlay(size_t template_index, shared_ptr<
overlay->inventory.items[13].extension_data2 = 1;
overlay->disp.stats.char_stats = level_table->base_stats_for_class(overlay->disp.visual.char_class);
for (overlay->disp.stats.level = 0;
overlay->disp.stats.level < tpl.level;
overlay->disp.stats.level++) {
const auto& level_stats = level_table->stats_delta_for_level(
overlay->disp.visual.char_class, overlay->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
overlay->disp.stats.char_stats.atp += level_stats.atp;
overlay->disp.stats.char_stats.mst += level_stats.mst;
overlay->disp.stats.char_stats.evp += level_stats.evp;
overlay->disp.stats.char_stats.hp += level_stats.hp;
overlay->disp.stats.char_stats.dfp += level_stats.dfp;
overlay->disp.stats.char_stats.ata += level_stats.ata;
// Note: It is not a bug that lck is ignored here; the original code
// ignores it too.
}
overlay->disp.stats.reset_to_base(overlay->disp.visual.char_class, level_table);
overlay->disp.stats.advance_to_level(overlay->disp.visual.char_class, tpl.level, level_table);
overlay->disp.stats.unknown_a1 = 40;
overlay->disp.stats.unknown_a3 = 10.0;
+9
View File
@@ -380,6 +380,15 @@ size_t PlayerInventory::find_item(uint32_t item_id) const {
throw out_of_range("item not present");
}
size_t PlayerInventory::find_item_by_primary_identifier(uint32_t primary_identifier) const {
for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.primary_identifier() == primary_identifier) {
return x;
}
}
throw out_of_range("item not present");
}
size_t PlayerInventory::find_equipped_weapon() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
+1
View File
@@ -70,6 +70,7 @@ struct PlayerInventory {
PlayerInventory();
size_t find_item(uint32_t item_id) const;
size_t find_item_by_primary_identifier(uint32_t primary_identifier) const;
size_t find_equipped_weapon() const;
size_t find_equipped_armor() const;
+8 -8
View File
@@ -669,11 +669,11 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF8B2, "read4", {REG, REG}, F_V2},
{0xF8B2, "read4", {REG, INT32}, F_V3_V4 | F_ARGS},
{0xF8B3, "write1", {REG, REG}, F_V2},
{0xF8B3, "write1", {INT32, REG}, F_V3_V4 | F_ARGS},
{0xF8B3, "write1", {INT32, INT32}, F_V3_V4 | F_ARGS},
{0xF8B4, "write2", {REG, REG}, F_V2},
{0xF8B4, "write2", {INT32, REG}, F_V3_V4 | F_ARGS},
{0xF8B4, "write2", {INT32, INT32}, F_V3_V4 | F_ARGS},
{0xF8B5, "write4", {REG, REG}, F_V2},
{0xF8B5, "write4", {INT32, REG}, F_V3_V4 | F_ARGS},
{0xF8B5, "write4", {INT32, INT32}, F_V3_V4 | F_ARGS},
{0xF8B6, "check_for_hacking", {REG}, F_V2}, // Returns a bitmask of 5 different types of detectable hacking. But it only works on DCv2 - it crashes on all other versions.
{0xF8B7, nullptr, {REG}, F_V2_V4}, // TODO (DX) - Challenge mode. Appears to be timing-related; regA is expected to be in [60, 3600]. Encodes the value with encrypt_challenge_time even though it's never sent over the network and is only decrypted locally.
{0xF8B8, "disable_retry_menu", {}, F_V2_V4},
@@ -829,13 +829,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF952, "bb_get_number_in_pack", {REG}, F_V4},
{0xF953, "bb_swap_item", {INT32, INT32, INT32, INT32, INT32, INT32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS}, // Sends 6xD5
{0xF954, "bb_check_wrap", {INT32, REG}, F_V4 | F_ARGS},
{0xF955, "bb_exchange_pd_item", {INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xD7
{0xF955, "bb_exchange_pd_item", {INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xD7
{0xF956, "bb_exchange_pd_srank", {INT32, INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xD8
{0xF957, "bb_exchange_pd_special", {INT32, INT32, INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xDA
{0xF958, "bb_exchange_pd_percent", {INT32, INT32, INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xDA
{0xF957, "bb_exchange_pd_percent", {INT32, INT32, INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xDA
{0xF958, "bb_exchange_ps_percent", {INT32, INT32, INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xDA
{0xF959, "bb_set_ep4_boss_can_escape", {INT32}, F_V4 | F_ARGS},
{0xF95A, "bb_is_ep4_boss_dying", {REG}, F_V4},
{0xF95B, "bb_send_6xD9", {INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xD9
{0xF95B, "bb_send_6xD9", {INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xD9
{0xF95C, "bb_exchange_slt", {INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xDE
{0xF95D, "bb_exchange_pc", {}, F_V4}, // Sends 6xDF
{0xF95E, "bb_box_create_bp", {INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xE0
@@ -1498,7 +1498,7 @@ std::string disassemble_quest_script(const void* data, size_t size, QuestScriptV
StringReader r = cmd_r.sub(l->offset, size);
lines.emplace_back(" // As F8F2 entries");
while (r.remaining() >= sizeof(UnknownF8F2Entry)) {
size_t offset = r.where() + cmd_r.where();
size_t offset = l->offset + cmd_r.where();
const auto& e = r.get<UnknownF8F2Entry>();
lines.emplace_back(string_printf(" %04zX entry %g, %g, %g, %g", offset, e.unknown_a1[0].load(), e.unknown_a1[1].load(), e.unknown_a1[2].load(), e.unknown_a1[3].load()));
}
+1 -3
View File
@@ -2520,9 +2520,7 @@ static void on_AA(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
}
// TODO: Send the right value here. (When should we send function_id2?)
S_ConfirmQuestStatistic_V3_BB_AB response;
response.function_id = cmd.function_id1;
send_command_t(c, 0xAB, 0x00, response);
send_quest_function_call(c, cmd.function_id1);
}
static void on_D7_GC(shared_ptr<Client> c, uint16_t, uint32_t, const string& data) {
+188 -17
View File
@@ -1743,9 +1743,9 @@ static void on_battle_restart_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
auto s = c->require_server_state();
auto l = c->require_lobby();
if (l->is_game() &&
(l->base_version == GameVersion::BB) &&
(l->mode == GameMode::BATTLE) &&
(l->flags & Lobby::Flag::QUEST_IN_PROGRESS) &&
(l->base_version == GameVersion::BB)) {
(l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) {
const auto& cmd = check_size_t<G_RestartBattle_BB_6xCF>(data, size);
shared_ptr<BattleRules> new_rules(new BattleRules(cmd.rules));
@@ -1763,19 +1763,190 @@ static void on_battle_restart_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
}
}
static void on_battle_level_up_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void*, size_t) {
static void on_battle_level_up_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() &&
(l->base_version == GameVersion::BB) &&
(l->mode == GameMode::BATTLE) &&
(l->flags & Lobby::Flag::QUEST_IN_PROGRESS) &&
(l->base_version == GameVersion::BB)) {
// Requests the client to be leveled up by num_levels levels. The server should
// respond with a 6x30 command.
(l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) {
const auto& cmd = check_size_t<G_BattleModeLevelUp_BB_6xD0>(data, size);
auto lc = l->clients[cmd.header.client_id];
if (lc) {
auto s = c->require_server_state();
auto lp = lc->game_data.player();
uint32_t target_level = lp->disp.stats.level + cmd.num_levels;
uint32_t before_exp = lp->disp.stats.experience;
lp->disp.stats.advance_to_level(lp->disp.visual.char_class, target_level, s->level_table);
send_give_experience(lc, lp->disp.stats.experience - before_exp);
send_level_up(lc);
}
}
}
struct G_BattleModeLevelUp_BB_6xD0 {
G_ClientIDHeader header;
le_uint32_t num_levels;
} __packed__;
static void on_quest_exchange_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() &&
(l->base_version == GameVersion::BB) &&
(l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) {
const auto& cmd = check_size_t<G_ExchangeItemInQuest_BB_6xD5>(data, size);
try {
auto p = c->game_data.player();
size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier());
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false);
send_destroy_item(c, found_item.id, 1);
// TODO: We probably should use an allow-list here to prevent the client
// from creating arbitrary items if cheat mode is disabled.
ItemData new_item = cmd.replace_item;
new_item.id = l->generate_item_id(c->lobby_client_id);
p->add_item(new_item);
send_create_inventory_item(c, new_item);
send_quest_function_call(c, cmd.success_function_id);
} catch (const exception& e) {
c->log.warning("Quest item exchange failed: %s", e.what());
send_quest_function_call(c, cmd.failure_function_id);
}
}
}
static void on_wrap_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB)) {
const auto& cmd = check_size_t<G_WrapItem_BB_6xD6>(data, size);
auto p = c->game_data.player();
auto item = p->remove_item(cmd.item.id, 1, false);
send_destroy_item(c, item.id, 1);
item.wrap();
p->add_item(cmd.item);
send_create_inventory_item(c, item);
}
}
static void on_photon_drop_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB)) {
const auto& cmd = check_size_t<G_PaganiniPhotonDropExchange_BB_6xD7>(data, size);
try {
auto p = c->game_data.player();
size_t found_index = p->inventory.find_item_by_primary_identifier(0x031000);
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, false);
send_destroy_item(c, found_item.id, found_item.stack_size());
// TODO: We probably should use an allow-list here to prevent the client
// from creating arbitrary items if cheat mode is disabled.
ItemData new_item = cmd.new_item;
new_item.id = l->generate_item_id(c->lobby_client_id);
p->add_item(new_item);
send_create_inventory_item(c, new_item);
send_quest_function_call(c, cmd.success_function_id);
} catch (const exception& e) {
c->log.warning("Quest Photon Drop exchange failed: %s", e.what());
send_quest_function_call(c, cmd.failure_function_id);
}
}
}
static void on_photon_crystal_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB) && (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) {
check_size_t<G_BlackPaperDealPhotonCrystalExchange_BB_6xDF>(data, size);
auto p = c->game_data.player();
size_t index = p->inventory.find_item_by_primary_identifier(0x031002);
auto item = p->remove_item(p->inventory.items[index].data.id, 1, false);
send_destroy_item(c, item.id, 1);
// TODO: Should we disable drops here?
}
}
static void on_momoka_item_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB) && (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) {
const auto& cmd = check_size_t<G_MomokaItemExchange_BB_6xD9>(data, size);
auto p = c->game_data.player();
try {
size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier());
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false);
G_ExchangeItemInQuest_BB_6xDB cmd_6xDB = {{0xDB, 0x04, c->lobby_client_id}, 1, found_item.id, 1};
send_command_t(c, 0x60, 0x00, cmd_6xDB);
send_destroy_item(c, found_item.id, 1);
// TODO: We probably should use an allow-list here to prevent the client
// from creating arbitrary items if cheat mode is disabled.
ItemData new_item = cmd.replace_item;
new_item.id = l->generate_item_id(c->lobby_client_id);
p->add_item(new_item);
send_create_inventory_item(c, new_item);
send_command(c, 0x23, 0x00);
} catch (const exception& e) {
c->log.warning("Momoka item exchange failed: %s", e.what());
send_command(c, 0x23, 0x01);
}
}
}
static void on_upgrade_weapon_attribute_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == GameVersion::BB) && (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) {
const auto& cmd = check_size_t<G_UpgradeWeaponAttribute_BB_6xDA>(data, size);
auto p = c->game_data.player();
try {
size_t item_index = p->inventory.find_item(cmd.item_id);
auto& item = p->inventory.items[item_index].data;
uint32_t payment_primary_identifier = cmd.payment_type ? 0x031001 : 0x031000;
size_t payment_index = p->inventory.find_item_by_primary_identifier(payment_primary_identifier);
auto& payment_item = p->inventory.items[payment_index].data;
if (payment_item.stack_size() < cmd.payment_count) {
throw runtime_error("not enough payment items present");
}
p->remove_item(payment_item.id, cmd.payment_count, false);
send_destroy_item(c, payment_item.id, cmd.payment_count);
uint8_t attribute_amount = 0;
if (cmd.payment_type == 1 && cmd.payment_count == 1) {
attribute_amount = 30;
} else if (cmd.payment_type == 0 && cmd.payment_count == 4) {
attribute_amount = 1;
} else if (cmd.payment_type == 1 && cmd.payment_count == 20) {
attribute_amount = 5;
} else {
throw runtime_error("unknown PD/PS expenditure");
}
size_t attribute_index = 0;
for (size_t z = 6; z <= 10; z += 2) {
if (!(item.data1[z] & 0x80) && (item.data1[z] == cmd.attribute)) {
attribute_index = z;
} else if (item.data1[z] == 0) {
attribute_index = z;
}
}
if (attribute_index == 0) {
throw runtime_error("no available attribute slots");
}
item.data1[attribute_index] = cmd.attribute;
item.data1[attribute_index] += attribute_amount;
send_destroy_item(c, item.id, 1);
send_create_inventory_item(c, item);
send_quest_function_call(c, cmd.success_function_id);
} catch (const exception& e) {
c->log.warning("Weapon attribute upgrade failed: %s", e.what());
send_quest_function_call(c, cmd.failure_function_id);
}
}
}
@@ -1997,17 +2168,17 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6xD2 */ nullptr,
/* 6xD3 */ nullptr,
/* 6xD4 */ nullptr,
/* 6xD5 */ nullptr,
/* 6xD6 */ nullptr,
/* 6xD7 */ nullptr,
/* 6xD5 */ on_quest_exchange_item_bb,
/* 6xD6 */ on_wrap_item_bb,
/* 6xD7 */ on_photon_drop_exchange_bb,
/* 6xD8 */ nullptr,
/* 6xD9 */ nullptr,
/* 6xDA */ nullptr,
/* 6xD9 */ on_momoka_item_exchange_bb,
/* 6xDA */ on_upgrade_weapon_attribute_bb,
/* 6xDB */ nullptr,
/* 6xDC */ nullptr,
/* 6xDD */ nullptr,
/* 6xDE */ nullptr,
/* 6xDF */ nullptr,
/* 6xDF */ on_photon_crystal_exchange_bb,
/* 6xE0 */ nullptr,
/* 6xE1 */ nullptr,
/* 6xE2 */ nullptr,
+10
View File
@@ -2209,6 +2209,16 @@ void send_rare_enemy_index_list(shared_ptr<Client> c, const vector<size_t>& inde
send_command_t(c, 0xDE, 0x00, cmd);
}
void send_quest_function_call(Channel& ch, uint16_t function_id) {
S_CallQuestFunction_V3_BB_AB cmd;
cmd.function_id = function_id;
ch.send(0xAB, 0x00, &cmd, sizeof(cmd));
}
void send_quest_function_call(shared_ptr<Client> c, uint16_t function_id) {
send_quest_function_call(c->channel, function_id);
}
////////////////////////////////////////////////////////////////////////////////
// ep3 only commands
+4
View File
@@ -307,6 +307,10 @@ void send_level_up(std::shared_ptr<Client> c);
void send_give_experience(std::shared_ptr<Client> c, uint32_t amount);
void send_set_exp_multiplier(std::shared_ptr<Lobby> l);
void send_rare_enemy_index_list(std::shared_ptr<Client> c, const std::vector<size_t>& indexes);
void send_quest_function_call(Channel& ch, uint16_t function_id);
void send_quest_function_call(std::shared_ptr<Client> c, uint16_t function_id);
void send_ep3_card_list_update(std::shared_ptr<Client> c);
void send_ep3_media_update(
std::shared_ptr<Client> c,