API - MarkusBordihn/BOs-Easy-NPC GitHub Wiki
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.
| 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 |
| Add, remove, or list NPC objectives | api.handler.EasyNPCObjectiveHandler |
| Create a preset item from an NPC | api.handler.EasyNPCPresetHandler |
| Seat an NPC on a player's shoulder | api.handler.EasyNPCShoulderHandler |
| 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
|
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.
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);
EasyNPCEntityHandler.revive(uuid, serverLevel);
EasyNPCEntityHandler.revive(uuid, serverLevel, position);
EasyNPCEntityHandler.respawn(easyNPC, serverLevel);Lookups are also available by entity type and by custom identifier.
Since 7.15.0, spawn refuses an NPC that was removed with the reason KILLED. Pass force = true
to spawn it with its saved state, or use revive: it brings the NPC back with full health and
without its death, hurt, or fire state. revive refuses NPCs that were deleted, sit on a player's
shoulder, or are already loaded. getSpawnRejection(uuid, force) and getReviveRejection(uuid)
return the reason why a call would fail, or an empty Optional. respawn replaces a loaded NPC
with a fresh entity built from its current data, for example to restart its goals.
Remove a loaded NPC with EasyNPCEntityHandler.despawn(easyNPC, reason) instead of discard(). It
saves the NPC first; a plain discard() loses its latest changes.
EasyNPCPauseHandler pauses and resumes single NPCs or all of them. isPaused also works on the
client for a single NPC; the global pause is only known on the server.
EasyNPCObjectiveHandler adds, removes, and lists the objectives of a loaded NPC on the server:
EasyNPCObjectiveHandler.addObjective(easyNPC, ObjectiveType.FOLLOW_OWNER);
EasyNPCObjectiveHandler.addObjective(easyNPC, new ObjectiveDataEntry(ObjectiveType.OWNER_HURT_TARGET));
EasyNPCObjectiveHandler.hasObjective(easyNPC, ObjectiveType.FOLLOW_OWNER);
EasyNPCObjectiveHandler.removeObjective(easyNPC, ObjectiveType.FOLLOW_OWNER);
EasyNPCObjectiveHandler.setPriority(easyNPC, ObjectiveType.FOLLOW_OWNER.name(), 4);
ObjectiveDataEntry followOwner = EasyNPCObjectiveHandler.getObjective(easyNPC, "FOLLOW_OWNER");
Set<ObjectiveDataEntry> objectives = EasyNPCObjectiveHandler.getObjectives(easyNPC);addObjective returns true once the objective is stored, even if its goal is only created later,
for example when the owner is not online yet. Removing an objective that does not exist returns
false. All methods return false or an empty set for client-side NPC instances. Adding an
objective with the same id again replaces the stored one and its running goal.
setPriority and getObjective take the objective id, which is the type name for built-in types.
setPriority moves the running goal to the new priority and returns false for an unknown id.
Built-in objective types use their default priority unless one is passed explicitly, for example
with new ObjectiveDataEntry(ObjectiveType.FOLLOW_OWNER, 4). An explicit priority is saved with the
NPC and kept in presets. Editing the objective in the configuration screens resets it to the
default. CUSTOM objectives without a stored priority use the default of their factory.
OWNER_HURT_TARGET ("Assist Owner",
assist_owner in commands) attacks whatever the owner attacks. Like OWNER_HURT_BY_TARGET, it
skips team members, pets and NPCs of the same owner, and players the owner may not hurt.
EasyNPCPresetHandler.createPresetItem(easyNPC) returns the same preset item the empty preset item
creates, or ItemStack.EMPTY on the client. Like the item, it leaves out the inventory contents.
EasyNPCShoulderHandler seats a loaded NPC on a player's shoulder and drops it off again on the
server:
EasyNPCShoulderHandler.sitOnShoulder(easyNPC, player);
EasyNPCShoulderHandler.dismount(player);
EasyNPCShoulderHandler.dismount(player, ShoulderSide.LEFT);
EasyNPCShoulderHandler.dismount(player, ShoulderSide.LEFT, remountCooldownTicks);
Optional<UUID> seatedNPC = EasyNPCShoulderHandler.getShoulderNPCUUID(player, ShoulderSide.RIGHT);
boolean seated = EasyNPCShoulderHandler.isOnShoulder(npcUUID);The NPC is stored in the player data like a parrot, so getShoulderNPCData returns its saved data
instead of an entity. sitOnShoulder returns false when both shoulders are taken or vanilla
refuses the seat, for example while the player is in the air or in water. Unlike the "Sit on
Shoulder" ability, it does not check owner or size. dismount only drops Easy NPC entities and
leaves a parrot on the other shoulder in place.
dismount places the NPC above the player, or at the player's position when that space is blocked.
If both are blocked or the NPC cannot be added to the level, it stays on the shoulder and dismount
returns false. With remountCooldownTicks, the "Sit on Shoulder" ability waits that many ticks
before the NPC jumps back on, for example after the owner dismounts it while sneaking. The cooldown
is saved with the NPC and survives a chunk reload.
A seated NPC is marked with the removal reason ON_SHOULDER. EasyNPCEntityHandler.spawn and
revive refuse it, so the NPC cannot exist twice.
The SIT_ON_OWNER_SHOULDER objective reads these settings from its ObjectiveDataEntry:
| Setting | Default | Effect |
|---|---|---|
setStartDistance |
0 |
Maximum distance to the owner, 0 uses 16 blocks |
setCooldownTicks |
100 |
Wait time after joining, dismounting, or giving up |
setOnlyOutOfCombat |
false |
Waits while the owner recently hurt something or was hurt |
The NPC gives up when the owner moves out of range or when it gets no closer for 100 ticks, and retries after the cooldown.
Override registerEasyNPCDefaultData() in your NPC class to give new NPCs of that type their own
objectives, attributes, or other defaults. Call the Easy NPC defaults first:
@Override
public void registerEasyNPCDefaultData() {
EasyNPC.super.registerEasyNPCDefaultData();
this.getEasyNPCObjectiveData().addOrUpdateCustomObjective(
new ObjectiveDataEntry(ObjectiveType.SIT_ON_OWNER_SHOULDER));
}It runs for every new NPC, whether it comes from a spawn egg, a spawner, a preset item, or a preset
import. Chunk loading, EasyNPCEntityHandler.spawn, revive, and dismount restore the saved data
of the NPC, so a default that was removed later does not come back. For logic on every join, use
EntityLifecycleListener.onNPCSpawned.
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.
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);
}
});Owner and objective changes have their own listeners:
EasyNPCEventRegistry.registerOwnerEventListener(
(easyNPC, previousOwnerUUID, ownerUUID) -> MyMod.ownerChanged(easyNPC, ownerUUID));
EasyNPCEventRegistry.registerObjectiveEventListener(
new ObjectiveEventListener() {
@Override
public void onObjectiveAdded(EasyNPC<?> easyNPC, ObjectiveDataEntry objectiveDataEntry) {}
@Override
public void onObjectiveRemoved(EasyNPC<?> easyNPC, ObjectiveDataEntry objectiveDataEntry) {}
});Both fire only on the server and only for real changes. Loading an NPC from disk fires no owner
event. An owner set before the first spawn of an NPC fires one owner event with a null previous
owner when the NPC spawns, right after the spawn event. Objective events
fire for every objective added or removed at runtime, including the standard objectives of a newly
spawned NPC and objectives that follow attribute changes. Replacing the whole objective data, for
example by a preset import, fires none.
Shoulder listeners learn when an NPC sits down and when it is back in the world:
EasyNPCEventRegistry.registerShoulderEventListener(
new ShoulderEventListener() {
@Override
public void onShoulderMounted(EasyNPC<?> easyNPC, Player player, ShoulderSide side) {}
@Override
public void onShoulderDismounted(EasyNPC<?> easyNPC) {}
});onShoulderDismounted fires when the NPC joins the level again, no matter whether damage, a fall,
sleep or dismount dropped it off.
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.
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.
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.
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 saveNPCEntityManager.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.
- API-Variant-System - variant enums, textures, and sync
- API-Custom-Models - custom model layers and renderers
- Progression - the progression API and XP formula