implement quest item creation masks
This commit is contained in:
+24
-10
@@ -6280,6 +6280,8 @@ struct G_AdjustPlayerMeseta_BB_6xC9 {
|
||||
} __packed_ws__(G_AdjustPlayerMeseta_BB_6xC9, 8);
|
||||
|
||||
// 6xCA: Request item reward from quest (BB; handled by server)
|
||||
// The server should create the item in the player's inventory using 6xBE if it
|
||||
// matches at least one of the item creation masks in the quest's header.
|
||||
|
||||
struct G_QuestCreateItem_BB_6xCA {
|
||||
G_UnusedHeader header;
|
||||
@@ -6365,6 +6367,10 @@ struct G_Unknown_BB_6xD4 {
|
||||
|
||||
// 6xD5: Exchange item in quest (BB; handled by server)
|
||||
// The client sends this when it executes an F953 quest opcode.
|
||||
// If any item matching find_item.data1[0-2] is present in the player's
|
||||
// inventory, the server should destroy that item using 6x29, then create
|
||||
// replace_item in the player's inventory using 6xBE if it matches at least one
|
||||
// of the item creation masks in the quest's header.
|
||||
|
||||
struct G_QuestExchangeItem_BB_6xD5 {
|
||||
G_ClientIDHeader header;
|
||||
@@ -6385,6 +6391,8 @@ struct G_WrapItem_BB_6xD6 {
|
||||
|
||||
// 6xD7: Paganini Photon Drop exchange (BB; handled by server)
|
||||
// The client sends this when it executes an F955 quest opcode.
|
||||
// The server should create the item in the player's inventory using 6xBE if it
|
||||
// matches at least one of the item creation masks in the quest's header.
|
||||
|
||||
struct G_PaganiniPhotonDropExchange_BB_6xD7 {
|
||||
G_ClientIDHeader header;
|
||||
@@ -6409,7 +6417,10 @@ struct G_AddSRankWeaponSpecial_BB_6xD8 {
|
||||
// The client sends this when it executes an F95B quest opcode. The client has
|
||||
// an unfortunate bug where it doesn't set the size field when generating this
|
||||
// command, so the size ends up as an uninitialized value and the client sends
|
||||
// more (or less!) data than necessary.
|
||||
// more (or less!) data than necessary. The MomokaItemExchangeFix patch fixes
|
||||
// this bug.
|
||||
// The server should create the item in the player's inventory using 6xBE if it
|
||||
// matches at least one of the item creation masks in the quest's header.
|
||||
|
||||
struct G_MomokaItemExchange_BB_6xD9 {
|
||||
/* 00 */ G_ClientIDHeader header;
|
||||
@@ -6470,18 +6481,21 @@ struct G_SetEXPMultiplier_BB_6xDD {
|
||||
|
||||
// 6xDE: Exchange Secret Lottery Ticket (BB; handled by server)
|
||||
// The client sends this when it executes an F95C quest opcode.
|
||||
// There appears to be a bug in the client here: it sets the subcommand size to
|
||||
// 2 instead of 3, so the last relevant field (failure_label) is not sent to
|
||||
// the server.
|
||||
// There is a bug in the client here: it sets the subcommand size to 2 instead
|
||||
// of 3, so the last relevant field (failure_label) is not sent to the server.
|
||||
// This is fixed in the MomokaItemExchangeFix patch.
|
||||
|
||||
struct G_ExchangeSecretLotteryTicket_BB_6xDE {
|
||||
struct G_ExchangeSecretLotteryTicket_Incomplete_BB_6xDE {
|
||||
G_ClientIDHeader header;
|
||||
uint8_t index = 0;
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t index = 0; // 1-8
|
||||
uint8_t start_reg_num = 0;
|
||||
le_uint16_t success_label = 0;
|
||||
// le_uint16_t failure_label = 0;
|
||||
// parray<uint8_t, 2> unused;
|
||||
} __packed_ws__(G_ExchangeSecretLotteryTicket_BB_6xDE, 8);
|
||||
} __packed_ws__(G_ExchangeSecretLotteryTicket_Incomplete_BB_6xDE, 8);
|
||||
|
||||
struct G_ExchangeSecretLotteryTicket_BB_6xDE : G_ExchangeSecretLotteryTicket_Incomplete_BB_6xDE {
|
||||
le_uint16_t failure_label = 0;
|
||||
parray<uint8_t, 2> unused;
|
||||
} __packed_ws__(G_ExchangeSecretLotteryTicket_BB_6xDE, 0x0C);
|
||||
|
||||
// 6xDF: Exchange Photon Crystals (BB; handled by server)
|
||||
// The client sends this when it executes an F95D quest opcode.
|
||||
|
||||
@@ -354,6 +354,9 @@ const string& Quest::name_for_language(Language language) const {
|
||||
|
||||
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
|
||||
this->meta.assert_compatible(vq->meta);
|
||||
if (this->meta.create_item_mask_entries.empty()) {
|
||||
this->meta.create_item_mask_entries = vq->meta.create_item_mask_entries;
|
||||
}
|
||||
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
|
||||
|
||||
size_t lang_index = static_cast<size_t>(vq->language);
|
||||
|
||||
@@ -47,6 +47,25 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
|
||||
if (this->enemy_exp_overrides != other.enemy_exp_overrides) {
|
||||
throw runtime_error("quest version has different enemy EXP overrides");
|
||||
}
|
||||
if (!this->create_item_mask_entries.empty() &&
|
||||
!other.create_item_mask_entries.empty() &&
|
||||
this->create_item_mask_entries != other.create_item_mask_entries) {
|
||||
string this_str, other_str;
|
||||
for (const auto& item : this->create_item_mask_entries) {
|
||||
if (!this_str.empty()) {
|
||||
this_str += ", ";
|
||||
}
|
||||
this_str += item.str();
|
||||
}
|
||||
for (const auto& item : other.create_item_mask_entries) {
|
||||
if (!other_str.empty()) {
|
||||
other_str += ", ";
|
||||
}
|
||||
other_str += item.str();
|
||||
}
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different set of create item masks (existing: {}, new: {})", this_str, other_str));
|
||||
}
|
||||
if (!this->battle_rules != !other.battle_rules) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different battle rules presence state (existing: {}, new: {})",
|
||||
@@ -151,6 +170,12 @@ phosg::JSON QuestMetadata::json() const {
|
||||
auto key_str = std::format("{}:0x{:02X}:{}", name_for_difficulty(difficulty), floor, phosg::name_for_enum(enemy_type));
|
||||
enemy_exp_overrides_json.emplace(key_str, exp_override);
|
||||
}
|
||||
|
||||
auto create_item_mask_entries_json = phosg::JSON::dict();
|
||||
for (const auto& item : this->create_item_mask_entries) {
|
||||
create_item_mask_entries_json.emplace_back(item.str());
|
||||
}
|
||||
|
||||
return phosg::JSON::dict({
|
||||
{"CategoryID", this->category_id},
|
||||
{"Number", this->quest_number},
|
||||
@@ -172,9 +197,69 @@ phosg::JSON QuestMetadata::json() const {
|
||||
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
|
||||
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
|
||||
{"EnemyEXPOverrides", std::move(enemy_exp_overrides_json)},
|
||||
{"CreateItemMasks", std::move(create_item_mask_entries_json)},
|
||||
});
|
||||
}
|
||||
|
||||
QuestMetadata::CreateItemMask::CreateItemMask(const std::string& s) {
|
||||
phosg::StringReader r(s);
|
||||
for (size_t z = 0; z < 12 && !r.eof(); z++) {
|
||||
auto& range = this->data1_ranges[z];
|
||||
char c = r.get_s8();
|
||||
if (c == '[') {
|
||||
c = r.get_s8();
|
||||
range.min = (phosg::value_for_hex_char(c) << 4) | phosg::value_for_hex_char(r.get_s8());
|
||||
if (r.get_s8() != '-') {
|
||||
throw std::runtime_error("invalid range spec");
|
||||
}
|
||||
c = r.get_s8();
|
||||
range.max = (phosg::value_for_hex_char(c) << 4) | phosg::value_for_hex_char(r.get_s8());
|
||||
if (r.get_s8() != ']') {
|
||||
throw std::runtime_error("invalid range spec");
|
||||
}
|
||||
} else {
|
||||
range.min = (phosg::value_for_hex_char(c) << 4) | phosg::value_for_hex_char(r.get_s8());
|
||||
range.max = range.min;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string QuestMetadata::CreateItemMask::str() const {
|
||||
std::string ret;
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
const auto& r = this->data1_ranges[z];
|
||||
if (r.min == r.max) {
|
||||
ret += std::format("{:02X}", r.min);
|
||||
} else {
|
||||
ret += std::format("[{:02X}-{:02X}]", r.min, r.max);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool QuestMetadata::CreateItemMask::match(const ItemData& item) const {
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
const auto& r = this->data1_ranges[z];
|
||||
uint8_t v = item.data1[z];
|
||||
if (v < r.min || v > r.max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t QuestMetadata::CreateItemMask::primary_identifier() const {
|
||||
uint32_t ret = 0;
|
||||
for (size_t z = 0; z < 3; z++) {
|
||||
const auto& r = this->data1_ranges[z];
|
||||
if (r.min != r.max) {
|
||||
throw std::runtime_error("create item mask is ambiguous; cannot compute primary identifier");
|
||||
}
|
||||
ret = (ret << 8) | r.min;
|
||||
}
|
||||
return ret << 8;
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> QuestMetadata::parse_enemy_exp_overrides(const phosg::JSON& json) {
|
||||
try {
|
||||
std::unordered_map<uint32_t, uint32_t> ret;
|
||||
|
||||
@@ -44,6 +44,33 @@ struct QuestMetadata {
|
||||
int16_t lock_status_register = -1;
|
||||
std::unordered_map<uint32_t, uint32_t> enemy_exp_overrides;
|
||||
|
||||
// Item create allowances (only used on BB)
|
||||
struct CreateItemMask {
|
||||
struct Range {
|
||||
uint8_t min = 0x00;
|
||||
uint8_t max = 0x00;
|
||||
|
||||
bool operator==(const Range& other) const = default;
|
||||
bool operator!=(const Range& other) const = default;
|
||||
};
|
||||
std::array<Range, 12> data1_ranges;
|
||||
|
||||
CreateItemMask() = default;
|
||||
CreateItemMask(const CreateItemMask& other) = default;
|
||||
CreateItemMask(CreateItemMask&& other) = default;
|
||||
CreateItemMask& operator=(const CreateItemMask& other) = default;
|
||||
CreateItemMask& operator=(CreateItemMask&& other) = default;
|
||||
bool operator==(const CreateItemMask& other) const = default;
|
||||
bool operator!=(const CreateItemMask& other) const = default;
|
||||
|
||||
explicit CreateItemMask(const std::string& s); // Inverse of str()
|
||||
std::string str() const;
|
||||
|
||||
bool match(const ItemData& item) const;
|
||||
uint32_t primary_identifier() const; // Raises if any of data1[0-2] are ambiguous
|
||||
};
|
||||
std::vector<CreateItemMask> create_item_mask_entries;
|
||||
|
||||
std::string name;
|
||||
std::string short_description;
|
||||
std::string long_description;
|
||||
|
||||
+123
-17
@@ -969,7 +969,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
|
||||
// Creates an item in the player's inventory. If the item is successfully
|
||||
// created, this opcode sends 6x2B on all versions except BB. On BB, this
|
||||
// opcode sends 6xCA, and the server sends 6xBE to create the item.
|
||||
// opcode sends 6xCA, and the server sends 6xBE to create the item. The
|
||||
// requested item must match one of the item creation masks in the quest
|
||||
// script's header.
|
||||
// regsA[0-2] = item.data1[0-2]
|
||||
// regB = returned item ID, or FFFFFFFF if item can't be created
|
||||
{0xB3, "item_create", nullptr, {{R_REG_SET_FIXED, 3}, W_REG}, F_V0_V4},
|
||||
@@ -2764,6 +2766,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// 5 = bank
|
||||
// 6 = tekker
|
||||
// 7 = government quest counter
|
||||
// 8 = Momoka item exchange (this opens a menu displaying the items
|
||||
// specified in the quest header, which sends 6xD9 when the player
|
||||
// chooses an item)
|
||||
// valueA is not bounds-checked, so it could be used to write a byte with
|
||||
// the value 1 anywhere in memory.
|
||||
{0xF950, "bb_p2_menu", "BB_p2_menu", {I32}, F_V4 | F_ARGS},
|
||||
@@ -2781,7 +2786,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// Returns the number of items in the player's inventory.
|
||||
{0xF952, "bb_get_number_in_pack", "BB_get_number_in_pack", {W_REG}, F_V4},
|
||||
|
||||
// Requests an item exchange in the player's inventory. Sends 6xD5.
|
||||
// Requests an item exchange in the player's inventory. Sends 6xD5. The
|
||||
// requested item must match one of the item creation masks in the quest
|
||||
// script's header.
|
||||
// valueA/valueB/valueC = item.data1[0-2] to search for
|
||||
// valueD/valueE/valueF = item.data1[0-2] to replace it with
|
||||
// labelG = label to call on success
|
||||
@@ -2794,11 +2801,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// 2 = item not found)
|
||||
{0xF954, "bb_check_wrap", "BB_check_wrap", {I32, W_REG}, F_V4 | F_ARGS},
|
||||
|
||||
// Requests an item exchange for Photon Drops. Sends 6xD7.
|
||||
// Requests an item exchange for Photon Drops. Sends 6xD7. The requested
|
||||
// item must match one of the item creation masks in the quest script's
|
||||
// header.
|
||||
// valueA/valueB/valueC = item.data1[0-2] for requested item
|
||||
// labelD = label to call on success
|
||||
// labelE = label to call on failure
|
||||
{0xF955, "bb_exchange_pd_item", "BB_exchange_PD_item", {I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
||||
{0xF955, "bb_exchange_pd_item", "BB_exchange_PD_item", {I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||
|
||||
// Requests an S-rank special upgrade in exchange for Photon Drops. Sends
|
||||
// 6xD8.
|
||||
@@ -2807,7 +2816,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// valueE = special type
|
||||
// labelF = label to call on success
|
||||
// labelG = label to call on failure
|
||||
{0xF956, "bb_exchange_pd_srank", "BB_exchange_PD_srank", {I32, I32, I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
||||
{0xF956, "bb_exchange_pd_srank", "BB_exchange_PD_srank", {I32, I32, I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||
|
||||
// Requests a weapon attribute upgrade in exchange for Photon Drops. Sends
|
||||
// 6xDA.
|
||||
@@ -2817,12 +2826,12 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// valueF = payment count (number of PDs)
|
||||
// labelG = label to call on success
|
||||
// labelH = label to call on failure
|
||||
{0xF957, "bb_exchange_pd_percent", "BB_exchange_PD_special", {I32, I32, I32, I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
||||
{0xF957, "bb_exchange_pd_percent", "BB_exchange_PD_special", {I32, I32, I32, I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||
|
||||
// Requests a weapon attribute upgrade in exchange for Photon Spheres.
|
||||
// Sends 6xDA. Same arguments as bb_exchange_pd_percent, except Photon
|
||||
// Spheres are used instead.
|
||||
{0xF958, "bb_exchange_ps_percent", "BB_exchange_PS_percent", {I32, I32, I32, I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
||||
{0xF958, "bb_exchange_ps_percent", "BB_exchange_PS_percent", {I32, I32, I32, I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||
|
||||
// Determines whether the Episode 4 boss can escape if undefeated after 20
|
||||
// minutes.
|
||||
@@ -2833,24 +2842,28 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// (even if the boss has already been defeated).
|
||||
{0xF95A, "bb_is_ep4_boss_dying", nullptr, {W_REG}, F_V4},
|
||||
|
||||
// Requests an item exchange. Sends 6xD9.
|
||||
// Requests an item exchange. Sends 6xD9. The requested item must match one
|
||||
// of the item creation masks in the quest script's header.
|
||||
// valueA = find_item.data1[0-2] (low 3 bytes; high byte unused)
|
||||
// valueB = replace_item.data1[0-2] (low 3 bytes; high byte unused)
|
||||
// valueC = token1 (see 6xD9 in CommandFormats.hh)
|
||||
// valueD = token2 (see 6xD9 in CommandFormats.hh)
|
||||
// labelE = label to call on success
|
||||
// labelF = label to call on failure
|
||||
{0xF95B, "bb_send_6xD9", nullptr, {I32, I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
||||
{0xF95B, "bb_replace_item", "bb_send_6xD9", {I32, I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||
|
||||
// Requests an exchange of Secret Lottery Tickets for items. Sends 6xDE.
|
||||
// See SecretLotteryResultItems in config.json for the item pool used by
|
||||
// this opcode.
|
||||
// The pool of items that can be returned by this opcode is determined by
|
||||
// the quest's header; newserv assembles/disassembles this field as
|
||||
// .exchange_item directives. The first entry in the header is the currency
|
||||
// item (which is to be deleted from the inventory); the others are the
|
||||
// items the server randomly chooses from.
|
||||
// valueA = index
|
||||
// valueB = unknown_a1
|
||||
// labelC = label to call on success
|
||||
// labelD = label to call on failure (unused because of a client bug; see
|
||||
// 6xDE description in CommandFormats.hh for details)
|
||||
{0xF95C, "bb_exchange_slt", "BB_exchange_SLT", {I32, I32, LABEL32, LABEL32}, F_V4 | F_ARGS},
|
||||
{0xF95C, "bb_exchange_slt", "BB_exchange_SLT", {I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||
|
||||
// Removes a single Photon Crystal from the player's inventory, and
|
||||
// disables drops for the rest of the quest. Sends 6xDF.
|
||||
@@ -2959,6 +2972,52 @@ void check_opcode_definitions() {
|
||||
}
|
||||
}
|
||||
|
||||
CreateItemMaskEntry::CreateItemMaskEntry(const QuestMetadata::CreateItemMask& mask) {
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
auto& r = mask.data1_ranges[z];
|
||||
if (r.min == r.max) {
|
||||
this->data1_fields[z] = r.min;
|
||||
} else if (r.min == 0x00 && r.max == 0xFF) {
|
||||
this->data1_fields[z] = -1;
|
||||
} else {
|
||||
this->data1_fields[z] = (r.min * 1000000) + (r.max * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CreateItemMaskEntry::operator QuestMetadata::CreateItemMask() const {
|
||||
using Range = QuestMetadata::CreateItemMask::Range;
|
||||
|
||||
QuestMetadata::CreateItemMask ret;
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
int32_t v = this->data1_fields[z];
|
||||
|
||||
if (v < 0) {
|
||||
// If v is negative, any value is allowed in this field
|
||||
ret.data1_ranges[z] = Range{.min = 0x00, .max = 0xFF};
|
||||
|
||||
} else if (v < 0x100) {
|
||||
// If v fits in an unsigned byte, this field must match exactly
|
||||
ret.data1_ranges[z] = Range{.min = static_cast<uint8_t>(v), .max = static_cast<uint8_t>(v)};
|
||||
|
||||
} else if (v >= 1000000 && v <= 2000000) {
|
||||
// Otherwise, the allowed range of values is encoded in decimal as
|
||||
// 1MMMmmm (m = min, M = max)
|
||||
uint32_t min = v % 1000;
|
||||
uint32_t max = (v / 1000) % 1000;
|
||||
if (min > 0xFF || max > 0xFF | min > max) {
|
||||
throw std::runtime_error(std::format("invalid range spec {} (0x{:X})", v, v));
|
||||
}
|
||||
ret.data1_ranges[z] = Range{.min = static_cast<uint8_t>(min), .max = static_cast<uint8_t>(max)};
|
||||
|
||||
} else {
|
||||
throw std::runtime_error(std::format("invalid range spec {} (0x{:X})", v, v));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string disassemble_quest_script(
|
||||
const void* data,
|
||||
size_t size,
|
||||
@@ -3047,7 +3106,7 @@ std::string disassemble_quest_script(
|
||||
}
|
||||
case Version::BB_V4: {
|
||||
use_wstrs = true;
|
||||
const auto& header = r.get<PSOQuestHeaderBB>();
|
||||
const auto& header = r.get<PSOQuestHeaderBBBase>();
|
||||
code_offset = header.code_offset;
|
||||
function_table_offset = header.function_table_offset;
|
||||
if (override_language != Language::UNKNOWN) {
|
||||
@@ -3061,9 +3120,25 @@ std::string disassemble_quest_script(
|
||||
if (header.joinable) {
|
||||
lines.emplace_back(".joinable");
|
||||
}
|
||||
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
|
||||
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
|
||||
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
|
||||
lines.emplace_back(std::format(".name {}", escape_string(header.name.decode(language))));
|
||||
lines.emplace_back(std::format(".short_desc {}", escape_string(header.short_description.decode(language))));
|
||||
lines.emplace_back(std::format(".long_desc {}", escape_string(header.long_description.decode(language))));
|
||||
// Quests saved with Qedit may not have the full header, so only parse
|
||||
// the full header if the code and function table offsets don't point to
|
||||
// space within it
|
||||
if ((header.code_offset >= sizeof(PSOQuestHeaderBB)) &&
|
||||
(header.function_table_offset >= sizeof(PSOQuestHeaderBB))) {
|
||||
r.go(0);
|
||||
const auto& header = r.get<PSOQuestHeaderBB>();
|
||||
for (size_t z = 0; z < header.create_item_mask_entries.size(); z++) {
|
||||
const auto& qh_mask = header.create_item_mask_entries[z];
|
||||
if (!qh_mask.is_valid()) {
|
||||
break;
|
||||
}
|
||||
QuestMetadata::CreateItemMask qm_mask = qh_mask;
|
||||
lines.emplace_back(std::format(".allow_create_item {}", qm_mask.str()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -4010,6 +4085,7 @@ AssembledQuestScript assemble_quest_script(
|
||||
Episode quest_episode = Episode::EP1;
|
||||
uint8_t quest_max_players = 4;
|
||||
bool quest_joinable = false;
|
||||
std::vector<QuestMetadata::CreateItemMask> create_item_mask_entries;
|
||||
for (const auto& line : lines) {
|
||||
if (line.text.empty()) {
|
||||
continue;
|
||||
@@ -4028,6 +4104,17 @@ AssembledQuestScript assemble_quest_script(
|
||||
quest_short_desc = phosg::parse_data_string(line.text.substr(12));
|
||||
} else if (line.text.starts_with(".long_desc ")) {
|
||||
quest_long_desc = phosg::parse_data_string(line.text.substr(11));
|
||||
} else if (line.text.starts_with(".allow_create_item ")) {
|
||||
if (create_item_mask_entries.size() >= 0x40) {
|
||||
throw std::runtime_error("too many .allow_create_item directives; at most 64 are allowed");
|
||||
}
|
||||
|
||||
string args_str = line.text.substr(19);
|
||||
phosg::strip_whitespace(args_str);
|
||||
|
||||
QuestMetadata::CreateItemMask mask(line.text.substr(19));
|
||||
create_item_mask_entries.emplace_back(mask);
|
||||
|
||||
} else if (line.text.starts_with(".quest_num ")) {
|
||||
quest_num = stoul(line.text.substr(11), nullptr, 0);
|
||||
} else if (line.text.starts_with(".language ")) {
|
||||
@@ -4646,6 +4733,10 @@ AssembledQuestScript assemble_quest_script(
|
||||
header.name.encode(quest_name, quest_language);
|
||||
header.short_description.encode(quest_short_desc, quest_language);
|
||||
header.long_description.encode(quest_long_desc, quest_language);
|
||||
header.unknown_a5.clear(0xFF);
|
||||
for (size_t z = 0; z < create_item_mask_entries.size(); z++) {
|
||||
header.create_item_mask_entries[z] = create_item_mask_entries[z];
|
||||
}
|
||||
w.put(header);
|
||||
break;
|
||||
}
|
||||
@@ -4753,7 +4844,7 @@ void populate_quest_metadata_from_script(
|
||||
break;
|
||||
}
|
||||
case Version::BB_V4: {
|
||||
const auto& header = r.get<PSOQuestHeaderBB>();
|
||||
const auto& header = r.get<PSOQuestHeaderBBBase>();
|
||||
meta.episode = episode_for_quest_episode_number(header.episode);
|
||||
meta.joinable |= header.joinable;
|
||||
meta.max_players = 4;
|
||||
@@ -4763,6 +4854,21 @@ void populate_quest_metadata_from_script(
|
||||
meta.name = header.name.decode(language);
|
||||
meta.short_description = header.short_description.decode(language);
|
||||
meta.long_description = header.long_description.decode(language);
|
||||
// Quests saved with Qedit may not have the full header, so only parse
|
||||
// the full header if the code and function table offsets don't point to
|
||||
// space within it
|
||||
if ((header.code_offset >= sizeof(PSOQuestHeaderBB)) &&
|
||||
(header.function_table_offset >= sizeof(PSOQuestHeaderBB))) {
|
||||
r.go(0);
|
||||
const auto& header = r.get<PSOQuestHeaderBB>();
|
||||
for (size_t z = 0; z < header.create_item_mask_entries.size(); z++) {
|
||||
const auto& item = header.create_item_mask_entries[z];
|
||||
if (!item.is_valid()) {
|
||||
break;
|
||||
}
|
||||
meta.create_item_mask_entries.emplace_back(item);
|
||||
}
|
||||
}
|
||||
code_offset = header.code_offset;
|
||||
function_table_offset = header.function_table_offset;
|
||||
break;
|
||||
|
||||
+22
-2
@@ -83,7 +83,21 @@ struct PSOQuestHeaderGC {
|
||||
/* 01D4 */
|
||||
} __packed_ws__(PSOQuestHeaderGC, 0x1D4);
|
||||
|
||||
struct PSOQuestHeaderBB {
|
||||
struct CreateItemMaskEntry {
|
||||
parray<le_int32_t, 12> data1_fields;
|
||||
le_uint32_t present = 0;
|
||||
le_uint32_t unknown_a3 = 0;
|
||||
|
||||
bool is_valid() const {
|
||||
return (this->data1_fields[0] || this->data1_fields[1] || this->data1_fields[2]);
|
||||
}
|
||||
|
||||
CreateItemMaskEntry() = default;
|
||||
CreateItemMaskEntry(const QuestMetadata::CreateItemMask& mask);
|
||||
operator QuestMetadata::CreateItemMask() const;
|
||||
} __packed_ws__(CreateItemMaskEntry, 0x38);
|
||||
|
||||
struct PSOQuestHeaderBBBase {
|
||||
/* 0000 */ le_uint32_t code_offset = 0;
|
||||
/* 0004 */ le_uint32_t function_table_offset = 0;
|
||||
/* 0008 */ le_uint32_t size = 0;
|
||||
@@ -99,7 +113,13 @@ struct PSOQuestHeaderBB {
|
||||
/* 0058 */ pstring<TextEncoding::UTF16, 0x80> short_description;
|
||||
/* 0158 */ pstring<TextEncoding::UTF16, 0x120> long_description;
|
||||
/* 0398 */
|
||||
} __packed_ws__(PSOQuestHeaderBB, 0x398);
|
||||
} __packed_ws__(PSOQuestHeaderBBBase, 0x0398);
|
||||
|
||||
struct PSOQuestHeaderBB : PSOQuestHeaderBBBase {
|
||||
/* 0398 */ parray<uint8_t, 0x94> unknown_a5;
|
||||
/* 042C */ parray<CreateItemMaskEntry, 0x40> create_item_mask_entries;
|
||||
/* 122C */
|
||||
} __packed_ws__(PSOQuestHeaderBB, 0x122C);
|
||||
|
||||
void check_opcode_definitions();
|
||||
|
||||
|
||||
+101
-50
@@ -4182,18 +4182,43 @@ static asio::awaitable<void> on_adjust_player_meseta_bb(shared_ptr<Client> c, Su
|
||||
co_return;
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_item_reward_request_bb(shared_ptr<Client> c, SubcommandMessage& msg) {
|
||||
const auto& cmd = msg.check_size_t<G_ItemRewardRequest_BB_6xCA>();
|
||||
static void assert_quest_item_create_allowed(shared_ptr<const Lobby> l, const ItemData& item) {
|
||||
// We always enforce these restrictions, even if the client has cheat mode
|
||||
// enabled or has debug enabled. If the client can cheat, there are much
|
||||
// easier ways to create items (e.g. the $item chat command) than spoofing
|
||||
// these quest item creation commands, so they should just do that instead.
|
||||
|
||||
if (!l->quest) {
|
||||
throw std::runtime_error("cannot create quest reward item with no quest loaded");
|
||||
}
|
||||
for (const auto& mask : l->quest->meta.create_item_mask_entries) {
|
||||
if (mask.match(item)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
l->log.warning_f("Player attempted to create quest item {}, but it does not match any create item mask", item.hex());
|
||||
l->log.info_f("Quest has {} create item masks:", l->quest->meta.create_item_mask_entries.size());
|
||||
for (const auto& mask : l->quest->meta.create_item_mask_entries) {
|
||||
l->log.info_f(" {}", mask.str());
|
||||
}
|
||||
throw std::runtime_error("invalid item creation from quest");
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_quest_create_item_bb(shared_ptr<Client> c, SubcommandMessage& msg) {
|
||||
const auto& cmd = msg.check_size_t<G_QuestCreateItem_BB_6xCA>();
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
const auto& limits = *s->item_stack_limits(c->version());
|
||||
|
||||
ItemData item;
|
||||
item = cmd.item_data;
|
||||
// enforce_stack_size_limits must come after this assert since quests may
|
||||
// attempt to create stackable items with a count of zero
|
||||
assert_quest_item_create_allowed(l, item);
|
||||
item.enforce_stack_size_limits(limits);
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
|
||||
// The logic for the item_create and item_create2 opcodes (B3 and B4)
|
||||
// The logic for the item_create and item_create2 quest opcodes (B3 and B4)
|
||||
// includes a precondition check to see if the player can actually add the
|
||||
// item to their inventory or not, and the entire command is skipped if not.
|
||||
// However, on BB, the implementation performs this check and sends a 6xCA
|
||||
@@ -4869,21 +4894,21 @@ static asio::awaitable<void> on_quest_exchange_item_bb(shared_ptr<Client> c, Sub
|
||||
throw runtime_error("6xD5 command sent during free play");
|
||||
}
|
||||
|
||||
const auto& cmd = msg.check_size_t<G_ExchangeItemInQuest_BB_6xD5>();
|
||||
const auto& cmd = msg.check_size_t<G_QuestExchangeItem_BB_6xD5>();
|
||||
auto s = c->require_server_state();
|
||||
|
||||
try {
|
||||
auto p = c->character_file();
|
||||
const auto& limits = *s->item_stack_limits(c->version());
|
||||
|
||||
ItemData new_item = cmd.replace_item;
|
||||
assert_quest_item_create_allowed(l, new_item);
|
||||
new_item.enforce_stack_size_limits(limits);
|
||||
|
||||
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, limits);
|
||||
send_destroy_item_to_lobby(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.enforce_stack_size_limits(limits);
|
||||
new_item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(new_item, limits);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
@@ -4934,14 +4959,14 @@ static asio::awaitable<void> on_photon_drop_exchange_for_item_bb(shared_ptr<Clie
|
||||
auto p = c->character_file();
|
||||
const auto& limits = *s->item_stack_limits(c->version());
|
||||
|
||||
ItemData new_item = cmd.new_item;
|
||||
assert_quest_item_create_allowed(l, new_item);
|
||||
new_item.enforce_stack_size_limits(limits);
|
||||
|
||||
size_t found_index = p->inventory.find_item_by_primary_identifier(0x03100000);
|
||||
auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, limits);
|
||||
send_destroy_item_to_lobby(c, found_item.id, found_item.stack_size(limits));
|
||||
|
||||
// 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.enforce_stack_size_limits(limits);
|
||||
new_item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(new_item, limits);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
@@ -4975,9 +5000,12 @@ static asio::awaitable<void> on_photon_drop_exchange_for_s_rank_special_bb(share
|
||||
uint8_t cost = costs.at(cmd.special_type);
|
||||
|
||||
size_t payment_item_index = p->inventory.find_item_by_primary_identifier(0x03100000);
|
||||
// Ensure weapon exists before removing PDs, so inventory state will be
|
||||
// consistent in case of error
|
||||
p->inventory.find_item(cmd.item_id);
|
||||
{
|
||||
const auto& item = p->inventory.items[p->inventory.find_item(cmd.item_id)];
|
||||
if (!item.data.is_s_rank_weapon()) {
|
||||
throw std::runtime_error("6xD8 cannot be used for non-ES weapons");
|
||||
}
|
||||
}
|
||||
|
||||
auto payment_item = p->remove_item(p->inventory.items[payment_item_index].data.id, cost, limits);
|
||||
send_destroy_item_to_lobby(c, payment_item.id, cost);
|
||||
@@ -5008,24 +5036,61 @@ static asio::awaitable<void> on_secret_lottery_ticket_exchange_bb(shared_ptr<Cli
|
||||
if (!l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) && !l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
|
||||
throw runtime_error("6xDE command sent during free play");
|
||||
}
|
||||
|
||||
auto s = c->require_server_state();
|
||||
const auto& cmd = msg.check_size_t<G_ExchangeSecretLotteryTicket_BB_6xDE>();
|
||||
|
||||
if (s->secret_lottery_results.empty()) {
|
||||
throw runtime_error("no secret lottery results are defined");
|
||||
if (!l->quest) {
|
||||
throw runtime_error("6xDE command sent with no quest loaded");
|
||||
}
|
||||
if (l->quest->meta.create_item_mask_entries.size() < 2) {
|
||||
throw runtime_error("quest does not have enough create item mask entries");
|
||||
}
|
||||
|
||||
// See notes about 6xDE in CommandFormats.hh about this weirdness
|
||||
const auto& cmd = msg.check_size_t<G_ExchangeSecretLotteryTicket_Incomplete_BB_6xDE>(0x0C);
|
||||
uint16_t failure_label;
|
||||
if (msg.size >= 0x0C) {
|
||||
const auto& cmd = msg.check_size_t<G_ExchangeSecretLotteryTicket_BB_6xDE>();
|
||||
failure_label = cmd.failure_label;
|
||||
} else {
|
||||
failure_label = cmd.success_label;
|
||||
}
|
||||
|
||||
// The last mask entry is the currency item (e.g. Secret Lottery Ticket)
|
||||
const auto& currency_mask = l->quest->meta.create_item_mask_entries.back();
|
||||
uint32_t currency_primary_identifier = currency_mask.primary_identifier();
|
||||
auto p = c->character_file();
|
||||
ssize_t slt_index = -1;
|
||||
ssize_t currency_index = -1;
|
||||
try {
|
||||
slt_index = p->inventory.find_item_by_primary_identifier(0x03100300); // Secret Lottery Ticket
|
||||
currency_index = p->inventory.find_item_by_primary_identifier(currency_primary_identifier);
|
||||
c->log.info_f("Currency item {:08X} found at index {}", currency_primary_identifier, currency_index);
|
||||
} catch (const out_of_range&) {
|
||||
c->log.info_f("Currency item {:08X} not found in inventory", currency_primary_identifier);
|
||||
}
|
||||
|
||||
if (slt_index >= 0) {
|
||||
S_ExchangeSecretLotteryTicketResult_BB_24 out_cmd;
|
||||
out_cmd.start_reg_num = cmd.start_reg_num;
|
||||
out_cmd.label = (currency_index >= 0) ? cmd.success_label.load() : failure_label;
|
||||
for (size_t z = 0; z < out_cmd.reg_values.size(); z++) {
|
||||
out_cmd.reg_values[z] = (l->rand_crypt->next() % (l->quest->meta.create_item_mask_entries.size() - 1)) + 1;
|
||||
c->log.info_f("Mask index {} is {} ({})", z, out_cmd.reg_values[z] - 1, l->quest->meta.create_item_mask_entries[out_cmd.reg_values[z] - 1].str());
|
||||
}
|
||||
|
||||
if (currency_index >= 0) {
|
||||
size_t mask_index = out_cmd.reg_values[cmd.index - 1] - 1;
|
||||
const auto& mask = l->quest->meta.create_item_mask_entries[mask_index];
|
||||
c->log.info_f("Chose mask {} ({})", mask_index, mask.str());
|
||||
|
||||
ItemData item;
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
const auto& r = mask.data1_ranges[z];
|
||||
if (r.min != r.max) {
|
||||
throw std::runtime_error("invalid range for bb_exchange_slt");
|
||||
}
|
||||
item.data1[z] = r.min;
|
||||
}
|
||||
auto s = c->require_server_state();
|
||||
const auto& limits = *s->item_stack_limits(c->version());
|
||||
uint32_t slt_item_id = p->inventory.items[slt_index].data.id;
|
||||
item.enforce_stack_size_limits(limits);
|
||||
|
||||
uint32_t slt_item_id = p->inventory.items[currency_index].data.id;
|
||||
|
||||
G_ExchangeItemInQuest_BB_6xDB exchange_cmd;
|
||||
exchange_cmd.header.subcommand = 0xDB;
|
||||
@@ -5038,28 +5103,12 @@ static asio::awaitable<void> on_secret_lottery_ticket_exchange_bb(shared_ptr<Cli
|
||||
|
||||
p->remove_item(slt_item_id, 1, limits);
|
||||
|
||||
ItemData item = (s->secret_lottery_results.size() == 1)
|
||||
? s->secret_lottery_results[0]
|
||||
: s->secret_lottery_results[l->rand_crypt->next() % s->secret_lottery_results.size()];
|
||||
item.enforce_stack_size_limits(limits);
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(item, limits);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
}
|
||||
|
||||
S_ExchangeSecretLotteryTicketResult_BB_24 out_cmd;
|
||||
out_cmd.start_reg_num = cmd.index;
|
||||
out_cmd.label = cmd.success_label;
|
||||
if (s->secret_lottery_results.empty()) {
|
||||
out_cmd.reg_values.clear(0);
|
||||
} else if (s->secret_lottery_results.size() == 1) {
|
||||
out_cmd.reg_values.clear(1);
|
||||
} else {
|
||||
for (size_t z = 0; z < out_cmd.reg_values.size(); z++) {
|
||||
out_cmd.reg_values[z] = l->rand_crypt->next() % s->secret_lottery_results.size();
|
||||
}
|
||||
}
|
||||
send_command_t(c, 0x24, (slt_index >= 0) ? 0 : 1, out_cmd);
|
||||
send_command_t(c, 0x24, (currency_index >= 0) ? 0 : 1, out_cmd);
|
||||
co_return;
|
||||
}
|
||||
|
||||
@@ -5284,6 +5333,11 @@ static asio::awaitable<void> on_momoka_item_exchange_bb(shared_ptr<Client> c, Su
|
||||
auto p = c->character_file();
|
||||
try {
|
||||
const auto& limits = *s->item_stack_limits(c->version());
|
||||
|
||||
ItemData new_item = cmd.replace_item;
|
||||
assert_quest_item_create_allowed(l, new_item);
|
||||
new_item.enforce_stack_size_limits(limits);
|
||||
|
||||
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, limits);
|
||||
|
||||
@@ -5292,10 +5346,6 @@ static asio::awaitable<void> on_momoka_item_exchange_bb(shared_ptr<Client> c, Su
|
||||
|
||||
send_destroy_item_to_lobby(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.enforce_stack_size_limits(limits);
|
||||
new_item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(new_item, limits);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
@@ -5326,6 +5376,9 @@ static asio::awaitable<void> on_upgrade_weapon_attribute_bb(shared_ptr<Client> c
|
||||
try {
|
||||
size_t item_index = p->inventory.find_item(cmd.item_id);
|
||||
auto& item = p->inventory.items[item_index].data;
|
||||
if (item.is_s_rank_weapon()) {
|
||||
throw std::runtime_error("6xDA command sent for ES weapon");
|
||||
}
|
||||
|
||||
uint32_t payment_primary_identifier = cmd.payment_type ? 0x03100100 : 0x03100000;
|
||||
size_t payment_index = p->inventory.find_item_by_primary_identifier(payment_primary_identifier);
|
||||
@@ -5348,7 +5401,7 @@ static asio::awaitable<void> on_upgrade_weapon_attribute_bb(shared_ptr<Client> c
|
||||
}
|
||||
|
||||
size_t attribute_index = 0;
|
||||
for (size_t z = 6; z <= 10; z += 2) {
|
||||
for (size_t z = 6; z <= (item.has_kill_count() ? 10 : 8); z += 2) {
|
||||
if ((item.data1[z] == 0) || (!(item.data1[z] & 0x80) && (item.data1[z] == cmd.attribute))) {
|
||||
attribute_index = z;
|
||||
break;
|
||||
@@ -5588,7 +5641,7 @@ const vector<SubcommandDefinition> subcommand_definitions{
|
||||
/* 6xC7 */ {NONE, NONE, 0xC7, on_charge_attack_bb},
|
||||
/* 6xC8 */ {NONE, NONE, 0xC8, on_enemy_exp_request_bb},
|
||||
/* 6xC9 */ {NONE, NONE, 0xC9, on_adjust_player_meseta_bb},
|
||||
/* 6xCA */ {NONE, NONE, 0xCA, on_item_reward_request_bb},
|
||||
/* 6xCA */ {NONE, NONE, 0xCA, on_quest_create_item_bb},
|
||||
/* 6xCB */ {NONE, NONE, 0xCB, on_transfer_item_via_mail_message_bb},
|
||||
/* 6xCC */ {NONE, NONE, 0xCC, on_exchange_item_for_team_points_bb},
|
||||
/* 6xCD */ {NONE, NONE, 0xCD, forward_subcommand_m},
|
||||
@@ -5682,5 +5735,3 @@ asio::awaitable<void> on_subcommand_multi(shared_ptr<Client> c, Channel::Message
|
||||
offset += cmd_size;
|
||||
}
|
||||
}
|
||||
|
||||
// NOCOMMIT: Make BB item creation opcodes use the quests' create masks
|
||||
|
||||
@@ -1432,7 +1432,6 @@ void ServerState::load_config_late() {
|
||||
this->quest_F95F_results.clear();
|
||||
this->quest_F960_success_results.clear();
|
||||
this->quest_F960_failure_results = QuestF960Result();
|
||||
this->secret_lottery_results.clear();
|
||||
if (this->item_name_index(Version::BB_V4)) {
|
||||
try {
|
||||
for (const auto& type_it : this->config_json->get_list("QuestF95EResultItems")) {
|
||||
@@ -1469,16 +1468,6 @@ void ServerState::load_config_late() {
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
for (const auto& it : this->config_json->get_list("SecretLotteryResultItems")) {
|
||||
try {
|
||||
this->secret_lottery_results.emplace_back(this->parse_item_description(Version::BB_V4, it->as_string()));
|
||||
} catch (const exception& e) {
|
||||
config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", it->as_string(), e.what());
|
||||
}
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
auto parse_primary_identifier_list = [&](const char* key, Version v) -> unordered_set<uint32_t> {
|
||||
unordered_set<uint32_t> ret;
|
||||
|
||||
@@ -239,7 +239,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::vector<std::pair<size_t, ItemData>> quest_F95F_results; // [(num_photon_tickets, item)]
|
||||
std::vector<QuestF960Result> quest_F960_success_results;
|
||||
QuestF960Result quest_F960_failure_results;
|
||||
std::vector<ItemData> secret_lottery_results;
|
||||
float bb_global_exp_multiplier = 1.0f;
|
||||
float exp_share_multiplier = 0.5f;
|
||||
float server_global_drop_rate_multiplier = 1.0f;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
.meta name="Item exch. fix"
|
||||
.meta description="Fixes Momoka item exchange\nopcode"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksBB
|
||||
|
||||
|
||||
|
||||
# Fix 6xDE failure label truncation
|
||||
.data 0x006B90DE
|
||||
.data 1
|
||||
.binary 03
|
||||
|
||||
|
||||
|
||||
# Fix send_6xD9 not setting size field
|
||||
|
||||
.data 0x006CA540
|
||||
.deltaof send_6xD9_start, send_6xD9_end
|
||||
.address 0x006CA540
|
||||
send_6xD9_start: # [std](void* this @ ecx) -> void
|
||||
push ebx
|
||||
mov ebx, ecx
|
||||
push 0 # cmd.success_label, cmd.failure_label
|
||||
mov eax, [0x00A9C4F4] # local_client_id
|
||||
xor eax, 1
|
||||
push eax # cmd.token2
|
||||
mov ecx, [ebx + 0x2C]
|
||||
call 0x00737D90 # [std](void* this @ ecx = *(this + 0x2C)) -> void* @ eax
|
||||
mov edx, [ebx + 0x3C]
|
||||
imul eax, eax, 0x14
|
||||
add edx, eax
|
||||
mov eax, [edx + 0x10]
|
||||
xor eax, [0x00A9C4F4] # local_client_id
|
||||
push eax # cmd.token1
|
||||
push dword [edx + 0x10] # cmd.replace_item.data2d
|
||||
push dword [edx + 0x0C] # cmd.replace_item.id
|
||||
push dword [edx + 0x08] # cmd.replace_item.data1[8-11]
|
||||
push dword [edx + 0x04] # cmd.replace_item.data1[4-7]
|
||||
push dword [edx] # cmd.replace_item.data1[0-3]
|
||||
push dword [ebx + 0x50] # cmd.find_item.data2d
|
||||
push dword [ebx + 0x4C] # cmd.find_item.id
|
||||
push dword [ebx + 0x48] # cmd.find_item.data1[8-11]
|
||||
push dword [ebx + 0x44] # cmd.find_item.data1[4-7]
|
||||
push dword [ebx + 0x40] # cmd.find_item.data1[0-3]
|
||||
push 0x00000ED9 # cmd.header
|
||||
|
||||
mov ecx, esp
|
||||
call 0x008003E0 # send_and_handle_60[std](void* cmd @ ecx) -> void
|
||||
add esp, 0x38
|
||||
|
||||
mov dword [ebx], 6
|
||||
push 0
|
||||
call 0x00859D2D # time[std](void* t @ [esp + 4] = nullptr) -> uint32_t @ eax
|
||||
add esp, 4
|
||||
mov [ebx + 0x5C], eax
|
||||
|
||||
pop ebx
|
||||
ret
|
||||
send_6xD9_end:
|
||||
|
||||
|
||||
|
||||
# Same fix as above, but for quest_F95B_send_6xD9
|
||||
|
||||
.data 0x006B9018
|
||||
.deltaof quest_F95B_send_6xD9_start, quest_F95B_send_6xD9_end
|
||||
.address 0x006B9018
|
||||
quest_F95B_send_6xD9_start: # [std]() -> void
|
||||
mov edx, 0x00A954CC # quest_args_list
|
||||
mov ax, [edx + 0x14] # quest_args_list[5] (failure_label)
|
||||
shl eax, 0x10
|
||||
mov ax, [edx + 0x10] # quest_args_list[4] (success_label)
|
||||
push eax # cmd.success_label, cmd.failure_label
|
||||
mov ecx, [0x00A9C4F4] # local_client_id
|
||||
mov eax, [edx + 0x0C] # quest_args_list[3] (token2)
|
||||
xor eax, ecx
|
||||
push eax # cmd.token2
|
||||
mov eax, [edx + 0x08] # quest_args_list[2] (token1)
|
||||
xor eax, ecx
|
||||
push eax # cmd.token1
|
||||
push 0x00000000 # cmd.replace_item.data2d
|
||||
push 0xFFFFFFFF # cmd.replace_item.id
|
||||
push 0x00000000 # cmd.replace_item.data1[8-11]
|
||||
push 0x00000000 # cmd.replace_item.data1[4-7]
|
||||
mov eax, [edx + 0x04] # quest_args_list[1] (data1[0-2] in low 3 bytes)
|
||||
shl eax, 8
|
||||
bswap eax
|
||||
push eax # cmd.replace_item.data1[0-3]
|
||||
push 0x00000000 # cmd.find_item.data2d
|
||||
push 0xFFFFFFFF # cmd.find_item.id
|
||||
push 0x00000000 # cmd.find_item.data1[8-11]
|
||||
push 0x00000000 # cmd.find_item.data1[4-7]
|
||||
mov eax, [edx] # quest_args_list[0] (data1[0-2] in low 3 bytes)
|
||||
shl eax, 8
|
||||
bswap eax
|
||||
push eax # cmd.find_item.data1[0-3]
|
||||
mov eax, 0xD90E0000
|
||||
mov ax, cx
|
||||
bswap eax
|
||||
push eax # cmd.header
|
||||
|
||||
mov ecx, esp
|
||||
call 0x008003E0 # send_and_handle_60[std](void* cmd @ ecx) -> void
|
||||
add esp, 0x38
|
||||
ret
|
||||
quest_F95B_send_6xD9_end:
|
||||
|
||||
|
||||
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
@@ -747,16 +747,6 @@
|
||||
[15, "000A07"],
|
||||
[20, "010157"],
|
||||
],
|
||||
// Result item definitions for Secret Lottery Ticket exchange (quest opcode
|
||||
// F95C, used in the Good Luck quest).
|
||||
"SecretLotteryResultItems": [
|
||||
"000106", "000107", "000206", "000407", "000606", "000807", "000D01",
|
||||
"001300", "002000", "002700", "002C00", "003400", "003900", "003C00",
|
||||
"003E00", "004100", "004400", "004500", "004C00", "006A00", "008F07",
|
||||
"009A00", "01011B", "01011C", "010129", "010129", "010130", "010131",
|
||||
"010132", "010133", "010221", "010224", "010229", "01022B", "010235",
|
||||
"031000",
|
||||
],
|
||||
|
||||
// Result items for Coren (quest opcodes F960/F961). Indexed by prize_tier.
|
||||
// When a prize is requested, the server chooses a random number and checks it
|
||||
|
||||
Binary file not shown.
@@ -59,6 +59,18 @@
|
||||
// capability.
|
||||
// .joinable
|
||||
|
||||
// On BB, quests that create items via the script must specify which items are
|
||||
// allowed to be created. To do so, use this directive one or more times, which
|
||||
// instructs the server to allow creation of that item. These masks can specify
|
||||
// each byte of the item's data1 as ranges, to allow for parameters in the item
|
||||
// data. For example, this directive allows the quest to create Trifluids
|
||||
// (030102) with stack sizes of 1-10:
|
||||
// .allow_create_item 0301020000[01-0A]000000000000
|
||||
// Another example: this directive allows the quest to create any weapon in the
|
||||
// basic rifle series, not including the rares in that series, with a grind
|
||||
// value of up to 5 and up to two bonuses between 0 and 30% each:
|
||||
// .allow_create_item 0007[00-04][00-05]0000[00-05][00-1E][00-05][00-1E]0000
|
||||
|
||||
// The quest script begins after the header directives. A quest script is a
|
||||
// sequence of opcodes, and labels denoting positions within that sequence that
|
||||
// can be jumped to or called like a function. All labels have names, and some
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -354,14 +354,6 @@
|
||||
"Friday": ["Monomate x1", "Dimate x1", "Trimate x1", "Monofluid x1", "Difluid x1", "Trifluid x1", "Sol Atomizer x1", "Moon Atomizer x1", "Antidote x1", "Antiparalysis x1", "Telepipe x1", "Trap Vision x1"],
|
||||
"Saturday": ["Monomate x1", "Dimate x1", "Trimate x1", "Monofluid x1", "Difluid x1", "Trifluid x1", "Sol Atomizer x1", "Moon Atomizer x1", "Antidote x1", "Antiparalysis x1", "Telepipe x1", "Trap Vision x1"],
|
||||
},
|
||||
"SecretLotteryResultItems": [
|
||||
"000106", "000107", "000206", "000407", "000606", "000807", "000D01",
|
||||
"001300", "002000", "002700", "002C00", "003400", "003900", "003C00",
|
||||
"003E00", "004100", "004400", "004500", "004C00", "006A00", "008F07",
|
||||
"009A00", "01011B", "01011C", "010129", "010129", "010130", "010131",
|
||||
"010132", "010133", "010221", "010224", "010229", "01022B", "010235",
|
||||
"031000",
|
||||
],
|
||||
"BBGlobalEXPMultiplier": 1,
|
||||
"BBEXPShareMultiplier": 0.5,
|
||||
"ServerGlobalDropRateMultiplier": 1.0,
|
||||
|
||||
Reference in New Issue
Block a user