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