Entities

What a Strut entity is, what it does not need to declare, and how its files are found.

The class

Any entity can be a Strut entity: implement StrutEntity.

public class HeroEntity extends PathAwareEntity implements StrutEntity {
	public HeroEntity(EntityType<? extends HeroEntity> type, World world) {
		super(type, world);
	}
}

Everything StrutEntity asks of you has a default:

Was requiredNow
A field holding the animation cache, and its getterHeld in a slot Strut adds to every entity, created on first use. Declare your own and yours wins.
getTickThe entity's age in ticks. Override it for a non-ticking entity.
registerControllersOptional. When the server plays an animation on a controller name that has none, one is created on the client. Register controllers only to tune one (transition length, triggerable animations) or to animate on the client.
A model class, a renderer class, and a registration callOne line: StrutClient.entity(type).

Registering the renderer

StrutClient is Strut's client entry point. Call these from your own client initialiser, once per entity type.

StrutClient.entity(AVA);

This uses the convention below. StrutClient.entity has three forms:

CallUse it when
entity(type)The files follow the convention.
entity(type, model)You need to change which model, texture or animation file is used.
entity(type, factory)You want a renderer of your own: a subclass of StrutEntityRenderer, or one with withScale and render layers.

The convention

An entity type with the registry id namespace:path is rendered from:

FileLocation
Modelassets/namespace/strut/models/entity/path.strutmodel (or assets/namespace/strut/models/entity/path.strutmodel: both folders are read, and a model asks for the id of the file's full path)
Textureassets/namespace/textures/entity/path.png
Animationsassets/namespace/strut/animations/entity/path.strutanim (or strut/animations/entity/path.strutanim; a data pack's server timelines are read from the same two folders under data/namespace/)

A nested id nests the files: mymod:creatures/ava reads strut/models/entity/creatures/ava.strutmodel.

Changing what differs

Start from the conventional model and replace one part. withAltModel, withAltTexture and withAltAnimations take an id and apply the same folder convention to it, so several entities can share a texture:

StrutClient.entity(NIA, StrutModel.<HeroEntity>entity(Identifier.of("mymod", "nia")).withAltTexture(shared));

When the files do not follow the convention at all, build the model from ids, or from functions of the entity so the model can change with its state:

return StrutModel.<HeroEntity>builder()
		.model(creature -> Identifier.of("mymod", "strut/models/forested/" + creature.getName().getString() + ".strutmodel"))
		.texture(creature -> Identifier.of("mymod", "textures/entities/" + creature.getName().getString() + ".png"))
		.animation(Identifier.of("mymod", "strut/animations/creature.strutanim"))
		.build();

The builder refuses to build() unless the model, texture and animation are all set, and names the one that is missing.

A renderer of your own

Pass a factory when you need more than a model, for example a scale:

StrutClient.entity(AVA, context -> new StrutEntityRenderer<>(context, AVA).withScale(1.2f));

Server-driven by default

A StrutEntity's controllers are server-authoritative: the server decides what plays, the client only mirrors it. That is what makes late joiners, lag and events reliable. A controller can be opted out to decide locally (AuthorityMode.CLIENT_COSMETIC), which is right for purely visual flourishes. Everything is in Playing animations.

Formats. The convention points at the binary formats. Legacy .geo.json and .animation.json files can still be loaded by naming them explicitly, but nothing new (events, the compact format, the Blockbench plugin) is built for them.

Worn models

ModelAttachment hat = ClientModelAttachments.set(entity, "hat", modelId, textureId).tint(0xFFFFD700);
hat.requireBone("head");            // not drawn on a model with no head bone
ClientModelAttachments.clear(entity, "hat");

A Strut model can be worn by another Strut-drawn entity (armour, a hat, a cape). It is bound to the wearer by bone name: each bone of the worn model that has the name of one of the wearer's bones is posed exactly as the wearer's is, every frame, so the model follows the wearer's animation, physics, overrides and visibility without being animated itself. Make the worn model on top of the wearer's skeleton (the same bone names, pivots and hierarchy) and keep only the part that is worn. Bones with no match stay at rest and move with their matched parent. A model with no bone in common with the wearer, or without the bone given to requireBone, is not drawn. It is local to the client, drawn by the GPU path in the same batch as the wearer (and in shadows), and has no armature support yet; it is not drawn where the wearer is drawn by the CPU fallback (a menu doll). /strutwear <model> <texture> puts a model on you in red (for example strut:strut/models/entity/test_dummy.strutmodel and strut:textures/entity/test_dummy.png), /strutwear status says why it is or is not drawn, and /strutwear clear takes it off. Only a player drawn by Strut (one with a body) can wear a model.

Attacks and item use

For any entity that is a LivingEntity, LivingActions.addTo(this, controllers) in registerControllers adds a controller that plays attack0, attack1, ... once for each swing of its hand, turn by turn (or swing if it has no combo), and one that plays use_eat, use_bow, use_block, ... (then use) while it uses an item. A clip the file does not have is skipped, and a swing's clip plays out in full. A second argument gives a prefix for files whose clips are named animation.base.attack0.