KubeJS Integration

Drive NeoOrigins from KubeJS scripts: listen to origin, power, mob, and mount events, invoke JS from JSON powers, and define whole power behaviors in JavaScript without touching Java.

  1. Availability
  2. Events
    1. Origin lifecycle
    2. Power lifecycle
    3. Mob origins
    4. Mounts & consent
  3. The NeoOrigins global
    1. Callbacks
    2. Resources
    3. JS-defined powers
      1. Passive power: neoorigins:js_custom
      2. Active power: neoorigins:js_active
  4. Reload behavior

Availability

KubeJS support is a soft dependency. It ships in the NeoOrigins 2.1.2+ builds for Minecraft 1.21.1 and, from 2.2.24, in the Minecraft 26.1 build as well. The 26.2 build does not bundle it: the NeoOrigins / NeoOriginsEvents globals, the neoorigins:kubejs_callback action and the neoorigins:js_custom / neoorigins:js_active power types are all absent there, and no amount of KubeJS being installed brings them back.

The reason is upstream, not a choice of ours: KubeJS has published no NeoForge build for Minecraft 26.2, so the 26.2 build declares no KubeJS dependency and carries none of the integration’s classes. Re-check KubeJS’s release list before repeating that claim.

On 26.1, KubeJS 8.0.4 itself requires NeoForge 26.1.2.84 or newer, so a pack on an older 26.1 loader has to move up before KubeJS will load at all. That is KubeJS’s floor, not ours: NeoOrigins still accepts any 26.1.

The two shipping branches are on different KubeJS major lines. 1.21.1 builds against the 7.x line, 26.1 against 26.1.2-8.0.4. The API break between those lines lands inside our plugin entrypoint rather than on any surface a script can see, so scripts move across unchanged, with one exception worth knowing about.

registerPower hands Rhino a plain JS object to adapt to an interface whose hooks are all optional. Rhino 2101.2.7-build.81, which the 7.x KubeJS builds pull in, will not adapt an object literal to an interface that has no required method, so on 1.21.1 registerPower throws Can't find method ...registerPower(string,object). Dropping Rhino 2101.2.8-build.91 or newer into the pack fixes it. 26.1 is unaffected, because KubeJS 8.0.4 already requires that Rhino build. registerActivePower works on both lines either way: its onUse is required, and one required method is all the adapter needs.

The 26.2 gap matters because an unregistered type is not a soft failure. A power file whose type the build doesn’t know is dropped whole at load: no error in game, no partial behaviour, the power simply never appears on the origin. So a js_custom power copied into a 26.2 pack looks like it silently vanished. Gate those files behind a pack that doesn’t ship on 26.2, or keep the behaviour in Java there.

When KubeJS is absent the whole subsystem short-circuits on a single cached check, so there is no overhead and no class-loading risk for packs that don’t use it.

All scripts here are server-side: origin, power, and mob state only changes on the logical server. Put them in server_scripts/ (or register from a server lifecycle event). Everything is cleared and must be re-registered on /kubejs reload; register inside a startup or server-loaded handler so stale handlers don’t pile up.


Events

Events live on the global NeoOriginsEvents group:

NeoOriginsEvents.originChanged(event => {
    console.log(`${event.player.name.string}: ${event.oldOriginId} -> ${event.newOriginId}`)
})

Event fields are exposed as KubeJS bean properties; event.player is the same as the Java getPlayer(). The tables below list the properties available on each event.

Origin lifecycle

Event Fires when Properties
originChosen A player picks an origin on a layer for the first time player, layerId, originId
originChanged A player’s origin on a layer changes: a pick in the selection screen, /neoorigins set, an advancement upgrade, automatic or random assignment, or a layer being cleared (Orb of Origin, re-opening a layer picker, a dependent layer invalidated) player, layerId, oldOriginId, newOriginId
evolutionTierChanged A player’s evolution tier changes player, oldTier, newTier
evolutionDeclined A player declines an evolution prompt that is standing player

* oldOriginId is null on a first-time selection; newOriginId is null when the layer is cleared. The /neoorigins reset command does not fire originChanged (nor powerRevoked): it tears powers down directly. Evolution tiers are integers: 0 base, 1 evolved, 2 ascended, 3 apex.

On this 26.1 build originChosen waits for the whole first walkthrough: while a player is still picking their first set of layers nothing fires, and once the last layer is filled it fires once per chosen layer, in layer order. A first pick made after that (a layer added later, for example) fires straight away.

evolutionDeclined only fires when there is an offer to decline: the player has to be below Apex and at or past the next tier’s kill threshold. /neoorigins evolve decline run with nothing on offer tells the player so and fires no event. Declining mutes the chat prompt for that tier, so in ordinary play the event fires once per tier. The command itself is not one-shot, though: the offer is still standing as long as the threshold holds, so running decline again fires the event again.

Power lifecycle

Event Fires when Properties
powerGranted A power is granted by an origin change (any of the originChanged paths above) player, powerId
powerRevoked A power is revoked by an origin change player, powerId
powerActivated A cooldown-based active power successfully fires (after cooldown/cost is paid) player, powerId
powerTick Every server tick, per active power, per player player, powerId

powerGranted / powerRevoked do not fire on login (powers are re-applied, not re-granted), for global powers, for powers swapped by an evolution-tier change, or for a full reset.

powerActivated does not fire when a use is aborted by cooldown, hunger, resource cost, or a no-op return. It only covers the cooldown-based active power types (including neoorigins:js_active). Toggle powers and active powers translated from Origins/Apoli packs never post it, even though they do trigger the datapack-side power_activated event. powerTick is high-frequency and only fires at all when at least one JS listener is registered, but inside the listener, assume it runs constantly and keep the body cheap.

Mob origins

Event Fires when Properties
mobOriginAssigned A mob is assigned a mob-origin (command, egg, or spawn rule) mob, originId
mobOriginCleared A mob’s mob-origin is cleared mob, previousOriginId

mob is a LivingEntity.

Event Fires when Properties
mountRequested A mount-power consent prompt is sent requester, target
mountAccepted The target accepts the prompt requester, target
mountDeclined The target declines the prompt requester, target
mountStarted A player starts riding a target rider, vehicle, position
mountEnded A player ends a mount-power ride rider, vehicle

position is the mount power’s mount_position (default "centered"). mountEnded fires whenever a ride that the mount power started ends, whatever removed the rider (it hooks the game’s own passenger removal, so sneaking off and deaths count as well as the power’s own dismount). It fires once per ride. Rides not started by the mount power (a horse, a boat) never fire it.


The NeoOrigins global

A NeoOrigins object is exposed to every script for registering callbacks and JS-defined power behaviors.

Callbacks

Register a function by id, then invoke it from any JSON action with the neoorigins:kubejs_callback action type. The callback receives a ServerPlayer.

NeoOrigins.registerCallback('mypack:shout', player => {
    player.tell('You triggered the power!')
})
Method Description
NeoOrigins.registerCallback(id, fn) Register fn(player) under id. Re-registering the same id overwrites.
NeoOrigins.unregisterCallback(id) Remove a callback.
NeoOrigins.hasCallback(id) true if a callback with that id is registered.

Invoke it from JSON anywhere an entity action is accepted:

{
  "type": "neoorigins:kubejs_callback",
  "id": "mypack:shout"
}

If no callback is registered for the id (KubeJS absent, or the script hasn’t run yet), the action is silently dropped with a debug log; it never errors.

neoorigins:kubejs_callback is unavailable on 26.2. There the action type is unknown, so it is skipped at parse time rather than at call time; the surrounding power still loads, but that step of the action never runs.

Resources

Read a player’s resource values from a script. Read-only: to change a value, run a neoorigins:change_resource action or the /resource command.

const mana = NeoOrigins.getResource(player, 'mypack:mana')
if (mana !== null && mana >= 10) {
    player.tell(`You have ${mana} mana`)
}
Method Description
NeoOrigins.getResource(player, id) The current value, or null if the player doesn’t have that resource.
NeoOrigins.hasResource(player, id) true if the player has that resource.

id is the resource power’s full id, the same string /resource get takes: mypack:mana, or mypack:parent_subkey for a sub-power of an origins:multiple. It works for neoorigins:resource bars, neoorigins:variable counters, and Origins/Apoli origins:resource powers. A bar with "backing": "irons_spellbooks:mana" reads the player’s live Iron’s Spells mana, truncated to a whole number; for such a bar hasResource is true only when the player holds that resource power. While disable_resource_bars is on in neoorigins/content.toml, a neoorigins:resource bar is never seeded, so both methods report it as absent.

player must be a server-side player, so call these from server scripts. Like everything else on this page, the two methods exist only where the KubeJS integration ships: the 1.21.1 and 26.1 builds, not 26.2.

JS-defined powers

Two power types let a JSON power delegate its whole behavior to a JS handler, keyed by js_id. This means new power behaviors are authorable from JS without any Java registry mutation.

Not on 26.2. neoorigins:js_custom and neoorigins:js_active are not registered in the 26.2 build, so a power file using either type is dropped whole at load there. See Availability.

Method Description
NeoOrigins.registerPower(id, handler) Passive power. Pairs with neoorigins:js_custom.
NeoOrigins.registerActivePower(id, handler) Active (keybind) power. Pairs with neoorigins:js_active.
NeoOrigins.hasPower(id) / NeoOrigins.hasActivePower(id) Registration checks.

The handler is a plain JS object; Rhino adapts it to the matching interface and fills any hook you omit with a no-op.

Passive power: neoorigins:js_custom

NeoOrigins.registerPower('mypack:slow_fall', {
    onGranted: player => player.tell('You gained slow fall!'),
    onTick: player => {
        if (player.deltaMovement.y < 0) {
            player.setDeltaMovement(player.deltaMovement.multiply(1, 0.9, 1))
        }
    },
    onRevoked: player => player.tell('You lost slow fall')
})
{
  "type": "neoorigins:js_custom",
  "js_id": "mypack:slow_fall",
  "name": "Slow Fall",
  "description": "You drift gently downward"
}

Hooks (all optional): onGranted(player), onRevoked(player), onTick(player).

Active power: neoorigins:js_active

onUse returns a boolean: true means the power fired and the cooldown/hunger cost should be paid; false is a no-op (nothing is consumed). This mirrors the contract every other active power uses.

NeoOrigins.registerActivePower('mypack:teleport_forward', {
    onUse: player => {
        let look = player.getForward()
        let target = player.position().add(look.scale(10))
        player.teleportTo(target.x, target.y, target.z)
        return true   // cooldown / hunger consumed
    },
    onGranted: player => player.tell('You gained the teleport power!')
})
{
  "type": "neoorigins:js_active",
  "js_id": "mypack:teleport_forward",
  "name": "Blink",
  "description": "Teleport ten blocks forward",
  "cooldown_ticks": 40,
  "hunger_cost": 2
}
Field Default Description
js_id (required) Id passed to registerActivePower.
cooldown_ticks 20 Ticks before the power can be used again.
hunger_cost 0 Hunger paid on a successful use.

Hooks: onUse(player) -> boolean (required), onGranted(player) and onRevoked(player) (optional).


Reload behavior

/kubejs reload wipes every registered callback and JS power handler. Register from a handler that runs on (re)load so your registrations come back:

ServerEvents.loaded(event => {
    NeoOrigins.registerCallback('mypack:shout', player => player.tell('hi'))
    NeoOrigins.registerActivePower('mypack:dash', { onUse: p => { /* ... */ return true } })
})

Events registered on NeoOriginsEvents follow normal KubeJS reload rules and do not need this treatment.