Custom Block Tracking

Custom Block Tracking

Learn how zItems tracks custom blocks to ensure they drop the correct custom item when broken, and how the system handles persistence and performance.


Overview

When a player places a custom item as a block, zItems needs to remember which custom item was placed. This allows effects like Hammer and Vein Mining to drop the correct custom item instead of vanilla drops.

Example problem without tracking:

  1. Player places a custom "Ruby Ore" block (custom item ID: ruby_ore)

  2. Player breaks it with Hammer

  3. Without tracking: Drops vanilla STONE (wrong!)

  4. With tracking: Drops custom ruby_ore item (correct!)


How It Works

Architecture

zItems uses a two-layer system:

  1. Memory Cache (Guava Table) - Fast lookups during gameplay

  2. Chunk PDC (PersistentDataContainer) - Persistent storage across restarts

Memory Cache

Data Structure: Table<WorldChunkKey, Integer, String>

  • Row: WorldChunkKey - Identifies chunk across all worlds (world UUID + chunk key)

  • Column: Integer - Packed block position (x, y, z encoded in 32 bits)

  • Value: String - Custom item ID

Why this structure?

  • Fast chunk-based lookups (O(1) for most operations)

  • Efficient memory usage (only loaded chunks are cached)

  • Automatically grouped by chunk for easy save/load

Persistent Storage

Storage: Chunk PersistentDataContainer (PDC)

Each chunk stores a List<TrackedBlock> where:

When saved:

  • On ChunkUnloadEvent - Chunk is unloading

  • Memory cache → PDC

When loaded:

  • On ChunkLoadEvent - Chunk is loading

  • PDC → Memory cache


Position Packing

To save memory, block positions are packed into a single 32-bit integer.

Bit Layout

Why No Collisions?

Each coordinate occupies separate, non-overlapping bits:

  • X: Bits 13-16 (mask 0xF000 when shifted)

  • Z: Bits 9-12 (mask 0x0F00 when shifted)

  • Y: Bits 0-8 (mask 0x01FF)

Total: 4 + 4 + 9 = 17 bits used out of 32 available

Packing Algorithm

Example

Block at chunk-relative position (5, 100, 10):

Why this works:

  • X shifted left 13 bits: 5 << 13 = 0x2000

  • Z shifted left 9 bits: 10 << 9 = 0x1400

  • Y stays in place: 164 = 0xA4

  • OR them together: 0x2000 | 0x1400 | 0xA4 = 0x34A4


WorldChunkKey

Chunks are identified using a composite key:

Why include world UUID?

Without it, chunks at the same (X, Z) in different worlds would collide:

  • Overworld chunk (0, 0)

  • Nether chunk (0, 0)

With world UUID, they are distinct:

  • WorldChunkKey(overworld_uuid, chunkKey(0, 0))

  • WorldChunkKey(nether_uuid, chunkKey(0, 0))


Block Lifecycle

Tracking a Block

When: Player places a custom item as a block

Implementation: BlockTracker.trackBlock()

Breaking a Block

When: Effect handler (Hammer, VeinMiner) breaks a tracked block

Implementation: BlockTracker.getCustomBlockDrop()

Untracking a Block

When: Block is successfully broken

Important: Always call untrackBlock() after breaking a tracked block to prevent memory leaks!


Chunk Load/Unload

Loading a Chunk

When: ChunkLoadEvent fires

Implementation:

Unloading a Chunk

When: ChunkUnloadEvent fires

Implementation:

Key points:

  • Only chunks with tracked blocks save PDC data

  • Empty chunks have their PDC data removed (cleanup)

  • Cache is cleared after successful save


Integration with Effects

Hammer Effect

Order of checks:

  1. BlockTracker - zItems custom blocks

  2. CustomBlockProviderRegistry - Third-party custom blocks (ItemsAdder, etc.)

  3. Vanilla drops - Fallback


Performance Considerations

Memory Usage

Per tracked block:

  • WorldChunkKey: 16 bytes (UUID) + 8 bytes (long) = 24 bytes

  • Packed position: 4 bytes (int)

  • Item ID: ~20 bytes (String)

  • Total: ~50 bytes per block

Example server:

  • 1000 tracked blocks = ~50 KB

  • 10,000 tracked blocks = ~500 KB

  • 100,000 tracked blocks = ~5 MB

Very efficient even for large servers!

Lookup Speed

All operations are O(1) average case:

  • trackBlock() - Guava Table put

  • getTrackedItemId() - Guava Table get

  • untrackBlock() - Guava Table remove

Chunk Load/Unload

Load: O(n) where n = tracked blocks in chunk

  • Typically < 100 blocks per chunk

  • Happens asynchronously during chunk load

Unload: O(n) where n = tracked blocks in chunk

  • Happens during chunk unload

  • No gameplay impact (player isn't in chunk)


Debugging

Enable Debug Logging

Debug Messages

When tracking:

When breaking:

When loading chunk:

When unloading chunk:

Checking Cache

Clear cache (for testing):

This clears memory but does not affect persistent data in chunk PDC.


Common Issues

Custom blocks drop vanilla items

Symptoms:

  • Place custom "Ruby Ore" block

  • Break it

  • Drops vanilla STONE instead of ruby_ore

Causes:

  1. Block wasn't tracked when placed

  2. BlockTracker was cleared (server restart without chunk unload)

  3. Item ID in registry doesn't match tracked ID

Debug:

Check logs for:

If missing, the block placement event isn't being handled.

Tracked blocks not persisting

Symptoms:

  • Place custom blocks

  • Restart server

  • Blocks drop vanilla items

Causes:

  1. Chunk didn't unload before shutdown (stayed loaded)

  2. PDC data was cleared

Solution:

  • Force chunk unload before shutdown (happens automatically with graceful stop)

  • Avoid killing server process (use /stop or /restart)

Memory leak

Symptoms:

  • Memory usage increases over time

  • Thousands of tracked blocks in cache

Causes:

  • Not calling untrackBlock() after breaking blocks

  • Blocks broken by non-player causes (explosions, etc.) not handled

Solution:

  • Always call untrackBlock() in effect handlers

  • Listen to BlockBreakEvent and BlockExplodeEvent to clean up


API Usage

Tracking a Block

Getting Tracked Item

Checking if Tracked

Manual Cleanup


Best Practices

  1. Always untrack after breaking - Prevents memory leaks

  2. Use getCustomBlockDrop() in effect handlers - Ensures correct drops

  3. Don't track vanilla items - Only track custom items

  4. Check BlockTracker first - Before checking CustomBlockProviderRegistry

  5. Enable debug logging during testing - Helps catch tracking issues

  6. Use graceful server shutdown - Ensures chunks save properly


Implementation Files

  • BlockTracker.java - Main tracking logic (src/main/java/fr/traqueur/items/blocks/)

  • TrackedBlock.java - Record for persistence (api)

  • ZTrackedBlock.java - Implementation (src)

  • ZTrackedBlockDataType.java - PDC serialization (src/main/java/fr/traqueur/items/serialization/)

  • Keys.java - PDC key constants (src/main/java/fr/traqueur/items/serialization/)


  • Effect Handlers Reference - How effects use BlockTracker

  • Hooks System - Custom block provider integrations


Need help? Join our Discord or check GitHub Issues!

Last updated