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.
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.
// For 26.1.x:
compileOnly "maven.modrinth:neo-origins:v2.0.25+26.1"
// For 1.21.1:
// compileOnly "maven.modrinth:neo-origins:v2.0.25+1.21.1"
}
Version format: Modrinth Maven uses the exact version string from the releases page. The format is
v{version}+{mc_version}(e.g.v2.0.25+26.1).
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 this player have the Aquatic origin's Water Breathing power?
if (NeoOriginsAPI.hasCapability(player, "water_breathing")) { ... }
// 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). |
summonerOf(entity) | Reverse-lookup a summoned minion’s owner. |
isMinionOf(entity, summoner) | Cheap check: is this entity summoner’s minion? |
isAnyMinion(entity) | Cheap check: is this entity anyone’s tracked minion? |
All methods are server-thread safe. Client-side use is read-only; mutating power state from the client is undefined.
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 = (ServerPlayer) event.getEntity();
Identifier newOrigin = event.getNewOrigin();
// React to the player picking a new origin.
}
}
| Event | When it fires |
|---|---|
OriginChangedEvent | Player’s origin changes. Cancellable — cancelling prevents the set. |
OriginsLoadedEvent | All origin JSONs finished parsing (datapack reload). |
PowerGrantedEvent | A power was just granted to a player. |
PowerRevokedEvent | A power was just revoked. |
All events carry the ServerPlayer via getEntity() plus event-specific identifiers. See the Javadoc on each event class.
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:
@SubscribeEvent
public static void onRegister(RegisterEvent event) {
event.register(NeoOriginsAPI.POWER_TYPE_REGISTRY_KEY,
helper -> helper.register(
ResourceLocation.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. 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. |
onHit(player, amount) | Player took damage. Reaction hook. |
Idempotency note:
onLoginandonRespawndefault to invokingonGranted. If your implementation registers a listener or adds an attribute modifier inonGranted, make sure it’s safe to run multiple times — use modifier UUIDs and removal-before-add to avoid stacking. Seefeedback_powertype_onGranted_idempotentfor the canonical pattern.
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 (list of origin IDs, name, order). |
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. |
Typical read:
Origin current = NeoOriginsAPI.currentOrigin(player, "neoorigins:origin");
if (current != null && current.impact() == Impact.HIGH) { ... }
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 or poll:
Origin o = NeoOriginsAPI.currentOrigin(player, myLayerId);
if (o != null && o.id().equals(Identifier.parse("mypack:forbidden"))) {
// kick / teleport / deny
}
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.
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. Full animated-projectile support is planned for 2.1.
VFX entities (api/content/vfx/)
Non-moving visual-effect entities — lingering clouds, black holes, tornados, ground markers. NeoOrigins ships three reference subclasses (LingeringAreaEntity, BlackHoleVfxEntity, TornadoVfxEntity) 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 clientgetEffectType()/setEffectType(String)— synched color keygetLifetime()/getMaxLifetime()/setMaxLifetime(int)getCasterUuid()/setCaster(UUID)/resolveCaster()emitParticles(ParticleOptions, count, xSpread, ySpread, zSpread)
The base handles tick(), lifetime countdown, expiry, and the hurt()/hurtServer() disable 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); }
@Override protected MyOrbRenderState createRenderState() { return new MyOrbRenderState(); }
@Override
protected void extractRenderState(MyOrbEntity entity, MyOrbRenderState state, float 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; }
}
The same subclass compiles unchanged on 1.21.1 and 26.1 — only the base class’s render-flow internals differ. 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:
MODEL.render(poseStack, buffer.getBuffer(RENDER_TYPE), 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 —
ActiveOriginServiceversion 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.