Просмотр исходного кода

fix(investment): complete later exotic catalysts

y9522 2 недель назад
Родитель
Сommit
a294e963b4

+ 0 - 6
Sunrise.sln

@@ -5,8 +5,6 @@ VisualStudioVersion = 18.0.30412.183
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Sunrise", "Sunrise\Sunrise.vcxproj", "{A665726B-97A2-4E35-B518-0FC207D3DA1B}"
 EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Sunrise.Tests", "tests\Sunrise.Tests.vcxproj", "{9D2A5866-9D2F-4A07-A88C-6E713A355C94}"
-EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
@@ -17,10 +15,6 @@ Global
 		{A665726B-97A2-4E35-B518-0FC207D3DA1B}.Debug|x64.Build.0 = Debug|x64
 		{A665726B-97A2-4E35-B518-0FC207D3DA1B}.Release|x64.ActiveCfg = Release|x64
 		{A665726B-97A2-4E35-B518-0FC207D3DA1B}.Release|x64.Build.0 = Release|x64
-		{9D2A5866-9D2F-4A07-A88C-6E713A355C94}.Debug|x64.ActiveCfg = Debug|x64
-		{9D2A5866-9D2F-4A07-A88C-6E713A355C94}.Debug|x64.Build.0 = Debug|x64
-		{9D2A5866-9D2F-4A07-A88C-6E713A355C94}.Release|x64.ActiveCfg = Release|x64
-		{9D2A5866-9D2F-4A07-A88C-6E713A355C94}.Release|x64.Build.0 = Release|x64
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 48 - 13
Sunrise/src/client/content/items/packages/package_catalyst_condition_reader.cpp

@@ -9,18 +9,28 @@
 namespace sunrise::client::content::items::packages {
 namespace {
 
-/** Native postfix opcodes for value, literal, and greater-than-or-equal. */
+/** Native postfix opcodes for flag, value, literal, and greater-than-or-equal. */
+constexpr std::uint32_t kFlagOpcode = 1;
 constexpr std::uint32_t kValueOpcode = 10;
 constexpr std::uint32_t kLiteralOpcode = 11;
 constexpr std::uint32_t kGreaterEqualOpcode = 14;
 /** Each postfix token is an opcode and one 32-bit operand. */
 constexpr std::size_t kExpressionTokenSize = 8;
-constexpr std::size_t kCompletionExpressionTokenCount = 3;
+constexpr std::size_t kFlagExpressionTokenCount = 1;
+constexpr std::size_t kValueExpressionTokenCount = 3;
+
+/** Marks a conflicting native completion rule and clears its partial operands. */
+void mark_ambiguous(catalysts::CompletionCondition& output) noexcept {
+    output.flagDefinitionIndex = catalysts::kUnavailableCompletionFlagIndex;
+    output.valueIndex = catalysts::kUnavailableCompletionValueIndex;
+    output.value = 0;
+    output.state = catalysts::CompletionConditionState::ambiguous;
+}
 
 } // namespace
 
 /**
- * Finds the unique three-token `value >= literal` completion rule in one effect item.
+ * Finds the unique flag and `value >= literal` completion rules in one effect item.
  * @param definition Complete installed definition of the catalyst effect item.
  * @param itemDefinitionIndex Native index of the catalyst effect item.
  * @param output Receives the unique condition or its absent or ambiguous state.
@@ -35,15 +45,42 @@ void read_catalyst_completion_condition(
          descriptor + 2 * sizeof(std::uint64_t) <= definition.size();
          descriptor += sizeof(std::uint64_t)) {
         tables::Array expression{};
-        if (!tables::find_array_at(definition, descriptor, expression)
-            || expression.count != kCompletionExpressionTokenCount
-            || expression.elementClass != tables::kInvestmentExpressionRowClass
-            || expression.dataOffset > definition.size()
+        if (!tables::find_array_at(definition, descriptor, expression)) {
+            continue;
+        }
+        if (expression.elementClass != tables::kInvestmentExpressionRowClass
+            || expression.dataOffset > definition.size()) {
+            continue;
+        }
+
+        if (expression.count == kFlagExpressionTokenCount
+            && definition.size() - expression.dataOffset
+                   >= kFlagExpressionTokenCount * kExpressionTokenSize) {
+            std::array<std::uint32_t, kFlagExpressionTokenCount * 2> tokens{};
+            std::memcpy(tokens.data(),
+                        definition.data() + expression.dataOffset,
+                        tokens.size() * sizeof(tokens.front()));
+            if (tokens[0] != kFlagOpcode
+                || tokens[1] >= state::build_data::items::kDefinitionCapacity) {
+                continue;
+            }
+            const auto flagDefinitionIndex = static_cast<std::uint16_t>(tokens[1]);
+            if (output.flagDefinitionIndex == catalysts::kUnavailableCompletionFlagIndex) {
+                output.flagDefinitionIndex = flagDefinitionIndex;
+                output.state = catalysts::CompletionConditionState::present;
+            } else if (output.flagDefinitionIndex != flagDefinitionIndex) {
+                mark_ambiguous(output);
+                return;
+            }
+            continue;
+        }
+
+        if (expression.count != kValueExpressionTokenCount
             || definition.size() - expression.dataOffset
-                   < kCompletionExpressionTokenCount * kExpressionTokenSize) {
+                   < kValueExpressionTokenCount * kExpressionTokenSize) {
             continue;
         }
-        std::array<std::uint32_t, kCompletionExpressionTokenCount * 2> tokens{};
+        std::array<std::uint32_t, kValueExpressionTokenCount * 2> tokens{};
         std::memcpy(tokens.data(),
                     definition.data() + expression.dataOffset,
                     tokens.size() * sizeof(tokens.front()));
@@ -57,14 +94,12 @@ void read_catalyst_completion_condition(
         }
         const auto valueIndex = static_cast<std::uint16_t>(tokens[1]);
         const auto value = static_cast<std::int32_t>(tokens[3]);
-        if (output.state == catalysts::CompletionConditionState::absent) {
+        if (output.valueIndex == catalysts::kUnavailableCompletionValueIndex) {
             output.valueIndex = valueIndex;
             output.value = value;
             output.state = catalysts::CompletionConditionState::present;
         } else if (output.valueIndex != valueIndex || output.value != value) {
-            output.valueIndex = catalysts::kUnavailableCompletionValueIndex;
-            output.value = 0;
-            output.state = catalysts::CompletionConditionState::ambiguous;
+            mark_ambiguous(output);
             return;
         }
     }

+ 2 - 0
Sunrise/src/state/build_data/cache/records/cache_exotic_catalyst_record_codec.cpp

@@ -15,6 +15,7 @@ bool encode(const items::catalysts::Definition& value,
     record.completedPlugDefinitionIndex = value.completedPlugDefinitionIndex;
     record.effectDefinitionIndex = value.effectDefinitionIndex;
     record.acquisitionDefinitionIndex = value.acquisitionDefinitionIndex;
+    record.completionFlagDefinitionIndex = value.completionFlagDefinitionIndex;
     record.completionValueIndex = value.completionValueIndex;
     record.socketLane = value.socketLane;
     record.availability = static_cast<std::uint8_t>(value.availability);
@@ -36,6 +37,7 @@ bool decode(const ExoticCatalystRecord& record,
              record.completedPlugDefinitionIndex,
              record.effectDefinitionIndex,
              record.acquisitionDefinitionIndex,
+             record.completionFlagDefinitionIndex,
              record.completionValueIndex,
              record.socketLane,
              availability,

+ 3 - 2
Sunrise/src/state/build_data/cache/records/format.h

@@ -30,7 +30,7 @@ inline constexpr std::array<char, 8> kCacheMagic{'S', 'U', 'N', 'R', 'I', 'S', '
  * Bump it when a stored shape changes, and when the extraction filling it changes what it writes.
  * A cached row survives a code change, so a corrected walk keeps publishing the old rows.
  */
-inline constexpr std::uint32_t kCacheFormatVersion = 47;
+inline constexpr std::uint32_t kCacheFormatVersion = 48;
 /** Signed -1 on disk means there is no equipment slot. */
 inline constexpr std::int8_t kAbsentEquipmentSlot = -1;
 /** The standard 64-bit FNV-1a offset basis starts the payload checksum. */
@@ -212,6 +212,7 @@ struct ExoticCatalystRecord {
     std::uint16_t completedPlugDefinitionIndex{};
     std::uint16_t effectDefinitionIndex{};
     std::uint16_t acquisitionDefinitionIndex{};
+    std::uint16_t completionFlagDefinitionIndex{};
     std::uint16_t completionValueIndex{};
     std::uint8_t socketLane{};
     std::uint8_t availability{};
@@ -509,7 +510,7 @@ static_assert(sizeof(SocketPlugRuleRecord)
 static_assert(sizeof(SocketPlugPoolRecord) == 2 * sizeof(std::uint32_t));
 static_assert(sizeof(SocketPlugMemberRecord) == sizeof(std::uint16_t));
 static_assert(sizeof(ExoticCatalystRecord)
-              == 2 * sizeof(std::uint32_t) + 5 * sizeof(std::uint16_t)
+              == 2 * sizeof(std::uint32_t) + 6 * sizeof(std::uint16_t)
                      + 2 * sizeof(std::uint8_t));
 static_assert(sizeof(InventoryBucketRecord)
               == 4 * sizeof(std::uint8_t) + 2 * sizeof(std::uint16_t));

+ 7 - 1
Sunrise/src/state/build_data/items/catalysts/definition.h

@@ -10,6 +10,8 @@ namespace sunrise::state::build_data::items::catalysts {
 inline constexpr std::size_t kDefinitionCapacity = 128;
 /** All bits set mark a catalyst relation with no completion-value requirement. */
 inline constexpr std::uint16_t kUnavailableCompletionValueIndex = 0xFFFFU;
+/** All bits set mark a catalyst relation with no completion-flag requirement. */
+inline constexpr std::uint16_t kUnavailableCompletionFlagIndex = 0xFFFFU;
 /** All bits set mark a catalyst relation with no acquired-state gate. */
 inline constexpr std::uint16_t kUnavailableAcquisitionIndex = 0xFFFFU;
 
@@ -80,6 +82,8 @@ struct Definition {
     std::uint16_t effectDefinitionIndex{};
     /** Family-5 acquired-state slot that makes the catalyst socket visible. */
     std::uint16_t acquisitionDefinitionIndex{kUnavailableAcquisitionIndex};
+    /** Family-5 flag slot used by later catalyst completion expressions. */
+    std::uint16_t completionFlagDefinitionIndex{kUnavailableCompletionFlagIndex};
     /** Family-5 value slot used by legacy catalyst completion expressions. */
     std::uint16_t completionValueIndex{kUnavailableCompletionValueIndex};
     std::uint8_t socketLane{};
@@ -94,6 +98,7 @@ struct CompletedCatalyst {
     std::uint16_t completedPlugDefinitionIndex{};
     std::uint16_t effectDefinitionIndex{};
     std::uint16_t acquisitionDefinitionIndex{kUnavailableAcquisitionIndex};
+    std::uint16_t completionFlagDefinitionIndex{kUnavailableCompletionFlagIndex};
     std::uint16_t completionValueIndex{kUnavailableCompletionValueIndex};
     std::int32_t completionValue{};
 };
@@ -105,9 +110,10 @@ enum class CompletionConditionState : std::uint8_t {
     ambiguous = 2,
 };
 
-/** One item definition's build-derived legacy completion value. */
+/** One item definition's build-derived completion flag and value. */
 struct CompletionCondition {
     std::uint16_t itemDefinitionIndex{};
+    std::uint16_t flagDefinitionIndex{kUnavailableCompletionFlagIndex};
     std::uint16_t valueIndex{kUnavailableCompletionValueIndex};
     std::int32_t value{};
     CompletionConditionState state{CompletionConditionState::absent};

+ 36 - 21
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.cpp

@@ -18,6 +18,7 @@ struct LaneResult {
     std::uint16_t completedPlugDefinitionIndex{details::kUnavailableItemIndex};
     std::uint16_t effectDefinitionIndex{details::kUnavailableItemIndex};
     std::uint16_t acquisitionDefinitionIndex{kUnavailableAcquisitionIndex};
+    std::uint16_t completionFlagDefinitionIndex{kUnavailableCompletionFlagIndex};
     std::uint16_t completionValueIndex{kUnavailableCompletionValueIndex};
     std::int32_t completionValue{};
 };
@@ -161,30 +162,31 @@ find_acquisition_gate(std::span<const AcquisitionGate> definitions,
                 .completedPlugDefinitionIndex = completedPlugDefinitionIndex};
     }
 
-    std::uint16_t completionValueIndex = kUnavailableCompletionValueIndex;
-    std::int32_t completionValue = 0;
-    if (*effect != completedPlugDefinitionIndex) {
-        const CompletionCondition* condition =
-            find_completion_condition(source.completionConditions, *effect);
-        if (condition == nullptr || condition->state != CompletionConditionState::present
-            || condition->valueIndex == kUnavailableCompletionValueIndex
-            || condition->value <= 0) {
-            return {.catalyst = true,
-                    .error = Error::invalidCompletion,
-                    .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
-                    .effectDefinitionIndex = *effect,
-                    .acquisitionDefinitionIndex = acquisition->definitionIndex};
-        }
-        completionValueIndex = condition->valueIndex;
-        completionValue = condition->value;
+    const CompletionCondition* condition =
+        find_completion_condition(source.completionConditions, *effect);
+    const bool hasFlag = condition != nullptr
+                         && condition->flagDefinitionIndex
+                                != kUnavailableCompletionFlagIndex;
+    const bool hasValue = condition != nullptr
+                          && condition->valueIndex != kUnavailableCompletionValueIndex
+                          && condition->value > 0;
+    const bool directEffect = *effect == completedPlugDefinitionIndex;
+    if (condition == nullptr || condition->state != CompletionConditionState::present
+        || (directEffect && !hasFlag) || (!directEffect && !hasValue)) {
+        return {.catalyst = true,
+                .error = Error::invalidCompletion,
+                .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
+                .effectDefinitionIndex = *effect,
+                .acquisitionDefinitionIndex = acquisition->definitionIndex};
     }
     return {.catalyst = true,
             .error = Error::none,
             .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
             .effectDefinitionIndex = *effect,
             .acquisitionDefinitionIndex = acquisition->definitionIndex,
-            .completionValueIndex = completionValueIndex,
-            .completionValue = completionValue};
+            .completionFlagDefinitionIndex = condition->flagDefinitionIndex,
+            .completionValueIndex = condition->valueIndex,
+            .completionValue = condition->value};
 }
 
 /**
@@ -398,6 +400,7 @@ bool derive(const Source& source,
                                           result.completedPlugDefinitionIndex,
                                           result.effectDefinitionIndex,
                                           result.acquisitionDefinitionIndex,
+                                          result.completionFlagDefinitionIndex,
                                           result.completionValueIndex,
                                           result.completionValue};
         }
@@ -426,6 +429,7 @@ bool derive(const Source& source,
                                          details::kUnavailableItemIndex,
                                          details::kUnavailableItemIndex,
                                          kUnavailableAcquisitionIndex,
+                                         kUnavailableCompletionFlagIndex,
                                          kUnavailableCompletionValueIndex,
                                          detectedLane,
                                          Availability::unsupported,
@@ -460,6 +464,7 @@ bool derive(const Source& source,
                                      completed->completedPlugDefinitionIndex,
                                      completed->effectDefinitionIndex,
                                      completed->acquisitionDefinitionIndex,
+                                     completed->completionFlagDefinitionIndex,
                                      completed->completionValueIndex,
                                      completed->socketLane,
                                      release.has_value() ? Availability::released
@@ -503,6 +508,8 @@ bool matches_derived(const Source& source,
                    && left.completedPlugDefinitionIndex == right.completedPlugDefinitionIndex
                    && left.effectDefinitionIndex == right.effectDefinitionIndex
                    && left.acquisitionDefinitionIndex == right.acquisitionDefinitionIndex
+                   && left.completionFlagDefinitionIndex
+                          == right.completionFlagDefinitionIndex
                    && left.completionValueIndex == right.completionValueIndex
                    && left.socketLane == right.socketLane
                    && left.availability == right.availability
@@ -524,7 +531,11 @@ bool matches_cached(const Source& source,
         const details::Definition* detail = find_detail(source.details,
                                                         definition.itemDefinitionIndex);
         if (detail == nullptr || definition.socketLane >= detail->ordinarySocketCount
-            || find_item(source.items, definition.acquisitionDefinitionIndex) == nullptr) {
+            || find_item(source.items, definition.acquisitionDefinitionIndex) == nullptr
+            || (definition.completionFlagDefinitionIndex
+                        != kUnavailableCompletionFlagIndex
+                && find_item(source.items, definition.completionFlagDefinitionIndex)
+                       == nullptr)) {
             return false;
         }
 
@@ -544,7 +555,8 @@ bool matches_cached(const Source& source,
                 socketType, definition.acquisitionDefinitionIndex, AcquisitionState::present};
         }
 
-        if (definition.completionValueIndex == kUnavailableCompletionValueIndex) {
+        if (definition.completionFlagDefinitionIndex == kUnavailableCompletionFlagIndex
+            && definition.completionValueIndex == kUnavailableCompletionValueIndex) {
             continue;
         }
         const auto priorCondition = std::find_if(
@@ -554,7 +566,9 @@ bool matches_cached(const Source& source,
                 return condition.itemDefinitionIndex == definition.effectDefinitionIndex;
             });
         if (priorCondition != completionConditions.begin() + completionCount) {
-            if (priorCondition->valueIndex != definition.completionValueIndex
+            if (priorCondition->flagDefinitionIndex
+                    != definition.completionFlagDefinitionIndex
+                || priorCondition->valueIndex != definition.completionValueIndex
                 || priorCondition->value != definition.completionValue) {
                 return false;
             }
@@ -563,6 +577,7 @@ bool matches_cached(const Source& source,
         } else {
             completionConditions[completionCount++] = {
                 definition.effectDefinitionIndex,
+                definition.completionFlagDefinitionIndex,
                 definition.completionValueIndex,
                 definition.completionValue,
                 CompletionConditionState::present};

+ 21 - 11
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.cpp

@@ -4,6 +4,7 @@
 #include <atomic>
 
 #include "../../../account/inventory/item_state.h"
+#include "../../../unlocks/definition.h"
 #include "../../table.h"
 #include "../details/definition.h"
 
@@ -42,9 +43,8 @@ find(std::span<const Definition> definitions, std::uint16_t itemDefinitionIndex)
                : nullptr;
 }
 
-/** Sets one acquired-state slot and removes duplicate authored rows for that slot. */
-[[nodiscard]] bool upsert_acquisition(state::Family5State& family,
-                                      std::uint16_t slot) noexcept {
+/** Sets one flag slot and removes duplicate authored rows for that slot. */
+[[nodiscard]] bool upsert_flag(state::Family5State& family, std::uint16_t slot) noexcept {
     const std::size_t oldCount = family.flagCount;
     std::size_t write = 0;
     bool found = false;
@@ -52,7 +52,7 @@ find(std::span<const Definition> definitions, std::uint16_t itemDefinitionIndex)
         const state::UnlockFlagOverride row = family.flags[read];
         if (row.slot == slot) {
             if (!found) {
-                family.flags[write++] = {slot, 2};
+                family.flags[write++] = {slot, state::unlocks::kFlagSet};
                 found = true;
             }
             continue;
@@ -63,7 +63,7 @@ find(std::span<const Definition> definitions, std::uint16_t itemDefinitionIndex)
         if (write >= family.flags.size()) {
             return false;
         }
-        family.flags[write++] = {slot, 2};
+        family.flags[write++] = {slot, state::unlocks::kFlagSet};
     }
     for (std::size_t index = write; index < oldCount; ++index) {
         family.flags[index] = {};
@@ -135,22 +135,26 @@ bool valid(std::span<const Definition> definitions) noexcept {
             definition.effectDefinitionIndex != details::kUnavailableItemIndex;
         const bool hasAcquisition =
             definition.acquisitionDefinitionIndex != kUnavailableAcquisitionIndex;
+        const bool hasCompletionFlag = definition.completionFlagDefinitionIndex
+                                       != kUnavailableCompletionFlagIndex;
         const bool hasCompletionValue =
             definition.completionValueIndex != kUnavailableCompletionValueIndex;
+        const bool directEffect = definition.completedPlugDefinitionIndex
+                                  == definition.effectDefinitionIndex;
         if (definition.itemDefinitionHash == 0
             || definition.socketLane >= details::kInitialPlugCapacity
             || (definition.availability != Availability::released
                 && definition.availability != Availability::placeholder
                 && definition.availability != Availability::unsupported)
             || (definition.availability == Availability::unsupported
-                && (hasCompletedPlug || hasEffect || hasAcquisition || hasCompletionValue
-                    || definition.completionValue != 0))
+                && (hasCompletedPlug || hasEffect || hasAcquisition || hasCompletionFlag
+                    || hasCompletionValue || definition.completionValue != 0))
             || (definition.availability != Availability::unsupported
                 && (!hasCompletedPlug || !hasEffect || !hasAcquisition))
             || (hasCompletionValue != (definition.completionValue > 0))
-            || ((definition.completedPlugDefinitionIndex
-                 != definition.effectDefinitionIndex)
-                != hasCompletionValue)
+            || (definition.availability != Availability::unsupported
+                && ((directEffect && !hasCompletionFlag)
+                    || (!directEffect && !hasCompletionValue)))
             || (index != 0 && !less(definitions[index - 1], definition))) {
             return false;
         }
@@ -198,6 +202,7 @@ Result resolve(std::uint16_t itemDefinitionIndex) noexcept {
              definition->completedPlugDefinitionIndex,
              definition->effectDefinitionIndex,
              definition->acquisitionDefinitionIndex,
+             definition->completionFlagDefinitionIndex,
              definition->completionValueIndex,
              definition->completionValue}};
 }
@@ -262,7 +267,12 @@ bool append_investment_overrides(state::Family5State& family) noexcept {
             continue;
         }
 
-        if (!upsert_acquisition(candidate, definition.acquisitionDefinitionIndex)) {
+        if (!upsert_flag(candidate, definition.acquisitionDefinitionIndex)) {
+            return false;
+        }
+
+        if (definition.completionFlagDefinitionIndex != kUnavailableCompletionFlagIndex
+            && !upsert_flag(candidate, definition.completionFlagDefinitionIndex)) {
             return false;
         }
 

+ 1 - 1
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.h

@@ -71,7 +71,7 @@ apply_completed(std::uint16_t itemDefinitionIndex,
                 std::span<std::optional<std::uint16_t>> plugs) noexcept;
 
 /**
- * Adds the acquired-state gates and legacy completion values for all released catalysts.
+ * Adds acquired-state gates, completion flags, and completion values for released catalysts.
  * Existing authored rows with the same slot are raised to the required value. The input stays
  * unchanged when either fixed override bank cannot hold the complete deduplicated result.
  * @param family Candidate Family-5 state.

+ 0 - 121
tests/Sunrise.Tests.vcxproj

@@ -1,121 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <ItemGroup Label="ProjectConfigurations">
-    <ProjectConfiguration Include="Debug|x64">
-      <Configuration>Debug</Configuration>
-      <Platform>x64</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Release|x64">
-      <Configuration>Release</Configuration>
-      <Platform>x64</Platform>
-    </ProjectConfiguration>
-  </ItemGroup>
-  <PropertyGroup Label="Globals">
-    <VCProjectVersion>18.0</VCProjectVersion>
-    <ProjectGuid>{9D2A5866-9D2F-4A07-A88C-6E713A355C94}</ProjectGuid>
-    <RootNamespace>sunrise_tests</RootNamespace>
-    <WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
-    <ProjectName>Sunrise.Tests</ProjectName>
-  </PropertyGroup>
-  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
-  <PropertyGroup Label="Configuration">
-    <ConfigurationType>Application</ConfigurationType>
-    <PlatformToolset>v145</PlatformToolset>
-    <CharacterSet>Unicode</CharacterSet>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
-    <UseDebugLibraries>true</UseDebugLibraries>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
-    <UseDebugLibraries>false</UseDebugLibraries>
-  </PropertyGroup>
-  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
-  <PropertyGroup>
-    <OutDir>$(ProjectDir)..\build\$(Platform)\$(Configuration)\</OutDir>
-    <IntDir>$(ProjectDir)..\build\obj\$(Platform)\$(Configuration)\tests\</IntDir>
-  </PropertyGroup>
-  <ItemDefinitionGroup>
-    <ClCompile>
-      <LanguageStandard>stdcpp20</LanguageStandard>
-      <WarningLevel>Level4</WarningLevel>
-      <TreatWarningAsError>true</TreatWarningAsError>
-      <ConformanceMode>true</ConformanceMode>
-      <MultiProcessorCompilation>false</MultiProcessorCompilation>
-      <AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
-      <AdditionalIncludeDirectories>$(ProjectDir)..\Sunrise\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>WIN32;_WINDOWS;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
-    </ClCompile>
-  </ItemDefinitionGroup>
-  <ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
-    <ClCompile>
-      <Optimization>Disabled</Optimization>
-      <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
-    </ClCompile>
-    <Link>
-      <GenerateDebugInformation>true</GenerateDebugInformation>
-    </Link>
-  </ItemDefinitionGroup>
-  <ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
-    <ClCompile>
-      <Optimization>MaxSpeed</Optimization>
-      <FunctionLevelLinking>true</FunctionLevelLinking>
-      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
-    </ClCompile>
-    <Link>
-      <GenerateDebugInformation>true</GenerateDebugInformation>
-      <OptimizeReferences>true</OptimizeReferences>
-    </Link>
-  </ItemDefinitionGroup>
-  <ItemGroup>
-    <ClCompile Include="item_state_regression_tests.cpp" />
-    <ClCompile Include="exotic_catalyst_regression_tests.cpp" />
-    <ClCompile Include="loadout_catalyst_regression_tests.cpp" />
-    <ClCompile Include="opcode406_item_state_regression_tests.cpp" />
-    <ClCompile Include="..\Sunrise\src\client\content\items\packages\package_catalyst_condition_reader.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\json_number.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\json_scalar.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\json_structure.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state_settings.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\address_text.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\client\client_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\client\client_ui_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\client\external\client_external_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\server\activation\activation_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\server\entitlement_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\server\gameplay\gameplay_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\server\gameplay\gameplay_settings_validation.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\server\server_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\account_rows_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\account_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\activity_arrival_override_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\activity_default_destination_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\audio_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\controls_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\display_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\family5_override_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\interface_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\inventory_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\key_bindings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\state\social_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\core\settings\steam\steam_settings_parser.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\account\inventory\inventory_state.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\build_data\items\catalysts\exotic_catalyst_builder.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\build_data\items\catalysts\exotic_catalyst_generated.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\build_data\items\catalysts\exotic_catalyst_catalog.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\build_data\cache\records\cache_exotic_catalyst_record_codec.cpp" />
-    <ClCompile Include="..\Sunrise\src\middleware\datagen\family4\loadout\loadout_item_resolver.cpp" />
-    <ClCompile Include="..\Sunrise\src\middleware\datagen\family4\loadout\subclass_socket_selection.cpp" />
-    <ClCompile Include="..\Sunrise\src\middleware\datagen\character_record\appearance\character_appearance_banks.cpp" />
-    <ClCompile Include="..\Sunrise\src\middleware\datagen\character_record\appearance\character_appearance_render.cpp" />
-    <ClCompile Include="..\Sunrise\src\middleware\datagen\character_record\appearance\character_appearance_stats.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\build_data\items\item_catalog.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\build_data\items\details\item_detail_catalog.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\build_data\inventory\buckets\inventory_bucket_catalog.cpp" />
-    <ClCompile Include="..\Sunrise\src\state\build_data\socket_entry_lists\socket_entry_list_catalog.cpp" />
-    <ClCompile Include="..\Sunrise\src\middleware\encoding\bit_reader.cpp" />
-    <ClCompile Include="..\Sunrise\src\middleware\content\packages\tables\definition_index_table.cpp" />
-    <ClCompile Include="..\Sunrise\src\middleware\web_service\messages\opcode406_codec.cpp" />
-  </ItemGroup>
-  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
-</Project>

+ 0 - 630
tests/exotic_catalyst_regression_tests.cpp

@@ -1,630 +0,0 @@
-#include <array>
-#include <cstddef>
-#include <cstdint>
-#include <cstdio>
-#include <cstring>
-#include <optional>
-#include <span>
-
-#include "client/content/items/packages/package_socket_plug_build.h"
-#include "middleware/content/packages/tables/definition_index_table.h"
-#include "state/account/inventory/item_state.h"
-#include "state/build_data/cache/records/codec.h"
-#include "state/build_data/items/catalysts/exotic_catalyst_builder.h"
-#include "state/build_data/items/catalysts/exotic_catalyst_catalog.h"
-
-extern int failures;
-
-namespace {
-
-namespace catalysts = sunrise::state::build_data::items::catalysts;
-namespace cache_records = sunrise::state::build_data::cache::records;
-namespace package_items = sunrise::client::content::items::packages;
-namespace package_tables = sunrise::middleware::content::packages::tables;
-namespace details = sunrise::state::build_data::items::details;
-namespace items = sunrise::state::build_data::items;
-namespace socket_plugs = sunrise::state::build_data::items::socket_plugs;
-
-constexpr std::uint32_t kTimestamp = 0x5F43138BU;
-constexpr std::uint32_t kImageSize = 0x08A5EA00U;
-constexpr std::uint32_t kEmptyCatalyst = 0x5957A904U;
-constexpr std::uint32_t kWorldline = 0x6F22FCECU;
-constexpr std::uint32_t kWorldlineDefault = 0x9B283E92U;
-constexpr std::uint32_t kWorldlineProgress = 0x172766F5U;
-constexpr std::uint32_t kWorldlineComplete = 0x61935831U;
-constexpr std::uint32_t kWorldlineEffect = 0x9F651900U;
-constexpr std::uint32_t kWorldlineCategory = 0x06989D65U;
-constexpr std::uint16_t kWorldlineSocketType = 447;
-constexpr std::uint16_t kWorldlineAcquisition = 5044;
-constexpr std::uint32_t kWorldlineAcquisitionHash = 0x12345678U;
-constexpr std::uint16_t kWorldlineCompletionValue = 3167;
-constexpr std::int32_t kWorldlineCompletionThreshold = 4;
-constexpr std::uint32_t kWitherhoard = 0x8C8180D6U;
-constexpr std::uint32_t kWitherhoardComplete = 0xAC29C6ACU;
-constexpr std::uint32_t kWitherhoardCategory = 0x1234ABCDU;
-constexpr std::uint16_t kWitherhoardSocketType = 727;
-constexpr std::uint16_t kWitherhoardAcquisition = 11276;
-constexpr std::uint32_t kWishEnder = 0x3092080CU;
-constexpr std::uint32_t kWishEnderActive = 0xCE1AD8BBU;
-
-void expect(bool value, const char* label) noexcept {
-    if (!value) {
-        std::fprintf(stderr, "FAIL %s\n", label);
-        ++failures;
-    }
-}
-
-template <typename Value>
-void write_bytes(std::span<std::byte> output,
-                 std::size_t offset,
-                 const Value& value) noexcept {
-    std::memcpy(output.data() + offset, &value, sizeof value);
-}
-
-void write_completion_expression(std::span<std::byte> output,
-                                 std::size_t descriptorOffset,
-                                 std::size_t headerOffset,
-                                 std::uint32_t valueIndex,
-                                 std::uint32_t value) noexcept {
-    constexpr std::uint64_t kTokenCount = 3;
-    constexpr std::uint32_t kHeaderMarker = 0x80800001U;
-    const std::int64_t relative = static_cast<std::int64_t>(headerOffset)
-                                  - static_cast<std::int64_t>(descriptorOffset + 8);
-    write_bytes(output, descriptorOffset, kTokenCount);
-    write_bytes(output, descriptorOffset + 8, relative);
-    write_bytes(output, headerOffset - 4, kHeaderMarker);
-    write_bytes(output, headerOffset, kTokenCount);
-    write_bytes(output, headerOffset + 8, package_tables::kInvestmentExpressionRowClass);
-    const std::array<std::uint32_t, 6> tokens{
-        10, valueIndex, 11, value, 14, UINT32_MAX};
-    std::memcpy(output.data() + headerOffset + 16, tokens.data(), sizeof tokens);
-}
-
-items::Definition item(std::uint16_t index,
-                       std::uint32_t hash,
-                       items::Tier tier = items::Tier::none) noexcept {
-    items::Definition value{};
-    value.definitionIndex = index;
-    value.definitionHash = hash;
-    value.tier = static_cast<std::uint8_t>(tier);
-    return value;
-}
-
-details::Definition weapon_detail(std::uint16_t index,
-                                  std::uint32_t hash,
-                                  std::uint8_t lane,
-                                  std::uint16_t defaultPlug,
-                                  std::uint16_t socketType) noexcept {
-    details::Definition value{};
-    value.definitionIndex = index;
-    value.definitionHash = hash;
-    value.instancedDefinitionState = details::InstancedDefinitionState::instanced;
-    value.equipmentSlot = std::int8_t{7};
-    value.ordinarySocketState = details::OrdinarySocketState::present;
-    value.ordinarySocketCount = static_cast<std::uint8_t>(lane + 1);
-    value.initialPlugIndices[lane] = defaultPlug;
-    value.socketTypes[lane] = socketType;
-    return value;
-}
-
-struct Fixture {
-    std::array<items::Definition, 6> itemRows{};
-    std::array<details::Definition, 2> detailRows{};
-    std::array<socket_plugs::Rule, 1> rules{{{10, 7, 0, 1}}};
-    std::array<socket_plugs::Pool, 2> pools{{{0, 0}, {0, 3}}};
-    std::array<socket_plugs::Member, 3> members{{20, 21, 22}};
-    std::array<std::uint32_t, 1> legacy{{kWorldlineComplete}};
-    std::array<std::uint32_t, 1> released{{kWorldline}};
-    std::array<catalysts::CompletionCondition, 1> completionConditions{{{
-        23,
-        kWorldlineCompletionValue,
-        kWorldlineCompletionThreshold,
-        catalysts::CompletionConditionState::present,
-    }}};
-    std::array<catalysts::AcquisitionGate, 1> acquisitionGates{{{
-        kWorldlineSocketType,
-        kWorldlineAcquisition,
-        catalysts::AcquisitionState::present,
-    }}};
-    std::array<catalysts::Definition, catalysts::kDefinitionCapacity> output{};
-
-    Fixture() noexcept {
-        itemRows = {item(10, kWorldline, items::Tier::exotic),
-                    item(20, kWorldlineDefault),
-                    item(21, kWorldlineProgress),
-                    item(22, kWorldlineComplete),
-                    item(23, kWorldlineEffect, items::Tier::exotic),
-                    item(kWorldlineAcquisition, kWorldlineAcquisitionHash)};
-        for (std::size_t index = 1; index < itemRows.size() - 1; ++index) {
-            itemRows[index].plugCategoryHash = kWorldlineCategory;
-        }
-        detailRows[0] = weapon_detail(10, kWorldline, 7, 20, kWorldlineSocketType);
-        detailRows[1].definitionIndex = 23;
-        detailRows[1].definitionHash = kWorldlineEffect;
-        detailRows[1].instancedDefinitionState =
-            details::InstancedDefinitionState::stackable;
-        detailRows[1].sandboxPerkCount = 1;
-        detailRows[1].sandboxPerks[0] = 77;
-    }
-
-    [[nodiscard]] catalysts::Source source() const noexcept {
-        return {{kTimestamp, kImageSize, 0},
-                itemRows,
-                detailRows,
-                rules,
-                pools,
-                members,
-                completionConditions,
-                acquisitionGates};
-    }
-
-    [[nodiscard]] catalysts::Facts facts() const noexcept {
-        return {kTimestamp, kImageSize, kEmptyCatalyst, legacy, released};
-    }
-};
-
-void test_completion_expression_reader() noexcept {
-    std::array<std::byte, 192> definition{};
-    write_completion_expression(
-        definition, 0, 64, kWorldlineCompletionValue, kWorldlineCompletionThreshold);
-    catalysts::CompletionCondition condition{};
-    package_items::read_catalyst_completion_condition(definition, 23, condition);
-    expect(condition.itemDefinitionIndex == 23
-               && condition.valueIndex == kWorldlineCompletionValue
-               && condition.value == kWorldlineCompletionThreshold
-               && condition.state == catalysts::CompletionConditionState::present,
-           "native completion expression resolves");
-
-    write_completion_expression(
-        definition, 16, 128, kWorldlineCompletionValue, kWorldlineCompletionThreshold);
-    package_items::read_catalyst_completion_condition(definition, 23, condition);
-    expect(condition.state == catalysts::CompletionConditionState::present
-               && condition.valueIndex == kWorldlineCompletionValue
-               && condition.value == kWorldlineCompletionThreshold,
-           "duplicate native completion expressions remain one condition");
-
-    write_completion_expression(
-        definition, 16, 128, kWorldlineCompletionValue - 1, kWorldlineCompletionThreshold);
-    package_items::read_catalyst_completion_condition(definition, 23, condition);
-    expect(condition.state == catalysts::CompletionConditionState::ambiguous
-               && condition.valueIndex == catalysts::kUnavailableCompletionValueIndex
-               && condition.value == 0,
-           "distinct native completion expressions fail closed");
-
-    definition = {};
-    write_completion_expression(
-        definition, 0, 64, kWorldlineCompletionValue, kWorldlineCompletionThreshold);
-    constexpr std::uint32_t kWrongComparison = 13;
-    write_bytes(definition, 64 + 16 + 4 * sizeof(std::uint32_t), kWrongComparison);
-    package_items::read_catalyst_completion_condition(definition, 23, condition);
-    expect(condition.state == catalysts::CompletionConditionState::absent,
-           "non-completion postfix expressions stay absent");
-}
-
-void test_worldline_legacy_completion() noexcept {
-    Fixture fixture;
-    std::size_t count = 0;
-    catalysts::Report report{};
-    expect(catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report),
-           "Worldline catalog derives");
-    expect(count == 1, "Worldline catalog has one record");
-    expect(report.released == 1 && report.placeholder == 0 && report.unsupported == 0,
-           "Worldline report is released");
-    expect(catalysts::matches_derived(
-               fixture.source(), fixture.facts(), std::span(fixture.output).first(count)),
-           "Worldline stored catalog matches fresh derivation");
-    if (count != 1) {
-        return;
-    }
-    const catalysts::Definition& record = fixture.output[0];
-    expect(record.itemDefinitionHash == kWorldline, "Worldline record keeps weapon hash");
-    expect(record.itemDefinitionIndex == 10, "Worldline record keeps weapon index");
-    expect(record.socketLane == 7, "Worldline resolves lane 7");
-    expect(record.completedPlugDefinitionIndex == 22, "Worldline resolves completed plug");
-    expect(record.effectDefinitionIndex == 23, "Worldline resolves actual catalyst effect");
-    expect(record.acquisitionDefinitionIndex == kWorldlineAcquisition,
-           "Worldline resolves its acquired-state gate");
-    expect(record.completionValueIndex == kWorldlineCompletionValue
-               && record.completionValue == kWorldlineCompletionThreshold,
-           "Worldline resolves its native completion condition");
-    expect(record.availability == catalysts::Availability::released,
-           "Worldline is released");
-
-    catalysts::clear();
-    expect(catalysts::replace(std::span(fixture.output).first(count)),
-           "Worldline catalog publishes");
-    expect(catalysts::resolve_effect(10, 7, 22) == 23,
-           "Worldline display plug resolves actual catalyst effect");
-    expect(catalysts::resolve_effect(10, 7, 20) == 20,
-           "Worldline default plug has no catalyst effect");
-    for (std::uint32_t flags = 0; flags <= 3; ++flags) {
-        std::array<std::optional<std::uint16_t>, 12> plugs{};
-        plugs[7] = std::uint16_t{20};
-        std::uint32_t changedFlags = flags;
-        expect(catalysts::apply_completed(10, changedFlags, plugs)
-                   == catalysts::ApplyResult::completed,
-               "Worldline completion applies");
-        expect(changedFlags == (flags | sunrise::state::account::inventory::kMasterworkItemFlag),
-               "Worldline completion preserves item-state bits");
-        expect(plugs[7] == 23, "Worldline completion applies native effect plug with flag");
-    }
-    catalysts::set_completion_enabled(false);
-    std::array<std::optional<std::uint16_t>, 12> disabledPlugs{};
-    disabledPlugs[7] = std::uint16_t{20};
-    const auto disabledBefore = disabledPlugs;
-    std::uint32_t disabledFlags = 3;
-    expect(catalysts::apply_completed(10, disabledFlags, disabledPlugs)
-               == catalysts::ApplyResult::unchanged,
-           "global policy can disable catalyst completion");
-    expect(disabledFlags == 3 && disabledPlugs == disabledBefore,
-           "disabled catalyst completion leaves item unchanged");
-    catalysts::set_completion_enabled(true);
-}
-
-void test_investment_completion_is_atomic() noexcept {
-    Fixture fixture;
-    std::size_t count = 0;
-    catalysts::Report report{};
-    if (!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
-        || !catalysts::replace(std::span(fixture.output).first(count))) {
-        expect(false, "investment fixture publishes");
-        return;
-    }
-
-    sunrise::state::Family5State family{};
-    family.objectSoid = 0x4000000000000005ULL;
-    family.contentGateArm = true;
-    family.flags[0] = {77, 1};
-    family.flags[1] = {kWorldlineAcquisition, 0};
-    family.flags[2] = {kWorldlineAcquisition, 1};
-    family.flagCount = 3;
-    family.values[0] = {88, 7};
-    family.values[1] = {kWorldlineCompletionValue, 2};
-    family.values[2] = {kWorldlineCompletionValue, 9};
-    family.valueCount = 3;
-
-    expect(catalysts::append_investment_overrides(family),
-           "Worldline investment state completes");
-    expect(family.objectSoid == 0x4000000000000005ULL && family.contentGateArm,
-           "catalyst completion preserves Family 5 identity and arm state");
-    expect(family.flagCount == 2 && family.flags[0].slot == 77
-               && family.flags[0].value == 1
-               && family.flags[1].slot == kWorldlineAcquisition
-               && family.flags[1].value == 2,
-           "catalyst completion deduplicates and sets its acquired-state gate");
-    expect(family.valueCount == 2 && family.values[0].slot == 88
-               && family.values[0].value == 7
-               && family.values[1].slot == kWorldlineCompletionValue
-               && family.values[1].value == 9,
-           "catalyst completion deduplicates without lowering authored values");
-
-    catalysts::set_completion_enabled(false);
-    const std::size_t disabledFlagCount = family.flagCount;
-    const std::size_t disabledValueCount = family.valueCount;
-    const auto disabledFlags = family.flags;
-    const auto disabledValues = family.values;
-    expect(catalysts::append_investment_overrides(family),
-           "disabled catalyst investment completion succeeds without changes");
-    expect(family.flagCount == disabledFlagCount && family.valueCount == disabledValueCount
-               && family.flags[0].slot == disabledFlags[0].slot
-               && family.flags[0].value == disabledFlags[0].value
-               && family.values[0].slot == disabledValues[0].slot
-               && family.values[0].value == disabledValues[0].value,
-           "disabled catalyst investment completion leaves authored state unchanged");
-    catalysts::set_completion_enabled(true);
-
-    sunrise::state::Family5State full{};
-    full.flags[0] = {kWorldlineAcquisition, 0};
-    full.flagCount = 1;
-    full.valueCount = full.values.size();
-    for (std::size_t index = 0; index < full.valueCount; ++index) {
-        full.values[index] = {static_cast<std::uint16_t>(index),
-                              static_cast<std::int32_t>(index)};
-    }
-    const auto fullValues = full.values;
-    expect(!catalysts::append_investment_overrides(full),
-           "full completion-value bank rejects catalyst completion");
-    expect(full.flagCount == 1 && full.flags[0].value == 0
-               && full.valueCount == full.values.size()
-               && full.values[full.valueCount - 1].slot
-                      == fullValues[full.valueCount - 1].slot
-               && full.values[full.valueCount - 1].value
-                      == fullValues[full.valueCount - 1].value,
-           "failed investment completion rolls back both Family 5 banks");
-}
-
-void test_later_two_plug_and_placeholder() noexcept {
-    std::array<items::Definition, 3> itemRows{
-        item(10, kWitherhoard, items::Tier::exotic),
-        item(20, kEmptyCatalyst),
-        item(21, kWitherhoardComplete),
-    };
-    itemRows[1].plugCategoryHash = kWitherhoardCategory;
-    itemRows[2].plugCategoryHash = kWitherhoardCategory;
-    itemRows[2].tier = static_cast<std::uint8_t>(items::Tier::exotic);
-    std::array<details::Definition, 2> detailRows{
-        weapon_detail(10, kWitherhoard, 10, 20, kWitherhoardSocketType),
-        details::Definition{},
-    };
-    detailRows[1].definitionIndex = 21;
-    detailRows[1].definitionHash = kWitherhoardComplete;
-    detailRows[1].instancedDefinitionState = details::InstancedDefinitionState::stackable;
-    detailRows[1].sandboxPerkCount = 1;
-    detailRows[1].sandboxPerks[0] = 88;
-    std::array<socket_plugs::Rule, 1> rules{{{10, 10, 0, 1}}};
-    std::array<socket_plugs::Pool, 2> pools{{{0, 0}, {0, 2}}};
-    std::array<socket_plugs::Member, 2> members{{20, 21}};
-    std::array<std::uint32_t, 1> released{{kWitherhoard}};
-    std::array<catalysts::AcquisitionGate, 1> acquisitionGates{{{
-        kWitherhoardSocketType,
-        kWitherhoardAcquisition,
-        catalysts::AcquisitionState::present,
-    }}};
-    std::array<catalysts::Definition, catalysts::kDefinitionCapacity> output{};
-    const catalysts::Facts facts{kTimestamp, kImageSize, kEmptyCatalyst, {}, released};
-    const catalysts::Source source{
-        {kTimestamp, kImageSize, 0},
-        itemRows,
-        detailRows,
-        rules,
-        pools,
-        members,
-        {},
-        acquisitionGates};
-    std::size_t count = 0;
-    catalysts::Report report{};
-    expect(catalysts::derive(source, facts, output, count, report),
-           "Witherhoard catalog derives");
-    expect(count == 1 && output[0].socketLane == 10
-               && output[0].completedPlugDefinitionIndex == 21
-               && output[0].effectDefinitionIndex == 21
-               && output[0].acquisitionDefinitionIndex == kWitherhoardAcquisition
-               && output[0].completionValueIndex
-                      == catalysts::kUnavailableCompletionValueIndex,
-           "Witherhoard resolves later lifecycle");
-
-    itemRows[0].definitionHash = kWishEnder;
-    itemRows[2].definitionHash = kWishEnderActive;
-    detailRows[0].definitionHash = kWishEnder;
-    detailRows[1].definitionHash = kWishEnderActive;
-    const catalysts::Facts placeholderFacts{kTimestamp, kImageSize, kEmptyCatalyst, {}, {}};
-    count = 0;
-    report = {};
-    expect(catalysts::derive(source, placeholderFacts, output, count, report),
-           "Wish-Ender placeholder catalog derives");
-    expect(count == 1 && output[0].availability == catalysts::Availability::placeholder,
-           "Wish-Ender stays a placeholder");
-    expect(catalysts::replace(std::span(output).first(count)),
-           "Wish-Ender placeholder publishes");
-    const catalysts::Result result = catalysts::resolve(10);
-    expect(result.error == catalysts::Error::placeholderOnly,
-           "Wish-Ender does not resolve a completed catalyst");
-    std::array<std::optional<std::uint16_t>, 12> plugs{};
-    plugs[10] = std::uint16_t{20};
-    const auto before = plugs;
-    std::uint32_t flags = 3;
-    expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::unchanged,
-           "Wish-Ender completion is skipped");
-    expect(flags == 3 && plugs == before, "Wish-Ender state stays unchanged");
-}
-
-void test_exclusions_and_errors() noexcept {
-    Fixture fixture;
-    std::size_t count = 0;
-    catalysts::Report report{};
-
-    fixture.itemRows[0].tier = static_cast<std::uint8_t>(items::Tier::legendary);
-    const std::array<std::uint32_t, 0> noReleased{};
-    const catalysts::Facts noReleaseFacts{
-        kTimestamp, kImageSize, kEmptyCatalyst, fixture.legacy, noReleased};
-    expect(catalysts::derive(
-               fixture.source(), noReleaseFacts, fixture.output, count, report)
-               && count == 0,
-           "non-exotic weapon is excluded");
-
-    fixture.itemRows[0].tier = static_cast<std::uint8_t>(items::Tier::exotic);
-    fixture.detailRows[0].equipmentSlot = std::int8_t{10};
-    count = 0;
-    report = {};
-    expect(catalysts::derive(
-               fixture.source(), noReleaseFacts, fixture.output, count, report)
-               && count == 0,
-           "exotic armor is excluded");
-
-    fixture.detailRows[0].equipmentSlot = std::int8_t{7};
-    fixture.itemRows[2].definitionHash = kWorldlineComplete;
-    fixture.legacy = {kWorldlineComplete};
-    count = 0;
-    report = {};
-    expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
-               && count == 0 && report.error == catalysts::Error::ambiguousLifecycle,
-           "unclear legacy lifecycle fails without partial records");
-
-    fixture.itemRows[2].definitionHash = kWorldlineProgress;
-    fixture.members = {21, 22, 10};
-    count = 0;
-    report = {};
-    expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
-               && count == 0 && report.error == catalysts::Error::invalidPlug,
-           "default plug outside its pool fails");
-
-    fixture.members = {20, 21, 22};
-    fixture.itemRows[4].tier = static_cast<std::uint8_t>(items::Tier::none);
-    count = 0;
-    report = {};
-    expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
-               && count == 0 && report.error == catalysts::Error::invalidEffect,
-           "missing legacy catalyst effect fails");
-    fixture.itemRows[4].tier = static_cast<std::uint8_t>(items::Tier::exotic);
-
-    fixture.acquisitionGates[0].state = catalysts::AcquisitionState::ambiguous;
-    count = 0;
-    report = {};
-    expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
-               && count == 0 && report.error == catalysts::Error::invalidAcquisition,
-           "unclear acquired-state gate fails a released catalyst");
-    fixture.acquisitionGates[0].state = catalysts::AcquisitionState::present;
-
-    fixture.completionConditions[0].state = catalysts::CompletionConditionState::ambiguous;
-    count = 0;
-    report = {};
-    expect(!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
-               && count == 0 && report.error == catalysts::Error::invalidCompletion,
-           "unclear completion expression fails a released catalyst");
-    fixture.completionConditions[0].state = catalysts::CompletionConditionState::present;
-
-    catalysts::Source wrongBuild = fixture.source();
-    wrongBuild.build.imageTimestamp ^= 1U;
-    count = 0;
-    report = {};
-    expect(!catalysts::derive(wrongBuild, fixture.facts(), fixture.output, count, report)
-               && count == 0 && report.error == catalysts::Error::unsupportedBuild,
-           "build fingerprint mismatch fails");
-}
-
-void test_unclear_placeholder_is_safe() noexcept {
-    Fixture fixture;
-    fixture.itemRows[2].definitionHash = kWorldlineComplete;
-    const std::array<std::uint32_t, 0> noReleased{};
-    const catalysts::Facts facts{
-        kTimestamp, kImageSize, kEmptyCatalyst, fixture.legacy, noReleased};
-    std::size_t count = 0;
-    catalysts::Report report{};
-    expect(catalysts::derive(fixture.source(), facts, fixture.output, count, report),
-           "unclear placeholder does not abort the catalog");
-    expect(count == 1 && report.unsupported == 1 && report.error == catalysts::Error::none,
-           "unclear placeholder is reported as unsupported");
-    if (count != 1) {
-        return;
-    }
-    expect(fixture.output[0].availability == catalysts::Availability::unsupported
-               && fixture.output[0].completedPlugDefinitionIndex
-                      == details::kUnavailableItemIndex
-               && fixture.output[0].effectDefinitionIndex
-                      == details::kUnavailableItemIndex,
-           "unclear placeholder has no completed state");
-    expect(catalysts::replace(std::span(fixture.output).first(count)),
-           "unsupported placeholder catalog publishes");
-    std::array<std::optional<std::uint16_t>, 12> plugs{};
-    plugs[7] = std::uint16_t{20};
-    const auto before = plugs;
-    std::uint32_t flags = 3;
-    expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::unchanged,
-           "unsupported placeholder is skipped");
-    expect(flags == 3 && plugs == before, "unsupported placeholder stays unchanged");
-}
-
-void test_cache_record_round_trip() noexcept {
-    const catalysts::Definition released{
-        kWorldline,
-        10,
-        22,
-        23,
-        kWorldlineAcquisition,
-        kWorldlineCompletionValue,
-        7,
-        catalysts::Availability::released,
-        kWorldlineCompletionThreshold};
-    cache_records::ExoticCatalystRecord record{};
-    catalysts::Definition decoded{};
-    expect(cache_records::encode(released, record), "released catalyst cache record encodes");
-    expect(cache_records::decode(record, decoded), "released catalyst cache record decodes");
-    expect(decoded.itemDefinitionHash == released.itemDefinitionHash
-               && decoded.itemDefinitionIndex == released.itemDefinitionIndex
-               && decoded.completedPlugDefinitionIndex
-                      == released.completedPlugDefinitionIndex
-               && decoded.effectDefinitionIndex == released.effectDefinitionIndex
-               && decoded.acquisitionDefinitionIndex
-                      == released.acquisitionDefinitionIndex
-               && decoded.completionValueIndex == released.completionValueIndex
-               && decoded.socketLane == released.socketLane
-               && decoded.availability == released.availability
-               && decoded.completionValue == released.completionValue,
-           "released catalyst cache record round-trips");
-
-    const catalysts::Definition unsupported{kWishEnder,
-                                             11,
-                                             details::kUnavailableItemIndex,
-                                             details::kUnavailableItemIndex,
-                                             catalysts::kUnavailableAcquisitionIndex,
-                                             catalysts::kUnavailableCompletionValueIndex,
-                                             10,
-                                             catalysts::Availability::unsupported,
-                                             0};
-    expect(cache_records::encode(unsupported, record),
-           "unsupported catalyst cache record encodes");
-    expect(cache_records::decode(record, decoded)
-               && decoded.availability == catalysts::Availability::unsupported
-               && decoded.completedPlugDefinitionIndex == details::kUnavailableItemIndex
-               && decoded.effectDefinitionIndex == details::kUnavailableItemIndex,
-           "unsupported catalyst cache record round-trips");
-    record.availability = 0xFFU;
-    expect(!cache_records::decode(record, decoded),
-           "noncanonical catalyst cache record is rejected");
-    expect(cache_records::kCacheFormatVersion == 47,
-           "catalyst records use the next cache format");
-}
-
-void test_cached_catalog_revalidation() noexcept {
-    Fixture fixture;
-    std::size_t count = 0;
-    catalysts::Report report{};
-    if (!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)) {
-        expect(false, "cache validation fixture derives");
-        return;
-    }
-    catalysts::Source cachedSource = fixture.source();
-    cachedSource.completionConditions = {};
-    cachedSource.acquisitionGates = {};
-    expect(catalysts::matches_cached(cachedSource,
-                                     fixture.facts(),
-                                     std::span(fixture.output).first(count)),
-           "cached catalyst catalog re-derives from stored relations");
-
-    fixture.output[0].acquisitionDefinitionIndex = kWorldlineAcquisition - 1;
-    expect(!catalysts::matches_cached(cachedSource,
-                                      fixture.facts(),
-                                      std::span(fixture.output).first(count)),
-           "cached catalyst catalog rejects an absent acquisition item");
-}
-
-void test_atomic_rollback() noexcept {
-    Fixture fixture;
-    std::size_t count = 0;
-    catalysts::Report report{};
-    if (!catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report)
-        || !catalysts::replace(std::span(fixture.output).first(count))) {
-        expect(false, "rollback fixture publishes");
-        return;
-    }
-
-    std::array<std::optional<std::uint16_t>, 12> plugs{};
-    plugs[7] = std::uint16_t{20};
-    const auto before = plugs;
-    std::uint32_t flags = 0x8U;
-    expect(catalysts::apply_completed(10, flags, plugs) == catalysts::ApplyResult::failed,
-           "invalid item state rejects completion");
-    expect(flags == 0x8U && plugs == before, "invalid item state rolls back both fields");
-
-    std::array<std::optional<std::uint16_t>, 7> shortPlugs{};
-    const auto shortBefore = shortPlugs;
-    flags = 3;
-    expect(catalysts::apply_completed(10, flags, shortPlugs) == catalysts::ApplyResult::failed,
-           "missing catalyst lane rejects completion");
-    expect(flags == 3 && shortPlugs == shortBefore, "missing lane rolls back both fields");
-}
-
-} // namespace
-
-void test_exotic_catalysts() noexcept {
-    test_completion_expression_reader();
-    test_worldline_legacy_completion();
-    test_investment_completion_is_atomic();
-    test_later_two_plug_and_placeholder();
-    test_exclusions_and_errors();
-    test_unclear_placeholder_is_safe();
-    test_atomic_rollback();
-    test_cache_record_round_trip();
-    test_cached_catalog_revalidation();
-    catalysts::clear();
-}

+ 0 - 156
tests/item_state_regression_tests.cpp

@@ -1,156 +0,0 @@
-#include <Windows.h>
-
-#include <crtdbg.h>
-
-#include <cstddef>
-#include <cstdint>
-#include <cstdio>
-#include <span>
-#include <string_view>
-
-#include "core/settings/parser.h"
-#include "core/settings/settings.h"
-
-namespace sunrise::core::settings::parser {
-
-struct ParserTestAccess {
-    static bool equipment_item(Parser& parser,
-                               state::account::inventory::Item& item) noexcept {
-        return parser.equipment_item(item);
-    }
-
-    static bool at_end(Parser& parser) noexcept { return parser.at_end(); }
-};
-
-} // namespace sunrise::core::settings::parser
-
-namespace sunrise::core::log {
-
-Settings defaults() noexcept { return {}; }
-
-void early(std::string_view) noexcept {}
-
-void write(Channel, Level, std::string_view) noexcept {}
-
-} // namespace sunrise::core::log
-
-namespace sunrise::state::activity::defaults {
-
-ActivityDefaults authored() noexcept { return {}; }
-
-bool valid(const DefaultDestination&) noexcept { return true; }
-
-bool valid(const ActivityDefaults&) noexcept { return true; }
-
-} // namespace sunrise::state::activity::defaults
-
-namespace sunrise::state::entitlements {
-
-Table authored() noexcept { return {}; }
-
-bool valid(const Entitlement&) noexcept { return true; }
-
-bool valid(const Table&) noexcept { return true; }
-
-} // namespace sunrise::state::entitlements
-
-namespace sunrise::state::account {
-
-bool valid_authored(const AccountState&) noexcept { return true; }
-
-} // namespace sunrise::state::account
-
-namespace sunrise::state::account::settings {
-
-bool valid(const AccountSettings&) noexcept { return true; }
-
-} // namespace sunrise::state::account::settings
-
-int failures{};
-
-namespace {
-
-void expect(bool value, const char* label) noexcept {
-    if (!value) {
-        std::fprintf(stderr, "FAIL %s\n", label);
-        ++failures;
-    }
-}
-
-bool parse_item_flags(std::uint32_t flags,
-                      sunrise::state::account::inventory::Item& item) noexcept {
-    char json[192]{};
-    const int written = std::snprintf(
-        json,
-        sizeof json,
-        R"({"instance_soid":"0x1","definition_hash":"0x12345678",)"
-        R"("level":50,"quantity":1,"plugs":[],"flags":%u})",
-        flags);
-    if (written <= 0 || static_cast<std::size_t>(written) >= sizeof json) {
-        return false;
-    }
-
-    sunrise::core::settings::parser::Parser parser(
-        std::string_view{json, static_cast<std::size_t>(written)});
-    return sunrise::core::settings::parser::ParserTestAccess::equipment_item(parser, item)
-           && sunrise::core::settings::parser::ParserTestAccess::at_end(parser);
-}
-
-void test_catalyst_item_state_flags() noexcept {
-    for (std::uint32_t flags = 0; flags <= 0x7U; ++flags) {
-        sunrise::state::account::inventory::Item item{};
-        expect(parse_item_flags(flags, item), "parser accepts known item-state bits");
-        expect(item.flags == flags, "parser preserves known item-state bits");
-    }
-
-    sunrise::state::account::inventory::Item item{};
-    expect(!parse_item_flags(0x8U, item), "parser rejects unknown item-state bits");
-}
-
-bool parse_catalyst_policy(std::string_view json, bool& output) noexcept {
-    sunrise::core::settings::Settings settings{};
-    if (!sunrise::core::settings::parse(json, settings)) {
-        return false;
-    }
-    output = settings.completeExoticCatalysts;
-    return true;
-}
-
-void test_catalyst_policy_setting() noexcept {
-    bool enabled = true;
-    expect(parse_catalyst_policy(R"({"version":8,"complete_exotic_catalysts":false})", enabled)
-               && !enabled,
-           "global catalyst policy parses false");
-    expect(parse_catalyst_policy(R"({"version":8})", enabled) && enabled,
-           "missing global catalyst policy keeps its default");
-    expect(!parse_catalyst_policy(
-               R"({"complete_exotic_catalysts":true,"complete_exotic_catalysts":false})",
-               enabled),
-           "duplicate global catalyst policy is rejected");
-    expect(!parse_catalyst_policy(R"({"complete_exotic_catalysts":1})", enabled),
-           "non-boolean global catalyst policy is rejected");
-}
-
-} // namespace
-
-void test_exotic_catalysts() noexcept;
-void test_resolved_catalyst_output() noexcept;
-void test_opcode406_item_state() noexcept;
-
-int main() {
-#if defined(_DEBUG)
-    _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
-    _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
-#endif
-    test_catalyst_item_state_flags();
-    test_catalyst_policy_setting();
-    test_opcode406_item_state();
-    test_exotic_catalysts();
-    test_resolved_catalyst_output();
-    if (failures != 0) {
-        std::fprintf(stderr, "%d regression test(s) failed\n", failures);
-        return 1;
-    }
-    std::puts("All regression tests passed");
-    return 0;
-}

+ 0 - 224
tests/loadout_catalyst_regression_tests.cpp

@@ -1,224 +0,0 @@
-#include <array>
-#include <cstdint>
-#include <cstdio>
-#include <optional>
-#include <span>
-
-#include "middleware/datagen/character_record/appearance/internal.h"
-#include "middleware/datagen/family4/loadout/loadout_item_resolver.h"
-#include "state/build_data/inventory/buckets/inventory_bucket_catalog.h"
-#include "state/build_data/items/catalysts/exotic_catalyst_catalog.h"
-#include "state/build_data/items/details/item_detail_catalog.h"
-#include "state/build_data/items/item_catalog.h"
-#include "state/build_data/runtime.h"
-#include "state/build_data/socket_entry_lists/socket_entry_list_catalog.h"
-
-extern int failures;
-
-namespace sunrise::state::build_data {
-
-bool find_item_definition_hash(std::uint32_t definitionHash,
-                               items::Definition& definition) noexcept {
-    return items::find_hash(definitionHash, definition);
-}
-
-bool find_configured_item_detail(std::uint16_t definitionIndex,
-                                 items::details::Definition& definition) noexcept {
-    return items::details::find(definitionIndex, definition);
-}
-
-bool find_inventory_bucket_descriptor(
-    std::uint8_t bucketId,
-    inventory::buckets::Descriptor& descriptor) noexcept {
-    return inventory::buckets::find(bucketId, descriptor);
-}
-
-bool find_socket_entry_list(std::uint16_t definitionIndex,
-                            socket_entry_lists::Definition& definition) noexcept {
-    return socket_entry_lists::find(definitionIndex, definition);
-}
-
-bool find_socket_entry_table(std::uint16_t definitionIndex,
-                             socket_entry_lists::EntryTable& table) noexcept {
-    return socket_entry_lists::find_entry_table(definitionIndex, table);
-}
-
-items::catalysts::ApplyResult complete_exotic_catalyst(
-    std::uint16_t itemDefinitionIndex,
-    std::uint32_t& flags,
-    std::span<std::optional<std::uint16_t>> plugs) noexcept {
-    return items::catalysts::apply_completed(itemDefinitionIndex, flags, plugs);
-}
-
-bool find_investment_constants(constants::InvestmentConstants& value) noexcept {
-    value = {.extracted = true,
-             .lightStatRow = 1,
-             .characterStatRows = {2, 3, 4, 5, 6, 7}};
-    return true;
-}
-
-std::uint16_t resolve_exotic_catalyst_effect(std::uint16_t itemDefinitionIndex,
-                                             std::uint8_t socketLane,
-                                             std::uint16_t plugDefinitionIndex) noexcept {
-    return items::catalysts::resolve_effect(
-        itemDefinitionIndex, socketLane, plugDefinitionIndex);
-}
-
-} // namespace sunrise::state::build_data
-
-namespace {
-
-namespace buckets = sunrise::state::build_data::inventory::buckets;
-namespace catalysts = sunrise::state::build_data::items::catalysts;
-namespace details = sunrise::state::build_data::items::details;
-namespace items = sunrise::state::build_data::items;
-namespace loadout = sunrise::middleware::datagen::family4::loadout;
-namespace appearance = sunrise::middleware::datagen::character_record::appearance;
-namespace character_layout = sunrise::middleware::datagen::character_record::layout;
-namespace socket_lists = sunrise::state::build_data::socket_entry_lists;
-
-constexpr std::uint32_t kWeaponHash = 0x12345678U;
-constexpr std::uint32_t kDefaultPlugHash = 0x23456789U;
-constexpr std::uint32_t kProgressPlugHash = 0x3456789AU;
-constexpr std::uint32_t kCompletedPlugHash = 0x456789ABU;
-constexpr std::uint32_t kEffectHash = 0x56789ABCU;
-constexpr std::uint32_t kCatalystCategory = 0x6789ABCDU;
-constexpr std::uint16_t kCatalystPerk = 77;
-constexpr std::uint8_t kCatalystStatRow = 24;
-constexpr std::int32_t kCatalystStatValue = 7;
-
-void expect(bool value, const char* label) noexcept {
-    if (!value) {
-        std::fprintf(stderr, "FAIL %s\n", label);
-        ++failures;
-    }
-}
-
-items::Definition item(std::uint16_t index,
-                       std::uint32_t hash,
-                       items::Tier tier = items::Tier::none) noexcept {
-    items::Definition value{};
-    value.definitionHash = hash;
-    value.definitionIndex = index;
-    value.tier = static_cast<std::uint8_t>(tier);
-    return value;
-}
-
-void clear_catalogs() noexcept {
-    catalysts::clear();
-    socket_lists::clear();
-    buckets::clear();
-    details::clear();
-    items::clear();
-}
-
-} // namespace
-
-void test_resolved_catalyst_output() noexcept {
-    clear_catalogs();
-    std::array<items::Definition, 5> itemRows{
-        item(0, kWeaponHash, items::Tier::exotic),
-        item(1, kDefaultPlugHash),
-        item(2, kProgressPlugHash),
-        item(3, kCompletedPlugHash),
-        item(4, kEffectHash, items::Tier::exotic),
-    };
-    itemRows[0].bucketId = 0;
-    itemRows[3].plugCategoryHash = kCatalystCategory;
-    itemRows[4].plugCategoryHash = kCatalystCategory;
-
-    details::Definition detail{};
-    detail.definitionIndex = 0;
-    detail.definitionHash = kWeaponHash;
-    detail.bucketId = 0;
-    detail.maxStackSize = 1;
-    detail.instancedDefinitionState = details::InstancedDefinitionState::instanced;
-    detail.equipmentSlot = std::int8_t{7};
-    detail.ordinarySocketState = details::OrdinarySocketState::present;
-    detail.ordinarySocketCount = 8;
-    detail.initialPlugIndices[7] = 1;
-    detail.socketEntryListIndex = 0;
-    details::Definition effectDetail{};
-    effectDetail.definitionIndex = 4;
-    effectDetail.definitionHash = kEffectHash;
-    effectDetail.bucketId = 0;
-    effectDetail.maxStackSize = 1;
-    effectDetail.instancedDefinitionState = details::InstancedDefinitionState::stackable;
-    effectDetail.sandboxPerkCount = 1;
-    effectDetail.sandboxPerks[0] = kCatalystPerk;
-    effectDetail.statCount = 1;
-    effectDetail.stats[0] = {kCatalystStatRow, kCatalystStatValue};
-    const std::array detailRows{detail, effectDetail};
-
-    const std::array bucketRows{
-        buckets::Descriptor{0, buckets::ArraySelector::character, 0, 10, 7, 0},
-    };
-    const std::array socketListRows{
-        socket_lists::Definition{0x12345678U, 0, 0, 0},
-    };
-    const std::array catalystRows{
-        catalysts::Definition{kWeaponHash,
-                              0,
-                              3,
-                              4,
-                              500,
-                              600,
-                              7,
-                              catalysts::Availability::released,
-                              4},
-    };
-
-    const bool ready = items::replace(itemRows) && details::replace(detailRows)
-                       && buckets::replace(bucketRows)
-                       && socket_lists::replace(socketListRows)
-                       && catalysts::replace(catalystRows);
-    expect(ready, "resolved-output fixture publishes");
-    if (!ready) {
-        clear_catalogs();
-        return;
-    }
-
-    sunrise::state::account::inventory::Item authored{};
-    authored.instanceSoid = 1;
-    authored.definitionHash = kWeaponHash;
-    authored.level = 50;
-    authored.quantity = 1;
-    authored.flags = 3;
-    sunrise::state::CharacterState character{};
-    loadout::Candidate output{};
-    expect(loadout::resolve_item(
-               authored, character, itemRows.size(), socketListRows.size(), output),
-           "resolved client item accepts catalyst completion");
-    expect(output.item.flags == 7, "resolved client item carries all item-state bits");
-    expect(output.item.instance.ordinarySockets.plugs[7] == 4,
-           "resolved client item carries the native catalyst effect plug");
-
-    loadout::ResolvedInstances instances{};
-    instances.itemCount = 1;
-    instances.items[0].equipmentSlot = 7;
-    instances.items[0].instance = output.item.instance;
-    character_layout::Appearance appearanceRecord{};
-    appearanceRecord.smallBankA.fill(character_layout::kEmptyDefinitionIndex);
-    appearanceRecord.overflowHashes.fill(character_layout::kNoHash);
-    appearance::apply_perk_banks(instances, appearanceRecord);
-    expect(appearanceRecord.smallBankA[0] == kCatalystPerk,
-           "Worldline display plug publishes the actual catalyst perk");
-    appearance::apply_overflow_hashes(instances, appearanceRecord);
-    expect(appearanceRecord.overflowHashes[0] == kEffectHash,
-           "Worldline display plug publishes the actual catalyst hash");
-    expect(appearance::apply_stats(instances, 50, appearanceRecord),
-           "Worldline display plug builds catalyst stats");
-    expect(appearanceRecord.weaponStats[0][0].key
-                   == static_cast<std::int8_t>(kCatalystStatRow)
-               && appearanceRecord.weaponStats[0][0].value == kCatalystStatValue,
-           "Worldline display plug publishes the actual catalyst stat");
-
-    authored.flags = 0x8U;
-    output.item.flags = 0xA5A5U;
-    expect(!loadout::resolve_item(
-               authored, character, itemRows.size(), socketListRows.size(), output),
-           "resolved client item rejects an invalid accumulated state");
-    expect(output.item.flags == 0xA5A5U,
-           "failed resolved client item leaves caller output unchanged");
-    clear_catalogs();
-}

+ 0 - 68
tests/opcode406_item_state_regression_tests.cpp

@@ -1,68 +0,0 @@
-#include <array>
-#include <cstddef>
-#include <cstdint>
-#include <cstdio>
-#include <span>
-
-#include "middleware/web_service/messages/opcode406.h"
-
-extern int failures;
-
-namespace {
-
-namespace opcode406 = sunrise::middleware::web_service::messages::opcode406;
-
-void expect(bool value, const char* label) noexcept {
-    if (!value) {
-        std::fprintf(stderr, "FAIL %s\n", label);
-        ++failures;
-    }
-}
-
-void write_bits(std::span<std::byte> output,
-                std::size_t& position,
-                std::uint64_t value,
-                std::uint8_t width) noexcept {
-    for (std::uint8_t index = 0; index < width; ++index) {
-        const unsigned shift = static_cast<unsigned>(width - index - 1);
-        const auto bit = static_cast<unsigned>((value >> shift) & 1U);
-        const std::size_t byteIndex = position / 8;
-        const unsigned byteShift = static_cast<unsigned>(7 - (position % 8));
-        output[byteIndex] |= std::byte{static_cast<unsigned char>(bit << byteShift)};
-        ++position;
-    }
-}
-
-std::array<std::byte, 15> payload(std::uint32_t flags) noexcept {
-    std::array<std::byte, 15> output{};
-    std::size_t position = 0;
-    write_bits(output, position, 1, 1);
-    write_bits(output, position, 0x1234U, 64);
-    write_bits(output, position, 1, 1);
-    write_bits(output, position, 42, 15);
-    write_bits(output, position, 0x80000000ULL + flags, 32);
-    write_bits(output, position, 0, 7);
-    return output;
-}
-
-} // namespace
-
-void test_opcode406_item_state() noexcept {
-    for (std::uint32_t flags = 0; flags <= 7; ++flags) {
-        const auto bytes = payload(flags);
-        const sunrise::middleware::web_service::Message message{
-            opcode406::kOpcode, 1, bytes};
-        opcode406::Request request{};
-        expect(opcode406::parse_request(message, request),
-               "opcode 406 accepts known item-state bits");
-        expect(request.instanceSoid == 0x1234U && request.definitionIndex == 42
-                   && request.flags == flags,
-               "opcode 406 preserves known item-state bits");
-    }
-
-    const auto bytes = payload(8);
-    const sunrise::middleware::web_service::Message message{opcode406::kOpcode, 1, bytes};
-    opcode406::Request request{};
-    expect(!opcode406::parse_request(message, request),
-           "opcode 406 rejects unknown item-state bits");
-}