Skip to main content
If you followed the Quick Start, you already have a working resolver. This page goes deeper into the three things you can configure on a resolver: archetype filtering, caching, and format variants.

Recap: What a Resolver Does

A resolver is a function you attach to a segment. NameplateBuilder calls it every tick for every visible entity. Your function reads data from the entity and returns a string to display, or null to skip that entity.
NameplateAPI.define(this, "bounty", "Bounty",
        SegmentTarget.NPCS, "$500")
    .resolver((store, entityRef, variantIndex) -> {
        BountyComponent bounty = store.getComponent(entityRef, bountyType);
        if (bounty == null) return null;
        return "$" + bounty.getAmount();
    });
The resolver receives three parameters:
  • store - The entity store. Use it to read components from the entity.
  • entityRef - A reference to the entity being processed. Pass this to store.getComponent().
  • variantIndex - Which display format the player chose (0 = default). Explained in Format Variants below.

Archetype Filtering with requires()

Without requires(), NameplateBuilder calls your resolver for every visible entity in the world - players, NPCs, projectiles, everything. Your resolver then has to check if the entity even has the right component and return null if it doesn’t. This works, but it’s wasteful. With requires(), you tell NameplateBuilder which component your resolver needs. It checks the entity’s archetype (the set of components it was created with) before calling your resolver. If the entity doesn’t have that component, your resolver is never called.
NameplateAPI.define(this, "health", "Health",
        SegmentTarget.ALL, "67/69")
    .requires(EntityStatMap.getComponentType()) // only call resolver for entities with stats
    .resolver((store, entityRef, variantIndex) -> {
        EntityStatMap stats = store.getComponent(entityRef, statMapType);
        if (stats == null) return null; // still safe to null-check
        int current = Math.round(stats.get(health).get());
        int max = Math.round(stats.get(health).getMax());
        return current + "/" + max;
    });
When to use it: Always, when your resolver reads a specific component. There’s no downside. Keep the null check anyway: requires() filters by archetype (the entity’s type), not by whether the component has data. In rare cases a component might exist but return null values. The null check is cheap insurance.

Caching with cacheTicks()

By default, your resolver runs every tick (30 times per second). For health or mana that changes constantly, that’s exactly what you want. But for data that rarely changes - like a faction name, level, or title - recomputing every tick is wasteful. cacheTicks() tells NameplateBuilder to remember the result and reuse it for a set number of ticks before calling your resolver again:
NameplateAPI.define(this, "faction", "Faction",
        SegmentTarget.NPCS, "<Undead>")
    .requires(FactionComponent.getComponentType())
    .cacheTicks(100) // recompute every 100 ticks (~3 seconds at 30 tps)
    .resolver((store, entityRef, variantIndex) -> {
        FactionComponent faction = store.getComponent(entityRef, factionType);
        if (faction == null) return null;
        return "<" + faction.getName() + ">";
    });
The cache is per entity - each entity gets its own cached result. When an entity dies, its cache is cleared automatically. Good candidates for caching: faction, level, tier, rank, title, class - anything that changes rarely or never. Bad candidates: health, mana, stamina, timers, buffs - anything that changes every few ticks.

Format Variants

Players can choose between different display formats for a segment. For example, health could be shown as "42/67", "63%", or "||||||------". The variantIndex parameter tells your resolver which format the player selected. First, register the variant names so the UI knows what to show:
NameplateAPI.defineVariants(this, "health", List.of(
    "Current/Max",    // variantIndex 0 (the default)
    "Percentage",     // variantIndex 1
    "Bar"             // variantIndex 2
));
Then handle the index in your resolver:
NameplateAPI.define(this, "health", "Health",
        SegmentTarget.ALL, "67/69")
    .requires(EntityStatMap.getComponentType())
    .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) + "%";
            case 2 -> buildBarString(current, max);
            default -> current + "/" + max; // variant 0, and any unknown index
        };
    });
Always include a default case. If a new variant is added later or the index is somehow unexpected, the default format is shown instead of crashing. If your segment only has one display format, don’t call defineVariants() and ignore the variantIndex parameter. The format button won’t appear in the UI. See Format Variants for more details on variant naming, prefix/suffix wrapping, and bar customization.

Resolver vs Manual Text Priority

If an entity has both a resolver and manual text (set via setText()) for the same segment, the manual text wins. This is intentional - it lets you override computed values for specific entities when needed. For example, you might have a resolver that computes health from stats, but use setText() to override one specific boss entity with custom text like "INVULNERABLE".

Complete Example

A full setup() method with three segments using different resolver configurations:
@Override
protected void setup() {
    ComponentType<EntityStore, EntityStatMap> statMapType =
        EntityStatMap.getComponentType();
    ComponentType<EntityStore, FactionComponent> factionType =
        FactionComponent.getComponentType();
    ComponentType<EntityStore, TitleData> titleType =
        TitleData.getComponentType();

    // Health - runs every tick, has format variants
    NameplateAPI.define(this, "health", "Health",
            SegmentTarget.ALL, "67/69")
        .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 - cached because it 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;
        });

    // Title - simple, no caching, no variants
    NameplateAPI.define(this, "title", "Title",
            SegmentTarget.PLAYERS, "[Knight]")
        .requires(titleType)
        .resolver((store, entityRef, variantIndex) -> {
            TitleData title = store.getComponent(entityRef, titleType);
            return title != null ? "[" + title.getTitle() + "]" : null;
        });
}

Next Steps

  • Manual Text - For when your data doesn’t come from an ECS component
  • Format Variants - Variant naming, prefix/suffix, and bar customization
  • Advanced - Hidden metadata keys, cleanup, and edge cases