# Developer Overview
Frame's API exists to answer one question: *what does this specific player see right now?*
Everything else follows from that. There is no global tablist to mutate — you contribute to a resolution that runs per viewer.
---
## Getting the API
```gradle
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
compileOnly 'com.github.Lodestones:Frame-API:1.0.3'
}
```
```java
IFrameAPI frame = FrameAPI.get();
```
`FrameAPI.get()` returns `null` until Frame enables. It enables before most plugins, but guard with `FrameAPI.isAvailable()` if you touch it from `onLoad()` or a static initialiser.
---
## How resolution works
Plugins register **providers** at a **priority**. On each refresh Frame walks them from highest priority to lowest and merges what they return **field by field** — the first non-null value for a field wins, and unset fields fall through to the layer below.
That merge is what lets unrelated plugins share a tablist. A vanish plugin overrides one field:
```java
public final class Vanish implements TablistProvider {
@Override
public int priority() {
return FramePriority.HIGHEST;
}
@Override
public TabEntry entry(Player viewer, Player target) {
if (!vanished(target) || viewer.hasPermission("vanish.see")) return null;
return TabEntry.builder().visible(false).build();
}
}
```
…and the rank plugin that set that player's name colour, sort key and ping never has to know it exists.
Returning `null` means "no opinion" — the next provider decides.
### Priorities
| Constant | Value | For |
|---|---|---|
| `CONFIG` | 0 | Frame's own `config.yml` |
| `LOW` | 250 | Always-on decoration — rank prefixes, default sorting |
| `NORMAL` | 500 | Feature plugins |
| `HIGH` | 750 | Contextual — minigame state, party colours |
| `HIGHEST` | 1000 | Must-win — vanish, spectator, admin tooling |
Ties break by registration order, earliest first, so resolution stays stable across restarts.
Frame's config is registered as an ordinary provider at priority 0. There is no special case for it — "plugins beat config" is a consequence of the priority, not a rule.
---
## Two ways in
**Providers** — Frame pulls from you on its refresh ticker. Best when the value is derived from live state.
```java
FrameAPI.get().register(plugin, new MyProvider());
```
**Layers** — a mutable provider you push into when your state changes. Best when the value changes on events and you already know when.
```java
FrameLayer layer = FrameAPI.get().createLayer(plugin, "ranks", FramePriority.LOW);
layer.setTabEntry(steve, TabEntry.builder()
.displayName(text("[VIP] Steve", NamedTextColor.GREEN))
.sortKey("020_vip_Steve")
.build());
```
Within a layer, per-viewer values beat "everyone" values. Across layers, normal priority rules apply. Setters are thread-safe and schedule their own refresh — there is no apply step.
---
## Threading and cost
Providers are called **off the main thread** on Frame's ticker, and on it when something calls `refresh(...)` explicitly. Read from your own cached fields, not from live world objects.
`entry` and `style` run once per viewer-and-target pair per refresh — on a 100-player server that is 10,000 calls. Keep them allocation-light, and use `appliesTo(viewer)` to opt out of a viewer entirely rather than returning `null` from five methods.
Frame diffs everything against what each client already has, so a provider returning the same value every tick costs no packets.
A provider that throws is logged once and then skipped. One broken plugin does not blank the server's tablist.
---
## Related
- [[Frame/Developers/API Reference]] — every type and method
- [[Frame/Server Owners/Configuration]] — the config layer you sit above