catalyst_regression_tests.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. #include <algorithm>
  2. #include <array>
  3. #include <cstddef>
  4. #include <cstdint>
  5. #include <cstdio>
  6. #include <optional>
  7. #include <span>
  8. #include "state/account/inventory/item_state.h"
  9. #include "state/build_data/cache/records/codec.h"
  10. #include "state/build_data/items/catalysts/exotic_catalyst_builder.h"
  11. #include "state/build_data/items/catalysts/exotic_catalyst_catalog.h"
  12. #include "state/build_data/runtime/persistence/build_data_persistence.h"
  13. extern int failures;
  14. namespace {
  15. namespace cache_records = sunrise::state::build_data::cache::records;
  16. namespace catalysts = sunrise::state::build_data::items::catalysts;
  17. namespace details = sunrise::state::build_data::items::details;
  18. namespace items = sunrise::state::build_data::items;
  19. namespace persistence = sunrise::state::build_data::runtime::persistence;
  20. namespace socket_plugs = sunrise::state::build_data::items::socket_plugs;
  21. constexpr std::uint32_t kTimestamp = 0x12345678U;
  22. constexpr std::uint32_t kImageSize = 0x01000000U;
  23. constexpr std::uint32_t kLegacyWeaponHash = 0x10000010U;
  24. constexpr std::uint32_t kLaterWeaponHash = 0x10000020U;
  25. constexpr std::uint32_t kPlaceholderWeaponHash = 0x10000030U;
  26. constexpr std::uint32_t kNonCatalystWeaponHash = 0x10000040U;
  27. void expect(bool value, const char* label) noexcept {
  28. if (!value) {
  29. std::fprintf(stderr, "FAIL %s\n", label);
  30. ++failures;
  31. }
  32. }
  33. items::Definition item(std::uint16_t index,
  34. std::uint32_t hash,
  35. items::Tier tier = items::Tier::none,
  36. std::uint32_t category = 0) noexcept {
  37. items::Definition value{};
  38. value.definitionIndex = index;
  39. value.definitionHash = hash;
  40. value.tier = static_cast<std::uint8_t>(tier);
  41. value.plugCategoryHash = category;
  42. return value;
  43. }
  44. details::Definition weapon_detail(std::uint16_t index,
  45. std::uint32_t hash,
  46. std::uint8_t lane,
  47. std::uint16_t defaultPlug) noexcept {
  48. details::Definition value{};
  49. value.definitionIndex = index;
  50. value.definitionHash = hash;
  51. value.instancedDefinitionState = details::InstancedDefinitionState::instanced;
  52. value.equipmentSlot = std::int8_t{7};
  53. value.ordinarySocketState = details::OrdinarySocketState::present;
  54. value.ordinarySocketCount = static_cast<std::uint8_t>(lane + 1);
  55. value.initialPlugIndices[lane] = defaultPlug;
  56. return value;
  57. }
  58. details::Definition plug_detail(std::uint16_t index,
  59. std::uint32_t hash,
  60. std::int32_t maxStackSize,
  61. bool hasEffect = false) noexcept {
  62. details::Definition value{};
  63. value.definitionIndex = index;
  64. value.definitionHash = hash;
  65. value.maxStackSize = maxStackSize;
  66. value.instancedDefinitionState = details::InstancedDefinitionState::stackable;
  67. if (hasEffect) {
  68. value.sandboxPerkCount = 1;
  69. value.sandboxPerks[0] = 7;
  70. }
  71. return value;
  72. }
  73. struct Fixture {
  74. static constexpr std::uint16_t kLegacyWeapon = 10;
  75. static constexpr std::uint16_t kLaterWeapon = 11;
  76. static constexpr std::uint16_t kPlaceholderWeapon = 12;
  77. static constexpr std::uint16_t kNonCatalystWeapon = 13;
  78. static constexpr std::uint16_t kLegacyDefault = 20;
  79. static constexpr std::uint16_t kLegacyProgress = 21;
  80. static constexpr std::uint16_t kLegacyComplete = 22;
  81. static constexpr std::uint16_t kLegacyEffect = 23;
  82. static constexpr std::uint16_t kLaterDefault = 30;
  83. static constexpr std::uint16_t kLaterActive = 31;
  84. static constexpr std::uint16_t kPlaceholderDefault = 40;
  85. static constexpr std::uint16_t kPlaceholderActive = 41;
  86. static constexpr std::uint16_t kOtherDefault = 50;
  87. static constexpr std::uint16_t kOtherActive = 51;
  88. std::array<items::Definition, 13> itemRows{};
  89. std::array<details::Definition, 11> detailRows{};
  90. std::array<socket_plugs::Rule, 4> rules{{
  91. {kLegacyWeapon, 7, 0, 1},
  92. {kLaterWeapon, 6, 0, 2},
  93. {kPlaceholderWeapon, 5, 0, 3},
  94. {kNonCatalystWeapon, 4, 0, 4},
  95. }};
  96. std::array<socket_plugs::Pool, 5> pools{{
  97. {0, 0},
  98. {0, 3},
  99. {3, 2},
  100. {5, 2},
  101. {7, 2},
  102. }};
  103. std::array<socket_plugs::Member, 9> members{{
  104. kLegacyDefault,
  105. kLegacyProgress,
  106. kLegacyComplete,
  107. kLaterDefault,
  108. kLaterActive,
  109. kPlaceholderDefault,
  110. kPlaceholderActive,
  111. kOtherDefault,
  112. kOtherActive,
  113. }};
  114. std::array<std::uint32_t, 2> released{{kLegacyWeaponHash, kLaterWeaponHash}};
  115. std::array<catalysts::Definition, catalysts::kDefinitionCapacity> output{};
  116. Fixture() noexcept {
  117. constexpr std::uint32_t kLegacyCategory = 0x20000010U;
  118. constexpr std::uint32_t kLaterCategory = 0x20000020U;
  119. constexpr std::uint32_t kPlaceholderCategory = 0x20000030U;
  120. constexpr std::uint32_t kOtherCategory = 0x20000040U;
  121. itemRows = {
  122. item(kLegacyWeapon, kLegacyWeaponHash, items::Tier::exotic),
  123. item(kLaterWeapon, kLaterWeaponHash, items::Tier::exotic),
  124. item(kPlaceholderWeapon, kPlaceholderWeaponHash, items::Tier::exotic),
  125. item(kNonCatalystWeapon, kNonCatalystWeaponHash, items::Tier::exotic),
  126. item(kLegacyDefault, 0x30000010U, items::Tier::none, kLegacyCategory),
  127. item(kLegacyProgress, 0x30000011U, items::Tier::none, kLegacyCategory),
  128. item(kLegacyComplete, 0x30000012U, items::Tier::none, kLegacyCategory),
  129. item(kLegacyEffect, 0x30000013U, items::Tier::exotic, kLegacyCategory),
  130. item(kLaterDefault, 0x30000020U, items::Tier::none, kLaterCategory),
  131. item(kLaterActive, 0x30000021U, items::Tier::exotic, kLaterCategory),
  132. item(kPlaceholderDefault, 0x30000030U, items::Tier::none, kPlaceholderCategory),
  133. item(kPlaceholderActive, 0x30000031U, items::Tier::exotic, kPlaceholderCategory),
  134. item(kOtherActive, 0x30000041U, items::Tier::legendary, kOtherCategory),
  135. };
  136. detailRows = {
  137. weapon_detail(kLegacyWeapon, kLegacyWeaponHash, 7, kLegacyDefault),
  138. weapon_detail(kLaterWeapon, kLaterWeaponHash, 6, kLaterDefault),
  139. weapon_detail(kPlaceholderWeapon, kPlaceholderWeaponHash, 5, kPlaceholderDefault),
  140. weapon_detail(kNonCatalystWeapon, kNonCatalystWeaponHash, 4, kOtherDefault),
  141. plug_detail(kLegacyDefault, 0x30000010U, 100),
  142. plug_detail(kLegacyProgress, 0x30000011U, 1),
  143. plug_detail(kLegacyComplete, 0x30000012U, 100),
  144. plug_detail(kLegacyEffect, 0x30000013U, 100, true),
  145. plug_detail(kLaterActive, 0x30000021U, 100, true),
  146. plug_detail(kPlaceholderActive, 0x30000031U, 100, true),
  147. plug_detail(kOtherActive, 0x30000041U, 100, true),
  148. };
  149. }
  150. [[nodiscard]] catalysts::Source source() const noexcept {
  151. return {{kTimestamp, kImageSize, 0}, itemRows, detailRows, rules, pools, members};
  152. }
  153. [[nodiscard]] catalysts::Facts facts() const noexcept {
  154. return {kTimestamp, kImageSize, released};
  155. }
  156. };
  157. bool derive(Fixture& fixture, std::size_t& count, catalysts::Report& report) noexcept {
  158. return catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report);
  159. }
  160. void test_structural_lifecycles() noexcept {
  161. Fixture fixture;
  162. std::size_t count = 0;
  163. catalysts::Report report{};
  164. expect(derive(fixture, count, report), "synthetic catalyst catalog derives");
  165. expect(count == 3, "only two released and one placeholder catalyst are found");
  166. expect(report.released == 2 && report.placeholder == 1 && report.unsupported == 0,
  167. "catalog report separates released and placeholder rows");
  168. if (count != 3) {
  169. return;
  170. }
  171. expect(fixture.output[0].itemDefinitionIndex == Fixture::kLegacyWeapon
  172. && fixture.output[0].completedPlugDefinitionIndex == Fixture::kLegacyComplete
  173. && fixture.output[0].effectDefinitionIndex == Fixture::kLegacyEffect,
  174. "legacy three-state lifecycle resolves display and effect rows");
  175. expect(fixture.output[1].itemDefinitionIndex == Fixture::kLaterWeapon
  176. && fixture.output[1].completedPlugDefinitionIndex == Fixture::kLaterActive
  177. && fixture.output[1].effectDefinitionIndex == Fixture::kLaterActive,
  178. "later two-state lifecycle resolves its direct active plug");
  179. expect(fixture.output[2].availability == catalysts::Availability::placeholder,
  180. "unreleased structural catalyst remains a placeholder");
  181. expect(std::none_of(std::span(fixture.output).first(count).begin(),
  182. std::span(fixture.output).first(count).end(),
  183. [](const catalysts::Definition& value) {
  184. return value.itemDefinitionIndex == Fixture::kNonCatalystWeapon;
  185. }),
  186. "a two-state lane without an exotic effect item is not a catalyst");
  187. expect(catalysts::matches_derived(
  188. fixture.source(), fixture.facts(), std::span(fixture.output).first(count)),
  189. "stored catalog matches a fresh structural derivation");
  190. auto altered = fixture.output;
  191. altered[0].completedPlugDefinitionIndex = Fixture::kLegacyEffect;
  192. expect(!catalysts::matches_derived(
  193. fixture.source(), fixture.facts(), std::span(altered).first(count)),
  194. "catalog rejects a completed plug outside its socket pool");
  195. altered = fixture.output;
  196. altered[0].completedPlugDefinitionIndex = Fixture::kLegacyProgress;
  197. expect(!catalysts::matches_derived(
  198. fixture.source(), fixture.facts(), std::span(altered).first(count)),
  199. "catalog rejects a completed plug outside the derived role");
  200. Fixture legendaryWeapon;
  201. legendaryWeapon.itemRows[2].tier = static_cast<std::uint8_t>(items::Tier::legendary);
  202. expect(derive(legendaryWeapon, count, report) && count == 2 && report.placeholder == 0,
  203. "non-exotic weapons are outside catalyst scope");
  204. Fixture exoticArmor;
  205. exoticArmor.detailRows[2].equipmentSlot = std::int8_t{3};
  206. expect(derive(exoticArmor, count, report) && count == 2 && report.placeholder == 0,
  207. "exotic armor is outside catalyst scope");
  208. }
  209. void test_safe_derivation_failures() noexcept {
  210. Fixture fixture;
  211. std::size_t count = 0;
  212. catalysts::Report report{};
  213. const catalysts::Facts facts = fixture.facts();
  214. expect(catalysts::supports_build(fixture.source().build, facts),
  215. "target catalyst facts support their exact executable build");
  216. catalysts::Source mismatched = fixture.source();
  217. ++mismatched.build.imageSize;
  218. expect(!catalysts::supports_build(mismatched.build, facts),
  219. "target catalyst facts reject a different executable build");
  220. expect(!catalysts::derive(mismatched, fixture.facts(), fixture.output, count, report)
  221. && count == 0 && report.error == catalysts::Error::unsupportedBuild,
  222. "build fingerprint mismatch fails only the catalog derivation");
  223. std::array<std::uint32_t, 3> missingRelease{kLegacyWeaponHash, kLaterWeaponHash, 0x1FFFFFFFU};
  224. catalysts::Facts missingFacts{kTimestamp, kImageSize, missingRelease};
  225. expect(!catalysts::derive(fixture.source(), missingFacts, fixture.output, count, report)
  226. && count == 0 && report.error == catalysts::Error::missingReleased,
  227. "missing released weapon clears all staged catalog rows");
  228. fixture.detailRows[5].maxStackSize = 100;
  229. expect(!derive(fixture, count, report) && count == 0
  230. && report.error == catalysts::Error::missingReleased,
  231. "unclear released legacy lifecycle fails closed");
  232. }
  233. void test_catalog_application() noexcept {
  234. Fixture fixture;
  235. std::size_t count = 0;
  236. catalysts::Report report{};
  237. if (!derive(fixture, count, report)
  238. || !catalysts::replace(std::span(fixture.output).first(count))) {
  239. expect(false, "application fixture publishes");
  240. return;
  241. }
  242. for (std::uint32_t flags = 0; flags <= 3; ++flags) {
  243. std::array<std::optional<std::uint16_t>, 12> plugs{};
  244. plugs[7] = Fixture::kLegacyDefault;
  245. std::uint32_t changedFlags = flags;
  246. expect(catalysts::apply_completed(Fixture::kLegacyWeapon, changedFlags, plugs)
  247. == catalysts::ApplyResult::completed,
  248. "released legacy catalyst completes");
  249. expect(changedFlags == (flags | sunrise::state::account::inventory::kMasterworkItemFlag),
  250. "completion preserves all prior item-state bits");
  251. expect(plugs[7] == Fixture::kLegacyComplete,
  252. "completion sockets the display plug from the allowed pool");
  253. }
  254. expect(catalysts::resolve_effect(Fixture::kLegacyWeapon, 7, Fixture::kLegacyComplete)
  255. == Fixture::kLegacyEffect,
  256. "legacy display plug resolves its effect item");
  257. expect(catalysts::resolve_effect(Fixture::kLegacyWeapon, 7, Fixture::kLegacyDefault)
  258. == Fixture::kLegacyDefault,
  259. "uncompleted display plug remains unchanged");
  260. std::array<std::optional<std::uint16_t>, 12> laterPlugs{};
  261. laterPlugs[6] = Fixture::kLaterDefault;
  262. std::uint32_t laterFlags = 0;
  263. expect(catalysts::apply_completed(Fixture::kLaterWeapon, laterFlags, laterPlugs)
  264. == catalysts::ApplyResult::completed
  265. && laterPlugs[6] == Fixture::kLaterActive,
  266. "later catalyst sockets its direct active plug");
  267. std::array<std::optional<std::uint16_t>, 12> placeholderPlugs{};
  268. placeholderPlugs[5] = Fixture::kPlaceholderDefault;
  269. const auto placeholderBefore = placeholderPlugs;
  270. std::uint32_t placeholderFlags = 3;
  271. expect(
  272. catalysts::apply_completed(Fixture::kPlaceholderWeapon, placeholderFlags, placeholderPlugs)
  273. == catalysts::ApplyResult::unchanged
  274. && placeholderFlags == 3 && placeholderPlugs == placeholderBefore,
  275. "placeholder catalyst remains unchanged");
  276. std::array<std::optional<std::uint16_t>, 12> invalidPlugs{};
  277. invalidPlugs[7] = Fixture::kLegacyDefault;
  278. const auto invalidBefore = invalidPlugs;
  279. std::uint32_t invalidFlags = 0x8U;
  280. expect(catalysts::apply_completed(Fixture::kLegacyWeapon, invalidFlags, invalidPlugs)
  281. == catalysts::ApplyResult::failed
  282. && invalidFlags == 0x8U && invalidPlugs == invalidBefore,
  283. "invalid item state fails without a partial change");
  284. std::array<std::optional<std::uint16_t>, 7> shortPlugs{};
  285. const auto shortBefore = shortPlugs;
  286. std::uint32_t shortFlags = 1;
  287. expect(catalysts::apply_completed(Fixture::kLegacyWeapon, shortFlags, shortPlugs)
  288. == catalysts::ApplyResult::failed
  289. && shortFlags == 1 && shortPlugs == shortBefore,
  290. "missing catalyst lane fails without a partial change");
  291. catalysts::set_completion_enabled(false);
  292. std::array<std::optional<std::uint16_t>, 12> disabledPlugs{};
  293. disabledPlugs[7] = Fixture::kLegacyDefault;
  294. const auto disabledBefore = disabledPlugs;
  295. std::uint32_t disabledFlags = 2;
  296. expect(catalysts::apply_completed(Fixture::kLegacyWeapon, disabledFlags, disabledPlugs)
  297. == catalysts::ApplyResult::unchanged
  298. && disabledFlags == 2 && disabledPlugs == disabledBefore,
  299. "global policy disables completion without changing the item");
  300. catalysts::clear();
  301. expect(!catalysts::completion_enabled(),
  302. "clearing catalyst records preserves the configured completion policy");
  303. catalysts::set_completion_enabled(true);
  304. }
  305. void test_cache_record() noexcept {
  306. const catalysts::Definition definition{
  307. 0x11111111U, 3, 7, 9, 5, catalysts::Availability::released};
  308. cache_records::ExoticCatalystRecord record{};
  309. catalysts::Definition decoded{};
  310. expect(cache_records::encode(definition, record), "catalyst cache record encodes");
  311. expect(cache_records::decode(record, decoded)
  312. && decoded.itemDefinitionHash == definition.itemDefinitionHash
  313. && decoded.itemDefinitionIndex == definition.itemDefinitionIndex
  314. && decoded.completedPlugDefinitionIndex == definition.completedPlugDefinitionIndex
  315. && decoded.effectDefinitionIndex == definition.effectDefinitionIndex
  316. && decoded.socketLane == definition.socketLane
  317. && decoded.availability == definition.availability,
  318. "catalyst cache record round-trips");
  319. record.availability = 0xFFU;
  320. expect(!cache_records::decode(record, decoded), "invalid catalyst availability is rejected");
  321. expect(cache_records::kCacheFormatVersion == 45,
  322. "catalyst records use one cache bump over upstream version 44");
  323. }
  324. void test_persistence_action() noexcept {
  325. using enum persistence::CacheAction;
  326. expect(persistence::cache_action(false, false, catalysts::Error::unsupportedBuild)
  327. == waitForDomains,
  328. "persistence waits for required domains");
  329. expect(persistence::cache_action(true, false, catalysts::Error::unsupportedBuild)
  330. == writeRequiredDomains,
  331. "unsupported builds cache every required domain without a catalyst catalog");
  332. constexpr std::array rejectedErrors{
  333. catalysts::Error::none,
  334. catalysts::Error::noCatalyst,
  335. catalysts::Error::placeholderOnly,
  336. catalysts::Error::missingReleased,
  337. catalysts::Error::ambiguousLifecycle,
  338. catalysts::Error::invalidSocket,
  339. };
  340. for (const catalysts::Error error : rejectedErrors) {
  341. expect(persistence::cache_action(true, false, error) == waitForDomains,
  342. "non-build catalyst failures stay failed closed");
  343. }
  344. expect(persistence::cache_action(true, true, catalysts::Error::none) == writeCompleteCache,
  345. "a complete catalog permits one cache write");
  346. }
  347. } // namespace
  348. void test_exotic_catalysts() noexcept {
  349. test_structural_lifecycles();
  350. test_safe_derivation_failures();
  351. test_catalog_application();
  352. test_cache_record();
  353. test_persistence_action();
  354. catalysts::clear();
  355. }