Skip to content

2 ‐ Developer Guide

Basti edited this page Sep 27, 2026 · 2 revisions

This document targets contributors who need to build Timberella from source, extend functionality, or troubleshoot code-level issues. Configuration, commands and permissions are documented for server admins on the wiki Home page.

1. Repository Layout

plugins/timberella-plugin/
├─ build.gradle              # Paper plugin build script (shadowJar enabled)
├─ settings.gradle           # Opt-in ../turtle-lib checkout (-PuseLocalTurtleLib=true)
├─ gradle/libs.versions.toml # Version catalog, a synced copy of the workspace root's catalog
├─ version.properties        # Plugin version
├─ config/                   # Spotless (Eclipse JDT) formatter settings
├─ src/main/java/...         # Plugin source (package net.kroet.timberella)
├─ src/main/resources/       # Config, locales, leaf mappings, bundled license texts, legacy/ upgrade baselines
├─ src/test/java/...         # Unit tests
├─ LICENSE                   # MIT license text, copied into the jar
└─ THIRD_PARTY_LICENSES.md

Library and Paper versions come from gradle/libs.versions.toml, which Gradle loads automatically. It is a copy of the workspace root's catalog, kept in sync by sync-gradle-wrapper.ps1. Always make version bumps (including those from Dependabot PRs) in the root catalog first, then run sync-gradle-wrapper.ps1; the script aborts without copying anything if a repo's copy was edited directly since the last sync, and lists the differing entries (-Force overwrites them). Adventure and Gson are not in the catalog: they come in through paper-api, which fixes their versions. How TurtleLib is resolved is explained in §5.

2. Tooling & Requirements

  • Java 25 (Temurin or equivalent). Configure JAVA_HOME accordingly.
  • Gradle Wrapper (Gradle 9.8.0; do not install system Gradle; use ./gradlew / gradlew.bat). The configuration cache is on (gradle.properties), so a repeated build skips the configuration phase; build-script code that runs at execution time must not reach back into project or layout.
  • Git for version control.
  • IDE of choice (IntelliJ IDEA, VS Code with Java extensions).
  • Compiles against Paper API 26.3.build.49-alpha (pinned in the version catalog); paper-plugin.yml declares api-version: "26.3". On every Paper build update, check the Paper internals listed under Coding Guidelines (§6).

3. Building & Testing

./gradlew clean build        # compiles + formatting check + tests + shaded jar, sources jar and checksums
./gradlew shadowJar          # only build the shaded Paper artifact (with its .sha256)
./gradlew test               # run unit tests (no running server needed)
./gradlew spotlessApply      # auto-format Java and resource files (build fails on unformatted code)

Artifacts land in dist/timberella-paper-<version>.jar and dist/sources/timberella-paper-<version>-sources.jar, each with a .sha256 file next to it (verify with sha256sum -c). Every jar task writes the checksums itself, so a partial build like shadowJar never leaves a jar without one. The cleanDist task wipes dist/ before every jar build (a lone shadowJar removes the sources jar too), so run the full build for a release and copy anything you want to keep elsewhere first.

  • Unit tests live under src/test/java. Prefer deterministic tests (no Bukkit mocks unless necessary). LocaleConsistencyTest runs TurtleLib's LocaleFileChecker: every locale must match en_US.yml in keys and MiniMessage tags, parse strictly and be translated, and en_US.yml must hold exactly the keys the code uses. SpeciesConfigConsistencyTest checks the built-in species defaults against the bundled config.yml; the legacy migration tests replay the 1.x upgrade against the files in legacy/ and older release files in src/test/resources/release-*/.
  • Manual QA: stand up a Paper test server, copy the shaded jar, and exercise commands and modules. Felling, replanting and anything else a player does needs a real client.
  • Performance: use timings or Spark to verify large tree operations remain below target thresholds; adjust leaves_decay.batch_size defaults cautiously.

4. Versioning & Release

  • version.properties holds the release version. If it is missing or has no version, Gradle falls back to a -Pversion=... project property, and finally to 1.0-SNAPSHOT. Commit it with every release so paper-plugin.yml and binary names stay in sync.
  • Tag releases with exactly the version from version.properties, without a v prefix (e.g. 2.0.0; pre-releases as 2.1.0-beta2, by convention without a dot before the number). Use the same string as the version number on Modrinth and Hangar. The update checker also reads common variants (v2.1.0, 2.1.0-beta.2), but stick to the format under "Version Format" on the turtle-lib wiki's VersionComparator page, as some spellings still sort unexpectedly (e.g. dates). Existing tags stay as they are.

Release checklist:

  1. Update the changelog and version.properties.
  2. Archive older jars from dist/ first (e.g. into dist-archive/), since the build wipes it; then run ./gradlew clean build and make sure all tests pass.
  3. Verify that THIRD_PARTY_LICENSES.md and src/main/resources/licenses/ reflect dependency changes; a new bundled library needs a license compatible with MIT distribution.
  4. Publish the jar from dist/ together with its .sha256 file (Modrinth, Hangar as applicable).
  5. Update README and wiki if user-facing behaviour changed.

5. Working with TurtleLib

  • By default, TurtleLib is resolved as turtle-lib:turtle-lib-paper:<version> from its tagged GitHub Release assets, so no checkout is needed (see gradle/libs.versions.toml for the pinned version and the ivy repository in build.gradle for how it's fetched; exclusiveContent makes that the only source for TurtleLib, and the Gradle wrapper checks its download against the official SHA-256). It is shaded into the jar, and its MIT license ships as licenses/turtle-lib-MIT.txt.
  • If Gradle can't find TurtleLib, check that the pinned version actually has a matching tag and release asset (turtle-lib-paper-<version>.jar) on turtle-lib.
  • Editing TurtleLib alongside Timberella: clone it as a sibling (git clone https://github.com/hrobasti/turtle-lib ../turtle-lib) and build with -PuseLocalTurtleLib=true. This switches settings.gradle/build.gradle to a local project dependency that picks up source changes automatically, no publish step needed.
  • Shared helpers used today: MessageService, UpdateChecker, ConfigWatcher, ConfigKeyMigrator, ConfigDefaultsInserter, LegacyDataUpgrade, FileChangeTracker, ServerMatcher, StartupBanner; in tests also LocaleFileChecker, which build.gradle excludes from the plugin jar.

6. Coding Guidelines

  • Language features: Java 25, prefer records/sealed classes only when they do not inflate bytecode for plugin targets.
  • Nullability: Use Objects.requireNonNull for constructor dependencies and explicit null checks for API inputs.
  • Logging: Route player-facing output and console status lines through MessageService (lang keys). Plain getLogger() is only for technical diagnostics such as exception logs.
  • MiniMessage: All user-visible strings live in lang/*.yml. Do not inline MiniMessage strings in code.
  • Configurability: Any new features must expose toggles/fallbacks in config.yml with sane defaults.
  • Other plugins' APIs (ItemsAdder, MMOItems, MythicCrucible, CraftEngine) are compileOnly and only touched from their adapter classes in net.kroet.timberella.compat, which load only when that plugin is enabled.
  • Paper internals: Timberella fires its own events through constructors that Paper marks @ApiStatus.Internal. In TreeChopListener, these are the BlockBreakEvent (as TimberBlockBreakEvent) for every further log in Felling.run() and for every leaf in scheduleLeavesDecay(), the BlockDropItemEvent for a further log's drops in dropItems(), and the BlockPlaceEvent in placeSapling(). They are created and fired through InternalEventGuard. If a Paper update breaks one of them (a LinkageError such as NoSuchMethodError), that log, leaf or sapling is left alone (a log's drops then fall without the drop event), and the console shows one warning per server run. Check these spots on every Paper build update, and keep new synthetic events behind the guard.

7. Localization Workflow

  1. Add keys to src/main/resources/lang/en_US.yml first.
  2. Translate the keys in all other locales. LocaleConsistencyTest fails on a value that is identical to en_US unless the key is on its identicalAllowed list, so don't leave English placeholders behind.
  3. Run the plugin once; MessageService syncs new keys into deployed plugins/Timberella/lang/ files without overwriting user customizations.
  4. Keep placeholders consistent (<prefix>, <prefix_label>, <player>, etc.). Add any new placeholder to PLACEHOLDERS in LocaleConsistencyTest and document it on the wiki Home page.

8. Contributing & FAQ

  • Fork the repository, create feature branches (feature/<topic>), and open pull requests against default.
  • Keep commits focused; reference issue numbers in commit messages when relevant.
  • Expect code review focusing on API stability, permissions security, and localization.
  • Contributions are accepted under the MIT License.
  • Parts of this plugin and its documentation were produced with AI assistance and reviewed by the maintainer before release; contributions are reviewed the same way, whoever or whatever wrote them.

Tests won’t run because Bukkit classes are missing. Unit tests may use server-independent Bukkit classes such as YamlConfiguration, which come from paper-api on the testImplementation classpath. Anything that needs a running server (worlds, blocks, players, the scheduler) needs lightweight stubs or a mocking library. Never spin up a server inside Gradle.

Shadow jar exploded to huge size — what changed? Check for accidental implementation scope dependencies (e.g., Paper API) or debug jars added to the runtime classpath. Only turtle-lib and bStats should be shaded — MiniMessage and Gson come in through the compileOnly Paper API, since Paper already provides them on the server classpath, and the custom-block plugin APIs are compileOnly as well.

How do I add a new config option safely? Add it to config.yml in snake_case, provide the same default in code, and document it on the wiki Home page. TimberellaPlugin merges missing keys into existing files without overwriting user values. When renaming a key, add an entry to LEGACY_CONFIG_KEY_MIGRATIONS (or LEGACY_LANG_KEY_MIGRATIONS) so existing files are rewritten in place.