mirror of
https://github.com/PlaceholderAPI/PlaceholderAPI
synced 2025-11-10 09:05:05 +01:00
Compare commits
2 Commits
2.11.7
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a8bb3695c | ||
|
|
683dc6e8e3 |
@@ -8,6 +8,7 @@
|
||||
[discord]: https://helpch.at/discord
|
||||
[spigot]: https://www.spigotmc.org/resources/6245/
|
||||
[hangar]: https://hangar.papermc.io/HelpChat/PlaceholderAPI
|
||||
[bbb]: https://builtbybit.com/resources/placeholderapi.24306
|
||||
[modrinth]: https://modrinth.com/plugin/placeholderapi
|
||||
[Expansions cloud]: https://api.extendedclip.com/home
|
||||
[placeholder list]: https://helpch.at/placeholders
|
||||
@@ -50,5 +51,6 @@ If you would like to create your own Placeholder Expansion for PlaceholderAPI, t
|
||||
- [Placeholder List]
|
||||
- [Spigot Page][spigot]
|
||||
- [Hangar Page][hangar]
|
||||
- [BuiltByBit Page][bbb]
|
||||
- [Modrinth Page][modrinth]
|
||||
- [Plugin Statistics][statistics]
|
||||
|
||||
@@ -8,7 +8,7 @@ plugins {
|
||||
}
|
||||
|
||||
group = "me.clip"
|
||||
version = "2.11.8-DEV-${System.getProperty("BUILD_NUMBER")}"
|
||||
version = "2.11.7-DEV-${System.getProperty("BUILD_NUMBER")}"
|
||||
|
||||
description = "An awesome placeholder provider!"
|
||||
|
||||
@@ -25,7 +25,7 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
implementation("org.bstats:bstats-bukkit:3.0.1")
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.3")
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.4.1")
|
||||
|
||||
//compileOnly("org.spigotmc:spigot-api:1.21-R0.1-SNAPSHOT")
|
||||
compileOnly("dev.folia:folia-api:1.20.1-R0.1-SNAPSHOT")
|
||||
|
||||
@@ -35,6 +35,8 @@ import me.clip.placeholderapi.replacer.CharsReplacer;
|
||||
import me.clip.placeholderapi.replacer.Replacer;
|
||||
import me.clip.placeholderapi.replacer.Replacer.Closure;
|
||||
import me.clip.placeholderapi.util.Msg;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextReplacementConfig;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
@@ -57,6 +59,12 @@ public final class PlaceholderAPI {
|
||||
|
||||
// === Current API ===
|
||||
|
||||
@NotNull
|
||||
public static Component setComponentPlaceholders(final OfflinePlayer player, @NotNull final Component component) {
|
||||
// change charsreplacer to custom that returns component instead of string - this is going to suck mega
|
||||
return component.replaceText(config -> config.match(PLACEHOLDER_PATTERN).replacement((result, builder) -> builder.content(REPLACER_PERCENT.apply(builder.content(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates all placeholders into their corresponding values.
|
||||
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
|
||||
|
||||
@@ -20,12 +20,16 @@
|
||||
|
||||
package me.clip.placeholderapi;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class PlaceholderHook {
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public String onRequest(final OfflinePlayer player, @NotNull final String params) {
|
||||
if (player != null && player.isOnline()) {
|
||||
@@ -35,8 +39,16 @@ public abstract class PlaceholderHook {
|
||||
return onPlaceholderRequest(null, params);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public String onPlaceholderRequest(final Player player, @NotNull final String params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Component onPlaceholderComponentRequest(final OfflinePlayer player, @NotNull final String params) {
|
||||
final String result = onRequest(player, params);
|
||||
|
||||
return result == null ? null : Component.text(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,7 @@ public enum NMSVersion {
|
||||
SPIGOT_1_21_R1("v1_21_R1"),
|
||||
SPIGOT_1_21_R2("V1_21_R2"),
|
||||
SPIGOT_1_21_R3("V1_21_R3"),
|
||||
SPIGOT_1_21_R4("V1_21_R4"),
|
||||
SPIGOT_1_21_R5("V1_21_R5"),
|
||||
SPIGOT_1_21_R6("V1_21_R6");
|
||||
SPIGOT_1_21_R4("V1_21_R4");
|
||||
|
||||
private final String version;
|
||||
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* This file is part of adventure, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) 2017-2025 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 me.clip.placeholderapi.replacer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.MatchResult;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import net.kyori.adventure.text.*;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.Style;
|
||||
import net.kyori.adventure.text.renderer.ComponentRenderer;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A renderer performing a replacement on every {@link TextComponent} element of a component tree.
|
||||
*/
|
||||
final class ComponentCharsReplacer implements ComponentRenderer<ComponentCharsReplacer.State> {
|
||||
//static final TextReplacementRenderer INSTANCE = new TextReplacementRenderer();
|
||||
private final OfflinePlayer player;
|
||||
private final Function<String, @Nullable PlaceholderExpansion> lookup;
|
||||
private static final Closure CLOSURE = Closure.PERCENT;
|
||||
|
||||
enum Closure {
|
||||
BRACKET('{', '}'),
|
||||
PERCENT('%', '%');
|
||||
|
||||
|
||||
public final char head, tail;
|
||||
|
||||
Closure(final char head, final char tail) {
|
||||
this.head = head;
|
||||
this.tail = tail;
|
||||
}
|
||||
}
|
||||
|
||||
public ComponentCharsReplacer(@Nullable final OfflinePlayer player,
|
||||
@NotNull final Function<String, @Nullable PlaceholderExpansion> lookup) {
|
||||
this.player = player;
|
||||
this.lookup = lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component render(final Component component, final State state) {
|
||||
if (!state.running) return component;
|
||||
final boolean prevFirstMatch = state.firstMatch;
|
||||
state.firstMatch = true;
|
||||
|
||||
final List<Component> oldChildren = component.children();
|
||||
final int oldChildrenSize = oldChildren.size();
|
||||
Style oldStyle = component.style();
|
||||
List<Component> children = null;
|
||||
Component modified = component;
|
||||
// replace the component itself
|
||||
if (component instanceof TextComponent) {
|
||||
TextComponent tc = (TextComponent) component;
|
||||
final String content = tc.content();
|
||||
|
||||
final char[] chars = content.toCharArray();
|
||||
final StringBuilder identifier = new StringBuilder();
|
||||
final StringBuilder parameters = new StringBuilder();
|
||||
|
||||
final Map<List<Integer>, Component> replacements = new HashMap<>();
|
||||
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
final char l = chars[i];
|
||||
|
||||
if (l != CLOSURE.head || i + 1 >= chars.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean identified = false;
|
||||
boolean invalid = true;
|
||||
boolean hadSpace = false;
|
||||
|
||||
while (++i < chars.length) {
|
||||
final char p = chars[i];
|
||||
|
||||
if (p == ' ' && !identified) {
|
||||
hadSpace = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (p == CLOSURE.tail) {
|
||||
invalid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (p == '_' && !identified) {
|
||||
identified = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (identified) {
|
||||
parameters.append(p);
|
||||
} else {
|
||||
identifier.append(p);
|
||||
}
|
||||
}
|
||||
|
||||
final String identifierString = identifier.toString();
|
||||
final String lowerIdentifiedString = identifierString.toLowerCase();
|
||||
final String parametersString = parameters.toString();
|
||||
|
||||
identifier.setLength(0);
|
||||
parameters.setLength(0);
|
||||
|
||||
if (invalid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
replacements.put(Lists.newArrayList(i, i + identifierString.length() + parametersString.length()), lookup.apply(lowerIdentifiedString).onPlaceholderComponentRequest(player, parametersString));
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// do something with our replacements
|
||||
|
||||
|
||||
|
||||
|
||||
final Matcher matcher = state.pattern.matcher(content);
|
||||
int replacedUntil = 0; // last index handled
|
||||
while (matcher.find()) {
|
||||
final PatternReplacementResult result = state.continuer.shouldReplace(matcher, ++state.matchCount, state.replaceCount);
|
||||
|
||||
if (matcher.start() == 0) {
|
||||
// if we're a full match, modify the component directly
|
||||
if (matcher.end() == content.length()) {
|
||||
final ComponentLike replacement = state.replacement.apply(matcher, Component.text().content(matcher.group())
|
||||
.style(component.style()));
|
||||
|
||||
modified = replacement == null ? Component.empty() : replacement.asComponent();
|
||||
|
||||
if (modified.style().hoverEvent() != null) {
|
||||
oldStyle = oldStyle.hoverEvent(null); // Remove original hover if it has been replaced completely
|
||||
}
|
||||
|
||||
// merge style of the match into this component to prevent unexpected loss of style
|
||||
modified = modified.style(modified.style().merge(component.style(), Style.Merge.Strategy.IF_ABSENT_ON_TARGET));
|
||||
|
||||
if (children == null) { // Prepare children
|
||||
children = new ArrayList<>(oldChildrenSize + modified.children().size());
|
||||
children.addAll(modified.children());
|
||||
}
|
||||
} else {
|
||||
// otherwise, work on a child of the root node
|
||||
modified = Component.text("", component.style());
|
||||
final ComponentLike child = state.replacement.apply(matcher, Component.text().content(matcher.group()));
|
||||
if (child != null) {
|
||||
if (children == null) {
|
||||
children = new ArrayList<>(oldChildrenSize + 1);
|
||||
}
|
||||
children.add(child.asComponent());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (children == null) {
|
||||
children = new ArrayList<>(oldChildrenSize + 2);
|
||||
}
|
||||
if (state.firstMatch) {
|
||||
// truncate parent to content before match
|
||||
modified = ((TextComponent) component).content(content.substring(0, matcher.start()));
|
||||
} else if (replacedUntil < matcher.start()) {
|
||||
children.add(Component.text(content.substring(replacedUntil, matcher.start())));
|
||||
}
|
||||
final ComponentLike builder = state.replacement.apply(matcher, Component.text().content(matcher.group()));
|
||||
if (builder != null) {
|
||||
children.add(builder.asComponent());
|
||||
}
|
||||
}
|
||||
state.replaceCount++;
|
||||
state.firstMatch = false;
|
||||
replacedUntil = matcher.end();
|
||||
}
|
||||
if (replacedUntil < content.length()) {
|
||||
// append trailing content
|
||||
if (replacedUntil > 0) {
|
||||
if (children == null) {
|
||||
children = new ArrayList<>(oldChildrenSize);
|
||||
}
|
||||
children.add(Component.text(content.substring(replacedUntil)));
|
||||
}
|
||||
// otherwise, we haven't modified the component, so nothing to change
|
||||
}
|
||||
} else if (modified instanceof TranslatableComponent) { // get TranslatableComponent with() args
|
||||
final List<TranslationArgument> args = ((TranslatableComponent) modified).arguments();
|
||||
List<TranslationArgument> newArgs = null;
|
||||
for (int i = 0, size = args.size(); i < size; i++) {
|
||||
final TranslationArgument original = args.get(i);
|
||||
final TranslationArgument replaced = original.value() instanceof Component ? TranslationArgument.component(this.render((Component) original.value(), state)) : original;
|
||||
if (replaced != original) {
|
||||
if (newArgs == null) {
|
||||
newArgs = new ArrayList<>(size);
|
||||
if (i > 0) {
|
||||
newArgs.addAll(args.subList(0, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newArgs != null) {
|
||||
newArgs.add(replaced);
|
||||
}
|
||||
}
|
||||
if (newArgs != null) {
|
||||
modified = ((TranslatableComponent) modified).arguments(newArgs);
|
||||
}
|
||||
}
|
||||
// Only visit children if we're running
|
||||
if (state.running) {
|
||||
// hover event
|
||||
if (state.replaceInsideHoverEvents) {
|
||||
final HoverEvent<?> event = oldStyle.hoverEvent();
|
||||
if (event != null) {
|
||||
final HoverEvent<?> rendered = event.withRenderedValue(this, state);
|
||||
if (event != rendered) {
|
||||
modified = modified.style(s -> s.hoverEvent(rendered));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Children
|
||||
boolean first = true;
|
||||
for (int i = 0; i < oldChildrenSize; i++) {
|
||||
final Component child = oldChildren.get(i);
|
||||
final Component replaced = this.render(child, state);
|
||||
if (replaced != child) {
|
||||
if (children == null) {
|
||||
children = new ArrayList<>(oldChildrenSize);
|
||||
}
|
||||
if (first) {
|
||||
children.addAll(oldChildren.subList(0, i));
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
if (children != null) {
|
||||
children.add(replaced);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we're not visiting children, re-add original children if necessary
|
||||
if (children != null) {
|
||||
children.addAll(oldChildren);
|
||||
}
|
||||
}
|
||||
|
||||
state.firstMatch = prevFirstMatch;
|
||||
// Update the modified component with new children
|
||||
if (children != null) {
|
||||
return modified.children(children);
|
||||
}
|
||||
return modified;
|
||||
}
|
||||
|
||||
static final class State {
|
||||
final Pattern pattern;
|
||||
final BiFunction<MatchResult, TextComponent.Builder, @Nullable ComponentLike> replacement;
|
||||
final TextReplacementConfig.Condition continuer;
|
||||
final boolean replaceInsideHoverEvents;
|
||||
boolean running = true;
|
||||
int matchCount = 0;
|
||||
int replaceCount = 0;
|
||||
boolean firstMatch = true;
|
||||
|
||||
State(final Pattern pattern, final BiFunction<MatchResult, TextComponent.Builder, @Nullable ComponentLike> replacement, final TextReplacementConfig.Condition continuer, final boolean replaceInsideHoverEvents) {
|
||||
this.pattern = pattern;
|
||||
this.replacement = replacement;
|
||||
this.continuer = continuer;
|
||||
this.replaceInsideHoverEvents = replaceInsideHoverEvents;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public interface TaskScheduler {
|
||||
|
||||
/**
|
||||
* <b>Folia</b>: Returns whether the current thread is ticking the global region <br>
|
||||
* <b>Paper and Bukkit</b>: Returns {@link org.bukkit.Server#isPrimaryThread}
|
||||
* <b>Paper & Bukkit</b>: Returns {@link org.bukkit.Server#isPrimaryThread}
|
||||
*/
|
||||
boolean isGlobalThread();
|
||||
|
||||
@@ -49,7 +49,7 @@ public interface TaskScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Folia and Paper</b>: Returns whether the current thread is ticking a region and that the region
|
||||
* <b>Folia & Paper</b>: Returns whether the current thread is ticking a region and that the region
|
||||
* being ticked owns the specified entity. Note that this function is the only appropriate method of
|
||||
* checking for ownership of an entity, as retrieving the entity's location is undefined unless the
|
||||
* entity is owned by the current region
|
||||
@@ -61,7 +61,7 @@ public interface TaskScheduler {
|
||||
boolean isEntityThread(Entity entity);
|
||||
|
||||
/**
|
||||
* <b>Folia and Paper</b>: Returns whether the current thread is ticking a region and that the region
|
||||
* <b>Folia & Paper</b>: Returns whether the current thread is ticking a region and that the region
|
||||
* being ticked owns the chunk at the specified world and block position as included in the specified location
|
||||
* <p>
|
||||
* <b>Bukkit</b>: returns {@link org.bukkit.Server#isPrimaryThread}
|
||||
@@ -72,7 +72,7 @@ public interface TaskScheduler {
|
||||
|
||||
/**
|
||||
* Schedules a task to be executed on the next tick <br>
|
||||
* <b>Folia and Paper</b>: ...on the global region <br>
|
||||
* <b>Folia & Paper</b>: ...on the global region <br>
|
||||
* <b>Bukkit</b>: ...on the main thread
|
||||
*
|
||||
* @param runnable The task to execute
|
||||
@@ -81,7 +81,7 @@ public interface TaskScheduler {
|
||||
|
||||
/**
|
||||
* Schedules a task to be executed after the specified delay in ticks <br>
|
||||
* <b>Folia and Paper</b>: ...on the global region <br>
|
||||
* <b>Folia & Paper</b>: ...on the global region <br>
|
||||
* <b>Bukkit</b>: ...on the main thread
|
||||
*
|
||||
* @param runnable The task to execute
|
||||
@@ -91,7 +91,7 @@ public interface TaskScheduler {
|
||||
|
||||
/**
|
||||
* Schedules a repeating task to be executed after the initial delay with the specified period <br>
|
||||
* <b>Folia and Paper</b>: ...on the global region <br>
|
||||
* <b>Folia & Paper</b>: ...on the global region <br>
|
||||
* <b>Bukkit</b>: ...on the main thread
|
||||
*
|
||||
* @param runnable The task to execute
|
||||
@@ -125,7 +125,7 @@ public interface TaskScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Folia and Paper</b>: Schedules a task to be executed on the region which owns the location on the next tick
|
||||
* <b>Folia & Paper</b>: Schedules a task to be executed on the region which owns the location on the next tick
|
||||
* <p>
|
||||
* <b>Bukkit</b>: same as {@link #runTask(Runnable)}
|
||||
*
|
||||
@@ -137,7 +137,7 @@ public interface TaskScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Folia and Paper</b>: Schedules a task to be executed on the region which owns the location after the
|
||||
* <b>Folia & Paper</b>: Schedules a task to be executed on the region which owns the location after the
|
||||
* specified delay in ticks
|
||||
* <p>
|
||||
* <b>Bukkit</b>: same as {@link #runTaskLater(Runnable, long)}
|
||||
@@ -151,7 +151,7 @@ public interface TaskScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Folia and Paper</b>: Schedules a repeating task to be executed on the region which owns the location
|
||||
* <b>Folia & Paper</b>: Schedules a repeating task to be executed on the region which owns the location
|
||||
* after the initial delay with the specified period
|
||||
* <p>
|
||||
* <b>Bukkit</b>: same as {@link #runTaskTimer(Runnable, long, long)}
|
||||
@@ -190,7 +190,7 @@ public interface TaskScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Folia and Paper</b>: Schedules a task to be executed on the region which owns the location
|
||||
* <b>Folia & Paper</b>: Schedules a task to be executed on the region which owns the location
|
||||
* of given entity on the next tick
|
||||
* <p>
|
||||
* <b>Bukkit</b>: same as {@link #runTask(Runnable)}
|
||||
@@ -203,7 +203,7 @@ public interface TaskScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Folia and Paper</b>: Schedules a task to be executed on the region which owns the location
|
||||
* <b>Folia & Paper</b>: Schedules a task to be executed on the region which owns the location
|
||||
* of given entity after the specified delay in ticks
|
||||
* <p>
|
||||
* <b>Bukkit</b>: same as {@link #runTaskLater(Runnable, long)}
|
||||
@@ -217,7 +217,7 @@ public interface TaskScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Folia and Paper</b>: Schedules a repeating task to be executed on the region which owns the
|
||||
* <b>Folia & Paper</b>: Schedules a repeating task to be executed on the region which owns the
|
||||
* location of given entity after the initial delay with the specified period
|
||||
* <p>
|
||||
* <b>Bukkit</b>: same as {@link #runTaskTimer(Runnable, long, long)}
|
||||
@@ -285,7 +285,7 @@ public interface TaskScheduler {
|
||||
|
||||
/**
|
||||
* Calls a method on the main thread and returns a Future object. This task will be executed
|
||||
* by the main(Bukkit)/global(FoliaandPaper) server thread.
|
||||
* by the main(Bukkit)/global(Folia&Paper) server thread.
|
||||
* <p>
|
||||
* Note: The Future.get() methods must NOT be called from the main thread.
|
||||
* <p>
|
||||
|
||||
Reference in New Issue
Block a user