Kzeroko KTM2

Cinematic scene runtime

Scene ownership, trigger history, actor authority, late viewers and client cleanup.

Updated Sep 26, 2026 15 min read

A cinematic scene has one server timeline and a local playback for each viewer. The server owns actor bindings, world events and team sessions; clients own camera sampling, player steering and presentation, so a correct integration must release both sides.

What it does

internal.scene.ServerSceneManager is an internal server entry point, not a public API facade. It starts definitions loaded from data packs and shares them with eligible viewers. Use the scene data reference for every authored field and the camera API for independent client-only animations.

Trigger blocks add automatic admission, progress-based scene selection and persistent replay rules. They do not save an active timeline or resume interrupted playback after a server restart.

Runtime model

OwnerState and update point
SceneDefinitionLoaderCurrent definition map and revision, replaced on server data reload; active sessions retain their captured definition
ServerSceneManagerStatic map keyed by team:<uuid> or player:<uuid>; ticks at END_SERVER_TICK, removes disconnected viewers
ServerScenePlaybackWorld, pivot, actors, viewer UUIDs, skip votes, fired event indices and age
ScenePuppetControllerActor AI/sprint snapshots, cue-start markers and server path followers
ClientSceneManagerOne active or pending scene, exit transition, saved HUD/perspective and input ownership
ClientScenePlaybackActor entity IDs, local movement followers, presentation event markers and local age
SceneTriggerMemoryStatePersistent trigger/audience history; distinct from active sessions

The server resolves actors once, claims them and then sends start packets. Each server tick prunes invalid viewers, admits eligible nearby viewers, stops an empty session, ticks puppets, fires due world events and increments age. It stops when age exceeds lengthTicks, so the final authored tick is included.

Clients apply movement before entity ticks and advance presentation after entity ticks. A start packet includes current age, pivot, actor entity IDs and a reconstructed definition. The scene JSON is compressed into NBT with a 262144-byte compressed limit, and packets carry at most 64 actor bindings. Reconstruction through SceneAuthoringDraft normalises and rounds data; it is not a byte-for-byte copy of the source JSON. In particular, a camera node with look_at loses its authored rot on this route.

Admission and replacement

Eligible viewers are alive, neither creative nor spectator, still connected and in the scene’s world. Team identity comes through the IsekaiCore team bridge; an unteamed player uses a personal key. This repository selects the team owner’s main-story progress when that owner is online, otherwise the triggering player’s progress.

Audience modeInitial and later admission
TRIGGERING_PLAYER / triggering_playerTriggering player only
TEAM_NEARBY / team_nearbyEligible same-team players within a spherical radius of the pivot
ALL_NEARBY / all_nearbyEligible players of any team within that sphere

Player actors are also added as viewers because their own clients drive their movement. They must still pass eligibility and world checks. A radius above zero enables subsequent admission checks; an existing viewer is not removed merely for leaving the radius. Non-actor viewers must continue meeting the audience’s identity/team rule; actors bypass that identity rule, but not world or eligibility checks.

Starting a scene replaces the current session for its team key and stops other sessions containing an initial viewer. Later audience refresh and player-actor admission do not perform that same conflict scan, so this is not a universal one-session-per-player guarantee. Avoid overlapping triggers with shared audiences or casts.

isPlaying(player) first finds the player’s current team session, then a session containing that viewer. A teammate may therefore report playing without being a viewer, and stop(player, ...) can stop that team session. Skip requests separately require actual viewer membership.

Trigger selection and replay

The isekaiexpansion:scene_trigger block is invisible and has no collision. Its selectable outline and main-hand editor require creative mode plus permission level 2. Placement adopts the player’s horizontal facing; the pivot is the block centre and its facing supplies yaw.

Detection uses a cube with the configured radius as half-extent. This differs from the spherical audience search. Each server tick outside cooldown scans eligible players; the first player whose selected scene starts successfully consumes the trigger attempt. Rules are checked in list order against inclusive progress bounds, falling back to the default scene when none match. A matching rule with a missing definition does not fall through to a later rule or the default.

Replay modeMemory and cooldown
ONCE_PER_AUDIENCE / once_per_audienceSaves the triggering team/personal key for this trigger and selected scene
GLOBAL_ONCE / global_onceSaves the key global for this trigger and selected scene
REPEATABLE / repeatableNo persistent memory; cooldown is at least 200 ticks (10 seconds)

Replay scope is independent of audience mode. For example, all_nearby with once_per_audience records only the triggering team’s key, not every viewer’s team. History is recorded when start is accepted, not when the scene completes, so skipping or disconnecting does not undo it. A player already associated with an active session is skipped by the trigger.

Cooldown is shared by the block, not by each audience. The configured default is 80 ticks (4 seconds), clamped to 1–72000 ticks (0.05–3600 seconds), or 200–72000 ticks (10–3600 seconds) for repeatable mode. The remaining countdown is not saved; after reload only the configured duration survives.

Contracts

Actor and world authority

Actor resolution and cue schemas are defined in the data reference. All resolved actors are claimed, even those with no cues: mob navigation stops, AI is disabled and the scene acquires the NPC brain claim. On stop it restores saved AI and non-player sprint state, releases claims and clears scene movement/busy state. It does not restore positions, world time or weather.

Snapshots are stored per actor key, not per unique entity. Do not bind several actor keys to the same entity and assume independent restoration. There is also no general cross-session actor lock in this manager.

Cue routeAuthority and timing
Cue startFires once when server age equals its tick; distinct from world events, which fire once when due or overdue
Player walk / sprintOwning client writes movement through PlayerLocomotionOverride.Source.CINEMATIC; ordinary travel produces position updates
Non-player walk / sprintServer follower drives collision-aware movement, ground following and body/head steering
navigationRequests mob navigation to the endpoint for path-aware actors; not the camera follower route
Other interpolated movementServer interpolates along the authored path and places the actor; this is not ground-following gait
Pose/teleportServer placement; player placement uses network teleport
Player animationClient animation layer; NPC animation and body-emote routes remain server actor operations

Movement followers measure progress from actual positions. A blocked actor does not advance merely because time passed. The locomotion audit can warn that a path and duration disagree, but it does not extend the cue window or scene length. Player animation fade is bounded by cue duration, not used to retime the animation clip.

World sound and particle events use world broadcasts, and time/weather change the world. The event’s visible_to_all switch belongs to the Effek route; it is not a universal viewer filter. Stopping a scene prevents later events but does not roll back earlier ones.

Locks, skip votes and cleanup

If either lock_input or lock_movement is enabled, the server disables item use, attack and jump under a unique cinematic_scene:<uuid> source. It deliberately leaves the action-system MOVE flag unset so player travel can run. Lock lifetime is remaining scene ticks plus fade-out ticks plus 20 ticks (1 second), with a minimum of 1 tick (0.05 seconds). Cleanup removes only this scene’s action-lock source.

The client forces rear third-person perspective, optionally hides the HUD, closes screens while input is locked and applies its cinematic movement override when requested. It saves and restores the prior perspective and HUD setting. While a scene is active it hides player entities not in the cast; this is not a rule that hides every non-cast entity.

A skip request succeeds only for a current viewer when allow_esc_skip is true. Votes are unique per viewer and require every current viewer. Leaving viewers lose their votes and can reduce the quorum; late viewers increase it. Repeated voting returns the current vote state. The client displays votes through a keyed popup lasting 2400 ms, with 80 ms entry and 160 ms exit fades.

Entry fade defers local playback and adds its duration to the received age, because the server keeps running. A late client primes only presentation still relevant at that age, rather than replaying expired subtitles or full animations. Movement follows the player’s actual position. Server events are not replayed for late viewers.

Exit fade stops local timeline work while retaining the last camera pose and locks until the callback restores state. Cancelling a pending start via normal stop(false) restores state but does not explicitly clear the fade controller; immediate replacement/disconnect cleanup does clear it. Disconnect also clears cinematic movement, input, subtitle and animation state. The shared shake/distortion controllers are cleared globally, so their effects are not isolated by scene ownership.

Start and stop packets run through the client executor, and skip/update packets run through the server executor. Stop/skip packets have no scene token; coordinate other callers rather than assuming stale requests can be matched to a specific playback.

Subtitles and client settings

Subtitles are immutable presentation snapshots passed from the client to a non-interactive ModernUI overlay. The shared RetainedOverlay owns attachment, detachment and atomic delivery; it ignores identical consecutive snapshots and applies the latest snapshot on attachment. The overlay keeps the latest pending snapshot and does not read config on the UI thread. It remains independent of vanilla HUD visibility. Text and speaker strings are resolved as translation keys with literal fallback.

FeatheredPlateView supplies the shared background for cinematic subtitles and CaptionLineView, including NPC command feedback sent through CaptionAPI on NPC_FEEDBACK. A cached mesh interpolates a quintic opacity curve between nested rounded contours in one draw. Shared edges avoid overlapping opacity, and the outer contour is fully transparent. Both subtitle systems use this feather; there is no opaque border or stretched background texture.

Geometry is rebuilt only when dimensions, colour, width fraction, feather profile or corner radius change. Detaching releases the cached arrays. Both subtitle and caption core corner radii use 10 dp multiplied by their presentation scale; the transparent outer contour expands the rounding through the feather. Text reveal and movement reuse the mesh.

SubtitleFade supplies both systems with quintic easing: entry lasts up to 360 ms, natural exit up to 460 ms, each capped at 35% of the remaining cue time when its view is created. A delayed UI delivery starts at transparent while text reveal still catches up to its authored timestamp. Replacement or clearing fades from the displayed opacity over 360 ms; an invisible outgoing line can be removed immediately. Feedback retirement preserves its existing age dimming without applying it twice, and stack layout changes interpolate from the displayed position instead of snapping.

One cinematic cue is current at a time. Its outgoing view retains its own text and layout during the crossfade; rapid replacements keep earlier visible fades until they finish. A single overlay frame callback advances all retained cues. Reveal timestamps use wall-clock milliseconds with 50 ms per scene tick, while cue expiration is also checked against scene age. Consequently render-clock reveal is not a promise that text freezes with scene ticks. Theme values are captured when the cue is shown; editing config does not recolour an already published snapshot.

OptionTypeDefaultWhat it does
cinematicSubtitleBandColorstring"#0A0F18"Band RGB; malformed colour falls back to this value.
cinematicSubtitleBandOpacityinteger88Percent, clamped to 0–100, converted to alpha by rounding ×2.55.
cinematicSubtitleBandWidthPercentinteger96Band width fraction, clamped to 40–100%.
cinematicSubtitleTextWidthPercentinteger62Text width fraction, clamped to 30–96%.
cinematicSubtitleSpeakerColorstring"#FFD79A"Opaque speaker RGB; malformed colour uses this fallback.
cinematicSubtitleTextColorstring"#F3F6FF"Opaque text RGB unless the cue has a nonzero alpha byte.
cinematicSubtitleScalePercentinteger100Scale fraction, clamped to 60–160%.

These are client presentation options, not scene data or server-synchronised values. Colour parsing trims whitespace, accepts 6 or 8 hexadecimal digits with an optional leading #, and discards any supplied alpha; band opacity is separate. A cue colour whose alpha byte is zero inherits the configured body colour.

Trigger save data

The following is block-entity NBT, not the scene JSON format. Editor updates run on the server, require the same creative/permission check, a trigger at the target position and distance at most 8 blocks. They validate ID syntax but do not require the referenced definition to exist.

NBT keyTypeDefaultMeaning
SceneidentifierNoneDefault scene; invalid syntax becomes null on read.
SceneRulesarray<object>[]First 16 valid rules retained; absent/wrong list type gives no rules.
SceneRules[].SceneidentifierRequiredInvalid identifier skips this rule.
SceneRules[].MinProgressinteger0Inclusive lower bound; reversed bounds are swapped.
SceneRules[].MaxProgressinteger0Inclusive upper bound; no positive-only restriction.
Radiusnumber2.0Blocks. Configuration clamps to 0.25–128; NBT read applies only the 0.25 lower bound.
ReplayModestring"once_per_audience"Replay ID from the table above; blank/unknown values use default.
AudienceModestring"team_nearby"Audience ID from the table above; blank/unknown values use default.
CooldownTicksinteger80Configured cooldown, sanitised for the replay mode; repeatable default becomes 200.

Mode readers trim and lowercase strings. Radius sanitisation is a numeric clamp, not an explicit finite-number check. Rule order is preserved; no overlap detection or sort is performed.

Persistent history uses the server-scoped isekaiexpansion/scene/trigger_memory_v2.dat described by persistent state. The scope includes dimension, immutable block position and selected scene ID, so changing the scene or moving the block creates a different history key.

NBT keyTypeDefaultMeaning
versioninteger0Must equal 2; missing/wrong version logs a warning and discards loaded history.
entriesarray<object>[]Trigger scopes; missing/wrong list type gives an empty state.
entries[].worldidentifierRequiredDimension ID syntax; invalid entry is skipped.
entries[].x, entries[].y, entries[].zinteger0Trigger coordinates.
entries[].sceneidentifierRequiredSelected scene ID; invalid entry is skipped.
entries[].audiencesarray<string>RequiredSet of audience keys; missing list skips entry, duplicate scopes/keys merge. Blank keys normalise to unknown.

Only adding a new audience marks this state dirty. Writes sort scopes and audience keys and omit empty sets. This is trigger history, not a list of successful scene completions, and this class supplies no automatic pruning or reset command.

Extending it

For an internal server integration, call from the server thread after definitions are loaded and the player is in a server world:

boolean accepted = ServerSceneManager.play(
        player,
        new Identifier("isekaiexpansion", "hidden_hud_subtitle_scene"),
        new SceneTransform(player.getPos(), player.getYaw())
);

This overload uses only the triggering player as the audience and a zero admission radius. A missing scene returns false before replacing the existing session. True means playback was created, not that every requested viewer was admitted or that playback completed; a creative trigger player can yield no viewers and stop on the next tick.

Use playFromTrigger with an explicit SceneAudienceMode when sharing is intentional. There is no automatic thread marshalling in these manager calls and no supported call before a server/player world exists. Keep client manager and camera classes off the dedicated-server path.

To add a cue or event, update definition parsing, draft serialisation and the appropriate server/client dispatcher together. A parser-only addition can be lost during packet reconstruction. Preserve source-owned action cleanup and the player/non-player movement split.

Verifying

  • Start a known scene with a survival player, then with an ineligible player. Check accepted-start versus admitted-viewer behaviour.
  • Exercise all audience modes with another team, an unteamed player, a late arrival, a radius departure and a dimension change.
  • Verify first-match progress rules, offline team-owner fallback, missing definitions and repeatable cooldown. Restart the world and inspect persistent versus repeatable history.
  • Vote to skip, disconnect a voter, add a new viewer and repeat a vote. Confirm the current audience determines the quorum.
  • Put a walking actor against an obstacle; confirm collision affects actual progress and the scene still ends at its authored time.
  • Interrupt entry and exit fades, replace a scene and disconnect. Confirm perspective, HUD, movement and action locks are released.
  • Use the bundled hidden_hud_subtitle_scene to check subtitles with hidden HUD, theme inheritance and a late entry. These are verification procedures, not claims of completed multiplayer or visual tests.