Browse Source

PR cleanup and merge

stan 1 day ago
parent
commit
1da7f7a86c
27 changed files with 456 additions and 440 deletions
  1. 2 0
      Sunrise/Sunrise.vcxproj
  2. 152 0
      Sunrise/src/client/content/activity/activity_sdk_authored_scene_identity.cpp
  3. 5 0
      Sunrise/src/client/content/activity/activity_sdk_authored_scene_internal.h
  4. 24 162
      Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.cpp
  5. 16 16
      Sunrise/src/client/content/activity/activity_sdk_live_publication.cpp
  6. 1 1
      Sunrise/src/client/content/activity/activity_sdk_lua_contract.cpp
  7. 1 121
      Sunrise/src/client/content/activity/activity_sdk_lua_missions.cpp
  8. 137 0
      Sunrise/src/client/content/activity/activity_sdk_lua_missions_catalog_constants.cpp
  9. 6 0
      Sunrise/src/client/content/activity/activity_sdk_lua_missions_internal.h
  10. 1 2
      Sunrise/src/client/content/activity/activity_sdk_lua_publish.cpp
  11. 1 2
      Sunrise/src/client/content/activity/activity_sdk_tree_publication.cpp
  12. 1 2
      Sunrise/src/client/content/items/packages/package_subclass_build.cpp
  13. 14 30
      Sunrise/src/client/hooks/stall_probe/stall_probe.cpp
  14. 3 2
      Sunrise/src/server/activity/activity_sdk_mission_internal.h
  15. 4 2
      Sunrise/src/server/activity/activity_sdk_mission_prepare.cpp
  16. 6 4
      Sunrise/src/server/activity/activity_sdk_mission_runtime.cpp
  17. 2 0
      Sunrise/src/server/activity/activity_sdk_mission_runtime.h
  18. 5 6
      Sunrise/src/server/activity/activity_sdk_scene_cast.cpp
  19. 2 5
      Sunrise/src/server/activity/activity_sdk_scene_dependencies.h
  20. 11 21
      Sunrise/src/server/activity/activity_sdk_scene_spawn.cpp
  21. 11 11
      Sunrise/src/server/activity/activity_sdk_scene_spawn.h
  22. 0 10
      Sunrise/src/server/ui/activity_host/activity_host_table_layout.h
  23. 10 7
      Sunrise/src/server/ui/activity_host/activity_host_tool_window.h
  24. 5 6
      Sunrise/src/server/web_service/web_service_vendor_actions.cpp
  25. 2 2
      Sunrise/src/state/activity_sdk/activity_sdk_relation_validation.cpp
  26. 3 7
      Sunrise/src/state/build_data/vendors/definition.h
  27. 31 21
      Sunrise/src/state/runtime/state_account_acquisition_runtime.cpp

+ 2 - 0
Sunrise/Sunrise.vcxproj

@@ -709,6 +709,7 @@
     <ClCompile Include="src\client\content\activity\activity_sdk_activity_enrichment_inventory.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_activity_enrichment_inventory.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_actor_rsat_inventory.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_actor_rsat_inventory.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_authored_scene_facts.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_authored_scene_facts.cpp" />
+    <ClCompile Include="src\client\content\activity\activity_sdk_authored_scene_identity.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_authored_scene_inventory.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_authored_scene_inventory.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_behavior_inventory.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_behavior_inventory.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_activities.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_activities.cpp" />
@@ -2130,6 +2131,7 @@
     <ClCompile Include="src\client\content\activity\scriptable_catalog_object_analysis.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_object_analysis.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_authored_scene_validation.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_authored_scene_validation.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_missions.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_missions.cpp" />
+    <ClCompile Include="src\client\content\activity\activity_sdk_lua_missions_catalog_constants.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_container_placement_cache.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_container_placement_cache.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_native_pack_text.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_native_pack_text.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_policy_inventory_capabilities.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_policy_inventory_capabilities.cpp" />

+ 152 - 0
Sunrise/src/client/content/activity/activity_sdk_authored_scene_identity.cpp

@@ -0,0 +1,152 @@
+#include "activity_sdk_authored_scene_internal.h"
+#include "activity_sdk_authored_scene_inventory.h"
+
+namespace sunrise::client::content::activity::sdk_generation::authored_scene_inventory {
+
+/** Formats one resource ID from its exact descriptor tuple. */
+bool resource_id(const topology::Snapshot& topology,
+                 const squad::DescriptorFact& descriptor,
+                 Text& output) noexcept {
+    if (descriptor.objectIndex >= topology.objects.size()
+        || descriptor.slotIndex >= topology.slots.size()) {
+        return false;
+    }
+    const topology::Object& object = topology.objects[descriptor.objectIndex];
+    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
+    return format_text(output,
+                       "authored-scene-resource/%08x/%08x/%08x/%04x/%04x",
+                       static_cast<unsigned>(descriptor.configTag),
+                       static_cast<unsigned>(object.objectTag),
+                       static_cast<unsigned>(descriptor.descriptorOffset),
+                       static_cast<unsigned>(slot.slotIndex),
+                       static_cast<unsigned>(slot.slotType));
+}
+
+/** Formats one event-key ID from the resource, its graph, the scene slot and the gate. */
+bool event_key_id(const topology::Snapshot& topology,
+                  const squad::DescriptorFact& descriptor,
+                  std::uint32_t graphTag,
+                  std::uint32_t gateOffset,
+                  Text& output) noexcept {
+    if (descriptor.slotIndex >= topology.slots.size()) {
+        return false;
+    }
+    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
+    return format_text(output,
+                       "authored-scene-event-key/%08x/%08x/%04x/%04x/%08x",
+                       static_cast<unsigned>(descriptor.configTag),
+                       static_cast<unsigned>(graphTag),
+                       static_cast<unsigned>(slot.slotIndex),
+                       static_cast<unsigned>(slot.slotType),
+                       static_cast<unsigned>(gateOffset));
+}
+
+/** Formats one scene-to-squad edge ID from its descriptor tuple and the squad slot it names. */
+bool edge_id(const topology::Snapshot& topology,
+             const squad::DescriptorFact& descriptor,
+             std::uint32_t squadSlotRow,
+             Text& output) noexcept {
+    if (descriptor.objectIndex >= topology.objects.size()
+        || descriptor.slotIndex >= topology.slots.size() || squadSlotRow >= topology.slots.size()) {
+        return false;
+    }
+    const topology::Object& object = topology.objects[descriptor.objectIndex];
+    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
+    const topology::Slot& squad = topology.slots[squadSlotRow];
+    return format_text(output,
+                       "authored-scene-squad-edge/%08x/%08x/%08x/%04x/%04x/%04x/%04x",
+                       static_cast<unsigned>(descriptor.configTag),
+                       static_cast<unsigned>(object.objectTag),
+                       static_cast<unsigned>(descriptor.descriptorOffset),
+                       static_cast<unsigned>(slot.slotIndex),
+                       static_cast<unsigned>(slot.slotType),
+                       static_cast<unsigned>(squad.slotIndex),
+                       static_cast<unsigned>(squad.slotType));
+}
+
+/** Formats one task-to-objective target ID from its exact descriptor tuple. */
+bool task_target_id(const topology::Snapshot& topology,
+                    const squad::DescriptorFact& descriptor,
+                    Text& output) noexcept {
+    if (descriptor.objectIndex >= topology.objects.size()
+        || descriptor.slotIndex >= topology.slots.size()) {
+        return false;
+    }
+    const topology::Object& object = topology.objects[descriptor.objectIndex];
+    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
+    return format_text(output,
+                       "task-target/%08x/%08x/%08x/%04x/%04x",
+                       static_cast<unsigned>(descriptor.configTag),
+                       static_cast<unsigned>(object.objectTag),
+                       static_cast<unsigned>(descriptor.descriptorOffset),
+                       static_cast<unsigned>(slot.slotIndex),
+                       static_cast<unsigned>(slot.slotType));
+}
+
+/** Tests one exact final slot shape supplied by the separate schema join. */
+bool slot_shape(const topology::Snapshot& topology,
+                const SchemaIndex& schemas,
+                std::uint32_t slotIndex,
+                std::uint32_t slotType,
+                std::uint32_t componentClass,
+                std::uint32_t senseSchema,
+                std::uint32_t authSchema) noexcept {
+    if (slotIndex >= topology.slots.size()) {
+        return false;
+    }
+    const auto found = schemas.find(slotIndex);
+    if (found == schemas.end() || found->second == nullptr || !found->second->exact) {
+        return false;
+    }
+    const topology::Slot& slot = topology.slots[slotIndex];
+    const squad::SlotSchemaFact& schema = *found->second;
+    return slot.slotType == slotType && schema.slotIndex == slotIndex
+           && schema.componentClass == componentClass && schema.senseSchema == senseSchema
+           && schema.authSchema == authSchema;
+}
+
+/** Builds and validates the unique global schema lookup. */
+bool schema_index(const topology::Snapshot& topology, const Facts& facts, SchemaIndex& output) {
+    output.clear();
+    try {
+        output.reserve(facts.slotSchemas.size());
+        for (const squad::SlotSchemaFact& schema : facts.slotSchemas) {
+            if (schema.slotIndex >= topology.slots.size()
+                || !output.emplace(schema.slotIndex, &schema).second) {
+                return false;
+            }
+        }
+        return true;
+    } catch (...) {
+        output.clear();
+        return false;
+    }
+}
+
+/** Checks the topology fields consumed by this bounded projection. */
+bool valid_topology(const topology::Snapshot& topology) noexcept {
+    if (!topology.ready || topology.objects.empty() || topology.slots.empty()) {
+        return false;
+    }
+    std::size_t nextSlot = 0;
+    for (std::size_t objectIndex = 0; objectIndex < topology.objects.size(); ++objectIndex) {
+        const topology::Object& object = topology.objects[objectIndex];
+        if (object.objectTag == 0 || object.objectTag == format::kAbsentIndex
+            || object.objectKey == 0 || object.objectKey == format::kAbsentIndex
+            || object.firstSlot != nextSlot || object.firstSlot > topology.slots.size()
+            || object.slotCount > topology.slots.size() - object.firstSlot) {
+            return false;
+        }
+        for (std::uint32_t index = object.firstSlot; index < object.firstSlot + object.slotCount;
+             ++index) {
+            const topology::Slot& slot = topology.slots[index];
+            if (slot.objectIndex != objectIndex || slot.slotIndex != index - object.firstSlot) {
+                return false;
+            }
+        }
+        nextSlot += object.slotCount;
+    }
+    return nextSlot == topology.slots.size();
+}
+
+} // namespace sunrise::client::content::activity::sdk_generation::authored_scene_inventory

+ 5 - 0
Sunrise/src/client/content/activity/activity_sdk_authored_scene_internal.h

@@ -100,6 +100,11 @@ is_performance_descriptor(const topology::Snapshot& topology,
     return std::tie(row.slotIndex, row.gateOffset);
     return std::tie(row.slotIndex, row.gateOffset);
 }
 }
 
 
+/** Compares event-key rows in final pack order. */
+[[nodiscard]] inline bool event_key_less(const EventKey& left, const EventKey& right) noexcept {
+    return event_key_natural(left) < event_key_natural(right);
+}
+
 /** Sort key matching the final task-target pack order. */
 /** Sort key matching the final task-target pack order. */
 [[nodiscard]] inline auto task_natural(const TaskTarget& row) noexcept {
 [[nodiscard]] inline auto task_natural(const TaskTarget& row) noexcept {
     return std::tie(row.taskSlotIndex, row.configTag, row.descriptorOffset);
     return std::tie(row.taskSlotIndex, row.configTag, row.descriptorOffset);

+ 24 - 162
Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.cpp

@@ -13,152 +13,6 @@
 
 
 namespace sunrise::client::content::activity::sdk_generation::authored_scene_inventory {
 namespace sunrise::client::content::activity::sdk_generation::authored_scene_inventory {
 
 
-/** Formats one resource ID from its exact descriptor tuple. */
-bool resource_id(const topology::Snapshot& topology,
-                 const squad::DescriptorFact& descriptor,
-                 Text& output) noexcept {
-    if (descriptor.objectIndex >= topology.objects.size()
-        || descriptor.slotIndex >= topology.slots.size()) {
-        return false;
-    }
-    const topology::Object& object = topology.objects[descriptor.objectIndex];
-    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
-    return format_text(output,
-                       "authored-scene-resource/%08x/%08x/%08x/%04x/%04x",
-                       static_cast<unsigned>(descriptor.configTag),
-                       static_cast<unsigned>(object.objectTag),
-                       static_cast<unsigned>(descriptor.descriptorOffset),
-                       static_cast<unsigned>(slot.slotIndex),
-                       static_cast<unsigned>(slot.slotType));
-}
-
-/** Formats one event-key ID from the resource, its graph, the scene slot and the gate. */
-bool event_key_id(const topology::Snapshot& topology,
-                  const squad::DescriptorFact& descriptor,
-                  std::uint32_t graphTag,
-                  std::uint32_t gateOffset,
-                  Text& output) noexcept {
-    if (descriptor.slotIndex >= topology.slots.size()) {
-        return false;
-    }
-    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
-    return format_text(output,
-                       "authored-scene-event-key/%08x/%08x/%04x/%04x/%08x",
-                       static_cast<unsigned>(descriptor.configTag),
-                       static_cast<unsigned>(graphTag),
-                       static_cast<unsigned>(slot.slotIndex),
-                       static_cast<unsigned>(slot.slotType),
-                       static_cast<unsigned>(gateOffset));
-}
-
-/** Formats one scene-to-squad edge ID from its descriptor tuple and the squad slot it names. */
-bool edge_id(const topology::Snapshot& topology,
-             const squad::DescriptorFact& descriptor,
-             std::uint32_t squadSlotRow,
-             Text& output) noexcept {
-    if (descriptor.objectIndex >= topology.objects.size()
-        || descriptor.slotIndex >= topology.slots.size() || squadSlotRow >= topology.slots.size()) {
-        return false;
-    }
-    const topology::Object& object = topology.objects[descriptor.objectIndex];
-    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
-    const topology::Slot& squad = topology.slots[squadSlotRow];
-    return format_text(output,
-                       "authored-scene-squad-edge/%08x/%08x/%08x/%04x/%04x/%04x/%04x",
-                       static_cast<unsigned>(descriptor.configTag),
-                       static_cast<unsigned>(object.objectTag),
-                       static_cast<unsigned>(descriptor.descriptorOffset),
-                       static_cast<unsigned>(slot.slotIndex),
-                       static_cast<unsigned>(slot.slotType),
-                       static_cast<unsigned>(squad.slotIndex),
-                       static_cast<unsigned>(squad.slotType));
-}
-
-/** Formats one task-to-objective target ID from its exact descriptor tuple. */
-bool task_target_id(const topology::Snapshot& topology,
-                    const squad::DescriptorFact& descriptor,
-                    Text& output) noexcept {
-    if (descriptor.objectIndex >= topology.objects.size()
-        || descriptor.slotIndex >= topology.slots.size()) {
-        return false;
-    }
-    const topology::Object& object = topology.objects[descriptor.objectIndex];
-    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
-    return format_text(output,
-                       "task-target/%08x/%08x/%08x/%04x/%04x",
-                       static_cast<unsigned>(descriptor.configTag),
-                       static_cast<unsigned>(object.objectTag),
-                       static_cast<unsigned>(descriptor.descriptorOffset),
-                       static_cast<unsigned>(slot.slotIndex),
-                       static_cast<unsigned>(slot.slotType));
-}
-
-/** Tests one exact final slot shape supplied by the separate schema join. */
-bool slot_shape(const topology::Snapshot& topology,
-                const SchemaIndex& schemas,
-                std::uint32_t slotIndex,
-                std::uint32_t slotType,
-                std::uint32_t componentClass,
-                std::uint32_t senseSchema,
-                std::uint32_t authSchema) noexcept {
-    if (slotIndex >= topology.slots.size()) {
-        return false;
-    }
-    const auto found = schemas.find(slotIndex);
-    if (found == schemas.end() || found->second == nullptr || !found->second->exact) {
-        return false;
-    }
-    const topology::Slot& slot = topology.slots[slotIndex];
-    const squad::SlotSchemaFact& schema = *found->second;
-    return slot.slotType == slotType && schema.slotIndex == slotIndex
-           && schema.componentClass == componentClass && schema.senseSchema == senseSchema
-           && schema.authSchema == authSchema;
-}
-
-/** Builds and validates the unique global schema lookup. */
-bool schema_index(const topology::Snapshot& topology, const Facts& facts, SchemaIndex& output) {
-    output.clear();
-    try {
-        output.reserve(facts.slotSchemas.size());
-        for (const squad::SlotSchemaFact& schema : facts.slotSchemas) {
-            if (schema.slotIndex >= topology.slots.size()
-                || !output.emplace(schema.slotIndex, &schema).second) {
-                return false;
-            }
-        }
-        return true;
-    } catch (...) {
-        output.clear();
-        return false;
-    }
-}
-
-/** Checks the topology fields consumed by this bounded projection. */
-bool valid_topology(const topology::Snapshot& topology) noexcept {
-    if (!topology.ready || topology.objects.empty() || topology.slots.empty()) {
-        return false;
-    }
-    std::size_t nextSlot = 0;
-    for (std::size_t objectIndex = 0; objectIndex < topology.objects.size(); ++objectIndex) {
-        const topology::Object& object = topology.objects[objectIndex];
-        if (object.objectTag == 0 || object.objectTag == format::kAbsentIndex
-            || object.objectKey == 0 || object.objectKey == format::kAbsentIndex
-            || object.firstSlot != nextSlot || object.firstSlot > topology.slots.size()
-            || object.slotCount > topology.slots.size() - object.firstSlot) {
-            return false;
-        }
-        for (std::uint32_t index = object.firstSlot; index < object.firstSlot + object.slotCount;
-             ++index) {
-            const topology::Slot& slot = topology.slots[index];
-            if (slot.objectIndex != objectIndex || slot.slotIndex != index - object.firstSlot) {
-                return false;
-            }
-        }
-        nextSlot += object.slotCount;
-    }
-    return nextSlot == topology.slots.size();
-}
-
 namespace {
 namespace {
 
 
 namespace tables = middleware::content::packages::tables;
 namespace tables = middleware::content::packages::tables;
@@ -710,8 +564,14 @@ bool build(const topology::Snapshot& topology,
                                descriptorOffset
                                descriptorOffset
                                    + format::kAuthoredSceneParticipantTableClassRelativeOffset,
                                    + format::kAuthoredSceneParticipantTableClassRelativeOffset,
                                tableClass)
                                tableClass)
-                || tableClass != format::kAuthoredSceneParticipantTableClass
-                || participantCount > format::kAuthoredSceneParticipantCapacity) {
+                || tableClass != format::kAuthoredSceneParticipantTableClass) {
+                log_scene_resource(
+                    descriptor, resourceTag, "participant_table", core::log::Level::warn);
+                continue;
+            }
+            if (participantCount > format::kAuthoredSceneParticipantCapacity) {
+                log_scene_resource(
+                    descriptor, resourceTag, "participant_capacity", core::log::Level::warn);
                 continue;
                 continue;
             }
             }
             for (std::uint64_t entry = 0; entry < participantCount; ++entry) {
             for (std::uint64_t entry = 0; entry < participantCount; ++entry) {
@@ -720,22 +580,24 @@ bool build(const topology::Snapshot& topology,
                     + static_cast<std::size_t>(entry)
                     + static_cast<std::size_t>(entry)
                           * format::kAuthoredSceneParticipantPointerSize;
                           * format::kAuthoredSceneParticipantPointerSize;
                 std::uint64_t pointer = 0;
                 std::uint64_t pointer = 0;
-                if (!read_value(blob, pointerField, pointer)
-                    || pointer > blob.size() - pointerField) {
-                    break;
-                }
-                const std::size_t payload = pointerField + static_cast<std::size_t>(pointer);
                 std::uint32_t blockClass = 0;
                 std::uint32_t blockClass = 0;
-                if (payload < format::kAuthoredSceneParticipantClassSize
-                    || !read_value(
-                        blob, payload - format::kAuthoredSceneParticipantClassSize, blockClass)) {
+                // A pointer or block that does not read is a misread table; the rest is dropped.
+                if (!read_value(blob, pointerField, pointer) || pointer > blob.size() - pointerField
+                    || pointerField + pointer < format::kAuthoredSceneParticipantClassSize
+                    || !read_value(blob,
+                                   pointerField + static_cast<std::size_t>(pointer)
+                                       - format::kAuthoredSceneParticipantClassSize,
+                                   blockClass)) {
+                    log_scene_resource(
+                        descriptor, resourceTag, "participant", core::log::Level::warn);
                     break;
                     break;
                 }
                 }
                 if (blockClass != format::kAuthoredSceneSquadBlockClass) {
                 if (blockClass != format::kAuthoredSceneSquadBlockClass) {
                     continue;
                     continue;
                 }
                 }
                 const std::size_t referenceField =
                 const std::size_t referenceField =
-                    payload + format::kAuthoredSceneSquadPayloadReferenceOffset;
+                    pointerField + static_cast<std::size_t>(pointer)
+                    + format::kAuthoredSceneSquadPayloadReferenceOffset;
                 std::uint32_t targetObjectKey = 0;
                 std::uint32_t targetObjectKey = 0;
                 std::uint16_t targetSlotType = 0;
                 std::uint16_t targetSlotType = 0;
                 std::uint16_t targetSlotIndex = 0;
                 std::uint16_t targetSlotIndex = 0;
@@ -744,6 +606,8 @@ bool build(const topology::Snapshot& topology,
                         blob, referenceField + kTargetSlotTypeRelativeOffset, targetSlotType)
                         blob, referenceField + kTargetSlotTypeRelativeOffset, targetSlotType)
                     || !read_value(
                     || !read_value(
                         blob, referenceField + kTargetSlotIndexRelativeOffset, targetSlotIndex)) {
                         blob, referenceField + kTargetSlotIndexRelativeOffset, targetSlotIndex)) {
+                    log_scene_resource(
+                        descriptor, resourceTag, "squad_block", core::log::Level::warn);
                     break;
                     break;
                 }
                 }
                 if (targetSlotType != format::kSquadSlotType
                 if (targetSlotType != format::kSquadSlotType
@@ -764,6 +628,8 @@ bool build(const topology::Snapshot& topology,
                                    format::kSquadComponentClass,
                                    format::kSquadComponentClass,
                                    format::kSquadSenseSchema,
                                    format::kSquadSenseSchema,
                                    format::kSquadAuthSchema)) {
                                    format::kSquadAuthSchema)) {
+                    log_scene_resource(
+                        descriptor, resourceTag, "squad_unrunnable", core::log::Level::debug);
                     continue;
                     continue;
                 }
                 }
                 SquadEdge row{};
                 SquadEdge row{};
@@ -805,11 +671,7 @@ bool build(const topology::Snapshot& topology,
             }
             }
         }
         }
         std::sort(pending.resources.begin(), pending.resources.end(), resource_less);
         std::sort(pending.resources.begin(), pending.resources.end(), resource_less);
-        std::sort(pending.eventKeys.begin(),
-                  pending.eventKeys.end(),
-                  [](const EventKey& left, const EventKey& right) {
-                      return event_key_natural(left) < event_key_natural(right);
-                  });
+        std::sort(pending.eventKeys.begin(), pending.eventKeys.end(), event_key_less);
         std::sort(pending.squadEdges.begin(), pending.squadEdges.end(), edge_less);
         std::sort(pending.squadEdges.begin(), pending.squadEdges.end(), edge_less);
         std::sort(pending.taskTargets.begin(), pending.taskTargets.end(), task_less);
         std::sort(pending.taskTargets.begin(), pending.taskTargets.end(), task_less);
         pending.resources.erase(std::unique(pending.resources.begin(),
         pending.resources.erase(std::unique(pending.resources.begin(),

+ 16 - 16
Sunrise/src/client/content/activity/activity_sdk_live_publication.cpp

@@ -23,6 +23,8 @@ constexpr std::wstring_view kCommittedMarkerSuffix = L"\\.activity-sdk-publicati
 constexpr std::size_t kAllocationAttempts = 16;
 constexpr std::size_t kAllocationAttempts = 16;
 /** Marker file magic, bytes "ASP1"; a file without it is not ours. */
 /** Marker file magic, bytes "ASP1"; a file without it is not ours. */
 constexpr std::uint32_t kMarkerMagic = 0x31505341U;
 constexpr std::uint32_t kMarkerMagic = 0x31505341U;
+/** A drive path opens with the drive letter, a colon and a separator. */
+constexpr std::size_t kDrivePrefixLength = 3;
 
 
 volatile LONG g_sequence{};
 volatile LONG g_sequence{};
 
 
@@ -61,7 +63,8 @@ struct Marker final {
             return false;
             return false;
         }
         }
         pending.resize(written);
         pending.resize(written);
-        while (pending.size() > 3U && (pending.back() == L'\\' || pending.back() == L'/')) {
+        while (pending.size() > kDrivePrefixLength
+               && (pending.back() == L'\\' || pending.back() == L'/')) {
             pending.pop_back();
             pending.pop_back();
         }
         }
         output = std::move(pending);
         output = std::move(pending);
@@ -72,15 +75,15 @@ struct Marker final {
     }
     }
 }
 }
 
 
-/** Requires every existing drive-path directory component to be ordinary, never a reparse point. */
-[[nodiscard]] bool ordinary_ancestry(const std::wstring& directory) noexcept {
-    if (directory.size() < 3U || directory[1] != L':' || directory[2] != L'\\') {
+/** Requires every component of a drive path to be an existing directory. */
+[[nodiscard]] bool directory_ancestry(const std::wstring& directory) noexcept {
+    if (directory.size() < kDrivePrefixLength || directory[1] != L':' || directory[2] != L'\\') {
         return false;
         return false;
     }
     }
-    if (!is_directory(directory.substr(0, 3U).c_str())) {
+    if (!is_directory(directory.substr(0, kDrivePrefixLength).c_str())) {
         return false;
         return false;
     }
     }
-    std::size_t cursor = 3U;
+    std::size_t cursor = kDrivePrefixLength;
     while (cursor < directory.size()) {
     while (cursor < directory.size()) {
         const std::size_t separator = directory.find(L'\\', cursor);
         const std::size_t separator = directory.find(L'\\', cursor);
         const std::size_t end = separator == std::wstring::npos ? directory.size() : separator;
         const std::size_t end = separator == std::wstring::npos ? directory.size() : separator;
@@ -96,9 +99,9 @@ struct Marker final {
     return true;
     return true;
 }
 }
 
 
-/** Resolves one existing ordinary directory and rejects reparse points in every ancestor. */
+/** Resolves one path whose every component is an existing directory. */
 [[nodiscard]] bool canonical_directory(const wchar_t* input, std::wstring& output) noexcept {
 [[nodiscard]] bool canonical_directory(const wchar_t* input, std::wstring& output) noexcept {
-    return full_path(input, output) && ordinary_ancestry(output);
+    return full_path(input, output) && directory_ancestry(output);
 }
 }
 
 
 /** Compares two complete Windows path components without locale-sensitive folding. */
 /** Compares two complete Windows path components without locale-sensitive folding. */
@@ -141,7 +144,7 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
     return MoveFileExW(source, target, MOVEFILE_WRITE_THROUGH) != FALSE;
     return MoveFileExW(source, target, MOVEFILE_WRITE_THROUGH) != FALSE;
 }
 }
 
 
-/** Requires one ordinary directory. */
+/** Requires one existing directory. */
 [[nodiscard]] bool is_directory(const wchar_t* path) noexcept {
 [[nodiscard]] bool is_directory(const wchar_t* path) noexcept {
     if (path == nullptr || path[0] == L'\0') {
     if (path == nullptr || path[0] == L'\0') {
         return false;
         return false;
@@ -150,14 +153,13 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
     return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
     return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
 }
 }
 
 
-/** Requires one ordinary file and rejects a reparse-backed leaf. */
+/** Requires one existing file that is not a directory. */
 [[nodiscard]] bool is_file(const wchar_t* path) noexcept {
 [[nodiscard]] bool is_file(const wchar_t* path) noexcept {
     if (path == nullptr || path[0] == L'\0') {
     if (path == nullptr || path[0] == L'\0') {
         return false;
         return false;
     }
     }
     const DWORD attributes = GetFileAttributesW(path);
     const DWORD attributes = GetFileAttributesW(path);
-    return attributes != INVALID_FILE_ATTRIBUTES
-           && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
+    return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
 }
 }
 
 
 /** Accepts a missing final path or checks its exact expected ordinary kind. */
 /** Accepts a missing final path or checks its exact expected ordinary kind. */
@@ -498,8 +500,7 @@ Status allocate(const wchar_t* finalPackPath, Stage& output) noexcept {
         }
         }
         const std::wstring finalSdkDirectory = parentPath + L"\\sdk";
         const std::wstring finalSdkDirectory = parentPath + L"\\sdk";
         const std::wstring finalCatalogPath = finalSdkDirectory + L"\\catalog.bin";
         const std::wstring finalCatalogPath = finalSdkDirectory + L"\\catalog.bin";
-        if (!is_directory(parentPath.c_str())
-            || !is_directory(finalSdkDirectory.c_str())
+        if (!is_directory(parentPath.c_str()) || !is_directory(finalSdkDirectory.c_str())
             || !recover(finalSdkDirectory.c_str(), normalizedPack.c_str(), finalCatalogPath.c_str())
             || !recover(finalSdkDirectory.c_str(), normalizedPack.c_str(), finalCatalogPath.c_str())
             || !discard_stale_stages(parentPath)) {
             || !discard_stale_stages(parentPath)) {
             return Status::invalidInput;
             return Status::invalidInput;
@@ -576,8 +577,7 @@ Status publish(const Stage& stage,
         || !full_path(stage.catalogPath.c_str(), canonicalStageCatalog)
         || !full_path(stage.catalogPath.c_str(), canonicalStageCatalog)
         || !full_path(finalPackPath, canonicalFinalPack)
         || !full_path(finalPackPath, canonicalFinalPack)
         || !full_path(finalCatalogPath, canonicalFinalCatalog)
         || !full_path(finalCatalogPath, canonicalFinalCatalog)
-        || !is_file(canonicalStagePack.c_str())
-        || !is_file(canonicalStageCatalog.c_str())) {
+        || !is_file(canonicalStagePack.c_str()) || !is_file(canonicalStageCatalog.c_str())) {
         return Status::invalidInput;
         return Status::invalidInput;
     }
     }
     const std::wstring expectedStageSdk = canonicalStageRoot + L"\\sdk";
     const std::wstring expectedStageSdk = canonicalStageRoot + L"\\sdk";

+ 1 - 1
Sunrise/src/client/content/activity/activity_sdk_lua_contract.cpp

@@ -328,7 +328,7 @@ local EventKind = {
 ---@class SunriseDirectiveArguments
 ---@class SunriseDirectiveArguments
 ---@field directive SunriseDirective Generated mission directive declaration.
 ---@field directive SunriseDirective Generated mission directive declaration.
 ---@field state? integer Defaults to 0, the native enter state.
 ---@field state? integer Defaults to 0, the native enter state.
----@field progress? integer[] One to four lane values, current then maximum first; the element must declare a counter.
+---@field progress? integer[] One to four lane values, current then maximum; counter elements only.
 ---@field audience? SunriseSlot Authored type-70 engagement sensor for the mission banner.
 ---@field audience? SunriseSlot Authored type-70 engagement sensor for the mission banner.
 ---@field navpoint? SunriseSlot Authored type-47 navigation marker.
 ---@field navpoint? SunriseSlot Authored type-47 navigation marker.
 ---@field waypoint? SunriseSlot Authored type-60 volume; inside it the HUD marker hides.
 ---@field waypoint? SunriseSlot Authored type-60 volume; inside it the HUD marker hides.

+ 1 - 121
Sunrise/src/client/content/activity/activity_sdk_lua_missions.cpp

@@ -19,12 +19,6 @@ namespace {
 
 
 namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
 namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
 
 
-void append_int(std::string& output, std::int32_t value) {
-    std::array<char, 16> buffer{};
-    const int length = std::snprintf(buffer.data(), buffer.size(), "%d", value);
-    output.append(buffer.data(), static_cast<std::size_t>(length));
-}
-
 void append_float(std::string& output, std::uint32_t bits) {
 void append_float(std::string& output, std::uint32_t bits) {
     std::array<char, 32> buffer{};
     std::array<char, 32> buffer{};
     const int length = std::snprintf(
     const int length = std::snprintf(
@@ -707,124 +701,10 @@ bool render_mission(const Source& source,
         authSlotConstants.append("    },\n");
         authSlotConstants.append("    },\n");
     }
     }
     authSlotConstants.append("}\n");
     authSlotConstants.append("}\n");
-    std::string actorMessageConstants = "mission.ActorMessage = {\n";
-    for (const format::ActorMessageSchema& row : source.actorMessageSchemas) {
-        actorMessageConstants.append("    ");
-        actorMessageConstants.append(identifier(text(source, row.name), true));
-        actorMessageConstants.append(" = { definition = ");
-        append_hex(actorMessageConstants, row.definitionHandle);
-        actorMessageConstants.append(", durable_key = ");
-        append_hex(actorMessageConstants, row.durableKey);
-        actorMessageConstants.append(", owner_class = ");
-        append_hex(actorMessageConstants, row.ownerClass);
-        actorMessageConstants.append(", handler_slot = ");
-        append_uint(actorMessageConstants, row.handlerSlot);
-        actorMessageConstants.append(", body_type = ");
-        append_uint(actorMessageConstants, row.bodyType);
-        actorMessageConstants.append(" },\n");
-    }
-    actorMessageConstants.append("}\n");
-    std::string actorCommandConstants = "mission.ActorCommand = {\n";
-    std::string actorCommandDefinitions = "mission.ActorCommandDefinition = {\n";
-    std::string factionConstants = "mission.Faction = {\n";
-    bool factionValuesWritten = false;
-    for (const format::ActorCommandDefinition& row : source.actorCommandDefinitions) {
-        const std::string commandName = identifier(text(source, row.name), true);
-        actorCommandConstants.append("    ");
-        actorCommandConstants.append(commandName);
-        actorCommandConstants.append(" = ");
-        append_uint(actorCommandConstants, row.selector);
-        actorCommandConstants.append(",\n");
-        actorCommandDefinitions.append("    ");
-        actorCommandDefinitions.append(commandName);
-        actorCommandDefinitions.append(" = { selector = mission.ActorCommand.");
-        actorCommandDefinitions.append(commandName);
-        actorCommandDefinitions.append(", payload = ");
-        append_hex(actorCommandDefinitions, row.payloadHandle);
-        actorCommandDefinitions.append(" },\n");
-        if (row.effect == format::ActorCommandEffect::setFaction && !factionValuesWritten) {
-            factionConstants.append("    ");
-            factionConstants.append(identifier(text(source, row.factionNoneName), true));
-            factionConstants.append(" = ");
-            append_int(factionConstants, row.factionNone);
-            factionConstants.append(",\n    ");
-            factionConstants.append(identifier(text(source, row.factionRemovedName), true));
-            factionConstants.append(" = ");
-            append_int(factionConstants, row.factionRemoved);
-            factionConstants.append(",\n    ");
-            factionConstants.append(identifier(text(source, row.factionHostileToAllName), true));
-            factionConstants.append(" = ");
-            append_int(factionConstants, row.factionHostileToAll);
-            factionConstants.append(",\n");
-            factionValuesWritten = true;
-        }
-    }
-    actorCommandConstants.append("}\n");
-    actorCommandDefinitions.append("}\n");
-    factionConstants.append("}\n");
-    std::string simulationEventConstants = "mission.SimulationEvent = {\n";
-    std::string simulationEventDefinitions = "mission.SimulationEventDefinition = {\n";
-    for (const format::SimulationEventDefinition& row : source.simulationEventDefinitions) {
-        const std::string eventName = identifier(text(source, row.name), true);
-        simulationEventConstants.append("    ");
-        simulationEventConstants.append(eventName);
-        simulationEventConstants.append(" = ");
-        append_uint(simulationEventConstants, row.eventType);
-        simulationEventConstants.append(",\n");
-        simulationEventDefinitions.append("    ");
-        simulationEventDefinitions.append(eventName);
-        simulationEventDefinitions.append(" = { event_type = mission.SimulationEvent.");
-        simulationEventDefinitions.append(eventName);
-        simulationEventDefinitions.append(", primary_schema = ");
-        if (row.primarySchema == format::kAbsentIndex) {
-            simulationEventDefinitions.append("nil");
-        } else {
-            append_hex(simulationEventDefinitions, row.primarySchema);
-        }
-        simulationEventDefinitions.append(", secondary_schema = ");
-        if (row.secondarySchema == format::kAbsentIndex) {
-            simulationEventDefinitions.append("nil");
-        } else {
-            append_hex(simulationEventDefinitions, row.secondarySchema);
-        }
-        simulationEventDefinitions.append(" },\n");
-    }
-    simulationEventConstants.append("}\n");
-    simulationEventDefinitions.append("}\n");
-    std::string runtimeFieldTypes = "mission.RuntimeFieldType = {\n";
-    // Lua name published for each runtime codec family.
-    constexpr std::array<std::pair<format::RuntimeCodecFamily, std::string_view>, 3> kFamilies{{
-        {format::RuntimeCodecFamily::activity, "ACTIVITY"},
-        {format::RuntimeCodecFamily::sobjectModeZero, "SOBJECT_MODE_ZERO"},
-        {format::RuntimeCodecFamily::sobjectModeOne, "SOBJECT_MODE_ONE"},
-    }};
-    for (const auto& [family, familyName] : kFamilies) {
-        runtimeFieldTypes.append("    ");
-        runtimeFieldTypes.append(familyName);
-        runtimeFieldTypes.append(" = {\n");
-        for (const format::RuntimeTypeDefinition& row : source.runtimeTypeDefinitions) {
-            if ((row.codecFamilies & static_cast<std::uint32_t>(family)) == 0) {
-                continue;
-            }
-            runtimeFieldTypes.append("        ");
-            runtimeFieldTypes.append(identifier(text(source, row.name), true));
-            runtimeFieldTypes.append(" = ");
-            append_uint(runtimeFieldTypes, row.typeCode);
-            runtimeFieldTypes.append(",\n");
-        }
-        runtimeFieldTypes.append("    },\n");
-    }
-    runtimeFieldTypes.append("}\n");
     output.append(stateConstants);
     output.append(stateConstants);
     output.append(slotConstants);
     output.append(slotConstants);
     output.append(authSlotConstants);
     output.append(authSlotConstants);
-    output.append(actorMessageConstants);
-    output.append(actorCommandConstants);
-    output.append(actorCommandDefinitions);
-    output.append(factionConstants);
-    output.append(simulationEventConstants);
-    output.append(simulationEventDefinitions);
-    output.append(runtimeFieldTypes);
+    append_catalog_constants(source, output);
     output.append(squadConstants);
     output.append(squadConstants);
     output.append(sceneConstants);
     output.append(sceneConstants);
     output.append(taskConstants);
     output.append(taskConstants);

+ 137 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_missions_catalog_constants.cpp

@@ -0,0 +1,137 @@
+#include <array>
+#include <cstdint>
+#include <cstdio>
+#include <string>
+#include <string_view>
+#include <utility>
+
+#include "activity_sdk_lua_missions_internal.h"
+
+namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
+
+void append_int(std::string& output, std::int32_t value) {
+    std::array<char, 16> buffer{};
+    const int length = std::snprintf(buffer.data(), buffer.size(), "%d", value);
+    output.append(buffer.data(), static_cast<std::size_t>(length));
+}
+
+/** Appends the catalog-wide tables every mission module shares, in their published order. */
+void append_catalog_constants(const Source& source, std::string& output) {
+    std::string actorMessageConstants = "mission.ActorMessage = {\n";
+    for (const format::ActorMessageSchema& row : source.actorMessageSchemas) {
+        actorMessageConstants.append("    ");
+        actorMessageConstants.append(identifier(text(source, row.name), true));
+        actorMessageConstants.append(" = { definition = ");
+        append_hex(actorMessageConstants, row.definitionHandle);
+        actorMessageConstants.append(", durable_key = ");
+        append_hex(actorMessageConstants, row.durableKey);
+        actorMessageConstants.append(", owner_class = ");
+        append_hex(actorMessageConstants, row.ownerClass);
+        actorMessageConstants.append(", handler_slot = ");
+        append_uint(actorMessageConstants, row.handlerSlot);
+        actorMessageConstants.append(", body_type = ");
+        append_uint(actorMessageConstants, row.bodyType);
+        actorMessageConstants.append(" },\n");
+    }
+    actorMessageConstants.append("}\n");
+    std::string actorCommandConstants = "mission.ActorCommand = {\n";
+    std::string actorCommandDefinitions = "mission.ActorCommandDefinition = {\n";
+    std::string factionConstants = "mission.Faction = {\n";
+    bool factionValuesWritten = false;
+    for (const format::ActorCommandDefinition& row : source.actorCommandDefinitions) {
+        const std::string commandName = identifier(text(source, row.name), true);
+        actorCommandConstants.append("    ");
+        actorCommandConstants.append(commandName);
+        actorCommandConstants.append(" = ");
+        append_uint(actorCommandConstants, row.selector);
+        actorCommandConstants.append(",\n");
+        actorCommandDefinitions.append("    ");
+        actorCommandDefinitions.append(commandName);
+        actorCommandDefinitions.append(" = { selector = mission.ActorCommand.");
+        actorCommandDefinitions.append(commandName);
+        actorCommandDefinitions.append(", payload = ");
+        append_hex(actorCommandDefinitions, row.payloadHandle);
+        actorCommandDefinitions.append(" },\n");
+        if (row.effect == format::ActorCommandEffect::setFaction && !factionValuesWritten) {
+            factionConstants.append("    ");
+            factionConstants.append(identifier(text(source, row.factionNoneName), true));
+            factionConstants.append(" = ");
+            append_int(factionConstants, row.factionNone);
+            factionConstants.append(",\n    ");
+            factionConstants.append(identifier(text(source, row.factionRemovedName), true));
+            factionConstants.append(" = ");
+            append_int(factionConstants, row.factionRemoved);
+            factionConstants.append(",\n    ");
+            factionConstants.append(identifier(text(source, row.factionHostileToAllName), true));
+            factionConstants.append(" = ");
+            append_int(factionConstants, row.factionHostileToAll);
+            factionConstants.append(",\n");
+            factionValuesWritten = true;
+        }
+    }
+    actorCommandConstants.append("}\n");
+    actorCommandDefinitions.append("}\n");
+    factionConstants.append("}\n");
+    std::string simulationEventConstants = "mission.SimulationEvent = {\n";
+    std::string simulationEventDefinitions = "mission.SimulationEventDefinition = {\n";
+    for (const format::SimulationEventDefinition& row : source.simulationEventDefinitions) {
+        const std::string eventName = identifier(text(source, row.name), true);
+        simulationEventConstants.append("    ");
+        simulationEventConstants.append(eventName);
+        simulationEventConstants.append(" = ");
+        append_uint(simulationEventConstants, row.eventType);
+        simulationEventConstants.append(",\n");
+        simulationEventDefinitions.append("    ");
+        simulationEventDefinitions.append(eventName);
+        simulationEventDefinitions.append(" = { event_type = mission.SimulationEvent.");
+        simulationEventDefinitions.append(eventName);
+        simulationEventDefinitions.append(", primary_schema = ");
+        if (row.primarySchema == format::kAbsentIndex) {
+            simulationEventDefinitions.append("nil");
+        } else {
+            append_hex(simulationEventDefinitions, row.primarySchema);
+        }
+        simulationEventDefinitions.append(", secondary_schema = ");
+        if (row.secondarySchema == format::kAbsentIndex) {
+            simulationEventDefinitions.append("nil");
+        } else {
+            append_hex(simulationEventDefinitions, row.secondarySchema);
+        }
+        simulationEventDefinitions.append(" },\n");
+    }
+    simulationEventConstants.append("}\n");
+    simulationEventDefinitions.append("}\n");
+    std::string runtimeFieldTypes = "mission.RuntimeFieldType = {\n";
+    // Lua name published for each runtime codec family.
+    constexpr std::array<std::pair<format::RuntimeCodecFamily, std::string_view>, 3> kFamilies{{
+        {format::RuntimeCodecFamily::activity, "ACTIVITY"},
+        {format::RuntimeCodecFamily::sobjectModeZero, "SOBJECT_MODE_ZERO"},
+        {format::RuntimeCodecFamily::sobjectModeOne, "SOBJECT_MODE_ONE"},
+    }};
+    for (const auto& [family, familyName] : kFamilies) {
+        runtimeFieldTypes.append("    ");
+        runtimeFieldTypes.append(familyName);
+        runtimeFieldTypes.append(" = {\n");
+        for (const format::RuntimeTypeDefinition& row : source.runtimeTypeDefinitions) {
+            if ((row.codecFamilies & static_cast<std::uint32_t>(family)) == 0) {
+                continue;
+            }
+            runtimeFieldTypes.append("        ");
+            runtimeFieldTypes.append(identifier(text(source, row.name), true));
+            runtimeFieldTypes.append(" = ");
+            append_uint(runtimeFieldTypes, row.typeCode);
+            runtimeFieldTypes.append(",\n");
+        }
+        runtimeFieldTypes.append("    },\n");
+    }
+    runtimeFieldTypes.append("}\n");
+    output.append(actorMessageConstants);
+    output.append(actorCommandConstants);
+    output.append(actorCommandDefinitions);
+    output.append(factionConstants);
+    output.append(simulationEventConstants);
+    output.append(simulationEventDefinitions);
+    output.append(runtimeFieldTypes);
+}
+
+} // namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal

+ 6 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_missions_internal.h

@@ -29,6 +29,12 @@ struct RenderIndex final {
                                             std::string_view name,
                                             std::string_view name,
                                             std::uint32_t fallback);
                                             std::uint32_t fallback);
 
 
+/** Appends one signed decimal literal. */
+void append_int(std::string& output, std::int32_t value);
+
+/** Appends the catalog-wide constant tables every mission module shares. */
+void append_catalog_constants(const Source& source, std::string& output);
+
 /** Emits one concrete mission module with no pack or resolver access. */
 /** Emits one concrete mission module with no pack or resolver access. */
 [[nodiscard]] bool render_mission(const Source& source,
 [[nodiscard]] bool render_mission(const Source& source,
                                   const RenderIndex& index,
                                   const RenderIndex& index,

+ 1 - 2
Sunrise/src/client/content/activity/activity_sdk_lua_publish.cpp

@@ -157,8 +157,7 @@ private:
         return false;
         return false;
     }
     }
     const DWORD attributes = GetFileAttributesW(path);
     const DWORD attributes = GetFileAttributesW(path);
-    if (attributes == INVALID_FILE_ATTRIBUTES
-        || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
+    if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
         return false;
         return false;
     }
     }
     const HANDLE file = CreateFileW(path,
     const HANDLE file = CreateFileW(path,

+ 1 - 2
Sunrise/src/client/content/activity/activity_sdk_tree_publication.cpp

@@ -62,8 +62,7 @@ struct Marker final {
 /** Requires one existing ordinary file. */
 /** Requires one existing ordinary file. */
 [[nodiscard]] bool is_file(const wchar_t* path) noexcept {
 [[nodiscard]] bool is_file(const wchar_t* path) noexcept {
     const DWORD attributes = GetFileAttributesW(path);
     const DWORD attributes = GetFileAttributesW(path);
-    return attributes != INVALID_FILE_ATTRIBUTES
-           && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
+    return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
 }
 }
 
 
 /** Requires every existing drive-path directory component to be ordinary. */
 /** Requires every existing drive-path directory component to be ordinary. */

+ 1 - 2
Sunrise/src/client/content/items/packages/package_subclass_build.cpp

@@ -172,8 +172,7 @@ bool build_character_abilities(
         }
         }
         output[count++] = row;
         output[count++] = row;
     };
     };
-    for (std::size_t character = 0; character < account.characterCount && count < output.size();
-         ++character) {
+    for (std::size_t character = 0; character < account.characterCount; ++character) {
         std::uint16_t equippedSocketEntryListIndex = 0;
         std::uint16_t equippedSocketEntryListIndex = 0;
         const char* subclassReason = "subclass";
         const char* subclassReason = "subclass";
         if (!subclass_list(
         if (!subclass_list(

+ 14 - 30
Sunrise/src/client/hooks/stall_probe/stall_probe.cpp

@@ -12,6 +12,7 @@
 #include <TlHelp32.h>
 #include <TlHelp32.h>
 #include <array>
 #include <array>
 #include <atomic>
 #include <atomic>
+#include <bit>
 #include <cstddef>
 #include <cstddef>
 #include <cstdint>
 #include <cstdint>
 #include <cstdio>
 #include <cstdio>
@@ -61,8 +62,7 @@ void resolve_module_ranges() noexcept {
 
 
 /**
 /**
  * Names the loaded module holding one address, as "<basename>+0x<rva>".
  * Names the loaded module holding one address, as "<basename>+0x<rva>".
- * Runs only after every suspended thread has resumed: the loader lock may be held by one of
- * them, and this takes it.
+ * Takes the loader lock, so it runs only after every suspended thread has resumed.
  * @return True when a module owns the address and the token fit.
  * @return True when a module owns the address and the token fit.
  */
  */
 [[nodiscard]] bool
 [[nodiscard]] bool
@@ -72,7 +72,7 @@ format_module_address(std::uint64_t value, char* out, std::size_t size) noexcept
     if (value < kLowestCodeAddress
     if (value < kLowestCodeAddress
         || GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
         || GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
                                   | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                                   | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
-                              reinterpret_cast<LPCWSTR>(static_cast<std::uintptr_t>(value)),
+                              std::bit_cast<LPCWSTR>(static_cast<std::uintptr_t>(value)),
                               &module)
                               &module)
                == 0
                == 0
         || !diagnostics::module_range(module, range) || !diagnostics::contains(range, value)) {
         || !diagnostics::module_range(module, range) || !diagnostics::contains(range, value)) {
@@ -103,41 +103,25 @@ format_module_address(std::uint64_t value, char* out, std::size_t size) noexcept
 }
 }
 
 
 /**
 /**
- * Formats one code address as a module-relative token: the game image, this DLL, any other
- * loaded module by name, or raw hex when nothing owns it.
+ * Formats one code address as a module-relative token, or raw hex when no image owns it.
+ * @return True when the game image, this DLL or another loaded module owns the address.
  */
  */
-void format_address(std::uint64_t value, char* out, std::size_t size) noexcept {
+bool format_address(std::uint64_t value, char* out, std::size_t size) noexcept {
     if (diagnostics::contains(g_gameRange, value)) {
     if (diagnostics::contains(g_gameRange, value)) {
         std::snprintf(
         std::snprintf(
             out, size, "exe+0x%llX", static_cast<unsigned long long>(value - g_gameRange.base));
             out, size, "exe+0x%llX", static_cast<unsigned long long>(value - g_gameRange.base));
-        return;
+        return true;
     }
     }
     if (diagnostics::contains(g_ownRange, value)) {
     if (diagnostics::contains(g_ownRange, value)) {
         std::snprintf(
         std::snprintf(
             out, size, "own+0x%llX", static_cast<unsigned long long>(value - g_ownRange.base));
             out, size, "own+0x%llX", static_cast<unsigned long long>(value - g_ownRange.base));
-        return;
+        return true;
     }
     }
     if (format_module_address(value, out, size)) {
     if (format_module_address(value, out, size)) {
-        return;
-    }
-    std::snprintf(out, size, "0x%llX", static_cast<unsigned long long>(value));
-}
-
-/** @return True when some loaded image, not only the two known ones, holds the address. */
-[[nodiscard]] bool in_any_module(std::uint64_t value) noexcept {
-    if (diagnostics::contains(g_gameRange, value) || diagnostics::contains(g_ownRange, value)) {
         return true;
         return true;
     }
     }
-    HMODULE module = nullptr;
-    diagnostics::ModuleRange range{};
-    // Wine answers the main image for a null address, so the range check is what decides.
-    return value >= kLowestCodeAddress
-           && GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
-                                     | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
-                                 reinterpret_cast<LPCWSTR>(static_cast<std::uintptr_t>(value)),
-                                 &module)
-                  != 0
-           && diagnostics::module_range(module, range) && diagnostics::contains(range, value);
+    std::snprintf(out, size, "0x%llX", static_cast<unsigned long long>(value));
+    return false;
 }
 }
 
 
 /**
 /**
@@ -182,7 +166,7 @@ void report_thread(std::uint32_t tid) noexcept {
         return;
         return;
     }
     }
     std::array<char, 64> ripText{};
     std::array<char, 64> ripText{};
-    format_address(context.Rip, ripText.data(), ripText.size());
+    (void)format_address(context.Rip, ripText.data(), ripText.size());
     std::array<char, core::log::kLineCapacity> line{};
     std::array<char, core::log::kLineCapacity> line{};
     int written = std::snprintf(line.data(),
     int written = std::snprintf(line.data(),
                                 line.size(),
                                 line.size(),
@@ -203,7 +187,9 @@ void report_thread(std::uint32_t tid) noexcept {
     for (std::size_t index = 0; index * 8 + 8 <= stackBytes && frames < kFrameLimit; ++index) {
     for (std::size_t index = 0; index * 8 + 8 <= stackBytes && frames < kFrameLimit; ++index) {
         std::uint64_t value = 0;
         std::uint64_t value = 0;
         std::memcpy(&value, stack.data() + index * 8, sizeof value);
         std::memcpy(&value, stack.data() + index * 8, sizeof value);
-        if (!in_any_module(value)) {
+        // A slot no image owns is data, not a return address.
+        std::array<char, 64> text{};
+        if (!format_address(value, text.data(), text.size())) {
             continue;
             continue;
         }
         }
         if (onLine == 0) {
         if (onLine == 0) {
@@ -211,8 +197,6 @@ void report_thread(std::uint32_t tid) noexcept {
                 line.data(), line.size(), "ev=probe stage=stall set=frames tid=0x%08X", tid);
                 line.data(), line.size(), "ev=probe stage=stall set=frames tid=0x%08X", tid);
             offset = prefix > 0 ? static_cast<std::size_t>(prefix) : 0;
             offset = prefix > 0 ? static_cast<std::size_t>(prefix) : 0;
         }
         }
-        std::array<char, 64> text{};
-        format_address(value, text.data(), text.size());
         const int piece = std::snprintf(
         const int piece = std::snprintf(
             line.data() + offset, line.size() - offset, " f%zu=%s", frames, text.data());
             line.data() + offset, line.size() - offset, " f%zu=%s", frames, text.data());
         if (piece > 0) {
         if (piece > 0) {

+ 3 - 2
Sunrise/src/server/activity/activity_sdk_mission_internal.h

@@ -14,10 +14,11 @@ namespace sunrise::server::activity::activity_sdk_mission::detail {
 struct PreparedScene final {
 struct PreparedScene final {
     host::ScriptableTarget target{};
     host::ScriptableTarget target{};
     state::build_data::scenarios::RosterGroup rosterGroup{};
     state::build_data::scenarios::RosterGroup rosterGroup{};
+    /** Squads the resolved cast draws from; a cast wider than the wire is refused. */
     middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies
     middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies
         sceneDependencies{};
         sceneDependencies{};
-    /** Squads the resolved cast draws from; more than the wire carries leaves the set empty. */
-    std::size_t castCount{};
+    /** Squad participants the cast left to the client. */
+    std::size_t omittedParticipants{};
     std::uint64_t activityClientGeneration{};
     std::uint64_t activityClientGeneration{};
     std::uint32_t scenarioRow{state::activity_sdk::format::kAbsentIndex};
     std::uint32_t scenarioRow{state::activity_sdk::format::kAbsentIndex};
     std::uint32_t stateRow{state::activity_sdk::format::kAbsentIndex};
     std::uint32_t stateRow{state::activity_sdk::format::kAbsentIndex};

+ 4 - 2
Sunrise/src/server/activity/activity_sdk_mission_prepare.cpp

@@ -358,8 +358,10 @@ namespace {
     if (collected != SceneStatus::ready) {
     if (collected != SceneStatus::ready) {
         return collected;
         return collected;
     }
     }
-    output.castCount = cast.count;
-    static_cast<void>(scene_dependencies(cast, output.sceneDependencies));
+    if (!scene_dependencies(cast, output.sceneDependencies)) {
+        return SceneStatus::dependencyCapacity;
+    }
+    output.omittedParticipants = cast.omitted;
 
 
     const SceneStatus lease = scene_lease_status(view, link, occurrence.stateIndex);
     const SceneStatus lease = scene_lease_status(view, link, occurrence.stateIndex);
     if (lease != SceneStatus::ready) {
     if (lease != SceneStatus::ready) {

+ 6 - 4
Sunrise/src/server/activity/activity_sdk_mission_runtime.cpp

@@ -127,8 +127,8 @@ SceneStatus activate_authored_scene(const sdk::BoundView& view,
     if (status != SceneStatus::ready) {
     if (status != SceneStatus::ready) {
         return status;
         return status;
     }
     }
-    if (prepared.sceneDependencies.count == 0 && prepared.castCount != 0) {
-        log_omitted_scene_dependencies(*view.catalog, slotRow, prepared.castCount);
+    if (prepared.omittedParticipants != 0) {
+        log_omitted_participants(*view.catalog, slotRow, prepared.omittedParticipants);
     }
     }
     if (server::bap::request_activity_state_local_authored_scene_override(
     if (server::bap::request_activity_state_local_authored_scene_override(
             view.binding,
             view.binding,
@@ -771,8 +771,8 @@ SceneStatus activate_authored_scene_reserved(const sdk::BoundView& view,
     if (status != SceneStatus::ready) {
     if (status != SceneStatus::ready) {
         return status;
         return status;
     }
     }
-    if (prepared.sceneDependencies.count == 0 && prepared.castCount != 0) {
-        log_omitted_scene_dependencies(*view.catalog, slotRow, prepared.castCount);
+    if (prepared.omittedParticipants != 0) {
+        log_omitted_participants(*view.catalog, slotRow, prepared.omittedParticipants);
     }
     }
     if (server::bap::request_activity_state_local_authored_scene_override(
     if (server::bap::request_activity_state_local_authored_scene_override(
             view.binding,
             view.binding,
@@ -869,6 +869,8 @@ const char* status_name(SceneStatus status) noexcept {
         return "mission_seed_pending";
         return "mission_seed_pending";
     case SceneStatus::outputBusy:
     case SceneStatus::outputBusy:
         return "output_busy";
         return "output_busy";
+    case SceneStatus::dependencyCapacity:
+        return "dependency_capacity";
     case SceneStatus::refused:
     case SceneStatus::refused:
         return "refused";
         return "refused";
     }
     }

+ 2 - 0
Sunrise/src/server/activity/activity_sdk_mission_runtime.h

@@ -50,6 +50,8 @@ enum class SceneStatus : std::uint8_t {
     missionSeedUnavailable,
     missionSeedUnavailable,
     missionSeedPending,
     missionSeedPending,
     outputBusy,
     outputBusy,
+    /** The cast has more squads than the eight dependencies the Auth schema carries. */
+    dependencyCapacity,
     refused,
     refused,
 };
 };
 
 

+ 5 - 6
Sunrise/src/server/activity/activity_sdk_scene_cast.cpp

@@ -69,12 +69,9 @@ using Source = state::gameplay::entity_identity::ActorSourceReference;
 } // namespace
 } // namespace
 
 
 /**
 /**
- * Resolves the complete cast before exposing any pair.
- *
- * A squad participant joins the cast only when one exact actor control in the scene's own
- * scenario state resolves to it. The client binds every role from its content and waits for
- * the squads nobody supplies, so a participant without an actor control is left to it rather
- * than refused.
+ * Resolves the complete cast before exposing any pair. A squad participant joins only when one
+ * exact actor control in the scene's own state resolves to it; the client binds every other role
+ * from its content, so such a participant is counted as omitted, not refused.
  * @param catalog Authenticated SDK data.
  * @param catalog Authenticated SDK data.
  * @param world Exact generated package references.
  * @param world Exact generated package references.
  * @param occurrenceRow Selected scene occurrence.
  * @param occurrenceRow Selected scene occurrence.
@@ -123,6 +120,7 @@ SceneStatus collect_scene_spawn_plan(const sdk::Catalog& catalog,
         const auto parent = source_squad(catalog, occurrenceRow, source, pair.squadRow);
         const auto parent = source_squad(catalog, occurrenceRow, source, pair.squadRow);
         if (parent == SceneStatus::targetUnavailable) {
         if (parent == SceneStatus::targetUnavailable) {
             pair = {};
             pair = {};
+            ++candidate.omitted;
             continue;
             continue;
         }
         }
         if (parent != SceneStatus::ready) {
         if (parent != SceneStatus::ready) {
@@ -164,6 +162,7 @@ SceneStatus collect_scene_spawn_plan(const sdk::Catalog& catalog,
         }
         }
         if (pair.actorSlotRow == sdk::format::kAbsentIndex) {
         if (pair.actorSlotRow == sdk::format::kAbsentIndex) {
             pair = {};
             pair = {};
+            ++candidate.omitted;
             continue;
             continue;
         }
         }
         pair.actorTarget = physical_target(catalog, pair.actorSlotRow);
         pair.actorTarget = physical_target(catalog, pair.actorSlotRow);

+ 2 - 5
Sunrise/src/server/activity/activity_sdk_scene_dependencies.h

@@ -24,11 +24,8 @@ struct SceneSquadParticipants final {
 };
 };
 
 
 /**
 /**
- * Collects the selected scene descriptor's exact squad edges.
- *
- * The client binds every role from the participant table in its own content; these rows only
- * tell the server which squads the scene may draw actors from. The wire dependency set is
- * narrower (see scene_dependencies in activity_sdk_scene_spawn.h).
+ * Collects the selected scene descriptor's exact squad edges: the squads the scene may draw
+ * actors from. The client binds every role from its own participant table.
  * @param catalog Authenticated SDK catalog.
  * @param catalog Authenticated SDK catalog.
  * @param sceneSlot Owned scene slot.
  * @param sceneSlot Owned scene slot.
  * @param resource Selected resource descriptor for that slot.
  * @param resource Selected resource descriptor for that slot.

+ 11 - 21
Sunrise/src/server/activity/activity_sdk_scene_spawn.cpp

@@ -118,7 +118,7 @@ prepare_pair(const sdk::BoundView& view, std::uint32_t sceneState, SceneSpawnPai
 
 
 } // namespace
 } // namespace
 
 
-/** Sends the cast's squads when the wire can carry them all, otherwise none. */
+/** Carries the cast's squads only when the schema holds them all. */
 bool scene_dependencies(
 bool scene_dependencies(
     const SceneSpawnPlan& plan,
     const SceneSpawnPlan& plan,
     middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies&
     middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies&
@@ -136,29 +136,19 @@ bool scene_dependencies(
     return true;
     return true;
 }
 }
 
 
-/** Logged once per activation; an availability check resolves the same cast every frame. */
-void log_omitted_scene_dependencies(const sdk::Catalog& catalog,
-                                    std::uint32_t sceneSlotRow,
-                                    std::size_t castCount) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
+/** Names the scene and how many of its squad participants the host did not cast. */
+void log_omitted_participants(const sdk::Catalog& catalog,
+                              std::uint32_t sceneSlotRow,
+                              std::size_t omitted) noexcept {
     const std::string_view scene = sceneSlotRow < catalog.slots().size()
     const std::string_view scene = sceneSlotRow < catalog.slots().size()
                                        ? catalog.string(catalog.slots()[sceneSlotRow].id)
                                        ? catalog.string(catalog.slots()[sceneSlotRow].id)
                                        : std::string_view{};
                                        : std::string_view{};
-    const int written = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=scene_dependencies result=omitted scene=%.*s cast=%zu capacity=%zu",
-        static_cast<int>(scene.size()),
-        scene.data(),
-        castCount,
-        middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies{}
-            .references.size());
-    if (written > 0) {
-        core::log::write(
-            core::log::Channel::server,
-            core::log::Level::warn,
-            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
-    }
+    core::log::writef(core::log::Channel::server,
+                      core::log::Level::info,
+                      "ev=scene_cast result=partial scene=%.*s omitted=%zu",
+                      static_cast<int>(scene.size()),
+                      scene.data(),
+                      omitted);
 }
 }
 
 
 /** Returns immutable scene cast identities without consulting live output state. */
 /** Returns immutable scene cast identities without consulting live output state. */

+ 11 - 11
Sunrise/src/server/activity/activity_sdk_scene_spawn.h

@@ -26,27 +26,27 @@ struct SceneSpawnPlan final {
     std::array<SceneSpawnPair, state::activity_sdk::format::kAuthoredSceneParticipantCapacity>
     std::array<SceneSpawnPair, state::activity_sdk::format::kAuthoredSceneParticipantCapacity>
         pairs{};
         pairs{};
     std::size_t count{};
     std::size_t count{};
+    /** Squad participants left to the client: no squad or no actor control in this state. */
+    std::size_t omitted{};
 };
 };
 
 
 /**
 /**
- * Names the cast's source squads as the scene's wire dependencies.
- *
- * The client uses them for one thing: once the scene runs, it marks each squad's remaining
- * spawn budget as consumed. It binds roles from its own content, so a scene plays without
- * them. The schema carries at most eight; a wider cast sends none.
+ * Names the cast's source squads as the scene's wire dependencies. The client only marks each
+ * squad's remaining spawn budget consumed once the scene runs; it binds roles from its own
+ * content. The schema carries at most eight; a wider cast is refused, not sent in part.
  * @param plan Complete resolved cast.
  * @param plan Complete resolved cast.
- * @param output Receives the bounded set; empty when the cast exceeds the wire capacity.
- * @return False when the cast was omitted.
+ * @param output Receives the whole cast, or nothing when it does not fit.
+ * @return False when the cast does not fit the schema.
  */
  */
 [[nodiscard]] bool
 [[nodiscard]] bool
 scene_dependencies(const SceneSpawnPlan& plan,
 scene_dependencies(const SceneSpawnPlan& plan,
                    middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies&
                    middleware::bap::activity_message::sensor_auth_update::AuthoredSceneDependencies&
                        output) noexcept;
                        output) noexcept;
 
 
-/** Logs, at warn, a cast the wire could not carry; for the activation paths only. */
-void log_omitted_scene_dependencies(const state::activity_sdk::Catalog& catalog,
-                                    std::uint32_t sceneSlotRow,
-                                    std::size_t castCount) noexcept;
+/** Logs, once per activation, the participants the host leaves to the client. */
+void log_omitted_participants(const state::activity_sdk::Catalog& catalog,
+                              std::uint32_t sceneSlotRow,
+                              std::size_t omitted) noexcept;
 
 
 /** Resolves the complete cast from exact package edges without reading Host state. */
 /** Resolves the complete cast from exact package edges without reading Host state. */
 [[nodiscard]] SceneStatus
 [[nodiscard]] SceneStatus

+ 0 - 10
Sunrise/src/server/ui/activity_host/activity_host_table_layout.h

@@ -4,8 +4,6 @@
 #include <cstddef>
 #include <cstddef>
 #include <imgui.h>
 #include <imgui.h>
 
 
-#include "../../../core/ui/scaling/dpi/ui_dpi_scaling.h"
-
 namespace sunrise::server::ui::activity_host::table_layout {
 namespace sunrise::server::ui::activity_host::table_layout {
 
 
 /** Eight rows leave room for the selected-object workspace below each browser. */
 /** Eight rows leave room for the selected-object workspace below each browser. */
@@ -25,14 +23,6 @@ inline constexpr std::size_t kVisibleRowLimit = 8;
     return {0.0F, header + (row_height() * static_cast<float>(visible)) + border};
     return {0.0F, header + (row_height() * static_cast<float>(visible)) + border};
 }
 }
 
 
-/** @return Authored window height occupied by a bounded set of data rows. */
-[[nodiscard]] inline float
-authored_rows_height(std::size_t rows, std::size_t maximumVisibleRows = kVisibleRowLimit) noexcept {
-    const float scale = (std::max)(core::ui::scaling::dpi::current(), 0.01F);
-    const std::size_t visible = (std::min)(rows, (std::min)(maximumVisibleRows, kVisibleRowLimit));
-    return row_height() * static_cast<float>(visible) / scale;
-}
-
 /** Freezes the header row before emitting it. */
 /** Freezes the header row before emitting it. */
 inline void frozen_headers() noexcept {
 inline void frozen_headers() noexcept {
     ImGui::TableSetupScrollFreeze(0, 1);
     ImGui::TableSetupScrollFreeze(0, 1);

+ 10 - 7
Sunrise/src/server/ui/activity_host/activity_host_tool_window.h

@@ -31,22 +31,25 @@ inline void set_next(const ImVec2& workFraction) noexcept {
     ImGui::SetNextWindowPos(viewport->GetWorkCenter(), ImGuiCond_FirstUseEver, {0.5F, 0.5F});
     ImGui::SetNextWindowPos(viewport->GetWorkCenter(), ImGuiCond_FirstUseEver, {0.5F, 0.5F});
 }
 }
 
 
-/** Begins a movable, resizable window whose title bar reads as a raised Sunrise panel. */
+/** Begins a movable window that resizes from its edges, with a title bar in the panel colour. */
 [[nodiscard]] inline bool begin(const char* id, bool& open, const ImVec2& workFraction) noexcept {
 [[nodiscard]] inline bool begin(const char* id, bool& open, const ImVec2& workFraction) noexcept {
-    namespace scaling = core::ui::scaling::dpi;
     set_next(workFraction);
     set_next(workFraction);
     // Dear ImGui reads these only while Begin draws the frame, so the body keeps the theme.
     // Dear ImGui reads these only while Begin draws the frame, so the body keeps the theme.
-    // The same colour whether focused or not, so focus never lights the bar up.
+    // The same colour whether focused or not, so focus never lights the bar up. The corner grips
+    // are drawn transparent; the edges carry the resize.
     const ImGuiStyle& style = ImGui::GetStyle();
     const ImGuiStyle& style = ImGui::GetStyle();
     const ImVec4 titleBackground = style.Colors[ImGuiCol_ChildBg];
     const ImVec4 titleBackground = style.Colors[ImGuiCol_ChildBg];
+    // Alpha 0 hides the corner grips; the edges still resize.
+    constexpr ImVec4 kTransparent{0.0F, 0.0F, 0.0F, 0.0F};
     ImGui::PushStyleColor(ImGuiCol_TitleBg, titleBackground);
     ImGui::PushStyleColor(ImGuiCol_TitleBg, titleBackground);
     ImGui::PushStyleColor(ImGuiCol_TitleBgActive, titleBackground);
     ImGui::PushStyleColor(ImGuiCol_TitleBgActive, titleBackground);
-    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
-                        ImVec2{style.FramePadding.x, scaling::pixels(8.0F)});
+    ImGui::PushStyleColor(ImGuiCol_ResizeGrip, kTransparent);
+    ImGui::PushStyleColor(ImGuiCol_ResizeGripHovered, kTransparent);
+    ImGui::PushStyleColor(ImGuiCol_ResizeGripActive, kTransparent);
     ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, style.WindowBorderSize);
     ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, style.WindowBorderSize);
     const bool visible = ImGui::Begin(id, &open, kWindowFlags);
     const bool visible = ImGui::Begin(id, &open, kWindowFlags);
-    ImGui::PopStyleVar(2);
-    ImGui::PopStyleColor(2);
+    ImGui::PopStyleVar();
+    ImGui::PopStyleColor(5);
     return visible;
     return visible;
 }
 }
 
 

+ 5 - 6
Sunrise/src/server/web_service/web_service_vendor_actions.cpp

@@ -299,15 +299,14 @@ constexpr std::array<LegacyQuestStep, 3> kLegacyQuestSteps{{
     // A sale row holds its cost as u32; the mutation charges an i32, so a wider row is refused.
     // A sale row holds its cost as u32; the mutation charges an i32, so a wider row is refused.
     constexpr auto kQuantityLimit =
     constexpr auto kQuantityLimit =
         static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)());
         static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)());
-    // An exchange charges exactly one static stack, so a row that is not plainly priced, or
-    // declares any number of entries but one, is not one.
+    // An exchange charges exactly one static stack; the caller already refused a row whose
+    // entries are not its price.
     const std::span<const vendor_domain::SaleCost> price = vendor_domain::cost_entries(row);
     const std::span<const vendor_domain::SaleCost> price = vendor_domain::cost_entries(row);
     const vendor_domain::SaleCost single = price.size() == 1 ? price[0] : vendor_domain::SaleCost{};
     const vendor_domain::SaleCost single = price.size() == 1 ? price[0] : vendor_domain::SaleCost{};
     state::build_data::items::Definition cost{};
     state::build_data::items::Definition cost{};
     // A recycle row owns its purchase from here, refused or not: falling through would grant the
     // A recycle row owns its purchase from here, refused or not: falling through would grant the
     // placeholder, which is the failure this path exists to avoid.
     // placeholder, which is the failure this path exists to avoid.
-    if (row.priceState != vendor_domain::PriceState::plain || price.size() != 1
-        || single.quantity == 0 || single.quantity > kQuantityLimit
+    if (price.size() != 1 || single.quantity == 0 || single.quantity > kQuantityLimit
         || !state::build_data::find_item_definition_index(single.itemIndex, cost)) {
         || !state::build_data::find_item_definition_index(single.itemIndex, cost)) {
         core::log::writef(core::log::Channel::server,
         core::log::writef(core::log::Channel::server,
                           core::log::Level::warn,
                           core::log::Level::warn,
@@ -644,8 +643,8 @@ constexpr std::uint32_t kAbsentNameHash = 0x811C9DC5U;
  * @param opcode Opcode to report under.
  * @param opcode Opcode to report under.
  * @param vendorIndex Vendor the request names.
  * @param vendorIndex Vendor the request names.
  * @param rowIndex Sale row the request names.
  * @param rowIndex Sale row the request names.
- * @param row The resolved sale row. Its price is spent by a grant and by a bounty roll, and an
- *        exchange spends it as the stack it recycles.
+ * @param row The resolved sale row; a grant and a bounty roll spend its price, an exchange recycles
+ * it.
  * @param outcome Receives whatever mutation the row prepared.
  * @param outcome Receives whatever mutation the row prepared.
  */
  */
 void settle_vendor_row(const middleware::web_service::Message& message,
 void settle_vendor_row(const middleware::web_service::Message& message,

+ 2 - 2
Sunrise/src/state/activity_sdk/activity_sdk_relation_validation.cpp

@@ -648,10 +648,10 @@ bool relations(const Catalog& catalog) {
         bool (*run)(const Catalog&) noexcept;
         bool (*run)(const Catalog&) noexcept;
     };
     };
     // Every relation check a catalog must pass, named so a refusal reports which one failed.
     // Every relation check a catalog must pass, named so a refusal reports which one failed.
-    static constexpr std::array<Check, 8> kChecks{
+    static constexpr std::array<Check, 9> kChecks{
         {{"authored_scene_event_keys", &authored_scene_event_keys},
         {{"authored_scene_event_keys", &authored_scene_event_keys},
          {"squad_profiles", &squad_profiles},
          {"squad_profiles", &squad_profiles},
-                                                   {"task_targets", &task_targets},
+         {"task_targets", &task_targets},
          {"authored_text", &authored_text},
          {"authored_text", &authored_text},
          {"behavior_edges", &behavior_edges},
          {"behavior_edges", &behavior_edges},
          {"actor_semantics", &actor_semantics},
          {"actor_semantics", &actor_semantics},

+ 3 - 7
Sunrise/src/state/build_data/vendors/definition.h

@@ -97,11 +97,7 @@ inline constexpr std::uint32_t kPlainCostWord = 100'000U;
 
 
 /**
 /**
  * One cost entry of a sale row (row +32 array, `kSaleCostRowClass`, 48 bytes), reduced to its
  * One cost entry of a sale row (row +32 array, `kSaleCostRowClass`, 48 bytes), reduced to its
- * static item and quantity. The entry also carries two expression arrays, at +8 and +24, and a
- * word at +40; those decide whether the static quantity is the price at all, and the answer is
- * kept on the row as its `PriceState`. On Xûr's definition every entry is static: item 128 with
- * 29, 23, 97 and 9 units, the Legendary Shard prices of his weapons, armour, Fated Engram and
- * Invitation of the Nine.
+ * static item and quantity. Whether those are the price is the row's `PriceState`.
  */
  */
 struct SaleCost {
 struct SaleCost {
     /** Entry +0. Cost item-definition index. */
     /** Entry +0. Cost item-definition index. */
@@ -140,9 +136,9 @@ struct SaleRow {
     PriceState priceState{PriceState::plain};
     PriceState priceState{PriceState::plain};
 };
 };
 
 
-/** @return The static cost entries of one sale row, which are its price only while it is plain. */
+/** @return The static cost entries of one sale row; the catalog bounds the count on install. */
 [[nodiscard]] inline std::span<const SaleCost> cost_entries(const SaleRow& row) noexcept {
 [[nodiscard]] inline std::span<const SaleCost> cost_entries(const SaleRow& row) noexcept {
-    return {row.costs.data(), row.costCount > row.costs.size() ? std::size_t{0} : row.costCount};
+    return {row.costs.data(), static_cast<std::size_t>(row.costCount)};
 }
 }
 
 
 /** One category row, reduced to the definition hash a rowless request resolves through. */
 /** One category row, reduced to the definition hash a rowless request resolves through. */

+ 31 - 21
Sunrise/src/state/runtime/state_account_acquisition_runtime.cpp

@@ -161,6 +161,24 @@ using Quest = build_data::items::QuestInitialization;
 
 
 } // namespace runtime::detail
 } // namespace runtime::detail
 
 
+namespace {
+
+/** A vendor row pays its own price; a Collections pull pays the collectible's materials. */
+[[nodiscard]] bool
+charge_acquisition(const AccountState& account,
+                   std::optional<std::span<const build_data::vendors::SaleCost>> price,
+                   const build_data::collectibles::Definition* collectible,
+                   AccountState& charged,
+                   bool& changed) noexcept {
+    if (price.has_value()) {
+        return apply_sale_price(account, *price, charged, changed);
+    }
+    return collectible == nullptr
+           || apply_collection_materials(account, *collectible, charged, changed);
+}
+
+} // namespace
+
 /**
 /**
  * Inventory and quest state must come from the same locked save view.
  * Inventory and quest state must come from the same locked save view.
  * @param collectibleIndex Collections row, or kNoCollectibleIndex for an item-only grant.
  * @param collectibleIndex Collections row, or kNoCollectibleIndex for an item-only grant.
@@ -201,19 +219,15 @@ bool prepare_item_acquisition(std::uint16_t collectibleIndex,
 
 
     AccountState chargedAccount = account;
     AccountState chargedAccount = account;
     bool profileChanged = false;
     bool profileChanged = false;
-    // A vendor row pays its own price, whatever its collectible would charge from Collections;
-    // a Collections pull pays with the collectible's materials, and an item-only grant is free.
-    const bool paid = price.has_value()
-                          ? apply_sale_price(account, *price, chargedAccount, profileChanged)
-                          : !hasCollectible
-                                || apply_collection_materials(
-                                    account, collectible, chargedAccount, profileChanged);
-    if (!paid) {
+    if (!charge_acquisition(account,
+                            price,
+                            hasCollectible ? &collectible : nullptr,
+                            chargedAccount,
+                            profileChanged)) {
         return false;
         return false;
     }
     }
 
 
-    // Commit re-checks the collectible's own cost fields, so the mutation carries those; a sale
-    // price is proven only by its before/after profile images.
+    // Commit re-checks the collectible's cost fields; a sale price is proven by the profile images.
     return finalize_item_acquisition(
     return finalize_item_acquisition(
         account,
         account,
         chargedAccount,
         chargedAccount,
@@ -749,22 +763,18 @@ bool prepare_profile_item_acquisition(
     }
     }
     AccountState chargedAccount = account;
     AccountState chargedAccount = account;
     bool materialsChanged = false;
     bool materialsChanged = false;
-    // A vendor row pays its own price, whatever its collectible would charge from Collections;
-    // a Collections pull pays with the collectible's materials, and an item-only grant is free.
-    const bool paid = price.has_value()
-                          ? apply_sale_price(account, *price, chargedAccount, materialsChanged)
-                          : collectibleIndex == build_data::collectibles::kNoCollectibleIndex
-                                || apply_collection_materials(
-                                    account, collectible, chargedAccount, materialsChanged);
-    if (!paid) {
+    const bool hasCollectible = collectibleIndex != build_data::collectibles::kNoCollectibleIndex;
+    if (!charge_acquisition(account,
+                            price,
+                            hasCollectible ? &collectible : nullptr,
+                            chargedAccount,
+                            materialsChanged)) {
         return false;
         return false;
     }
     }
-    (void)materialsChanged;
     const bool actionSource =
     const bool actionSource =
         build_data::is_profile_action_source(item.definitionIndex, item.bucketId);
         build_data::is_profile_action_source(item.definitionIndex, item.bucketId);
 
 
-    // Commit re-checks the collectible's own cost fields, so the mutation carries those; a sale
-    // price is proven only by its before/after profile images.
+    // Commit re-checks the collectible's cost fields; a sale price is proven by the profile images.
     return finalize_profile_item_acquisition(
     return finalize_profile_item_acquisition(
         account,
         account,
         chargedAccount,
         chargedAccount,