diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index 4ff26ce52..b68555040 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -43,6 +43,9 @@ dependencies {
api(libs.brigadier)
api(libs.bundles.configurate4)
api(libs.caffeine)
+
+ compileOnly(libs.auto.service.annotations)
+ annotationProcessor(libs.auto.service)
}
tasks {
diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java
index c1788da5c..9f24b6cb9 100644
--- a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java
+++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java
@@ -7,6 +7,7 @@
package com.velocitypowered.api.plugin.ap;
+import com.google.auto.service.AutoService;
import com.google.gson.Gson;
import com.velocitypowered.api.plugin.Plugin;
import java.io.BufferedWriter;
@@ -16,6 +17,7 @@ import java.util.Objects;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
@@ -30,6 +32,7 @@ import javax.tools.StandardLocation;
/**
* Annotation processor for Velocity.
*/
+@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.velocitypowered.api.plugin.Plugin"})
public class PluginAnnotationProcessor extends AbstractProcessor {
diff --git a/api/src/ap/resources/META-INF/services/javax.annotation.processing.Processor b/api/src/ap/resources/META-INF/services/javax.annotation.processing.Processor
deleted file mode 100644
index a96abf5d2..000000000
--- a/api/src/ap/resources/META-INF/services/javax.annotation.processing.Processor
+++ /dev/null
@@ -1 +0,0 @@
-com.velocitypowered.api.plugin.ap.PluginAnnotationProcessor
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f0e89ed57..ec7542507 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,6 +14,8 @@ spotless = "com.diffplug.spotless:6.12.0"
adventure-bom = "net.kyori:adventure-bom:4.15.0"
adventure-facet = "net.kyori:adventure-platform-facet:4.3.0"
asm = "org.ow2.asm:asm:9.5"
+auto-service = "com.google.auto.service:auto-service:1.0.1"
+auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.0.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.5"
diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts
index b3026977a..0edd10cce 100644
--- a/proxy/build.gradle.kts
+++ b/proxy/build.gradle.kts
@@ -126,5 +126,8 @@ dependencies {
implementation(libs.asm)
implementation(libs.bundles.flare)
compileOnly(libs.spotbugs.annotations)
+ compileOnly(libs.auto.service.annotations)
testImplementation(libs.mockito)
+
+ annotationProcessor(libs.auto.service)
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java
new file mode 100644
index 000000000..f8a27d1fd
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 Velocity Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.adventure;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Expiry;
+import com.github.benmanes.caffeine.cache.Scheduler;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.event.ClickCallback;
+import org.checkerframework.checker.index.qual.NonNegative;
+
+/**
+ * Click callback manager.
+ */
+public class ClickCallbackManager {
+ public static final ClickCallbackManager INSTANCE = new ClickCallbackManager();
+
+ static final String COMMAND = "/velocity callback ";
+
+ private final Cache registrations = Caffeine.newBuilder()
+ .expireAfter(new Expiry() {
+ @Override
+ public long expireAfterCreate(UUID key, RegisteredCallback value, long currentTime) {
+ return value.duration().toNanos();
+ }
+
+ @Override
+ public long expireAfterUpdate(UUID key, RegisteredCallback value, long currentTime,
+ @NonNegative long currentDuration) {
+ return currentDuration;
+ }
+
+ @Override
+ public long expireAfterRead(UUID key, RegisteredCallback value, long currentTime,
+ @NonNegative long currentDuration) {
+ final AtomicInteger remainingUses = value.remainingUses();
+ if (remainingUses != null && remainingUses.get() <= 0) {
+ return 0;
+ }
+ return currentDuration;
+ }
+ })
+ .scheduler(Scheduler.systemScheduler())
+ .build();
+
+ private ClickCallbackManager() {
+ }
+
+ /**
+ * Run a callback.
+ *
+ * @param audience the audience
+ * @param id the callback's ID
+ * @return {@code true} if the callback was run, {@code false} if not
+ */
+ public boolean runCallback(final Audience audience, final UUID id) {
+ final RegisteredCallback callback = this.registrations.getIfPresent(id);
+ if (callback != null && callback.tryUse()) {
+ callback.callback().accept(audience);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Registers a click callback.
+ *
+ * @param callback the callback to register
+ * @param options associated options
+ * @return the callback ID
+ */
+ public UUID register(
+ final ClickCallback callback,
+ final ClickCallback.Options options
+ ) {
+ final UUID id = UUID.randomUUID();
+ final RegisteredCallback registration = new RegisteredCallback(
+ options.lifetime(),
+ options.uses(),
+ callback
+ );
+ this.registrations.put(id, registration);
+ return id;
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackProviderImpl.java b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackProviderImpl.java
new file mode 100644
index 000000000..4632d53ae
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackProviderImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 Velocity Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.adventure;
+
+import com.google.auto.service.AutoService;
+import java.util.UUID;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.event.ClickCallback;
+import net.kyori.adventure.text.event.ClickEvent;
+
+/**
+ * Implementation of {@link ClickCallback.Provider}.
+ */
+@AutoService(ClickCallback.Provider.class)
+@SuppressWarnings("UnstableApiUsage") // permitted provider
+public class ClickCallbackProviderImpl implements ClickCallback.Provider {
+ @Override
+ public ClickEvent create(
+ final ClickCallback callback,
+ final ClickCallback.Options options
+ ) {
+ final UUID id = ClickCallbackManager.INSTANCE.register(callback, options);
+ return ClickEvent.runCommand(ClickCallbackManager.COMMAND + id);
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/adventure/RegisteredCallback.java b/proxy/src/main/java/com/velocitypowered/proxy/adventure/RegisteredCallback.java
new file mode 100644
index 000000000..508dd306b
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/adventure/RegisteredCallback.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 Velocity Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.adventure;
+
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicInteger;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.event.ClickCallback;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+@NullMarked
+record RegisteredCallback(
+ Duration duration,
+ @Nullable AtomicInteger remainingUses,
+ ClickCallback callback
+) {
+ RegisteredCallback(
+ final Duration duration,
+ final int maxUses,
+ final ClickCallback callback
+ ) {
+ this(
+ duration,
+ maxUses == ClickCallback.UNLIMITED_USES
+ ? null
+ : new AtomicInteger(maxUses),
+ callback
+ );
+ }
+
+ boolean tryUse() {
+ if (this.remainingUses != null) {
+ return this.remainingUses.decrementAndGet() >= 0;
+ }
+ return true;
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java
index 93b27243b..839537e89 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java
@@ -31,6 +31,7 @@ import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.ProxyVersion;
import com.velocitypowered.proxy.VelocityServer;
+import com.velocitypowered.proxy.adventure.ClickCallbackManager;
import com.velocitypowered.proxy.util.InformationUtils;
import java.io.BufferedWriter;
import java.io.IOException;
@@ -49,6 +50,7 @@ import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import net.kyori.adventure.text.Component;
@@ -93,6 +95,7 @@ public class VelocityCommand implements SimpleCommand {
.put("reload", new Reload(server))
.put("dump", new Dump(server))
.put("heap", new Heap())
+ .put("callback", new Callback())
.build();
}
@@ -485,4 +488,31 @@ public class VelocityCommand implements SimpleCommand {
}
}
+
+ /**
+ * Callback SubCommand.
+ */
+ public static class Callback implements SubCommand {
+
+ @Override
+ public void execute(final CommandSource source, final String @NonNull [] args) {
+ if (args.length != 1) {
+ return;
+ }
+
+ final UUID id;
+ try {
+ id = UUID.fromString(args[0]);
+ } catch (final IllegalArgumentException ignored) {
+ return;
+ }
+
+ ClickCallbackManager.INSTANCE.runCallback(source, id);
+ }
+
+ @Override
+ public boolean hasPermission(final CommandSource source, final String @NonNull [] args) {
+ return true;
+ }
+ }
}