Displays and NPCs
Solid objects and player-shaped bodies, sent as packets and animated by the client.
Three modules with the same shape: none of them creates a real entity, all three are drawn straight to a client, and all three own the lifetime of what they show.
| Module | Entry point | What it draws |
|---|---|---|
| Displays | Displays.of(plugin) | Item, block, head and text displays that move, spin and fall. |
| NPCs | Npcs.of(plugin) | Player-shaped bodies: a corpse, a statue, a double. |
| Ragdolls | Ragdolls.of(plugin) | A body cut into its six parts, thrown, opened out or carried off. |
All three need PacketEvents. Without it the module says so once and draws nothing.
All three are reachable from a sequence line, which is how every cosmetic plugin in the ecosystem uses them. The API is for what configuration cannot describe.
Why the client animates it
A display is told a pose and how long it has to get there, and draws every frame in between at the viewer's own frame rate. A two-second animation is about six packets per viewer, and it stays smooth on a server running at fifteen ticks a second, because the smoothness never depended on the tick rate.
Moving something by re-sending its position every tick is a twenty-frames-a-second animation that gets worse under load — which is what a particle trail pretending to be an object looks like.
Displays give an effect weight, silhouette and shadow. Particles give it light, smoke and atmosphere. Effects that look expensive are both.
Drawing a display from a sequence
Any shape line becomes a display line by naming what it is drawn with:
effects:
# Twelve swords that fall out of the sky in a ring, spinning as they come.
- '[CIRCLE] NETHERITE_SWORD;as:item;radius:2.6;points:12;from:0,9,0;to:0,0,0;spin:2;axis:x;life:0.9;face_out:true;light:15'
# The floor buckling: a ring of blocks, flattened, growing outwards.
- '[CIRCLE] CRYING_OBSIDIAN;as:block;radius:0.3;points:20;size:0.1;size_to:1.4;to:0,0.2,0;life:0.5;light:15'
# One item, thrown up, tumbling, falling back down.
- '[DISPLAY] TRIDENT;from:0,0.5,0;to:0,4,0;gravity:14;spin:3;axis:z;life:1.4;size:1.5'How it moves
| Parameter | What it does | Default |
|---|---|---|
life: | Seconds it exists. | 1 |
from:x,y,z | Where it starts, relative to its point. | 0,0,0 |
to:x,y,z | Where it ends. | 0,0,0 |
rise: | Shorthand for to:0,n,0. | |
gravity: | Blocks per second squared, added to the line. | 0; vanilla is about 32 |
ease: | in, out, in_out. | linear |
spin: | Turns over its whole life; x,y,z tumbles on three axes. | 0 |
axis: | Which axis a single-number spin turns around. | y |
orbit: | Turns each point carries round the anchor. | 0 |
vary: | How much the pieces differ in size, as a fraction. | 0 |
size: | One number, or x,y,z for a plate, a pillar or a blade. | 1 |
size_to: | The size it ends at. | same as size |
tilt: roll: turn: | A fixed rotation, in degrees. | 0 |
face_out: | Each point faces away from the centre. | false |
pull: | How far each point travels towards the centre; negative throws it outwards. | 0 |
glow: | Outline colour: a name, #rrggbb or a {palette} token. | none |
light: | Fixed light level, 0 to 15. | the light where it stands |
model: | Custom model data. | none |
billboard: | FIXED, VERTICAL, HORIZONTAL, CENTER. | FIXED, CENTER for text |
hold: | Item display context: 0 the model, 5 head, 7 dropped, 8 item frame. | 0 |
orbit: is the movement a straight line cannot express and the one that separates a shape that
appears from a shape that is alive. vary:0.4 gives the pieces sizes that differ by up to two fifths
— a hash of each point's position, not a random number, so it is the same every play. spin:0.5,2,1
tumbles on three axes, because nothing thrown in the world spins about exactly one.
Making a movement land
ease:in for a slam
It holds the movement back and spends it late: a wind-up and a strike, from the same two numbers.
gravity: for debris only
It is added to the line rather than replacing it, so from:0,9,0;to:0,1,0;gravity:40 descends
eight blocks and then falls twelve more, through the floor. Half of gravity times the life
squared is how far it drops: a second in the air wants about 5.
size: for weight
size:3,0.15,3 is a block flattened into a plate. Grown from nothing it is a shockwave;
stretched the other way it is a pillar or a blade.
size_to: for arrival
A few percent larger on impact, or smaller on a wind-up. It is the difference between an object arriving and an object being placed.
An item model is a flat plate standing in its own XY plane, facing south, with a sword's tip up and
to the right. roll:135 puts it tip-down, roll:315 tip-up, roll:225 point-first along its face.
The turn runs the opposite way from the right-hand rule, so a blade that comes out horizontal wants
90 added or taken. face_out: and turn: are applied after roll: and tilt:, which is the
order a ring needs. A block display is a cube and needs none of it.
light:15 is worth setting on nearly every effect: a display lit by the world is black at night, and
an effect that disappears after sunset is an effect players report as broken.
The display API
PluginDisplays displays = Displays.of(this);
DisplayModel blade = DisplayModel.item(new ItemStack(Material.NETHERITE_SWORD))
.glow(0xFF6B9D)
.light(15);
DisplayMotion thrown = DisplayMotion.builder()
.life(1200)
.from(0, 7, 0).to(0, 0, 0)
.spin(Rotation.Axis.Z, 3)
.build();
displays.show(blade, thrown, where, observers);| Method | What it does |
|---|---|
show(model, motion, at, viewers) | Shows one display; it removes itself when the motion ends. Returns a DisplayHandle, or null when nobody can see it or PacketEvents is absent. |
removeAll() | Everything this plugin is showing. |
active() | How many it has on screen. |
isSupported() | Whether displays can be shown at all here. |
NPCs
effects:
# The victim, face down, wearing what they died in, for four seconds.
- '[NPC] {victim};pose:lying;life:4;equip:true'
# Standing, turned to face whoever did it, outlined.
- '[NPC] {victim};pose:standing;life:3;glow:{error};face:true'| Parameter | What it does | Default |
|---|---|---|
pose: | lying, standing, crawling, sneaking or spinning. | lying |
life: | Seconds it stays, from 0.2 to 120. | 5 |
equip: | Wears the armour and weapon they died in. | true |
glow: | Outline colour. | none |
y: | Height above the anchor. | 0 |
face: | Turns to face whoever set the sequence off. | true |
from:x,y,z / to:x,y,z | Where it appears and where it ends up. | 0,0,0 |
over: | Seconds the movement takes. | 0.7 |
ease: | out, in, in_out or linear. | out |
gravity: | Blocks per second squared, added to the line. | 0 |
turn: | Degrees it turns on the spot over the movement. | 0 |
pose_to: / after: | A second pose, and how long before it. | none / 0.4 |
hurt: | Flinches red as it appears. | false |
spin: | Degrees a second it keeps turning, for its whole life. | 0 |
bob: / bob_every: | Blocks it rises and falls on a loop, and how long one rise and fall takes. | 0 / 1.6 |
swing: | Seconds between arm swings. | 0 |
scale: | How big it is drawn, 1 being player-sized. | 1 |
hold: / offhand: | A material put in each hand. | its own |
pitch: | Head pitch in degrees, negative being up. | 0 |
spin: is the one to reach for when a body should not settle: turn: is one turn spread over the
movement and then finished, spin: never stops. A body caught in a vortex is spin:220, one hanging
in the light is bob:0.3;spin:40, and one still swinging at whatever killed it is
swing:0.5;hold:NETHERITE_SWORD. scale: is the client's own scale attribute, so the whole model
grows — 0.4 is a doll and 2.5 is something the room is not big enough for; it needs a 1.20.5
client, and an older one sees a player-sized body.
PluginNpcs npcs = Npcs.of(this);
NpcHandle body = npcs.show(NpcModel.of(victim)
.wearing(victim)
.pose(NpcPose.LYING)
.glow(0xA33B53),
victim.getLocation(), 4000, observers);
body.lookAt(killer.getLocation());A body reads better at about half vanilla gravity — a real one is heavier than the eye expects and
slower than the number says. pose:lying is the pose a sleeping player is drawn in, which is the only
way to put a body on the floor without a model of your own; pose:standing with face:true is a
different effect entirely: somebody who has stopped, and is looking at you.
NpcModel.of(player) reads the texture from the connection this server already holds — no lookup, no
waiting, no failure halfway. A name would be a request to Mojang, and an effect cannot wait for one.
A base64 texture written into the file works, and is resolved when the file is read.
It is announced under a UUID of its own, never the wearer's: a second entry under a real player's id takes that player's skin off their own body until they relog. It is also announced unlisted, so it never appears in the tab list beside real players.
Ragdolls
A body that comes apart. The player is cut into its six parts — head, torso, two arms, two legs — each one wearing the colours of their own skin, and then those parts are moved as a single piece of choreography: thrown, opened out, spun, flattened, spelled into letters, or carried off whole.
effects:
# Thrown apart. The pieces leave outwards, fall, bounce and rest.
- '[RAGDOLL] {victim};intact:0.3;life:2.6;speed:4.2;up:6.4;spin:2.2;detail:2;light:15'
# Lifted, opened out, and taken upwards. Nothing is left on the floor.
- '[RAGDOLL] {victim};pose:vortex;rise:3.0;open:0.7;turns:1.8;life:3.0;detail:2;glow:{highlight}'The head takes {victim}, {killer}, or a base64 texture, exactly as [NPC] does. If what died was
not a player the line says so once and draws nothing.
The poses
| Pose | What happens | Also written |
|---|---|---|
burst | Thrown apart: the pieces leave outwards, fall, bounce and rest. The default. | |
spread | Lifted off the ground and opened out, arms and legs wide, turning slowly — then let go. | starfish, open |
knocked | Held open like spread, then struck several times from different sides. | knock, batted, hit |
vortex | Taken: the pieces spiral inwards and upwards and are gone at the top. Nothing is left. | taken, ascend, spiral |
balloon | The head swells far too big, wobbles, and bursts. The body waits underneath it. | bighead, swell, pop |
helicopter | The arms go flat above the head as a rotor, the body counter-turns, and it leaves forwards. | chopper, rotor |
plane | Arms out as wings, nose up, banking as it climbs. A negative climb is a dive. | fly, glide, jet |
flatten | Driven straight down and left flat: a player-shaped stain, not a pile of blocks. | pancake, squash, flat |
melt | Sinks. The pieces lose their height where they stand and are gone. No throw, no bounce. | sink, dissolve |
sign | The pieces lay themselves out into letters and hold there, then let go and fall. | letters, spell, word |
thrown | Sent somewhere in one piece, turning end over end. With no gravity it never comes back. | launched, ejected, carried |
Its parameters
| Parameter | What it does | Default |
|---|---|---|
pose: | Which of the eleven above. | burst |
life: | Seconds the pieces last. | 2.2 |
intact: | Seconds the body stands whole before anything happens to it. | 0.3 |
detail: | Cells each part is cut into on each axis, 1 to 4. 1 is one colour per limb. | 1 |
size: | How big it is; 1 is player-sized. | 1 |
light: | Light level, 0 to 15. | the world's own |
glow: | Outline colour — a name, #rrggbb or a {palette} token. | none |
fade: | Shrinks away at the end instead of vanishing. | false |
settle: | Stops turning once it lands. | true |
y: | Height above the anchor. | 0 |
face: | Turns to face whoever set the sequence off. | true |
speed: up: | How fast the pieces leave, outwards and upwards. | 3.2 / 6.5 |
spread: | How much the pieces differ from each other, 0 to 1. | 0.45 |
gravity: bounce: | Blocks per second squared, and the speed kept on landing. | 26 / 0.32 |
spin: | Turns a second. | 1.8 |
rise: open: | How far off the ground a held body hangs, and how far its arms and legs open out. | 1.1 / 0.55 |
lift: hang: turns: | Seconds the lift takes, seconds it hangs there, and turns it makes while it hangs. | 0.45 / 0.9 / 0.35 |
hits: every: force: | For knocked: how many blows, seconds between them, and how far one shoves it in blocks. | 3 / 0.32 / 0.85 |
swell: | For balloon: how many times its own size the head reaches. | 3 |
squash: | For flatten: what is left of a piece's height. | 0.14 |
sign: letters: | For sign: what the body spells, and how tall one letter is in blocks. | EZ / 2.4 |
dir: | Which way it is thrown or flies, in degrees. 0 is east, 90 is south. | the anchor's own facing |
PluginRagdolls ragdolls = Ragdolls.of(this);
ragdolls.show(RagdollModel.of(victim).detail(2).light(15),
RagdollMotion.builder().pose(RagdollPose.VORTEX).rise(3.0).build(),
victim.getLocation(), observers);A part is drawn as a detail × detail grid, so six parts cost 6 × detail² displays: detail:1 is
six, detail:2 is twenty-four, and detail:4 is ninety-six. Two reads as a body at any distance
somebody actually watches one from; four is for a body that hangs still in front of the camera.
Every one of these is solved in advance and handed to the client as poses, so what separates them is what the file wanted to say, never what the server could afford. Nothing here is simulated, and nothing here is ticked.
Real skins: mineskin-key
Out of the box a ragdoll's head wears the player's real face, but the rest of the body is drawn in
blocks: each piece takes the block nearest in colour to that part of the skin. A dark outfit therefore
reads as a black body made of blocks. To have the whole body wear the real skin, ExyliaLib needs a
MineSkin API key, set in ExyliaLib's own plugins/ExyliaLib/config.yml:
mineskin-key: 'your-key'
ragdoll-skin-quality: normal| Key | What it does | Default |
|---|---|---|
mineskin-key | A free key from account.mineskin.org/keys. Empty keeps every body in blocks. | empty |
ragdoll-skin-quality | How finely a body in its real skin is cut, and so how many uploads a new skin costs. | normal |
| Quality | Uploads per new skin | What it looks like | New skins an hour on the free plan |
|---|---|---|---|
high | 18 | 4-pixel cubes; a breaking body comes apart in 19 pieces. | about 5 |
normal | 10 | Every skin pixel kept exactly; 11 larger pieces. | about 10 |
low | 5 | One head per part; a third of the rows lost. | about 20 |
- Made once per skin. A skin is prepared when its owner joins, uploaded in the background, and
kept in the database of the plugin that shows the bodies (its
database.yml, tableexylia_ragdoll_skins). That server, or a network sharing that database, never uploads it again. - The free plan's limits. MineSkin's free plan allows 20 uploads a minute and 100 an hour. Past the hour, uploads wait for it to reset and the console says when they start again.
- Never a wait. A piece whose texture has not arrived yet is drawn in blocks until it does.
- Applied live.
/exylialib reloadreads both keys, and a changed key or quality prepares the skins of everybody online again.
What all three guarantee
| Nothing the server carries | Not entities: not ticked, not saved, not in any chunk, no hitbox. Two players standing together can be shown different ones. |
| Nothing can be left behind | Nothing else would clean them up, so the module owns their lives: they go when the motion or life ends, when the plugin is disabled, or when the server stops. There is no fourth case. |
| One timer, not one per object | Every display on the server is moved from a single asynchronous driver. |
| A spin is cut up for you | The client turns by the shortest arc, so spins are split into sixths of a turn; spin:3 is correct without anybody knowing that. |
| Capped work | A file asking for two hundred turns is capped at forty-eight poses rather than sending two hundred packets. |
| PacketEvents or nothing | There is no fallback worth pretending about. |
The ceiling on what displays may cost
plugins/ExyliaLib/displays.yml holds two numbers for the whole server, because what a client can be
sent is a fact about the server rather than about the plugin that happened to send it.
max-viewer-displays: 20000
max-per-effect: 128| Key | What it caps |
|---|---|
max-viewer-displays | Display-viewer pairs at once: one display seen by thirty players counts as thirty. Effects that would go over are dropped before their packets are built, so a crowded arena loses the tail of an effect instead of its tick rate. The console says so at most once a minute. 0 removes the ceiling. |
max-per-effect | How many displays one shape may draw — a points:900 written by hand is caught at load rather than on the wire. |
The budget counts viewers because that is the number that reaches the network: two hundred displays in
an empty corner are nothing, and the same two hundred with thirty players around them are six thousand
entities on clients that also have a fight to render. /exylialib reload picks up an edit; no restart.
Region flags
WorldGuard flags the library registers, so a region can refuse effects without any plugin depending on WorldGuard:
| Flag | Denying it stops |
|---|---|
kill-effects | Kill effects playing where somebody died inside the region. |
hit-effects | Hit effects playing on a blow landed inside the region. |
arrows-effects | Arrow launch, trail and impact effects inside the region. |
They are registered from the library's own onLoad, because WorldGuard locks its registry the instant
it enables. On a server without WorldGuard every flag allows, and an unregistered flag, a missing world
or a refused registry all answer the same way: a gate that cannot be asked never withholds anything.
Something missing on this page? Tell us on Discord