Skip to content
Pétrus Pradella edited this page Nov 2, 2026 · 4 revisions

NPCs

The core knows which NPC this is - not how to make one. A quest, a dialogue or a shop names an NPC by an NpcRef, and every NPC plugin (or the platform) answers for its own through an NpcProvider. Package br.com.finalcraft.evernifecore.npc (:common, no platform imports).

NpcRef - provider:id

NpcRef guard = NpcRef.parse("citizens:12");      // or NpcRef.of("citizens", "12")
guard.getProvider();                             // "citizens"
guard.getId();                                   // "12"
guard.toString();                                // "citizens:12" - what parse reads back
Provider Id Example
citizens the numeric NPC id citizens:12
fancynpcs the id of its NpcData (a UUID) fancynpcs:0b7d...
znpcsplus the entry id an admin chose znpcsplus:merchant
hytale the entity's UUID hytale:4f2a...

Both halves are lowercased: NPC plugins differ in case, and each provider compares ids ignoring it. A malformed text throws IllegalArgumentException saying it expects provider:id. A reference to a provider nobody registered (pixelmon:... without the plugin that provides it) is still valid: every lookup simply finds nothing. What must fail loud at load time calls Npcs.requireProvider(ref), which throws NpcProviderException:

The npc reference pixelmon:4f2a names the provider 'pixelmon', but no plugin registered it on this server
(registered: citizens, fancynpcs, znpcsplus). Install the plugin that provides it or fix the reference.

Looking an NPC up - Npcs

Optional<WorldLocPos> where = Npcs.getLocation(guard); // the stored location - a despawned NPC answers too
Optional<String> name       = Npcs.getName(guard);     // display name, colour codes kept
Optional<NativeEntity> live = Npcs.find(guard);        // its entity now; empty when despawned or a packet NPC
Optional<NpcRef> which      = Npcs.resolve(nativeEntity); // which NPC an entity is, by the first available provider

Each lookup goes to the provider the reference names and answers empty while that provider is unregistered or unavailable (its plugin absent). find runs on the main thread on Bukkit and on the entity's world thread on Hytale; from another thread read getLocation.

Who the NPC is in an action - Actor.getNpc()

Every action context the platforms build carries the NPC on its actor: Actor.getNpc() is the NpcRef a provider recognised, null for a player, a block, the environment or an entity no provider knows. The three triggers a quest or a dialogue compares are action events:

Trigger Event NPC
talked to InteractNpcActionEvent (source interact-npc), getContext().getClickType() = RIGHT event.getContext().getEntity().getNpc()
punched the same event, getClickType() = LEFT the same
handed an item the same event plus getContext().getItemInHandKey()
killed KillActionEvent getContext().getVictim().getNpc() - NPCs with an entity only

A denied interact-npc cancels the NPC plugin's click, not the punch: the punch's damage is the damage source's, asked on the NPC's entity when it has one (a packet NPC has none, so it takes no hit).

ECEventBus.global().subscribe(InteractNpcActionEvent.Post.class, event -> {
    if (guard.equals(event.getContext().getEntity().getNpc())) {
        // the player event.getContext().getPlayer() talked to the guard
    }
});

interact-npc is a source apart from interact: a packet NPC has no server entity to raise an interact, so each provider raises it from its plugin's own click events - both clicks, each with its EntityContext.ClickType (RIGHT the use, LEFT the punch; Citizens' NPCRightClickEvent and NPCLeftClickEvent, the getInteractionType/getClickType of FancyNpcs and ZNPCsPlus). A listener that only means a talk checks getClickType() == RIGHT; the stock consumers already do: a dialogue opens on the right click only, and an interact-npc objective counts the right click unless it says click: left ({ id: punch, type: interact-npc, npc: "citizens:12", click: left }). Like every source it is on-demand: a provider subscribes to its plugin's click only while somebody needs interact-npc (a region, or a listener of InteractNpcActionEvent), and a cancelled Pre cancels the plugin's click - for a punch, the plugin's own left-click event, so whatever that plugin does on a left click (its click actions or commands) does not run. No core flag decides interact-npc; only a listener that cancels the Pre, or a plugin's own flag, denies it. On Hytale the right click is the Use of the NPC (UseEntityEvent.Pre, InteractionType.Use); the left click is a hit the player lands on it with his own hand or weapon - the server's primary attack ends in a Damage, never in a UseEntityEvent (only UseEntityInteraction, the Use root, fires that) - so the DAMAGE System raises it as LEFT, and a cancelled Pre cancels the hit. A projectile is no click. A UseEntityEvent a custom root fires with InteractionType.Primary is a LEFT too.

Packet NPCs (FancyNpcs, ZNPCsPlus) have no entity on the server: they never appear as the victim of a hit or a death, nor as the actor of a Bukkit event. Only "talked to" and "handed an item" exist for them, and their actor is keyed <provider>:npc with no native handle.

near-npc and waypoints

require:
  type: near-npc
  npc: citizens:12
  radius: 5          # blocks

Passes while the player stands within radius of the NPC's stored location, in the same world; fails with Go to <name> (23 blocks away). (or (in <world>)). getNeeds() is {MOVE}. An NPC of an unregistered provider fails the load with the text above.

A waypoint to an NPC points at its stored location, read again on every refresh; while the provider is gone the waypoint hides itself. The constructor refuses an unregistered provider like near-npc:

Waypoints.get().show(player, Waypoint.of(WaypointId.of(plugin, "guard"), new NpcWaypointTarget(guard)).label("Guard").build());

Registering a provider

A plugin that owns its NPCs (a Pixelmon bridge, a custom NPC system) implements NpcProvider and registers it once, in its enable:

public final class PixelmonNpcProvider implements NpcProvider {
    @Override public String getId() { return "pixelmon"; }
    @Override public boolean isAvailable() { return true; }                     // cheap: called on every actor built
    @Override public Optional<NpcRef> resolve(NativeEntity entity) { ... }     // entity -> pixelmon:<uuid>
    @Override public Optional<WorldLocPos> getLocation(NpcRef ref) { ... }
    @Override public Optional<String> getName(NpcRef ref) { ... }
    @Override public void start() { /* subscribe to the plugin's click, post interact-npc */ }
    @Override public void stop() { /* unsubscribe */ }
}

Npcs.register(getPluginData(), new PixelmonNpcProvider());

One provider per id: a second registration of the same id throws IllegalStateException naming both owners. The owner's shutdown unregisters it (Npcs.unregisterAll(owner), run by the bootstrap). A provider with entities only needs resolve: the platform's actor-building asks it. A provider that raises clicks posts Actions.denied(context) (cancel the click when true) and then Actions.post(EventSource.INTERACT_NPC, ...) with an EntityContext of source INTERACT_NPC whose entity actor carries the reference.

Platforms

  • Bukkit - McNpcProviders registers Citizens, FancyNpcs and ZNPCsPlus on every server, by reflection only (no dependency): see Integrations.
  • Hytale - HyNpcProvider: every entity with an NPCEntity component is hytale:<uuid>, its role stays the actor's key (hytale:<npc type id>), and a Use of it raises interact-npc from the use System. See Hytale Platform.

Testing

common-tests has FakeNpcProvider - an NPC without a plugin:

FakeNpcProvider citizens = new FakeNpcProvider("citizens")
        .at(guard, WorldLocPos.of(10, 64, 10, "world")).named(guard, "Guard")
        .knows(nativeEntity, guard);          // resolve/find
Npcs.register(owner, citizens);
citizens.click(player, guard);                // raises interact-npc (RIGHT) while the source is needed
citizens.leftClick(player, guard);            // ... the punch (LEFT)
citizens.click(player, guard, "minecraft:emerald");   // ... holding an emerald
// after the test: Npcs.unregisterAll(owner)

Actors.npc("citizens:npc", guard) builds an NPC actor for a hand-made context.

See also

Clone this wiki locally