NeoOrigins Java API

For mods that want to integrate with NeoOrigins: check if a player has a particular power, listen for origin changes, register a custom power type, or exempt summoned minions from their own logic.

Pack authors: this doc isn’t for you. See API.md for the datapack-facing reference.


Stability contract

Types under com.cyberday1.neoorigins.api.** follow semver:

Release What can change
3.0.0 (major) Any breaking change. Deprecations honoured for at least one minor cycle beforehand.
2.x.0 (minor) Additive only: new methods, new power types, new events. Existing signatures stable.
2.0.x (patch) Bug fixes only. No API surface changes.

Types under service/, event/, power/builtin/, compat/, mixin/, network/ are internal. They can change between patch releases. Don’t import them from your mod. A few samples below still name internal classes (SummonMinionPower and TameMobPower from power/builtin/, GeckoLibCompat from compat/); treat those references as internal too.

If an integration you need isn’t available through api/, open an issue. We’ll promote the internal method to API rather than ask you to import a service class directly.


Dependency setup

build.gradle:

repositories {
    maven { url "https://api.modrinth.com/maven" }
}

dependencies {
    // Compile against a stable minor release; runtime will use whatever
    // version of NeoOrigins the user has installed ≥ the declared version.
    // Replace <VERSION> with the latest release for your MC version — see the
    // releases page linked below.
    // For 26.1.x:
    compileOnly "maven.modrinth:neo-origins:<VERSION>+26.1"
    // For 26.2:
    // compileOnly "maven.modrinth:neo-origins:<VERSION>+26.2"
    // For 1.21.1:
    // compileOnly "maven.modrinth:neo-origins:<VERSION>+1.21.1"
}

Version format: Modrinth Maven uses the exact version string from the releases page. The format is v{version}+{mc_version}, where {mc_version} is 1.21.1, 26.1 or 26.2 (e.g. v{version}+26.1). Always pull the newest published string from the releases page rather than hard-coding a value here, so this snippet doesn’t go stale between releases.

neoforge.mods.toml:

[[dependencies.your_mod]]
    modId = "neoorigins"
    type = "optional"   # or "required"
    versionRange = "[2.0.0,3.0.0)"
    ordering = "AFTER"
    side = "BOTH"

Use optional unless your mod is useless without NeoOrigins: gives players the choice. Check with ModList.get().isLoaded("neoorigins") before calling into the API.


Entry point: NeoOriginsAPI

The preferred way to call into NeoOrigins.

import com.cyberday1.neoorigins.api.NeoOriginsAPI;

// Does any active power on this player emit the "enhanced_vision" capability tag?
if (NeoOriginsAPI.hasCapability(player, "enhanced_vision")) { ... }

// Exempt our own damage code from hitting tracked minions of their owner:
if (NeoOriginsAPI.isMinionOf(targetEntity, sourcePlayer)) return;

// Iterate all SummonMinionPower configs on the player:
NeoOriginsAPI.forEachOfType(player, SummonMinionPower.class, cfg -> {
    int maxCount = cfg.maxCount();
    // ...
});

Full method list:

Method Purpose
powers(player) All active power holders.
has(player, PowerClass, filter) True if player has at least one matching power.
forEachOfType(player, PowerClass, visitor) Iterate configs of the given type.
hasCapability(player, tag) True if the capability tag is emitted (shared vocabulary with client-side effect layers). Tags are emitted by specific power types, e.g. enhanced_vision, wall_climb, walk_on_water; there is no water_breathing tag.
summonerOf(entity) Reverse-lookup a summoned minion’s owner.
isMinionOf(entity, summoner) Cheap check: is this entity summoner’s minion?
isAnyMinion(entity) Is this entity anyone’s tracked minion? Falls back to a scan of tracked minions when the entity carries no minion marker.

Call these on the logical server: the player-facing methods take a ServerPlayer and read server-side state.


Events (api/event/)

Listen on the NeoForge event bus:

@EventBusSubscriber(modid = YourMod.MOD_ID)
public class YourOriginListener {
    @SubscribeEvent
    public static void onOriginChanged(OriginChangedEvent event) {
        ServerPlayer player = event.getEntity();
        Identifier newOrigin = event.getNewOrigin();
        // React to the player picking a new origin.
    }
}

All four are posted on NeoForge.EVENT_BUS.

Event When it fires Data
OriginChangedEvent A player picks an origin in the selection screen (the server’s handler for the pick packet). Not posted for /neoorigins set, reset, advancement upgrades, or automatic/random assignment. Cancellable: cancelling prevents the set. getEntity() (ServerPlayer), getLayer(), getOldOrigin() (nullable), getNewOrigin(), setNewOrigin(id)
OriginsLoadedEvent After the origin JSONs are applied, on every datapack load, including the first one at server start. Layers are reloaded after it, so layer data is not yet current when it fires. None: it extends Event and has no getters.
PowerGrantedEvent A power was granted through an origin change or a global power. getEntity() (ServerPlayer), getPowerId()
PowerRevokedEvent A power was revoked through an origin change or a global power. getEntity() (ServerPlayer), getPowerId()

PowerGrantedEvent / PowerRevokedEvent are not posted on every path that runs a power’s onGranted / onRevoked: an evolution-tier change and a full tear-down (origin reset, profile re-apply, random-origin-on-death) call the hooks without posting the events.


Custom power types

Extend com.cyberday1.neoorigins.api.power.PowerType<C> and register it in your mod’s registration phase.

public class MyBoostPower extends PowerType<MyBoostPower.Config> {
    public record Config(double amount, String type) implements PowerConfiguration {
        public static final Codec<Config> CODEC = RecordCodecBuilder.create(i -> i.group(
            Codec.DOUBLE.fieldOf("amount").forGetter(Config::amount),
            Codec.STRING.optionalFieldOf("type", "").forGetter(Config::type)
        ).apply(i, Config::new));
    }

    @Override public Codec<Config> codec() { return Config.CODEC; }

    @Override
    public void onTick(ServerPlayer player, Config config) {
        // Called every tick while the player has this power granted.
    }
}

Register during RegisterEvent for PowerType at your mod’s initialisation:

// The power-type registry id is neoorigins:power_type. NeoOriginsAPI exposes no
// constant for it, so build the key yourself.
private static final ResourceKey<Registry<PowerType<?>>> POWER_TYPES =
    ResourceKey.createRegistryKey(Identifier.fromNamespaceAndPath("neoorigins", "power_type"));

@SubscribeEvent
public static void onRegister(RegisterEvent event) {
    event.register(POWER_TYPES,
        helper -> helper.register(
            Identifier.fromNamespaceAndPath("mymod", "my_boost"),
            new MyBoostPower()));
}

Pack authors can now use "type": "mymod:my_boost" in their power JSONs.

Lifecycle hooks

Hook When it fires
onGranted(player, config) Power was just added to the player’s active set.
onRevoked(player, config) Power was just removed.
onTick(player, config) Every server tick while granted and the power’s condition passes. Keep this cheap.
onLogin(player, config) Player logged in with this power. Defaults to calling onGranted.
onRespawn(player, config) Player respawned with this power. Defaults to calling onGranted.
onActivated(player, config) The power was activated: its skill hotkey was pressed, or an activate_power action targeted it.
onHit(player, config, amount) Player took damage. Reaction hook. Skipped while the power’s condition fails.
onKill(player, config, killed) Player killed a LivingEntity. Skipped while the power’s condition fails.

PowerType also has overridable queries: isActivePower() / isActivePower(config), occupiesHotkeySlot(config), capabilities(config) / capabilities(player, config), and the mob-origin hooks appliesToMobs(config), applyToMob(...) and removeFromMob(...).

Idempotency note: onLogin and onRespawn default to invoking onGranted. If your implementation registers a listener or adds an attribute modifier in onGranted, make sure it’s safe to run multiple times. Give modifiers a fixed id and remove before adding to avoid stacking.


Origin data model (api/origin/)

Read-only data types representing loaded origin JSON.

Type Purpose
Origin One origin record (name, description, impact, icon, powers).
OriginLayer One picker layer (a List<ConditionedOrigin>, name, order). getAvailableOriginIds(...) resolves the ids.
Impact Enum: NONE, LOW, MEDIUM, HIGH.
OriginUpgrade Upgrade condition (vanilla advancement) that migrates one origin to another.
ConditionedOrigin Wraps an origin with a predicate determining availability.

NeoOriginsAPI has no “current origin” lookup: its methods are the seven listed above. A player’s chosen origin per layer lives in internal state (PlayerOriginData), so reading it today means importing internal classes. Open an issue if you need it promoted to the API.


Common integration patterns

“My mod adds a damage source; should I exempt minions of the victim?”

Yes. Before applying damage:

if (victim instanceof ServerPlayer sp
        && sourceEntity instanceof LivingEntity le
        && NeoOriginsAPI.summonerOf(le).filter(s -> s == sp).isPresent()) {
    return; // friendly fire from a summoner's own minion
}

“My mod has a tamed-pet concept; should I honour NeoOrigins’ tamer?”

If you’re iterating tamed mobs belonging to a player, include those tracked by NeoOrigins:

NeoOriginsAPI.forEachOfType(player, TameMobPower.class, cfg -> {
    // NeoOrigins tracks the mobs internally via MinionTracker —
    // use NeoOriginsAPI.isAnyMinion(entity) as the filter.
});

“I want to block a player with a specific origin from entering a region.”

Listen for OriginChangedEvent (it only covers picks made in the selection screen, see Events):

@SubscribeEvent
public static void onOriginChanged(OriginChangedEvent event) {
    if (event.getNewOrigin().equals(Identifier.parse("mypack:forbidden"))) {
        // kick / teleport / deny, or event.setCanceled(true) to refuse the pick
    }
}

Custom projectiles

For projectiles with behavior that can’t be expressed via spawn_projectile + on_hit_action (homing, chaining, trail effects, continuous in-flight ticks), subclass com.cyberday1.neoorigins.api.content.projectile.AbstractNeoProjectile and register an {@link net.minecraft.world.entity.EntityType}.

public class MySeekerProjectile extends AbstractNeoProjectile {
    public MySeekerProjectile(EntityType<? extends MySeekerProjectile> type, Level level) {
        super(type, level);
    }

    @Override
    protected Item getVisualItem() { return Items.ARROW; }

    @Override
    public void tick() {
        super.tick();
        if (this.level().isClientSide() || this.isRemoved()) return;
        // Your per-tick AI — seeking, trail particles, speed clamp, etc.
    }

    @Override
    protected void onImpact(ServerLevel level, HitResult result) {
        // Your impact behavior. The projectile is discarded automatically
        // after this method returns.
    }
}

Register the entity type during RegisterEvent / DeferredRegister, register the renderer in EntityRenderersEvent.RegisterRenderers. See HomingProjectile + ModEntities#HOMING_PROJECTILE in the NeoOrigins source for the canonical pattern.

Pack authors reference your entity by its registered ID from any spawn_projectile action:

{
  "type": "neoorigins:spawn_projectile",
  "entity_type": "yourmod:seeker",
  "speed": 1.2,
  "on_hit_action": { "type": "neoorigins:heal", "amount": 2 }
}

The on_hit_action still fires independently of your subclass’s onImpact: the DSL callback and the entity-class callback complement each other. The action runs with the shooter as its actor, so the heal above heals the player who fired, not the entity that was hit.

GeckoLib soft-dep

If you want custom-modeled / animated projectiles rather than item-textured ones, GeckoLib is a runtime-optional integration. NeoOrigins ships {@code com.cyberday1.neoorigins.compat.GeckoLibCompat#isLoaded()} for the presence probe. Gate any renderer that touches GeckoLib classes on that call and provide a {@link net.minecraft.client.renderer.entity.ThrownItemRenderer}-based fallback for the no-GeckoLib case. NeoOrigins itself ships no GeckoLib projectile renderer; its only GeckoLib use is the trigger_morph_animation action.


VFX entities (api/content/vfx/)

Non-moving visual-effect entities: lingering clouds, black holes, tornados, ground markers. NeoOrigins ships five reference subclasses (LingeringAreaEntity, BlackHoleVfxEntity, TornadoVfxEntity, ProjectileRainVfxEntity, TelegraphVfxEntity) and a base class + renderer stack for custom VFX.

Base class: AbstractVfxEntity

public class MyAuraEntity extends AbstractVfxEntity {
    public MyAuraEntity(EntityType<? extends MyAuraEntity> type, Level level) {
        super(type, level);
    }

    @Override
    protected void onVfxTick(ServerLevel level) {
        // Per-tick server-side behavior. Range, lifetime, caster, and
        // effect-type are already tracked by the base class.
        if (getLifetime() % 20 == 0) {
            // ...apply an effect to entities in getRange()
        }
        emitParticles(ParticleTypes.SOUL, 3, getRange() * 0.5, 0.1, getRange() * 0.5);
    }

    @Override
    protected void onExpire(ServerLevel level) {
        // Optional — fires once before discard when lifetime ends.
    }
}

Public API the base provides for free:

  • getRange() / setRange(float): synched to client
  • getEffectType() / setEffectType(String): synched color key
  • getLifetime() / getMaxLifetime() / setMaxLifetime(int) / getLifetimeProgress()
  • getCasterUuid() / setCaster(UUID) / resolveCaster()
  • emitParticles(ParticleOptions, count, xSpread, ySpread, zSpread)

The base handles tick(), lifetime countdown, expiry, saving lifetime, range, effect type and caster with the world, and overrides hurtServer() so your VFX can’t be killed by damage.

Procedural quad renderer: ProceduralQuadRenderer<T, S>

For projectile-style VFX (orbs, crossed billboards, pulsing glows), extend ProceduralQuadRenderer and implement the abstract hooks:

public class MyOrbRenderer extends ProceduralQuadRenderer<MyOrbEntity, MyOrbRenderState> {
    public MyOrbRenderer(EntityRendererProvider.Context ctx) { super(ctx); }

    // public, not protected: on 26.x these override EntityRenderer's public methods.
    @Override public MyOrbRenderState createRenderState() { return new MyOrbRenderState(); }

    @Override
    public void extractRenderState(MyOrbEntity entity, MyOrbRenderState state, float partialTick) {
        super.extractRenderState(entity, state, partialTick);
        AbstractVfxRenderState.extract(entity, state);
    }

    @Override protected RenderType renderType() { return MY_RENDER_TYPE; }

    // Optional animation tuning:
    @Override protected float coreYawPerTick() { return 25f; }
    @Override protected float glowPulseAmplitude() { return 0.12f; }
}

AbstractVfxRenderState.extract(entity, state) takes an AbstractVfxEntity, so MyOrbEntity must extend it; a projectile-based orb fills the state fields itself. RenderType here is net.minecraft.client.renderer.rendertype.RenderType. The subclass does not compile unchanged on 1.21.1, where the base extends EntityRenderer<T>: there it also needs getTextureLocation(T), the two render-state hooks are protected, and RenderType lives in net.minecraft.client.renderer. See CUSTOM_PROJECTILES.md for the three-tier extension guide.

Effect-type registry: VfxEffectTypes

Colour keys used by the built-in VFX. Register your own for other mods to reuse from JSON via the effect_type field on spawn_projectile / spawn_lingering_area:

VfxEffectTypes.register("yourmod:radiant", 255, 240, 200);

Then in a datapack:

{ "type": "neoorigins:spawn_projectile",
  "entity_type": "neoorigins:magic_orb",
  "effect_type": "yourmod:radiant" }

Bedrock-model loader: GeoJsonModel

Loads a Bedrock .geo.json model from the classpath, bakes vertex data once, and renders via a PoseStack + VertexConsumer. Face culling is occupancy-based (adjacent cubes hide touching faces). No bone animation: spin/transform models from the renderer directly.

private static final GeoJsonModel MODEL =
    GeoJsonModel.load("/assets/yourmod/geo/runestone.geo.json");

// In the renderer's submit(state, poseStack, collector, camera):
collector.submitCustomGeometry(poseStack, RENDER_TYPE, (pose, consumer) ->
    MODEL.render(pose, consumer, packedLight, OverlayTexture.NO_OVERLAY));

Graceful fallback to a unit cube if the model fails to load. The exception is logged but the renderer continues so a broken asset doesn’t crash the game.


What’s NOT in the API

The following commonly-requested things are intentionally internal. Open an issue if you need them elevated:

  • Picker UI payloads: the network protocol between client and server for origin selection. Bound to break as the UI evolves.
  • PowerHolder internals: the wrapper around power type + config + origin. You see it through powers() but shouldn’t mutate it.
  • Mixin targets: the internal mixin classes. Depending on them couples your mod to our injection points.
  • Capability cache internals: ActiveOriginService version numbers and dimension-scoped caches.

If you find yourself wanting to reach into these, we’ve probably missed a proper API surface. Please file an issue.