diff options
Diffstat (limited to 'patches/api/0073-AsyncTabCompleteEvent.patch')
-rw-r--r-- | patches/api/0073-AsyncTabCompleteEvent.patch | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/patches/api/0073-AsyncTabCompleteEvent.patch b/patches/api/0073-AsyncTabCompleteEvent.patch new file mode 100644 index 0000000000..b88930e4dc --- /dev/null +++ b/patches/api/0073-AsyncTabCompleteEvent.patch @@ -0,0 +1,603 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <[email protected]> +Date: Sun, 26 Nov 2017 13:17:09 -0500 +Subject: [PATCH] AsyncTabCompleteEvent + +Let plugins be able to control tab completion of commands and chat async. + +This will be useful for frameworks like ACF so we can define async safe completion handlers, +and avoid going to main for tab completions. + +Especially useful if you need to query a database in order to obtain the results for tab +completion, such as offline players. + +Co-authored-by: Aikar <[email protected]> + +diff --git a/src/main/java/com/destroystokyo/paper/event/server/AsyncTabCompleteEvent.java b/src/main/java/com/destroystokyo/paper/event/server/AsyncTabCompleteEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8965974988ad20fbe1d45885f20a3a98d2e9595f +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/event/server/AsyncTabCompleteEvent.java +@@ -0,0 +1,339 @@ ++/* ++ * Copyright (c) 2017 Daniel Ennis (Aikar) MIT License ++ * ++ * 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 com.destroystokyo.paper.event.server; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.util.TransformingRandomAccessList; ++import net.kyori.adventure.text.Component; ++import net.kyori.examination.Examinable; ++import net.kyori.examination.ExaminableProperty; ++import net.kyori.examination.string.StringExaminer; ++import org.bukkit.Location; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Objects; ++import java.util.stream.Stream; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Allows plugins to compute tab completion results asynchronously. ++ * <p> ++ * If this event provides completions, then the standard synchronous process ++ * will not be fired to populate the results. ++ * However, the synchronous TabCompleteEvent will fire with the Async results. ++ * <p> ++ * Only 1 process will be allowed to provide completions, the Async Event, or the standard process. ++ */ ++public class AsyncTabCompleteEvent extends Event implements Cancellable { ++ ++ private static final HandlerList HANDLER_LIST = new HandlerList(); ++ ++ @NotNull private final CommandSender sender; ++ @NotNull private final String buffer; ++ private final boolean isCommand; ++ @Nullable ++ private final Location location; ++ private final List<Completion> completions = new ArrayList<>(); ++ private final List<String> stringCompletions = new TransformingRandomAccessList<>( ++ this.completions, ++ Completion::suggestion, ++ Completion::completion ++ ); ++ private boolean handled; ++ private boolean cancelled; ++ ++ @ApiStatus.Internal ++ public AsyncTabCompleteEvent(@NotNull CommandSender sender, @NotNull String buffer, boolean isCommand, @Nullable Location loc) { ++ super(true); ++ this.sender = sender; ++ this.buffer = buffer; ++ this.isCommand = isCommand; ++ this.location = loc; ++ } ++ ++ @Deprecated ++ @ApiStatus.Internal ++ public AsyncTabCompleteEvent(@NotNull CommandSender sender, @NotNull List<String> completions, @NotNull String buffer, boolean isCommand, @Nullable Location loc) { ++ super(true); ++ this.sender = sender; ++ this.completions.addAll(fromStrings(completions)); ++ this.buffer = buffer; ++ this.isCommand = isCommand; ++ this.location = loc; ++ } ++ ++ /** ++ * Get the sender completing this command. ++ * ++ * @return the {@link CommandSender} instance ++ */ ++ @NotNull ++ public CommandSender getSender() { ++ return this.sender; ++ } ++ ++ /** ++ * The list of completions which will be offered to the sender, in order. ++ * This list is mutable and reflects what will be offered. ++ * <p> ++ * If this collection is not empty after the event is fired, then ++ * the standard process of calling {@link Command#tabComplete(CommandSender, String, String[])} ++ * or current player names will not be called. ++ * ++ * @return a list of offered completions ++ */ ++ @NotNull ++ public List<String> getCompletions() { ++ return this.stringCompletions; ++ } ++ ++ /** ++ * Set the completions offered, overriding any already set. ++ * If this collection is not empty after the event is fired, then ++ * the standard process of calling {@link Command#tabComplete(CommandSender, String, String[])} ++ * or current player names will not be called. ++ * <p> ++ * The passed collection will be cloned to a new {@code List}. You must call {{@link #getCompletions()}} to mutate from here ++ * ++ * @param completions the new completions ++ */ ++ public void setCompletions(@NotNull List<String> completions) { ++ Preconditions.checkArgument(completions != null, "Completions list cannot be null"); ++ if (completions == this.stringCompletions) { ++ return; ++ } ++ this.completions.clear(); ++ this.completions.addAll(fromStrings(completions)); ++ } ++ ++ /** ++ * The list of {@link Completion completions} which will be offered to the sender, in order. ++ * This list is mutable and reflects what will be offered. ++ * <p> ++ * If this collection is not empty after the event is fired, then ++ * the standard process of calling {@link Command#tabComplete(CommandSender, String, String[])} ++ * or current player names will not be called. ++ * ++ * @return a list of offered completions ++ */ ++ public @NotNull List<Completion> completions() { ++ return this.completions; ++ } ++ ++ /** ++ * Set the {@link Completion completions} offered, overriding any already set. ++ * If this collection is not empty after the event is fired, then ++ * the standard process of calling {@link Command#tabComplete(CommandSender, String, String[])} ++ * or current player names will not be called. ++ * <p> ++ * The passed collection will be cloned to a new {@code List}. You must call {@link #completions()} to mutate from here ++ * ++ * @param newCompletions the new completions ++ */ ++ public void completions(final @NotNull List<Completion> newCompletions) { ++ Preconditions.checkArgument(newCompletions != null, "new completions cannot be null"); ++ this.completions.clear(); ++ this.completions.addAll(newCompletions); ++ } ++ ++ /** ++ * Return the entire buffer which formed the basis of this completion. ++ * ++ * @return command buffer, as entered ++ */ ++ @NotNull ++ public String getBuffer() { ++ return this.buffer; ++ } ++ ++ /** ++ * @return {@code true} if it is a command being tab completed, {@code false} if it is a chat message. ++ */ ++ public boolean isCommand() { ++ return this.isCommand; ++ } ++ ++ /** ++ * @return The position looked at by the sender, or {@code null} if none ++ */ ++ @Nullable ++ public Location getLocation() { ++ return this.location != null ? this.location.clone() : null; ++ } ++ ++ /** ++ * If {@code true}, the standard process of calling {@link Command#tabComplete(CommandSender, String, String[])} ++ * or current player names will not be called. ++ * ++ * @return Is completions considered handled. Always {@code true} if completions is not empty. ++ */ ++ public boolean isHandled() { ++ return !this.completions.isEmpty() || this.handled; ++ } ++ ++ /** ++ * Sets whether to consider the completion request handled. ++ * If {@code true}, the standard process of calling {@link Command#tabComplete(CommandSender, String, String[])} ++ * or current player names will not be called. ++ * ++ * @param handled if this completion should be marked as being handled ++ */ ++ public void setHandled(boolean handled) { ++ this.handled = handled; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return this.cancelled; ++ } ++ ++ /** ++ * {@inheritDoc} ++ * <br> ++ * Will provide no completions, and will not fire the synchronous process ++ */ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancelled = cancel; ++ } ++ ++ @NotNull ++ public HandlerList getHandlers() { ++ return HANDLER_LIST; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return HANDLER_LIST; ++ } ++ ++ private static @NotNull List<Completion> fromStrings(final @NotNull List<String> suggestions) { ++ final List<Completion> list = new ArrayList<>(suggestions.size()); ++ for (final String suggestion : suggestions) { ++ list.add(new CompletionImpl(suggestion, null)); ++ } ++ return list; ++ } ++ ++ /** ++ * A rich tab completion, consisting of a string suggestion, and a nullable {@link Component} tooltip. ++ */ ++ public interface Completion extends Examinable { ++ ++ /** ++ * Get the suggestion string for this {@link Completion}. ++ * ++ * @return suggestion string ++ */ ++ @NotNull String suggestion(); ++ ++ /** ++ * Get the suggestion tooltip for this {@link Completion}. ++ * ++ * @return tooltip component ++ */ ++ @Nullable Component tooltip(); ++ ++ @Override ++ default @NotNull Stream<? extends ExaminableProperty> examinableProperties() { ++ return Stream.of(ExaminableProperty.of("suggestion", this.suggestion()), ExaminableProperty.of("tooltip", this.tooltip())); ++ } ++ ++ /** ++ * Create a new {@link Completion} from a suggestion string. ++ * ++ * @param suggestion suggestion string ++ * @return new completion instance ++ */ ++ static @NotNull Completion completion(final @NotNull String suggestion) { ++ return new CompletionImpl(suggestion, null); ++ } ++ ++ /** ++ * Create a new {@link Completion} from a suggestion string and a tooltip {@link Component}. ++ * <p> ++ * If the provided component is {@code null}, the suggestion will not have a tooltip. ++ * ++ * @param suggestion suggestion string ++ * @param tooltip tooltip component, or {@code null} ++ * @return new completion instance ++ */ ++ static @NotNull Completion completion(final @NotNull String suggestion, final @Nullable Component tooltip) { ++ return new CompletionImpl(suggestion, tooltip); ++ } ++ } ++ ++ @ApiStatus.Internal ++ static final class CompletionImpl implements Completion { ++ ++ private final String suggestion; ++ private final Component tooltip; ++ ++ CompletionImpl(final @NotNull String suggestion, final @Nullable Component tooltip) { ++ this.suggestion = suggestion; ++ this.tooltip = tooltip; ++ } ++ ++ @Override ++ public @NotNull String suggestion() { ++ return this.suggestion; ++ } ++ ++ @Override ++ public @Nullable Component tooltip() { ++ return this.tooltip; ++ } ++ ++ @Override ++ public boolean equals(final @Nullable Object o) { ++ if (this == o) { ++ return true; ++ } ++ if (o == null || this.getClass() != o.getClass()) { ++ return false; ++ } ++ final CompletionImpl that = (CompletionImpl) o; ++ return this.suggestion.equals(that.suggestion) ++ && Objects.equals(this.tooltip, that.tooltip); ++ } ++ ++ @Override ++ public int hashCode() { ++ return Objects.hash(this.suggestion, this.tooltip); ++ } ++ ++ @Override ++ public @NotNull String toString() { ++ return StringExaminer.simpleEscaping().examine(this); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/TransformingRandomAccessList.java b/src/main/java/io/papermc/paper/util/TransformingRandomAccessList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6f560a51277ccbd46a9142cfa057d276118c1c7b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/TransformingRandomAccessList.java +@@ -0,0 +1,169 @@ ++package io.papermc.paper.util; ++ ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.AbstractList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.ListIterator; ++import java.util.RandomAccess; ++import java.util.function.Function; ++import java.util.function.Predicate; ++ ++import static com.google.common.base.Preconditions.checkNotNull; ++ ++/** ++ * Modified version of the Guava class with the same name to support add operations. ++ * ++ * @param <F> backing list element type ++ * @param <T> transformed list element type ++ */ ++public final class TransformingRandomAccessList<F, T> extends AbstractList<T> implements RandomAccess { ++ final List<F> fromList; ++ final Function<? super F, ? extends T> toFunction; ++ final Function<? super T, ? extends F> fromFunction; ++ ++ /** ++ * Create a new {@link TransformingRandomAccessList}. ++ * ++ * @param fromList backing list ++ * @param toFunction function mapping backing list element type to transformed list element type ++ * @param fromFunction function mapping transformed list element type to backing list element type ++ */ ++ public TransformingRandomAccessList( ++ final @NonNull List<F> fromList, ++ final @NonNull Function<? super F, ? extends T> toFunction, ++ final @NonNull Function<? super T, ? extends F> fromFunction ++ ) { ++ this.fromList = checkNotNull(fromList); ++ this.toFunction = checkNotNull(toFunction); ++ this.fromFunction = checkNotNull(fromFunction); ++ } ++ ++ @Override ++ public void clear() { ++ this.fromList.clear(); ++ } ++ ++ @Override ++ public T get(int index) { ++ return this.toFunction.apply(this.fromList.get(index)); ++ } ++ ++ @Override ++ public @NotNull Iterator<T> iterator() { ++ return this.listIterator(); ++ } ++ ++ @Override ++ public @NotNull ListIterator<T> listIterator(int index) { ++ return new TransformedListIterator<F, T>(this.fromList.listIterator(index)) { ++ @Override ++ T transform(F from) { ++ return TransformingRandomAccessList.this.toFunction.apply(from); ++ } ++ ++ @Override ++ F transformBack(T from) { ++ return TransformingRandomAccessList.this.fromFunction.apply(from); ++ } ++ }; ++ } ++ ++ @Override ++ public boolean isEmpty() { ++ return this.fromList.isEmpty(); ++ } ++ ++ @Override ++ public boolean removeIf(Predicate<? super T> filter) { ++ checkNotNull(filter); ++ return this.fromList.removeIf(element -> filter.test(this.toFunction.apply(element))); ++ } ++ ++ @Override ++ public T remove(int index) { ++ return this.toFunction.apply(this.fromList.remove(index)); ++ } ++ ++ @Override ++ public int size() { ++ return this.fromList.size(); ++ } ++ ++ @Override ++ public T set(int i, T t) { ++ return this.toFunction.apply(this.fromList.set(i, this.fromFunction.apply(t))); ++ } ++ ++ @Override ++ public void add(int i, T t) { ++ this.fromList.add(i, this.fromFunction.apply(t)); ++ } ++ ++ static abstract class TransformedListIterator<F, T> implements ListIterator<T>, Iterator<T> { ++ final Iterator<F> backingIterator; ++ ++ TransformedListIterator(ListIterator<F> backingIterator) { ++ this.backingIterator = checkNotNull((Iterator<F>) backingIterator); ++ } ++ ++ private ListIterator<F> backingIterator() { ++ return cast(this.backingIterator); ++ } ++ ++ static <A> ListIterator<A> cast(Iterator<A> iterator) { ++ return (ListIterator<A>) iterator; ++ } ++ ++ @Override ++ public final boolean hasPrevious() { ++ return this.backingIterator().hasPrevious(); ++ } ++ ++ @Override ++ public final T previous() { ++ return this.transform(this.backingIterator().previous()); ++ } ++ ++ @Override ++ public final int nextIndex() { ++ return this.backingIterator().nextIndex(); ++ } ++ ++ @Override ++ public final int previousIndex() { ++ return this.backingIterator().previousIndex(); ++ } ++ ++ @Override ++ public void set(T element) { ++ this.backingIterator().set(this.transformBack(element)); ++ } ++ ++ @Override ++ public void add(T element) { ++ this.backingIterator().add(this.transformBack(element)); ++ } ++ ++ abstract T transform(F from); ++ ++ abstract F transformBack(T to); ++ ++ @Override ++ public final boolean hasNext() { ++ return this.backingIterator.hasNext(); ++ } ++ ++ @Override ++ public final T next() { ++ return this.transform(this.backingIterator.next()); ++ } ++ ++ @Override ++ public final void remove() { ++ this.backingIterator.remove(); ++ } ++ } ++} +diff --git a/src/main/java/org/bukkit/event/server/TabCompleteEvent.java b/src/main/java/org/bukkit/event/server/TabCompleteEvent.java +index 270e6d8ad4358baa256cee5f16cff281f063ce3b..b43c3cb5c88eada186d6f81712c244aaa18fb53e 100644 +--- a/src/main/java/org/bukkit/event/server/TabCompleteEvent.java ++++ b/src/main/java/org/bukkit/event/server/TabCompleteEvent.java +@@ -29,6 +29,13 @@ public class TabCompleteEvent extends Event implements Cancellable { + private boolean cancelled; + + public TabCompleteEvent(@NotNull CommandSender sender, @NotNull String buffer, @NotNull List<String> completions) { ++ // Paper start ++ this(sender, buffer, completions, sender instanceof org.bukkit.command.ConsoleCommandSender || buffer.startsWith("/"), null); ++ } ++ public TabCompleteEvent(@NotNull CommandSender sender, @NotNull String buffer, @NotNull List<String> completions, boolean isCommand, @org.jetbrains.annotations.Nullable org.bukkit.Location location) { ++ this.isCommand = isCommand; ++ this.loc = location; ++ // Paper end + Preconditions.checkArgument(sender != null, "sender"); + Preconditions.checkArgument(buffer != null, "buffer"); + Preconditions.checkArgument(completions != null, "completions"); +@@ -69,14 +76,35 @@ public class TabCompleteEvent extends Event implements Cancellable { + return completions; + } + ++ // Paper start ++ private final boolean isCommand; ++ private final org.bukkit.Location loc; ++ /** ++ * @return True if it is a command being tab completed, false if it is a chat message. ++ */ ++ public boolean isCommand() { ++ return isCommand; ++ } ++ ++ /** ++ * @return The position looked at by the sender, or null if none ++ */ ++ @org.jetbrains.annotations.Nullable ++ public org.bukkit.Location getLocation() { ++ return this.loc != null ? this.loc.clone() : null; ++ } ++ // Paper end ++ + /** + * Set the completions offered, overriding any already set. + * ++ * The passed collection will be cloned to a new List. You must call {{@link #getCompletions()}} to mutate from here ++ * + * @param completions the new completions + */ + public void setCompletions(@NotNull List<String> completions) { + Preconditions.checkArgument(completions != null); +- this.completions = completions; ++ this.completions = new java.util.ArrayList<>(completions); // Paper + } + + @Override +diff --git a/src/test/java/org/bukkit/AnnotationTest.java b/src/test/java/org/bukkit/AnnotationTest.java +index a899f63eb2ce58b3cf708e91819cbbdeffda5d9f..057dc3ebea3516863dda24252fe05d344c16fab3 100644 +--- a/src/test/java/org/bukkit/AnnotationTest.java ++++ b/src/test/java/org/bukkit/AnnotationTest.java +@@ -48,6 +48,8 @@ public class AnnotationTest { + // Generic functional interface + "org/bukkit/util/Consumer", + // Paper start ++ "io/papermc/paper/util/TransformingRandomAccessList", ++ "io/papermc/paper/util/TransformingRandomAccessList$TransformedListIterator", + // Timings history is broken in terms of nullability due to guavas Function defining that the param is NonNull + "co/aikar/timings/TimingHistory$2", + "co/aikar/timings/TimingHistory$2$1", |