Plugin Manifest
Every Pano addon ships with a small metadata file — the manifest — that tells Pano what your addon is called, who made it, which class to run first, and what it depends on. This page shows you the handful of values you set, where you set them, and how to confirm they ended up in your built addon.
Coming from JavaScript?
A manifest is the Java-world equivalent of package.json — a small file that describes your project. An entry point just means the class Pano runs first when it loads your addon.
A few terms you will see on this page:
- PF4J (Plugin Framework for Java) — a library that finds and loads plugin jars while Pano is running. You never call it yourself; it just needs certain metadata to exist.
- JAR — Java's packaged output, which is really just a zip file. When you build your addon you get one
.jar. MANIFEST.MF— a plain-text file inside that jar (underMETA-INF/) that holds the metadata PF4J reads.
You do not edit MANIFEST.MF by hand. Instead you set everything in a properties file, and the build copies your values into the manifest for you.
This page assumes you scaffolded from the boilerplate
These instructions apply to a plugin project created from the Pano boilerplate. If you have not done that yet, start with Getting Started. The boilerplate already ships a gradle.properties with every key below filled in.
When do you actually touch this? In practice you change only five lines before your first build — pluginId, pluginName, pluginDescription, pluginClass, and pluginDeveloper. Everything else can stay exactly as the boilerplate ships it.
Configuring gradle.properties
Open gradle.properties in the root folder of your plugin project — the boilerplate already contains one, pre-filled with all the keys below. During a build, Gradle reads these values and injects them into the final JAR manifest for you.
Example
Here is a complete gradle.properties from the Announcements addon:
pluginId=pano-plugin-announcement
pluginName=Announcements
pluginDescription=Create, edit and manage your Minecraft server announcements!
pluginPanoVersion=local-build
pluginClass=com.panomc.plugins.announcement.AnnouncementPlugin
pluginDeveloper=Pano
pluginLicense=MIT
pluginSourceUrl=https://github.com/panomc/pano-plugin-announcement
pluginDependencies=
pluginRequires=Keep these values to plain ASCII (unaccented letters, digits, basic punctuation). If you need an em dash, an accent, or an emoji, read the encoding note under Gotchas below first.
Checkpoint. Build once and look inside the jar:
./gradlew build
unzip -p build/libs/*.jar META-INF/MANIFEST.MFYou should see your id, name, and main-class among the lines printed.
What gets generated
Given the gradle.properties above, the build writes lines like these into META-INF/MANIFEST.MF (the attribute names come from PF4J; the values come straight from your properties):
id: pano-plugin-announcement
name: Announcements
description: Create, edit and manage your Minecraft server announcements!
pano-version: local-build
main-class: com.panomc.plugins.announcement.AnnouncementPlugin
version: local-build
developer: Pano
license: MIT
source-url: https://github.com/panomc/pano-plugin-announcementThat is the whole point of the file: gradle.properties in, MANIFEST.MF out. (The exact lines depend on which optional properties you filled in.)
Key Properties
Now the values, line by line. (Required) means the Pano build fails without it — if a required property is missing, ./gradlew build stops during the shadowJar step with an error naming the property. It does not mean PF4J needs it. (Optional) properties can be left blank or removed.
pluginId: (Required) A unique id for your addon. Use lowercase letters, digits, and hyphens only — no spaces. The convention ispano-plugin-<name>(e.g.pano-plugin-announcement); thepano-plugin-prefix is a convention, not a hard requirement, but stick to it. Pick it once and never change it (see the tip below).pluginName: (Required) Human-readable name of the plugin (e.g.,Announcements).pluginDescription: (Optional) A brief description of what your plugin does.pluginPanoVersion: (Required) The version of Pano this plugin is built for. Leave this aslocal-buildduring development — see the versions warning below.pluginClass: (Required) The full path to your addon's main class — its package name plus the class name, e.g.com.example.myplugin.MyPlugin. This is the class insrc/main/kotlin/...whose declaration contains: PanoPlugin(— the class Pano runs first when it loads your addon. (Developers call this the fully qualified name.)pluginDeveloper: (Required) The author or organization developing the plugin.pluginLicense: (Optional) License of the plugin (e.g.,MIT,Apache-2.0).pluginSourceUrl: (Optional) URL to the plugin's source code.pluginDependencies: (Optional) Other Pano plugins your addon needs, separated by commas — e.g.pluginDependencies=other-plugin, some-plugin?. See Dependencies below for the full syntax.pluginRequires: (Optional) Which Pano versions your addon is allowed to run on, written as a version range like>=1.0.0. Empty (the default) means any version. Set it only if your addon relies on a feature added in a specific Pano release, e.g.pluginRequires=>=1.2.0. (This maps to the manifestrequiresattribute.)
pluginPanoVersion vs pluginRequires
These two sound alike but do different jobs:
pluginPanoVersionjust records which Pano version you built against. It is informational.pluginRequiresis enforced: if you set a range, Pano refuses to load your addon on any Pano version outside it.
Your pluginId is used everywhere — choose it carefully
Pano reuses this one string all over the system, so it also names:
- your addon's data folder (
plugins/<pluginId>/), - how Pano tracks your addon's database schema version (which migration your tables are on),
- the URL segment for your addon's UI,
- every permission your addon defines (each one is prefixed
pano.plugin.<pluginId>.…), and - your marketplace listing id (
resourceId) when you publish — see Building & Publishing.
Pick it once, and never change it after your addon is live. Renaming it makes Pano treat the addon as brand-new: your old database tables, files, and granted permissions are ignored and effectively lost.
You don't set version numbers by hand
Version numbers are computed for you, and getting this wrong breaks the release pipeline — so just leave them alone:
- You never type a version number. The boilerplate does not ask you to set a Gradle
versionat all, so don't add one. - Local builds are always
local-build. KeeppluginPanoVersion=local-build, and the Gradleversion(injected at build time) stayslocal-buildtoo. - On release, the pipeline fills it in. When you push, CI — the automated build that runs on GitHub — uses a tool called semantic-release to compute the real
versionfrom your commit messages. Hand-editing it breaks that.
The Pano version shown on the Marketplace is yet another, separate value, configured when you publish. See Building & Publishing for how versions are derived from your commits.
Dependencies
You declare which other Pano plugins your addon needs in gradle.properties; the build writes them into the manifest for you.
Not the same as a library dependency
This is not a build.gradle.kts library dependency (the implementation(...) lines — Pano's equivalent of npm install). pluginDependencies means other Pano plugins that must be installed on the server at runtime for your addon to work.
Plugin Dependencies (pluginDependencies)
List the other addons yours needs, separated by commas.
- Syntax:
pluginIdorpluginId@version - Optional Dependency: append
?to the plugin ID.
The part after @ is a version range. Standard comparison operators like >=, <=, >, and < are supported; for the full grammar see the PF4J documentation.
Examples:
pluginDependencies=other-plugin: Requires any version ofother-plugin.pluginDependencies=other-plugin@1.2.0: Requires exactly version1.2.0.pluginDependencies=other-plugin@>=1.2.0: Requires version1.2.0or higher.pluginDependencies=other-plugin@<2.0.0: Requires any version below2.0.0.pluginDependencies=other-plugin?: Optional dependency. If present, it is loaded before your plugin; if not, yours still loads.pluginDependencies=other-plugin, some-plugin?: Two dependencies, comma-separated — one required, one optional.
What if a required dependency is missing?
If a required dependency is not installed on the server, Pano refuses to load your addon and logs an error at startup — check the platform console/log to see which one is missing.
Gotchas
gradle.properties is read as ISO-8859-1
Use plain ASCII in gradle.properties values
Use only plain ASCII characters — unaccented English letters, digits, and basic punctuation — in these values. For anything else (an em dash, an accented letter, or an emoji), write the Unicode \uXXXX escape instead of the raw character, or it will be mangled in the manifest.
Why: Gradle parses .properties files in ISO-8859-1 (Latin-1) encoding, not UTF-8. (For the curious: a literal em dash — is the UTF-8 bytes 0xE2 0x80 0x94, which read back as Latin-1 turns into â€".)
The two lines below differ only in the value — the first uses a raw em dash, the second uses its \u2014 escape:
# Wrong — the literal em dash is mangled to â€"
pluginDescription=Manage your server — fast and simple.
# Right - \u2014 is the escape for an em dash
pluginDescription=Manage your server \u2014 fast and simple.To find the escape for any character, look it up on a Unicode site (search e.g. "unicode code point for é") and write it as \uXXXX — for example é is \u00E9.
Advanced: Manual Configuration
The gradle.properties file is just a convenience layer. If you prefer to configure your manifest yourself or need dynamic values, you can edit the shadowJar task in build.gradle.kts. (shadowJar is the build step that packages your addon plus its libraries into the single .jar Pano loads.)
Here is how the Pano Boilerplate maps properties to the manifest. Each val … by project line pulls the matching value out of gradle.properties; the manifest { } block then writes it into MANIFEST.MF under the attribute name PF4J expects:
shadowJar {
val pluginId: String by project
val pluginName: String by project
val pluginDescription: String? by project
val pluginPanoVersion: String by project
val pluginClass: String by project
val pluginDeveloper: String by project
val pluginLicense: String? by project
val pluginSourceUrl: String? by project
val pluginDependencies: String? by project
val pluginRequires: String? by project
manifest {
attributes["id"] = pluginId
attributes["name"] = pluginName
pluginDescription?.let { attributes["description"] = it }
attributes["pano-version"] = pluginPanoVersion
attributes["main-class"] = pluginClass
attributes["version"] = version
attributes["developer"] = pluginDeveloper
pluginLicense?.let { attributes["license"] = it }
pluginSourceUrl?.let { attributes["source-url"] = it }
pluginDependencies?.let { attributes["dependencies"] = it }
pluginRequires?.let { attributes["requires"] = it }
}
}Don't rename the attribute keys
You can add attributes freely, but do not rename the existing keys (id, name, main-class, pano-version, and so on). PF4J looks them up by these exact names, so a renamed key silently stops your addon from loading. For reference, PF4J itself only requires id, main-class, and version; everything else is optional and is written to the manifest only when you set it.
Pano uses PF4J in the background to handle all plugin loading and management. You never interact with it directly for standard development, but if you want the deeper technical details you can consult the PF4J documentation.
Premium build properties
Shipping a premium (paid) addon? The build embeds a license public key — a small key Pano uses to verify that a buyer actually paid — into your jar at build time. This is set with build flags, not manifest properties, and without any of them your addon builds as a free (unlicensed) jar. The full workflow lives on the Premium addons page; the flags it uses are:
-PlicenseServer=dev|prod|<url>— which license server the build points at.-PpanoLicensePublicKey=<base64>— the public key itself, passed as a base64 string.PANO_LICENSE_PUBLIC_KEY— an environment variable you can use instead of the flag above.
Check your work
Once you have edited your five lines, confirm the whole pipeline end to end:
- Build: run
./gradlew build. It should finish without errors. (If a (Required) property is missing, the build stops duringshadowJarand names it.) - Inspect the manifest inside the jar: run
unzip -p build/libs/*.jar META-INF/MANIFEST.MF. You should see yourid,name,main-class, and the other lines from What gets generated above. - Install it: drop the jar into a Pano install's
plugins/folder. - Confirm it loaded: start Pano and open Panel → Addons — you should now see your addon listed by its
pluginName.