|
|
@@ -4,8 +4,12 @@
|
|
|
|
|
|
#include <algorithm>
|
|
|
#include <cstdio>
|
|
|
+#include <new>
|
|
|
+#include <utility>
|
|
|
|
|
|
#include "../../core/logging/log.h"
|
|
|
+#include "../../state/activity_sdk/runtime.h"
|
|
|
+#include "../../state/build_data/scriptables/scriptable_catalog.h"
|
|
|
#include "../../state/gameplay/external/entity_object_types.h"
|
|
|
#include "../../state/gameplay/external/entity_position_profiles.h"
|
|
|
#include "../activity/host_runtime.h"
|
|
|
@@ -18,11 +22,43 @@ namespace policy = state::gameplay::squad_entity_retirement;
|
|
|
namespace identities = state::gameplay::entity_identity;
|
|
|
SRWLOCK g_lock{SRWLOCK_INIT};
|
|
|
policy::Store g_store;
|
|
|
+policy::Store g_propStore;
|
|
|
struct Target final {
|
|
|
std::uint64_t session{}, revision{}, generation{};
|
|
|
policy::Eligibility eligibility{};
|
|
|
};
|
|
|
std::vector<Target> g_targets;
|
|
|
+struct PropTransition final {
|
|
|
+ identities::Source source{};
|
|
|
+ std::uint64_t transition{};
|
|
|
+ std::int32_t fromRegion{}, toRegion{};
|
|
|
+ std::uint8_t bubble{};
|
|
|
+};
|
|
|
+std::vector<PropTransition> g_propTransitions;
|
|
|
+/** Eight authored states share one bubble's wire authority selector. */
|
|
|
+constexpr std::int32_t kStatesPerBubble = 8;
|
|
|
+/** One retained transition per admitted source bounds pending lifetime state. */
|
|
|
+constexpr std::size_t kMaximumPropTransitions = identities::kSourceCapacity;
|
|
|
+
|
|
|
+/** Resolves native cells through the installed destination map. */
|
|
|
+policy::CellBubbles cell_bubbles(const state::activity::SessionBinding& binding) noexcept {
|
|
|
+ policy::CellBubbles cells{};
|
|
|
+ cells.fill(-1);
|
|
|
+ const auto& destination = binding.destination;
|
|
|
+ if (destination.packageNameLength > destination.packageName.size()) {
|
|
|
+ return cells;
|
|
|
+ }
|
|
|
+ const std::string_view name(reinterpret_cast<const char*>(destination.packageName.data()),
|
|
|
+ destination.packageNameLength);
|
|
|
+ for (std::size_t index = 0; index < cells.size(); ++index) {
|
|
|
+ std::uint8_t bubble{};
|
|
|
+ if (state::gameplay::entity_position_profiles::lookup_bubble(
|
|
|
+ name, static_cast<std::uint16_t>(index), bubble)) {
|
|
|
+ cells[index] = bubble;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return cells;
|
|
|
+}
|
|
|
/** Exactly one admitted current view must own the ActivityClient generation. */
|
|
|
bool snapshot(const state::activity::SessionBinding& binding,
|
|
|
std::uint64_t generation,
|
|
|
@@ -190,6 +226,204 @@ void report(const char* stage,
|
|
|
core::log::write(core::log::Channel::server, core::log::Level::info, {line.data(), length});
|
|
|
}
|
|
|
} // namespace
|
|
|
+/**
|
|
|
+ * Captures outgoing map-prop allocations for an explicit same-bubble state reset.
|
|
|
+ * @param
|
|
|
+ * view Authenticated activity and source generation.
|
|
|
+ * @param world Pinned generated package graph
|
|
|
+ * for that activity.
|
|
|
+ * @param transition Durable mission intent identity.
|
|
|
+ * @param fromRegion
|
|
|
+ * Instantiated outgoing authored state.
|
|
|
+ * @param toRegion Requested authored state.
|
|
|
+ * @return
|
|
|
+ * Ready only after every captured tree's purge publication has committed.
|
|
|
+ */
|
|
|
+TransitionStatus begin_placed_transition(const state::activity_sdk::BoundView& view,
|
|
|
+ const state::build_data::scriptables::Snapshot& world,
|
|
|
+ std::uint64_t transition,
|
|
|
+ std::int32_t fromRegion,
|
|
|
+ std::int32_t toRegion) noexcept {
|
|
|
+ namespace data = state::build_data::scriptables;
|
|
|
+ const auto& destination = view.binding.destination;
|
|
|
+ if (!transition || !view.activityClientGeneration || world.scenarioNameLength == 0
|
|
|
+ || world.scenarioNameLength > world.scenarioName.size()
|
|
|
+ || destination.packageNameLength > destination.packageName.size()
|
|
|
+ || std::string_view(world.scenarioName.data(), world.scenarioNameLength)
|
|
|
+ != std::string_view(reinterpret_cast<const char*>(destination.packageName.data()),
|
|
|
+ destination.packageNameLength)
|
|
|
+ || !state::activity::binding_matches(view.binding)) {
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+ AcquireSRWLockShared(&g_lock);
|
|
|
+ for (const auto& prior : g_propTransitions) {
|
|
|
+ if (prior.source.activitySessionId == view.binding.sessionId
|
|
|
+ && prior.source.activityRevision == view.binding.createdRevision
|
|
|
+ && prior.source.activityClientGeneration == view.activityClientGeneration
|
|
|
+ && prior.transition == transition) {
|
|
|
+ const bool pending = g_propStore.pending(prior.source, prior.bubble);
|
|
|
+ const bool same = prior.toRegion == toRegion;
|
|
|
+ ReleaseSRWLockShared(&g_lock);
|
|
|
+ return !same ? TransitionStatus::refused
|
|
|
+ : (pending ? TransitionStatus::pending : TransitionStatus::ready);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ReleaseSRWLockShared(&g_lock);
|
|
|
+ if (fromRegion == toRegion && fromRegion >= 0) {
|
|
|
+ return TransitionStatus::ready;
|
|
|
+ }
|
|
|
+ const auto& diagnostics = world.containerPlacementDiagnostics;
|
|
|
+ if (fromRegion < 0 || toRegion < 0
|
|
|
+ || fromRegion / kStatesPerBubble >= state::activity::bubble_authority::kFallbackBubble
|
|
|
+ || fromRegion / kStatesPerBubble != toRegion / kStatesPerBubble
|
|
|
+ || world.status != data::BuildStatus::ready || world.coverage != data::BuildCoverage::full
|
|
|
+ || !diagnostics.complete || !diagnostics.contextResolved
|
|
|
+ || !diagnostics.identityOwnerInventoryComplete
|
|
|
+ || !state::gameplay::entity_object_types::available()) {
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+ const auto bubble = static_cast<std::uint8_t>(fromRegion / kStatesPerBubble);
|
|
|
+ if (bubble >= world.bubbles.size() || world.bubbles[bubble].isPublic
|
|
|
+ || static_cast<std::uint32_t>(fromRegion % kStatesPerBubble)
|
|
|
+ >= world.bubbles[bubble].stateCount
|
|
|
+ || static_cast<std::uint32_t>(toRegion % kStatesPerBubble)
|
|
|
+ >= world.bubbles[bubble].stateCount) {
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+ identities::Source source{};
|
|
|
+ std::vector<identities::Identity> rows;
|
|
|
+ if (!snapshot(view.binding, view.activityClientGeneration, source, rows)) {
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ std::vector<bool> owned(world.containerPlacementLists.size());
|
|
|
+ for (const auto& owner : world.containerPlacementOwners) {
|
|
|
+ if (owner.listRow < owned.size()
|
|
|
+ && data::container_placement_owner_applies(owner, bubble)) {
|
|
|
+ owned[owner.listRow] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::vector<std::pair<std::uint32_t, std::uint8_t>> definitions;
|
|
|
+ for (const auto& placement : world.containerPlacements) {
|
|
|
+ if (placement.listRow < owned.size() && owned[placement.listRow] && placement.complete
|
|
|
+ && world.containerPlacementLists[placement.listRow].complete
|
|
|
+ && placement.placementIdentifierRead && placement.placementIdentifier != 0
|
|
|
+ && placement.placementIdentifier != UINT64_MAX
|
|
|
+ && std::find(policy::kRetirablePropTypes.begin(),
|
|
|
+ policy::kRetirablePropTypes.end(),
|
|
|
+ placement.objectType)
|
|
|
+ != policy::kRetirablePropTypes.end()) {
|
|
|
+ definitions.emplace_back(placement.classListTag, placement.objectType);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::sort(definitions.begin(), definitions.end());
|
|
|
+ definitions.erase(std::unique(definitions.begin(), definitions.end()), definitions.end());
|
|
|
+ std::vector<policy::Eligibility> eligible;
|
|
|
+ policy::Mask mask{};
|
|
|
+ for (const auto& row : rows) {
|
|
|
+ if (!row.known || !row.present || row.conflicted || row.type != 0
|
|
|
+ || !row.metadata.hasRsat) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ state::gameplay::entity_object_types::Row object{};
|
|
|
+ if (!state::gameplay::entity_object_types::lookup(row.metadata.rsatTag, object)
|
|
|
+ || !std::binary_search(definitions.begin(),
|
|
|
+ definitions.end(),
|
|
|
+ std::pair{object.definitionTag, object.objectType})) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ policy::Eligibility target{};
|
|
|
+ target.rsatTag = object.rsatTag;
|
|
|
+ target.bubble = bubble;
|
|
|
+ target.enabled = true;
|
|
|
+ target.placedProp = true;
|
|
|
+ target.objectType = object.objectType;
|
|
|
+ if (std::find(eligible.begin(), eligible.end(), target) == eligible.end()) {
|
|
|
+ eligible.push_back(target);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // The script ends these lifetimes; release reports are not needed to authorize their end.
|
|
|
+ for (const auto& row : rows) {
|
|
|
+ if (row.present && row.token.slot < identities::kSlotCapacity) {
|
|
|
+ mask[row.token.slot / 8] |= std::byte(1U << (row.token.slot % 8));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (eligible.empty()) {
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+ const auto cells = cell_bubbles(view.binding);
|
|
|
+ AcquireSRWLockExclusive(&g_lock);
|
|
|
+ for (const auto& old : g_propTransitions) {
|
|
|
+ if (old.source.activitySessionId == source.activitySessionId
|
|
|
+ && old.source.activityClientGeneration == source.activityClientGeneration
|
|
|
+ && g_propStore.pending(old.source, old.bubble)) {
|
|
|
+ ReleaseSRWLockExclusive(&g_lock);
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::erase_if(g_propTransitions, [&](const PropTransition& old) {
|
|
|
+ if (old.source.activitySessionId != source.activitySessionId) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ g_propStore.invalidate_source(old.source, old.bubble);
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+ if (g_propTransitions.size() == kMaximumPropTransitions) {
|
|
|
+ ReleaseSRWLockExclusive(&g_lock);
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+ // Reserve before capturing so allocation failure cannot orphan an active retirement.
|
|
|
+ try {
|
|
|
+ g_propTransitions.reserve(g_propTransitions.size() + 1);
|
|
|
+ } catch (const std::bad_alloc&) {
|
|
|
+ ReleaseSRWLockExclusive(&g_lock);
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+ const bool captured =
|
|
|
+ g_propStore.capture(source, bubble, mask, rows, eligible, cells, true);
|
|
|
+ if (captured) {
|
|
|
+ g_propTransitions.push_back({source, transition, fromRegion, toRegion, bubble});
|
|
|
+ }
|
|
|
+ ReleaseSRWLockExclusive(&g_lock);
|
|
|
+ report("placed_transition", captured, bubble);
|
|
|
+ return captured ? TransitionStatus::pending : TransitionStatus::refused;
|
|
|
+ } catch (const std::bad_alloc&) {
|
|
|
+ return TransitionStatus::refused;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Reports a retained world change without starting a new capture. */
|
|
|
+bool placed_transition_pending(const state::activity::SessionBinding& binding,
|
|
|
+ std::uint64_t generation) noexcept {
|
|
|
+ AcquireSRWLockShared(&g_lock);
|
|
|
+ const bool pending = std::any_of(
|
|
|
+ g_propTransitions.begin(), g_propTransitions.end(), [&](const PropTransition& transition) {
|
|
|
+ return transition.source.activitySessionId == binding.sessionId
|
|
|
+ && transition.source.activityRevision == binding.createdRevision
|
|
|
+ && transition.source.activityClientGeneration == generation
|
|
|
+ && g_propStore.pending(transition.source, transition.bubble);
|
|
|
+ });
|
|
|
+ ReleaseSRWLockShared(&g_lock);
|
|
|
+ return pending;
|
|
|
+}
|
|
|
+
|
|
|
+/** Withdraws the exact mission's captured prop trees without sending a purge. */
|
|
|
+void cancel_placed_transition(const state::activity::SessionBinding& binding,
|
|
|
+ std::uint64_t generation,
|
|
|
+ std::uint64_t transition) noexcept {
|
|
|
+ AcquireSRWLockExclusive(&g_lock);
|
|
|
+ std::erase_if(g_propTransitions, [&](const PropTransition& row) {
|
|
|
+ const bool matches = row.source.activitySessionId == binding.sessionId
|
|
|
+ && row.source.activityRevision == binding.createdRevision
|
|
|
+ && row.source.activityClientGeneration == generation
|
|
|
+ && (transition == 0 || row.transition == transition);
|
|
|
+ if (matches) {
|
|
|
+ g_propStore.invalidate_source(row.source, row.bubble);
|
|
|
+ }
|
|
|
+ return matches;
|
|
|
+ });
|
|
|
+ ReleaseSRWLockExclusive(&g_lock);
|
|
|
+}
|
|
|
/** Delivered squad choices replace eligibility for their exact authored target. */
|
|
|
void record_delivered_target(const state::activity::SessionBinding& binding,
|
|
|
std::uint64_t generation,
|
|
|
@@ -289,6 +523,7 @@ void returned_slots(const state::activity::SessionBinding& binding,
|
|
|
const state::activity::bubble_authority::EntitySlotMask& mask) noexcept {
|
|
|
AcquireSRWLockExclusive(&g_lock);
|
|
|
g_store.returned_slots(binding.sessionId, generation, mask);
|
|
|
+ g_propStore.returned_slots(binding.sessionId, generation, mask);
|
|
|
ReleaseSRWLockExclusive(&g_lock);
|
|
|
}
|
|
|
/** A fresh atomic identity snapshot must still match each captured tree. */
|
|
|
@@ -303,7 +538,9 @@ bool prepare_retirement(const state::activity::SessionBinding& binding,
|
|
|
return false;
|
|
|
}
|
|
|
AcquireSRWLockShared(&g_lock);
|
|
|
- const bool ready = g_store.prepare(source, bubble, rows, output);
|
|
|
+ const bool ready = g_propStore.pending(source, bubble)
|
|
|
+ ? g_propStore.prepare(source, bubble, rows, output)
|
|
|
+ : g_store.prepare(source, bubble, rows, output);
|
|
|
ReleaseSRWLockShared(&g_lock);
|
|
|
report("prepare", ready, bubble, &output.entities);
|
|
|
return ready;
|
|
|
@@ -333,11 +570,12 @@ bool begin_retirement_publication(const state::activity::SessionBinding& binding
|
|
|
}
|
|
|
RetirementPlan current{};
|
|
|
AcquireSRWLockShared(&g_lock);
|
|
|
- const bool valid = g_store.prepare(plan.source, plan.bubble, rows, current)
|
|
|
- && current.source == plan.source && current.entities == plan.entities
|
|
|
- && current.lifetimes == plan.lifetimes
|
|
|
- && current.lifetimeCount == plan.lifetimeCount
|
|
|
- && current.revision == plan.revision && current.bubble == plan.bubble;
|
|
|
+ const auto& store = plan.placedProps ? g_propStore : g_store;
|
|
|
+ const bool valid =
|
|
|
+ store.prepare(plan.source, plan.bubble, rows, current) && current.source == plan.source
|
|
|
+ && current.entities == plan.entities && current.lifetimes == plan.lifetimes
|
|
|
+ && current.lifetimeCount == plan.lifetimeCount && current.revision == plan.revision
|
|
|
+ && current.bubble == plan.bubble && current.placedProps == plan.placedProps;
|
|
|
ReleaseSRWLockShared(&g_lock);
|
|
|
if (!valid) {
|
|
|
lease.release();
|
|
|
@@ -352,15 +590,23 @@ bool validate_retirement(const state::activity::SessionBinding& binding,
|
|
|
return plan.pending && prepare_retirement(binding, generation, plan.bubble, current)
|
|
|
&& current.source == plan.source && current.entities == plan.entities
|
|
|
&& current.lifetimes == plan.lifetimes && current.lifetimeCount == plan.lifetimeCount
|
|
|
- && current.revision == plan.revision && current.bubble == plan.bubble;
|
|
|
+ && current.revision == plan.revision && current.bubble == plan.bubble
|
|
|
+ && current.placedProps == plan.placedProps;
|
|
|
}
|
|
|
/** Retires exact lifetimes only after their carrying publication and identity lease have ended. */
|
|
|
void commit_retirement(const RetirementPlan& plan) noexcept {
|
|
|
AcquireSRWLockExclusive(&g_lock);
|
|
|
- const bool committed = g_store.commit(plan);
|
|
|
+ auto& store = plan.placedProps ? g_propStore : g_store;
|
|
|
+ const bool stored = store.commit(plan);
|
|
|
+ // A prop purge already published remains final if its script was cancelled after validation.
|
|
|
+ const bool committed = stored
|
|
|
+ || (plan.placedProps && plan.pending && plan.lifetimeCount != 0
|
|
|
+ && plan.lifetimeCount <= plan.lifetimes.size());
|
|
|
if (committed) {
|
|
|
g_store.returned_slots(
|
|
|
plan.source.activitySessionId, plan.source.activityClientGeneration, plan.entities);
|
|
|
+ g_propStore.returned_slots(
|
|
|
+ plan.source.activitySessionId, plan.source.activityClientGeneration, plan.entities);
|
|
|
}
|
|
|
ReleaseSRWLockExclusive(&g_lock);
|
|
|
if (committed) {
|
|
|
@@ -372,7 +618,9 @@ void commit_retirement(const RetirementPlan& plan) noexcept {
|
|
|
void reset() noexcept {
|
|
|
AcquireSRWLockExclusive(&g_lock);
|
|
|
g_store.reset();
|
|
|
+ g_propStore.reset();
|
|
|
g_targets.clear();
|
|
|
+ g_propTransitions.clear();
|
|
|
ReleaseSRWLockExclusive(&g_lock);
|
|
|
}
|
|
|
} // namespace sunrise::server::gameplay::squad_entity_retirement
|