Noodloft.Components 0.2.0-beta.2

This is a prerelease version of Noodloft.Components.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Noodloft.Components --version 0.2.0-beta.2
                    
NuGet\Install-Package Noodloft.Components -Version 0.2.0-beta.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Noodloft.Components" Version="0.2.0-beta.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Noodloft.Components" Version="0.2.0-beta.2" />
                    
Directory.Packages.props
<PackageReference Include="Noodloft.Components" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Noodloft.Components --version 0.2.0-beta.2
                    
#r "nuget: Noodloft.Components, 0.2.0-beta.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Noodloft.Components@0.2.0-beta.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Noodloft.Components&version=0.2.0-beta.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Noodloft.Components&version=0.2.0-beta.2&prerelease
                    
Install as a Cake Tool

Noodloft.Components

Engine-agnostic gameplay components for .NET games, with save/load built in and a Godot 4 addon on top.

The gameplay rules — how armor and resistances combine, when a level-up fires, where an item stacks — live in plain C# classes that can be unit-tested without an engine running. The engine layer is a thin shell over them.

Projects

Project What it is
Noodloft.Components The reactive component base: ReactiveComponentBase, ReactiveResult, IStatefulComponent, ILogger. Zero dependencies.
Noodloft.Components.Games The thirteen component families. Zero dependencies.
Noodloft.Components.Persistence Save/load. References the two above; System.Text.Json is in the shared framework.
Noodloft.Components.Games.Godot Logging, user:// saves and encrypted identity tokens, node lookups, and the relay multiplayer peer. Compiled, shipped as a DLL.
Noodloft.Components.Games.Godot.Addon The nodes, systems, resources and autoload. Shipped as source — see below. Needs all four packages, including .Arch.
Noodloft.Components.Games.Godot.Arch Separate package, separate audience: runs the Arch ECS inside Godot. Depends on nothing else here.

The component families

Family Owns
Health A pool with a floor and a ceiling.
Damage Both halves. DefaultAttackEvaluator turns power, strength and a crit roll into a number; DefaultDamageEvaluator decides what the target keeps of it — crit, armor, typed resistances, invulnerability frames, death, revival. Composes a health pool. Neither one owns a random number generator.
Attributes A modifiable stat: flat, additive-percent and multiplicative-percent modifiers, removable by id or by source.
Progression XP and levels, over a pluggable curve.
Skills Many named progressions under one SkillSet, each paying out SkillRewards into an AttributeSet. Rewards are re-derived from current level, never accumulated.
Items Base stats, rarity, reforges, requirements and socketed item assemblies. ItemAssembly resolves player-authored builds from catalog data; OwnedItemTransactions keeps each identity in one authoritative location.
Loot Weighted and independent tables, Magic Find and Gathering Fortune, both opt-in per entry. Takes an IRollSource, so a table and a seed always produce the same drops.
Cooldowns One cooldown, or a whole ability bar under a CooldownRegistry.
Gameplay abilities Source-derived specs, hierarchical owner tags, atomic costs, shared cooldowns and explicit activation lifetimes.
Inventory Slot-based container with stacking, an optional weight budget, and all-or-nothing or partial operations.
Interaction Focus, hold-to-complete, lock, one-shot.
Input Three components: a buffer (press buffering, hold-to-charge, double tap), a rebindable action map that saves the player's control scheme, and movement intent (circular dead zone, snapping, ramping).
Camera Trauma-based shake, an orbit rig with clamped pitch and zoom, and frame-rate-independent follow damping.

The shape every component takes

Model            mutable state, with internal setters
DTO              a readonly struct describing one operation
Evaluator        a pure static function: (DTO, Model) -> result
Component        wires them together and raises events

An operation returns one of three outcomes, and the difference matters:

var result = cooldown.SetModel(CooldownOperation.Tick(delta));

result.Outcome switch
{
    ReactiveOutcome.Applied   => // the model changed; OnModelSet fired
    ReactiveOutcome.Unchanged => // legal, but a no-op. Silent: this is the per-frame path
    ReactiveOutcome.Rejected  => // the caller asked for something impossible. Logged, with a reason
};

Input, specifically

The two halves are split because only one of them is save state.

// Timing. Never saved: restoring a half-held button leaves it stuck down forever.
if (buffer.TryConsume("jump"))   // pressed up to BufferDuration ago, and not yet claimed
    Jump();

buffer.HoldRatio("charge");      // 0..1, for a charge meter

// Bindings. Saved — but only the actions the player actually rebound, so retuning a
// default control scheme still reaches everyone who left that action alone.
bindings.SetModel(BindingOperation.Rebind("jump", new InputBinding(InputDeviceKind.Keyboard, "Enter")));

A rebind onto a button another action owns is refused and names the clash, which is what a controls menu needs in order to say "Space is already Jump". InputBindingsNode is the only thing that turns a stored binding into a real InputEvent, so a save survives an engine upgrade renumbering its keycodes.

Movement intent is the third piece — the one usually written inline in a character script and usually written slightly wrong:

movement.Set(rawX, rawY);        // straight off the device, before any dead zone
movement.Intent;                 // ramped: what the character moves along
movement.RawIntent;              // unramped: what a dodge or a dash fires along

The dead zone is a circle across the pair, not a threshold per axis. With a per-axis dead zone of 0.2, a stick pushed exactly diagonally to 0.19 on each axis reads as nothing — while its true magnitude is 0.27, well past the threshold. That is how a worn controller ends up drifting diagonally while nobody is touching it. Everything past the dead zone is rescaled from 0, so crossing it is a nudge rather than an instant fifth of full speed.

Gameplay abilities

Character classes, equipped items, installed parts, vehicles and temporary effects all grant through the same source-aware seam:

var abilities = new GameplayAbilitySystem(catalog, resourceBank, logger);

abilities.TrySetSource(
    GameplayAbilitySource.Item(bootsInstanceId),
    new GameplayAbilityGrantSet
    {
        Abilities = [new GameplayAbilityGrant { AbilityKey = "dash" }],
    },
    out _,
    out _);

var activation = abilities.TryActivate("dash"); // tags, cost and actor-level cooldown

EquipmentAbilityBinder derives item, installed-part and optional catalog-backed enchantment grants from authoritative instance ids. Enchanted copies persist only key and level; current definitions re-derive compatibility and abilities. Cooldowns survive source removal, so gear swapping cannot reset them; removing a source cancels its active abilities without touching another source's copy. The accepted activation is authority proof for game-side execution—the library does not pretend it can generically perform a dash, axe throw or vehicle boost. See gameplay-abilities.md for tags, lifetimes, persistence and the server command contract.

Camera

shake.Add(0.4f);        // trauma, 0..1 — adds rather than replacing
shake.Offset;           // where to displace the camera this frame

orbit.Look(dx, dy);     // device units; sensitivity, inversion and clamping are the component's
orbit.TargetYaw;        // what a character should face — leads the camera's own angle

Trauma, not "play a shake for 0.3 seconds". A duration-based shake restarts on every hit, so a machine gun produces one permanent small shake instead of a build-up, and two sources at once means whichever fired last wins. Trauma adds, saturates at 1, and drains from wherever it reached. The displacement is trauma squared, so the tail of a shake fades out instead of leaving the camera buzzing at low amplitude — which reads as a rendering fault rather than an impact.

Follow damping is a critically damped spring rather than the lerp(current, target, rate * delta) every tutorial reaches for. That one is frame-rate dependent: the same rate lands somewhere else at 144 fps than at 60, so a camera tuned on the developer's machine is wrong on the player's.

The camera nodes are the camera rather than driving one, so exactly one thing writes the transform per frame. Follow and shake as separate scripts is the classic way to get a camera that jitters because both assign to Position and whichever runs last wins.

Unchanged exists so that ticking sixty idle cooldowns a frame raises no events and logs nothing. Every reason string on that path is a constant, because an interpolated one would allocate on every tick of every component — measured at 128 B per tick before it was fixed.

Three words, no overlap

Word What it is Saved?
Definition Authored configuration as a plain record. Validate() returns problems; Create() builds a component. No
Resource The Godot inspector's editable wrapper around a Definition. [Export]s and a ToDefinition(). No
Snapshot Runtime state. Yes, and only this

Configuration is never written to a save file. That is what lets a designer rebalance a curve or widen a chest and have it take effect on saves that already exist, instead of being overwritten by them.

Persistence

var session = new SaveSession(new FileSystemSaveStore("saves"), gameVersion: "1.0.0");

session.Register(SaveParticipants.ForDamageable("player", damageable));
session.Register(SaveParticipants.ForInventory("player.bag", bag));

session.Save("slot1");
session.Load("slot1");

Save merges over what is already in the slot rather than replacing it, so saving while only one scene is loaded does not wipe every other scene's state. Restoring writes models directly and fires no domain events: loading a dead boss raises no OnDied, loading level 12 raises no eleven level-ups.

The full contract — the JSON shape, the type-id table, the version-mismatch rules and the config-drift table — is in docs/save-format.md.

Item modding has its own trust boundary. Definitions author sockets and compatibility tags; saves and network commands carry only instance ids and links, while the authority re-derives every stat and ability grant. The ownership transaction API, integrity audit and server command checklist are in docs/item-authority.md.

Godot

Add addons/noodloft.components/ to your project and enable the plugin. That registers the NoodloftSaveManager autoload; the nodes and resources appear in Create New Node and Create New Resource as soon as the project builds.

The addon source references all four packages, Noodloft.Components.Games.Godot.Arch included — NetworkedEntityNode links a scene node to an Arch entity. A game that uses none of the networking still needs the reference for the addon to compile.

Drop a node in, point it at a .tres, give it a Save Id, and it is saved:

GetNode<NoodloftSaveManager>("/root/NoodloftSaveManager").Save("slot1");

Nodes are gated hard on processing. SetProcess(false) is the normal state — a ready cooldown, a damageable out of invulnerability frames, an idle interactable — so a level full of components costs nothing. At 64 actors an idle frame measures 4.8 µs and 0 B allocated.

Nodes hold state; systems hold rules

A node answers what is this thing's health. It cannot answer may this attacker damage that target, because that is a question about a pair and neither node owns it. Answering it inside every weapon script is how a game ends up with six copies of the friendly-fire rule and five of them updated.

var result = damageSystem.Hit(bullet.Shooter, target, 25f, DamageType.Physical);

if (result.Landed)      SpawnHitNumber(result.AppliedAmount, result.WasFatal);
else if (result.WasBlocked) bullet.PassThrough();   // a teammate: do not consume the shot

Blocked is deliberately not Rejected. Your rules refusing a hit is the common case — a hitbox resting against a teammate refuses on every physics frame — and it must not read as something going wrong.

System Does
DamageSystem Runs composable DamageFilter resources over a hit, applies it, announces what happened.
TickSystem Ticks many component nodes from one frame callback instead of one each.
InteractionSystem2D / 3D Picks which of several things in range should hold focus, with hysteresis so the prompt does not strobe.

Rules ship as Resources — SelfDamageFilter, GroupDamageFilter, DeadTargetFilter — so turning friendly fire on for one arena is a .tres edit. Teams are Godot's own groups; this library ships no team component, because the engine already has a better one and two sources of truth would only disagree.

This is not an ECS and does not claim to be. An ECS is fast because components are flat structs in contiguous arrays walked linearly; these are class instances on scene-tree nodes, and systems on top change nothing about that. What the split does buy is real and is two things: rules in one editable place, and fewer engine crossings. Every _Process override is a marshalled call from C++ into managed code, per node, per frame — two hundred actors with a live cooldown are two hundred crossings, or one plus a C# loop under a TickSystem. Gating decides how much work happens; batching decides how often the engine has to cross into C# to do it.

The full contract — the node API, the filter interface, the recipes and the pitfalls — is in docs/godot-guide.md.

When you actually want an ECS

Noodloft.Components.Games.Godot.Arch is a separate package that runs the Arch ECS inside Godot: a Node that owns the World and drives ordered system stages, a two-way entity↔node map with a resolution cache, and sync systems that write results back onto the scene tree. It builds on the Godot glue package for engine integration.

That is the real thing — flat structs in contiguous chunks, walked linearly — and it is worth reaching for in one specific situation: thousands of similar things that per-node scripts can no longer afford. Bullets, particles with gameplay meaning, a crowd. Not the player, not a boss, not a chest: those are individually interesting, want editor authoring, and there are twelve of them.

If the goal is tidier code rather than ten thousand affordable things, the systems layer above is the answer and it keeps your nodes. See docs/arch.md.

The package also contains the server-authoritative ECS protocol under .Arch.Net: stable network IDs, compact codecs, spawn/despawn/input/snapshot systems, wrap-safe interpolation, and a GodotNetTransport bridge over SceneMultiplayer custom packets. The addon supplies NoodloftSessionNode, RelayClientNode, and NetworkedEntityNode; component state follows this one snapshot path and must not also be assigned to a MultiplayerSynchronizer.

Why the addon ships as source

Godot registers C# types as scripts keyed by a res:// path, stamped at compile time by Godot.SourceGenerators. A [GlobalClass] compiled into a referenced DLL has no such path in the consuming project, so it cannot be attached to a node in the editor and a .tres cannot name it. The node and resource types therefore ship as source; the engine-independent logic they call stays in the compiled libraries.

Shipping source also removes GodotSharp version skew: the consumer compiles against their own engine's bindings.

Build

dotnet build Noodloft.Components.slnx -c Release   # must stay at zero warnings
dotnet test  Noodloft.Components.slnx
dotnet run -c Release --project benchmarks/Noodloft.Components.Benchmarks -- --filter '*FrameLoop*' --job short

The packages multi-target net8.0 and net10.0. net8.0 is what Godot 4.4 gives a C# project by default, and a library that only shipped net10.0 would fail to install there with NU1201 — in the very engine version the addon names as its floor. The test projects target both, so the net8.0 asset is executed rather than merely compiled; running the whole suite locally therefore needs the .NET 8 runtime installed alongside the SDK. Without it, dotnet test -f net10.0 runs the net10.0 half.

Analyzers run at latest-recommended with EnforceCodeStyleInBuild, so the IDE and the command line agree on what counts as a warning. Nothing is suppressed in src/; the few suppressions that exist are scoped to a test or benchmark csproj with a written reason.

Releases

Tagging is what publishes. A vX.Y.Z tag is the version — the workflow refuses a tag that is not a semantic version, and refuses one with no matching section in CHANGELOG.md. That section becomes both the package's release notes and the body of the Gitea release, so the three can never disagree.

Below 0.1.0 every tag carries -alpha and the API is explicitly unstable.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on Noodloft.Components:

Package Downloads
Noodloft.Components.Games

Engine-agnostic gameplay components: health, damage with typed resistances, modifiable attributes, experience and levelling, authoritative socketed item assemblies, source-aware gameplay abilities, cooldowns, slot-based inventory, interaction, input buffering with rebindable controls and movement intent, and camera shake, orbit and follow damping. The rules live in pure evaluators that unit-test without an engine running. No dependencies.

Noodloft.Components.Persistence

Save and load for Noodloft components. Source-generated JSON, pluggable stores, per-entry versioning, and a merging save that does not wipe the state of scenes that are not loaded. Configuration is never written to disk, so rebalancing takes effect on existing saves.

Noodloft.Components.Games.Godot

Godot 4 glue for Noodloft components: logging, user:// save and encrypted token storage, and the WebSocket relay MultiplayerPeer used by server-authoritative games. The nodes and resources are NOT in this package. Godot keys C# script types to a res:// path, which a type inside a DLL does not have, so they ship as addon source instead.

Noodloft.Components.Games.Godot.Arch

Runs the Arch entity component system inside Godot 4: a composable runtime driven from _Process and _PhysicsProcess, a two-way map between entities and nodes, and sync systems that write simulation results back onto the scene tree. Arch is a real archetype ECS — components are flat structs packed into contiguous chunks and walked linearly — so it is worth reaching for exactly when a scene has thousands of similar things and per-node scripts have stopped being affordable. It is not a replacement for nodes, and this package does not try to make it one.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0-beta.9 91 8/14/2026
0.2.0-beta.8 88 8/13/2026
0.2.0-beta.7 87 8/13/2026
0.2.0-beta.6 80 8/13/2026
0.2.0-beta.5 76 8/13/2026
0.2.0-beta.4 92 8/13/2026
0.2.0-beta.3 83 8/13/2026
0.2.0-beta.2 96 8/13/2026
0.2.0-beta.1 78 8/10/2026
0.1.0 158 8/8/2026
0.0.1-alpha9 140 8/5/2026
0.0.1-alpha8 132 8/5/2026
0.0.1-alpha7 144 8/5/2026
0.0.1-alpha6 134 8/5/2026
0.0.1-alpha5 132 8/5/2026
0.0.1-alpha4 115 8/4/2026
0.0.1-alpha3 118 8/4/2026
0.0.1-alpha2 128 8/3/2026
0.0.1-alpha11 144 8/6/2026
0.0.1-alpha10 134 8/6/2026
Loading failed

The half a shooter needs that 0.2.0-beta.1 did not have: a body that moves in three dimensions with
the client predicting it, a shot the server decides, and items whose copies carry state that is spent.

Prediction is the reason most of this exists. A client that waits for the server before it moves feels
broken, and a client that moves without ever being corrected is a client that can walk through walls.
Both ends run the same `IMovementStep3D` at the same fixed delta, the client keeps its commands until
the server acknowledges them, and a disagreement rewinds and replays rather than snapping. Nothing
here trusts a client's position, its muzzle origin, or what it says it hit.

### Added

- **3D replication.** `Position3D`, `Velocity3D`, `Orientation` and `Motion3D` with their codecs,
 `MotionSystem3D`, `NodeSyncSystem3D`, and `NetInterpolationSystem3D` for bodies that are not local.
- **Prediction and reconciliation.** `NetPredictionSystem`, `NetReconciliationSystem`,
 `PredictionHistory`, `InputCommand`, and `IMovementStep3D`/`KinematicMovementStep3D` — one step run
 identically on both ends. `IPeerInputAcknowledgements` and the acknowledgement carried on each
 snapshot are what let a client discard commands the server has already seen.
- **Server-authoritative hitscan.** `Hitscan`, `IHitscanResolver`/`NodeHitscanResolver`,
 `IHitscanDamage`/`BodyHitscanDamage`, `NetFireSystem` and `NetHitApplySystem`. A client sends that it
 pulled a trigger; the server decides the ray, what it struck, and for how much.
- **Per-copy item state.** Durability and charges on `ItemInstanceStore`, bounded by the item's current
 definition at the moment of the write. See `docs/item-authority.md`.
- **Economy.** `CurrencyReactiveComponent`, `TraderReactiveComponent` and their definitions.
- **Vitals.** Zoned `BodyHealthReactiveComponent` and `HealingReactiveComponent`.
- Relay: `RelayClientNode.ServiceToken` and `ServiceSecret`, so a headless process with nobody signed
 in can host a session.

- Source-aware gameplay abilities: immutable definitions/catalogs, per-source granted specs,
 hierarchical owner tags, atomic resource costs, shared actor-level cooldowns, instant and
 run-to-completion lifetimes, and exact cancellation on source or tag invalidation.
- `EquipmentAbilityBinder` and `ItemDefinition.Abilities`, including exact installed-part sources in
 `ResolvedItemAssembly`; actor, item, vehicle and effect grants all use the same reconciliation seam.
- Godot `GameplayAbilitySystemNode` plus catalog/definition/grant/cost/pool resources, with item-source
 binding and signals for grants, activations, endings, cooldowns and resources.
- `SaveParticipants.ForGameplayAbilities`, persisting cooldown remainders and built-in resource
 balances while re-deriving grants and dropping transient active state.
- Catalog-backed item enchantments with tag compatibility, levels, conflicts, one-ultimate rules,
 exact-instance ability grants, live equipment reconciliation, save persistence and Godot authoring
 resources.
- Socketed, nested item assemblies with authored compatibility tags, required sockets,
 deterministic catalog-derived stats/requirements/weight/value, bounded acyclic graphs and
 persistence containing links rather than calculated values.
- `OwnedItemTransactions` for serialized inventory ↔ equipment ↔ assembly transfers and
 `OwnedItemIntegrity` for trust-boundary audits.
- Godot `ItemSocketResource`, assembly-aware `ItemResource`/`EquipmentNode`, and automatic stat
 re-derivation when an equipped build changes.

### Changed

- `NoodloftSessionNode.Scope` now asks for `relay.api` as well as `aarf.play`. The relay is shared by
 every game that uses it, so it is no longer behind one game's scope. A project overriding this
 property must add `relay.api` or it will sign in and then be unable to host or join.

### Security

- Durability and charges are clamped to the definition on every write and again on `RestoreState`, so
 an edited save claiming forty rounds in a thirty-round magazine does not become live state. Charges
 are spent all or nothing: a burst that cannot complete is refused rather than half-applied.
- An item carrying per-copy state must not stack. `Validate()` reports it, because a stack shares one
 entry and firing from one copy would otherwise empty every copy in it.
- Unknown/ungranted ability requests, malformed or oversized source sets, duplicated grants/tags,
 unaffordable multi-resource costs and client-side Godot activation are rejected without partial
 mutation. Cooldowns survive gear revocation, closing unequip/re-equip resets.
- Enchantment requests accept only an owned instance id, catalog key and bounded level; effects and
 grants are re-derived, malformed save entries are sanitized, and online clients cannot mutate
 enchantments through `EquipmentNode`.
- Duplicate instance creation, replayed unique inventory adds, duplicate identities in restored
 inventories/equipment, spoofed equipment slots, dangling/cyclic assembly links and oversized item
 identifiers are rejected or sanitized without partially applying state.
- **Snapshots, spawns, despawns and avatar assignments are applied only from the authoritative peer.**
 Every transport this library targets relays peer-addressed payloads, so a client could address
 another client directly and author its world — teleporting entities, rewriting health, spawning and
 deleting entities, or reassigning the victim's avatar. `NetSnapshotApplySystem` and `NetSpawnSystem`
 now require `NetworkRole.Client` and check `NetIncomingPacket.SourcePeerId` against their immutable
 `AuthorityPeerId` (`NetPeer.Server` by default). This rejects accidental listen-server registration
 during configuration instead of trusting peer 1 to distinguish the host from itself.
- Remote movement input is clamped to the unit disc in `NetInputApplySystem`. The wire format is two
 independently clamped axes, so a peer writing both extremes moved 41% faster diagonally than the
 bound `DefaultMovementIntentEvaluator` puts on a local device reading.
- `NetMessageQueue` bounds each message id at `MaxQueuedPerMessage` (256 by default) and counts what
 it drops. Only a handful of ids are ever drained, so any connected peer could previously grow the
 queue without limit by repeating an id nothing reads.

### Fixed

- Snapshot and spawn packets are decoded completely into temporary records before any world mutation.
 Malformed later records and duplicate network ids now reject the entire packet without partially
 applying earlier records, and the receive loop no longer throws when a second decoding pass diverges.
- A spawn batch whose factory returns a dead entity now rolls back the entities already built from
 that batch, instead of leaving them mapped and replicated with the rest of the batch dropped.
 `LastSpawnedCount` no longer reports entities a rollback removed.