> For the complete documentation index, see [llms.txt](https://wiki.halosdev.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wiki.halosdev.com/halos-player-spoofer/api.md).

# API

## API

One cross platform API for Spigot, BungeeCord and Velocity, with a small platform module on top of each.

Full reference: [spoofer-docs.halosdev.com](https://spoofer-docs.halosdev.com/). When this page and the Javadoc disagree, trust the Javadoc.

### Requirements

* Java 17
* Your plugin must load after HalosPlayerSpoofer

### Adding the dependency

Group `com.halosdev.spoofer`, published at [maven.halosdev.com](https://maven.halosdev.com/#/releases).

| Artifact       | Use it when              |
| -------------- | ------------------------ |
| `api-core`     | Cross platform code only |
| `api-spigot`   | Spigot or Paper          |
| `api-bungee`   | BungeeCord or Waterfall  |
| `api-velocity` | Velocity                 |

Declare one. The platform artifacts pull in `api-core` transitively and add that platform's `Account`, its events and the deprecated pre 3.5 API.

#### Gradle (Kotlin DSL)

```kotlin
repositories {
    maven("https://maven.halosdev.com/releases")
}

dependencies {
    compileOnly("com.halosdev.spoofer:api-spigot:3.5.0")
}
```

#### Gradle (Groovy)

```groovy
repositories {
    maven { url 'https://maven.halosdev.com/releases' }
}

dependencies {
    compileOnly 'com.halosdev.spoofer:api-spigot:3.5.0'
}
```

#### Maven

```xml
<repositories>
    <repository>
        <id>halosdev</id>
        <url>https://maven.halosdev.com/releases</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.halosdev.spoofer</groupId>
        <artifactId>api-spigot</artifactId>
        <version>3.5.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
```

Never shade these into your jar. The plugin provides them at runtime.

#### Load order

Spigot and BungeeCord:

```yaml
softdepend:
  - HalosPlayerSpoofer
```

Velocity: declare it in your `@Plugin` annotation dependencies.

Server owners can rename the plugin, so a hard depend is not always reliable. `HalosSpooferProvider.isLoaded()` always is.

### Getting the API

```java
import com.halos.spoofer.api.HalosSpoofer;
import com.halos.spoofer.api.HalosSpooferProvider;

if (!HalosSpooferProvider.isLoaded()) {
    getLogger().warning("HalosPlayerSpoofer is not installed, spoofer features disabled.");
    return;
}

HalosSpoofer spoofer = HalosSpooferProvider.get();
```

`get()` throws `IllegalStateException` before the plugin enables, so call it from `onEnable` or later, never a static initializer or `onLoad`.

On Spigot it is also in Bukkit's `ServicesManager`:

```java
RegisteredServiceProvider<HalosSpoofer> provider =
        getServer().getServicesManager().getRegistration(HalosSpoofer.class);

HalosSpoofer spoofer = provider.getProvider();
```

### Threading

Read this first.

* Methods returning `CompletableFuture` may hit the network sync layer. Never `.join()` or `.get()` on the main thread.
* Everything else answers from memory and is main thread safe.
* Futures and event bus listeners run on internal threads, never the main thread.
* Hop back with your scheduler before touching worlds, entities or anything else that is not thread safe.

```java
spoofer.players().all().thenAccept(players ->
        getServer().getScheduler().runTask(this, () -> {
            // safe to touch Bukkit here
        }));
```

### Environment and capabilities

```java
Environment env = spoofer.environment();

env.platform();          // SPIGOT, BUNGEE or VELOCITY
env.proxy();             // true on a proxy
env.networked();         // true when part of a proxied network
env.localServerName();   // Optional, empty on a proxy
env.serverNames();       // every server the proxy knows, or just this one
env.pluginVersion();
```

Server names are always lowercase, and the API matches them case insensitively.

Some features are backend only. Check first:

```java
if (spoofer.supports(Capability.FAKE_PLAYER_DETAILS)) {
    // expiresAt(), chatter() and metadata() are available here
}
```

Unsupported operations throw `UnsupportedOperationException`. `FAKE_PLAYER_DETAILS` works on backend servers, not proxies.

### Accounts

An `Account` is a UUID and a username. Most of the API takes one.

```java
AccountService accounts = spoofer.accounts();

accounts.mode();   // ONLINE or OFFLINE
```

`ONLINE` resolves against Mojang data, so only real accounts can be spoofed. `OFFLINE` derives the UUID from the username, so anything goes.

```java
// may perform a cached web request, so it returns a future
accounts.byUsername("Notch").thenCompose(account -> spoofer.players().add(account, "lobby"));

accounts.byUuid(uuid);

// no lookup, you vouch for the pair
Account account = accounts.of(uuid, "Notch");
```

Lookups fail with `AccountNotFoundException`. In online mode the username that comes back can differ in case or entirely after a name change, so use what the service returns.

Each platform module has its own `Account` implementing the core interface. It is what the platform events give you and it goes straight into any API method.

```java
// Spigot
Account account = com.halos.spoofer.api.spigot.account.Account.from(offlinePlayer);
```

### Fake players

#### Querying

Local means hosted by this instance: on a backend the players on that server, on a proxy the ones connected through it. These never block.

```java
FakePlayerService players = spoofer.players();

players.isFakePlayer(uuid);        // boolean
players.local(uuid);               // Optional<LocalFakePlayer>
players.local();                   // Collection<LocalFakePlayer>
```

Network wide queries return futures.

```java
players.all();                     // every fake player on the network
players.onServer("lobby");         // every fake player on one server
players.find(uuid);                // Optional<FakePlayer>, anywhere
players.find(account);
```

`FakePlayer` gives you `account()` and `server()`. `LocalFakePlayer` adds `joinedAt()`, plus `expiresAt()`, `chatter()` and `metadata()` on a backend server.

#### Adding, moving and removing

```java
players.add(account, "lobby");     // join a specific server
players.add(account);              // join this server, fails on a proxy
players.move(account, "survival"); // leave and rejoin elsewhere
players.remove(account);           // remove from wherever it is
players.remove(uuid);
players.clear("lobby");            // remove everything on one server
```

`add` and `move` complete with the resulting `FakePlayer` once it has joined. `remove` and `clear` complete once the players are gone.

```java
spoofer.accounts().byUsername("Notch")
        .thenCompose(account -> spoofer.players().add(account, "lobby"))
        .thenAccept(player -> getLogger().info(player.account().username() + " joined " + player.server()))
        .exceptionally(throwable -> {
            FakePlayerException.from(throwable).ifPresent(failure ->
                    getLogger().warning("Could not add fake player: " + failure.reason()));
            return null;
        });
```

#### Handling failures

Requests fail with a `FakePlayerException`. Futures wrap it, so unwrap with the static helper instead of casting:

```java
FakePlayerException.from(throwable).ifPresent(failure -> {
    FailureReason reason = failure.reason();
    Optional<String> detail = failure.detail();
});
```

| Reason                    | Meaning                                                           |
| ------------------------- | ----------------------------------------------------------------- |
| `NOT_WHITELISTED`         | The account is not on the server whitelist                        |
| `ALREADY_ONLINE`          | That account is already online somewhere                          |
| `NOT_ON_SERVER`           | No fake player to remove, move or send chat for                   |
| `UNKNOWN_SERVER`          | Server is not known, or you called the no server `add` on a proxy |
| `SERVER_FULL`             | Target server is at its player limit                              |
| `BANNED`                  | The account is banned                                             |
| `LOGIN_DENIED`            | The target server rejected the login                              |
| `ACCOUNT_NOT_FOUND`       | The account could not be resolved                                 |
| `TEXTURE_NOT_FOUND`       | No skin data could be resolved                                    |
| `BACKEND_NOT_READY`       | Target server is up but not ready for the request                 |
| `REQUEST_ALREADY_PENDING` | The same request is already in flight                             |
| `SERVER_UNREACHABLE`      | The target server did not answer in time                          |
| `INTERNAL_ERROR`          | Anything else, check the console                                  |

`detail()` carries extra context when the failing side gave any. Log it, do not branch on it.

### Metadata

Key value data that survives rejoins, depending on the server's configured metadata storage. It lives on the backend, so it needs `FAKE_PLAYER_DETAILS`.

```java
spoofer.players().local(uuid).ifPresent(player -> {
    FakePlayerMetadata meta = player.metadata();

    meta.put("myplugin:rank", "vip");
    meta.put("myplugin:kills", 12);

    String rank = meta.getOrDefault("myplugin:rank", "default");
    boolean seen = meta.has("myplugin:visited");

    meta.remove("myplugin:kills");
});
```

Values must be JSON friendly: strings, numbers, booleans, and lists or maps of those. `get` throws `ClassCastException` on a type mismatch. Namespace your keys, the plugin's own modules share this store.

### Chat

```java
spoofer.chat().chat(account, "hello world")
        .exceptionally(throwable -> {
            // NOT_ON_SERVER when that fake player is not online
            return null;
        });
```

The message is sent on whichever server the fake player is on, and calls from a proxy or another backend are forwarded for you. The future completes when the request is handled, not when the message appears.

### Fluctuation

Fluctuation joins and removes fake players on its own to chase a moving target count. It runs on the proxy in a network, or on the server itself when standalone, and exposes the same state `/spoofer fluctuation` shows.

```java
FluctuationService fluctuation = spoofer.fluctuation();

if (!fluctuation.active()) {
    return; // not enabled, or not run on this instance
}

for (FluctuationServer server : fluctuation.servers()) {
    FluctuationTargets targets = server.targets();

    getLogger().info(server.name()
            + " target " + targets.total()
            + " (fixed " + targets.fixed()
            + ", dynamic " + targets.dynamic()
            + ", manual " + targets.manual() + ")"
            + " real " + server.realPlayerCount()
            + " total " + server.totalPlayerCount());
}
```

`total` is `fixed + dynamic + manual`, clamped to the configured bounds. `fixed` is a slowly random walking base, `dynamic` is `ceil(realMultiplier * realPlayerCount)`, `manual` is what operators or your plugin set.

```java
fluctuation.server("lobby").ifPresent(server -> {
    server.addManualDelta(5);   // five above what fluctuation would pick
    server.manualDelta(0);      // back to neutral

    server.suspend();           // target drops to zero and its fake players leave
    server.resume();
});
```

The manual delta is clamped to the configured maximum. `players()` returns the fake players fluctuation owns there, each with `joinedAt()` and the `leavesAt()` moment it intends to pull them out.

### Events

#### Cross platform event bus

Fires on every instance on the network, including ones that had nothing to do with the change.

```java
EventSubscription subscription = spoofer.events().subscribe(FakePlayerAddedEvent.class, event ->
        getLogger().info(event.player().account().username() + " joined " + event.player().server()));

// later, for example in onDisable
subscription.unsubscribe();
```

| Event                    | Fires when                                            |
| ------------------------ | ----------------------------------------------------- |
| `FakePlayerAddedEvent`   | A fake player joined a server anywhere on the network |
| `FakePlayerRemovedEvent` | A fake player left a server anywhere on the network   |

Both give you a `FakePlayer`, on the removed event as it was, including the server it left. Subscribing to `SpooferEvent` gives you everything, and subtypes reach supertype listeners.

Unsubscribe when your plugin disables. Listeners run on an internal thread and must not block.

#### Platform events

Each platform module fires native events for fake players on that instance, handing you an `Account` you can pass into any API method.

```java
@EventHandler
public void onFakePlayerCreated(FakePlayerCreatedEvent event) {
    Account account = event.getAccount();
    Player player = event.getPlayer();
}
```

`FakePlayerCreatedEvent` and `FakePlayerDestroyEvent` exist on Spigot, BungeeCord and Velocity, each in that platform's `event` package with that platform's player type. On Spigot they can fire off the main thread, which the async flag reflects.

### The deprecated SpooferAPI classes

`SpigotSpooferAPI`, `BungeeSpooferAPI` and `VelocitySpooferAPI` still work and forward to the API above, so existing plugins keep running. They log a warning on first use and their synchronous queries block on the network sync layer.

| Old                                     | New                                                                 |
| --------------------------------------- | ------------------------------------------------------------------- |
| `SpigotSpooferAPI.get()`                | `HalosSpooferProvider.get()`                                        |
| `isFakePlayer(uuid)`                    | `players().isFakePlayer(uuid)`                                      |
| `getServer(uuid)`                       | `players().find(uuid)` then `FakePlayer::server`                    |
| `addFakePlayer(name, server)`           | `accounts().byUsername(name)` then `players().add(account, server)` |
| `addFakePlayer(uuid, server)`           | `accounts().byUuid(uuid)` then `players().add(account, server)`     |
| `moveFakePlayer(uuid, server)`          | `players().move(account, server)`                                   |
| `removeFakePlayer(uuid)`                | `players().remove(uuid)`                                            |
| `getFakePlayerIds()`                    | `players().all()`                                                   |
| `getFakePlayerIds(server)`              | `players().onServer(server)`                                        |
| `getLocalServerName()`                  | `environment().localServerName()`                                   |
| `Account.fetch(name)`                   | `accounts().byUsername(name)`                                       |
| `Account.fetch(uuid)`                   | `accounts().byUuid(uuid)`                                           |
| `Account.fromBackend(...)`, `backend()` | Not available, use `Account.of(uuid, username)`                     |
| `SpooferAPI.initialize(...)`            | Nothing to call, the plugin handles it                              |

New features land on the API above, not on these classes.

### Reference

| Type                   | Javadoc                                                                                             |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
| `HalosSpoofer`         | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/HalosSpoofer.html)                   |
| `HalosSpooferProvider` | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/HalosSpooferProvider.html)           |
| `AccountService`       | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/account/AccountService.html)         |
| `FakePlayerService`    | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/player/FakePlayerService.html)       |
| `FakePlayerMetadata`   | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/player/FakePlayerMetadata.html)      |
| `FailureReason`        | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/player/FailureReason.html)           |
| `FluctuationService`   | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/fluctuation/FluctuationService.html) |
| `ChatService`          | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/chat/ChatService.html)               |
| `EventBus`             | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/event/EventBus.html)                 |
| `Environment`          | [link](https://spoofer-docs.halosdev.com/com/halos/spoofer/api/environment/Environment.html)        |

Everything else, platform modules and deprecated classes included: [spoofer-docs.halosdev.com](https://spoofer-docs.halosdev.com/)

Questions the Javadoc does not answer: [discord.gg/halosdev](https://discord.gg/halosdev).
