exotic_catalyst_regression_tests.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. #include <array>
  2. #include <cstddef>
  3. #include <cstdint>
  4. #include <cstdio>
  5. #include <optional>
  6. #include <span>
  7. #include "state/account/inventory/item_state.h"
  8. #include "state/build_data/cache/records/codec.h"
  9. #include "state/build_data/items/catalysts/exotic_catalyst_builder.h"
  10. #include "state/build_data/items/catalysts/exotic_catalyst_catalog.h"
  11. extern int failures;
  12. namespace {
  13. namespace catalysts = sunrise::state::build_data::items::catalysts;
  14. namespace cache_records = sunrise::state::build_data::cache::records;
  15. namespace details = sunrise::state::build_data::items::details;
  16. namespace items = sunrise::state::build_data::items;
  17. namespace socket_plugs = sunrise::state::build_data::items::socket_plugs;
  18. constexpr std::uint32_t kTimestamp = 0x5F43138BU;
  19. constexpr std::uint32_t kImageSize = 0x08A5EA00U;
  20. constexpr std::uint32_t kEmptyCatalyst = 0x5957A904U;
  21. constexpr std::uint32_t kWorldline = 0x6F22FCECU;
  22. constexpr std::uint32_t kWorldlineDefault = 0x9B283E92U;
  23. constexpr std::uint32_t kWorldlineProgress = 0x172766F5U;
  24. constexpr std::uint32_t kWorldlineComplete = 0x61935831U;
  25. constexpr std::uint32_t kWorldlineEffect = 0x9F651900U;
  26. constexpr std::uint32_t kWorldlineCategory = 0x06989D65U;
  27. constexpr std::uint32_t kWitherhoard = 0x8C8180D6U;
  28. constexpr std::uint32_t kWitherhoardComplete = 0xAC29C6ACU;
  29. constexpr std::uint32_t kWitherhoardCategory = 0x1234ABCDU;
  30. constexpr std::uint32_t kWishEnder = 0x3092080CU;
  31. constexpr std::uint32_t kWishEnderActive = 0xCE1AD8BBU;
  32. void expect(bool value, const char* label) noexcept {
  33. if (!value) {
  34. std::fprintf(stderr, "FAIL %s\n", label);
  35. ++failures;
  36. }
  37. }
  38. items::Definition item(std::uint16_t index,
  39. std::uint32_t hash,
  40. items::Tier tier = items::Tier::none) noexcept {
  41. items::Definition value{};
  42. value.definitionIndex = index;
  43. value.definitionHash = hash;
  44. value.tier = static_cast<std::uint8_t>(tier);
  45. return value;
  46. }
  47. details::Definition weapon_detail(std::uint16_t index,
  48. std::uint32_t hash,
  49. std::uint8_t lane,
  50. std::uint16_t defaultPlug) noexcept {
  51. details::Definition value{};
  52. value.definitionIndex = index;
  53. value.definitionHash = hash;
  54. value.instancedDefinitionState = details::InstancedDefinitionState::instanced;
  55. value.equipmentSlot = std::int8_t{7};
  56. value.ordinarySocketState = details::OrdinarySocketState::present;
  57. value.ordinarySocketCount = static_cast<std::uint8_t>(lane + 1);
  58. value.initialPlugIndices[lane] = defaultPlug;
  59. return value;
  60. }
  61. struct Fixture {
  62. std::array<items::Definition, 5> itemRows{};
  63. std::array<details::Definition, 2> detailRows{};
  64. std::array<socket_plugs::Rule, 1> rules{{{10, 7, 0, 1}}};
  65. std::array<socket_plugs::Pool, 2> pools{{{0, 0}, {0, 3}}};
  66. std::array<socket_plugs::Member, 3> members{{20, 21, 22}};
  67. std::array<std::uint32_t, 1> legacy{{kWorldlineComplete}};
  68. std::array<std::uint32_t, 1> released{{kWorldline}};
  69. std::array<catalysts::Definition, catalysts::kDefinitionCapacity> output{};
  70. Fixture() noexcept {
  71. itemRows = {item(10, kWorldline, items::Tier::exotic),
  72. item(20, kWorldlineDefault),
  73. item(21, kWorldlineProgress),
  74. item(22, kWorldlineComplete),
  75. item(23, kWorldlineEffect, items::Tier::exotic)};
  76. for (std::size_t index = 1; index < itemRows.size(); ++index) {
  77. itemRows[index].plugCategoryHash = kWorldlineCategory;
  78. }
  79. detailRows[0] = weapon_detail(10, kWorldline, 7, 20);
  80. detailRows[1].definitionIndex = 23;
  81. detailRows[1].definitionHash = kWorldlineEffect;
  82. detailRows[1].instancedDefinitionState =
  83. details::InstancedDefinitionState::stackable;
  84. detailRows[1].sandboxPerkCount = 1;
  85. detailRows[1].sandboxPerks[0] = 77;
  86. }
  87. [[nodiscard]] catalysts::Source source() const noexcept {
  88. return {{kTimestamp, kImageSize, 0}, itemRows, detailRows, rules, pools, members};
  89. }
  90. [[nodiscard]] catalysts::Facts facts() const noexcept {
  91. return {kTimestamp, kImageSize, kEmptyCatalyst, legacy, released};
  92. }
  93. };
  94. void test_worldline_legacy_completion() noexcept {
  95. Fixture fixture;
  96. std::size_t count = 0;
  97. catalysts::Report report{};
  98. expect(catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report),
  99. "Worldline catalog derives");
  100. expect(count == 1, "Worldline catalog has one record");
  101. expect(report.released == 1 && report.placeholder == 0 && report.unsupported == 0,
  102. "Worldline report is released");
  103. expect(catalysts::matches_derived(
  104. fixture.source(), fixture.facts(), std::span(fixture.output).first(count)),
  105. "Worldline stored catalog matches fresh derivation");
  106. if (count != 1) {
  107. return;
  108. }
  109. const catalysts::Definition& record = fixture.output[0];
  110. expect(record.itemDefinitionHash == kWorldline, "Worldline record keeps weapon hash");
  111. expect(record.itemDefinitionIndex == 10, "Worldline record keeps weapon index");
  112. expect(record.socketLane == 7, "Worldline resolves lane 7");
  113. expect(record.completedPlugDefinitionIndex == 22, "Worldline resolves completed plug");
  114. expect(record.effectDefinitionIndex == 23, "Worldline resolves actual catalyst effect");
  115. expect(record.availability == catalysts::Availability::released,
  116. "Worldline is released");
  117. catalysts::clear();
  118. expect(catalysts::replace(std::span(fixture.output).first(count)),
  119. "Worldline catalog publishes");
  120. expect(catalysts::resolve_effect(10, 7, 22) == 23,
  121. "Worldline display plug resolves actual catalyst effect");
  122. expect(catalysts::resolve_effect(10, 7, 20) == 20,
  123. "Worldline default plug has no catalyst effect");
  124. for (std::uint32_t flags = 0; flags <= 3; ++flags) {
  125. std::array<std::optional<std::uint16_t>, 12> plugs{};
  126. plugs[7] = std::uint16_t{20};
  127. std::uint32_t changedFlags = flags;
  128. expect(catalysts::apply_completed(10, changedFlags, plugs)
  129. == catalysts::ApplyResult::completed,
  130. "Worldline completion applies");
  131. expect(changedFlags == (flags | sunrise::state::account::inventory::kMasterworkItemFlag),
  132. "Worldline completion preserves item-state bits");
  133. expect(plugs[7] == 22, "Worldline completion applies plug with flag");
  134. }
  135. catalysts::set_completion_enabled(false);
  136. std::array<std::optional<std::uint16_t>, 12> disabledPlugs{};
  137. disabledPlugs[7] = std::uint16_t{20};
  138. const auto disabledBefore = disabledPlugs;
  139. std::uint32_t disabledFlags = 3;
  140. expect(catalysts::apply_completed(10, disabledFlags, disabledPlugs)
  141. == catalysts::ApplyResult::unchanged,
  142. "global policy can disable catalyst completion");
  143. expect(disabledFlags == 3 && disabledPlugs == disabledBefore,
  144. "disabled catalyst completion leaves item unchanged");
  145. catalysts::set_completion_enabled(true);
  146. }
  147. void test_later_two_plug_and_placeholder() noexcept {
  148. std::array<items::Definition, 3> itemRows{
  149. item(10, kWitherhoard, items::Tier::exotic),
  150. item(20, kEmptyCatalyst),
  151. item(21, kWitherhoardComplete),
  152. };
  153. itemRows[1].plugCategoryHash = kWitherhoardCategory;
  154. itemRows[2].plugCategoryHash = kWitherhoardCategory;
  155. itemRows[2].tier = static_cast<std::uint8_t>(items::Tier::exotic);
  156. std::array<details::Definition, 2> detailRows{
  157. weapon_detail(10, kWitherhoard, 10, 20),
  158. details::Definition{},
  159. };
  160. detailRows[1].definitionIndex = 21;
  161. detailRows[1].definitionHash = kWitherhoardComplete;
  162. detailRows[1].instancedDefinitionState = details::InstancedDefinitionState::stackable;
  163. detailRows[1].sandboxPerkCount = 1;
  164. detailRows[1].sandboxPerks[0] = 88;
  165. std::array<socket_plugs::Rule, 1> rules{{{10, 10, 0, 1}}};
  166. std::array<socket_plugs::Pool, 2> pools{{{0, 0}, {0, 2}}};
  167. std::array<socket_plugs::Member, 2> members{{20, 21}};
  168. std::array<std::uint32_t, 1> released{{kWitherhoard}};
  169. std::array<catalysts::Definition, catalysts::kDefinitionCapacity> output{};
  170. const catalysts::Facts facts{kTimestamp, kImageSize, kEmptyCatalyst, {}, released};
  171. const catalysts::Source source{
  172. {kTimestamp, kImageSize, 0}, itemRows, detailRows, rules, pools, members};
  173. std::size_t count = 0;
  174. catalysts::Report report{};
  175. expect(catalysts::derive(source, facts, output, count, report),
  176. "Witherhoard catalog derives");
  177. expect(count == 1 && output[0].socketLane == 10
  178. && output[0].completedPlugDefinitionIndex == 21
  179. && output[0].effectDefinitionIndex == 21,
  180. "Witherhoard resolves later lifecycle");
  181. itemRows[0].definitionHash = kWishEnder;
  182. itemRows[2].definitionHash = kWishEnderActive;
  183. detailRows[0].definitionHash = kWishEnder;
  184. detailRows[1].definitionHash = kWishEnderActive;
  185. const catalysts::Facts placeholderFacts{kTimestamp, kImageSize, kEmptyCatalyst, {}, {}};
  186. count = 0;
  187. report = {};
  188. expect(catalysts::derive(source, placeholderFacts, output, count, report),
  189. "Wish-Ender placeholder catalog derives");
  190. expect(count == 1 && output[0].availability == catalysts::Availability::placeholder,
  191. "Wish-Ender stays a placeholder");
  192. expect(catalysts::replace(std::span(output).first(count)),
  193. "Wish-Ender placeholder publishes");
  194. const catalysts::Result result = catalysts::resolve(10);
  195. expect(result.error == catalysts::Error::placeholderOnly,
  196. "Wish-Ender does not resolve a completed catalyst");
  197. std::array<std::optional<std::uint16_t>, 12> plugs{};
  198. plugs[10] = std::uint16_t{20};
  199. const auto before = plugs;
  200. std::uint32_t flags = 3;
  201. expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::unchanged,
  202. "Wish-Ender completion is skipped");
  203. expect(flags == 3 && plugs == before, "Wish-Ender state stays unchanged");
  204. }
  205. void test_exclusions_and_errors() noexcept {
  206. Fixture fixture;
  207. std::size_t count = 0;
  208. catalysts::Report report{};
  209. fixture.itemRows[0].tier = static_cast<std::uint8_t>(items::Tier::legendary);
  210. const std::array<std::uint32_t, 0> noReleased{};
  211. const catalysts::Facts noReleaseFacts{
  212. kTimestamp, kImageSize, kEmptyCatalyst, fixture.legacy, noReleased};
  213. expect(catalysts::derive(
  214. fixture.source(), noReleaseFacts, fixture.output, count, report)
  215. && count == 0,
  216. "non-exotic weapon is excluded");
  217. fixture.itemRows[0].tier = static_cast<std::uint8_t>(items::Tier::exotic);
  218. fixture.detailRows[0].equipmentSlot = std::int8_t{10};
  219. count = 0;
  220. report = {};
  221. expect(catalysts::derive(
  222. fixture.source(), noReleaseFacts, fixture.output, count, report)
  223. && count == 0,
  224. "exotic armor is excluded");
  225. fixture.detailRows[0].equipmentSlot = std::int8_t{7};
  226. fixture.itemRows[2].definitionHash = kWorldlineComplete;
  227. fixture.legacy = {kWorldlineComplete};
  228. count = 0;
  229. report = {};
  230. expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  231. && count == 0 && report.error == catalysts::Error::ambiguousLifecycle,
  232. "unclear legacy lifecycle fails without partial records");
  233. fixture.itemRows[2].definitionHash = kWorldlineProgress;
  234. fixture.members = {21, 22, 10};
  235. count = 0;
  236. report = {};
  237. expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  238. && count == 0 && report.error == catalysts::Error::invalidPlug,
  239. "default plug outside its pool fails");
  240. fixture.members = {20, 21, 22};
  241. fixture.itemRows[4].tier = static_cast<std::uint8_t>(items::Tier::none);
  242. count = 0;
  243. report = {};
  244. expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  245. && count == 0 && report.error == catalysts::Error::invalidEffect,
  246. "missing legacy catalyst effect fails");
  247. fixture.itemRows[4].tier = static_cast<std::uint8_t>(items::Tier::exotic);
  248. catalysts::Source wrongBuild = fixture.source();
  249. wrongBuild.build.imageTimestamp ^= 1U;
  250. count = 0;
  251. report = {};
  252. expect(!catalysts::derive(wrongBuild, fixture.facts(), fixture.output, count, report)
  253. && count == 0 && report.error == catalysts::Error::unsupportedBuild,
  254. "build fingerprint mismatch fails");
  255. }
  256. void test_unclear_placeholder_is_safe() noexcept {
  257. Fixture fixture;
  258. fixture.itemRows[2].definitionHash = kWorldlineComplete;
  259. const std::array<std::uint32_t, 0> noReleased{};
  260. const catalysts::Facts facts{
  261. kTimestamp, kImageSize, kEmptyCatalyst, fixture.legacy, noReleased};
  262. std::size_t count = 0;
  263. catalysts::Report report{};
  264. expect(catalysts::derive(fixture.source(), facts, fixture.output, count, report),
  265. "unclear placeholder does not abort the catalog");
  266. expect(count == 1 && report.unsupported == 1 && report.error == catalysts::Error::none,
  267. "unclear placeholder is reported as unsupported");
  268. if (count != 1) {
  269. return;
  270. }
  271. expect(fixture.output[0].availability == catalysts::Availability::unsupported
  272. && fixture.output[0].completedPlugDefinitionIndex
  273. == details::kUnavailableItemIndex
  274. && fixture.output[0].effectDefinitionIndex
  275. == details::kUnavailableItemIndex,
  276. "unclear placeholder has no completed state");
  277. expect(catalysts::replace(std::span(fixture.output).first(count)),
  278. "unsupported placeholder catalog publishes");
  279. std::array<std::optional<std::uint16_t>, 12> plugs{};
  280. plugs[7] = std::uint16_t{20};
  281. const auto before = plugs;
  282. std::uint32_t flags = 3;
  283. expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::unchanged,
  284. "unsupported placeholder is skipped");
  285. expect(flags == 3 && plugs == before, "unsupported placeholder stays unchanged");
  286. }
  287. void test_cache_record_round_trip() noexcept {
  288. const catalysts::Definition released{
  289. kWorldline, 10, 22, 23, 7, catalysts::Availability::released, 0};
  290. cache_records::ExoticCatalystRecord record{};
  291. catalysts::Definition decoded{};
  292. expect(cache_records::encode(released, record), "released catalyst cache record encodes");
  293. expect(cache_records::decode(record, decoded), "released catalyst cache record decodes");
  294. expect(decoded.itemDefinitionHash == released.itemDefinitionHash
  295. && decoded.itemDefinitionIndex == released.itemDefinitionIndex
  296. && decoded.completedPlugDefinitionIndex
  297. == released.completedPlugDefinitionIndex
  298. && decoded.effectDefinitionIndex == released.effectDefinitionIndex
  299. && decoded.socketLane == released.socketLane
  300. && decoded.availability == released.availability && decoded.reserved == 0,
  301. "released catalyst cache record round-trips");
  302. const catalysts::Definition unsupported{kWishEnder,
  303. 11,
  304. details::kUnavailableItemIndex,
  305. details::kUnavailableItemIndex,
  306. 10,
  307. catalysts::Availability::unsupported,
  308. 0};
  309. expect(cache_records::encode(unsupported, record),
  310. "unsupported catalyst cache record encodes");
  311. expect(cache_records::decode(record, decoded)
  312. && decoded.availability == catalysts::Availability::unsupported
  313. && decoded.completedPlugDefinitionIndex == details::kUnavailableItemIndex
  314. && decoded.effectDefinitionIndex == details::kUnavailableItemIndex,
  315. "unsupported catalyst cache record round-trips");
  316. record.reserved = 1;
  317. expect(!cache_records::decode(record, decoded),
  318. "noncanonical catalyst cache record is rejected");
  319. expect(cache_records::kCacheFormatVersion == 46,
  320. "catalyst records use the next cache format");
  321. }
  322. void test_atomic_rollback() noexcept {
  323. Fixture fixture;
  324. std::size_t count = 0;
  325. catalysts::Report report{};
  326. if (!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  327. || !catalysts::replace(std::span(fixture.output).first(count))) {
  328. expect(false, "rollback fixture publishes");
  329. return;
  330. }
  331. std::array<std::optional<std::uint16_t>, 12> plugs{};
  332. plugs[7] = std::uint16_t{20};
  333. const auto before = plugs;
  334. std::uint32_t flags = 0x8U;
  335. expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::failed,
  336. "invalid item state rejects completion");
  337. expect(flags == 0x8U && plugs == before, "invalid item state rolls back both fields");
  338. std::array<std::optional<std::uint16_t>, 7> shortPlugs{};
  339. const auto shortBefore = shortPlugs;
  340. flags = 3;
  341. expect(catalysts::apply_completed(10, flags, shortPlugs) == catalysts::ApplyResult::failed,
  342. "missing catalyst lane rejects completion");
  343. expect(flags == 3 && shortPlugs == shortBefore, "missing lane rolls back both fields");
  344. }
  345. } // namespace
  346. void test_exotic_catalysts() noexcept {
  347. test_worldline_legacy_completion();
  348. test_later_two_plug_and_placeholder();
  349. test_exclusions_and_errors();
  350. test_unclear_placeholder_is_safe();
  351. test_atomic_rollback();
  352. test_cache_record_round_trip();
  353. catalysts::clear();
  354. }