From 350b14ecfa7f3772eaa378406beda62467097c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossi=20Erkkil=C3=A4?= Date: Thu, 15 Jan 2026 18:08:35 +0200 Subject: [PATCH 1/3] Implement Adventure ClickEvent.callback --- SpongeAPI | 2 +- .../common/adventure/CallbackCommand.java | 67 ++---------------- .../adventure/ClickCallbackProviderImpl.java | 40 +++++++++++ .../common/adventure/SpongeAdventure.java | 69 ++++++++++++++++++- .../common/scheduler/ServerScheduler.java | 6 ++ ...dventure.text.event.ClickCallback$Provider | 1 + .../test/command/CommandTest.java | 4 +- 7 files changed, 120 insertions(+), 69 deletions(-) create mode 100644 src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java create mode 100644 src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider diff --git a/SpongeAPI b/SpongeAPI index cbf8f6fa5d6..ebde41215c3 160000 --- a/SpongeAPI +++ b/SpongeAPI @@ -1 +1 @@ -Subproject commit cbf8f6fa5d6b874dfaa8998f4f1abc41c5fee6f8 +Subproject commit ebde41215c37c1c8e06a43ac6e8535de8e406aa7 diff --git a/src/main/java/org/spongepowered/common/adventure/CallbackCommand.java b/src/main/java/org/spongepowered/common/adventure/CallbackCommand.java index 28e40258c3b..24543ee5a58 100644 --- a/src/main/java/org/spongepowered/common/adventure/CallbackCommand.java +++ b/src/main/java/org/spongepowered/common/adventure/CallbackCommand.java @@ -24,93 +24,34 @@ */ package org.spongepowered.common.adventure; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.leangen.geantyref.TypeToken; import net.kyori.adventure.text.Component; -import org.checkerframework.checker.nullness.qual.NonNull; import org.spongepowered.api.command.Command; -import org.spongepowered.api.command.CommandCause; -import org.spongepowered.api.command.CommandCompletion; import org.spongepowered.api.command.CommandResult; -import org.spongepowered.api.command.exception.ArgumentParseException; -import org.spongepowered.api.command.parameter.ArgumentReader; -import org.spongepowered.api.command.parameter.CommandContext; import org.spongepowered.api.command.parameter.Parameter; -import org.spongepowered.api.command.parameter.managed.ValueParameter; -import org.spongepowered.common.command.SpongeCommandCompletion; -import java.time.Duration; -import java.util.List; -import java.util.Optional; import java.util.UUID; -import java.util.function.Consumer; -import java.util.stream.Collectors; public final class CallbackCommand { public static final String NAME = "callback"; public static final CallbackCommand INSTANCE = new CallbackCommand(); - private final Cache> callbacks = Caffeine.newBuilder() - .expireAfterWrite(Duration.ofMinutes(10)) - .build(); - private CallbackCommand() { } public Command.Parameterized createCommand() { - this.callbacks.invalidateAll(); + SpongeAdventure.invalidateCallbacks(); - final Parameter.Key> key = Parameter.key("key", new TypeToken>() {}); + final Parameter.Key key = Parameter.key("key", new TypeToken<>() {}); return Command.builder() .shortDescription(Component.text("Execute a callback registered as part of a TextComponent. Primarily for internal use")) - .addParameter(Parameter.builder(key).addParser(new CallbackValueParameter()).build()) + .addParameter(Parameter.uuid().key(key).build()) .executor(context -> { - context.requireOne(key).accept(context.cause()); + SpongeAdventure.runCallback(context.requireOne(key), context.cause()); return CommandResult.success(); }) .build(); } - public UUID registerCallback(final Consumer callback) { - final UUID key = UUID.randomUUID(); - this.callbacks.put(key, callback); - return key; - } - - private final class CallbackValueParameter implements ValueParameter> { - @Override - public List complete(final @NonNull CommandContext context, final @NonNull String currentInput) { - return CallbackCommand.this.callbacks - .asMap() - .keySet() - .stream() - .map(UUID::toString) - .filter(string -> string.startsWith(currentInput)) - .map(SpongeCommandCompletion::new) - .collect(Collectors.toList()); - } - - @Override - public @NonNull Optional> parseValue( - final Parameter.@NonNull Key> parameterKey, - final ArgumentReader.@NonNull Mutable reader, - final CommandContext.@NonNull Builder context - ) throws ArgumentParseException { - final String next = reader.parseString(); - try { - final UUID id = UUID.fromString(next); - final Consumer ret = CallbackCommand.this.callbacks.getIfPresent(id); - if (ret == null) { - throw reader.createException(Component.text( - "The callback you provided was not valid. Keep in mind that callbacks will expire after 10 " + - "minutes, so you might want to consider clicking faster next time!")); - } - return Optional.of(ret); - } catch (final IllegalArgumentException ex) { - throw reader.createException(Component.text("Input " + next + " was not a valid UUID")); - } - } - } } diff --git a/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java b/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java new file mode 100644 index 00000000000..afeb7fec8fe --- /dev/null +++ b/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java @@ -0,0 +1,40 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.adventure; + +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.event.ClickCallback; +import net.kyori.adventure.text.event.ClickEvent; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("UnstableApiUsage") // permitted provider +public class ClickCallbackProviderImpl implements ClickCallback.Provider { + + @Override + public @NotNull ClickEvent create(@NotNull ClickCallback callback, ClickCallback.@NotNull Options options) { + return SpongeAdventure.createCallbackClickEvent(options, cause -> callback.accept(cause.audience())); + } + +} diff --git a/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java b/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java index ef073687f61..72b97acb7af 100644 --- a/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java +++ b/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java @@ -48,6 +48,7 @@ import net.kyori.adventure.text.StorageNBTComponent; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.event.ClickCallback; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.DataComponentValue; import net.kyori.adventure.text.event.HoverEvent; @@ -112,6 +113,8 @@ import org.spongepowered.common.launch.Launch; import java.io.IOException; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -756,6 +759,65 @@ private record SpongeDataComponentValue(Optional value) implements DataCom } + // ----------------------- + // ---- ClickCallback ---- + // ----------------------- + + private static final Map CALLBACKS = new ConcurrentHashMap<>(); + + public static void invalidateCallbacks() { + CALLBACKS.clear(); + } + + public static void runCallbackHousekeeping() { + CALLBACKS.values().removeIf(callback -> !callback.isValid()); + } + + public static void runCallback(UUID uuid, CommandCause cause) { + final var callback = CALLBACKS.get(uuid); + + if (callback == null) { + return; + } + + if (callback.isValid()) { + callback.useAndRecord(cause); + return; + } + + CALLBACKS.remove(uuid, callback); + } + + public static ClickEvent createCallbackClickEvent(ClickCallback.Options options, Consumer callback) { + final UUID key = UUID.randomUUID(); + CALLBACKS.put(key, new StoredClickCallback(options, callback)); + return ClickEvent.runCommand(String.format("/%s:%s %s", Launch.instance().id(), CallbackCommand.NAME, key)); + } + + private static class StoredClickCallback { + private final ClickCallback.Options options; + private final Consumer handler; + private final Instant expiryTime; + private int useCounter = 0; + + public StoredClickCallback(ClickCallback.Options options, Consumer handler) { + this.options = options; + this.handler = handler; + this.expiryTime = Instant.now().plus(options.lifetime()); + } + + private void useAndRecord(CommandCause cause) { + this.useCounter++; + this.handler.accept(cause); + } + + private boolean isValid() { + return (this.options.uses() == ClickCallback.UNLIMITED_USES || this.useCounter < this.options.uses()) && + Instant.now().compareTo(expiryTime) < 0; + } + + } + // Key public static ResourceLocation asVanilla(final Key key) { @@ -827,9 +889,10 @@ public static Iterable unpackAudiences(final Audience audien public static class Factory implements SpongeComponents.Factory { @Override public @NonNull ClickEvent callbackClickEvent(final @NonNull Consumer callback) { - Objects.requireNonNull(callback); - final UUID key = CallbackCommand.INSTANCE.registerCallback(callback); - return ClickEvent.runCommand(String.format("/%s:%s %s", Launch.instance().id(), CallbackCommand.NAME, key)); + return SpongeAdventure.createCallbackClickEvent(ClickCallback.Options.builder() + .uses(ClickCallback.UNLIMITED_USES) + .lifetime(Duration.ofMinutes(10)) + .build(), callback); } @Override diff --git a/src/main/java/org/spongepowered/common/scheduler/ServerScheduler.java b/src/main/java/org/spongepowered/common/scheduler/ServerScheduler.java index 76f8599ed66..6a8b0e420ca 100644 --- a/src/main/java/org/spongepowered/common/scheduler/ServerScheduler.java +++ b/src/main/java/org/spongepowered/common/scheduler/ServerScheduler.java @@ -25,6 +25,8 @@ package org.spongepowered.common.scheduler; import org.spongepowered.api.Sponge; +import org.spongepowered.common.SpongeCommon; +import org.spongepowered.common.adventure.SpongeAdventure; import org.spongepowered.common.bridge.world.entity.player.PlayerInventoryBridge; import org.spongepowered.common.event.tracking.PhaseTracker; import org.spongepowered.common.event.tracking.phase.tick.EntityTickContext; @@ -47,5 +49,9 @@ public void tick() { ((PlayerInventoryBridge) ((net.minecraft.world.entity.player.Player) player).getInventory()).bridge$cleanupDirty(); } }); + + if (SpongeCommon.server().getTickCount() % (20 * 60) == 0) { // Run once every minute + SpongeAdventure.runCallbackHousekeeping(); + } } } diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider new file mode 100644 index 00000000000..d822d326188 --- /dev/null +++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider @@ -0,0 +1 @@ +org.spongepowered.common.adventure.ClickCallbackProviderImpl diff --git a/testplugins/src/main/java/org/spongepowered/test/command/CommandTest.java b/testplugins/src/main/java/org/spongepowered/test/command/CommandTest.java index d7c7ff1356c..c1a742f4f9a 100644 --- a/testplugins/src/main/java/org/spongepowered/test/command/CommandTest.java +++ b/testplugins/src/main/java/org/spongepowered/test/command/CommandTest.java @@ -30,6 +30,7 @@ import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import org.apache.logging.log4j.Logger; @@ -38,7 +39,6 @@ import org.spongepowered.api.ResourceKey; import org.spongepowered.api.Sponge; import org.spongepowered.api.SystemSubject; -import org.spongepowered.api.adventure.SpongeComponents; import org.spongepowered.api.command.Command; import org.spongepowered.api.command.CommandCompletion; import org.spongepowered.api.command.CommandResult; @@ -198,7 +198,7 @@ private void onRegisterSpongeCommand(final RegisterCommandEvent { x.sendMessage(Identity.nil(), Component.text().content("Click Me") - .clickEvent(SpongeComponents.executeCallback(ctx -> ctx.sendMessage(Identity.nil(), Component.text("Hello")))) + .clickEvent(ClickEvent.callback(aud -> aud.sendMessage(Component.text("Hello")))) .build() ); return CommandResult.success(); From 74872b1e751f52955c0ad88ae3af42a4edfd5a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossi=20Erkkil=C3=A4?= Date: Fri, 16 Jan 2026 14:39:02 +0200 Subject: [PATCH 2/3] Add 'final' to params --- .../common/adventure/ClickCallbackProviderImpl.java | 2 +- .../common/adventure/SpongeAdventure.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java b/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java index afeb7fec8fe..d63633470ab 100644 --- a/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java +++ b/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java @@ -33,7 +33,7 @@ public class ClickCallbackProviderImpl implements ClickCallback.Provider { @Override - public @NotNull ClickEvent create(@NotNull ClickCallback callback, ClickCallback.@NotNull Options options) { + public @NotNull ClickEvent create(final @NotNull ClickCallback callback, final ClickCallback.@NotNull Options options) { return SpongeAdventure.createCallbackClickEvent(options, cause -> callback.accept(cause.audience())); } diff --git a/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java b/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java index 72b97acb7af..8423bdd831c 100644 --- a/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java +++ b/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java @@ -773,8 +773,8 @@ public static void runCallbackHousekeeping() { CALLBACKS.values().removeIf(callback -> !callback.isValid()); } - public static void runCallback(UUID uuid, CommandCause cause) { - final var callback = CALLBACKS.get(uuid); + public static void runCallback(final UUID uuid, final CommandCause cause) { + final StoredClickCallback callback = CALLBACKS.get(uuid); if (callback == null) { return; @@ -788,7 +788,7 @@ public static void runCallback(UUID uuid, CommandCause cause) { CALLBACKS.remove(uuid, callback); } - public static ClickEvent createCallbackClickEvent(ClickCallback.Options options, Consumer callback) { + public static ClickEvent createCallbackClickEvent(final ClickCallback.Options options, final Consumer callback) { final UUID key = UUID.randomUUID(); CALLBACKS.put(key, new StoredClickCallback(options, callback)); return ClickEvent.runCommand(String.format("/%s:%s %s", Launch.instance().id(), CallbackCommand.NAME, key)); @@ -800,13 +800,13 @@ private static class StoredClickCallback { private final Instant expiryTime; private int useCounter = 0; - public StoredClickCallback(ClickCallback.Options options, Consumer handler) { + public StoredClickCallback(final ClickCallback.Options options, final Consumer handler) { this.options = options; this.handler = handler; this.expiryTime = Instant.now().plus(options.lifetime()); } - private void useAndRecord(CommandCause cause) { + private void useAndRecord(final CommandCause cause) { this.useCounter++; this.handler.accept(cause); } From e9f6c1e80d6edb20dd0ff332cd643b6c0206077e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossi=20Erkkil=C3=A4?= Date: Sat, 17 Jan 2026 03:35:31 +0200 Subject: [PATCH 3/3] Add missing final for classes --- .../common/adventure/ClickCallbackProviderImpl.java | 2 +- .../org/spongepowered/common/adventure/SpongeAdventure.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java b/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java index d63633470ab..b9c205dcf9f 100644 --- a/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java +++ b/src/main/java/org/spongepowered/common/adventure/ClickCallbackProviderImpl.java @@ -30,7 +30,7 @@ import org.jetbrains.annotations.NotNull; @SuppressWarnings("UnstableApiUsage") // permitted provider -public class ClickCallbackProviderImpl implements ClickCallback.Provider { +public final class ClickCallbackProviderImpl implements ClickCallback.Provider { @Override public @NotNull ClickEvent create(final @NotNull ClickCallback callback, final ClickCallback.@NotNull Options options) { diff --git a/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java b/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java index 8423bdd831c..e23dd2689c5 100644 --- a/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java +++ b/src/main/java/org/spongepowered/common/adventure/SpongeAdventure.java @@ -794,7 +794,7 @@ public static ClickEvent createCallbackClickEvent(final ClickCallback.Options op return ClickEvent.runCommand(String.format("/%s:%s %s", Launch.instance().id(), CallbackCommand.NAME, key)); } - private static class StoredClickCallback { + private static final class StoredClickCallback { private final ClickCallback.Options options; private final Consumer handler; private final Instant expiryTime;