A Paper plugin that lets Citizens NPCs wear HMCCosmetics cosmetics.
HMCCosmetics is built around online players: its user objects are keyed by player UUID, and several of its render paths look an entity up by that UUID before touching it. Citizens NPCs are entities without a player behind them, so equipping a cosmetic on one through HMCCosmetics alone does nothing. HMCCitizens supplies an NPC-backed cosmetic user and the packet-level rendering that the player-only paths would otherwise have done, then persists the selection in a Citizens trait so it survives restarts.
/npc hmccosmetics equip|remove|list— manage cosmetics on the selected NPC, with tab completion for cosmetic IDs, slots, and#RRGGBBcolors.- Cosmetics persist per NPC in Citizens' own NPC storage and are re-applied on spawn.
- Optional dyeing for leather-based cosmetics via a
#RRGGBBargument. - Armor, backpack, and balloon cosmetic types all render on NPCs.
- Backpacks marked first-person-compatible get a dedicated renderer, because HMCCosmetics' own first-person path needs a real player client on the other end.
| Server | Paper 1.21.4+ |
| Java | 21 |
| Citizens | 2.0.43+ |
| HMCCosmetics | 2.9.1 (with HibiscusCommons 0.9.2) |
Both Citizens and HMCCosmetics must be enabled. If either is missing, HMCCitizens logs a warning and stays inactive rather than failing the server start.
./gradlew buildThe jar lands in build/libs/HMCCitizens-0.1.0-SNAPSHOT.jar. Drop it in plugins/.
Select an NPC first (/npc select <id>), then:
/npc hmccosmetics equip <cosmetic-id> [#RRGGBB]
/npc hmccosmetics remove <cosmetic-id|slot|all>
/npc hmccosmetics list
equip replaces whatever occupies that cosmetic's slot. remove accepts a cosmetic ID, an HMCCosmetics slot
name (HELMET, BACKPACK, …), or all.
| Node | Default | Grants |
|---|---|---|
hmccitizens.use |
op | Use /npc hmccosmetics on NPCs you own |
hmccitizens.admin |
op | Use it on any NPC regardless of owner |
HmccosmeticsTrait — a Citizens trait named hmccosmetics, saved into the NPC's data alongside every
other trait. It holds a PersistedCosmeticState (slot → cosmetic ID + optional color) and, while the NPC is
spawned, an NpcCosmeticUser. On spawn it rebuilds the runtime user and re-applies each saved selection;
cosmetics that no longer exist in HMCCosmetics' config are skipped with a warning but kept in storage, so
renaming a cosmetic back restores it. It ticks once per Citizens trait tick to keep the render in sync.
NpcCosmeticUser — a subclass of HMCCosmetics' CosmeticUser whose entity is the NPC and whose
getPlayer() is null. It overrides the paths that assume a player: armor is pushed with explicit
equipment-slot packets to the viewers around the NPC, balloons are teleported and re-leashed manually, and
canEquipCosmetic always returns true since access is already gated by the Citizens command permissions
rather than by player unlock state.
NpcBackpackRenderer / NpcBackpackRotation — a backpack is an invisible armor stand riding the NPC with
the cosmetic item on its head. Because it is a fake entity, its rotation only reaches clients through packets,
and clients do not apply those directly: they treat the value as a lerp target and close a third of the gap
per tick. Sending the plain body yaw therefore yields a backpack that matches while the NPC stands still and
visibly trails the torso through a turn.
The renderer solves that the way HMCCosmetics does for players:
- The NPC's body yaw is simulated, not read off the entity — the body yaw a viewer sees is one their own client computed, and the server copy is at best a tick behind. The simulation reproduces vanilla's rules: ease toward the direction of travel while moving, drag the body once the head exceeds a 50° offset, and flip around when movement is backwards.
- A per-renderer mirror of where clients are rendering the backpack lets it send an overshooting yaw that lands on the true body yaw after the lerp completes, instead of the body yaw itself. The overshoot is quantised to the byte the packet actually carries before advancing the mirror, so the mirror cannot drift away from the client over a long turn.
- Teleports and world changes reset the simulation rather than being read as travel, which would otherwise spin the body toward the destination.
- Yaw, rotation, and head-rotation packets all carry the same value. They each retarget the client's lerp, so disagreeing values leave the backpack oscillating. The head packet is required: a client renders a passenger's head at the passenger's own head yaw, clamped to an offset from the vehicle's body yaw, so without it the backpack holds still and then snaps once the clamp is hit.
- Updates are skipped when the NPC has neither moved nor turned beyond the packets' own quantisation, and newly-arriving viewers get a spawn, equipment, and mount bundle before the shared rotation packets.
./gradlew testThe suite covers the rotation arithmetic (body yaw simulation, head clamp, backwards movement, overshoot, mirror advance, byte quantisation, and the 360° wrap), command-completion filtering, and the persisted state model. Rendering itself is verified in-game — packet behavior against a live client is not something the unit tests can stand in for.
imonlyzuzu