Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ default void delete() {
*/
void runCommand(@NonNull String command);

/**
* Gets the console-suggestion(tab-complete) using ChannelMessage, if it is not supported, the response will get empty list.
*
* @param line the command line currently
* @return fully command list if supported
*/
Collection<String> consoleSuggestion(@NonNull String line);

/**
* Gets the templates that actually are installed on the service. If a template is present in the configuration
* {@link eu.cloudnetservice.driver.service.ServiceConfiguration} but wasn't pulled onto the service it won't appear
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
Expand Down Expand Up @@ -212,6 +213,10 @@ public void handleServiceUpdate(@NonNull ServiceInfoSnapshot snapshot) {
}
}

public @NonNull List<String> consoleSuggestion(@NonNull String line) {
return List.of();
}

public @NonNull Optional<ServiceInfoSnapshot> fallback(
@NonNull UUID playerId,
@Nullable String currentServerName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package eu.cloudnetservice.modules.bridge.impl.platform.bukkit;

import dev.derklaro.reflexion.Reflexion;
import eu.cloudnetservice.driver.event.EventManager;
import eu.cloudnetservice.driver.network.NetworkClient;
import eu.cloudnetservice.driver.network.rpc.factory.RPCFactory;
Expand All @@ -32,18 +33,21 @@
import eu.cloudnetservice.modules.bridge.player.PlayerManager;
import eu.cloudnetservice.modules.bridge.player.ServicePlayer;
import eu.cloudnetservice.modules.bridge.player.executor.PlayerExecutor;
import eu.cloudnetservice.utils.base.concurrent.TaskUtil;
import eu.cloudnetservice.wrapper.configuration.WrapperConfiguration;
import eu.cloudnetservice.wrapper.event.ServiceInfoPropertiesConfigureEvent;
import eu.cloudnetservice.wrapper.holder.ServiceInfoHolder;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiFunction;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.command.CommandMap;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permissible;
import org.bukkit.plugin.Plugin;
Expand All @@ -57,6 +61,7 @@ final class BukkitBridgeManagement extends PlatformBridgeManagement<Player, Netw

private final Server server;
private final Plugin plugin;
private final Optional<CommandMap> commandMap;
private final PlayerExecutor directGlobalExecutor;

@Inject
Expand Down Expand Up @@ -84,6 +89,15 @@ public BukkitBridgeManagement(
// init fields
this.server = server;
this.plugin = plugin;
this.commandMap = Reflexion.onBound(server)
.findField("commandMap")
.flatMap(fieldAccessor -> {
var value = fieldAccessor.getValue();
if (value.wasSuccessful()) {
return Optional.of((CommandMap) value.get());
}
return Optional.empty();
});
this.directGlobalExecutor = new BukkitDirectPlayerExecutor(
plugin,
PlayerExecutor.GLOBAL_UNIQUE_ID,
Expand Down Expand Up @@ -123,6 +137,17 @@ public boolean isOnAnyFallbackInstance(@NonNull Player player) {
return this.isOnAnyFallbackInstance(this.ownNetworkServiceInfo.serverName(), null, player::hasPermission);
}

@Override
public @NonNull List<String> consoleSuggestion(@NonNull String line) {
// Ensure better compatibility
var future = BukkitUtil.supplyOnMainThread(
this.plugin,
() -> this.commandMap.map(map -> map.tabComplete(this.server.getConsoleSender(), line))
);
return TaskUtil.getOrDefault(future, Optional.empty())
.orElseGet(List::of);
}

@Override
public @NonNull Optional<ServiceInfoSnapshot> fallback(@NonNull Player player) {
return this.fallback(player, this.ownNetworkServiceInfo.serverName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Supplier;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.Nullable;

final class BukkitUtil {
Expand Down Expand Up @@ -72,4 +76,30 @@ private BukkitUtil() {
// resolve the locale tag from the cache, put it in if missing
return LOCALE_CACHE.computeIfAbsent(localeTag, tag -> Locale.forLanguageTag(tag.replace('_', '-')));
}


public static <T> CompletableFuture<T> supplyOnMainThread(Plugin plugin, Supplier<T> supplier) {
CompletableFuture<T> future = new CompletableFuture<>();

if (Bukkit.isPrimaryThread()) {
try {
T result = supplier.get();
future.complete(result);
} catch (Throwable t) {
future.completeExceptionally(t);
}
return future;
}

Bukkit.getScheduler().runTask(plugin, () -> {
try {
T result = supplier.get();
future.complete(result);
} catch (Throwable t) {
future.completeExceptionally(t);
}
});

return future;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@
import eu.cloudnetservice.wrapper.holder.ServiceInfoHolder;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiFunction;
Expand All @@ -48,6 +51,7 @@
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import org.jetbrains.annotations.Nullable;

@Singleton
Expand Down Expand Up @@ -135,6 +139,31 @@ public boolean isOnAnyFallbackInstance(@NonNull ProxiedPlayer player) {
player::hasPermission);
}

@Override
public @NonNull List<String> consoleSuggestion(@NonNull String line) {
// BungeeCord have not provided tab-complete as root directly. we need to process manually
var console = this.proxyServer.getConsole();
List<String> suggestions = new ArrayList<>();

if (line.indexOf(' ') == -1) {
for (Map.Entry<String, Command> entry : this.proxyServer.getPluginManager().getCommands()) {
var name = entry.getKey();
if (name.startsWith(line)) {
var command = entry.getValue();
String permission = command.getPermission();
if (permission == null || permission.isEmpty() || console.hasPermission(permission)) {
suggestions.add(command.getName());
}
}
}
} else {
// Complete command arguments
this.proxyServer.getPluginManager().dispatchCommand(console, line, suggestions);
}

return suggestions;
}

@Override
public @NonNull Optional<ServiceInfoSnapshot> fallback(@NonNull ProxiedPlayer player) {
return this.fallback(player, player.getServer() == null ? null : player.getServer().getInfo().getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
import eu.cloudnetservice.modules.bridge.player.NetworkPlayerServerInfo;
import eu.cloudnetservice.modules.bridge.player.NetworkServiceInfo;
import eu.cloudnetservice.modules.bridge.player.executor.ServerSelectorType;
import eu.cloudnetservice.utils.base.concurrent.TaskUtil;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import lombok.NonNull;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
Expand Down Expand Up @@ -64,6 +68,28 @@ public void handleConfigurationChannelMessage(@NonNull ChannelMessageReceiveEven
}
}

@EventListener
public void handleConsoleSuggestionChannelMessage(@NonNull ChannelMessageReceiveEvent event) {
// TODO need a better way to get channel of 'cloudnet:internal' from NetworkConstants.INTERNAL_MSG_CHANNEL.
if (event.channel().equals("cloudnet:internal") && event.message()
.equals("request_console_suggestion")) {
// read the line
var line = event.content().readString();
var future = TaskUtil.supplyAsync(() -> this.management.consoleSuggestion(line));
// make sure normal while service console is lagging
var rawList = TaskUtil.getOrDefault(future, Duration.of(100, ChronoUnit.MILLIS), List.of());
List<String> suggestions;
// Preventing cloudnet console from dying
if (rawList.size() > 50) {
suggestions = rawList.subList(0, 50);
} else {
suggestions = rawList;
}

event.binaryResponse(DataBuf.empty().writeObject(suggestions));
}
}

@EventListener
public void handlePlayerChannelMessage(@NonNull ChannelMessageReceiveEvent event) {
if (event.channel().equals(BridgeManagement.BRIDGE_PLAYER_CHANNEL_NAME)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@

import static net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection;

import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.permission.PermissionSubject;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import dev.derklaro.reflexion.Reflexion;
import eu.cloudnetservice.driver.event.EventManager;
import eu.cloudnetservice.driver.network.NetworkClient;
import eu.cloudnetservice.driver.network.rpc.factory.RPCFactory;
Expand All @@ -47,8 +49,10 @@
import jakarta.inject.Singleton;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -142,6 +146,20 @@ public boolean isOnAnyFallbackInstance(@NonNull Player player) {
player::hasPermission);
}

@Override
@SuppressWarnings("unchecked")
public @NonNull List<String> consoleSuggestion(@NonNull String line) {
var commandManager = this.proxyServer.getCommandManager();
var consoleSource = this.proxyServer.getConsoleCommandSource();

CompletableFuture<List<String>> offerSuggestions = Reflexion.onBound(commandManager)
.findMethod("offerSuggestions", CommandSource.class, String.class)
.map((acc) -> (CompletableFuture<List<String>>) acc.invokeWithArgs(consoleSource, line).get())
.orElse(CompletableFuture.completedFuture(List.of()));

return offerSuggestions.join();
}

@Override
public @NonNull Optional<ServiceInfoSnapshot> fallback(@NonNull Player player) {
return this.fallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@ final class DefaultSuggestionProcessor implements SuggestionProcessor<CommandSou
@NonNull CommandPreprocessingContext<CommandSource> context,
@NonNull Stream<Suggestion> allSuggestions
) {
var commandInput = context.commandInput();

// fix the suggestion of greedy
var lastWhitespace = commandInput.input().lastIndexOf(' ');
if (lastWhitespace != -1) {
commandInput.cursor(lastWhitespace);
Copy link
Author

@Score2 Score2 May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth mentioning that this is a potentially destructive fix.

actually, I've tried setting the cursor in the Parser, but it obviously doesn't work for the Greedy argument, because CommandTree#L841(Incendo/cloud) restores the cursor modified in the Parser.

at least in my opinion, there is no better way to solve it properly.

}

// íf there is no input yet, just return all suggestions
var input = context.commandInput().peekString();
var input = commandInput.peekString();
if (Strings.isNullOrEmpty(input)) {
return allSuggestions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.incendo.cloud.annotations.Permission;
import org.incendo.cloud.annotations.parser.Parser;
import org.incendo.cloud.annotations.suggestion.Suggestions;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
Expand Down Expand Up @@ -127,6 +128,26 @@ public ServiceCommand(@NonNull EventManager eventManager, @NonNull CloudServiceP
return matchedServices;
}

@Suggestions("serviceCommands")
public @NonNull Stream<String> suggestCommands(@NonNull CommandContext<?> context, @NonNull String input) {
Collection<ServiceInfoSnapshot> services = context.get("name");
var service = services.stream().findFirst().orElseThrow();

var fullyInput = context.rawInput().input();
var rawInput = fullyInput.substring(fullyInput.indexOf(service.name()) + service.name().length() + 1);
String command;
if (rawInput.contains(" ")) {
command = rawInput.substring(rawInput.indexOf(' '));
if (command.startsWith(" ")) {
command = command.substring(1);
}
} else {
command = "";
}

return service.provider().consoleSuggestion(command).stream();
}

@Command("service|ser list|l")
public void displayServices(
@NonNull CommandSource source,
Expand Down Expand Up @@ -301,10 +322,10 @@ public void deployResources(
public void sendCommand(
@NonNull CommandSource source,
@NonNull @Argument("name") Collection<ServiceInfoSnapshot> matchedServices,
@NonNull @Greedy @Argument("command") String command
@NonNull @Argument(value = "command", suggestions = "serviceCommands") @Greedy String line
) {
for (var matchedService : matchedServices) {
matchedService.provider().runCommand(command);
matchedService.provider().runCommand(line);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import eu.cloudnetservice.utils.base.concurrent.TaskUtil;
import eu.cloudnetservice.utils.base.io.FileUtil;
import eu.cloudnetservice.utils.base.resource.CpuUsageResolver;
import io.leangen.geantyref.TypeFactory;
import io.vavr.Tuple2;
import java.net.Inet6Address;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -411,6 +412,19 @@ public void restart() {
this.updateLifecycle(ServiceLifeCycle.RUNNING);
}

@Override
public Collection<String> consoleSuggestion(@NonNull String line) {
var listPropertyType = TypeFactory.parameterizedClass(List.class, String.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this method should return a future instead so that the caller can better handle errors and apply timeouts.

var response = ChannelMessage.builder()
.targetService(this.serviceId().name())
.channel(NetworkConstants.INTERNAL_MSG_CHANNEL)
.message("request_console_suggestion")
.buffer(DataBuf.empty().writeString(line))
.build().sendSingleQuery();

return response != null ? response.content().readObject(listPropertyType) : List.of();
}

@Override
public @NonNull Collection<ServiceTemplate> installedTemplates() {
return this.installedTemplates;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public void restart() {
public void runCommand(@NonNull String command) {
}

@Override
public Collection<String> consoleSuggestion(@NonNull String line) {
return List.of();
}

@Override
public @NonNull Collection<ServiceTemplate> installedTemplates() {
return List.of();
Expand Down