Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/roadmaps/barc-01-coordinator-admission.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,39 @@ Validation on 2026-09-28 from protected `master` `66483515`:
- `coordinator_command_admission_test` passed 100 consecutive executions.
- `CoordinatorClient.cpp` passed a standalone C++20 syntax compile against the
generated protobuf/gRPC headers.

## BARC-01.5f — bounded owned-damage replacement source window

2026-10-03 source follow-up, retaining the FSBar-owned BARC-01.5f horizon.
Owned damage now requests the existing coalesced engine-thread full entity/economy
replacement. Only handled sparse owned/enemy damage and enemy destruction arms
are removed from the coordinator projection when that replacement is scheduled;
legacy bytes, dispatch/economy order and unsupported lifecycle/unknown arms remain
intact. Resulting owned health still comes from the engine wrapper in
`SnapshotBuilder::FillOwnUnits`; no damage subtraction or protocol change occurs.

The production planner/projector and snapshot publication gate have focused
compiled coverage: 128 damage notifications coalesce to one replacement; mixed
dispatch/economy retain order; sparse-only sequence 35 is skipped and complete
replacement 36 can recover; deferred scheduling retains the replacement request;
failed build/serialization or enqueue cannot grant a new basis; recorded identity
is the exact enqueued snapshot identity. Controlled engine observations serialize
owned unit 42 with health 83 through the actual protobuf implementation. The
optional `HIGHBAR_CONTROLLED_FIXTURE_DIR` output from
`state_update_projection_test` emits legacy/projected/replacement `.pb` files for
the subsequent actual FSBar typed-reducer join. These are **controlled fixtures**,
not reconstructed native payload evidence: the retained native packet lacks raw
StateDelta bytes. This test does not execute the engine manager walk.

Qualification: focused C++ test compilation/execution and gateway syntax compile
against retained Recoil 2026.07.04 engine/SDK headers. Full ABI-matched plugin
rebuild, source/build/configuration/loaded custody, consumer metadata ordering and
native operation remain separate integrator gates. No publication or native
operation is granted; native useful play remains **0/6**. Creation, finish,
ownership, visibility entry and owned removal remain outside this source window.
Telemetry begin returned `not-configured`; native usage remains **Unknown**.
The exact-schema focused build ran **14 passing tests**; removing owned-damage
projection from a private header copy reproduced both sparse-only and burst test
failures. The gateway syntax check passed in 5.05 seconds (existing `LOG` macro
redefinition warning); the focused exact-schema compilation took 12.35 seconds
with one compiler process. Neither check rebuilt or loaded a native plugin.
16 changes: 14 additions & 2 deletions src/circuit/grpc/StateUpdateProjection.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,18 @@ class StateUpdateOrder {
bool replacement_pending_ = false;
};

// Copies the source identity and every delta arm except the two sparse enemy
// The builder may throw (including serialization failure). A failed build or
// enqueue never grants a new basis; record receives the exact enqueued update.
// Callers retain their existing exception/disabled-state boundary.
template <typename Build, typename Enqueue, typename Record>
bool PublishSnapshotBeforeBasis(Build build, Enqueue enqueue, Record record) {
auto update = build();
if (!enqueue(update)) return false;
record(update);
return true;
}

// Copies the source identity and every delta arm except the handled sparse damage/destroy
// events whose wire shape cannot update current health/presence truthfully.
// Returns false when no coordinator delta remains; the following complete
// snapshot is then the coordinator's recovery baseline across the skipped seq.
Expand All @@ -47,7 +58,8 @@ inline bool BuildCoordinatorDeltaProjection(
projection->set_frame(source.frame());
projection->set_send_monotonic_ns(source.send_monotonic_ns());
for (const auto& event : source.delta().events()) {
if (event.kind_case() == ::highbar::v1::DeltaEvent::kEnemyDamaged
if (event.kind_case() == ::highbar::v1::DeltaEvent::kUnitDamaged
|| event.kind_case() == ::highbar::v1::DeltaEvent::kEnemyDamaged
|| event.kind_case() == ::highbar::v1::DeltaEvent::kEnemyDestroyed) {
continue;
}
Expand Down
81 changes: 44 additions & 37 deletions src/circuit/module/GrpcGatewayModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,9 @@ void CGrpcGatewayModule::OnUnitDamagedFull(CCircuitUnit* unit,
SetVec3(ev->mutable_direction(), dir);
ev->set_weapon_def_id(weaponDefId);
ev->set_is_paralyzer(paralyzer);
// Damage is not resulting health (repair, regeneration and paralysis can
// intervene). Read current engine facts in the coalesced end-frame snapshot.
state_update_order_.RequestFullStateReplacement();
});
}

Expand Down Expand Up @@ -1624,7 +1627,7 @@ void CGrpcGatewayModule::FlushDelta(bool project_complete_world_state) {

// Client-mode consumers require complete world facts at damage and
// destroy boundaries. Preserve the exact legacy delta in ring/DeltaBus,
// but remove only those two sparse arms from the coordinator projection;
// but remove only handled sparse arms from the coordinator projection;
// the complete replacement snapshot follows at seq+1.
if (coordinator_client_) {
if (project_complete_world_state) {
Expand Down Expand Up @@ -1690,44 +1693,48 @@ void CGrpcGatewayModule::BroadcastSnapshot(std::uint32_t effective_cadence_frame
try {
const std::uint64_t t0 = NowMicros();

::highbar::v1::StateUpdate update;
update.set_seq(++seq_);
update.set_frame(FrameForWire(circuit));

// Build the snapshot. BuildIncremental omits StaticMap — it was
// already delivered in HelloResponse and on any StreamState
// resume-from-empty path; per-tick resends would be wasted bytes.
auto* snap = update.mutable_snapshot();
*snap = snapshot_->BuildIncremental();
snap->set_effective_cadence_frames(effective_cadence_frames);
snap->set_frame_number(FrameForWire(circuit));

// Constitution V: stamp CLOCK_MONOTONIC_ns at the moment we hand
// the frame to the fan-out. Same pattern as CoordinatorClient.
const auto snapshot_emitted_at = grpc::LiveControlState::Clock::now();
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
update.set_send_monotonic_ns(
static_cast<std::uint64_t>(ts.tv_sec) * 1000000000ULL
+ static_cast<std::uint64_t>(ts.tv_nsec));
}
grpc::LiveControlState::Clock::time_point snapshot_emitted_at;
grpc::PublishSnapshotBeforeBasis([&]() {
::highbar::v1::StateUpdate update;
update.set_seq(++seq_);
update.set_frame(FrameForWire(circuit));

// Build the snapshot. BuildIncremental omits StaticMap — it was
// already delivered in HelloResponse and on any StreamState
// resume-from-empty path; per-tick resends would be wasted bytes.
auto* snap = update.mutable_snapshot();
*snap = snapshot_->BuildIncremental();
snap->set_effective_cadence_frames(effective_cadence_frames);
snap->set_frame_number(FrameForWire(circuit));

// Constitution V: stamp CLOCK_MONOTONIC_ns at the moment we hand
// the frame to the fan-out. Same pattern as CoordinatorClient.
snapshot_emitted_at = grpc::LiveControlState::Clock::now();
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
update.set_send_monotonic_ns(
static_cast<std::uint64_t>(ts.tv_sec) * 1000000000ULL
+ static_cast<std::uint64_t>(ts.tv_nsec));
}

auto payload = std::make_shared<std::string>();
if (!update.SerializeToString(payload.get())) {
throw std::runtime_error("SerializeToString failed");
}
auto frozen = std::const_pointer_cast<const std::string>(payload);
auto payload = std::make_shared<std::string>();
if (!update.SerializeToString(payload.get())) {
throw std::runtime_error("SerializeToString failed");
}
auto frozen = std::const_pointer_cast<const std::string>(payload);

{
std::unique_lock<std::shared_mutex> lock(state_mutex_);
ring_->Push(seq_, frozen);
}
delta_bus_->Publish(frozen);
{
std::unique_lock<std::shared_mutex> lock(state_mutex_);
ring_->Push(seq_, frozen);
}
delta_bus_->Publish(frozen);

if (coordinator_client_) {
const bool enqueued = coordinator_client_->PushStateUpdate(update);
if (enqueued && live_control_state_) {
return update;
}, [&](const ::highbar::v1::StateUpdate& update) {
return coordinator_client_ && coordinator_client_->PushStateUpdate(update);
}, [&](const ::highbar::v1::StateUpdate& update) {
if (live_control_state_) {
auto units = live_control_state_->SnapshotUnitMetadata();
if (units) {
::highbar::v1::LiveSnapshotMetadata metadata;
Expand All @@ -1741,7 +1748,7 @@ void CGrpcGatewayModule::BroadcastSnapshot(std::uint32_t effective_cadence_frame
BuildAndReportTacticalSnapshot(basis);
}
}
}
});

if (counters_ != nullptr) {
counters_->RecordFrameFlushUs(NowMicros() - t0);
Expand Down
169 changes: 169 additions & 0 deletions tests/unit/state_update_projection_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

#include <gtest/gtest.h>

#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <stdexcept>

namespace {

Expand Down Expand Up @@ -46,6 +48,7 @@ TEST(StateUpdateProjection, LegacyBytesStayExactAndCoordinatorKeepsFeedback) {
legacy.set_seq(113);
legacy.set_frame(900);
legacy.mutable_delta()->add_events()->mutable_enemy_damaged()->set_enemy_id(77);
legacy.mutable_delta()->add_events()->mutable_unit_damaged()->set_unit_id(42);
legacy.mutable_delta()->add_events()->mutable_command_dispatch()->set_frame(900);
legacy.mutable_delta()->add_events()->mutable_economy_tick()->set_metal(250.0f);
legacy.mutable_delta()->add_events()->mutable_unit_idle()->set_unit_id(42);
Expand Down Expand Up @@ -92,6 +95,172 @@ TEST(StateUpdateProjection, SparseOnlyDestroyDoesNotReachCoordinatorAsDelta) {
EXPECT_FALSE(BuildCoordinatorDeltaProjection(legacy, &projected));
}

// Controlled engine observations, not a replay of retained native payloads.
// The retained packet has sequence/arm observations but no raw StateDelta.
TEST(StateUpdateProjection, OwnedDamageBurstSerializesAuthoritativeReplacement) {
StateUpdateOrder order;
::highbar::v1::StateUpdate legacy;
legacy.set_seq(35);
legacy.set_frame(900);
legacy.set_send_monotonic_ns(900000);
for (int i = 0; i < 128; ++i) {
auto* event = legacy.mutable_delta()->add_events()->mutable_unit_damaged();
event->set_unit_id(42);
event->set_damage(25.0f);
event->set_is_paralyzer(i % 2 == 0);
order.RequestFullStateReplacement();
}
legacy.mutable_delta()->add_events()->mutable_command_dispatch()->set_frame(900);
legacy.mutable_delta()->add_events()->mutable_economy_tick()->set_metal(250.0f);
const auto original = legacy.SerializeAsString();
const auto plan = order.Plan(true, true);
ASSERT_FALSE(plan.snapshot_before_delta);
ASSERT_TRUE(plan.flush_delta);
ASSERT_TRUE(plan.snapshot_after_delta);
ASSERT_FALSE(order.ReplacementPending());
::highbar::v1::StateUpdate projected;
ASSERT_TRUE(BuildCoordinatorDeltaProjection(legacy, &projected));
ASSERT_EQ(projected.delta().events_size(), 2);
EXPECT_EQ(projected.seq(), 35u);
EXPECT_EQ(projected.frame(), 900u);
EXPECT_EQ(projected.send_monotonic_ns(), 900000u);
EXPECT_EQ(projected.delta().events(0).kind_case(), ::highbar::v1::DeltaEvent::kCommandDispatch);
EXPECT_EQ(projected.delta().events(1).kind_case(), ::highbar::v1::DeltaEvent::kEconomyTick);
EXPECT_EQ(legacy.SerializeAsString(), original);
// The authoritative controlled observation is 83, regardless of 128 damage
// notifications, repair/regeneration, or paralyzer damage. No subtraction.
::highbar::v1::StateUpdate replacement;
replacement.set_seq(36);
replacement.set_frame(900);
replacement.set_send_monotonic_ns(900001);
auto* snapshot = replacement.mutable_snapshot();
snapshot->set_frame_number(900);
snapshot->set_effective_cadence_frames(1);
auto* unit = snapshot->add_own_units();
unit->set_unit_id(42);
unit->set_team_id(0);
unit->set_def_id(7);
unit->set_health(83.0f);
unit->set_max_health(100.0f);
unit->mutable_position()->set_x(100.0f);
unit->mutable_position()->set_z(200.0f);
snapshot->mutable_economy()->set_metal(250.0f);
::highbar::v1::StateUpdate decoded;
ASSERT_TRUE(decoded.ParseFromString(replacement.SerializeAsString()));
EXPECT_EQ(decoded.snapshot().own_units(0).health(), 83.0f);
EXPECT_EQ(decoded.snapshot().own_units_size(), 1);
EXPECT_FALSE(decoded.snapshot().has_static_map());
const auto next = order.Plan(false, false);
EXPECT_FALSE(next.snapshot_before_delta);
EXPECT_FALSE(next.snapshot_after_delta);
EXPECT_FALSE(next.flush_delta);
if (const char* output = std::getenv("HIGHBAR_CONTROLLED_FIXTURE_DIR")) {
std::filesystem::create_directories(output);
const auto write = [&](const char* name, const auto& update) {
std::ofstream file(std::filesystem::path(output) / name, std::ios::binary);
ASSERT_TRUE(file.good());
ASSERT_TRUE(update.SerializeToOstream(&file));
};
auto baseline = replacement;
baseline.set_seq(34);
baseline.set_frame(899);
baseline.set_send_monotonic_ns(899001);
baseline.mutable_snapshot()->set_frame_number(899);
baseline.mutable_snapshot()->mutable_own_units(0)->set_health(100.0f);
write("owned-damage-baseline.pb", baseline);
write("owned-damage-legacy.pb", legacy);
write("owned-damage-projected.pb", projected);
write("owned-damage-replacement.pb", replacement);
}
}

TEST(StateUpdateProjection, FailedBuildOrEnqueueCannotAdvanceBasis) {
using ::circuit::grpc::PublishSnapshotBeforeBasis;
int enqueue_calls = 0;
std::uint64_t basis = 34;
const auto enqueue = [&](const auto&) { ++enqueue_calls; return false; };
const auto record = [&](const auto& update) { basis = update.seq(); };
EXPECT_THROW(PublishSnapshotBeforeBasis([]() -> ::highbar::v1::StateUpdate {
throw std::runtime_error("controlled build/serialization failure");
}, enqueue, record), std::runtime_error);
EXPECT_EQ(enqueue_calls, 0);
EXPECT_EQ(basis, 34u);
EXPECT_FALSE(PublishSnapshotBeforeBasis([]() {
::highbar::v1::StateUpdate update;
update.set_seq(36);
update.mutable_snapshot();
return update;
}, enqueue, record));
EXPECT_EQ(enqueue_calls, 1);
EXPECT_EQ(basis, 34u);
}

TEST(StateUpdateProjection, BasisUsesExactEnqueuedSnapshotIdentity) {
using ::circuit::grpc::PublishSnapshotBeforeBasis;
std::string enqueued_bytes;
::highbar::v1::StateUpdate recorded;
ASSERT_TRUE(PublishSnapshotBeforeBasis([]() {
::highbar::v1::StateUpdate update;
update.set_seq(36);
update.set_frame(900);
update.set_send_monotonic_ns(900001);
update.mutable_snapshot()->set_frame_number(900);
return update;
}, [&](const auto& update) {
enqueued_bytes = update.SerializeAsString();
return true;
}, [&](const auto& update) { recorded = update; }));
EXPECT_EQ(recorded.SerializeAsString(), enqueued_bytes);
EXPECT_EQ(recorded.seq(), 36u);
EXPECT_EQ(recorded.frame(), 900u);
EXPECT_EQ(recorded.send_monotonic_ns(), 900001u);
}

TEST(StateUpdateProjection, OwnedDamageOnlySkipsSparseSequence) {
::highbar::v1::StateUpdate legacy;
legacy.set_seq(35);
legacy.mutable_delta()->add_events()->mutable_unit_damaged()->set_unit_id(42);
::highbar::v1::StateUpdate projected;
EXPECT_FALSE(BuildCoordinatorDeltaProjection(legacy, &projected));
EXPECT_EQ(projected.delta().events_size(), 0);
}

TEST(StateUpdateProjection, UnscheduledReplacementRemainsPending) {
StateUpdateOrder order;
order.RequestFullStateReplacement();
const auto deferred = order.Plan(false, true);
EXPECT_FALSE(deferred.snapshot_before_delta);
EXPECT_FALSE(deferred.snapshot_after_delta);
EXPECT_TRUE(deferred.flush_delta);
EXPECT_TRUE(order.ReplacementPending());
EXPECT_TRUE(order.Plan(true, false).snapshot_after_delta);
}

TEST(StateUpdateProjection, LifecycleArmsRemainVisibleForConsumerRefusal) {
::highbar::v1::StateUpdate source;
source.mutable_delta()->add_events()->mutable_unit_damaged()->set_unit_id(42);
source.mutable_delta()->add_events()->mutable_unit_created()->set_unit_id(43);
source.mutable_delta()->add_events()->mutable_unit_destroyed()->set_unit_id(44);
::highbar::v1::StateUpdate projected;
ASSERT_TRUE(BuildCoordinatorDeltaProjection(source, &projected));
ASSERT_EQ(projected.delta().events_size(), 2);
EXPECT_EQ(projected.delta().events(0).kind_case(), ::highbar::v1::DeltaEvent::kUnitCreated);
EXPECT_EQ(projected.delta().events(1).kind_case(), ::highbar::v1::DeltaEvent::kUnitDestroyed);
}

TEST(StateUpdateProjection, OwnedCallbackRequestsReplacementWithoutInferringHealth) {
const auto root = std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
const auto source = ReadSource(root / "src/circuit/module/GrpcGatewayModule.cpp");
const auto begin = source.find("void CGrpcGatewayModule::OnUnitDamagedFull");
const auto end = source.find("int CGrpcGatewayModule::UnitDestroyed", begin);
ASSERT_NE(begin, std::string::npos);
ASSERT_NE(end, std::string::npos);
const auto callback = source.substr(begin, end - begin);
EXPECT_NE(callback.find("mutable_unit_damaged()"), std::string::npos);
EXPECT_NE(callback.find("RequestFullStateReplacement();"), std::string::npos);
EXPECT_EQ(callback.find("set_health"), std::string::npos);
}

TEST(StateUpdateProjection, DamageBoundaryRefreshesSpringCacheBeforeReplacement) {
const auto root = std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
const auto source = ReadSource(root / "src/circuit/module/GrpcGatewayModule.cpp");
Expand Down
Loading