Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 3 additions & 8 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,14 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java:
- 17
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v6
- name: 'Set up JDK ${{ matrix.java }}'
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: adopt
java-version: '${{ matrix.java }}'
distribution: temurin
java-version: '21'
- name: Cache Gradle
uses: actions/cache@v5
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
java-version: 21
cache: 'gradle'

- name: Grant execute permission for gradlew
Expand Down
16 changes: 16 additions & 0 deletions buildSrc/src/main/kotlin/commons-java-21.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
`java-library`
}

java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

tasks.compileJava {
options.encoding = "UTF-8"
}

tasks.compileTestJava {
options.encoding = "UTF-8"
}
60 changes: 60 additions & 0 deletions eternalcode-commons-bukkit-loom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# eternalcode-commons-bukkit-loom

Virtual Thread scheduler for Bukkit/Paper plugins.

## Dependency

```kotlin
// Gradle
implementation("com.eternalcode:eternalcode-commons-bukkit-loom:1.3.1")
```

```xml
<!-- Maven -->
<dependency>
<groupId>com.eternalcode</groupId>
<artifactId>eternalcode-commons-bukkit-loom</artifactId>
<version>1.3.1</version>
</dependency>
```

Repository: `https://repo.eternalcode.pl/releases`

## Requirements

- Java 21+
- Paper/Spigot 1.19+
- **Not for Folia** - use `folia-loom` instead

## Usage

```java
public class MyPlugin extends JavaPlugin {
private BukkitLoomScheduler scheduler;

@Override
public void onEnable() {
this.scheduler = BukkitLoomScheduler.create(this);
}

@Override
public void onDisable() {
this.scheduler.shutdown(Duration.ofSeconds(5));
}

public void loadData(Player player) {
scheduler.supplyAsync(() -> database.load(player.getUniqueId()))
.thenAcceptSync(data -> {
// safe - main thread
player.sendMessage("Loaded: " + data);
})
.exceptionally(e -> getLogger().severe(e.getMessage()));
}
}
```

## Rules

- `Async` methods → Virtual Thread (no Bukkit API!)
- `Sync` methods → Main Thread (Bukkit API safe)
- Use `.thenAcceptSync()` to switch from async to sync
19 changes: 19 additions & 0 deletions eternalcode-commons-bukkit-loom/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
`commons-java-21`
`commons-publish`
`commons-repositories`
}

sourceSets {
main {
java.setSrcDirs(listOf("src/main/java"))
}
}

dependencies {
api(project(":eternalcode-commons-loom"))

compileOnly("org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT")

api("org.jetbrains:annotations:26.0.2-1")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.eternalcode.commons.bukkit.scheduler;

import com.eternalcode.commons.scheduler.loom.LoomFuture;
import com.eternalcode.commons.scheduler.loom.LoomScheduler;
import com.eternalcode.commons.scheduler.loom.LoomSchedulerImpl;
import com.eternalcode.commons.scheduler.loom.LoomTask;
import org.bukkit.plugin.Plugin;

import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
* Bukkit wrapper for LoomScheduler.
* Create in onEnable(), shutdown in onDisable().
*/
public final class BukkitLoomScheduler implements LoomScheduler {

private final BukkitMainThreadDispatcher dispatcher;
private final LoomSchedulerImpl delegate;
private final Plugin plugin;

private BukkitLoomScheduler(Plugin plugin, BukkitMainThreadDispatcher dispatcher) {
this.plugin = plugin;
this.dispatcher = dispatcher;
this.delegate = new LoomSchedulerImpl(dispatcher);
}

public static BukkitLoomScheduler create(Plugin plugin) {
if (!plugin.getServer().isPrimaryThread()) {
throw new IllegalStateException("BukkitLoomScheduler must be created on main thread");
}
return new BukkitLoomScheduler(plugin, new BukkitMainThreadDispatcher(plugin));
}

@Override
public LoomTask runAsync(Runnable task) {
return this.delegate.runAsync(task);
}

@Override
public <T> LoomFuture<T> supplyAsync(Supplier<T> supplier) {
return this.delegate.supplyAsync(supplier);
}

@Override
public <T> LoomFuture<T> callAsync(Callable<T> callable) {
return this.delegate.callAsync(callable);
}

@Override
public LoomTask runAsyncLater(Runnable task, Duration delay) {
return this.delegate.runAsyncLater(task, delay);
}

@Override
public LoomTask runAsyncTimer(Runnable task, Duration delay, Duration period) {
return this.delegate.runAsyncTimer(task, delay, period);
}

@Override
public LoomTask runSync(Runnable task) {
return this.delegate.runSync(task);
}

@Override
public <T> LoomFuture<T> supplySync(Supplier<T> supplier) {
return this.delegate.supplySync(supplier);
}

@Override
public LoomTask runSyncLater(Runnable task, Duration delay) {
return this.delegate.runSyncLater(task, delay);
}

@Override
public LoomTask runSyncTimer(Runnable task, Duration delay, Duration period) {
return this.delegate.runSyncTimer(task, delay, period);
}

@Override
public <T> LoomTask runAsyncThenSync(Supplier<T> asyncSupplier, Consumer<T> syncConsumer) {
return this.delegate.runAsyncThenSync(asyncSupplier, syncConsumer);
}

@Override
public <T, R> LoomTask runAsyncThenSync(Supplier<T> asyncSupplier, Function<T, R> transformer,
Consumer<R> syncConsumer) {
return this.delegate.runAsyncThenSync(asyncSupplier, transformer, syncConsumer);
}

@Override
public LoomFuture<Void> delay(Duration duration) {
return this.delegate.delay(duration);
}

@Override
public boolean isMainThread() {
return this.dispatcher.isMainThread();
}

@Override
public boolean shutdown(Duration timeout) {
this.dispatcher.shutdown();
return this.delegate.shutdown(timeout);
}

@Override
public void shutdownNow() {
this.dispatcher.shutdown();
this.delegate.shutdownNow();
}

public Plugin getPlugin() {
return this.plugin;
}

public int getPendingSyncTasks() {
return this.dispatcher.getPendingCount();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.eternalcode.commons.bukkit.scheduler;

import com.eternalcode.commons.scheduler.loom.MainThreadDispatcher;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;

import java.util.concurrent.ConcurrentLinkedQueue;

/**
* Bukkit implementation - queues tasks from VT to main thread.
*/
public final class BukkitMainThreadDispatcher implements MainThreadDispatcher {

private final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
private final Plugin plugin;
private final BukkitScheduler bukkitScheduler;
private final BukkitTask tickTask;

public BukkitMainThreadDispatcher(Plugin plugin) {
this.plugin = plugin;
this.bukkitScheduler = plugin.getServer().getScheduler();
this.tickTask = this.bukkitScheduler.runTaskTimer(this.plugin, this::drainQueue, 1L, 1L);
}

private void drainQueue() {
Runnable task;
while ((task = this.queue.poll()) != null) {
try {
task.run();
} catch (Throwable t) {
this.plugin.getLogger().severe("Exception in sync task: " + t.getMessage());
t.printStackTrace();
}
}
}

@Override
public void dispatch(Runnable task) {
if (isMainThread()) {
try {
task.run();
} catch (Throwable t) {
this.plugin.getLogger().severe("Exception in sync task: " + t.getMessage());
t.printStackTrace();
}
return;
}
this.queue.offer(task);
}

@Override
public boolean isMainThread() {
return this.plugin.getServer().isPrimaryThread();
}

@Override
public void dispatchLater(Runnable task, long ticks) {
this.bukkitScheduler.runTaskLater(this.plugin, task, ticks);
}

@Override
public Cancellable dispatchTimer(Runnable task, long delay, long period) {
BukkitTask t = this.bukkitScheduler.runTaskTimer(this.plugin, task, delay, period);
return t::cancel;
}

public void shutdown() {
this.tickTask.cancel();
Runnable task;
while ((task = this.queue.poll()) != null) {
try {
task.run();
} catch (Throwable t) {
this.plugin.getLogger().severe("Exception in shutdown task: " + t.getMessage());
}
}
}

public int getPendingCount() {
return this.queue.size();
}
}
Loading