# API Reference
Painting-API provides the public interfaces for inspecting the pack registry and listening for pack lifecycle events. The API is platform-neutral — both `Painting-Paper` and `Painting-Velocity` register the same `IPaintingAPI` instance under `PaintingAPI.get()`.
---
## PaintingAPI
The static entry point for all API access.
```java
import gg.lode.paintingapi.PaintingAPI;
if (PaintingAPI.isAvailable()) {
IPaintingAPI api = PaintingAPI.get();
}
```
| Method | Description |
|---|---|
| `get()` | Returns the registered `IPaintingAPI`. Throws `IllegalStateException` if Painting hasn't initialized. |
| `isAvailable()` | `true` if the API has been registered. |
| `setApi(IPaintingAPI)` | Internal — called by platform plugins on enable. Throws if already set. |
---
## IPaintingAPI
The platform-neutral facade. Implemented by `PackManager` on both Paper and Velocity.
```java
IPaintingAPI api = PaintingAPI.get();
```
| Method | Description |
|---|---|
| `isEnabled()` | `true` if Painting is enabled in config. |
| `getPackDefinitions()` | A snapshot list of every configured `PackDefinition`. |
| `getPackDefinition(String)` | A single pack by name, or `null` if not configured. |
| `getServerEntries()` | A snapshot list of every configured `ServerEntry`. |
| `isRequiredForServer(String)` | `true` if any server entry matching the name has `required: true`. |
| `reload()` | Reload pack and server registries from disk. |
| `resendPacksToOnlinePlayers()` | Resend pack offers to every online player. |
| `resendPacksToPlayer(UUID)` | Resend pack offers to a specific player. |
| `computeHash(String)` | SHA-1 of a pack, from a URL or a file path — whichever the source turns out to be. Off-thread. |
| `computeHashFromUrl(String)` | As above, refusing anything that isn't an `http(s)` URL. |
| `computeHashFromFile(String)` | As above, refusing anything that isn't a file inside the allowed roots. |
| `refreshPackHashes(boolean force)` | Re-check every configured pack URL and rewrite any hash that moved. Returns how many changed. |
The four hashing methods are `default` methods that throw `UnsupportedOperationException`, so a plugin compiled against Painting-API 1.0.3 still links against an older Painting build instead of failing with `AbstractMethodError`. They arrived in 1.0.3 and require loader 1.1.6 or newer on the server — `IPaintingAPI` is pinned parent-first, so an older loader jar keeps serving its own copy of the interface without them.
---
## Model Classes
### PackDefinition
A named pack with one or more variants.
| Method | Description |
|---|---|
| `name()` | The pack name from config. |
| `variants()` | Unmodifiable list of `PackVariant` in declaration order. |
| `pickFor(int protocol)` | Returns the first variant whose protocol set contains the given protocol, or `null`. (Note: this convenience method does **not** evaluate version expressions — use `PackManager.pickVariant` internally for full resolution.) |
### PackVariant
A single URL/hash pair gated by protocols and/or version expressions.
| Method | Description |
|---|---|
| `url()` | Pack download URL. |
| `hash()` | SHA-1 hash, hex-encoded. May be empty if not yet generated. |
| `protocols()` | Unmodifiable set of protocol numbers this variant matches. |
| `versionExpressions()` | Unmodifiable list of version expressions (e.g. `1.21+`, `>=1.21.4,<26.1`). |
| `matches(int protocol)` | `true` if the protocol is in the variant's protocol set. |
### ServerEntry
A regex-keyed group of packs to send.
| Method | Description |
|---|---|
| `name()` | Entry name from config. |
| `pattern()` | Compiled `Pattern` of the regex. |
| `required()` | `true` if declining or failing should kick the player. |
| `packs()` | Unmodifiable list of pack names to send, in order. |
| `matches(String serverName)` | `true` if the server name matches `pattern()`. |
### PackStatus
Enum representing the player's resource pack state.
| Value | Meaning |
|---|---|
| `ACCEPTED` | Prompt accepted. |
| `DOWNLOADED` | Download finished. |
| `LOADED` | Pack applied. |
| `DECLINED` | Prompt declined. |
| `FAILED` | Download failed. |
| `DISCARDED` | Pack unloaded. |
| `INVALID_URL` | Client rejected the URL. |
| `UNKNOWN` | Future status not mapped. |
---
## Hashing
All four methods return a `CompletableFuture` and do their work off the server thread.
```java
IPaintingAPI api = PaintingAPI.get();
// From a link
api.computeHash("https://cdn.example.com/pack.zip")
.thenAccept(sha1 -> getLogger().info("hash: " + sha1));
// From a file — relative paths resolve against the Painting data folder,
// then the server directory
api.computeHashFromFile("packs/base.zip")
.thenAccept(sha1 -> ...);
// Re-check every configured pack URL
api.refreshPackHashes(false)
.thenAccept(changed -> getLogger().info(changed + " hashes moved"));
```
A future completes exceptionally with `SecurityException` when the source falls outside the configured limits (non-http scheme, a host resolving to a private address, a path escaping the allowed roots) and `IOException` for an HTTP error, an unreadable file, or a source over the byte cap. Unwrap `CompletionException#getCause` to tell them apart.
`refreshPackHashes(false)` is throttled and deduped against the automatic refresh — a run that happened within `hash-refresh-throttle` seconds is reused rather than repeated, so calling it on a hot path is cheap. Pass `true` to force a full re-download.
### What gets checked
| Source | Enforced |
|---|---|
| URL | `http` / `https` only; redirects followed one hop at a time with every hop re-validated (no silent `https` → `http`, no jump to an unchecked host), capped at 5; `Content-Length` and the streamed body both bounded by `hash-security.max-bytes`; hosts resolving to loopback / link-local / site-local / unique-local addresses refused unless `hash-security.allow-private-hosts` is on — every address the name resolves to is checked, not just literal IPs. |
| File | Resolved through its symlinks, then required to sit inside an allowed root — the Painting data folder, plus the server directory when `hash-security.allow-server-folder` is on. `../` traversal and a symlink planted in the pack folder are both refused. Must be a regular file within the byte cap. |
Config-supplied URLs (the ones in your own `resourcepacks:` block) are hashed under a policy that permits private hosts — a pack served from the same box or a LAN CDN is an ordinary setup. Only API-supplied sources get the strict policy.
The underlying types are public: [[Painting/API/Hash/PackHasher]], [[Painting/API/Hash/HashPolicy]], [[Painting/API/Hash/UrlHashResult]]. Use them directly to hash under your own limits.
---
## Version Expressions
### VersionExpression
Parsed form of a single comparison like `>=1.21.4` or `1.21+`.
| Method | Description |
|---|---|
| `parse(String)` | Static factory. Recognises `>=`, `<=`, `>`, `<`, trailing `+` (alias for `>=`), or bare version (`EQ`). |
| `operator()` | The `Operator` enum (`EQ`, `GT`, `GTE`, `LT`, `LTE`). |
| `version()` | The version string after stripping the operator. |
| `evaluate(int playerProtocol, int referenceProtocol)` | Evaluate the comparison against two protocol numbers. |
### VersionResolver
Maps version strings to protocol numbers and evaluates compound expressions.
| Method | Description |
|---|---|
| `protocolFor(String)` | Looks up a version's protocol number, or `null` if unknown. |
| `override(String, int)` | Inserts or updates a single mapping (used when reading `version_protocols:` from config). |
| `overrideAll(Map)` | Bulk insert. |
| `table()` | A defensive copy of the version-to-protocol map. |
| `evaluateExpressions(Collection<String>, int playerProtocol)` | Evaluates a list of expressions (OR semantics across the list, AND across comma-separated terms within each entry). Throws `IllegalArgumentException` for unknown versions. |
`version_protocols.yml` ships with the platform plugins (not the API jar) and covers 1.7.9 through 26.2 at the time of release. It is layered: the table bundled in the jar, then the admin-editable copy in the plugin's data folder, then a `version_protocols:` block in the config.
A consumer holding only `Painting-API` gets an empty resolver — populate it with `override` / `overrideAll`, or read the live table off `PaintingAPI`.
---
## Event Interfaces
The `gg.lode.paintingapi.api.event` package declares platform-neutral interfaces. Each platform plugin provides a concrete subclass that integrates with the host event bus.
### PackStatusEvent
| Method | Description |
|---|---|
| `getPlayerId()` | Player UUID. |
| `getServerName()` | Server name the status is reported against. |
| `getStatus()` | A `PackStatus` value. |
| `isRequiredForServer()` | `true` if the matching server entry is `required: true`. |
Concrete implementations: `PaperPackStatusEvent` (Bukkit event), `VelocityPackStatusEvent` (Velocity event).
### PackPreSendEvent
| Method | Description |
|---|---|
| `getPlayerId()` | Player UUID. |
| `getServerName()` | Server name being sent. `<proxied>` when delivered through the proxy push path on Paper. |
| `getPackName()` | Pack name from config. |
| `getPackUrl()` | Pack URL (post hash-append if enabled). |
| `getPackHash()` | SHA-1 hash. |
| `isCancelled()` / `setCancelled(boolean)` | Cancel the offer for this pack. |
Concrete implementations: `PaperPackPreSendEvent` (cancellable Bukkit event), `VelocityPackPreSendEvent` (interface-only — Velocity's native dispatch is currently used by Paper-side pre-send only).
### PackRegistryReloadEvent
| Method | Description |
|---|---|
| `getDefinitionCount()` | Number of pack definitions after reload. |
| `getServerEntryCount()` | Number of server entries after reload. |
Concrete implementation: `PaperPackRegistryReloadEvent` (Bukkit event fired after `/painting reload`).
---
## Related Pages
- [[Painting/Developers/Overview]] — getting started and usage examples
- [[Painting/API/PaintingAPI]] — static accessor source
- [[Painting/API/IPaintingAPI]] — platform-neutral interface source
- [[Painting/API/Hash/PackHasher]] — hashing implementation behind `computeHash`
- [[Painting/Server Owners/Commands]] — `/painting reload` triggers `PackRegistryReloadEvent`