catalyst_regression_tests.cpp 16 KB

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