# Configuration Painting-Paper uses `config.yml`. Painting-Velocity uses `plugins/painting/resourcepack.yml`. Both share the same shape with a few platform-specific keys called out below. Apply changes by running `/painting reload`. --- ## General Settings | Field | Type | Default | Description | |---|---|---|---| | `enabled` | Boolean | `true` | Master switch. When `false`, Painting does nothing. | | `mode` *(Paper only)* | String | `AUTO` | `AUTO`, `STANDALONE`, or `PROXIED`. `AUTO` flips to `PROXIED` after the first plugin-message receipt from Velocity. | | `server_name` *(Paper only)* | String | `default` | Used to match the `servers:` regex when running standalone. | | `append-hash-to-url` | Boolean | `false` | Append `?hash=<sha1>` to the pack URL. Useful for cache-busting CDN responses. | | `prompt-timeout` | Integer | `15` *(Velocity)* / `15` *(Paper)* | Seconds to wait for the player to accept the prompt before kicking. Only applies to required packs. `0` disables the timeout. | | `kickdelay` | Integer | `0` | Seconds to wait before kicking after `DECLINED` or `FAILED_DOWNLOAD`. `0` kicks immediately. | | `failed-retries` | Integer | `2` | How many times to retry a `FAILED_DOWNLOAD` before kicking. Each retry re-issues the offer with `?_painting_retry=N` so a stale CDN edge doesn't kick a player while others on the same server are unaffected. `0` disables retry. | | `hash-refresh-throttle` | Integer | `60` | Seconds between automatic hash freshness checks. Player joins and a periodic timer share the same throttled refresh, so joins inside the window cost nothing. `0` disables auto-refresh. Paper gained this in 1.0.20; Velocity has had it since 1.0.0. | --- ## Hash Security Limits on what Painting will hash. They apply to sources handed in by other plugins through `IPaintingAPI.computeHash` and to `/painting hash`: URLs you wrote in this file yourself are trusted and may point at a private address regardless. | Field | Type | Default | Description | |---|---|---|---| | `hash-security.max-bytes` | Integer | `268435456` | Hard cap on how much of any source is read, in bytes (256 MiB). Enforced as the body streams, so a lying `Content-Length` doesn't get past it. | | `hash-security.allow-private-hosts` | Boolean | `false` | Allow an API-supplied URL to resolve to a loopback / LAN / link-local address. Off by default so a third-party plugin can't use Painting to probe this machine's network. Every address the hostname resolves to is checked. | | `hash-security.allow-server-folder` | Boolean | `true` | Allow an API-supplied file path to resolve anywhere under the server (or proxy) directory. When `false`, only files inside Painting's own folder can be hashed. | Paths that escape the allowed roots, via `../` or a symlink pointing outside, are refused either way, and only `http` / `https` URLs are accepted at all. ```yaml hash-refresh-throttle: 60 hash-security: max-bytes: 268435456 allow-private-hosts: false allow-server-folder: true ``` --- ## Messages | Field | Type | Description | |---|---|---| | `messages.prompt` | String | Prompt text shown above the resource pack offer. MiniMessage. | | `messages.declined` | String | Kick message when a required pack is declined. MiniMessage. | | `messages.failed` | String | Kick message when a required pack fails to download (after retries). MiniMessage. | ```yaml messages: prompt: "<green>Welcome! Please install the resourcepack." declined: "<red>You need to accept the resourcepack in order to play." failed: "<red>Failed to download the resourcepack. Please try reconnecting." ``` --- ## Pack Definitions Each pack name maps to a list of variants. The first variant whose `protocols` or `versions` matches the player is sent. | Field | Type | Description | |---|---|---| | `resourcepacks.<name>[].url` | String | Public URL of the `.zip` resource pack. | | `resourcepacks.<name>[].file` | String | Local path to a `.zip`, relative to the plugin folder. Alternative to `url`: requires [[#Self-Hosting (Local Files)|self-hosting]]. | | `resourcepacks.<name>[].hash` | String | SHA-1 hash of the pack. Populated by `/painting generatehashes` or auto-refresh. Not needed for `file` variants, computed automatically. | | `resourcepacks.<name>[].protocols` | Integer List | Raw protocol numbers this variant matches. Optional. | | `resourcepacks.<name>[].versions` | String List | Version expressions this variant matches. Optional if `protocols` is set. | A variant must define at least one of `protocols` or `versions`, otherwise it is skipped. ```yaml resourcepacks: base: - url: "https://cdn.example.com/packs/base-1.21.zip" hash: "" versions: ["1.21+"] legacy: - url: "https://cdn.example.com/packs/legacy-old.zip" hash: "" protocols: [763, 764] - url: "https://cdn.example.com/packs/legacy-new.zip" hash: "" versions: [">=1.21,<26.1"] ``` ### Version Expressions | Form | Meaning | |---|---| | `1.21` | Exact match. | | `1.21+` | Greater than or equal to 1.21. | | `>=1.21.4` | Greater than or equal. | | `>1.21.4` | Strictly greater than. | | `<=1.21.7` | Less than or equal. | | `<26.1` | Strictly less than. | | `>=1.21.4,<26.1` | AND, comma chains conditions. | | `["1.21+", "26.1+"]` | OR, list entries are evaluated as alternatives. | ### The version table Painting writes `version_protocols.yml` into `plugins/painting/` (beside `resourcepack.yml` on Velocity) the first time it starts. It holds the full Minecraft version to protocol mapping and **it is yours to edit**: when a new Minecraft version ships you can add it yourself rather than waiting for a Painting release: ```yaml "26.3": 777 ``` Protocol numbers are listed on the [Minecraft wiki](https://minecraft.wiki/w/Protocol_version). Add the line, run `/painting reload`, and version expressions match that client immediately. The file is written once and **never overwritten**, so your edits survive plugin updates, and versions added by later Painting releases still arrive, because the table built into the plugin is loaded first and your file layered over it. Delete a line to fall back to the built-in value for that version. Three layers apply in order, each winning over the last: | Layer | Where | Use it for | | --- | --- | --- | | Built-in table | inside the plugin | every version known at release | | `version_protocols.yml` | `plugins/painting/` | a version that shipped after your build | | `version_protocols:` | `config.yml` / `resourcepack.yml` | a one-off override | ```yaml version_protocols: "<mc-version>": <protocol> ``` --- ## Self-Hosting (Local Files) Painting can serve pack zips straight from the server's disk over a built-in HTTP server, so you don't need a CDN or external host. Enable `selfhost` and use `file:` instead of `url:` on a variant, the SHA-1 hash is computed automatically and players download from `http://<public-address>:<port>/<sha1>.zip`. | Field | Type | Default | Description | |---|---|---|---| | `selfhost.enabled` | Boolean | `false` | Start the embedded HTTP pack server. | | `selfhost.public-address` | String | `""` | Address players can reach this machine on, an IP, a domain, or a full base URL with scheme for reverse-proxy setups (e.g. `https://packs.example.com`). Required. | | `selfhost.port` | Integer | `8163` | Port the pack server listens on. Must be open to players. | | `selfhost.bind-address` | String | `0.0.0.0` | Interface to bind. | ```yaml selfhost: enabled: true public-address: "play.example.com" port: 8163 bind-address: "0.0.0.0" resourcepacks: base: - file: "packs/base.zip" versions: ["1.21+"] ``` File paths resolve relative to the plugin folder (`plugins/Painting/` on Paper, `plugins/painting/` on Velocity). Variants with a missing file, or `file:` used while `selfhost` is disabled, are skipped with a warning at reload. > [!note] > After replacing a pack zip on disk, run `/painting reload` so the hash and served URL pick up the new contents. --- ## Server Entries Server entries match against the current server name (Paper: `server_name` config; Velocity: registered server name). Packs from every matching entry are sent in declaration order. `required: true` kicks players who decline or fail. | Field | Type | Description | |---|---|---| | `servers.<name>.regex` | String | Regex matched against the server name. | | `servers.<name>.required` | Boolean | When `true`, declining or failing kicks the player. | | `servers.<name>.packs` | String List | Pack names to send, in order. Top of the list is the bottom layer; later entries override earlier ones. | ```yaml servers: global: regex: ".*" required: false packs: - base survival: regex: "^survival-\\d+quot; required: true packs: - base - survival-overlay ``` Server entries that reference an unknown pack name log a warning at reload. --- ## Redis (CI/CD Push) Painting can subscribe to a Redis Pub/Sub channel and react to JSON envelopes. Useful for re-deploying a pack from CI without restarting the server. | Field | Type | Default | Description | |---|---|---|---| | `redis.enabled` | Boolean | `false` | Subscribe to the channel. | | `redis.host` | String | `127.0.0.1` | Redis host. | | `redis.port` | Integer | `6379` | Redis port. | | `redis.password` | String | `""` | Optional AUTH password. | | `redis.channel` | String | `painting:redis` | Pub/Sub channel name. | In hybrid setups, Velocity owns Redis. Disable `redis.enabled` on Paper backends to avoid double handling. ### Envelope Format | Action | Payload | Effect | |---|---|---| | `reload` | `{"action":"reload"}` | Reload pack and server registries from disk. | | `update_pack` | `{"action":"update_pack","force_resend_online":true}` | Reload, then optionally resend to all online players. | | `resend` | `{"action":"resend","force_resend_online":true}` | Resend pack offers without reloading config. | --- ## Example Configuration (Paper) ```yaml enabled: true mode: AUTO append-hash-to-url: false prompt-timeout: 15 kickdelay: 0 failed-retries: 2 hash-refresh-throttle: 60 server_name: "default" hash-security: max-bytes: 268435456 allow-private-hosts: false allow-server-folder: true redis: enabled: false host: "127.0.0.1" port: 6379 password: "" channel: "painting:redis" messages: prompt: "<green>Welcome! Please install the resourcepack." declined: "<red>You need to accept the resourcepack in order to play." failed: "<red>Failed to download the resourcepack. Please try reconnecting." resourcepacks: base: - url: "https://cdn.example.com/base.zip" hash: "" versions: ["1.21+"] servers: global: regex: ".*" required: false packs: - base ``` --- ## Example Configuration (Velocity) ```yaml enabled: true append-hash-to-url: false prompt-timeout: 15 kickdelay: 0 failed-retries: 2 hash-refresh-throttle: 60 hash-security: max-bytes: 268435456 allow-private-hosts: false allow-server-folder: true redis: enabled: false host: "127.0.0.1" port: 6379 password: "" channel: "painting:redis" messages: prompt: "<green>Welcome! Please install the resourcepack." declined: "<red>You need to accept the resourcepack in order to play." failed: "<red>Failed to download the resourcepack. Please try reconnecting." resourcepacks: base: - url: "https://cdn.example.com/base.zip" hash: "" versions: ["1.21+"] servers: global: regex: ".*" required: false packs: - base ``` --- ## Related Pages - [[Painting/Server Owners/Overview]], plugin features and modes - [[Painting/Server Owners/Commands]], command reference - [[Painting/Developers/Overview]], developer API documentation - [[Painting/API/Hash/HashPolicy]], how `hash-security` reaches the hasher