Skip to content

Provenance

Pétrus Pradella edited this page Nov 1, 2026 · 2 revisions

Provenance

Where a block, a creature or an item came from: a player placed it, a spawner made it, a player dropped it - or nothing recorded it, and it is NATURAL. A job that pays only for natural ore, a quest that counts only wild mobs, a pickup reward that must not pay for one's own drop all ask the same question. The package is br.com.finalcraft.evernifecore.action.provenance.

ProvenanceTracker origins = ProvenanceTracker.open(getEcPluginData(), storage);   // the plugin's own ECStorage
ProvenanceHold recording = origins.record();                                    // while something needs origins

origins.getOrigin(blockPos).thenAccept(origin -> {        // async: the chunk may not be cached
    if (origin.getKind() == OriginKind.NATURAL) { ... }
    if (origin.isOwn(player)) { ... }                      // placed or dropped by that player
});
Provenance mob = origins.getOrigin(entity.getUniqueId());  // sync: creatures and items live in memory

recording.close();                                         // nothing needs origins any more

The model

Provenance carries getKind(), getPlayer() (who placed or dropped it; null for creatures), getAt() and getKey() (what was placed, spawned or dropped - a block whose key changed since, a sapling grown into a tree, is the plugin's to judge). isOwn(player) is true for a PLAYER_PLACED block or a PLAYER_DROP item of that player.

OriginKind Recorded from Kept
PLAYER_PLACED PlaceActionEvent.Post with a player behind the actor per chunk in the storage, until broken or past retention
SPAWNER SpawnActionEvent.Post, reason SPAWNER in memory, until killed
EGG reason EGG, SPAWNER_EGG, DISPENSE_EGG in memory
BREEDING reason BREEDING in memory
COMMAND reason COMMAND, CUSTOM in memory
PLAYER_DROP DropActionEvent.Post with the item entity in memory, until picked up
NATURAL no record - never stored -

BreakActionEvent.Post drops the block's record, KillActionEvent.Post the creature's and PickupActionEvent.Post the item's, each on the bus's last priority, so every other subscriber of those events still reads the origin. On Hytale every creature is natural: the server tells no spawn reason but AddReason.SPAWN, and a drop does not carry its item entity.

Recording is subscribing

A tracker records only while record() has a ProvenanceHold open (closing it twice is closing it once). The first hold subscribes to the six action events above and the last one closed unsubscribes - and those subscriptions are what switch the platform's listeners on (Events). A server where no plugin needs origins pays nothing. The flip side: a block placed while nobody recorded reads NATURAL.

Separate or shared

  • Separate (the default): open(owner, storage) gives each plugin its own tracker on its own storage - the same object for the same storage. Blocks go to the collection provenance_blocks, write-back per chunk through SVWorldDataManager (ISOLATED, so it never collides with the plugin's own block manager): a placed block is a change to the cached chunk, written on the next flush (10 min) or by flush().
  • Shared (Provenance.shared: true): the core opens one tracker on a storage of its own over the default backend of its storage.yml, and open(owner, storage) returns it to every plugin, ignoring storage (the log says so once per plugin). One source of truth for a network of plugins, at the cost of one more connection to that backend. Applied on the next restart.

ofIfPresent(owner) returns the tracker without opening one. The plugin's tracker closes - and writes its blocks - in IECPluginBootstrap.onECPluginShutdownPre, before its regions and before the plugin closes its storage.

Settings

In the core's config.yml:

Provenance:
  shared: false        # true: one tracker in EverNifeCore's own storage answers every plugin
  always: false        # true: record from boot; false: only while some plugin holds a recording
  retention:
    player-placed: 30d # how long a placed block remembers who placed it

A placed block past retention.player-placed reads NATURAL, and its record is dropped when read.

Threads and limits

  • getOrigin(UUID) and peekOrigin(block) answer on any thread; getOrigin(block) completes on the storage's thread. peekOrigin reads only cached chunks and returns null when it has no record in memory.
  • Recording runs on the action event's thread.
  • Creatures and items live in memory only, at most 50 000 (the least recently read leave first): a mob that outlives a restart reads NATURAL. A native tag on the entity is the upgrade if a product ever needs it.
  • A block moved by a piston keeps nothing: pistons post no action event. Nor does growth, nor a replant done by another plugin: a crop it replants by setting the block raises no place event, so that block reads NATURAL; a grown block keeps the record only while its key is the one that was placed (see the model).

Testing

ActionWorld in common-tests opens a plugin's tracker on an in-memory storage and posts the actions as a native listener would - see Testing Toolkit.

See also

Clone this wiki locally