> 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 1.11.0 exposes a stable, read-oriented integration API. The public contract is API version `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`, `internal`, `model`, or another implementation package is private and may change without notice.

## 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>1.11.0</version>
  <classifier>api</classifier>
  <scope>provided</scope>
</dependency>
```

Gradle:

```groovy
repositories { mavenLocal() }
dependencies { compileOnly 'io.nightbeam:InfinityRifts:1.11.0: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.

## API surface

`InfinityRiftsApi` exposes the following methods:

| Method                                                    | Purpose                                                         |
| --------------------------------------------------------- | --------------------------------------------------------------- |
| `getApiVersion()`                                         | Returns the API contract version, currently `1.1`.              |
| `getPluginVersion()`                                      | Returns the running plugin version from `plugin.yml`.           |
| `getGateways()`                                           | Returns every loaded `GatewayDefinition`, sorted by gateway ID. |
| `getGateway(String gatewayId)`                            | Looks up one loaded gateway definition.                         |
| `getActiveSessions()`                                     | Returns immutable snapshots of all active sessions.             |
| `getSession(UUID playerId)`                               | Finds the session containing an owner or participant.           |
| `getSessionById(UUID sessionId)`                          | Finds a session by its runtime session UUID.                    |
| `startGateway(Player player, String gatewayId)`           | Starts a configured gateway for a player.                       |
| `endGateway(UUID sessionId, GatewayEndReason reason)`     | Requests cleanup for an active session.                         |
| `getActiveWaveEntities()`                                 | Returns every alive tracked wave mob across active sessions.    |
| `getActiveWaveEntities(UUID sessionId)`                   | Returns alive wave mobs for one session.                        |
| `getActiveWaveEntities(GatewaySession session)`           | Same as session ID lookup using a snapshot.                     |
| `getActiveWaveEntities(GatewaySession session, int wave)` | Returns alive mobs for a specific wave.                         |
| `getWaveMob(UUID entityUuid)`                             | Looks up a tracked mob snapshot, including terminal states.     |
| `isWaveMob(Entity entity)`                                | Whether the entity is tracked as a gateway wave mob.            |
| `getSessionByEntity(Entity entity)`                       | Finds the active session owning a tracked mob.                  |
| `getWaveByEntity(Entity entity)`                          | Finds the wave number for a tracked mob.                        |

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.

## 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 configured wave number.                |
| `maxPlayers()`         | Participant limit.                             |
| `maxTimeSeconds()`     | Overall session time limit.                    |
| `randomSpawnEnabled()` | Whether scheduled random spawning is enabled.  |

Example:

```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`.       |
| `gatewayId()`             | Configured gateway ID.                                         |
| `ownerId()`               | Owner UUID. Ownerless sessions use an internal synthetic UUID. |
| `participantIds()`        | Immutable set of current participant UUIDs.                    |
| `state()`                 | Current `GatewaySessionState`.                                 |
| `currentWave()`           | Current wave; `0` before the first wave.                       |
| `maxWaves()`              | Highest configured wave number.                                |
| `mobsRemaining()`         | Currently tracked mob count.                                   |
| `totalSecondsRemaining()` | Overall session time remaining.                                |
| `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 configured combat wave is active.
* `WAVE_TRANSITION` — a completed wave is transitioning to the next wave.
* `COMPLETING` — all waves are complete and rewards are being processed.
* `FAILED` — the session has entered its failure path.
* `ENDING` — cleanup is in progress.

## Starting and ending sessions

`startGateway` is a trusted operation. It does not check Bukkit permissions, item keys, commands, or your plugin's authorization rules. Enforce authorization before calling it.

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

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.");
}
```

`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 runtime startup failed.

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; 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`.

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 unless noted otherwise. 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 considered complete after required mobs are gone.               | session, wave, spawn/kill/remove counts, duration, reason, next wave                     |
| `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: natural deaths publish only `GatewayWaveMobRemoveEvent` is **not** fired for them. Plugin cleanup publishes `GatewayWaveMobRemoveEvent` with `PLUGIN_CLEANUP` or `RIFT_STOPPED`, not a natural death.

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

public final class WaveMobListener implements Listener {
    @EventHandler
    public void onWaveMobDeath(GatewayWaveMobDeathEvent event) {
        getLogger().info(
                "Wave mob " + event.getEntityId()
                        + " died in wave " + event.getWave()
                        + ". Remaining: " + event.getRemainingMobCount());
    }

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

## 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.

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 (the same thread that processed the death or removal). Wave completion is published from the gateway session tick or mob lifecycle path on that gateway's region context. Schedule unrelated world or entity work onto the correct Folia owner.

## Compatibility policy

The plugin version (`1.11.0`) and API contract version (`1.1`) 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.
* 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-1.11.0-api.jar`; the public package is also preserved in the production obfuscated JAR.
