|
|
@@ -239,54 +239,63 @@ struct ConfigOccurrenceCountKeyHash final {
|
|
|
value.row.spawnRuleConfigTag);
|
|
|
}
|
|
|
|
|
|
-// Squads whose spawner names no rule: emitted without a rule or anchors, placeable only with a
|
|
|
-// selected type-66 rule slot. Shares the linker's private identity and member helpers.
|
|
|
-
|
|
|
-// Match the full descriptor tuple; no schema guesses based on slot type alone.
|
|
|
+/**
|
|
|
+ * Tests that one descriptor's whole schema tuple matches its slot's proven schema. The slot type
|
|
|
+ * alone never implies a schema.
|
|
|
+ */
|
|
|
[[nodiscard]] bool unbound_descriptor_exact(
|
|
|
const topology::Snapshot& topology,
|
|
|
const std::unordered_map<std::uint32_t, const GraphSlotSchemaProvenance*>& schemas,
|
|
|
- const GraphDescriptor& d) {
|
|
|
- if (!d.complete || d.slotIndex >= topology.slots.size()
|
|
|
- || d.objectIndex >= topology.objects.size()) {
|
|
|
+ const GraphDescriptor& descriptor) {
|
|
|
+ if (!descriptor.complete || descriptor.slotIndex >= topology.slots.size()
|
|
|
+ || descriptor.objectIndex >= topology.objects.size()) {
|
|
|
return false;
|
|
|
}
|
|
|
- const auto& slot = topology.slots[d.slotIndex];
|
|
|
- if (slot.objectIndex != d.objectIndex || slot.slotType != format::kSquadSlotType) {
|
|
|
+ const auto& slot = topology.slots[descriptor.slotIndex];
|
|
|
+ if (slot.objectIndex != descriptor.objectIndex || slot.slotType != format::kSquadSlotType) {
|
|
|
return false;
|
|
|
}
|
|
|
- const auto found = schemas.find(d.slotIndex);
|
|
|
+ const auto found = schemas.find(descriptor.slotIndex);
|
|
|
const auto* match = found == schemas.end() ? nullptr : found->second;
|
|
|
- return match != nullptr && match->exact && match->componentClass == d.componentClass
|
|
|
- && match->senseSchema == d.senseSchema && match->authSchema == d.authSchema;
|
|
|
+ return match != nullptr && match->exact && match->componentClass == descriptor.componentClass
|
|
|
+ && match->senseSchema == descriptor.senseSchema
|
|
|
+ && match->authSchema == descriptor.authSchema;
|
|
|
}
|
|
|
|
|
|
-// A repeated authored config path is ambiguity, even when all paths have equal coordinates.
|
|
|
+/**
|
|
|
+ * Tests that one occurrence places the descriptor's config exactly once. A config path repeated
|
|
|
+ * in one occurrence is ambiguous, even when every copy has the same coordinates.
|
|
|
+ */
|
|
|
[[nodiscard]] bool unbound_occurrence_exact(const topology::Snapshot& topology,
|
|
|
const std::vector<const GraphConfigContext*>& contexts,
|
|
|
- const GraphDescriptor& d,
|
|
|
+ const GraphDescriptor& descriptor,
|
|
|
std::uint32_t occurrenceIndex) {
|
|
|
if (occurrenceIndex >= topology.occurrences.size()) {
|
|
|
return false;
|
|
|
}
|
|
|
- const auto& o = topology.occurrences[occurrenceIndex];
|
|
|
- if (o.objectIndex != d.objectIndex) {
|
|
|
+ const auto& occurrence = topology.occurrences[occurrenceIndex];
|
|
|
+ if (occurrence.objectIndex != descriptor.objectIndex) {
|
|
|
return false;
|
|
|
}
|
|
|
std::uint32_t count = 0;
|
|
|
bool exact = false;
|
|
|
- for (const auto* cp : contexts) {
|
|
|
- const auto& c = *cp;
|
|
|
- if (c.configTag != d.configTag || c.occurrenceIndex != occurrenceIndex) {
|
|
|
+ for (const GraphConfigContext* context : contexts) {
|
|
|
+ if (context->configTag != descriptor.configTag
|
|
|
+ || context->occurrenceIndex != occurrenceIndex) {
|
|
|
continue;
|
|
|
}
|
|
|
++count;
|
|
|
- exact = c.complete && c.objectIndex == d.objectIndex && c.scenarioIndex == o.scenarioIndex;
|
|
|
+ exact = context->complete && context->objectIndex == descriptor.objectIndex
|
|
|
+ && context->scenarioIndex == occurrence.scenarioIndex;
|
|
|
}
|
|
|
return count == 1 && exact;
|
|
|
}
|
|
|
|
|
|
-// Append independently owned sources. Never construct or modify a GraphEdge.
|
|
|
+/**
|
|
|
+ * Appends one squad per exact occurrence of a spawner that names no rule of its own. Such a squad
|
|
|
+ * has no rule edge or anchors, so it is placeable only with a selected type-66 rule slot. The
|
|
|
+ * authored graph's edges are left untouched.
|
|
|
+ */
|
|
|
[[nodiscard]] bool project_unbound_squads(const topology::Snapshot& topology,
|
|
|
const Facts& facts,
|
|
|
const GraphSnapshot& graph,
|
|
|
@@ -302,58 +311,59 @@ struct ConfigOccurrenceCountKeyHash final {
|
|
|
}
|
|
|
std::unordered_map<std::uint32_t, std::size_t> spawnersByConfig{};
|
|
|
std::unordered_map<std::uint32_t, std::vector<const GraphConfigContext*>> contextsByConfig{};
|
|
|
- for (std::size_t i = 0; i < graph.spawners.size(); ++i) {
|
|
|
- if (!spawnersByConfig.emplace(graph.spawners[i].configTag, i).second) {
|
|
|
+ for (std::size_t index = 0; index < graph.spawners.size(); ++index) {
|
|
|
+ if (!spawnersByConfig.emplace(graph.spawners[index].configTag, index).second) {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
- for (const auto& c : graph.configContexts) {
|
|
|
- contextsByConfig[c.configTag].push_back(&c);
|
|
|
+ for (const auto& context : graph.configContexts) {
|
|
|
+ contextsByConfig[context.configTag].push_back(&context);
|
|
|
}
|
|
|
- for (const auto& d : graph.descriptors) {
|
|
|
- const auto foundSource = spawnersByConfig.find(d.configTag);
|
|
|
- if (foundSource == spawnersByConfig.end()) {
|
|
|
+ for (const auto& descriptor : graph.descriptors) {
|
|
|
+ const auto foundSpawner = spawnersByConfig.find(descriptor.configTag);
|
|
|
+ if (foundSpawner == spawnersByConfig.end()) {
|
|
|
continue;
|
|
|
}
|
|
|
- const auto i = foundSource->second;
|
|
|
- if (i >= facts.spawners.size()) {
|
|
|
+ const std::size_t spawnerIndex = foundSpawner->second;
|
|
|
+ if (spawnerIndex >= facts.spawners.size()) {
|
|
|
return false;
|
|
|
}
|
|
|
- const GraphSpawner& spawner = graph.spawners[i];
|
|
|
- const SpawnerFact& source = facts.spawners[i];
|
|
|
+ const GraphSpawner& spawner = graph.spawners[spawnerIndex];
|
|
|
+ const SpawnerFact& source = facts.spawners[spawnerIndex];
|
|
|
if (!spawner.requiresSelectedRule) {
|
|
|
continue;
|
|
|
}
|
|
|
if (spawner.configTag != source.configTag) {
|
|
|
return false;
|
|
|
}
|
|
|
- if (d.componentClass != format::kSquadComponentClass
|
|
|
- || d.senseSchema != format::kSquadSenseSchema
|
|
|
- || d.authSchema != format::kSquadAuthSchema
|
|
|
- || !unbound_descriptor_exact(topology, schemas, d)) {
|
|
|
+ if (descriptor.componentClass != format::kSquadComponentClass
|
|
|
+ || descriptor.senseSchema != format::kSquadSenseSchema
|
|
|
+ || descriptor.authSchema != format::kSquadAuthSchema
|
|
|
+ || !unbound_descriptor_exact(topology, schemas, descriptor)) {
|
|
|
continue;
|
|
|
}
|
|
|
- const auto foundContexts = contextsByConfig.find(d.configTag);
|
|
|
+ const auto foundContexts = contextsByConfig.find(descriptor.configTag);
|
|
|
if (foundContexts == contextsByConfig.end()) {
|
|
|
continue;
|
|
|
}
|
|
|
std::vector<std::uint32_t> occurrenceIndexes{};
|
|
|
- for (const auto* c : foundContexts->second) {
|
|
|
- occurrenceIndexes.push_back(c->occurrenceIndex);
|
|
|
+ for (const GraphConfigContext* context : foundContexts->second) {
|
|
|
+ occurrenceIndexes.push_back(context->occurrenceIndex);
|
|
|
}
|
|
|
std::sort(occurrenceIndexes.begin(), occurrenceIndexes.end());
|
|
|
occurrenceIndexes.erase(std::unique(occurrenceIndexes.begin(), occurrenceIndexes.end()),
|
|
|
occurrenceIndexes.end());
|
|
|
- for (const auto oi : occurrenceIndexes) {
|
|
|
- if (!unbound_occurrence_exact(topology, foundContexts->second, d, oi)) {
|
|
|
+ for (const std::uint32_t occurrenceIndex : occurrenceIndexes) {
|
|
|
+ if (!unbound_occurrence_exact(
|
|
|
+ topology, foundContexts->second, descriptor, occurrenceIndex)) {
|
|
|
continue;
|
|
|
}
|
|
|
- const auto& occurrence = topology.occurrences[oi];
|
|
|
+ const auto& occurrence = topology.occurrences[occurrenceIndex];
|
|
|
std::string_view occurrenceId{};
|
|
|
if (!text_view(occurrence.id, occurrenceId)) {
|
|
|
return false;
|
|
|
}
|
|
|
- const std::array<std::string_view, 2> parts{d.id, occurrenceId};
|
|
|
+ const std::array<std::string_view, 2> parts{descriptor.id, occurrenceId};
|
|
|
std::string digest{};
|
|
|
if (!domain_hash(hasher, "sunrise-runtime-unbound-squad-v1", parts, digest)) {
|
|
|
return false;
|
|
|
@@ -361,20 +371,21 @@ struct ConfigOccurrenceCountKeyHash final {
|
|
|
PendingSquad squad{};
|
|
|
squad.row.id = "squad/" + digest;
|
|
|
squad.row.scenarioIndex = occurrence.scenarioIndex;
|
|
|
- squad.row.objectIndex = d.objectIndex;
|
|
|
- squad.row.slotIndex = d.slotIndex;
|
|
|
- squad.row.spawnerConfigTag = d.configTag;
|
|
|
+ squad.row.objectIndex = descriptor.objectIndex;
|
|
|
+ squad.row.slotIndex = descriptor.slotIndex;
|
|
|
+ squad.row.spawnerConfigTag = descriptor.configTag;
|
|
|
squad.row.spawnRuleConfigTag = format::kAbsentIndex;
|
|
|
- squad.row.occurrenceIndex = oi;
|
|
|
+ squad.row.occurrenceIndex = occurrenceIndex;
|
|
|
squad.row.flags = format::kSquadRequiresSelectedRule
|
|
|
| format::kSquadSourceDescriptorExact
|
|
|
| format::kSquadScenarioOccurrenceExact;
|
|
|
bool sourceActorLinksComplete = true;
|
|
|
- for (std::uint32_t mi = 0; mi < source.members.size(); ++mi) {
|
|
|
+ for (std::uint32_t memberIndex = 0; memberIndex < source.members.size();
|
|
|
+ ++memberIndex) {
|
|
|
SquadMember member{};
|
|
|
- if (!detail::build_member(source.members[mi],
|
|
|
+ if (!detail::build_member(source.members[memberIndex],
|
|
|
digest,
|
|
|
- mi,
|
|
|
+ memberIndex,
|
|
|
actorResolver,
|
|
|
actorContext,
|
|
|
member,
|
|
|
@@ -383,7 +394,7 @@ struct ConfigOccurrenceCountKeyHash final {
|
|
|
}
|
|
|
squad.members.push_back(std::move(member));
|
|
|
}
|
|
|
- // An unresolved unbound source must not disable the authored bound squads.
|
|
|
+ // An unresolved rule-less squad is skipped; it must not fail the authored squads.
|
|
|
if (!sourceActorLinksComplete) {
|
|
|
continue;
|
|
|
}
|
|
|
@@ -394,22 +405,22 @@ struct ConfigOccurrenceCountKeyHash final {
|
|
|
}
|
|
|
if (countValid
|
|
|
&& std::all_of(
|
|
|
- squad.members.begin(), squad.members.end(), [](const SquadMember& m) {
|
|
|
- return (m.flags & format::kSquadMemberInvariantReadyMask)
|
|
|
+ squad.members.begin(), squad.members.end(), [](const SquadMember& member) {
|
|
|
+ return (member.flags & format::kSquadMemberInvariantReadyMask)
|
|
|
== format::kSquadMemberInvariantReadyMask
|
|
|
- && m.defaultCount > 0;
|
|
|
+ && member.defaultCount > 0;
|
|
|
})) {
|
|
|
squad.row.flags |= format::kSquadCandidateCountsInvariantComplete;
|
|
|
}
|
|
|
- std::vector<std::uint32_t> keys{};
|
|
|
- for (const auto& m : squad.members) {
|
|
|
- keys.push_back(m.memberKey);
|
|
|
+ std::vector<std::uint32_t> memberKeys{};
|
|
|
+ for (const SquadMember& member : squad.members) {
|
|
|
+ memberKeys.push_back(member.memberKey);
|
|
|
}
|
|
|
- std::sort(keys.begin(), keys.end());
|
|
|
- if (std::adjacent_find(keys.begin(), keys.end()) != keys.end()) {
|
|
|
+ std::sort(memberKeys.begin(), memberKeys.end());
|
|
|
+ if (std::adjacent_find(memberKeys.begin(), memberKeys.end()) != memberKeys.end()) {
|
|
|
return false;
|
|
|
}
|
|
|
- // Empty anchors and an absent rule are intentional; kSquadRunnableMask cannot pass.
|
|
|
+ // No rule edge or anchors: placement refuses this squad unless a rule slot is selected.
|
|
|
pending.push_back(std::move(squad));
|
|
|
}
|
|
|
}
|