Skip to main content
This page covers features you’ll rarely need, but are good to know about.

Hidden Metadata Keys

Sometimes you need to store per-entity data alongside your visible segments - things like spawn timestamps, internal state flags, or tracking counters. You don’t want players to see this data in their nameplate. Any key that starts with _ (underscore) is hidden. It’s stored in the NameplateData component like any other key, but NameplateBuilder’s aggregator skips it when building the nameplate text. It never appears in the player UI either.
// Store a spawn tick so you can compute a lifetime later
data.setText("_spawn_tick", String.valueOf(currentTick));

// Store internal state - invisible to players
data.setText("_phase", "enraged");
data.setText("_last_hit_by", attackerName);

// Read it back in a later tick
String spawnTick = data.getText("_spawn_tick");
long lifetime = currentTick - Long.parseLong(spawnTick);
Why use this instead of a separate ECS component? Convenience. If you only need a couple of small values per entity and they’re closely tied to your nameplate logic, hidden keys save you from registering and managing a separate component. For larger or more structured data, a dedicated component is still the better choice.

Automatic Cleanup

NameplateBuilder handles two cleanup scenarios automatically. You don’t need to write any code for either.

Entity Death

When an entity receives a DeathComponent, NameplateBuilder immediately:
  1. Sends an empty nameplate to all viewers (clears the text above the entity)
  2. Removes the NameplateData component from the entity
This prevents stale nameplate text from lingering during death animations.

Plugin Unload

When your mod is unloaded (server shutdown, hot reload, etc.), NameplateBuilder automatically removes all segment definitions that your plugin registered. You don’t need a shutdown hook or cleanup code.

Removing a Segment at Runtime with undefine()

In rare cases, you might want to remove a segment from the UI while the server is running. For example, a minigame mod that adds segments when a game starts and removes them when it ends:
// Game starts - add the segment
NameplateAPI.define(this, "score", "Score",
        SegmentTarget.PLAYERS, "0 pts");

// Game ends - remove it from the UI
NameplateAPI.undefine(this, "score");
After calling undefine():
  • The segment disappears from the /npb UI
  • Players who had it in their chain no longer see it rendered
  • Any NameplateData text on entities is not removed - it stays until the entity dies or you explicitly clear it
Most mods will never need this. If your segment exists for the lifetime of your plugin, the automatic plugin-unload cleanup is sufficient.

Common Mistakes

Using store.addComponent() inside a tick system

Problem: The entity store is locked during tick processing. Adding a component directly throws IllegalStateException: Store is currently processing. Fix: Use commandBuffer.putComponent() instead. The CommandBuffer queues the change and applies it after the tick finishes. See Manual Text - Tick System Pattern.

Using NameplateAPI.setText() inside a tick system on a new entity

Problem: setText() tries to add a NameplateData component if the entity doesn’t have one, which hits the same store-lock issue. Fix: Build a NameplateData object manually and use commandBuffer.putComponent():
NameplateData data = new NameplateData();
data.setText("bounty", "$500");
commandBuffer.putComponent(entityRef, nameplateDataType, data);

Forgetting the manifest dependency

Problem: Calling any NameplateAPI method throws NameplateNotInitializedException. Fix: Add the dependency to your manifest.json:
{
  "Dependencies": {
    "Frotty27:NameplateBuilder": ">=4.260326.2"
  }
}

Calling undefine() in a shutdown hook

Problem: Unnecessary - NameplateBuilder already cleans up all your segments when your plugin unloads. Fix: Remove the undefine() call from your shutdown code.

Removing nameplate data on entity death

Problem: Unnecessary - NameplateBuilder already handles death cleanup automatically. Fix: Remove your death-cleanup code. Let NameplateBuilder handle it.

Not handling the default case in variant switches

Problem: If a player selects a variant index your resolver doesn’t handle, it returns null and the segment disappears. Fix: Always include a default case that returns the base format:
return switch (variantIndex) {
    case 1 -> percent + "%";
    case 2 -> barString;
    default -> current + "/" + max; // catches index 0 and any unexpected index
};

Complete Plugin Example

A full plugin using both resolvers and manual text, with format variants:
package com.example.mymod;

import com.frotty27.nameplatebuilder.api.NameplateAPI;
import com.frotty27.nameplatebuilder.api.SegmentTarget;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;

import java.util.List;

public final class MyModPlugin extends JavaPlugin {

    public MyModPlugin(JavaPluginInit init) {
        super(init);
    }

    @Override
    protected void setup() {
        ComponentType<EntityStore, EntityStatMap> statMapType =
            EntityStatMap.getComponentType();
        ComponentType<EntityStore, FactionComponent> factionType =
            FactionComponent.getComponentType();

        // Health - resolver with format variants, recomputed every tick
        NameplateAPI.define(this, "health", "Health",
                SegmentTarget.ALL, "100/100")
            .requires(statMapType)
            .resolver((store, entityRef, variantIndex) -> {
                EntityStatMap stats = store.getComponent(entityRef, statMapType);
                if (stats == null) return null;
                int current = Math.round(stats.get(health).get());
                int max = Math.round(stats.get(health).getMax());
                return switch (variantIndex) {
                    case 1 -> Math.round(100f * current / max) + "%";
                    default -> current + "/" + max;
                };
            });

        NameplateAPI.defineVariants(this, "health", List.of(
            "Current/Max", "Percentage"
        ));

        // Faction - resolver with caching, rarely changes
        NameplateAPI.define(this, "faction", "Faction",
                SegmentTarget.NPCS, "<Undead>")
            .requires(factionType)
            .cacheTicks(100)
            .resolver((store, entityRef, variantIndex) -> {
                FactionComponent faction = store.getComponent(entityRef, factionType);
                return faction != null ? "<" + faction.getName() + ">" : null;
            });

        // Guild - manual text, set from event handlers or commands
        // No resolver needed - text is pushed via setText() at runtime
        NameplateAPI.define(this, "guild", "Guild Tag",
                SegmentTarget.PLAYERS, "[Warriors]");
    }
}

Further Reading