Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions .agents/skills/fill-template-plugin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ name: fill-template-plugin
description: Customize this PaperMC/Spigot plugin template for a real plugin. Use when asked to fill in, adapt, rename, fork, or customize the template project.
---

If information required in the checklist below is missing, end your turn and ask for it before editing.
Read `docs/customization.md` before editing. Ask for any missing plugin details before making
changes.

# Template Customization Checklist
# Checklist

When adapting this template for a real plugin, update:
1. `settings.gradle.kts` — `rootProject.name`
2. `build.gradle.kts` — `group` (Java package)
3. `src/main/resources/plugin.yml` — `author`, `description`, `permissions`
4. Rename the Java package and source directory from `com.crimsonwarpedcraft.exampleplugin`
5. `.github/CODEOWNERS`, `.github/FUNDING.yml` — replace `leviem1`
6. `CODE_OF_CONDUCT.md` line 63 — contact method
7. README badges and Discord invite link
1. Set `rootProject.name`; rename `ExamplePlugin.java` and all references to the example plugin.
2. Set the Gradle `group`; rename main and test package paths, declarations, and imports.
3. Replace or remove the example code.
4. Update `plugin.yml` metadata and permissions. Do not declare commands there.
5. Align the Paper API, `api-version`, Java toolchain, CI Java versions, dependencies, shading,
and supported-version docs.
6. Update README content and links, project docs, `AGENTS.md`, and `.agents/skills/`.
7. Update GitHub ownership, funding, conduct contact, templates, policies, workflows, variables,
and secrets as applicable.
8. Search for template leftovers, then run `./gradlew clean build`.
8 changes: 4 additions & 4 deletions .agents/skills/run-plugin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ folder.
./gradlew test

# Run a single test class
./gradlew test --tests "com.example.plugin.command.PingTest"
./gradlew test --tests "com.crimsonwarpedcraft.exampleplugin.command.PingTest"

# Run a single test method
./gradlew test --tests "com.example.plugin.command.GreetTest.greetsTarget"
./gradlew test --tests "com.crimsonwarpedcraft.exampleplugin.command.GreetTest.greetsTarget"
Comment thread
This conversation was marked as resolved.
Outdated
```

Checkstyle enforces Google Java style with `maxWarnings = 0` — the build fails on any warning. SpotBugs runs FindSecBugs. Both run as part of `build`; fix all findings before committing.
Expand All @@ -42,8 +42,8 @@ Command executor unit tests (`Ping`, `Greet`, etc.) use Mockito directly — moc

Versioning logic (`build.gradle.kts`):
- No `-Pver` → `yyMMdd-HHmm-SNAPSHOT`
- `-Pver=vX.Y.Z-RC-N` `X.Y.Z-SNAPSHOT`
- `-Pver=vX.Y.Z` `X.Y.Z` (stable; the `release` task then renames the shadow jar to
- `-Pver=vX.Y.Z-RC-N` -> `X.Y.Z-RC-N-SNAPSHOT`
- `-Pver=vX.Y.Z` -> `X.Y.Z` (stable; the `release` task then renames the shadow jar to
`${rootProject.name}.jar`)

Quote the `-Pver` value to stop the shell/PowerShell from mangling the `=`.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
files: ${{ github.workspace }}/build/libs/*
generate_release_notes: true
name: ${{ format('Release {0}', github.ref_name) }}
prerelease: ${{ contains(github.ref_name, '-rc-') }}
prerelease: ${{ contains(github.ref_name, '-RC-') }}
Comment thread
This conversation was marked as resolved.
fail_on_unmatched_files: true
draft: true
outputs:
Expand Down
30 changes: 19 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
This is a **PaperMC/Spigot Minecraft plugin template**. The intent is that users fork/copy it and replace the example scaffolding with their own plugin.
This repository is a **PaperMC/Spigot Minecraft plugin template**. Users fork or copy it, then replace the example scaffolding with their own plugin.

## Architecture

**Entry point**: `ExamplePlugin extends JavaPlugin`Bukkit/Paper calls `onEnable()` and `onDisable()` on the plugin lifecycle. The main class is referenced in `plugin.yml` via the `${PACKAGE}.${NAME}` substitution, which is filled at build time by `processResources` from `group` (build.gradle.kts) and `rootProject.name` (settings.gradle.kts).
**Entry point**: `ExamplePlugin extends JavaPlugin`. Bukkit/Paper calls `onEnable()` and `onDisable()` during the plugin lifecycle. In `plugin.yml`, `${PACKAGE}.${NAME}` identifies the main class. The `processResources` task fills these placeholders at build time from `group` in `build.gradle.kts` and `rootProject.name` in `settings.gradle.kts`.

**JAR packaging**: The standard `jar` task is disabled. `shadowJar` is the sole output — it shades CommandAPI (relocated to `<group>.commandapi`). CommandAPI is excluded from `minimize()` because it loads classes via reflection. `assemble` depends on `shadowJar`.
**JAR packaging**: The standard `jar` task is disabled, and `shadowJar` is the sole output. It shades CommandAPI and relocates it to `<group>.commandapi`. CommandAPI is excluded from `minimize()` because it loads classes through reflection. `assemble` depends on `shadowJar`.

**cw-commons dependency**: `Command`/`BaseCommand` (command registration) and `Config`/`ConfigManager` (YAML config loading + Jakarta validation) come from [cw-commons](https://github.com/CrimsonWarpedcraft/cw-commons), not local code — consumed via JitPack (`com.github.CrimsonWarpedcraft:cw-commons`). `build.gradle.kts` pins a tagged release (e.g. `v0.1.0`) rather than the unstable `main-SNAPSHOT`; bump that tag deliberately. Jackson and Hibernate Validator remain direct dependencies here because `PluginConfig` uses their annotations (`@JsonProperty`, `@NotBlank`) directly — cw-commons exposes them as `api` (transitive, unbundled) dependencies specifically so each consumer shades/relocates its own copy without classloader conflicts. Note that cw-commons' `api` deps already put Jackson/Hibernate Validator on this project's classpath transitively, so the explicit declarations here are redundant for resolution — kept anyway since `PluginConfig` references them directly and shouldn't rely on another project's transitive exposure choices.
**cw-commons dependency**: [cw-commons](https://github.com/CrimsonWarpedcraft/cw-commons) provides `Command`/`BaseCommand` for command registration and `Config`/`ConfigManager` for YAML loading and Jakarta validation. These classes are not defined locally. The project consumes cw-commons from JitPack as `com.github.CrimsonWarpedcraft:cw-commons`. `build.gradle.kts` pins a tagged release, such as `v0.1.0`, instead of the unstable `main-SNAPSHOT`. Update that tag deliberately.

Jackson and Hibernate Validator remain direct dependencies because `PluginConfig` uses their `@JsonProperty` and `@NotBlank` annotations. cw-commons exposes these libraries as transitive, unbundled `api` dependencies so each consumer can shade and relocate its own copy without classloader conflicts. The direct declarations are redundant for dependency resolution, but they prevent this project from relying on another project's transitive exposure choices.

**Command declaration**: CommandAPI registers commands programmatically in `onEnable()` (see `ExampleCommand`/`BaseCommand`). Adding a matching entry under `commands:` in `plugin.yml` makes Bukkit register the same command a second time, which CommandAPI flags at startup with a "Plugin command ... is registered by Bukkit" warning. `permissions:` entries are unaffected and still required.

**Versioning logic** (in `build.gradle.kts`):
- No `-Pver` supplied → `yyMMdd-HHmm-SNAPSHOT`
- `-Pver=vX.Y.Z-RC-N` → `X.Y.Z-SNAPSHOT`
- `-Pver=vX.Y.Z` → `X.Y.Z` (stable release)

- No `-Pver` supplied -> `yyMMdd-HHmm-SNAPSHOT`
- `-Pver=vX.Y.Z-RC-N` -> `X.Y.Z-RC-N-SNAPSHOT`
- `-Pver=vX.Y.Z` -> `X.Y.Z` (stable release)

**CI workflows** (`.github/workflows/`):
- `pr.yml` — builds and tests on Ubuntu + Windows for PRs and merge queue
- `main.yml` — builds, tests, and cuts a snapshot release on push to `main`
- `tag.yml` / `release.yml` — handle tagged releases and Discord notifications

**Agent instructions**: (1) Canonical skills live in `.agents/skills/`; `.claude/skills/` is a generated mirror. (2) `CLAUDE.md` is a generated copy of this `AGENTS.md`. (3) No agents are permitted to edit or create `CLAUDE.md` or `.claude/skills/`; Claude hooks established in `.claude/settings.json` automatically sync these mirrors on `SessionStart` and `PostToolUse`.
- `pr.yml`: builds and tests on Ubuntu + Windows for PRs and merge queue
- `main.yml`: builds, tests, and uploads a snapshot artifact on push to `main`
- `tag.yml` / `release.yml`: handle tagged releases and Discord notifications

## Agent instructions

1. Canonical skills live in `.agents/skills/`. The `.claude/skills/` directory is a generated mirror.
2. `CLAUDE.md` is a generated copy of this `AGENTS.md`.
3. Do not edit or create `CLAUDE.md` or files under `.claude/skills/`. Claude hooks configured in `.claude/settings.json` synchronize these mirrors on `SessionStart` and `PostToolUse`.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ This build step will also run all checks and tests, making sure your code is cle
JARs can be found in `build/libs/`.

## Contributing
See [CONTRIBUTING.md](https://github.com/CrimsonWarpedcraft/plugin-template/blob/main/CONTRIBUTING.md).
See [CONTRIBUTING.md](CONTRIBUTING.md).
43 changes: 17 additions & 26 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ version = (if (!hasProperty("ver")) {
if (ver.startsWith("v") && !ver.lowercase().contains("-rc-")) base else "$base-SNAPSHOT"
}).uppercase()

java {
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
java.toolchain.languageVersion = JavaLanguageVersion.of(25)

repositories {
maven {
Expand Down Expand Up @@ -62,7 +59,7 @@ repositories {
}
}

val mockitoAgent by configurations.creating
val mockitoAgent = configurations.create("mockitoAgent")

dependencies {
compileOnly("io.papermc.paper:paper-api:26.1.2.build.72-stable")
Expand All @@ -75,17 +72,13 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:6.1.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.1.0")


// Dependencies used by the example code. Not required for Paper plugins.
// Example dependencies. Paper plugins do not require these libraries.
implementation("com.github.CrimsonWarpedcraft:cw-commons:v0.1.1")
// Jackson + Hibernate Validator: also exposed transitively via cw-commons' `api` deps,
// but declared directly anyway since PluginConfig imports their annotations — don't
// rely on a transitive exposure decision made by another project for code we compile against.
// PluginConfig imports annotations from Jackson and Hibernate Validator directly.
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.22.0")
// Example command implementation via CommandAPI
// https://commandapi.jorel.dev
implementation("dev.jorel:commandapi-paper-shade:11.2.0")
implementation("org.hibernate.validator:hibernate-validator:9.1.1.Final")

testImplementation("org.mockito:mockito-core:5.23.0")
mockitoAgent("org.mockito:mockito-core:5.23.0") { isTransitive = false }
}
Expand All @@ -97,7 +90,7 @@ tasks.test {

tasks.processResources {
filesMatching("**/plugin.yml") {
expand(mapOf("NAME" to rootProject.name, "VERSION" to version, "PACKAGE" to rootProject.group.toString()))
expand(mapOf("NAME" to rootProject.name, "VERSION" to version, "PACKAGE" to project.group))
}
}

Expand Down Expand Up @@ -129,16 +122,15 @@ tasks.withType<SpotBugsTask>().configureEach {
}
}

tasks.named<ShadowJar>("shadowJar") {
val shadowJar = tasks.named<ShadowJar>("shadowJar") {
archiveClassifier.set("")
mergeServiceFiles()
// Update the destination package to match your group when renaming the plugin
relocate("dev.jorel.commandapi", "com.crimsonwarpedcraft.exampleplugin.commandapi")
relocate("com.fasterxml", "com.crimsonwarpedcraft.exampleplugin.fasterxml")
relocate("org.yaml.snakeyaml", "com.crimsonwarpedcraft.exampleplugin.snakeyaml")
relocate("org.hibernate.validator", "com.crimsonwarpedcraft.exampleplugin.hibernatevalidator")
relocate("jakarta.validation", "com.crimsonwarpedcraft.exampleplugin.jakartavalidation")
relocate("org.jboss.logging", "com.crimsonwarpedcraft.exampleplugin.jbosslogging")
relocate("dev.jorel.commandapi", "${project.group}.commandapi")
relocate("com.fasterxml", "${project.group}.fasterxml")
relocate("org.yaml.snakeyaml", "${project.group}.snakeyaml")
relocate("org.hibernate.validator", "${project.group}.hibernatevalidator")
relocate("jakarta.validation", "${project.group}.jakartavalidation")
relocate("org.jboss.logging", "${project.group}.jbosslogging")
// These libs load classes via reflection or SPI and must not be minimized
minimize {
exclude(dependency("dev.jorel:commandapi-paper-shade:.*"))
Expand All @@ -160,7 +152,7 @@ tasks.jar {
}

tasks.assemble {
dependsOn(tasks.named("shadowJar"))
dependsOn(shadowJar)
}

tasks.register("printProjectName") {
Expand All @@ -170,13 +162,12 @@ tasks.register("printProjectName") {
}

tasks.register("release") {
dependsOn(tasks.named("build"))
dependsOn("build")

doLast {
if (!version.toString().endsWith("-SNAPSHOT")) {
// Rename final JAR to trim off version information
tasks.named<ShadowJar>("shadowJar").get().archiveFile.get().asFile
.renameTo(layout.buildDirectory.get().asFile.resolve("libs/${rootProject.name}.jar"))
val releaseJar = layout.buildDirectory.file("libs/${rootProject.name}.jar").get().asFile
shadowJar.get().archiveFile.get().asFile.renameTo(releaseJar)
}
}
}
116 changes: 46 additions & 70 deletions docs/customization.md
Original file line number Diff line number Diff line change
@@ -1,89 +1,65 @@
# Customizing This Template

When adapting this template for your own plugin, you'll need to update the following files.
Use this checklist when turning the template into a plugin.

### Discord Notifications
This repo allows automatically pushing releases to a Discord webhook.
## Plugin identity

To use this Action, you will need to set two GitHub Actions secrets.
- `DISCORD_WEBHOOK_ID`
- `DISCORD_WEBHOOK_TOKEN`
1. Set `rootProject.name` in `settings.gradle.kts` to the Java entry point class name.
2. Rename `ExamplePlugin.java`, the `ExamplePlugin` class, and all references to it. The current
`plugin.yml` build substitution requires this name to match `rootProject.name`.
3. Set `group` in `build.gradle.kts` to the Java package.
4. Rename the main and test package directories, declarations, and imports from
`com.crimsonwarpedcraft.exampleplugin`.

You can find these values by copying the Discord Webhook URL:
`https://discord.com/api/webhooks/<DISCORD_WEBHOOK_ID>/<DISCORD_WEBHOOK_TOKEN>`
## Example code

Optionally, you can also configure `DISCORD_RELEASE_WEBHOOK_ID` and `DISCORD_RELEASE_WEBHOOK_TOKEN`
to send release announcements to a separate channel.
Replace or remove the example command, permission, config, data store, listener, and tests. Keep
these parts in sync:

For more information, see [Discord Message Notify](https://github.com/marketplace/actions/discord-message-notify).
- Command names and permission checks in Java
- Permission declarations in `src/main/resources/plugin.yml`
- Fields in `PluginConfig` and `src/main/resources/config.yml`
- Main and test code

### `README.md`
Make this relevant to your project.
CommandAPI registers commands in Java. Do not add matching entries under `commands:` in
`plugin.yml`.

Be sure to replace the badges for build status and Discord.
## Metadata and build

### `settings.gradle.kts`
Replace `ExamplePlugin` with the name of your plugin.
- Update `author`, `description`, `permissions`, and `api-version` in `plugin.yml`.
- Keep the Paper API version, Java toolchain, CI Java versions, and documented server support in
sync.
- Review repositories, dependencies, Shadow relocations, and `minimize` exclusions. Remove
example dependencies the plugin no longer uses.

```kotlin
rootProject.name = "ExamplePlugin"
```
## Project files

### `build.gradle.kts`
Make sure to update `group` to your package's name in the following section.
- Rewrite `README.md` for the plugin. Replace the build badge, Discord link, commands, features,
and repository links.
- Update `docs/usage.md`, `docs/releases.md`, `AGENTS.md`, and canonical skills under
`.agents/skills/` when their examples or architecture change.
- Do not edit `CLAUDE.md` or `.claude/skills/`. They are generated mirrors.
- Check source attribution and license terms before changing copyright notices.

```kotlin
group = "com.crimsonwarpedcraft.exampleplugin"
```
## GitHub

Add any required repositories for your dependencies:
- Update `.github/CODEOWNERS` and update or delete `.github/FUNDING.yml`.
- Replace the enforcement contact in `CODE_OF_CONDUCT.md`.
- Review issue templates, labels, the stale policy, Dependabot, branch protection, and workflows.
Update each `main` reference if the repository uses a different default branch.
Comment thread
This conversation was marked as resolved.
Outdated

```kotlin
repositories {
// ...
}
```
Discord notifications use:

Also, update your dependencies as needed (of course).
- Repository variable `DISCORD_WEBHOOK_ID`
- Actions secret `DISCORD_WEBHOOK_TOKEN`
- Optional repository variable `DISCORD_RELEASE_WEBHOOK_ID`
- Optional Actions secret `DISCORD_RELEASE_WEBHOOK_TOKEN`

```kotlin
dependencies {
// ...
}
```
Remove the notification jobs if the plugin will not use Discord webhooks.

### `src/main/resources/plugin.yml`
First, update the following with your information.
## Verify

```yaml
author: AUTHOR
description: DESCRIPTION
```

Next, the `permissions` section below should be updated as needed.

```yaml
permissions:
example.test:
description: DESCRIPTION
default: true
example.*:
description: Grants all other permissions
default: false
children:
example.test: true
```

Do NOT create a `commands:` section — CommandAPI registers commands programmatically in
`onEnable()` (see `ExampleCommand`), not via `plugin.yml`.

Declaring a command in both places
causes Bukkit to register it a second time, which CommandAPI will warn about at startup.

### `.github/`
- `CODEOWNERS` -> Replace `leviem1` with your username.
- `FUNDING.yml` -> Update or delete this file, [whatever applies to you.](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository)

### Code of Conduct
If you choose to adopt the Code of Conduct for your project,
please update line 63 of `CODE_OF_CONDUCT.md` with your preferred contact method.
1. Search for old names, packages, permissions, placeholders, attribution, and repository URLs.
2. Run `./gradlew clean build`.
3. Check the processed `plugin.yml` and shaded JAR for the correct main class.
4. Start the JAR on the oldest supported Paper version.
Loading