KubeJS

KubeJS scripts can drive EmbersTextAPI directly. Send and update immersive HUD messages, run sequenced queues, and apply effects to item names and tooltips, all from JavaScript, with no Java mod to compile.

The integration exposes a single global, EmbersText, in every script type. It wraps the same send pipeline and effect registry the Java API and /eta commands use, so anything you can do from a command you can do from a script.

EmbersTextAPI treats KubeJS as an optional dependency. Nothing extra to configure.

  1. Install KubeJS (NeoForge 1.21.1) and its dependencies alongside the matching EmbersTextAPI build.
  2. Launch once so KubeJS generates its kubejs/ script folders.
  3. Put your scripts in kubejs/startup_scripts/, kubejs/server_scripts/, or kubejs/client_scripts/ depending on what they do (see below).

If KubeJS is absent, ETA loads and runs normally; the binding simply isn’t registered.

Script folderRuns onUse EmbersText for
startup_scripts/both, at loaditemName rules
server_scripts/serversend, hud, queue, and everything that targets players
client_scripts/clienttooltip styling via ItemEvents.dynamicTooltips

Messages, HUD elements, and queues are sent from the server (they target ServerPlayer). Item-name rules register at startup. Tooltip styling is client-side because tooltips render on the client.

EmbersText.markup(duration, text) builds a message from ETA markup; EmbersText.message(duration, text) builds one from plain text. Both take a duration in ticks (20 ticks = 1 second) and return a builder you can position and style with fluent calls. Pass the result to send:

kubejs/server_scripts/eta_demo.js
ServerEvents.commandRegistry(event => {
const cmd = event.commands
event.register(cmd.literal('eta_hello').executes(ctx => {
EmbersText.send(ctx.source.player, EmbersText.markup(60, '<wave>Hello from KubeJS</wave>'))
return 1
}))
})

Send to everyone online with sendToAll(server, message):

EmbersText.sendToAll(event.server, EmbersText.markup(80, '<rainbow>Server restarting soon</rainbow>'))

The builder returned by message/markup carries the same layout options as the Java ImmersiveMessage. Anchors are passed as lowercase strings:

EmbersText.markup(0, '<bold>Wave incoming</bold>')
.anchor('top_center')
.scale(1.5)
.fadeInTicks(10)
.fadeOutTicks(10)
.offset(0, 20)

Common builder methods: anchor(string), align(string), offset(x, y), scale(size), color(hexOrName), shadow(bool), background(bool), bgColor(color), bgAlpha(0..1), fadeInTicks(int), fadeOutTicks(int), effect(tag), wrap(maxWidthPx). See Layout & Positioning for what each one does.

hud(player, id, message) opens a persistent HUD element identified by id (any string). Call it again with the same id to swap the contents in place, with no flicker and no fade restart. Unlike send, a hud element ignores the client’s maximum-duration cap, so a duration of 0 stays on screen until you close it. This is what you want for timers, counters, and stat readouts.

kubejs/server_scripts/eta_clock.js
// Rhino re-runs tick callbacks repeatedly. Declare per-call locals with `var`,
// not `const`/`let`, or you'll hit "redeclaration of var".
const CLOCK_ID = 'eta_clock'
var clockOn = false
var clockSeconds = 0
ServerEvents.commandRegistry(event => {
const cmd = event.commands
event.register(cmd.literal('eta_clock').executes(ctx => {
clockOn = !clockOn
clockSeconds = 0
if (!clockOn) {
ctx.source.server.playerList.players.forEach(p => EmbersText.closeHud(p, CLOCK_ID))
}
return 1
}))
})
ServerEvents.tick(event => {
if (!clockOn) return
if (event.server.tickCount % 20 !== 0) return
clockSeconds++
EmbersText.hudToAll(event.server, CLOCK_ID,
EmbersText.markup(0, '<bold>Uptime: ' + clockSeconds + 's</bold>')
.anchor('top_center')
.scale(1.5))
})

hudToAll(server, id, message) does the same for every online player. closeHud(player, id) removes one element; closeAllMessages(player) clears everything on a player at once.

A queue plays a sequence of steps on a named channel. Each step is a list of messages that display together, and steps advance in order. Build it as a list of lists:

kubejs/server_scripts/eta_queue.js
const steps = [
[EmbersText.markup(40, '<type>Step one...</type>')],
[EmbersText.markup(40, '<type>Step two...</type>')]
]
EmbersText.queue(ctx.source.player, 'demo', steps)

Control queues by channel name: stopQueue(player, channel) halts the active one and drops what’s pending; clearQueue(player, channel) drops the pending steps but lets the current message finish; clearAllQueues(player) and stopAllQueues(player) apply to every channel. See Queues for the timing model.

itemName(matcher, markup) decorates an item’s default name wherever it appears (inventory, tooltips, hover), with no language-file edits. Register the rules at startup. The matcher selects items; the markup is normal ETA markup, so chain tags to stack effects:

kubejs/startup_scripts/eta_names.js
EmbersText.itemName('minecraft:diamond_sword', '<rainbow>')
EmbersText.itemName('#minecraft:planks', '<wave>')
EmbersText.itemName('/_ingot$/', '<glow color=gold>')
EmbersText.itemName('minecraft:netherite_sword', '<glow color=gold><wave>')

Matchers are tested against the item id:

Matcher formMatchesExample
Exact idone itemminecraft:diamond_sword
#tagevery item in the tag#minecraft:planks
namespace:*every item from a modcreate:*
/regex/ids matching the pattern/_ingot$/

A player rename (anvil) overrides the rule for that stack.

For tooltip lines that depend on the stack (durability, a name match, NBT), use KubeJS’s own ItemEvents.dynamicTooltips and turn effect specs into styled components with EmbersText.style or EmbersText.styled. These run in client_scripts.

style(component, ...effectSpecs) returns a copy of a Component with effects attached; styled(text, ...effectSpecs) does the same starting from a literal string. Effect specs are tag bodies without angle brackets, one per argument: 'rainbow', 'glow color=gold', 'shake speed=2'.

kubejs/client_scripts/eta_tooltips.js
// Rainbow the name line of any sword.
ItemEvents.dynamicTooltips('emberstextapi:swords', event => {
if (!Ingredient.of('#minecraft:swords').test(event.item)) return
event.lines.set(0, EmbersText.style(event.lines.get(0), 'rainbow'))
})
// Shake and pulse red when a tool is near breaking.
ItemEvents.dynamicTooltips('emberstextapi:lowdurability', event => {
const stack = event.item
if (!stack.isDamageableItem()) return
const remaining = (stack.getMaxDamage() - stack.getDamageValue()) / stack.getMaxDamage()
if (remaining > 0.15) return
event.lines.set(0, EmbersText.style(event.lines.get(0), 'shake speed=2', 'pulse color=red'))
})
MethodReturnsDescription
message(duration, text)builderBuild a message from plain text.
markup(duration, markup)builderBuild a message from ETA markup.
send(player, message)voidSend to one player.
sendToAll(server, message)voidSend to every online player.
closeAllMessages(player)voidClose all of a player’s messages and HUD elements.

A few things are intentionally out of scope for the scripting binding:

  • Chat-word styling. Effects don’t survive the vanilla Style network codec yet, so styling individual chat words from a script is deferred to a later phase.
  • Script-defined effects. You can use any built-in effect from JS, but defining brand-new per-glyph effects in a script is not supported (it’s a render hot path). Write a custom effect in Java instead.
  • GUI hover-name styling outside tooltips. There’s no KubeJS event for it. Use itemName rules, which cover the name everywhere it renders.