#include #include #include #include #include #include #include #include #include #include #include #include "middleware/content/packages/tables/definition_index_table.h" #include "middleware/content/packages/tables/quest_initialization_reader.h" #include "middleware/datagen/definitions.h" #include "middleware/datagen/family4/account/account_encoder.h" #include "middleware/datagen/family4/account/layout.h" #include "middleware/datagen/family4/character/character_encoder.h" #include "middleware/datagen/family4/character/layout.h" #include "middleware/datagen/family4/loadout/loadout_resolver.h" #include "server/bap/encrypted/queuez/queuez_state_validation.h" #include "state/build_data/cache/records/codec.h" #include "state/build_data/inventory/buckets/inventory_bucket_catalog.h" #include "state/build_data/items/details/item_detail_catalog.h" #include "state/build_data/items/item_catalog.h" #include "state/build_data/progressions/progression_catalog.h" #include "state/build_data/runtime/domain_markers.h" #include "state/build_data/socket_entry_lists/socket_entry_list_catalog.h" #include "state/investment/store_internal.h" #include "state/runtime/runtime.h" namespace fs = std::filesystem; namespace s = sunrise::state; namespace items = s::build_data::items; namespace tables = sunrise::middleware::content::packages::tables; namespace store = s::investment::store; using Quest = items::QuestInitialization; using Bytes = std::vector; std::string read_text(const fs::path& path) { std::ifstream file(path, std::ios::binary); assert(file.good()); return {std::istreambuf_iterator(file), {}}; } Bytes read_bytes(const fs::path& path) { const auto text = read_text(path); Bytes bytes(text.size()); std::memcpy(bytes.data(), text.data(), text.size()); return bytes; } // Source-level wiring guard, not an end-to-end world-reward delivery test. // Keep the real caller covered without requiring the game's compression DLL. void world_reward_caller_check(const fs::path& repository) { auto source = read_text(repository / "Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp"); std::erase_if(source, [](unsigned char c) { return std::isspace(c) != 0; }); const auto function = source.find("boolconsume_world_item_acquisition("); assert(function != std::string::npos); const auto call = source.find("queuez::stage_item_acquisition(", function); const auto end = source.find("acquisition))", call); assert(call != std::string::npos && end != std::string::npos); auto arguments = source.substr(call, end + std::strlen("acquisition))") - call); constexpr auto expected = "queuez::stage_item_acquisition(session.queuez,pending.accountSoid," "pending.characterSoid,pending.acquiredInstanceSoid," "pending.updates_account(),acquisition))"; assert(arguments == expected); // Negative control: restoring the old argument must fail this wiring check. const auto flag = arguments.find("pending.updates_account()"); arguments.replace(flag, std::strlen("pending.updates_account()"), "pending.profileChanged"); assert(arguments != expected); std::cout << "PASS world-reward caller source guard (old argument rejected)\n"; } void check_acquisition_staging(const s::PendingItemAcquisition& mutation, bool updatesAccount) { namespace queuez = sunrise::server::bap::encrypted::queuez; namespace datagen = sunrise::middleware::datagen; queuez::SessionState before{}; before.family4Active = true; before.family4RootSoid = mutation.accountSoid; before.family4ResidentCount = 2; before.family4Residents[0] = {mutation.accountSoid, datagen::kAccountObjectId}; before.family4Residents[1] = {mutation.characterSoid, datagen::kCharacterObjectId}; queuez::ItemAcquisition staged{}; assert(queuez::stage_item_acquisition(before, mutation.accountSoid, mutation.characterSoid, mutation.acquiredInstanceSoid, mutation.updates_account(), staged)); assert(staged.updatesAccount == updatesAccount); assert(staged.accountDefinitionId == datagen::kAccountObjectId); assert(staged.characterDefinitionId == datagen::kCharacterObjectId); assert(staged.itemInstanceDefinitionId == datagen::kItemInstanceObjectId); assert(staged.after.family4Version == before.family4Version + 1); assert(staged.after.family4ResidentCount == before.family4ResidentCount + 1); assert(staged.after.family4Residents[2].objectSoid == mutation.acquiredInstanceSoid); assert(before.family4Version == 0 && before.family4ResidentCount == 2); } template void put(Bytes& bytes, std::size_t at, T value) { assert(at <= bytes.size() && sizeof value <= bytes.size() - at); std::memcpy(bytes.data() + at, &value, sizeof value); } void block(Bytes& bytes, std::size_t field, std::size_t at, std::uint32_t cls) { put(bytes, field, static_cast(at) - static_cast(field)); put(bytes, at - 4, cls); } void array( Bytes& bytes, std::size_t field, std::size_t at, std::uint64_t count, std::uint32_t cls) { put(bytes, field, count); put(bytes, field + 8, static_cast(at) - static_cast(field + 8)); put(bytes, at - 4, std::uint32_t{0x80800000}); put(bytes, at, count); put(bytes, at + 8, cls); } Quest parser_checks() { Bytes item(800), map(256); put(item, 184, std::uint8_t{40}); block(item, 0x30, 260, 0x808077EB); put(item, 288, std::uint16_t{0}); array(item, 260, 500, 1, 0x808087B1); block(item, 0x60, 320, 0x808077C8); put(item, 336, std::uint16_t{7}); put(item, 348, std::uint8_t{1}); array(item, 320, 550, 2, 0x808077CA); put(item, 566, std::int32_t{-123}); put(item, 570, std::uint16_t{0}); put(item, 574, std::int32_t{42}); put(item, 578, std::uint16_t{1}); block(item, 0x90, 380, 0x808077AB); array(item, 380, 600, 1, 0x80807D4B); put(item, 616, std::uint16_t{11}); array(map, 8, 100, 1, 0x80800001); put(map, 120, std::int16_t{7}); const Quest expected{-123, 0, Quest::Scope::account}; const auto parse = [&](const Bytes& a, const Bytes& m) { return tables::items::read_quest_initialization(a, 0, a, 2, m); }; assert(parse(item, map) == expected); assert(items::initialized_value(expected, 0) == -123); for (auto before : {100, 200, -1, -1583618456}) { assert(items::initialized_value(expected, before) == before); } assert(!items::valid(Quest{100, 6200, Quest::Scope::account})); assert(!items::valid(Quest{100, 768, Quest::Scope::character})); auto bad = item; put(bad, 0x60, (std::numeric_limits::max)()); assert(parse(bad, map) == Quest{}); bad = item; put(bad, 578, std::uint16_t{0}); assert(parse(bad, map) == Quest{}); // Duplicate membership. bad = item; put(bad, 574, std::int32_t{-123}); assert(parse(bad, map) == Quest{}); // Ambiguous initial identifier. auto separate = item; put(separate, 288, std::uint16_t{1}); put(separate, 0x60, std::int64_t{0}); assert(tables::items::read_quest_initialization(separate, 0, item, 2, map) == expected); bad = item; put(bad, 348, std::uint8_t{0}); assert(parse(bad, map) == Quest{}); bad = item; put(bad, 0x90, std::int64_t{0}); assert(parse(bad, map) == Quest{}); bad = item; bad.resize(240); assert(parse(bad, map) == Quest{}); for (auto value : {0, -1}) { bad = item; put(bad, 566, value); assert(parse(bad, map) == Quest{}); } assert(tables::items::read_quest_initialization(item, 1, item, 2, map) == Quest{}); auto duplicateMap = map; array(duplicateMap, 24, 160, 1, 0x80800001); put(duplicateMap, 180, std::int16_t{7}); assert(parse(item, duplicateMap) == Quest{}); auto characterMap = map; put(characterMap, 8, std::uint64_t{0}); put(characterMap, 16, std::int64_t{0}); array(characterMap, 24, 160, 1, 0x80800001); put(characterMap, 180, std::int16_t{7}); assert((parse(item, characterMap) == Quest{-123, 0, Quest::Scope::character})); auto contextMap = map; put(contextMap, 8, std::uint64_t{0}); put(contextMap, 16, std::int64_t{0}); array(contextMap, 40, 160, 1, 0x80800001); put(contextMap, 180, std::int16_t{7}); assert(parse(item, contextMap) == Quest{}); auto root = item; put(root, 184, std::uint8_t{37}); put(root, 0x30, std::int64_t{0}); put(root, 566, std::int32_t{-1583618456}); auto noFlags = separate; put(noFlags, 380, std::uint64_t{0}); put(noFlags, 388, std::int64_t{0}); Bytes rootMap(3800); array(rootMap, 24, 100, 443, 0x80800001); put(rootMap, 116 + 442 * 8 + 4, std::int16_t{7}); const auto rootParse = [&](const Bytes& a, const Bytes& p) { return tables::items::read_quest_initialization(a, 0, p, 2, rootMap); }; const Quest rootQuest{-1583618456, 442, Quest::Scope::character}; assert(rootParse(noFlags, root) == rootQuest); auto absent = noFlags; put(absent, 0x90, std::int64_t{0}); assert(rootParse(absent, root) == rootQuest); auto malformed = noFlags; put(malformed, 0x90, (std::numeric_limits::max)()); assert(rootParse(malformed, root) == Quest{}); malformed = noFlags; put(malformed, 380, std::uint64_t{1}); // Nonempty flags need a valid array. assert(rootParse(malformed, root) == Quest{}); auto wrongRoot = root; put(wrongRoot, 184, std::uint8_t{40}); assert(rootParse(noFlags, wrongRoot) == Quest{}); wrongRoot = root; put(wrongRoot, 0x30, std::int64_t{212}); assert(rootParse(noFlags, wrongRoot) == Quest{}); wrongRoot = root; put(wrongRoot, 570, std::uint16_t{1}); put(wrongRoot, 578, std::uint16_t{0}); // Later members stay unsupported. assert(rootParse(noFlags, wrongRoot) == Quest{}); assert(tables::items::read_quest_initialization(noFlags, 0, root, 2, map) == Quest{}); std::cout << "PASS synthetic parser, signed values, bounds, ambiguity and preservation\n"; std::cout << "PASS separate-root first step: empty/absent flags, malformed inputs, " "wrong root/scope and later-step rejection\n"; return rootQuest; } void content_checks(const fs::path& root) { struct Row { std::uint32_t hash{}, tag{}; unsigned bucket{}; bool objective{}, set{}; }; std::vector rows; std::istringstream input(read_text(root / "items.tsv")); std::string line; std::getline(input, line); while (std::getline(input, line)) { std::istringstream fields(line); std::size_t index = 0; Row row; fields >> index >> row.hash >> row.tag >> row.bucket >> row.objective >> row.set; assert(fields && index == rows.size()); rows.push_back(row); } auto blob = [&](std::size_t index) { std::ostringstream filename; filename << std::uppercase << std::hex << rows.at(index).tag << ".bin"; return read_bytes(root / filename.str()); }; const auto map = read_bytes(root / "81319320.bin"); std::vector quests(rows.size()); std::size_t account = 0, character = 0; for (std::size_t i = 0; i < rows.size(); ++i) { if (rows[i].bucket != 40 || !rows[i].objective) { continue; } const auto item = blob(i); const auto parent = tables::items::quest_parent(item); if (parent >= rows.size()) { continue; } quests[i] = tables::items::read_quest_initialization( item, static_cast(i), blob(parent), rows.size(), map); account += quests[i].scope == Quest::Scope::account; character += quests[i].scope == Quest::Scope::character; } assert((quests.at(15282) == Quest{100, 5762, Quest::Scope::account})); assert((quests.at(14844) == Quest{100, 442, Quest::Scope::character})); assert((quests.at(13138) == Quest{-1583618456, 162, Quest::Scope::character})); for (auto index : {15U, 13139U, 13140U, 13141U, 13142U, 13143U, 14845U, 15283U}) { assert(quests.at(index) == Quest{}); } assert(account == 50 && character >= 133); std::cout << "PASS installed content: " << account << " account + " << character << " character first steps (structural coverage, not gameplay certification)\n"; } void cache_checks() { namespace cache = s::build_data::cache::records; items::Definition item{}; item.definitionHash = 123; item.bucketId = 40; item.questInitialization = {-123, 7, Quest::Scope::character}; cache::ItemRecord record{}; items::Definition decoded{}; assert(cache::encode(item, record) && cache::decode(record, decoded)); assert(decoded.questInitialization == item.questInitialization); record.questValueScope = 255; assert(!cache::decode(record, decoded)); std::cout << "PASS cache round trip and invalid scope rejection\n"; } void runtime_checks(const fs::path& repo, const Quest& characterQuest) { const auto resources = repo / "Sunrise/resources/database"; assert(store::open(":memory:", read_text(resources / "investment_schema.sql"), read_text(resources / "investment_defaults.sql"), read_text(resources / "account_settings_schema.sql"), read_text(resources / "account_settings_defaults.sql"))); auto baseline = store::account(); baseline.profileItems = {}; baseline.profileItemCount = 0; for (std::size_t i = 0; i < baseline.characterCount; ++i) { auto& character = baseline.characters[i]; character.equipment = {}; character.inventory = {}; character.stacks = {}; character.selected = i == 0; } assert(baseline.characterCount >= 2); assert(store::write_account(baseline)); std::array definitions{}; std::array details{}; for (std::uint16_t i = 0; i < definitions.size(); ++i) { definitions[i].definitionIndex = i; definitions[i].definitionHash = 1000U + i; definitions[i].bucketId = 40; details[i].definitionIndex = i; details[i].definitionHash = 1000U + i; details[i].bucketId = 40; details[i].maxStackSize = 1; details[i].instancedDefinitionState = items::details::InstancedDefinitionState::instanced; } definitions[0].questInitialization = {100, 5762, Quest::Scope::account}; definitions[1].questInitialization = characterQuest; // The existing character encoder independently requires these four legacy prerequisites. constexpr std::array legacyQuests{ 0x57C4540AU, 0x85CC476EU, 0xB099029AU, 0xC3535D63U}; for (std::size_t i = 0; i < legacyQuests.size(); ++i) { definitions[i + 3].definitionHash = details[i + 3].definitionHash = legacyQuests[i]; details[i + 3].instancedDefinitionState = items::details::InstancedDefinitionState::stackable; } assert(items::replace(definitions)); assert(items::details::replace(details)); s::build_data::runtime::details::publish(); namespace buckets = s::build_data::inventory::buckets; const std::array bucketRows{ {{0, buckets::ArraySelector::character, 0, 1, 0}, {40, buckets::ArraySelector::character, 1, 349}}}; assert(buckets::replace(bucketRows)); const s::build_data::socket_entry_lists::Definition sockets{1, 0, 0, 0}; assert(s::build_data::socket_entry_lists::replace({&sockets, 1})); const s::build_data::progressions::Definition progression{}; assert(s::build_data::progressions::replace({&progression, 1}, {})); const auto value = [](store::Bank bank, std::uint16_t row) { std::int32_t result = 0; assert(store::read_unlock(bank, row, result)); return result; }; const auto reset = [&] { assert(store::write_account(baseline)); assert(store::execute("DELETE FROM unlocks")); }; s::PendingItemAcquisition mutation{}; s::AccountState after{}; s::unlocks::Table unlocks{}; const auto check_encoded = [&](bool accountScope, std::uint16_t row, std::int32_t expected) { namespace family4 = sunrise::middleware::datagen::family4; std::int32_t sent = 0; if (accountScope) { Bytes encoded(family4::account::layout::kObjectSize); assert(family4::account::encode(after, encoded, unlocks)); std::memcpy(&sent, encoded.data() + offsetof(family4::account::layout::Object, objectiveValues) + row * sizeof sent, sizeof sent); } else { Bytes encoded(family4::character::layout::kObjectSize); family4::loadout::ResolvedLoadout resolved{}; assert(family4::loadout::resolve(after, 0, resolved)); s::equipment::light::Evaluation light{}; light.divisor = 1; assert( family4::character::encode(after.characters[0], resolved, light, encoded, unlocks)); std::memcpy(&sent, encoded.data() + offsetof(family4::character::layout::Object, objectiveValues) + row * sizeof sent, sizeof sent); } assert(sent == expected); }; reset(); assert(s::prepare_item_acquisition_for_item(0, mutation)); assert(mutation.updates_account()); assert(!mutation.profileChanged); // The old world-reward flag misses this account write. check_acquisition_staging(mutation, true); assert(s::preview_item_acquisition(mutation, after, unlocks)); assert(unlocks.objectiveValues[5762] == 100); check_encoded(true, 5762, 100); assert(value(store::Bank::objectiveValues, 5762) == 0); assert(store::account().characters[0].inventory.count == 0); assert(s::commit_item_acquisition(mutation) && !mutation.prepared); assert(value(store::Bank::objectiveValues, 5762) == 100); assert(store::account().characters[0].inventory.count == 1); for (auto previous : {100, 200, -1, -1583618456}) { reset(); assert(store::write_unlock(store::Bank::objectiveValues, 5762, previous)); assert(store::write_unlock(store::Bank::objectiveValues, 5763, 4)); assert(s::prepare_item_acquisition_for_item(0, mutation)); assert(!mutation.updates_account()); check_acquisition_staging(mutation, false); assert(s::commit_item_acquisition(mutation)); assert(value(store::Bank::objectiveValues, 5762) == previous); assert(value(store::Bank::objectiveValues, 5763) == 4); } reset(); auto otherCharacter = baseline; otherCharacter.characters[0].selected = false; otherCharacter.characters[1].selected = true; assert(store::write_account(otherCharacter)); assert(store::write_unlock(store::Bank::objectiveValues, 5762, 200)); assert(s::prepare_item_acquisition( s::build_data::collectibles::kNoCollectibleIndex, definitions[0].definitionHash, mutation)); assert(s::commit_item_acquisition(mutation)); assert(value(store::Bank::objectiveValues, 5762) == 200); assert(store::account().characters[1].inventory.count == 1); reset(); assert(s::prepare_item_acquisition_for_item(1, mutation)); assert(!mutation.updates_account()); check_acquisition_staging(mutation, false); assert(s::preview_item_acquisition(mutation, after, unlocks)); assert(unlocks.characterObjectValues[442] == characterQuest.value); check_encoded(false, 442, characterQuest.value); assert(value(store::Bank::characterObjectValues, 442) == 0); assert(s::commit_item_acquisition(mutation)); assert(value(store::Bank::characterObjectValues, 442) == characterQuest.value); assert(store::read_unlocks(unlocks, 1)); assert(unlocks.characterObjectValues[442] == 0); for (auto previous : {characterQuest.value, 790208398, -1}) { reset(); assert(store::write_unlock(store::Bank::characterObjectValues, 442, previous)); assert(s::prepare_item_acquisition_for_item(1, mutation)); assert(s::preview_item_acquisition(mutation, after, unlocks)); check_encoded(false, 442, previous); assert(s::commit_item_acquisition(mutation)); assert(value(store::Bank::characterObjectValues, 442) == previous); } reset(); assert(s::prepare_item_acquisition_for_item(0, mutation)); assert(store::write_unlock(store::Bank::objectiveValues, 5762, 200)); assert(!s::commit_item_acquisition(mutation)); assert(store::account().characters[0].inventory.count == 0); assert(value(store::Bank::objectiveValues, 5762) == 200); reset(); assert(s::prepare_item_acquisition_for_item(1, mutation)); auto changed = baseline; changed.characters[0].selected = false; changed.characters[1].selected = true; assert(store::write_account(changed)); assert(!s::commit_item_acquisition(mutation)); assert(value(store::Bank::characterObjectValues, 442) == 0); reset(); assert(s::prepare_item_acquisition_for_item(0, mutation)); mutation.questInitialization.row = 5763; // A changed contract cannot redirect the write. assert(!s::commit_item_acquisition(mutation)); assert(store::account().characters[0].inventory.count == 0); reset(); assert(s::prepare_item_acquisition_for_item(0, mutation)); assert(store::execute("CREATE TEMP TRIGGER fail_item BEFORE INSERT ON items " "BEGIN SELECT RAISE(ABORT,'injected inventory failure'); END;")); assert(!s::commit_item_acquisition(mutation)); assert(store::execute("DROP TRIGGER fail_item")); assert(store::account().characters[0].inventory.count == 0); assert(value(store::Bank::objectiveValues, 5762) == 0); reset(); auto full = baseline; auto& inventory = full.characters[0].inventory; for (std::size_t i = 0; i < inventory.values.size(); ++i) { auto& item = inventory.values[i]; item.instanceSoid = 0x4000000000000001ULL + i; item.definitionHash = definitions[2].definitionHash; item.quantity = 1; } inventory.count = inventory.values.size(); assert(store::write_account(full)); assert(!s::prepare_item_acquisition_for_item(0, mutation)); assert(!mutation.prepared && value(store::Bank::objectiveValues, 5762) == 0); reset(); assert(!s::prepare_item_acquisition_for_item(65535, mutation)); assert(!mutation.prepared && value(store::Bank::objectiveValues, 5762) == 0); reset(); assert(s::prepare_item_acquisition_for_item(0, mutation)); assert(store::execute("CREATE TEMP TRIGGER fail_quest BEFORE INSERT ON unlocks " "BEGIN SELECT RAISE(ABORT,'injected quest write failure'); END;")); assert(!s::commit_item_acquisition(mutation)); assert(store::execute("DROP TRIGGER fail_quest")); assert(store::account().characters[0].inventory.count == 0); assert(value(store::Bank::objectiveValues, 5762) == 0); reset(); assert(s::prepare_item_acquisition_for_item(2, mutation)); assert(mutation.questInitialization == Quest{}); check_acquisition_staging(mutation, false); auto profileMutation = mutation; profileMutation.profileChanged = true; check_acquisition_staging(profileMutation, true); // Existing profile-charge behavior. assert(s::commit_item_acquisition(mutation)); assert(value(store::Bank::objectiveValues, 5762) == 0); assert(value(store::Bank::characterObjectValues, 442) == 0); store::shutdown(); std::cout << "PASS production queuez staging: fresh account/character quests, existing " "progress, ordinary item, profile charge\n"; std::cout << "PASS production acquisition + SQLite: atomic rollback, stale value/character, " "both scopes, preservation, encoded after-image, full/refused grants, unsupported " "control\n"; } int main(int argc, char** argv) { std::cout << std::unitbuf; assert(argc == 2 || argc == 3); const auto characterQuest = parser_checks(); cache_checks(); world_reward_caller_check(argv[1]); runtime_checks(argv[1], characterQuest); if (argc == 3) { content_checks(argv[2]); } }