Mod block entities can replace, clear and atomically take an owner-controlled batch of client update fields without saving it or changing retained contents and revisions. The lazy queue is empty for new, decoded and cloned entities; normal loaded-chunk and block broadcast paths remain unchanged. Validated with 3 real entity lifecycle/concurrency/poison tests and 3 reusable save/chunk/two-viewer Hub tests. Strict all-target Clippy passes for horizon-client-render and horizon-client-lib. No new dependency, third-party implementation or resource files. Co-authored-by: Codex <[email protected]>
2.9 KiB
Transient client update fields
A mod block entity can own a batch of fields that belongs in the next client update, but not in its saved contents. Examples include the items used by a processing particle effect. Repeated block-change notifications may coalesce; the batch is consumed when the update tag is constructed, not when an update is requested.
ModBlockEntity provides three methods:
set_client_update_fields(&mut self, Compound)replaces the pending batch.clear_client_update_fields(&mut self)discards it.take_client_update_fields(&self) -> Compoundatomically drains it.
The block entity's owner explicitly extends its API 45
native_block_entity_update_tag result with the drained fields. The engine does
not add them automatically. If clients need an explicit empty value on a later
update, the owner supplies that default in its tag. Setting fields does not
schedule an update: use the ordinary block-entity synchronization path for that.
The private OnceLock<Mutex<Compound>> is not initialized by an idle entity,
an empty replacement, a clear, or a take. It is safe to consume through the
shared reference available to API 45, including saved chunks encoded by worker
threads. A poisoned lock is recovered with its pending data intact. The fields
are not part of ModBlockEntityFields: they are neither saved nor included in
equality, debug output, or revision changes. A clone is a different entity and
starts with no pending batch; a save does not consume the live entity's batch,
and cold decoding starts empty.
Loaded chunks use ChunkEncoder::body_in and the live entity map. The block
broadcast path constructs each entity's tag once and sends the encoded packet
to all viewers; an update with no viewers does not construct or consume a tag.
Saved chunks beyond view distance are loaded independently and do not inherit
a live entity's pending effects. These existing boundaries are unchanged.
Validation covers replacement and cancellation, concurrent drains, poison recovery, clone identity, cold save/decode, loaded-chunk encoding, and a Hub broadcast to two viewers after a skipped broadcast with no viewers. The tests exercise the ordinary encoder and broadcaster; the API does not introduce an additional packet or timing model.
Run the focused tests and strict checks from the workspace root:
cargo test -p horizon-client-render --lib client_update
cargo test -p horizon-client-lib --lib transient_client
cargo clippy -p horizon-client-render -p horizon-client-lib --all-targets -- -D warnings
The first command runs three entity tests; the second runs three save/chunk/Hub
tests. They need no prepared game resources. The Hub test opens an ephemeral
loopback listener. The reusable crate is horizon-client-lib, whose source lives
under crates/horizon-client/src; selecting the startup horizon-client package
does not run these reusable tests.