Skip to main content
This documentation is for NameplateBuilder v4.260326.2 with API v2.0.0.
This page walks you through adding a custom nameplate segment from scratch.

How It Works

Normally in Hytale, each entity has one Nameplate component. If two mods both try to set it, they overwrite each other. NameplateBuilder fixes this by letting each mod register named segments (like "health", "guild", "bounty"), and combining them into one nameplate per entity. Players get a UI to pick which segments they want to see, reorder them, and customize how they look. Your job as a modder is two things:
  1. Define your segment - tell NameplateBuilder it exists (name, icon, example text)
  2. Give it a value - tell NameplateBuilder how to compute the text for each entity
That’s it. NameplateBuilder handles everything else: combining segments, the player UI, saving preferences, cleaning up on death, etc.

Step 1 - Manifest Dependency

Add NameplateBuilder as a dependency in your manifest.json so it loads before your mod:
{
  "Dependencies": {
    "Frotty27:NameplateBuilder": ">=4.260326.2"
  }
}

Step 2 - API Jar

Download the API jar from CurseForge, place it in your libs/ folder, and add it to your build.gradle:
dependencies {
    compileOnly files('libs/NameplateBuilder-API-2.0.0.jar')
}
You only need this lightweight jar to compile against. It does not get bundled into your mod.

Step 3 - Define Your Segment

Call NameplateAPI.define() in your plugin’s setup() method. This tells NameplateBuilder that your segment exists so it shows up in the player UI:
import com.frotty27.nameplatebuilder.api.NameplateAPI;
import com.frotty27.nameplatebuilder.api.SegmentTarget;

public final class MyModPlugin extends JavaPlugin {

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

    @Override
    protected void setup() {
        NameplateAPI.define(this, "bounty", "Bounty",
                SegmentTarget.NPCS, "$500");
    }
}
What this does: Registers your segment in the /npb player UI with:
  • "bounty" - a unique ID for your segment (only your mod uses this)
  • "Bounty" - the display name players see in the UI
  • SegmentTarget.NPCS - a hint that this is for NPCs (can also be ALL or PLAYERS)
  • "$500" - example text shown as a preview in the UI
What this does NOT do: It does not put any text on any entity yet. The segment appears in the UI, but entities won’t show it until you provide a value.

Step 4 - Provide a Value (Resolver)

Now you need to tell NameplateBuilder what text to show for each entity. You do this with a resolver - a function that receives an entity and returns the text to display. NameplateBuilder calls it automatically for every visible entity. Chain .resolver() directly onto your define() call:
@Override
protected void setup() {
    // Store a reference to the component type your mod uses
    ComponentType<EntityStore, BountyComponent> bountyType =
        BountyComponent.getComponentType();

    NameplateAPI.define(this, "bounty", "Bounty",
            SegmentTarget.NPCS, "$500")
        .resolver((store, entityRef, variantIndex) -> {
            // Read the entity's bounty component
            BountyComponent bounty = store.getComponent(entityRef, bountyType);

            // No bounty on this entity? Return null to skip it.
            if (bounty == null) return null;

            // Return the text to display
            return "$" + bounty.getAmount();
        });
}
What this does: Every tick, for every entity a player can see, NameplateBuilder calls your resolver. Your function reads data from the entity and returns a string. If the entity doesn’t have a bounty, you return null and the segment is skipped for that entity. That’s it. No tick systems to write, no components to manage, no cleanup to handle. Your segment now works: players can add “Bounty” to their nameplate chain via /npb, and they’ll see $500 (or whatever the actual bounty is) above NPCs that have a BountyComponent.

A More Realistic Example

Here’s a complete plugin with two segments - one that reads health stats and one that reads a faction component:
@Override
protected void setup() {
    ComponentType<EntityStore, EntityStatMap> statMapType =
        EntityStatMap.getComponentType();
    ComponentType<EntityStore, FactionComponent> factionType =
        FactionComponent.getComponentType();

    // Health segment - reads from the entity's stat map
    NameplateAPI.define(this, "health", "Health",
            SegmentTarget.ALL, "67/69")
        .requires(statMapType)  // optimization: only run for entities that have stats
        .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 current + "/" + max;
        });

    // Faction segment - reads from a faction component, rarely changes
    NameplateAPI.define(this, "faction", "Faction",
            SegmentTarget.NPCS, "<Undead>")
        .requires(factionType)
        .cacheTicks(100)  // only recompute every 100 ticks since factions rarely change
        .resolver((store, entityRef, variantIndex) -> {
            FactionComponent faction = store.getComponent(entityRef, factionType);
            if (faction == null) return null;
            return "<" + faction.getName() + ">";
        });
}
Notice the two extra methods:
  • .requires(componentType) - An optimization. Tells NameplateBuilder to only call your resolver for entities that have this component in their archetype. Entities without it are skipped entirely.
  • .cacheTicks(100) - Another optimization. Caches the result for 100 ticks (~3 seconds) instead of recomputing every tick. Use this for values that don’t change often.

What About Manual Text?

The resolver pattern above covers most use cases - any time your segment’s value comes from an ECS component on the entity (health, stats, faction, level, tier, etc.). There is one scenario where resolvers don’t work: when the value doesn’t come from a component at all. For example, if an admin runs a chat command like /setbounty goblin 500 and you want to store that bounty externally (in a config or database, not as a component on the entity), there’s no component for the resolver to read. For that case, you can push text directly:
// Somewhere in your command handler:
NameplateAPI.setText(store, entityRef, "bounty", "$500");

// Later, to remove it:
NameplateAPI.clearText(store, entityRef, "bounty");
You still call define() in setup() the same way - you just skip the .resolver() part. See Manual Text for the full guide including tick system patterns. Rule of thumb: If your data lives on the entity as a component, use a resolver. If it doesn’t, use manual text.

Next Steps

  • Resolvers - requires(), cacheTicks(), and format variant support
  • Manual Text - setText(), clearText(), tick system patterns
  • Format Variants - Let players pick between display formats (e.g. “42/67” vs “63%”)
  • Advanced - Hidden metadata keys, cleanup, and edge cases