Skip to content

Commit

Permalink
feat(4.13): Implement callback click event
Browse files Browse the repository at this point in the history
  • Loading branch information
zml2008 committed Feb 26, 2023
1 parent 57383ec commit 563805c
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 9 deletions.
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ dependencies {
}
modApi(fabricApi.module("fabric-api-base", libs.versions.fabricApi.get()))
modImplementation(fabricApi.module("fabric-networking-api-v1", libs.versions.fabricApi.get()))
modImplementation(fabricApi.module("fabric-command-api-v2", libs.versions.fabricApi.get()))
// Only used for prod test
modCompileOnly(fabricApi.module("fabric-lifecycle-events-v1", libs.versions.fabricApi.get()))

Expand Down Expand Up @@ -150,6 +151,13 @@ loom {
source("testmod")
server()
}

configureEach {
vmArgs(
"-Dmixin.debug.countInjections=true",
"-Dmixin.debug.strict=true"
)
}
}

mods {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* This file is part of adventure-platform-fabric, licensed under the MIT License.
*
* Copyright (c) 2020-2022 KyoriPowered
* Copyright (c) 2020-2023 KyoriPowered
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand All @@ -23,18 +23,23 @@
*/
package net.kyori.adventure.platform.fabric.impl;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.logging.LogUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.kyori.adventure.Adventure;
Expand Down Expand Up @@ -68,13 +73,21 @@ public class AdventureCommon implements ModInitializer {

private static final Logger LOGGER = LogUtils.getLogger();

public static final ScheduledExecutorService SCHEDULER;
public static final SidedProxy SIDE_PROXY;

public static final ComponentFlattener FLATTENER;
private static final Pattern LOCALIZATION_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?s");
public static final String MOD_FAPI_NETWORKING = "fabric-networking-api-v1";

static {
// Daemon thread executor for scheduled tasks
SCHEDULER = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder()
.setNameFormat("adventure-platform-fabric-scheduler-%d")
.setDaemon(true)
.setUncaughtExceptionHandler((thread, ex) -> LOGGER.error("An uncaught exception occurred in scheduler thread '{}':", thread.getName(), ex))
.build());

final var sidedProxy = chooseSidedProxy();
SIDE_PROXY = sidedProxy;
FLATTENER = createFlattener(sidedProxy);
Expand Down Expand Up @@ -168,6 +181,18 @@ public void onInitialize() {
});
});

CommandRegistrationCallback.EVENT.register((dispatcher, registries, env) -> {
ClickCallbackRegistry.INSTANCE.registerToDispatcher(dispatcher);
});

// Perform scheduled cleanup
SCHEDULER.scheduleWithFixedDelay(
ClickCallbackRegistry.INSTANCE::cleanUp,
ClickCallbackRegistry.CLEAN_UP_RATE,
ClickCallbackRegistry.CLEAN_UP_RATE,
TimeUnit.SECONDS
);

// If we are in development mode, shut down immediately
if (Boolean.getBoolean("adventure.testMode")) {
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* This file is part of adventure-platform-fabric, licensed under the MIT License.
*
* Copyright (c) 2023 KyoriPowered
*
* 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 net.kyori.adventure.platform.fabric.impl;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.logging.LogUtils;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickCallback;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.UuidArgument;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;

public final class ClickCallbackRegistry {
private static final Logger LOGGER = LogUtils.getLogger();

public static final ClickCallbackRegistry INSTANCE = new ClickCallbackRegistry();

public static final int CLEAN_UP_RATE = 30 /* seconds */;

private static final String UUID_ARG = "uuid";
private static final String COMMAND_NAME = "adventure_callback";
private static final Component FAILURE_MESSAGE = Component.text("No callback with that ID could be found! You may have used it too many times, or it may have expired.", NamedTextColor.RED);

private final Cache<UUID, CallbackRegistration> registrations = CacheBuilder.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS)
.maximumSize(1024) // to avoid unbounded growth
.removalListener((RemovalListener<UUID, CallbackRegistration>) notification -> LOGGER.debug("Removing callback {} from cache for reason {}", notification.getKey(), notification.getCause()))
.build();

private ClickCallbackRegistry() {
}

/**
* Register a callback, returning the command to be attached to a click event.
*
* @param callback the callback to register
* @param options options
* @return a new callback handler command
* @since 5.7.0
*/
public String register(final ClickCallback<Audience> callback, final ClickCallback.Options options) {
final UUID id = UUID.randomUUID();
final CallbackRegistration reg = new CallbackRegistration(
options,
callback,
Instant.now().plus(options.lifetime()),
new AtomicInteger()
);

this.registrations.put(id, reg);

return "/" + COMMAND_NAME + " " + id;
}

public void registerToDispatcher(final CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(Commands.literal(COMMAND_NAME)
.requires(HiddenRequirement.alwaysAllowed()) // hide from client tab completion
.then(Commands.argument(UUID_ARG, UuidArgument.uuid())
.executes(ctx -> {
final UUID callbackId = UuidArgument.getUuid(ctx, UUID_ARG);
final @Nullable CallbackRegistration reg = this.registrations.getIfPresent(callbackId);

if (reg == null) {
ctx.getSource().sendFailure(FAILURE_MESSAGE);
return 0; // unsuccessful
}

// check use count
boolean expire = false;
boolean allowUse = true;
final int allowedUses = reg.options.uses();
if (allowedUses != ClickCallback.UNLIMITED_USES) {
final int useCount = reg.useCount().incrementAndGet();
if (useCount >= allowedUses) {
expire = true;
allowUse = !(useCount > allowedUses);
// allowUse: determine
}
}

// check duration expiry
final Instant now = Instant.now();
if (now.isAfter(reg.expiryTime())) {
expire = true;
allowUse = false;
}

if (expire) {
this.registrations.invalidate(callbackId);
}
if (allowUse) {
reg.callback().accept(ctx.getSource()); // pass the CommandSourceStack to the callback action
} else {
ctx.getSource().sendFailure(FAILURE_MESSAGE);
}
return Command.SINGLE_SUCCESS;
})));
}

/**
* Perform a periodic cleanup of callbacks.
*/
public void cleanUp() {
final Instant now = Instant.now();

final Set<UUID> uuidsToClean = new HashSet<>();
for (final var entry : this.registrations.asMap().entrySet()) {
final var reg = entry.getValue();
final int allowedUses = reg.options().uses();
if (allowedUses != ClickCallback.UNLIMITED_USES && reg.useCount().get() >= allowedUses) {
uuidsToClean.add(entry.getKey());
} else if (now.isAfter(reg.expiryTime())) {
uuidsToClean.add(entry.getKey());
}
}

for (final UUID id : uuidsToClean) {
this.registrations.invalidate(id);
}
}

private record CallbackRegistration(ClickCallback.Options options, ClickCallback<Audience> callback, Instant expiryTime, AtomicInteger useCount) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* This file is part of adventure-platform-fabric, licensed under the MIT License.
*
* Copyright (c) 2023 KyoriPowered
*
* 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 net.kyori.adventure.platform.fabric.impl;

import java.util.Objects;
import java.util.function.Predicate;
import org.jetbrains.annotations.NotNull;

/**
* A requirement for commands that prevents them from being synced to the client.
*
* <p>Not yet implemented for client commands.</p>
*
* @param base the base predicate
* @param <V> value type
*/
public record HiddenRequirement<V>(Predicate<V> base) implements Predicate<V> {
public static <T> HiddenRequirement<T> alwaysAllowed() {
return new HiddenRequirement<>(t -> true);
}

@Override
public boolean test(final V v) {
return this.base.test(v);
}

@Override
public @NotNull Predicate<V> and(final @NotNull Predicate<? super V> other) {
return new HiddenRequirement<>(this.base.and(unwrap(Objects.requireNonNull(other, "other"))));
}

@Override
public @NotNull Predicate<V> negate() {
return new HiddenRequirement<>(this.base.negate());
}

@Override
public @NotNull Predicate<V> or(final @NotNull Predicate<? super V> other) {
return new HiddenRequirement<>(this.base.or(unwrap(Objects.requireNonNull(other, "other"))));
}

private static <T> @NotNull Predicate<T> unwrap(final @NotNull Predicate<T> pred) {
return pred instanceof HiddenRequirement<T> req ? req.base : pred;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* This file is part of adventure-platform-fabric, licensed under the MIT License.
*
* Copyright (c) 2023 KyoriPowered
*
* 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 net.kyori.adventure.platform.fabric.impl.service;

import com.google.auto.service.AutoService;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.fabric.impl.ClickCallbackRegistry;
import net.kyori.adventure.text.event.ClickCallback;
import net.kyori.adventure.text.event.ClickEvent;
import org.jetbrains.annotations.NotNull;

@AutoService(ClickCallback.Provider.class)
public final class FabricClickCallbackProviderImpl implements ClickCallback.Provider {
@Override
public @NotNull ClickEvent create(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options) {
return ClickEvent.runCommand(ClickCallbackRegistry.INSTANCE.register(callback, options));
}
}
1 change: 1 addition & 0 deletions src/main/resource-templates/fabric.mod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ custom:
depends:
fabricloader: ">=0.14.0"
fabric-api-base: "*"
fabric-command-api-v2: "*"

recommends:
fabric-networking-api-v1: "*"
Loading

0 comments on commit 563805c

Please sign in to comment.