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

Detach descendants that receive no record, as the native anchor pass does

The native anchor pass (0x141713250) runs before its remove pass: an entity that
receives a record detaches to root every direct child that received none in the
same batch. Without it the registry kept children the client had already
dropped from the group, and the next implicit group misparsed.
Confetti3 1 месяц назад
Родитель
Сommit
0249ac2a8c

+ 38 - 0
Sunrise/src/middleware/gameplay/external/composite_entity_baseline_registry.cpp

@@ -1,4 +1,5 @@
 #include <array>
+#include <bitset>
 #include <cstddef>
 #include <limits>
 #include <memory>
@@ -370,6 +371,43 @@ bool stage_entity_baseline_mutation(const CompositeEntityCodecContext& context,
         }
         return context.registry->slots[slot];
     };
+    // The native anchor pass (0x141713250) runs before its remove pass: an entity that receives a
+    // record detaches to root every descendant that received none in the same batch. A parent sent
+    // as a direct element therefore loses its children, and they outlive its removal as roots.
+    std::bitset<kMaximumEntitySlot + 1U> recorded{};
+    for (std::size_t index = 0; index < count; ++index) {
+        if (!candidate.ignoredRecordMask.test(index)) {
+            recorded.set(entity_record_at(batch, index).token.slot);
+        }
+    }
+    const auto& retained = context.registry->slots;
+    for (std::size_t slot = 0; slot < retained.size(); ++slot) {
+        if (recorded.test(slot) || !retained[slot].occupied || !retained[slot].anchorPresent) {
+            continue;
+        }
+        // The native walk detaches a stale node and does not descend into it, so a stale node's own
+        // subtree leaves with it: only the direct parent's record matters.
+        const EntityToken parent = retained[slot].anchor;
+        bool stale = false;
+        if (recorded.test(parent.slot)) {
+            const auto& staged = current_slot(parent.slot);
+            stale = staged.occupied && staged.incarnation == parent.incarnation;
+        }
+        if (!stale) {
+            continue;
+        }
+        if (changes == kEntityBatchCapacity) {
+            return false;
+        }
+        auto& change = change_at(changes++);
+        change.slot = static_cast<std::uint16_t>(slot);
+        change.expected = retained[slot];
+        change.replacement = change.expected;
+        change.replacement.anchor = {};
+        change.replacement.anchorPresent = false;
+        change.replacement.anchorOrder = 0;
+        ++candidate.detachedCount;
+    }
     std::array<EntityToken, kEntityBatchCapacity> terminals{};
     std::size_t terminalCount = 0;
     const auto append_terminal = [&](EntityToken token) {

+ 2 - 0
Sunrise/src/middleware/gameplay/external/composite_entity_codec.h

@@ -101,6 +101,8 @@ struct EntityBaselineMutation final : EntityBaselineChange {
     bool scoped{};
     std::array<EntityBaselineChange, kEntityBatchCapacity - 1> additionalChanges{};
     std::uint16_t additionalChangeCount{};
+    /** Descendants this batch detached because an ancestor received a record and they did not. */
+    std::uint16_t detachedCount{};
     std::uint64_t expectedAnchorOrder{}, replacementAnchorOrder{};
 };
 

+ 6 - 0
Sunrise/src/server/gameplay/actor_command_policy_transport.cpp

@@ -233,6 +233,12 @@ void reset_lane0_adapter(const void*, std::uint64_t groupSessionId) noexcept {
     const bool accepted = external::prepare_scoped_entity_batch(
         store, source, batch, sequence, hasSequence, ordinal, mutation);
     ReleaseSRWLockExclusive(&g_transportLock);
+    if (accepted && mutation.detachedCount != 0) {
+        report(core::log::Level::debug,
+               "ev=entity_identity stage=anchor_prune detached=%u records=%zu",
+               static_cast<unsigned>(mutation.detachedCount),
+               external::entity_record_count(batch));
+    }
     return accepted;
 }