CitizensManager

CitizensManager is the heart of the HyCitizens API. It manages the full lifecycle of every citizen — spawning, saving, updating, removing — and exposes runtime controls for animations, groups, movement, skins, and events.

package com.electro.hycitizens.managers;

// Access from anywhere in your plugin:
CitizensManager manager = HyCitizensPlugin.get().getCitizensManager();
💡

The save parameter

Most methods accept a boolean save parameter. Pass true to write the citizen to disk so it persists through reloads. Pass false if you're making multiple changes and want to call saveCitizen(citizen) yourself once at the end to avoid unnecessary disk I/O.

Creating & Registering Citizens

addCitizen

void addCitizen(@Nonnull CitizenData citizen, boolean save)

Registers a new citizen, optionally saves it to disk, and immediately spawns its NPC in the world. This is the primary method for creating a citizen from code. The citizen must have its worldUUID, position, rotation, and modelId set before calling this.

CitizenData citizen = new CitizenData(
        "npc_1_id", // ID
        "Example Citizen", // Name
        "Player", // Model ID
        worldUuid, // World UUID
        new Vector3d(0, 70, 0), // Position
        new Vector3f(0, 0, 0), // Rotation
        1.0f, // Scale
        null, // NPC UUID - You should usually set this as null
        new ArrayList<>(), // Hologram UUIDs - You should usually leave this empty
        "", // Required Permission
        "", // No Permission Message - Leaving it empty sets a default message
        List.of(), // Command actions
        true, // Is Player Model
        false, // Use Live Skin
        "Simon", // Skin Username
        null, // Cached Skin - Usually set to null
        0L, // Last Skin Update - Usually set to 0L
        true // Rotate towards players
);

manager.addCitizen(citizen, true); // true means it will save to storage and will respawn every time the world loads.

saveCitizen

void saveCitizen(@Nonnull CitizenData citizen)
void saveCitizen(@Nonnull CitizenData citizen, boolean respawnIfRoleChanged)

Writes a citizen's current state to disk. Call this after modifying a CitizenData object's fields directly if you want the changes to survive a reload. The respawnIfRoleChanged overload will trigger a full NPC respawn when the role file on disk changes. This can cause problems when used incorrectly, so it's often best to use updateCitizen() instead of respawnIfRoleChanged

Querying Citizens

getCitizen

@Nullable CitizenData getCitizen(@Nonnull String citizenId)

Returns the citizen with the given internal ID, or null if it doesn't exist. The ID is the key used in the config file and returned by citizen.getId().

getAllCitizens

@Nonnull List<CitizenData> getAllCitizens()

Returns a snapshot of every registered citizen. Safe to iterate — modifying this list has no effect on the manager.

for (CitizenData c : manager.getAllCitizens()) {
    System.out.println(c.getName() + " is at " + c.getPosition());
}

getCitizenCount

int getCitizenCount()

Returns the total number of registered citizens.

citizenExists

boolean citizenExists(@Nonnull String citizenId)

Returns true if a citizen with that ID is currently registered.

getCitizensNear

@Nonnull List<CitizenData> getCitizensNear(@Nonnull Vector3d position, double maxDistance)

Returns all citizens whose spawn position is within maxDistance blocks of the given coordinate. Note: this uses the citizen's configured position, not their live location (which may differ for wandering and patrolling citizens).

// Find all citizens within 20 blocks of a player
Vector3d playerPos = playerRef.getTransform().getPosition();
List<CitizenData> nearby = manager.getCitizensNear(playerPos, 20.0);

Updating Citizens

After modifying a CitizenData object, call one of these to push those changes to the live NPC in the world. Choosing the right update method avoids unnecessarily despawning and respawning the entire entity.

MethodWhen to use it
updateCitizen(citizen, save) Full respawn of both the NPC entity and hologram. Use when changing model, skin, or attitude.
updateCitizenNPC(citizen, save) Respawns only the NPC entity. Use when changing combat, detection, or path config.
updateCitizenHologram(citizen, save) Updates only the floating name tag. Use when changing the citizen's name or nametag offset.
updateCitizenNPCItems(citizen) Pushes item changes (armor, held items) to the live entity without a respawn.
// Rename a citizen and update only the nametag (no NPC respawn needed)
CitizenData citizen = manager.getCitizen(citizenId);
if (citizen != null) {
    citizen.setName("Example name");
    manager.updateCitizenHologram(citizen, true);
}

// Equip a sword and update items live
citizen.setNpcHand("Weapon_Sword_Iron");
manager.updateCitizenNPCItems(citizen);
manager.saveCitizen(citizen);

One-Call Config Updates

These convenience methods update a specific config subsystem on a citizen, save it, and respawn the NPC — all in a single call. They're the easiest way to change one aspect of a citizen's behavior from code.

setCitizenCombatConfig

void setCitizenCombatConfig(@Nonnull String citizenId, @Nonnull CombatConfig combatConfig)

setCitizenDetectionConfig

void setCitizenDetectionConfig(@Nonnull String citizenId, @Nonnull DetectionConfig detectionConfig)

setCitizenPathConfig

void setCitizenPathConfig(@Nonnull String citizenId, @Nonnull PathConfig pathConfig)
// Make a guard much more perceptive after nightfall
DetectionConfig nightConfig = new DetectionConfig(true);
nightConfig.setViewRange(30f);
nightConfig.setHearingRange(20f);
nightConfig.setAlertedRange(60f);
manager.setCitizenDetectionConfig(guard.getId(), nightConfig);

Removing Citizens

removeCitizen

void removeCitizen(@Nonnull String citizenId)

Permanently deletes a citizen — despawns the NPC and hologram, removes them from disk, and deletes their role file. This cannot be undone. If you just want to hide an NPC temporarily, use despawnCitizen instead.

manager.removeCitizen(citizen.getId());

Group Management

Groups let you organize related citizens together and query or operate on them as a batch. A citizen belongs to at most one group, stored as a string name on CitizenData.

getAllGroups

@Nonnull List<String> getAllGroups()

Returns all registered group names, sorted alphabetically.

getCitizensByGroup

@Nonnull List<CitizenData> getCitizensByGroup(@Nullable String groupName)

Returns all citizens in the given group. Pass null or "" to get all ungrouped citizens.

createGroup

void createGroup(@Nonnull String groupName)

Registers a new group name. Citizens can be assigned to it via citizen.setGroup(groupName).

deleteGroup

void deleteGroup(@Nonnull String groupName)

Deletes the group and clears the group assignment from every citizen that was in it. Citizens are not removed — they just become ungrouped.

groupExists

boolean groupExists(@Nonnull String groupName)
// Assign a citizen to a group when creating them
manager.createGroup("Village Guards");
citizen.setGroup("Village Guards");
manager.addCitizen(citizen, true);

// Later: despawn all village guards (e.g. after a raid event ends)
for (CitizenData guard : manager.getCitizensByGroup("Village Guards")) {
    manager.despawnCitizen(guard);
}

// Re-spawn them when the event resets
for (CitizenData guard : manager.getCitizensByGroup("Village Guards")) {
    manager.spawnCitizen(guard, false);
}

Playing Animations

Hytale doesn't have many animations built-in. It's recommended to install a third-party animation mod like "Emotale" or "Emotes" from CurseForge.

playAnimationForCitizen

void playAnimationForCitizen(@Nonnull CitizenData citizen, @Nonnull String animName, int slot)
void playAnimationForCitizen(@Nonnull CitizenData citizen, @Nonnull String animName, int slot, @Nullable AnimationBehavior behavior)

Plays an animation immediately on a citizen's live NPC. The slot is the animation layer — 0 is Movement, 1 is Status, 2 is Action, 3 is Face, 4 is Emote. In most cases, Action (2) should be used. Passing null for behavior uses a default 3-second stop timer. Pass an AnimationBehavior to use its configured stop settings.

triggerAnimations

void triggerAnimations(@Nonnull CitizenData citizen, @Nonnull String type)

Fires all animation behaviors of the given type. This is the same thing that happens internally on events — for example, "ON_INTERACT" is triggered automatically when a player interacts. You can call this manually for custom events.

Valid types: "DEFAULT", "ON_INTERACT", "ON_ATTACK", "ON_PROXIMITY_ENTER", "ON_PROXIMITY_EXIT", "TIMED".

// Play a wave emote immediately on slot 2 (Action)
manager.playAnimationForCitizen(citizen, "Wave", 2);

// Trigger all ON_INTERACT animations (e.g. when your plugin detects a custom event)
manager.triggerAnimations(citizen, "ON_INTERACT");

Movement & Patrol Control

These methods let you control a citizen's movement at runtime without reloading or respawning. See the dedicated Moving Citizens guide for full examples.

moveCitizenToPosition

void moveCitizenToPosition(@Nonnull String citizenId, @Nonnull Vector3d position)

Commands a citizen to walk to a specific position. Uses an invisible waypoint entity that the NPC pathfinds toward.

stopCitizenMovement

void stopCitizenMovement(@Nonnull String citizenId)

Cancels any active movement command. Does not stop an active patrol — use stopCitizenPatrol for that.

startCitizenPatrol

void startCitizenPatrol(@Nonnull String citizenId, @Nonnull String pathName)

Starts a citizen patrolling along a named patrol path. The path must be registered first (via the UI or patrolManager.savePath()).

stopCitizenPatrol

void stopCitizenPatrol(@Nonnull String citizenId)

Stops an active patrol. The citizen will stop at their current position.

isCitizenPatrolling

boolean isCitizenPatrolling(@Nonnull String citizenId)

getCitizenActivePatrolPath

@Nullable String getCitizenActivePatrolPath(@Nonnull String citizenId)

Returns the name of the patrol path a citizen is currently following, or null if they're not patrolling.

Combat Control

forceAttackEntity

void forceAttackEntity(@Nonnull CitizenData citizen, @Nonnull String attackInteractionId)

Forces a hostile citizen to use a specific attack interaction, overriding their configured default. Useful for triggering special attacks from scripted boss events. Clears any previous attack overrides before setting the new one.

autoResolveAttackType

void autoResolveAttackType(@Nonnull CitizenData citizen)

Sets the citizen's attackType to the appropriate value for their model automatically. Call this after changing a citizen's modelId if you don't want to manually determine the correct attack animation name.

applyHealthOverride

void applyHealthOverride(@Nonnull Ref<EntityStore> entityRef, @Nonnull CitizenData citizen)

Applies the citizen's overrideHealth / healthAmount settings to the live NPC entity. This is called automatically at spawn, but you can call it manually if you change health settings after the citizen is already spawned.

Skin Utilities

updateCitizenSkin

void updateCitizenSkin(CitizenData citizen, boolean save)

Fetches and applies the latest skin for a citizen with a skinUsername set. If useLiveSkin is true, it makes a fresh network request to Mojang's API. If false and a cached skin already exists, it applies the cached version without a request.

updateCitizenSkinFromPlayer

void updateCitizenSkinFromPlayer(CitizenData citizen, PlayerRef playerRef, boolean save)

Copies a player's current skin and applies it to the citizen. Clears skinUsername and disables live skin updates.

// Mirror a player's skin onto a citizen (e.g. for a player-look-alike NPC)
manager.updateCitizenSkinFromPlayer(citizen, playerRef, true);

Miscellaneous

reload

void reload()

Reloads all citizen configs from disk and re-registers all citizens. This is the same as running /citizens reload in-game.

getPatrolManager

@Nonnull PatrolManager getPatrolManager()

Returns the underlying PatrolManager for advanced operations like adding waypoints to a path programmatically. See the Moving Citizens guide for details.

Full Example: A Dynamic Quest NPC

CitizensManager manager = HyCitizensPlugin.get().getCitizensManager();

// 1. Build the citizen
CitizenData questGiverm = new CitizenData(
        "quest_giver_npc", // ID
        "Elder Soryn", // Name
        "Player", // Model ID
        worldUuid, // World UUID
        new Vector3d(0, 70, 0), // Position
        new Vector3f(0, 0, 0), // Rotation
        1.0f, // Scale
        null, // NPC UUID
        new ArrayList<>(), // Hologram UUIDs
        "", // Required Permission
        "", // No Permission Message
        List.of(), // Command actions
        true, // Is Player Model
        false, // Use Live Skin
        "Simon", // Skin Username
        null, // Cached Skin
        0L, // Last Skin Update
        true // Rotate towards players
);

questGiver.setGroup("quest-givers");

// 2. Sequential dialogue on F key
MessagesConfig messages = new MessagesConfig();
messages.setSelectionMode("SEQUENTIAL");
messages.setMessages(List.of(
    new CitizenMessage("{WHITE}The forest grows dark...", "F_KEY", 0.0f),
    new CitizenMessage("{WHITE}Strange creatures lurk near the old mill.", "F_KEY", 0.5f),
    new CitizenMessage("{YELLOW}Will you investigate for me?", "F_KEY", 0.5f)
));
questGiver.setMessagesConfig(messages);

// 3. Give the player a quest item via server command after they accept
questGiver.setCommandActions(List.of(
    new CommandAction("questitem give {PlayerName} Deco_Scroll", true, 1.5f, "F_KEY")
));

// 4. Register and spawn
manager.addCitizen(questGiver, true);

// 5. Cancel the interaction if the player already has the quest
manager.addCitizenInteractListener(event -> {
    if (!event.getCitizen().getId().equals(questGiver.getId())) return;
    
    if (playerHasQuest(event.getPlayerRef())) {
        event.setCancelled(true); // No messages or commands fire
    }
});