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
- API-Variant-System - variant enums, textures, and sync
- API-Custom-Models - custom model layers and renderers
- Progression - the progression API and XP formula