Scenes
Several entities, players with a body among them, animated together by the server on one clock: the base a mod builds a paired animation, a cutscene or a scene system on.
The idea
Strut owns what every such system needs and none of what a scene means. A scene has roles (names you pick: host, partner), each filled by an entity placed at a spot in the scene's space. A step says which clip each role plays. Every clip of a step starts on the same game tick, so the clips stay in step for everyone however they are watching (the clock is the server's, see playing animations), and the next step replaces the one before on all roles at once. Which step follows, what an event does and how a scene is won are yours.
Starting one
Scene scene = StrutScenes.create(new Identifier("mymod", "embrace"), world, origin, yaw)
.participant("host", creature, ScenePlacement.ORIGIN)
.participant("partner", player, ScenePlacement.offset(0, 0, 0.8).facing(180))
.onStepEnd(next -> next.step().clip("host", "embrace_end").clip("partner", "embrace_end").start())
.onEnd((ended, why) -> ...)
.start();
scene.step().clip("host", "embrace_in").clip("partner", "embrace_in").start(); // both start on the same tick
scene.setRate(1.5f); // faster for both, without a jump
start() places everyone and plays nothing; scene.step()...start() plays. A role the step does not name stops what it was playing, so a step is also how a branch drops a participant's clip. clip(role, name) plays once and holds the last frame; clip(role, name, LoopMode.LOOP) loops. clipThen(role, intro, then, LoopMode.LOOP) plays an intro and, the moment it ends, the clip that follows it on the same clock with no gap (an intro and its loop, for one role); the step is not finished while the intro runs.
Where everyone stands
The scene has an origin and a yaw. A ScenePlacement is a spot in the scene's own space: z ahead, x to the left, y up, plus a turn from the scene's facing. Author the scene once with the host at the origin and the partner a step ahead and it works wherever it is placed; scene.move(origin, yaw) moves them all.
- A player with a body has the body drawn at the spot (
StrutPlayers.setAnchor). The player themself is not moved or held by Strut: hold them with your own rule, askingStrutScenes.inScene(player)(it works on both sides). - Any other entity is moved to its spot and held there, its AI off, for the scene's length, and given back afterwards.
holdEntities(false)in the builder leaves them alone.
Which controller plays
Every role plays on a controller of its entity: scene by default, which every player body has (over locomotion, swings and item use) and which a StrutEntity makes on demand. participant(role, entity, placement, controllerName, required) picks another. The clip names are found the way they are for any server-played animation: through the body's or entity's declared animation files, then by name if only one file has it.
Steps, branches and the end
- When every clip of a step that does not loop has run to its end,
onStepEndruns (once for each step). Start the next step in it, or the scene is over (SceneEnd.COMPLETED). A step made only of loops never ends by itself: the mod ends it, or starts another, when its own rule says. - A clip whose length the server cannot find counts as still playing; give the scene a
timeout(ticks)as a safety net. scene.seek(tick, fireCrossed)andscene.setRate(rate)act on every clip of the step together.scene.end()ends it now. Every way a scene ends stops the participants' clips, puts back the AI and the body anchors, and runsonEndwith the reason.
Joining and leaving
scene.join(role, entity, placement) adds a participant to a scene that is running (a partner who gets on), placed and announced as at the start, with no clip until it is given one: by the next step, or by scene.playNow(role, clip, loop), which starts a clip for that role alone and leaves the others as they were. scene.leave(role) takes one out: its clip stops, what the scene changed on it is put back, and the scene goes on, even if the role was required. So a participant changing does not mean a new scene, and nothing restarts.
Losing a participant
A participant is required unless added with optional(...). If a required one logs out, dies, is removed or changes dimension, the scene ends with SceneEnd.PARTICIPANT_LOST; an optional one just leaves its role. An entity is in one scene at a time: starting a second throws an exception that names the entity, the role and the scene it is in.
Events and the client
Clips of a scene fire their markers and typed events as any server-played clip does (events). In a handler, StrutScenes.sceneOf(context.entity()) on the server finds the scene and scene.roleOf(entity) the role. On every client that sees an entity in a scene, StrutScenes.of(entity) is its SceneMember: the scene's id, the role, the kind of scene and the origin, for a camera, a HUD or a rule. Taking part in a scene also makes a player a participant of what the other members' clips do (ClientEventContext#isLocalPlayerParticipant).
Players and outfits
A scene with players needs nothing special: give them a body (players with a body) and the scene controller shows what the server plays. The same calls work outside a scene: StrutPlayers.play(player, "outfit", LoopMode.PLAY_ONCE), setRate, seek, playing, hasEnded. To hide the armour, use StrutArmour.hideAll(entity) (setHidden for one slot, showAll to dress again), and change the model a character is drawn with through the body's variant or its model (players). A mod's strip then is: play the clip, hide the armour on its event, swap the variant.
What to know
- Scenes live in memory on the server and do not survive a restart.
- Strut does not hold a player in place (see above), choose a camera, move a partner onto a bone or decide what a scene means: those are the mod's.
- Required participants are checked every tick, so a scene ends within a tick of one leaving.