API - MarkusBordihn/BOs-Easy-NPC GitHub Wiki

Easy NPC API 🧩

Easy NPC exposes a public API for mods that want to create their own NPC types, control existing NPCs, or reuse the rendering pipeline. Everything below lives under de.markusbordihn.easynpc.api.

The API is still evolving. Breaking changes can happen in minor versions until it is declared stable; if a source file and this page disagree, the source code is the current truth.

What You Can Do

Goal Start at
Create your own NPC entity type api.npc raw classes, see below
Query, spawn, or despawn NPCs from code api.handler.EasyNPCEntityHandler
Let an NPC talk or act from code api.action.EasyNPCActionHandler
Register your own action or condition api.action.ActionRegistry, api.condition
React to dialogs, actions, and states api.event.EasyNPCEventRegistry
Set poses programmatically api.pose.ModelPoseAPI
Give an entity variant-based textures api.skin, see API-Variant-System
Add or replace model geometry api.model, see API-Custom-Models

Custom NPC Types

Extend one of the raw NPC classes in de.markusbordihn.easynpc.api.npc.raw. They already contain the Easy NPC data, sync, and configuration handling:

public class MyCustomHorse extends HorseRaw {

  public MyCustomHorse(EntityType<? extends Horse> entityType, Level level) {
    super(entityType, level);
  }
}

Then register the entity type with your mod loader (Forge, Fabric, or NeoForge) as usual.

Raw classes are available for most vanilla mob families - humanoid, zombie, skeleton, villager, illager, piglin, horse, spider, slime, ghast, wolf, cat, fox, pig, chicken, allay, vex, creeper, enderman, iron golem, and witch. PathfinderMobRaw is the generic base.

Override getConfigurationData() from api.npc.BaseEasyNPC to control which configuration screens your NPC offers.

Managing NPCs from Code

EasyNPCEntityHandler works on the server-side NPC index, including NPCs that are currently despawned:

Collection<SavedNPCEntityEntry> all = EasyNPCEntityHandler.getAll();
Collection<SavedNPCEntityEntry> mine = EasyNPCEntityHandler.getByOwner(ownerUUID);
Collection<SavedNPCEntityEntry> here = EasyNPCEntityHandler.getByDimension("minecraft:overworld");

EasyNPCEntityHandler.spawn(uuid, serverLevel);
EasyNPCEntityHandler.spawn(uuid, serverLevel, position);
EasyNPCEntityHandler.despawn(uuid, serverLevel, NPCRemovalReason.DESPAWNED);

Lookups are also available by entity type and by custom identifier.

Letting an NPC Act

EasyNPCActionHandler uses the same executors as preset actions, including sender name validation, dialog conditions, and trading checks:

EasyNPCActionHandler.say(easyNPC, "Good to see you again!");
EasyNPCActionHandler.say(easyNPC, List.of("Hello!", "Welcome back!"));
EasyNPCActionHandler.sayTo(easyNPC, serverPlayer, "This one is only for you.");
EasyNPCActionHandler.showSpeechBubble(easyNPC, "text.my_mod.npc.greeting");
EasyNPCActionHandler.showSpeechBubble(easyNPC, List.of("Look up!", "Over here!"));

EasyNPCActionHandler.setState(easyNPC, questStateId, StateEntry.of(3), serverPlayer);
EasyNPCActionHandler.openDialog(easyNPC, serverPlayer, "quest_start");
EasyNPCActionHandler.trigger(easyNPC, ActionEventType.ON_INTERACTION, ActionContext.of(serverPlayer));

A text matching the translation key format is translated on the client; all other text is sent as written. The talk and speech bubble methods accept one text or a list of up to six texts and select one entry per call. Invalid input is logged and returns false. Client-side NPC instances are ignored.

Command actions have no convenience method because their permission level is stored on the action entry. Pass an ActionDataEntry to EasyNPCActionHandler.execute(...) for these actions.

Event Audiences

An event that applies to several players provides an ActionContext:

public record ActionContext(
    ActionEventType eventType,
    ServerPlayer initiator,
    List<ServerPlayer> audience,
    ResourceLocation sourceId) {}

initiator is the single player macros like @initiator resolve against; audience lists every player the event applies to, so an integration can filter it. For a time based, environment, or spawn event the audience is every player within 16 blocks, and the owner of the NPC is preferred as the initiator. For ON_STATE_CHANGE the sourceId names the state that changed.

Listeners keep their old signature and can override the context variant instead:

EasyNPCEventRegistry.registerActionEventListener(
    (easyNPC, actionDataEntry, actionContext) -> {
      for (ServerPlayer serverPlayer : actionContext.audience()) {
        MyMod.noticed(easyNPC, serverPlayer);
      }
    });

Custom Actions and Conditions

ActionRegistry.register(
    new ResourceLocation("my_mod", "teleport"),
    (actionDataEntry, easyNPC, serverPlayer, arguments) -> MyMod.teleport(easyNPC, arguments));

ConditionRegistry.register(
    new ResourceLocation("my_mod", "has_quest"),
    (conditionDataEntry, serverPlayer, npcContext) -> MyQuests.isActive(serverPlayer));

A preset then calls the action as my_mod:teleport home 3. An event without a triggering player, such as On Spawn or On State Change, passes null as the server player. A condition that needs a player must return false.

Poses

ModelPoseAPI is the supported way to change a pose without touching internal data classes:

ModelPoseAPI.setPose(npc, new ResourceLocation("easy_npc", "pose/humanoid/sitting"));
ModelPoseAPI.setPose(npc, "sitting");
ModelPoseAPI.setVanillaPose(npc, Pose.CROUCHING);
ModelPoseAPI.resetPose(npc);

String currentPose = ModelPoseAPI.getCurrentPoseName(npc);
ModelPose mode = ModelPoseAPI.getCurrentPoseMode(npc);
Set<ResourceLocation> available = ModelPoseAPI.getAvailablePoses(skinModel);

setPose(npc, "sitting") resolves the pose against the NPC's own skin model, which is the easier call when you do not want to build the ResourceLocation yourself.

setVanillaPose(...) clears custom rotation and position data, so it is also the way back to plain vanilla behavior.

Entity Data

Per-NPC data is reached through the getEasyNPC…Data() accessors on EasyNPC<?>, for example:

SkinDataCapable<?> skinData = npc.getEasyNPCSkinData();
ModelDataCapable<?> modelData = npc.getEasyNPCModelData();
ProgressionDataCapable<?> progression = npc.getEasyNPCProgressionData();

These interfaces live in de.markusbordihn.easynpc.entity.easynpc.data. They are stable enough to build on, but they are not part of the api package - not every internal class is meant to be an extension point.

Saving Changed Data

The SavedNPCEntityEntry results of EasyNPCEntityHandler.getAll() and its siblings come from a copy Easy NPC keeps of every NPC, which is what makes despawned NPCs findable at all.

That copy is refreshed from a dirty flag, not on every unload. The flag is set automatically when you go through the normal setters, which covers everything the API and the data interfaces offer:

npc.setSynchedEntityData(index, value);              // persistent entries only
npc.setServerEntityData(accessor, value);

If you change NPC state some other way - writing fields directly, patching the entity NBT, or restoring data from your own storage - tell Easy NPC about it, otherwise the registry copy stays on the last known state while the NPC is unloaded:

npc.getEasyNPCStatusData().markNPCDataUpdated();     // include it in the next save

NPCEntityManager.saveNPC(npc) writes the entry out right away, but like the data interfaces above it is not part of the api package.

The live entity itself is never affected by this - it is saved with its chunk like any other entity.

Related Pages