Framework-Plugin für PostgreSQL und Redis. ByteOctopus wird einmal auf Paper und Velocity installiert und dort mit Zugangsdaten konfiguriert – alle anderen Plugins binden nur noch das API-Modul über JitPack ein und arbeiten mit Repositories, Queries und Pub/Sub-Channels. Kein eigener Hikari-Pool, kein Jedis-Setup, kein SQL.
de.j0byte.octopus.api → API (JitPack, compileOnly im Consumer-Plugin)
de.j0byte.octopus.common → Implementierung (Hikari, JDBC, Jedis, Gson)
de.j0byte.octopus.paper → ByteOctopus-Paper.jar
de.j0byte.octopus.velocity → ByteOctopus-Velocity.jar
Ausführliche Doku für Plugin-Entwickler: docs/
| Einstieg | Dependency, Plugin-Dependency, erster Zugriff |
| Datenbank | Entities, Tabellen, Schreiben, Lesen |
| Queries | Filter, Sortierung, Massen-Updates |
| Messaging | Channels, publish, subscribe |
| Cache | Key-Value, Hashes, Locks |
| Async & Threading | CompletableFuture, Callbacks im Tick |
| Rezepte | Fertige Lösungen |
| Fehler & FAQ | Was schiefgeht und warum |
| API-Referenz | Alle Methoden auf einen Blick |
./gradlew buildpaper/build/libs/ByteOctopus-Paper-1.0.0.jarauf jeden Paper/Leaf-Server,velocity/build/libs/ByteOctopus-Velocity-1.0.0.jarauf den Proxy.- Auf Paper zusätzlich das CommandAPI-Plugin (Standalone) installieren – ByteOctopus lädt sonst nicht.
- Einmal starten,
plugins/ByteOctopus/config.jsonausfüllen,/octopus reload.
Eine Datei pro Plugin, alles Konfigurierbare an einer Stelle:
{
"serverId": "auto",
"database": { "host": "127.0.0.1", "port": 5432, "database": "blockbucht",
"username": "blockbucht", "password": "changeme", "schema": "public",
"poolSize": 10, "minimumIdle": 2, "connectionTimeoutMs": 10000,
"idleTimeoutMs": 600000, "maxLifetimeMs": 1800000, "ssl": false,
"properties": {} },
"redis": { "host": "127.0.0.1", "port": 6379, "username": "", "password": "",
"database": 0, "timeoutMs": 5000, "poolSize": 8, "ssl": false,
"keyPrefix": "blockbucht:", "channelPrefix": "blockbucht:" },
"async": { "threads": 4 },
"messages": { "prefix": "<gray>[<aqua>Octopus</aqua><gray>]</gray> ", "...": "..." }
}serverId: "auto"liestSIMPLECLOUD_SERVICE_NAME,SERVICE_NAME,SERVER_NAME,HOSTNAMEund fällt sonst auf den Hostnamen zurück. Diese Kennung steht als Absender in jeder Redis-Nachricht.- Alle Texte sind MiniMessage; auf Paper laufen sie vorher durch PlaceholderAPI
(
setPlaceholders→deserialize), damit%img_prefix%und Co. funktionieren. - Fehlende Schlüssel werden beim Start mit Defaults ergänzt und zurückgeschrieben.
Befehle: /octopus status, /octopus reload (Permission octopus.admin).
repositories {
maven("https://jitpack.io")
}
dependencies {
compileOnly("com.github.xJ0byte.Octopus:api:1.0.0") // Tag oder Commit-Hash
}Paper (paper-plugin.yml):
dependencies:
server:
ByteOctopus:
load: BEFORE
required: true
join-classpath: trueVelocity: @Plugin(id = "meinplugin", dependencies = @Dependency(id = "byteoctopus")).
Zugriff wahlweise statisch oder per Guice:
Octopus octopus = Octopus.get();
// oder
Injector injector = Guice.createInjector(new OctopusModule(), new MyModule());@Table("coin_profiles")
@Index({"coins"})
public class CoinProfile {
@Id private UUID uuid;
@Column(nullable = false) private String name;
private long coins;
@Json private List<String> unlockedKits = new ArrayList<>();
private Instant lastSeen;
public CoinProfile() {} // parameterloser Konstruktor, darf private sein
}records funktionieren ebenfalls (dann ohne @Id(generated = true)).
Feldnamen werden zu snake_case, Typen automatisch abgeleitet:
| Java | PostgreSQL |
|---|---|
UUID |
UUID |
String, enum |
TEXT |
int / long / double |
INTEGER / BIGINT / DOUBLE PRECISION |
boolean |
BOOLEAN |
Instant |
TIMESTAMPTZ |
byte[] |
BYTEA |
@Json + alles andere |
JSONB |
Repository<CoinProfile> profiles = octopus.database().repository(CoinProfile.class);
profiles.createTable().join(); // CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTSDynamisch, ohne Entity-Klasse:
octopus.database().schema().create(
TableDefinition.builder("shop_logs")
.column(ColumnDefinition.of("id", ColumnType.BIGSERIAL))
.column(ColumnDefinition.of("player", ColumnType.UUID).notNull())
.primaryKey("id")
.index("idx_shop_logs_player", false, "player")
.build());Oder klassisch nach eurer Konvention aus src/main/resources/schema.sql:
octopus.database().schema().applyResource(getClass().getClassLoader(), "schema.sql");schema().renderCreate(CoinProfile.class) gibt das erzeugte DDL zurück, ohne es
auszuführen – praktisch zum Gegenprüfen.
profiles.insert(profile); // INSERT
profiles.save(profile); // Upsert über den Primärschlüssel
profiles.saveAll(list); // Batch-Upsert in einer Transaktion
profiles.update(profile); // UPDATE ... WHERE id = ?
profiles.delete(profile);
profiles.deleteById(uuid);
profiles.findById(uuid); // CompletableFuture<Optional<CoinProfile>>
profiles.findAll();
profiles.findBy("name", "Jona");
profiles.count();
profiles.existsById(uuid);profiles.query()
.where(Filter.gte("coins", 1000))
.and(Filter.or(Filter.likeIgnoreCase("name", "jo%"),
Filter.isNull("lastSeen")))
.orderBy("coins", Order.DESC)
.limit(10)
.findAll();Verfügbare Filter: eq, ne, gt, gte, lt, lte, like, likeIgnoreCase,
between, in, notIn, isNull, notNull, and, or, not.
Werte werden immer als Prepared-Statement-Parameter gebunden – kein String-Bau,
keine Injection.
Massen-Update und -Delete ohne Objekte zu laden:
profiles.query().where(Filter.lt("lastSeen", cutoff)).set("coins", 0).executeUpdate();
profiles.query().where(Filter.eq("coins", 0)).delete();Escape-Hatch, falls doch mal echtes SQL nötig ist:
octopus.database().withConnection(connection -> { ... });
octopus.database().transaction(connection -> { ... }); // mit RollbackAlle DB- und Redis-Methoden laufen im Async-Pool von Octopus und geben ein
CompletableFuture zurück. Ergebnis auf dem Main-Thread verarbeiten:
octopus.scheduler().callback(
profiles.findById(player.getUniqueId()),
found -> player.sendMessage(...)); // läuft im Server-TickAuf Velocity gibt es keinen Main-Thread – callback führt dort direkt im Pool aus.
Channel channel = octopus.messaging().channel("economy:balances");
Subscription subscription = channel.subscribe(BalanceChanged.class, message -> {
message.payload(); // deserialisiertes Objekt
message.source(); // serverId des Absenders
message.timestamp();
});
channel.publish(new BalanceChanged(uuid, 500, "shop"));- Payloads werden als JSON übertragen und mit Absender + Zeitstempel verpackt.
- Ein Server empfängt seine eigenen Nachrichten nicht
(
subscribeIncludingSelf(...), wenn doch gewünscht). - Handler laufen im Async-Pool, nicht auf dem Listener-Thread und nicht im Tick.
- Ein einziger Listener-Thread hält die Verbindung und baut sie bei Abbruch neu auf.
- Beim Deaktivieren des Plugins
subscription.unsubscribe()aufrufen.
octopus.cache().set("rank:" + uuid, rank, Duration.ofMinutes(10));
octopus.cache().get("rank:" + uuid, Rank.class);
octopus.cache().increment("kills:" + uuid, 1);
octopus.cache().hset("party:" + id, "leader", uuid.toString());
octopus.cache().tryLock("daily-reset", Duration.ofMinutes(5))
.thenAccept(lock -> lock.ifPresent(l -> { /* nur ein Server läuft hier rein */ }));Alle Keys bekommen den keyPrefix aus der config.json.
./gradlew build # baut alle Module + Shadow-Jars
./gradlew :api:publishToMavenLocalGeshadet und relocated nach de.j0byte.octopus.libs.* werden HikariCP, der
PostgreSQL-Treiber, Jedis, commons-pool2 und org.json. Gson und SLF4J kommen vom Server.
jitpack.yml liegt im Repo, JitPack baut :api:publishToMavenLocal mit JDK 21.
Nach einem Git-Tag ist die Version als com.github.xJ0byte.Octopus:api:<tag> verfügbar.
Diese Versionen stammen aus gradle/libs.versions.toml und sollten einmal gegen die
Repos gegengecheckt werden:
| Artefakt | Version |
|---|---|
io.papermc.paper:paper-api |
1.21.11-R0.1-SNAPSHOT |
com.velocitypowered:velocity-api |
3.4.0-SNAPSHOT |
com.zaxxer:HikariCP |
7.1.0 |
org.postgresql:postgresql |
42.7.12 |
redis.clients:jedis |
6.0.0 |
dev.jorel:commandapi-bukkit-core |
11.2.0 |
me.clip:placeholderapi |
2.11.6 |
studio.o7.remora (Plugin) |
0.2.5 (aktuell wäre 0.4.0) |
com.gradleup.shadow (Plugin) |
9.4.2 |
Der Jedis-Pool wird in RedisProvider über
new JedisPool(JedisPoolConfig, HostAndPort, JedisClientConfig) gebaut – falls Jedis 6
diese Signatur geändert hat, ist das die einzige Stelle, die angepasst werden muss.