exotic_catalyst_regression_tests.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. #include <array>
  2. #include <cstddef>
  3. #include <cstdint>
  4. #include <cstdio>
  5. #include <cstring>
  6. #include <optional>
  7. #include <span>
  8. #include "client/content/items/packages/package_socket_plug_build.h"
  9. #include "middleware/content/packages/tables/definition_index_table.h"
  10. #include "state/account/inventory/item_state.h"
  11. #include "state/build_data/cache/records/codec.h"
  12. #include "state/build_data/items/catalysts/exotic_catalyst_builder.h"
  13. #include "state/build_data/items/catalysts/exotic_catalyst_catalog.h"
  14. extern int failures;
  15. namespace {
  16. namespace catalysts = sunrise::state::build_data::items::catalysts;
  17. namespace cache_records = sunrise::state::build_data::cache::records;
  18. namespace package_items = sunrise::client::content::items::packages;
  19. namespace package_tables = sunrise::middleware::content::packages::tables;
  20. namespace details = sunrise::state::build_data::items::details;
  21. namespace items = sunrise::state::build_data::items;
  22. namespace socket_plugs = sunrise::state::build_data::items::socket_plugs;
  23. constexpr std::uint32_t kTimestamp = 0x5F43138BU;
  24. constexpr std::uint32_t kImageSize = 0x08A5EA00U;
  25. constexpr std::uint32_t kEmptyCatalyst = 0x5957A904U;
  26. constexpr std::uint32_t kWorldline = 0x6F22FCECU;
  27. constexpr std::uint32_t kWorldlineDefault = 0x9B283E92U;
  28. constexpr std::uint32_t kWorldlineProgress = 0x172766F5U;
  29. constexpr std::uint32_t kWorldlineComplete = 0x61935831U;
  30. constexpr std::uint32_t kWorldlineEffect = 0x9F651900U;
  31. constexpr std::uint32_t kWorldlineCategory = 0x06989D65U;
  32. constexpr std::uint16_t kWorldlineSocketType = 447;
  33. constexpr std::uint16_t kWorldlineAcquisition = 5044;
  34. constexpr std::uint32_t kWorldlineAcquisitionHash = 0x12345678U;
  35. constexpr std::uint16_t kWorldlineCompletionValue = 3167;
  36. constexpr std::int32_t kWorldlineCompletionThreshold = 4;
  37. constexpr std::uint32_t kWitherhoard = 0x8C8180D6U;
  38. constexpr std::uint32_t kWitherhoardComplete = 0xAC29C6ACU;
  39. constexpr std::uint32_t kWitherhoardCategory = 0x1234ABCDU;
  40. constexpr std::uint16_t kWitherhoardSocketType = 727;
  41. constexpr std::uint16_t kWitherhoardAcquisition = 11276;
  42. constexpr std::uint32_t kWishEnder = 0x3092080CU;
  43. constexpr std::uint32_t kWishEnderActive = 0xCE1AD8BBU;
  44. void expect(bool value, const char* label) noexcept {
  45. if (!value) {
  46. std::fprintf(stderr, "FAIL %s\n", label);
  47. ++failures;
  48. }
  49. }
  50. template <typename Value>
  51. void write_bytes(std::span<std::byte> output,
  52. std::size_t offset,
  53. const Value& value) noexcept {
  54. std::memcpy(output.data() + offset, &value, sizeof value);
  55. }
  56. void write_completion_expression(std::span<std::byte> output,
  57. std::size_t descriptorOffset,
  58. std::size_t headerOffset,
  59. std::uint32_t valueIndex,
  60. std::uint32_t value) noexcept {
  61. constexpr std::uint64_t kTokenCount = 3;
  62. constexpr std::uint32_t kHeaderMarker = 0x80800001U;
  63. const std::int64_t relative = static_cast<std::int64_t>(headerOffset)
  64. - static_cast<std::int64_t>(descriptorOffset + 8);
  65. write_bytes(output, descriptorOffset, kTokenCount);
  66. write_bytes(output, descriptorOffset + 8, relative);
  67. write_bytes(output, headerOffset - 4, kHeaderMarker);
  68. write_bytes(output, headerOffset, kTokenCount);
  69. write_bytes(output, headerOffset + 8, package_tables::kInvestmentExpressionRowClass);
  70. const std::array<std::uint32_t, 6> tokens{
  71. 10, valueIndex, 11, value, 14, UINT32_MAX};
  72. std::memcpy(output.data() + headerOffset + 16, tokens.data(), sizeof tokens);
  73. }
  74. items::Definition item(std::uint16_t index,
  75. std::uint32_t hash,
  76. items::Tier tier = items::Tier::none) noexcept {
  77. items::Definition value{};
  78. value.definitionIndex = index;
  79. value.definitionHash = hash;
  80. value.tier = static_cast<std::uint8_t>(tier);
  81. return value;
  82. }
  83. details::Definition weapon_detail(std::uint16_t index,
  84. std::uint32_t hash,
  85. std::uint8_t lane,
  86. std::uint16_t defaultPlug,
  87. std::uint16_t socketType) noexcept {
  88. details::Definition value{};
  89. value.definitionIndex = index;
  90. value.definitionHash = hash;
  91. value.instancedDefinitionState = details::InstancedDefinitionState::instanced;
  92. value.equipmentSlot = std::int8_t{7};
  93. value.ordinarySocketState = details::OrdinarySocketState::present;
  94. value.ordinarySocketCount = static_cast<std::uint8_t>(lane + 1);
  95. value.initialPlugIndices[lane] = defaultPlug;
  96. value.socketTypes[lane] = socketType;
  97. return value;
  98. }
  99. struct Fixture {
  100. std::array<items::Definition, 6> itemRows{};
  101. std::array<details::Definition, 2> detailRows{};
  102. std::array<socket_plugs::Rule, 1> rules{{{10, 7, 0, 1}}};
  103. std::array<socket_plugs::Pool, 2> pools{{{0, 0}, {0, 3}}};
  104. std::array<socket_plugs::Member, 3> members{{20, 21, 22}};
  105. std::array<std::uint32_t, 1> legacy{{kWorldlineComplete}};
  106. std::array<std::uint32_t, 1> released{{kWorldline}};
  107. std::array<catalysts::CompletionCondition, 1> completionConditions{{{
  108. 23,
  109. kWorldlineCompletionValue,
  110. kWorldlineCompletionThreshold,
  111. catalysts::CompletionConditionState::present,
  112. }}};
  113. std::array<catalysts::AcquisitionGate, 1> acquisitionGates{{{
  114. kWorldlineSocketType,
  115. kWorldlineAcquisition,
  116. catalysts::AcquisitionState::present,
  117. }}};
  118. std::array<catalysts::Definition, catalysts::kDefinitionCapacity> output{};
  119. Fixture() noexcept {
  120. itemRows = {item(10, kWorldline, items::Tier::exotic),
  121. item(20, kWorldlineDefault),
  122. item(21, kWorldlineProgress),
  123. item(22, kWorldlineComplete),
  124. item(23, kWorldlineEffect, items::Tier::exotic),
  125. item(kWorldlineAcquisition, kWorldlineAcquisitionHash)};
  126. for (std::size_t index = 1; index < itemRows.size() - 1; ++index) {
  127. itemRows[index].plugCategoryHash = kWorldlineCategory;
  128. }
  129. detailRows[0] = weapon_detail(10, kWorldline, 7, 20, kWorldlineSocketType);
  130. detailRows[1].definitionIndex = 23;
  131. detailRows[1].definitionHash = kWorldlineEffect;
  132. detailRows[1].instancedDefinitionState =
  133. details::InstancedDefinitionState::stackable;
  134. detailRows[1].sandboxPerkCount = 1;
  135. detailRows[1].sandboxPerks[0] = 77;
  136. }
  137. [[nodiscard]] catalysts::Source source() const noexcept {
  138. return {{kTimestamp, kImageSize, 0},
  139. itemRows,
  140. detailRows,
  141. rules,
  142. pools,
  143. members,
  144. completionConditions,
  145. acquisitionGates};
  146. }
  147. [[nodiscard]] catalysts::Facts facts() const noexcept {
  148. return {kTimestamp, kImageSize, kEmptyCatalyst, legacy, released};
  149. }
  150. };
  151. void test_completion_expression_reader() noexcept {
  152. std::array<std::byte, 192> definition{};
  153. write_completion_expression(
  154. definition, 0, 64, kWorldlineCompletionValue, kWorldlineCompletionThreshold);
  155. catalysts::CompletionCondition condition{};
  156. package_items::read_catalyst_completion_condition(definition, 23, condition);
  157. expect(condition.itemDefinitionIndex == 23
  158. && condition.valueIndex == kWorldlineCompletionValue
  159. && condition.value == kWorldlineCompletionThreshold
  160. && condition.state == catalysts::CompletionConditionState::present,
  161. "native completion expression resolves");
  162. write_completion_expression(
  163. definition, 16, 128, kWorldlineCompletionValue, kWorldlineCompletionThreshold);
  164. package_items::read_catalyst_completion_condition(definition, 23, condition);
  165. expect(condition.state == catalysts::CompletionConditionState::present
  166. && condition.valueIndex == kWorldlineCompletionValue
  167. && condition.value == kWorldlineCompletionThreshold,
  168. "duplicate native completion expressions remain one condition");
  169. write_completion_expression(
  170. definition, 16, 128, kWorldlineCompletionValue - 1, kWorldlineCompletionThreshold);
  171. package_items::read_catalyst_completion_condition(definition, 23, condition);
  172. expect(condition.state == catalysts::CompletionConditionState::ambiguous
  173. && condition.valueIndex == catalysts::kUnavailableCompletionValueIndex
  174. && condition.value == 0,
  175. "distinct native completion expressions fail closed");
  176. definition = {};
  177. write_completion_expression(
  178. definition, 0, 64, kWorldlineCompletionValue, kWorldlineCompletionThreshold);
  179. constexpr std::uint32_t kWrongComparison = 13;
  180. write_bytes(definition, 64 + 16 + 4 * sizeof(std::uint32_t), kWrongComparison);
  181. package_items::read_catalyst_completion_condition(definition, 23, condition);
  182. expect(condition.state == catalysts::CompletionConditionState::absent,
  183. "non-completion postfix expressions stay absent");
  184. }
  185. void test_worldline_legacy_completion() noexcept {
  186. Fixture fixture;
  187. std::size_t count = 0;
  188. catalysts::Report report{};
  189. expect(catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report),
  190. "Worldline catalog derives");
  191. expect(count == 1, "Worldline catalog has one record");
  192. expect(report.released == 1 && report.placeholder == 0 && report.unsupported == 0,
  193. "Worldline report is released");
  194. expect(catalysts::matches_derived(
  195. fixture.source(), fixture.facts(), std::span(fixture.output).first(count)),
  196. "Worldline stored catalog matches fresh derivation");
  197. if (count != 1) {
  198. return;
  199. }
  200. const catalysts::Definition& record = fixture.output[0];
  201. expect(record.itemDefinitionHash == kWorldline, "Worldline record keeps weapon hash");
  202. expect(record.itemDefinitionIndex == 10, "Worldline record keeps weapon index");
  203. expect(record.socketLane == 7, "Worldline resolves lane 7");
  204. expect(record.completedPlugDefinitionIndex == 22, "Worldline resolves completed plug");
  205. expect(record.effectDefinitionIndex == 23, "Worldline resolves actual catalyst effect");
  206. expect(record.acquisitionDefinitionIndex == kWorldlineAcquisition,
  207. "Worldline resolves its acquired-state gate");
  208. expect(record.completionValueIndex == kWorldlineCompletionValue
  209. && record.completionValue == kWorldlineCompletionThreshold,
  210. "Worldline resolves its native completion condition");
  211. expect(record.availability == catalysts::Availability::released,
  212. "Worldline is released");
  213. catalysts::clear();
  214. expect(catalysts::replace(std::span(fixture.output).first(count)),
  215. "Worldline catalog publishes");
  216. expect(catalysts::resolve_effect(10, 7, 22) == 23,
  217. "Worldline display plug resolves actual catalyst effect");
  218. expect(catalysts::resolve_effect(10, 7, 20) == 20,
  219. "Worldline default plug has no catalyst effect");
  220. for (std::uint32_t flags = 0; flags <= 3; ++flags) {
  221. std::array<std::optional<std::uint16_t>, 12> plugs{};
  222. plugs[7] = std::uint16_t{20};
  223. std::uint32_t changedFlags = flags;
  224. expect(catalysts::apply_completed(10, changedFlags, plugs)
  225. == catalysts::ApplyResult::completed,
  226. "Worldline completion applies");
  227. expect(changedFlags == (flags | sunrise::state::account::inventory::kMasterworkItemFlag),
  228. "Worldline completion preserves item-state bits");
  229. expect(plugs[7] == 23, "Worldline completion applies native effect plug with flag");
  230. }
  231. catalysts::set_completion_enabled(false);
  232. std::array<std::optional<std::uint16_t>, 12> disabledPlugs{};
  233. disabledPlugs[7] = std::uint16_t{20};
  234. const auto disabledBefore = disabledPlugs;
  235. std::uint32_t disabledFlags = 3;
  236. expect(catalysts::apply_completed(10, disabledFlags, disabledPlugs)
  237. == catalysts::ApplyResult::unchanged,
  238. "global policy can disable catalyst completion");
  239. expect(disabledFlags == 3 && disabledPlugs == disabledBefore,
  240. "disabled catalyst completion leaves item unchanged");
  241. catalysts::set_completion_enabled(true);
  242. }
  243. void test_investment_completion_is_atomic() noexcept {
  244. Fixture fixture;
  245. std::size_t count = 0;
  246. catalysts::Report report{};
  247. if (!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  248. || !catalysts::replace(std::span(fixture.output).first(count))) {
  249. expect(false, "investment fixture publishes");
  250. return;
  251. }
  252. sunrise::state::Family5State family{};
  253. family.objectSoid = 0x4000000000000005ULL;
  254. family.contentGateArm = true;
  255. family.flags[0] = {77, 1};
  256. family.flags[1] = {kWorldlineAcquisition, 0};
  257. family.flags[2] = {kWorldlineAcquisition, 1};
  258. family.flagCount = 3;
  259. family.values[0] = {88, 7};
  260. family.values[1] = {kWorldlineCompletionValue, 2};
  261. family.values[2] = {kWorldlineCompletionValue, 9};
  262. family.valueCount = 3;
  263. expect(catalysts::append_investment_overrides(family),
  264. "Worldline investment state completes");
  265. expect(family.objectSoid == 0x4000000000000005ULL && family.contentGateArm,
  266. "catalyst completion preserves Family 5 identity and arm state");
  267. expect(family.flagCount == 2 && family.flags[0].slot == 77
  268. && family.flags[0].value == 1
  269. && family.flags[1].slot == kWorldlineAcquisition
  270. && family.flags[1].value == 2,
  271. "catalyst completion deduplicates and sets its acquired-state gate");
  272. expect(family.valueCount == 2 && family.values[0].slot == 88
  273. && family.values[0].value == 7
  274. && family.values[1].slot == kWorldlineCompletionValue
  275. && family.values[1].value == 9,
  276. "catalyst completion deduplicates without lowering authored values");
  277. catalysts::set_completion_enabled(false);
  278. const std::size_t disabledFlagCount = family.flagCount;
  279. const std::size_t disabledValueCount = family.valueCount;
  280. const auto disabledFlags = family.flags;
  281. const auto disabledValues = family.values;
  282. expect(catalysts::append_investment_overrides(family),
  283. "disabled catalyst investment completion succeeds without changes");
  284. expect(family.flagCount == disabledFlagCount && family.valueCount == disabledValueCount
  285. && family.flags[0].slot == disabledFlags[0].slot
  286. && family.flags[0].value == disabledFlags[0].value
  287. && family.values[0].slot == disabledValues[0].slot
  288. && family.values[0].value == disabledValues[0].value,
  289. "disabled catalyst investment completion leaves authored state unchanged");
  290. catalysts::set_completion_enabled(true);
  291. sunrise::state::Family5State full{};
  292. full.flags[0] = {kWorldlineAcquisition, 0};
  293. full.flagCount = 1;
  294. full.valueCount = full.values.size();
  295. for (std::size_t index = 0; index < full.valueCount; ++index) {
  296. full.values[index] = {static_cast<std::uint16_t>(index),
  297. static_cast<std::int32_t>(index)};
  298. }
  299. const auto fullValues = full.values;
  300. expect(!catalysts::append_investment_overrides(full),
  301. "full completion-value bank rejects catalyst completion");
  302. expect(full.flagCount == 1 && full.flags[0].value == 0
  303. && full.valueCount == full.values.size()
  304. && full.values[full.valueCount - 1].slot
  305. == fullValues[full.valueCount - 1].slot
  306. && full.values[full.valueCount - 1].value
  307. == fullValues[full.valueCount - 1].value,
  308. "failed investment completion rolls back both Family 5 banks");
  309. }
  310. void test_later_two_plug_and_placeholder() noexcept {
  311. std::array<items::Definition, 3> itemRows{
  312. item(10, kWitherhoard, items::Tier::exotic),
  313. item(20, kEmptyCatalyst),
  314. item(21, kWitherhoardComplete),
  315. };
  316. itemRows[1].plugCategoryHash = kWitherhoardCategory;
  317. itemRows[2].plugCategoryHash = kWitherhoardCategory;
  318. itemRows[2].tier = static_cast<std::uint8_t>(items::Tier::exotic);
  319. std::array<details::Definition, 2> detailRows{
  320. weapon_detail(10, kWitherhoard, 10, 20, kWitherhoardSocketType),
  321. details::Definition{},
  322. };
  323. detailRows[1].definitionIndex = 21;
  324. detailRows[1].definitionHash = kWitherhoardComplete;
  325. detailRows[1].instancedDefinitionState = details::InstancedDefinitionState::stackable;
  326. detailRows[1].sandboxPerkCount = 1;
  327. detailRows[1].sandboxPerks[0] = 88;
  328. std::array<socket_plugs::Rule, 1> rules{{{10, 10, 0, 1}}};
  329. std::array<socket_plugs::Pool, 2> pools{{{0, 0}, {0, 2}}};
  330. std::array<socket_plugs::Member, 2> members{{20, 21}};
  331. std::array<std::uint32_t, 1> released{{kWitherhoard}};
  332. std::array<catalysts::AcquisitionGate, 1> acquisitionGates{{{
  333. kWitherhoardSocketType,
  334. kWitherhoardAcquisition,
  335. catalysts::AcquisitionState::present,
  336. }}};
  337. std::array<catalysts::Definition, catalysts::kDefinitionCapacity> output{};
  338. const catalysts::Facts facts{kTimestamp, kImageSize, kEmptyCatalyst, {}, released};
  339. const catalysts::Source source{
  340. {kTimestamp, kImageSize, 0},
  341. itemRows,
  342. detailRows,
  343. rules,
  344. pools,
  345. members,
  346. {},
  347. acquisitionGates};
  348. std::size_t count = 0;
  349. catalysts::Report report{};
  350. expect(catalysts::derive(source, facts, output, count, report),
  351. "Witherhoard catalog derives");
  352. expect(count == 1 && output[0].socketLane == 10
  353. && output[0].completedPlugDefinitionIndex == 21
  354. && output[0].effectDefinitionIndex == 21
  355. && output[0].acquisitionDefinitionIndex == kWitherhoardAcquisition
  356. && output[0].completionValueIndex
  357. == catalysts::kUnavailableCompletionValueIndex,
  358. "Witherhoard resolves later lifecycle");
  359. itemRows[0].definitionHash = kWishEnder;
  360. itemRows[2].definitionHash = kWishEnderActive;
  361. detailRows[0].definitionHash = kWishEnder;
  362. detailRows[1].definitionHash = kWishEnderActive;
  363. const catalysts::Facts placeholderFacts{kTimestamp, kImageSize, kEmptyCatalyst, {}, {}};
  364. count = 0;
  365. report = {};
  366. expect(catalysts::derive(source, placeholderFacts, output, count, report),
  367. "Wish-Ender placeholder catalog derives");
  368. expect(count == 1 && output[0].availability == catalysts::Availability::placeholder,
  369. "Wish-Ender stays a placeholder");
  370. expect(catalysts::replace(std::span(output).first(count)),
  371. "Wish-Ender placeholder publishes");
  372. const catalysts::Result result = catalysts::resolve(10);
  373. expect(result.error == catalysts::Error::placeholderOnly,
  374. "Wish-Ender does not resolve a completed catalyst");
  375. std::array<std::optional<std::uint16_t>, 12> plugs{};
  376. plugs[10] = std::uint16_t{20};
  377. const auto before = plugs;
  378. std::uint32_t flags = 3;
  379. expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::unchanged,
  380. "Wish-Ender completion is skipped");
  381. expect(flags == 3 && plugs == before, "Wish-Ender state stays unchanged");
  382. }
  383. void test_exclusions_and_errors() noexcept {
  384. Fixture fixture;
  385. std::size_t count = 0;
  386. catalysts::Report report{};
  387. fixture.itemRows[0].tier = static_cast<std::uint8_t>(items::Tier::legendary);
  388. const std::array<std::uint32_t, 0> noReleased{};
  389. const catalysts::Facts noReleaseFacts{
  390. kTimestamp, kImageSize, kEmptyCatalyst, fixture.legacy, noReleased};
  391. expect(catalysts::derive(
  392. fixture.source(), noReleaseFacts, fixture.output, count, report)
  393. && count == 0,
  394. "non-exotic weapon is excluded");
  395. fixture.itemRows[0].tier = static_cast<std::uint8_t>(items::Tier::exotic);
  396. fixture.detailRows[0].equipmentSlot = std::int8_t{10};
  397. count = 0;
  398. report = {};
  399. expect(catalysts::derive(
  400. fixture.source(), noReleaseFacts, fixture.output, count, report)
  401. && count == 0,
  402. "exotic armor is excluded");
  403. fixture.detailRows[0].equipmentSlot = std::int8_t{7};
  404. fixture.itemRows[2].definitionHash = kWorldlineComplete;
  405. fixture.legacy = {kWorldlineComplete};
  406. count = 0;
  407. report = {};
  408. expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  409. && count == 0 && report.error == catalysts::Error::ambiguousLifecycle,
  410. "unclear legacy lifecycle fails without partial records");
  411. fixture.itemRows[2].definitionHash = kWorldlineProgress;
  412. fixture.members = {21, 22, 10};
  413. count = 0;
  414. report = {};
  415. expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  416. && count == 0 && report.error == catalysts::Error::invalidPlug,
  417. "default plug outside its pool fails");
  418. fixture.members = {20, 21, 22};
  419. fixture.itemRows[4].tier = static_cast<std::uint8_t>(items::Tier::none);
  420. count = 0;
  421. report = {};
  422. expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  423. && count == 0 && report.error == catalysts::Error::invalidEffect,
  424. "missing legacy catalyst effect fails");
  425. fixture.itemRows[4].tier = static_cast<std::uint8_t>(items::Tier::exotic);
  426. fixture.acquisitionGates[0].state = catalysts::AcquisitionState::ambiguous;
  427. count = 0;
  428. report = {};
  429. expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  430. && count == 0 && report.error == catalysts::Error::invalidAcquisition,
  431. "unclear acquired-state gate fails a released catalyst");
  432. fixture.acquisitionGates[0].state = catalysts::AcquisitionState::present;
  433. fixture.completionConditions[0].state = catalysts::CompletionConditionState::ambiguous;
  434. count = 0;
  435. report = {};
  436. expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  437. && count == 0 && report.error == catalysts::Error::invalidCompletion,
  438. "unclear completion expression fails a released catalyst");
  439. fixture.completionConditions[0].state = catalysts::CompletionConditionState::present;
  440. catalysts::Source wrongBuild = fixture.source();
  441. wrongBuild.build.imageTimestamp ^= 1U;
  442. count = 0;
  443. report = {};
  444. expect(!catalysts::derive(wrongBuild, fixture.facts(), fixture.output, count, report)
  445. && count == 0 && report.error == catalysts::Error::unsupportedBuild,
  446. "build fingerprint mismatch fails");
  447. }
  448. void test_unclear_placeholder_is_safe() noexcept {
  449. Fixture fixture;
  450. fixture.itemRows[2].definitionHash = kWorldlineComplete;
  451. const std::array<std::uint32_t, 0> noReleased{};
  452. const catalysts::Facts facts{
  453. kTimestamp, kImageSize, kEmptyCatalyst, fixture.legacy, noReleased};
  454. std::size_t count = 0;
  455. catalysts::Report report{};
  456. expect(catalysts::derive(fixture.source(), facts, fixture.output, count, report),
  457. "unclear placeholder does not abort the catalog");
  458. expect(count == 1 && report.unsupported == 1 && report.error == catalysts::Error::none,
  459. "unclear placeholder is reported as unsupported");
  460. if (count != 1) {
  461. return;
  462. }
  463. expect(fixture.output[0].availability == catalysts::Availability::unsupported
  464. && fixture.output[0].completedPlugDefinitionIndex
  465. == details::kUnavailableItemIndex
  466. && fixture.output[0].effectDefinitionIndex
  467. == details::kUnavailableItemIndex,
  468. "unclear placeholder has no completed state");
  469. expect(catalysts::replace(std::span(fixture.output).first(count)),
  470. "unsupported placeholder catalog publishes");
  471. std::array<std::optional<std::uint16_t>, 12> plugs{};
  472. plugs[7] = std::uint16_t{20};
  473. const auto before = plugs;
  474. std::uint32_t flags = 3;
  475. expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::unchanged,
  476. "unsupported placeholder is skipped");
  477. expect(flags == 3 && plugs == before, "unsupported placeholder stays unchanged");
  478. }
  479. void test_cache_record_round_trip() noexcept {
  480. const catalysts::Definition released{
  481. kWorldline,
  482. 10,
  483. 22,
  484. 23,
  485. kWorldlineAcquisition,
  486. kWorldlineCompletionValue,
  487. 7,
  488. catalysts::Availability::released,
  489. kWorldlineCompletionThreshold};
  490. cache_records::ExoticCatalystRecord record{};
  491. catalysts::Definition decoded{};
  492. expect(cache_records::encode(released, record), "released catalyst cache record encodes");
  493. expect(cache_records::decode(record, decoded), "released catalyst cache record decodes");
  494. expect(decoded.itemDefinitionHash == released.itemDefinitionHash
  495. && decoded.itemDefinitionIndex == released.itemDefinitionIndex
  496. && decoded.completedPlugDefinitionIndex
  497. == released.completedPlugDefinitionIndex
  498. && decoded.effectDefinitionIndex == released.effectDefinitionIndex
  499. && decoded.acquisitionDefinitionIndex
  500. == released.acquisitionDefinitionIndex
  501. && decoded.completionValueIndex == released.completionValueIndex
  502. && decoded.socketLane == released.socketLane
  503. && decoded.availability == released.availability
  504. && decoded.completionValue == released.completionValue,
  505. "released catalyst cache record round-trips");
  506. const catalysts::Definition unsupported{kWishEnder,
  507. 11,
  508. details::kUnavailableItemIndex,
  509. details::kUnavailableItemIndex,
  510. catalysts::kUnavailableAcquisitionIndex,
  511. catalysts::kUnavailableCompletionValueIndex,
  512. 10,
  513. catalysts::Availability::unsupported,
  514. 0};
  515. expect(cache_records::encode(unsupported, record),
  516. "unsupported catalyst cache record encodes");
  517. expect(cache_records::decode(record, decoded)
  518. && decoded.availability == catalysts::Availability::unsupported
  519. && decoded.completedPlugDefinitionIndex == details::kUnavailableItemIndex
  520. && decoded.effectDefinitionIndex == details::kUnavailableItemIndex,
  521. "unsupported catalyst cache record round-trips");
  522. record.availability = 0xFFU;
  523. expect(!cache_records::decode(record, decoded),
  524. "noncanonical catalyst cache record is rejected");
  525. expect(cache_records::kCacheFormatVersion == 47,
  526. "catalyst records use the next cache format");
  527. }
  528. void test_cached_catalog_revalidation() noexcept {
  529. Fixture fixture;
  530. std::size_t count = 0;
  531. catalysts::Report report{};
  532. if (!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)) {
  533. expect(false, "cache validation fixture derives");
  534. return;
  535. }
  536. catalysts::Source cachedSource = fixture.source();
  537. cachedSource.completionConditions = {};
  538. cachedSource.acquisitionGates = {};
  539. expect(catalysts::matches_cached(cachedSource,
  540. fixture.facts(),
  541. std::span(fixture.output).first(count)),
  542. "cached catalyst catalog re-derives from stored relations");
  543. fixture.output[0].acquisitionDefinitionIndex = kWorldlineAcquisition - 1;
  544. expect(!catalysts::matches_cached(cachedSource,
  545. fixture.facts(),
  546. std::span(fixture.output).first(count)),
  547. "cached catalyst catalog rejects an absent acquisition item");
  548. }
  549. void test_atomic_rollback() noexcept {
  550. Fixture fixture;
  551. std::size_t count = 0;
  552. catalysts::Report report{};
  553. if (!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
  554. || !catalysts::replace(std::span(fixture.output).first(count))) {
  555. expect(false, "rollback fixture publishes");
  556. return;
  557. }
  558. std::array<std::optional<std::uint16_t>, 12> plugs{};
  559. plugs[7] = std::uint16_t{20};
  560. const auto before = plugs;
  561. std::uint32_t flags = 0x8U;
  562. expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::failed,
  563. "invalid item state rejects completion");
  564. expect(flags == 0x8U && plugs == before, "invalid item state rolls back both fields");
  565. std::array<std::optional<std::uint16_t>, 7> shortPlugs{};
  566. const auto shortBefore = shortPlugs;
  567. flags = 3;
  568. expect(catalysts::apply_completed(10, flags, shortPlugs) == catalysts::ApplyResult::failed,
  569. "missing catalyst lane rejects completion");
  570. expect(flags == 3 && shortPlugs == shortBefore, "missing lane rolls back both fields");
  571. }
  572. } // namespace
  573. void test_exotic_catalysts() noexcept {
  574. test_completion_expression_reader();
  575. test_worldline_legacy_completion();
  576. test_investment_completion_is_atomic();
  577. test_later_two_plug_and_placeholder();
  578. test_exclusions_and_errors();
  579. test_unclear_placeholder_is_safe();
  580. test_atomic_rollback();
  581. test_cache_record_round_trip();
  582. test_cached_catalog_revalidation();
  583. catalysts::clear();
  584. }