Skip to content

API and Events

Drehverschluss edited this page Nov 7, 2026 · 2 revisions

API and events

RunelessRPG has a small API for other mods and scripts: one class with static methods (RunelessApi) and two events on the NeoForge event bus. Everything is in the package com.drehverschluss.runelessrpg.api.

RunelessApi

Reading works on both sides (on the client: the values of the local player last sent by the server). Changing mana only works on the logical server; every change is clamped to 0 … max mana, sent to the client and reported as a ManaChangeEvent with reason API.

Method Side Returns / effect
getMana(Player) both Current mana (may be negative after casting in a debt mode)
getMaxMana(Player) both Value of runelessrpg:max_mana (never negative)
getManaRegen(Player) both Value of runelessrpg:mana_regen in mana per second (may be negative)
setMana(ServerPlayer, double) server Sets the mana
addMana(ServerPlayer, double) server Adds mana (negative amounts remove it)
trySpend(ServerPlayer, double) server Removes the amount if the player has at least that much; true if spent (always true for amounts ≤ 0), false and no change otherwise
getCategory(Player, Holder<Spell>) both ManaCategory of the spell (NONE = Spell Engine costs apply)
getBaseCost(Player, Holder<Spell>) both OptionalDouble: base cost (tier table × multiplier, or the datapack value); empty for NONE and FREE
getCost(Player, Holder<Spell>) both OptionalDouble: final cost for this player after ManaCostEvent (total for a full channel); creative players get the regular value
getOverchargeBonus(Player, Holder<Spell>) both Damage / healing multiplier from Overcharge for this spell right now (1.0 = none)
isChanneled(Holder<Spell>) both true for channeled spells
getCostPerChannelTick(Player, Holder<Spell>) both OptionalDouble: cost of one channel tick; empty if not channeled or free

ManaCategory: NONE, FREE, SPELL, CLASS_SKILL, WEAPON_SKILL, ITEM_ABILITY.

ManaCostEvent

Fired every time RunelessRPG calculates the mana cost of a spell for a player, after base cost, Mana Cost (with its floor) and Overcharge. Listeners can change the final cost.

Method Meaning
getPlayer() The casting player (client or server instance)
getSpell() The spell (Holder<Spell>)
getCategory() The resolved ManaCategory
getBaseCost() Base cost before player attributes
getCost() / setCost(double) Final cost; negative values become 0
  • Fired on both logical sides. The client calculates costs itself for the cast check, the "missing mana" message and the tooltip (every frame while a tooltip is open). A listener must return the same result on client and server and should be cheap.
  • Only fired for spells that cost mana (not for NONE and FREE).
  • For channeled spells the cost is the total for the full channel.

ManaChangeEvent

Fired on the logical server after a player's mana actually changed. Informational, not cancelable.

Method Meaning
getPlayer() The player
getOldMana() Mana before the change
getNewMana() Mana after the change (may be negative in a debt mode or with Borrowed Power)
getReason() Why it changed
Reason Cause
REGEN Regeneration (also negative). Reported in steps of at least 0.05 mana, or when the pool becomes full or empty; getOldMana() is then the value of the previous report
COMMAND /runelessrpg mana
API Another mod through RunelessApi
RESPAWN Refill after respawning
CLAMP Lowered to a smaller maximum (gear taken off)
SPELL Paid for a spell, also every tick of a channeled spell
LEECH Mana Leech
SPELL_LEECH Spell Mana Leech
POTION Mana Restoration (potion, splash, lingering cloud, tipped arrow)

Java example

Declare RunelessRPG as an optional dependency and compile against its jar (compileOnly).

import com.drehverschluss.runelessrpg.api.ManaChangeEvent;
import com.drehverschluss.runelessrpg.api.ManaCostEvent;
import com.drehverschluss.runelessrpg.api.RunelessApi;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.neoforge.common.NeoForge;

public final class MyManaCompat {
    private static final ResourceLocation FIREBALL = ResourceLocation.parse("wizards:fireball");

    public static void init() {
        // Fireball costs half. Runs on client and server: only use data both sides know.
        NeoForge.EVENT_BUS.addListener((ManaCostEvent event) -> {
            if (event.getSpell().is(FIREBALL)) {
                event.setCost(event.getCost() * 0.5);
            }
        });
        // Server only
        NeoForge.EVENT_BUS.addListener((ManaChangeEvent event) -> {
            if (event.getReason() == ManaChangeEvent.Reason.SPELL && event.getNewMana() < 0) {
                // the player is in debt now
            }
        });
    }

    // Somewhere on the server
    static void reward(net.minecraft.server.level.ServerPlayer player) {
        RunelessApi.addMana(player, 5.0);
    }
}

KubeJS example

KubeJS for 1.21.1 (2101) listens to NeoForge events with NativeEvents.onEvent(Class, handler). ManaCostEvent must run on both sides, so it belongs into startup_scripts; ManaChangeEvent only fires on the server.

kubejs/startup_scripts/runelessrpg.js

const ManaCostEvent = Java.loadClass('com.drehverschluss.runelessrpg.api.ManaCostEvent')

NativeEvents.onEvent(ManaCostEvent, event => {
  // Weapon skills cost nothing while the player is in the Nether
  if (event.getCategory().name() == 'WEAPON_SKILL'
      && String(event.getPlayer().getLevel().getDimension()) == 'minecraft:the_nether') {
    event.setCost(0)
  }
})

kubejs/server_scripts/runelessrpg.js

const ManaChangeEvent = Java.loadClass('com.drehverschluss.runelessrpg.api.ManaChangeEvent')
const RunelessApi = Java.loadClass('com.drehverschluss.runelessrpg.api.RunelessApi')

NativeEvents.onEvent(ManaChangeEvent, event => {
  if (event.getReason().name() == 'POTION') {
    event.getPlayer().tell(`Mana: ${event.getNewMana().toFixed(1)} / ${RunelessApi.getMaxMana(event.getPlayer())}`)
  }
})

Both examples were tested with KubeJS 2101.7.2 (Rhino 2101.2.8):

  • Use KubeJS's own helpers on Minecraft objects, e.g. getLevel().getDimension(). In KubeJS level is a property, so the vanilla call player.level() fails with "It is not a function".
  • An error inside a ManaCostEvent handler is thrown into the cost calculation, so the spell cast or the /runelessrpg cost command fails with it. Test new handlers with /runelessrpg cost <spell>.
  • The ManaChangeEvent handler in server_scripts survives /reload and fires once afterwards (not twice). Changes to startup_scripts need a game restart.

Clone this wiki locally