Skip to content

Commit

Permalink
Fix ProtocolLib tab completion on modern server versions when using P…
Browse files Browse the repository at this point in the history
…aper module
  • Loading branch information
vaperion committed Aug 7, 2024
1 parent 1d664d5 commit ba57814
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 5 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ allprojects {
}

group = 'me.vaperion.blade'
version = '3.0.14'
version = '3.0.15'

// workaround for gradle issue: https://github.com/gradle/gradle/issues/17236#issuecomment-894385386
tasks.withType(Copy).configureEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class BladeBukkitPlatform implements BladePlatform {
SYNC_COMMANDS = syncCommands;
}

private final JavaPlugin plugin;
protected final JavaPlugin plugin;

@Override
public @NotNull Object getPluginInstance() {
Expand All @@ -60,13 +60,19 @@ public void configureBlade(Blade.@NotNull Builder builder, @NotNull BladeConfigu
configuration.setPluginInstance(plugin);
configuration.setFallbackPrefix(plugin.getName().toLowerCase(Locale.ROOT));
configuration.setHelpGenerator(new BukkitHelpGenerator());
configuration.setTabCompleter(Bukkit.getPluginManager().isPluginEnabled("ProtocolLib") ? new ProtocolLibTabCompleter(plugin) : new TabCompleter.Default());
configureTabCompleter(configuration);

Binder binder = new Binder(builder, true);
binder.bind(Player.class, new PlayerArgument());
binder.bind(OfflinePlayer.class, new OfflinePlayerArgument());
}

public void configureTabCompleter(@NotNull BladeConfiguration configuration) {
configuration.setTabCompleter(Bukkit.getPluginManager().isPluginEnabled("ProtocolLib")
? new ProtocolLibTabCompleter(plugin)
: new TabCompleter.Default());
}

@Override
public void postCommandMapUpdate() {
if (SYNC_COMMANDS != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.FieldAccessException;
import me.vaperion.blade.Blade;
import me.vaperion.blade.bukkit.context.BukkitSender;
import me.vaperion.blade.platform.TabCompleter;
Expand Down Expand Up @@ -48,7 +49,14 @@ public void onPacketReceiving(PacketEvent event) {
ProtocolLibrary.getProtocolManager().sendServerPacket(player, tabComplete);
} catch (Exception ex) {
System.err.println("An exception was thrown while attempting to tab complete '" + commandLine + "' for player " + player.getName());

ex.printStackTrace();

if (ex instanceof FieldAccessException) {
System.err.println("This is likely due to a version incompatibility between Blade and ProtocolLib. " +
"If your server is running Minecraft 1.13 or newer, please consider using BladePaperPlatform instead of BladeBukkitPlatform," +
" as it properly adds support for Brigadier. If this is not your plugin, please relay this information to the plugin author.");
}
}
}
}
7 changes: 5 additions & 2 deletions paper/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ targetCompatibility = JavaVersion.VERSION_17

dependencies {
implementation project(":core")
implementation project(":bukkit")

implementation(project(":bukkit")) {
exclude group: 'com.comphenix.protocol', module: 'ProtocolLib'
}

compileOnly 'io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT'
compileOnly 'io.papermc.paper:paper-mojangapi:1.20.4-R0.1-SNAPSHOT'
compileOnly 'com.comphenix.protocol:ProtocolLib:4.6.0'
compileOnly 'com.comphenix.protocol:ProtocolLib:5.3.0-SNAPSHOT'
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import me.vaperion.blade.bukkit.BladeBukkitPlatform;
import me.vaperion.blade.bukkit.context.BukkitSender;
import me.vaperion.blade.paper.brigadier.BladeBrigadierSupport;
import me.vaperion.blade.paper.platform.NewProtocolLibTabCompleter;
import me.vaperion.blade.platform.BladeConfiguration;
import me.vaperion.blade.platform.TabCompleter;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;

Expand All @@ -24,4 +28,11 @@ public void ingestBlade(@NotNull Blade blade) {
t.printStackTrace();
}
}

@Override
public void configureTabCompleter(@NotNull BladeConfiguration configuration) {
configuration.setTabCompleter(Bukkit.getPluginManager().isPluginEnabled("ProtocolLib")
? new NewProtocolLibTabCompleter(plugin)
: new TabCompleter.Default());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package me.vaperion.blade.paper.platform;

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.wrappers.Converters;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import me.vaperion.blade.Blade;
import me.vaperion.blade.bukkit.context.BukkitSender;
import me.vaperion.blade.platform.TabCompleter;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Optional;

public class NewProtocolLibTabCompleter extends PacketAdapter implements TabCompleter {

private Blade blade;

public NewProtocolLibTabCompleter(@NotNull JavaPlugin plugin) {
super(plugin, PacketType.Play.Client.TAB_COMPLETE);
}

@Override
public void init(@NotNull Blade blade) {
this.blade = blade;
ProtocolLibrary.getProtocolManager().addPacketListener(this);
}

@Override
public void onPacketReceiving(PacketEvent event) {
if (event.getPlayer() == null) return;

Player player = event.getPlayer();
String commandLine = event.getPacket().getStrings().read(0);

if (!commandLine.startsWith("/")) return;
else commandLine = commandLine.substring(1);

List<String> suggestions = blade.getCompleter().suggest(commandLine, () -> new BukkitSender(player));
if (suggestions == null) return; // if command was not found

try {
event.setCancelled(true);

PacketContainer tabComplete = new PacketContainer(PacketType.Play.Server.TAB_COMPLETE);
int intCount = tabComplete.getIntegers().size();

if (intCount == 0) {
sendLegacyCompletions(player, suggestions, tabComplete);
} else if (intCount == 1) {
sendModernCompletions(player, commandLine, suggestions,
event.getPacket(), tabComplete);
} else if (intCount == 3) {
sendLatestCompletions(player, commandLine, suggestions,
event.getPacket(), tabComplete);
} else {
throw new UnsupportedOperationException("Unsupported tab complete packet structure, int count: " + intCount);
}
} catch (Exception ex) {
System.err.println("An exception was thrown while attempting to tab complete '" + commandLine + "' for player " + player.getName());
ex.printStackTrace();
}
}

private void sendLegacyCompletions(@NotNull Player player,
@NotNull List<String> suggestions,
@NotNull PacketContainer container) {
// Used: 1.8 - 1.12.2
// Packet structure: String[] (suggestions)

container.getStringArrays().write(0, suggestions.toArray(new String[0]));
ProtocolLibrary.getProtocolManager().sendServerPacket(player, container);
}

private void sendModernCompletions(@NotNull Player player,
@NotNull String commandLine,
@NotNull List<String> suggestions,
@NotNull PacketContainer received,
@NotNull PacketContainer container) {
// Used: 1.13 - 1.20.4
// Packet structure: int (transaction id), Suggestions (suggestions)

int rangeStart = commandLine.lastIndexOf(' ') + 1;
int rangeEnd = commandLine.length();

StringRange stringRange = StringRange.between(rangeStart + 1, rangeEnd + 1);

List<Suggestion> entries = suggestions.stream()
.map(suggestion -> new Suggestion(stringRange, suggestion))
.toList();

Suggestions brigadierSuggestions = new Suggestions(stringRange, entries);

container.getIntegers().write(0, received.getIntegers().read(0)); // transaction id

container.getSpecificModifier(Suggestions.class).write(0, brigadierSuggestions);

ProtocolLibrary.getProtocolManager().sendServerPacket(player, container);
}

private void sendLatestCompletions(@NotNull Player player,
@NotNull String commandLine,
@NotNull List<String> suggestions,
@NotNull PacketContainer received,
@NotNull PacketContainer container) {
// Used: 1.20.5 and onwards
// Packet structure: int (transaction id), int (start), int (end), List<Entry> (suggestions)

int rangeStart = commandLine.lastIndexOf(' ') + 1;
int rangeEnd = commandLine.length();

List<SuggestionEntry> entries = suggestions.stream()
.map(suggestion -> new SuggestionEntry(suggestion, Optional.empty()))
.toList();

container.getIntegers().write(0, received.getIntegers().read(0)); // transaction id

container.getIntegers().write(1, rangeStart + 1); // start
container.getIntegers().write(2, rangeEnd + 1); // end

container.getLists(SuggestionEntry.converter()).write(0, entries);

ProtocolLibrary.getProtocolManager().sendServerPacket(player, container);
}

// Yes, this is terrible.
// Blame ProtocolLib for not providing a wrapper for this type.
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
record SuggestionEntry(String text, Optional<Component> tooltip) {
private static final List<String> CLASS_NAMES = List.of(
"net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket$Entry"
);

@NotNull
static EquivalentConverter<SuggestionEntry> converter() {
return Converters.ignoreNull(Converters.handle(SuggestionEntry::toHandle,
SuggestionEntry::fromHandle,
SuggestionEntry.class));
}

@NotNull
static Object toHandle(@NotNull SuggestionEntry entry) {
Object handle = tryCreateHandle(entry.text(), entry.tooltip());
if (handle == null) throw new UnsupportedOperationException("Failed to create handle for SuggestionEntry");

return handle;
}

@NotNull
static SuggestionEntry fromHandle(@NotNull Object object) {
throw new UnsupportedOperationException("Not implemented");
}

@Nullable
private static Object tryCreateHandle(@NotNull String text,
@NotNull Optional<Component> tooltip) {
for (String name : CLASS_NAMES) {
try {
return Class.forName(name)
.getDeclaredConstructor(String.class, Optional.class)
.newInstance(text, tooltip);
} catch (Throwable ignored) {
}
}

return null;
}
}
}

0 comments on commit ba57814

Please sign in to comment.