> For the complete documentation index, see [llms.txt](https://docs-nightbeam.gitbook.io/infinity-rifts/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-nightbeam.gitbook.io/infinity-rifts/developer-api.md).

# Developer API

InfinityRifts 2.0.1 exposes a stable integration API. The public contract is API version `1.2` (1.12.0 shipped `1.1`) and consists of the types in:

* `io.nightbeam.infinityrifts.api`
* `io.nightbeam.infinityrifts.api.event`

These packages are kept during release obfuscation. Everything under `gateway`, `infinity`, `mutator`, `stats`, `internal`, `model` or another implementation package is private and may change without notice.

API 1.2 is additive. Every 1.1 type, method, record component and enum constant is unchanged, and no existing enum gained constants. The new methods on `InfinityRiftsApi` are `default` methods, so plugins compiled against 1.1, and your own implementations or test doubles of the interface, keep compiling and linking.

## Add the API dependency

Build and install InfinityRifts first:

```
mvn clean install
```

Maven installs the unobfuscated API classifier in your local Maven repository:

```xml
<dependency>
  <groupId>io.nightbeam</groupId>
  <artifactId>InfinityRifts</artifactId>
  <version>2.0.1</version>
  <classifier>api</classifier>
  <scope>provided</scope>
</dependency>
```

Gradle:

```groovy
repositories { mavenLocal() }
dependencies { compileOnly 'io.nightbeam:InfinityRifts:2.0.1:api' }
```

Do not shade or relocate the API. The server's InfinityRifts plugin supplies it at runtime. Declare the runtime dependency in `plugin.yml`:

```yaml
depend: [InfinityRifts]
```

Use `softdepend` only when your plugin can run without InfinityRifts.

## Obtain the API service

InfinityRifts registers `InfinityRiftsApi` with Bukkit's `ServicesManager` during `onEnable` and unregisters it during shutdown.

```java
import io.nightbeam.infinityrifts.api.InfinityRiftsApi;
import io.nightbeam.infinityrifts.api.InfinityRiftsProvider;
import org.bukkit.plugin.java.JavaPlugin;

public final class ExamplePlugin extends JavaPlugin {
    private InfinityRiftsApi infinityRifts;

    @Override
    public void onEnable() {
        infinityRifts = InfinityRiftsProvider.require();
        getLogger().info("InfinityRifts " + infinityRifts.getPluginVersion()
                + ", API " + infinityRifts.getApiVersion());
    }
}
```

Use `InfinityRiftsProvider.get()` for an optional integration:

```java
InfinityRiftsProvider.get().ifPresent(api ->
        getLogger().info("Connected to InfinityRifts " + api.getPluginVersion()));
```

`require()` throws `IllegalStateException` if the service is unavailable. Do not retain the service across a plugin disable/reload boundary; obtain it again after the dependency is enabled.

To use the 1.2 additions while still supporting older InfinityRifts servers, check the contract version first. On an older server the 1.2 types and methods do not exist, and using them throws `NoClassDefFoundError` or `NoSuchMethodError`:

```java
String[] version = api.getApiVersion().split("\\.");
boolean api12 = Integer.parseInt(version[0]) > 1
        || (Integer.parseInt(version[0]) == 1 && Integer.parseInt(version[1]) >= 2);
```

Keep the code that touches `GatewayRunMode`, `GatewayRunInfo` and the other 1.2 types in a separate class that is loaded only when `api12` is true.

## API surface

`InfinityRiftsApi` exposes the following methods:

| Method                                                               | Since plugin | Purpose                                                                                                          |
| -------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------- |
| `getApiVersion()`                                                    | 1.10.0       | Returns the API contract version, currently `1.2`. Also available as the constant `InfinityRiftsApi.API_VERSION` |
| `getPluginVersion()`                                                 | 1.10.0       | Returns the running plugin version from `plugin.yml`                                                             |
| `getGateways()`                                                      | 1.10.0       | Returns every loaded `GatewayDefinition`, sorted by gateway ID                                                   |
| `getGateway(String gatewayId)`                                       | 1.10.0       | Looks up one loaded gateway definition                                                                           |
| `getActiveSessions()`                                                | 1.10.0       | Returns immutable snapshots of all active sessions                                                               |
| `getSession(UUID playerId)`                                          | 1.10.0       | Finds the session containing an owner or participant                                                             |
| `getSessionById(UUID sessionId)`                                     | 1.10.0       | Finds a session by its runtime session UUID                                                                      |
| `startGateway(Player player, String gatewayId)`                      | 1.10.0       | Starts a Classic run of a configured gateway for a player                                                        |
| `endGateway(UUID sessionId, GatewayEndReason reason)`                | 1.10.0       | Requests cleanup for an active session                                                                           |
| `getActiveWaveEntities()`                                            | 1.11.0       | Returns every alive tracked wave mob across active sessions                                                      |
| `getActiveWaveEntities(UUID sessionId)`                              | 1.11.0       | Returns alive wave mobs for one session                                                                          |
| `getActiveWaveEntities(GatewaySession session)`                      | 1.11.0       | Same as session ID lookup using a snapshot                                                                       |
| `getActiveWaveEntities(GatewaySession session, int wave)`            | 1.11.0       | Returns alive mobs for a specific wave                                                                           |
| `getWaveMob(UUID entityUuid)`                                        | 1.11.0       | Looks up a tracked mob snapshot, including terminal states                                                       |
| `isWaveMob(Entity entity)`                                           | 1.11.0       | Whether the entity is or was tracked as a gateway wave mob                                                       |
| `getSessionByEntity(Entity entity)`                                  | 1.11.0       | Finds the active session owning a tracked mob                                                                    |
| `getWaveByEntity(Entity entity)`                                     | 1.11.0       | Finds the wave number for a tracked mob                                                                          |
| `startGateway(Player player, String gatewayId, GatewayRunMode mode)` | 2.0.0        | Starts a Classic or Infinity run                                                                                 |
| `getRunInfo(UUID sessionId)`                                         | 2.0.0        | Mode, mutators, depth, milestone, decision and vault state of an active session                                  |
| `getMutators()`                                                      | 2.0.0        | Every mutator loaded from `mutators.yml`, enabled or not, in file order                                          |
| `getPlayerStats(UUID playerId)`                                      | 2.0.0        | A player's Rift Records                                                                                          |
| `getServerRecordDepth(String gatewayId)`                             | 2.0.0        | Deepest Infinity wave cleared on a gateway by any player; `0` when none                                          |

Lookups return `Optional.empty()` when there is no match. Collections are immutable snapshots and are not live views; query the API again for current state.

The interface defaults for the 1.2 methods are fallbacks for other implementations: `startGateway(..., mode)` delegates Classic requests to the two-argument method and returns `REJECTED` for Infinity, and the other defaults return empty results. The implementation registered by InfinityRifts overrides all of them.

## Gateway definitions

`GatewayDefinition` is an immutable record describing loaded configuration:

| Component              | Meaning                                                       |
| ---------------------- | ------------------------------------------------------------- |
| `id()`                 | Exact configured gateway ID.                                  |
| `displayName()`        | Configured MiniMessage-formatted display name.                |
| `description()`        | Configured description.                                       |
| `maxWaves()`           | Highest authored wave number. Infinity runs continue past it. |
| `maxPlayers()`         | Participant limit.                                            |
| `maxTimeSeconds()`     | Classic overall time limit (`settings.max-time-seconds`).     |
| `randomSpawnEnabled()` | Whether scheduled random spawning is enabled.                 |

`GatewayDefinition` has no Infinity fields in API 1.2. Whether a gateway accepts Infinity runs shows only through `startGateway(..., GatewayRunMode.INFINITY)` returning `REJECTED`.

```java
api.getGateway("fire_gate").ifPresent(definition -> {
    getLogger().info(definition.displayName());
    getLogger().info("Waves: " + definition.maxWaves()
            + ", players: " + definition.maxPlayers());
});
```

## Session snapshots

`GatewaySession` is an immutable point-in-time record:

| Component                 | Meaning                                                                                                          |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `sessionId()`             | Unique runtime session UUID; use this with `endGateway` and `getRunInfo`.                                        |
| `gatewayId()`             | Configured gateway ID.                                                                                           |
| `ownerId()`               | Owner UUID. Ownerless sessions use an internal synthetic UUID.                                                   |
| `participantIds()`        | Immutable set of current participant UUIDs. Ownerless random rifts have none.                                    |
| `state()`                 | Current `GatewaySessionState`.                                                                                   |
| `currentWave()`           | Current wave; `0` before the first wave. In Infinity runs it can exceed `maxWaves()`.                            |
| `maxWaves()`              | Highest authored wave number, in both modes.                                                                     |
| `mobsRemaining()`         | Currently tracked mob count.                                                                                     |
| `totalSecondsRemaining()` | Overall time remaining, or `-1` for an Infinity run without an overall limit (`infinity.time-limit-seconds: 0`). |
| `waveSecondsRemaining()`  | Current wave time remaining.                                                                                     |
| `competitive()`           | Whether the session is open/competitive.                                                                         |
| `location()`              | Immutable `GatewayLocation` snapshot.                                                                            |

`GatewayLocation` contains `world()`, `x()`, `y()` and `z()`. It deliberately does not expose a mutable Bukkit `Location`.

```java
api.getSession(player.getUniqueId()).ifPresent(session -> {
    if (session.state() == GatewaySessionState.IN_WAVE) {
        getLogger().info(session.gatewayId() + " wave " + session.currentWave()
                + " has " + session.mobsRemaining() + " mobs remaining");
    }
});
```

### Session states

`GatewaySessionState` values are:

* `INITIALIZING`: runtime objects and timers are being prepared.
* `WAITING`: the session is active before its first wave.
* `IN_WAVE`: a combat wave is active.
* `WAVE_TRANSITION`: a completed wave is transitioning to the next wave. An Infinity run's Extract-or-Delve window is also reported as `WAVE_TRANSITION`; use `GatewayRunInfo#awaitingDecision()` to tell them apart.
* `COMPLETING`: the run is ending successfully (Classic completion or Infinity extraction) and rewards are being paid.
* `FAILED`: the session has entered its failure path.
* `ENDING`: cleanup is in progress.

## Run mode, run info, mutators and records (API 1.2)

`GatewayRunMode` is `CLASSIC` or `INFINITY`.

`GatewayRunInfo` is an immutable snapshot returned by `getRunInfo(sessionId)`:

| Component                                   | Meaning                                                                                                                                                |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sessionId()`                               | Session UUID.                                                                                                                                          |
| `mode()`                                    | `CLASSIC` or `INFINITY`. Ownerless random rifts are always `CLASSIC`.                                                                                  |
| `mutators()`                                | Active `RiftMutator`s in the order they were added, including a gateway's `FIXED` mutators.                                                            |
| `highestClearedWave()`                      | Deepest wave cleared so far (the run's depth).                                                                                                         |
| `nextMilestone()`                           | Next Infinity milestone wave, counting the wave in progress. While a decision is open it is the milestone after the one just reached. `0` for Classic. |
| `awaitingDecision()`                        | `true` while an Infinity Extract-or-Delve window is open.                                                                                              |
| `decisionSecondsRemaining()`                | Seconds left in the open window, otherwise `0`.                                                                                                        |
| `vaultXp()`, `vaultMoney()`, `vaultItems()` | Rift Vault contents: XP, money and total item count. Classic runs have an empty vault.                                                                 |
| `elapsedSeconds()`                          | Run time so far. Server downtime is excluded for restored runs.                                                                                        |

`RiftMutator` is an immutable view of a mutator: `id()`, `displayName()` (MiniMessage), `description()`, `rewardBonus()` (vault bonus while active, `0.15` = +15 %), `minWave()` and `enabled()`.

`PlayerRiftStats` is an immutable snapshot of `stats.yml`: `playerId()`, `name()`, `runs()`, `clears()` (Classic completions), `failures()` (Classic and Infinity collapses), `extractions()`, `kills()`, `bossKills()`, `wavesCleared()` and `bestDepth()` (deepest Infinity wave cleared on any gateway). `getPlayerStats` returns empty when the player has no record. Setting `records.enabled: false` stops new recording; records that already exist are still returned.

```java
api.getSession(player.getUniqueId())
        .flatMap(session -> api.getRunInfo(session.sessionId()))
        .ifPresent(run -> {
            if (run.mode() == GatewayRunMode.INFINITY) {
                getLogger().info("Depth " + run.highestClearedWave()
                        + ", vault " + run.vaultXp() + " XP, "
                        + run.mutators().size() + " mutators");
            }
        });

int record = api.getServerRecordDepth("void_gate");
```

## Starting and ending sessions

`startGateway` is a trusted operation. It does not check Bukkit permissions, item keys, spawn conditions, commands or your plugin's authorization rules. Enforce authorization before calling it. Like `/rift start`, it charges the gateway's `settings.cost` to the player when it is above 0, pulls in the player's pending party, and starts at the player's location.

```java
GatewayStartResult result = api.startGateway(player, "fire_gate", GatewayRunMode.INFINITY);

switch (result) {
    case STARTED -> player.sendMessage("Gateway started.");
    case PLAYER_ALREADY_ACTIVE -> player.sendMessage("You are already in a gateway.");
    case UNKNOWN_GATEWAY -> player.sendMessage("That gateway is not configured.");
    case REJECTED -> player.sendMessage("The gateway could not be started.");
    default -> player.sendMessage("Unexpected result: " + result);
}
```

`GatewayStartResult` means:

* `STARTED`: a new session was created.
* `PLAYER_ALREADY_ACTIVE`: the player already belongs to an active session.
* `UNKNOWN_GATEWAY`: no loaded definition matches the requested ID.
* `REJECTED`: initial validation passed but the start was refused, for example Infinity requested on a gateway with `infinity.enabled: false`, or an entry cost that could not be charged (the player receives the `economy.*` message).

`startGateway(player, gatewayId)` without a mode starts a Classic run, as in 1.x.

To stop a session, pass its `sessionId()` and a `GatewayEndReason`:

```java
api.getSession(player.getUniqueId()).ifPresent(session ->
        api.endGateway(session.sessionId(), GatewayEndReason.ADMIN_STOP));
```

`endGateway` returns `true` when an active session was found and cleanup was requested, otherwise `false`. It does not throw when the session is already absent. `GatewayEndReason` values are `COMPLETED`, `FAILED`, `PLAYER_QUIT`, `PLAYER_DIED`, `PLAYER_LEAVE`, `ADMIN_STOP`, `SHUTDOWN` and `TASK_FAILURE`. Use `ADMIN_STOP` for an intentional external stop. Ending a session this way runs cleanup only: no completion rewards, vault payout or collapse share are paid. Do not use internal implementation enums.

## Wave mob access

Use `InfinityRiftsProvider.require()` (or `get()`) to obtain the API, then query tracked mobs as immutable snapshots. Collections are not live views.

```java
InfinityRiftsApi api = InfinityRiftsProvider.require();

api.getSession(player.getUniqueId()).ifPresent(session -> {
    Collection<LivingEntity> entities = api.getActiveWaveEntities(session);
    getLogger().info("Wave " + session.currentWave() + " has " + entities.size() + " alive mobs");
});

api.getWaveMob(entity.getUniqueId()).ifPresent(waveMob -> {
    getLogger().info(waveMob.getMobId() + " state=" + waveMob.getState());
});

api.getSessionByEntity(entity).ifPresent(session ->
        getLogger().info(entity.getUniqueId() + " belongs to " + session.gatewayId()));
```

`WaveMob` exposes `getUniqueId()`, `getEntity()` (empty when invalid/dead/unloaded), `getMobId()`, `getEntityType()`, `getWave()`, `getSession()`, `isAlive()`, `getState()` and `getSpawnTimeMillis()`. `WaveMobState` values are `SPAWNING`, `ALIVE`, `DEAD`, `REMOVED` and `UNLOADED`. Mobs of generated Infinity waves are tracked like authored ones; their mob id is the template mob's configured type.

Nullability: API arguments reject `null`. `getEntity()` on `WaveMob` and `GatewayWaveMobRemoveEvent` never throws for dead or invalid entities; they return `Optional.empty()`. `getKiller()` and `getDamageCause()` on death events may be `null` when unavailable.

## Bukkit lifecycle events

The API publishes synchronous Bukkit notification events. They are not cancellable. Session data on each event is an immutable snapshot.

| Event                                | Fired                                                                                                                                        | Accessors                                                                                                                  |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `GatewayStartedEvent`                | After a new session is registered and started, including ownerless random gateways.                                                          | `getSession()`                                                                                                             |
| `GatewayWaveStartedEvent`            | After a session enters a wave and before that wave's mobs are spawned.                                                                       | `getSession()`, `getWave()`                                                                                                |
| `GatewayWaveMobDeathEvent`           | Once per wave mob when it dies naturally.                                                                                                    | session, wave, entity, mob id, killer, damage cause, remaining count, wave-complete flag                                   |
| `GatewayWaveMobRemoveEvent`          | Once per wave mob removed without a natural death.                                                                                           | session, wave, entity id, mob id, remove reason, remaining count                                                           |
| `GatewayWaveCompletedEvent`          | Once when a wave is complete after all its mobs are gone.                                                                                    | session, wave, spawn/kill/remove counts, duration, reason, next wave                                                       |
| `GatewayMilestoneReachedEvent` (1.2) | After an Infinity milestone wave is cleared: the milestone bundle is banked, new mutators are rolled and the decision window opens.          | `getSession()`, `getWave()`, `getNewMutators()`, `getActiveMutators()`                                                     |
| `GatewayExtractedEvent` (1.2)        | When an Infinity run extracts (chosen, by decision timeout, or at `infinity.max-wave`), after the vault payout is issued and before cleanup. | `getSession()`, `getDepth()`, `getMutators()`, `getVaultXp()`, `getVaultMoney()`, `getVaultItems()`, `isMaxDepthReached()` |
| `GatewayRestoredEvent`               | After a persisted player-owned session resumes.                                                                                              | `getSession()`                                                                                                             |
| `GatewayEndedEvent`                  | After session cleanup is complete.                                                                                                           | `getSession()`, `getReason()`                                                                                              |

Death and remove events are mutually exclusive for the same mob: a natural death publishes only `GatewayWaveMobDeathEvent`. Plugin cleanup publishes `GatewayWaveMobRemoveEvent` with `PLUGIN_CLEANUP` or `RIFT_STOPPED`, not a natural death.

Infinity runs end in one of two ways:

* **Extraction**: `GatewayExtractedEvent`, then `GatewayEndedEvent` with reason `COMPLETED`.
* **Collapse**: `GatewayEndedEvent` with reason `FAILED`.

`GatewayWaveCompletedEvent#getNextWave()` is empty only on a run's final wave: the last authored wave in Classic, or `infinity.max-wave` in Infinity. The vault values on `GatewayExtractedEvent` are what each online participant received; `getVaultMoney()` is deposited only when an economy is available. Payout of XP, money and items is scheduled, so it can complete a tick after the event.

```java
import io.nightbeam.infinityrifts.api.event.GatewayExtractedEvent;
import io.nightbeam.infinityrifts.api.event.GatewayMilestoneReachedEvent;
import io.nightbeam.infinityrifts.api.event.GatewayWaveCompletedEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

public final class InfinityListener implements Listener {
    @EventHandler
    public void onWaveComplete(GatewayWaveCompletedEvent event) {
        getLogger().info("Wave " + event.getWave() + " completed with "
                + event.getKilledMobCount() + " mobs killed.");
    }

    @EventHandler
    public void onMilestone(GatewayMilestoneReachedEvent event) {
        event.getNewMutators().forEach(mutator ->
                getLogger().info("New mutator " + mutator.id() + " at wave " + event.getWave()));
    }

    @EventHandler
    public void onExtract(GatewayExtractedEvent event) {
        getLogger().info(event.getSession().gatewayId() + " extracted at depth " + event.getDepth()
                + (event.isMaxDepthReached() ? " (max depth)" : ""));
    }
}
```

## Nulls, threading, and Folia

All public method arguments and required record components reject `null` with `NullPointerException`. Use `Optional` for lookup misses and structured result enums for start outcomes.

Call `startGateway` on the player's owning thread: the main server thread on Bukkit/Paper or the player's entity scheduler on Folia. Call `endGateway` from an appropriate server-owned synchronous or region thread. Do not call mutating methods from arbitrary asynchronous tasks. The read methods (`getRunInfo`, `getMutators`, `getPlayerStats`, `getServerRecordDepth`) return snapshots and do not touch the world.

Events are synchronous Bukkit events (`Event#isAsynchronous()` is `false`). On Folia, mob death and remove events run in the region/entity context that handled the lifecycle change. Wave completion and milestone events are published from the gateway session tick or the mob lifecycle path. Extraction is published from the gateway's scheduled tasks or from the thread that ran `/rift extract`. Schedule unrelated world or entity work onto the correct Folia owner.

## Compatibility policy

The plugin version (`2.0.1`) and API contract version (`1.2`) are independent. Within API major version 1:

* Existing public types, methods, record components and enum constants are not intentionally removed or obfuscated.
* New methods, records, events and enum constants may be added in minor releases. New interface methods are added as `default` methods.
* Include a `default` branch when switching on public enums if forward compatibility matters.
* Implementation packages have no binary or source compatibility guarantee.

The API classifier is `target/InfinityRifts-2.0.1-api.jar`; the public packages are also preserved in the production obfuscated JAR.
