quest_initialization_test.cpp 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. #include <algorithm>
  2. #include <cassert>
  3. #include <cctype>
  4. #include <cstring>
  5. #include <filesystem>
  6. #include <fstream>
  7. #include <iostream>
  8. #include <iterator>
  9. #include <limits>
  10. #include <sstream>
  11. #include <vector>
  12. #include "middleware/content/packages/tables/definition_index_table.h"
  13. #include "middleware/content/packages/tables/quest_initialization_reader.h"
  14. #include "middleware/datagen/definitions.h"
  15. #include "middleware/datagen/family4/account/account_encoder.h"
  16. #include "middleware/datagen/family4/account/layout.h"
  17. #include "middleware/datagen/family4/character/character_encoder.h"
  18. #include "middleware/datagen/family4/character/layout.h"
  19. #include "middleware/datagen/family4/loadout/loadout_resolver.h"
  20. #include "server/bap/encrypted/queuez/queuez_state_validation.h"
  21. #include "state/build_data/cache/records/codec.h"
  22. #include "state/build_data/inventory/buckets/inventory_bucket_catalog.h"
  23. #include "state/build_data/items/details/item_detail_catalog.h"
  24. #include "state/build_data/items/item_catalog.h"
  25. #include "state/build_data/progressions/progression_catalog.h"
  26. #include "state/build_data/runtime/domain_markers.h"
  27. #include "state/build_data/socket_entry_lists/socket_entry_list_catalog.h"
  28. #include "state/investment/store_internal.h"
  29. #include "state/runtime/runtime.h"
  30. namespace fs = std::filesystem;
  31. namespace s = sunrise::state;
  32. namespace items = s::build_data::items;
  33. namespace tables = sunrise::middleware::content::packages::tables;
  34. namespace store = s::investment::store;
  35. using Quest = items::QuestInitialization;
  36. using Bytes = std::vector<std::byte>;
  37. std::string read_text(const fs::path& path) {
  38. std::ifstream file(path, std::ios::binary);
  39. assert(file.good());
  40. return {std::istreambuf_iterator<char>(file), {}};
  41. }
  42. Bytes read_bytes(const fs::path& path) {
  43. const auto text = read_text(path);
  44. Bytes bytes(text.size());
  45. std::memcpy(bytes.data(), text.data(), text.size());
  46. return bytes;
  47. }
  48. // Source-level wiring guard, not an end-to-end world-reward delivery test.
  49. // Keep the real caller covered without requiring the game's compression DLL.
  50. void world_reward_caller_check(const fs::path& repository) {
  51. auto source =
  52. read_text(repository / "Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp");
  53. std::erase_if(source, [](unsigned char c) { return std::isspace(c) != 0; });
  54. const auto function = source.find("boolconsume_world_item_acquisition(");
  55. assert(function != std::string::npos);
  56. const auto call = source.find("queuez::stage_item_acquisition(", function);
  57. const auto end = source.find("acquisition))", call);
  58. assert(call != std::string::npos && end != std::string::npos);
  59. auto arguments = source.substr(call, end + std::strlen("acquisition))") - call);
  60. constexpr auto expected = "queuez::stage_item_acquisition(session.queuez,pending.accountSoid,"
  61. "pending.characterSoid,pending.acquiredInstanceSoid,"
  62. "pending.updates_account(),acquisition))";
  63. assert(arguments == expected);
  64. // Negative control: restoring the old argument must fail this wiring check.
  65. const auto flag = arguments.find("pending.updates_account()");
  66. arguments.replace(flag, std::strlen("pending.updates_account()"), "pending.profileChanged");
  67. assert(arguments != expected);
  68. std::cout << "PASS world-reward caller source guard (old argument rejected)\n";
  69. }
  70. void check_acquisition_staging(const s::PendingItemAcquisition& mutation, bool updatesAccount) {
  71. namespace queuez = sunrise::server::bap::encrypted::queuez;
  72. namespace datagen = sunrise::middleware::datagen;
  73. queuez::SessionState before{};
  74. before.family4Active = true;
  75. before.family4RootSoid = mutation.accountSoid;
  76. before.family4ResidentCount = 2;
  77. before.family4Residents[0] = {mutation.accountSoid, datagen::kAccountObjectId};
  78. before.family4Residents[1] = {mutation.characterSoid, datagen::kCharacterObjectId};
  79. queuez::ItemAcquisition staged{};
  80. assert(queuez::stage_item_acquisition(before,
  81. mutation.accountSoid,
  82. mutation.characterSoid,
  83. mutation.acquiredInstanceSoid,
  84. mutation.updates_account(),
  85. staged));
  86. assert(staged.updatesAccount == updatesAccount);
  87. assert(staged.accountDefinitionId == datagen::kAccountObjectId);
  88. assert(staged.characterDefinitionId == datagen::kCharacterObjectId);
  89. assert(staged.itemInstanceDefinitionId == datagen::kItemInstanceObjectId);
  90. assert(staged.after.family4Version == before.family4Version + 1);
  91. assert(staged.after.family4ResidentCount == before.family4ResidentCount + 1);
  92. assert(staged.after.family4Residents[2].objectSoid == mutation.acquiredInstanceSoid);
  93. assert(before.family4Version == 0 && before.family4ResidentCount == 2);
  94. }
  95. template <class T> void put(Bytes& bytes, std::size_t at, T value) {
  96. assert(at <= bytes.size() && sizeof value <= bytes.size() - at);
  97. std::memcpy(bytes.data() + at, &value, sizeof value);
  98. }
  99. void block(Bytes& bytes, std::size_t field, std::size_t at, std::uint32_t cls) {
  100. put(bytes, field, static_cast<std::int64_t>(at) - static_cast<std::int64_t>(field));
  101. put(bytes, at - 4, cls);
  102. }
  103. void array(
  104. Bytes& bytes, std::size_t field, std::size_t at, std::uint64_t count, std::uint32_t cls) {
  105. put(bytes, field, count);
  106. put(bytes, field + 8, static_cast<std::int64_t>(at) - static_cast<std::int64_t>(field + 8));
  107. put(bytes, at - 4, std::uint32_t{0x80800000});
  108. put(bytes, at, count);
  109. put(bytes, at + 8, cls);
  110. }
  111. Quest parser_checks() {
  112. Bytes item(800), map(256);
  113. put(item, 184, std::uint8_t{40});
  114. block(item, 0x30, 260, 0x808077EB);
  115. put(item, 288, std::uint16_t{0});
  116. array(item, 260, 500, 1, 0x808087B1);
  117. block(item, 0x60, 320, 0x808077C8);
  118. put(item, 336, std::uint16_t{7});
  119. put(item, 348, std::uint8_t{1});
  120. array(item, 320, 550, 2, 0x808077CA);
  121. put(item, 566, std::int32_t{-123});
  122. put(item, 570, std::uint16_t{0});
  123. put(item, 574, std::int32_t{42});
  124. put(item, 578, std::uint16_t{1});
  125. block(item, 0x90, 380, 0x808077AB);
  126. array(item, 380, 600, 1, 0x80807D4B);
  127. put(item, 616, std::uint16_t{11});
  128. array(map, 8, 100, 1, 0x80800001);
  129. put(map, 120, std::int16_t{7});
  130. const Quest expected{-123, 0, Quest::Scope::account};
  131. const auto parse = [&](const Bytes& a, const Bytes& m) {
  132. return tables::items::read_quest_initialization(a, 0, a, 2, m);
  133. };
  134. assert(parse(item, map) == expected);
  135. assert(items::initialized_value(expected, 0) == -123);
  136. for (auto before : {100, 200, -1, -1583618456}) {
  137. assert(items::initialized_value(expected, before) == before);
  138. }
  139. assert(!items::valid(Quest{100, 6200, Quest::Scope::account}));
  140. assert(!items::valid(Quest{100, 768, Quest::Scope::character}));
  141. auto bad = item;
  142. put(bad, 0x60, (std::numeric_limits<std::int64_t>::max)());
  143. assert(parse(bad, map) == Quest{});
  144. bad = item;
  145. put(bad, 578, std::uint16_t{0});
  146. assert(parse(bad, map) == Quest{}); // Duplicate membership.
  147. bad = item;
  148. put(bad, 574, std::int32_t{-123});
  149. assert(parse(bad, map) == Quest{}); // Ambiguous initial identifier.
  150. auto separate = item;
  151. put(separate, 288, std::uint16_t{1});
  152. put(separate, 0x60, std::int64_t{0});
  153. assert(tables::items::read_quest_initialization(separate, 0, item, 2, map) == expected);
  154. bad = item;
  155. put(bad, 348, std::uint8_t{0});
  156. assert(parse(bad, map) == Quest{});
  157. bad = item;
  158. put(bad, 0x90, std::int64_t{0});
  159. assert(parse(bad, map) == Quest{});
  160. bad = item;
  161. bad.resize(240);
  162. assert(parse(bad, map) == Quest{});
  163. for (auto value : {0, -1}) {
  164. bad = item;
  165. put(bad, 566, value);
  166. assert(parse(bad, map) == Quest{});
  167. }
  168. assert(tables::items::read_quest_initialization(item, 1, item, 2, map) == Quest{});
  169. auto duplicateMap = map;
  170. array(duplicateMap, 24, 160, 1, 0x80800001);
  171. put(duplicateMap, 180, std::int16_t{7});
  172. assert(parse(item, duplicateMap) == Quest{});
  173. auto characterMap = map;
  174. put(characterMap, 8, std::uint64_t{0});
  175. put(characterMap, 16, std::int64_t{0});
  176. array(characterMap, 24, 160, 1, 0x80800001);
  177. put(characterMap, 180, std::int16_t{7});
  178. assert((parse(item, characterMap) == Quest{-123, 0, Quest::Scope::character}));
  179. auto contextMap = map;
  180. put(contextMap, 8, std::uint64_t{0});
  181. put(contextMap, 16, std::int64_t{0});
  182. array(contextMap, 40, 160, 1, 0x80800001);
  183. put(contextMap, 180, std::int16_t{7});
  184. assert(parse(item, contextMap) == Quest{});
  185. auto root = item;
  186. put(root, 184, std::uint8_t{37});
  187. put(root, 0x30, std::int64_t{0});
  188. put(root, 566, std::int32_t{-1583618456});
  189. auto noFlags = separate;
  190. put(noFlags, 380, std::uint64_t{0});
  191. put(noFlags, 388, std::int64_t{0});
  192. Bytes rootMap(3800);
  193. array(rootMap, 24, 100, 443, 0x80800001);
  194. put(rootMap, 116 + 442 * 8 + 4, std::int16_t{7});
  195. const auto rootParse = [&](const Bytes& a, const Bytes& p) {
  196. return tables::items::read_quest_initialization(a, 0, p, 2, rootMap);
  197. };
  198. const Quest rootQuest{-1583618456, 442, Quest::Scope::character};
  199. assert(rootParse(noFlags, root) == rootQuest);
  200. auto absent = noFlags;
  201. put(absent, 0x90, std::int64_t{0});
  202. assert(rootParse(absent, root) == rootQuest);
  203. auto malformed = noFlags;
  204. put(malformed, 0x90, (std::numeric_limits<std::int64_t>::max)());
  205. assert(rootParse(malformed, root) == Quest{});
  206. malformed = noFlags;
  207. put(malformed, 380, std::uint64_t{1}); // Nonempty flags need a valid array.
  208. assert(rootParse(malformed, root) == Quest{});
  209. auto wrongRoot = root;
  210. put(wrongRoot, 184, std::uint8_t{40});
  211. assert(rootParse(noFlags, wrongRoot) == Quest{});
  212. wrongRoot = root;
  213. put(wrongRoot, 0x30, std::int64_t{212});
  214. assert(rootParse(noFlags, wrongRoot) == Quest{});
  215. wrongRoot = root;
  216. put(wrongRoot, 570, std::uint16_t{1});
  217. put(wrongRoot, 578, std::uint16_t{0}); // Later members stay unsupported.
  218. assert(rootParse(noFlags, wrongRoot) == Quest{});
  219. assert(tables::items::read_quest_initialization(noFlags, 0, root, 2, map) == Quest{});
  220. std::cout << "PASS synthetic parser, signed values, bounds, ambiguity and preservation\n";
  221. std::cout << "PASS separate-root first step: empty/absent flags, malformed inputs, "
  222. "wrong root/scope and later-step rejection\n";
  223. return rootQuest;
  224. }
  225. void content_checks(const fs::path& root) {
  226. struct Row {
  227. std::uint32_t hash{}, tag{};
  228. unsigned bucket{};
  229. bool objective{}, set{};
  230. };
  231. std::vector<Row> rows;
  232. std::istringstream input(read_text(root / "items.tsv"));
  233. std::string line;
  234. std::getline(input, line);
  235. while (std::getline(input, line)) {
  236. std::istringstream fields(line);
  237. std::size_t index = 0;
  238. Row row;
  239. fields >> index >> row.hash >> row.tag >> row.bucket >> row.objective >> row.set;
  240. assert(fields && index == rows.size());
  241. rows.push_back(row);
  242. }
  243. auto blob = [&](std::size_t index) {
  244. std::ostringstream filename;
  245. filename << std::uppercase << std::hex << rows.at(index).tag << ".bin";
  246. return read_bytes(root / filename.str());
  247. };
  248. const auto map = read_bytes(root / "81319320.bin");
  249. std::vector<Quest> quests(rows.size());
  250. std::size_t account = 0, character = 0;
  251. for (std::size_t i = 0; i < rows.size(); ++i) {
  252. if (rows[i].bucket != 40 || !rows[i].objective) {
  253. continue;
  254. }
  255. const auto item = blob(i);
  256. const auto parent = tables::items::quest_parent(item);
  257. if (parent >= rows.size()) {
  258. continue;
  259. }
  260. quests[i] = tables::items::read_quest_initialization(
  261. item, static_cast<std::uint16_t>(i), blob(parent), rows.size(), map);
  262. account += quests[i].scope == Quest::Scope::account;
  263. character += quests[i].scope == Quest::Scope::character;
  264. }
  265. assert((quests.at(15282) == Quest{100, 5762, Quest::Scope::account}));
  266. assert((quests.at(14844) == Quest{100, 442, Quest::Scope::character}));
  267. assert((quests.at(13138) == Quest{-1583618456, 162, Quest::Scope::character}));
  268. for (auto index : {15U, 13139U, 13140U, 13141U, 13142U, 13143U, 14845U, 15283U}) {
  269. assert(quests.at(index) == Quest{});
  270. }
  271. assert(account == 50 && character >= 133);
  272. std::cout << "PASS installed content: " << account << " account + " << character
  273. << " character first steps (structural coverage, not gameplay certification)\n";
  274. }
  275. void cache_checks() {
  276. namespace cache = s::build_data::cache::records;
  277. items::Definition item{};
  278. item.definitionHash = 123;
  279. item.bucketId = 40;
  280. item.questInitialization = {-123, 7, Quest::Scope::character};
  281. cache::ItemRecord record{};
  282. items::Definition decoded{};
  283. assert(cache::encode(item, record) && cache::decode(record, decoded));
  284. assert(decoded.questInitialization == item.questInitialization);
  285. record.questValueScope = 255;
  286. assert(!cache::decode(record, decoded));
  287. std::cout << "PASS cache round trip and invalid scope rejection\n";
  288. }
  289. void runtime_checks(const fs::path& repo, const Quest& characterQuest) {
  290. const auto resources = repo / "Sunrise/resources/database";
  291. assert(store::open(":memory:",
  292. read_text(resources / "investment_schema.sql"),
  293. read_text(resources / "investment_defaults.sql"),
  294. read_text(resources / "account_settings_schema.sql"),
  295. read_text(resources / "account_settings_defaults.sql")));
  296. auto baseline = store::account();
  297. baseline.profileItems = {};
  298. baseline.profileItemCount = 0;
  299. for (std::size_t i = 0; i < baseline.characterCount; ++i) {
  300. auto& character = baseline.characters[i];
  301. character.equipment = {};
  302. character.inventory = {};
  303. character.stacks = {};
  304. character.selected = i == 0;
  305. }
  306. assert(baseline.characterCount >= 2);
  307. assert(store::write_account(baseline));
  308. std::array<items::Definition, 7> definitions{};
  309. std::array<items::details::Definition, 7> details{};
  310. for (std::uint16_t i = 0; i < definitions.size(); ++i) {
  311. definitions[i].definitionIndex = i;
  312. definitions[i].definitionHash = 1000U + i;
  313. definitions[i].bucketId = 40;
  314. details[i].definitionIndex = i;
  315. details[i].definitionHash = 1000U + i;
  316. details[i].bucketId = 40;
  317. details[i].maxStackSize = 1;
  318. details[i].instancedDefinitionState = items::details::InstancedDefinitionState::instanced;
  319. }
  320. definitions[0].questInitialization = {100, 5762, Quest::Scope::account};
  321. definitions[1].questInitialization = characterQuest;
  322. // The existing character encoder independently requires these four legacy prerequisites.
  323. constexpr std::array<std::uint32_t, 4> legacyQuests{
  324. 0x57C4540AU, 0x85CC476EU, 0xB099029AU, 0xC3535D63U};
  325. for (std::size_t i = 0; i < legacyQuests.size(); ++i) {
  326. definitions[i + 3].definitionHash = details[i + 3].definitionHash = legacyQuests[i];
  327. details[i + 3].instancedDefinitionState =
  328. items::details::InstancedDefinitionState::stackable;
  329. }
  330. assert(items::replace(definitions));
  331. assert(items::details::replace(details));
  332. s::build_data::runtime::details::publish();
  333. namespace buckets = s::build_data::inventory::buckets;
  334. const std::array<buckets::Descriptor, 2> bucketRows{
  335. {{0, buckets::ArraySelector::character, 0, 1, 0},
  336. {40, buckets::ArraySelector::character, 1, 349}}};
  337. assert(buckets::replace(bucketRows));
  338. const s::build_data::socket_entry_lists::Definition sockets{1, 0, 0, 0};
  339. assert(s::build_data::socket_entry_lists::replace({&sockets, 1}));
  340. const s::build_data::progressions::Definition progression{};
  341. assert(s::build_data::progressions::replace({&progression, 1}, {}));
  342. const auto value = [](store::Bank bank, std::uint16_t row) {
  343. std::int32_t result = 0;
  344. assert(store::read_unlock(bank, row, result));
  345. return result;
  346. };
  347. const auto reset = [&] {
  348. assert(store::write_account(baseline));
  349. assert(store::execute("DELETE FROM unlocks"));
  350. };
  351. s::PendingItemAcquisition mutation{};
  352. s::AccountState after{};
  353. s::unlocks::Table unlocks{};
  354. const auto check_encoded = [&](bool accountScope, std::uint16_t row, std::int32_t expected) {
  355. namespace family4 = sunrise::middleware::datagen::family4;
  356. std::int32_t sent = 0;
  357. if (accountScope) {
  358. Bytes encoded(family4::account::layout::kObjectSize);
  359. assert(family4::account::encode(after, encoded, unlocks));
  360. std::memcpy(&sent,
  361. encoded.data() + offsetof(family4::account::layout::Object, objectiveValues)
  362. + row * sizeof sent,
  363. sizeof sent);
  364. } else {
  365. Bytes encoded(family4::character::layout::kObjectSize);
  366. family4::loadout::ResolvedLoadout resolved{};
  367. assert(family4::loadout::resolve(after, 0, resolved));
  368. s::equipment::light::Evaluation light{};
  369. light.divisor = 1;
  370. assert(
  371. family4::character::encode(after.characters[0], resolved, light, encoded, unlocks));
  372. std::memcpy(&sent,
  373. encoded.data()
  374. + offsetof(family4::character::layout::Object, objectiveValues)
  375. + row * sizeof sent,
  376. sizeof sent);
  377. }
  378. assert(sent == expected);
  379. };
  380. reset();
  381. assert(s::prepare_item_acquisition_for_item(0, mutation));
  382. assert(mutation.updates_account());
  383. assert(!mutation.profileChanged); // The old world-reward flag misses this account write.
  384. check_acquisition_staging(mutation, true);
  385. assert(s::preview_item_acquisition(mutation, after, unlocks));
  386. assert(unlocks.objectiveValues[5762] == 100);
  387. check_encoded(true, 5762, 100);
  388. assert(value(store::Bank::objectiveValues, 5762) == 0);
  389. assert(store::account().characters[0].inventory.count == 0);
  390. assert(s::commit_item_acquisition(mutation) && !mutation.prepared);
  391. assert(value(store::Bank::objectiveValues, 5762) == 100);
  392. assert(store::account().characters[0].inventory.count == 1);
  393. for (auto previous : {100, 200, -1, -1583618456}) {
  394. reset();
  395. assert(store::write_unlock(store::Bank::objectiveValues, 5762, previous));
  396. assert(store::write_unlock(store::Bank::objectiveValues, 5763, 4));
  397. assert(s::prepare_item_acquisition_for_item(0, mutation));
  398. assert(!mutation.updates_account());
  399. check_acquisition_staging(mutation, false);
  400. assert(s::commit_item_acquisition(mutation));
  401. assert(value(store::Bank::objectiveValues, 5762) == previous);
  402. assert(value(store::Bank::objectiveValues, 5763) == 4);
  403. }
  404. reset();
  405. auto otherCharacter = baseline;
  406. otherCharacter.characters[0].selected = false;
  407. otherCharacter.characters[1].selected = true;
  408. assert(store::write_account(otherCharacter));
  409. assert(store::write_unlock(store::Bank::objectiveValues, 5762, 200));
  410. assert(s::prepare_item_acquisition(
  411. s::build_data::collectibles::kNoCollectibleIndex, definitions[0].definitionHash, mutation));
  412. assert(s::commit_item_acquisition(mutation));
  413. assert(value(store::Bank::objectiveValues, 5762) == 200);
  414. assert(store::account().characters[1].inventory.count == 1);
  415. reset();
  416. assert(s::prepare_item_acquisition_for_item(1, mutation));
  417. assert(!mutation.updates_account());
  418. check_acquisition_staging(mutation, false);
  419. assert(s::preview_item_acquisition(mutation, after, unlocks));
  420. assert(unlocks.characterObjectValues[442] == characterQuest.value);
  421. check_encoded(false, 442, characterQuest.value);
  422. assert(value(store::Bank::characterObjectValues, 442) == 0);
  423. assert(s::commit_item_acquisition(mutation));
  424. assert(value(store::Bank::characterObjectValues, 442) == characterQuest.value);
  425. assert(store::read_unlocks(unlocks, 1));
  426. assert(unlocks.characterObjectValues[442] == 0);
  427. for (auto previous : {characterQuest.value, 790208398, -1}) {
  428. reset();
  429. assert(store::write_unlock(store::Bank::characterObjectValues, 442, previous));
  430. assert(s::prepare_item_acquisition_for_item(1, mutation));
  431. assert(s::preview_item_acquisition(mutation, after, unlocks));
  432. check_encoded(false, 442, previous);
  433. assert(s::commit_item_acquisition(mutation));
  434. assert(value(store::Bank::characterObjectValues, 442) == previous);
  435. }
  436. reset();
  437. assert(s::prepare_item_acquisition_for_item(0, mutation));
  438. assert(store::write_unlock(store::Bank::objectiveValues, 5762, 200));
  439. assert(!s::commit_item_acquisition(mutation));
  440. assert(store::account().characters[0].inventory.count == 0);
  441. assert(value(store::Bank::objectiveValues, 5762) == 200);
  442. reset();
  443. assert(s::prepare_item_acquisition_for_item(1, mutation));
  444. auto changed = baseline;
  445. changed.characters[0].selected = false;
  446. changed.characters[1].selected = true;
  447. assert(store::write_account(changed));
  448. assert(!s::commit_item_acquisition(mutation));
  449. assert(value(store::Bank::characterObjectValues, 442) == 0);
  450. reset();
  451. assert(s::prepare_item_acquisition_for_item(0, mutation));
  452. mutation.questInitialization.row = 5763; // A changed contract cannot redirect the write.
  453. assert(!s::commit_item_acquisition(mutation));
  454. assert(store::account().characters[0].inventory.count == 0);
  455. reset();
  456. assert(s::prepare_item_acquisition_for_item(0, mutation));
  457. assert(store::execute("CREATE TEMP TRIGGER fail_item BEFORE INSERT ON items "
  458. "BEGIN SELECT RAISE(ABORT,'injected inventory failure'); END;"));
  459. assert(!s::commit_item_acquisition(mutation));
  460. assert(store::execute("DROP TRIGGER fail_item"));
  461. assert(store::account().characters[0].inventory.count == 0);
  462. assert(value(store::Bank::objectiveValues, 5762) == 0);
  463. reset();
  464. auto full = baseline;
  465. auto& inventory = full.characters[0].inventory;
  466. for (std::size_t i = 0; i < inventory.values.size(); ++i) {
  467. auto& item = inventory.values[i];
  468. item.instanceSoid = 0x4000000000000001ULL + i;
  469. item.definitionHash = definitions[2].definitionHash;
  470. item.quantity = 1;
  471. }
  472. inventory.count = inventory.values.size();
  473. assert(store::write_account(full));
  474. assert(!s::prepare_item_acquisition_for_item(0, mutation));
  475. assert(!mutation.prepared && value(store::Bank::objectiveValues, 5762) == 0);
  476. reset();
  477. assert(!s::prepare_item_acquisition_for_item(65535, mutation));
  478. assert(!mutation.prepared && value(store::Bank::objectiveValues, 5762) == 0);
  479. reset();
  480. assert(s::prepare_item_acquisition_for_item(0, mutation));
  481. assert(store::execute("CREATE TEMP TRIGGER fail_quest BEFORE INSERT ON unlocks "
  482. "BEGIN SELECT RAISE(ABORT,'injected quest write failure'); END;"));
  483. assert(!s::commit_item_acquisition(mutation));
  484. assert(store::execute("DROP TRIGGER fail_quest"));
  485. assert(store::account().characters[0].inventory.count == 0);
  486. assert(value(store::Bank::objectiveValues, 5762) == 0);
  487. reset();
  488. assert(s::prepare_item_acquisition_for_item(2, mutation));
  489. assert(mutation.questInitialization == Quest{});
  490. check_acquisition_staging(mutation, false);
  491. auto profileMutation = mutation;
  492. profileMutation.profileChanged = true;
  493. check_acquisition_staging(profileMutation, true); // Existing profile-charge behavior.
  494. assert(s::commit_item_acquisition(mutation));
  495. assert(value(store::Bank::objectiveValues, 5762) == 0);
  496. assert(value(store::Bank::characterObjectValues, 442) == 0);
  497. store::shutdown();
  498. std::cout << "PASS production queuez staging: fresh account/character quests, existing "
  499. "progress, ordinary item, profile charge\n";
  500. std::cout << "PASS production acquisition + SQLite: atomic rollback, stale value/character, "
  501. "both scopes, preservation, encoded after-image, full/refused grants, unsupported "
  502. "control\n";
  503. }
  504. int main(int argc, char** argv) {
  505. std::cout << std::unitbuf;
  506. assert(argc == 2 || argc == 3);
  507. const auto characterQuest = parser_checks();
  508. cache_checks();
  509. world_reward_caller_check(argv[1]);
  510. runtime_checks(argv[1], characterQuest);
  511. if (argc == 3) {
  512. content_checks(argv[2]);
  513. }
  514. }