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; + } + } }