Skip to content

Addon Quickstart - Register a Custom Shape

This is the shortest path from nothing to a working RTP addon that registers a custom region shape. It is the "hello world" companion to the router in FOR_ADDON_DEVELOPERS.md and the fuller walkthrough in addons/LeafRTPCountdownAddon/README.md.

An RTP addon is platform-agnostic: it implements the RTPAddon SPI, is discovered via ServiceLoader, and runs unchanged on Bukkit / Spigot / Paper / Folia and Fabric. No org.bukkit.* imports, no plugin loader. See ADR-057 for the loading model and ADR-051 for why shape registration is the typed RTP.addShape(Shape<?>) call.


1. Depend on RTP (build.gradle)

Compile against rtp-api (the RTPAddon SPI) and rtp-core (the Shape hierarchy and the RTP facade). Both are compileOnly - RTP provides them at runtime.

In-repo addon (a sub-project of this build):

dependencies {
    compileOnly project(':rtp-api')   // RTPAddon SPI
    compileOnly project(':rtp-core')  // RTP.addShape, Shape, built-in shapes
}

Building outside this repository? Pull the published artifacts from JitPack instead:

repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }
}

dependencies {
    compileOnly 'com.github.DailyStruggle.RTP:rtp-api:3.0.1'   // RTPAddon SPI
    compileOnly 'com.github.DailyStruggle.RTP:rtp-core:3.0.1'  // RTP.addShape, Shape, built-in shapes
}

(3.0.1 is a git tag; a branch like master-SNAPSHOT or a commit SHA also works. See dev/PUBLISHING.md for the publishing setup and the Maven Central path.)


2. Register the addon for ServiceLoader

Create one file so RTP can discover your addon on every platform:

src/main/resources/META-INF/services/io.github.dailystruggle.rtp.api.addon.RTPAddon

with a single line naming your implementation class:

com.example.myrtpaddon.MyRtpAddon

3. Register a custom shape in ~20 lines

Register your shape from onLoad() (runs once, after rtp-core is up). A genuine custom shape changes the geometry in code, not just a re-configured clone. Below, DiamondShape subclasses Square and overrides select() to rotate each pick 45 degrees into a diamond - a bounded transform, no reroll (ADR-001).

Because bounds are Chebyshev (max-axis), rotation alone overshoots radius by ~1.41x, so select() also divides by sqrt(2) to refit. And since the base bounds test Shape.contains(int x, int z) still speaks the square parameterization, override it to invert the rotation first - exactly as the built-in Polygon refines super.contains(...).

package com.example.myrtpaddon;

import io.github.dailystruggle.rtp.api.addon.RTPAddon;
import io.github.dailystruggle.rtp.common.RTP;
import io.github.dailystruggle.rtp.common.selection.region.selectors.memory.shapes.Square;

/** A square spawn ring rotated 45 degrees into a diamond, computed programmatically. */
public final class DiamondShape extends Square {

  public DiamondShape() {
    super("DIAMOND"); // operators select it as shape=DIAMOND, like any built-in
  }

  @Override
  public int[] select() {
    // Rotate the square pick 45 degrees; the extra 1/sqrt(2) refits the
    // diamond onto the configured radius (max-axis bounds), no reroll.
    int[] xz = super.select();
    double inv = 1.0 / 2.0; // (1/sqrt(2)) for the rotation, times (1/sqrt(2)) to re-fit bounds
    int rx = (int) Math.round((xz[0] - xz[1]) * inv);
    int rz = (int) Math.round((xz[0] + xz[1]) * inv);
    return new int[] {rx, rz};
  }

  @Override
  public boolean contains(int x, int z) {
    // Invert select()'s rotation, then use the parent's square bounds test.
    return super.contains(x + z, z - x);
  }
}

Register the instance from your RTPAddon.onLoad():

public final class MyRtpAddon implements RTPAddon {
  @Override
  public void onLoad() {
    RTP.addShape(new DiamondShape());
    RTP.log(java.util.logging.Level.INFO, "[MyRtpAddon] registered shape DIAMOND");
  }
}

That is the whole addon. Build it, drop the jar on RTP's classpath (see dev/ADDON_LOADING.md), and DIAMOND is available everywhere a built-in shape is. Because select() is overridden the new geometry comes entirely from your code; the inherited radius / centerRadius knobs still tune the size of the ring you rotate.


4. Fully custom geometry (when a re-config is not enough)

The select() override above reuses the parent square's spiral mapping. To define brand-new geometry from scratch - so that the bad-location cache, uniqueplacements, and the scan bitmap all stay consistent with your shape - extend MemoryShape<E extends Enum<E>> (the base for all spiral-mapped shapes) and implement its xzToLocation / locationToXZ / getRange / rand contract as a matched pair, then register the instance with the same RTP.addShape(...) call. Read dev/CONCEPTS.md (the spiral 1D mapping) and ADR-001 first - the bounded-distribution contract is mandatory, and unbounded reroll loops are prohibited.


5. Clean up in onUnload()

If your real addon allocates anything (scheduled tasks via RTP.scheduler, chunk tickets, DB writes), release it in onUnload(). A pure shape registration needs no teardown.

@Override
public void onUnload() {
  // cancel tasks / release tickets / flush state here
}

Where to go next