Implement rich commands for Cloud on standalone

This commit is contained in:
Camotoy 2024-11-01 00:39:24 -04:00
parent edd62eb39e
commit df8bd9b583
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
2 changed files with 116 additions and 0 deletions

View file

@ -27,6 +27,13 @@ package org.geysermc.geyser.command;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.protocol.bedrock.data.command.CommandData;
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumConstraint;
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumData;
import org.cloudburstmc.protocol.bedrock.data.command.CommandOverloadData;
import org.cloudburstmc.protocol.bedrock.data.command.CommandParam;
import org.cloudburstmc.protocol.bedrock.data.command.CommandParamData;
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.command.Command;
import org.geysermc.geyser.api.event.EventRegistrar;
@ -51,14 +58,26 @@ import org.geysermc.geyser.command.defaults.StopCommand;
import org.geysermc.geyser.command.defaults.VersionCommand;
import org.geysermc.geyser.event.type.GeyserDefineCommandsEventImpl;
import org.geysermc.geyser.extension.command.GeyserExtensionCommand;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import org.incendo.cloud.Command.Builder;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.execution.ExecutionCoordinator;
import org.incendo.cloud.internal.CommandNode;
import org.incendo.cloud.parser.standard.EnumParser;
import org.incendo.cloud.parser.standard.IntegerParser;
import org.incendo.cloud.parser.standard.LiteralParser;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import static org.geysermc.geyser.command.GeyserCommand.DEFAULT_ROOT_COMMAND;
@ -299,4 +318,96 @@ public class CommandRegistry implements EventRegistrar {
public void runCommand(@NonNull GeyserCommandSource source, @NonNull String command) {
cloud.commandExecutor().executeCommand(source, command);
}
public void export(GeyserSession session, List<CommandData> bedrockCommands) {
cloud.commandTree().rootNode().children().forEach(commandTree -> {
var command = commandTree.command();
// Command null happens if you register an extension command with custom Cloud parameters...
if (command == null || session.hasPermission(command.commandPermission().permissionString())) {
var rootComponent = commandTree.component();
String name = rootComponent.name();
LinkedHashMap<String, Set<CommandEnumConstraint>> values = new LinkedHashMap<>();
for (String s : rootComponent.aliases()) {
values.put(s, EnumSet.of(CommandEnumConstraint.ALLOW_ALIASES));
}
CommandEnumData aliases = new CommandEnumData(name + "Aliases", values, false);
List<CommandOverloadData> data = new ArrayList<>();
for (var node : commandTree.children()) {
List<List<CommandParamData>> params = new ArrayList<>();
createParamData(node, params);
params.forEach(param -> data.add(new CommandOverloadData(false, param.toArray(CommandParamData[]::new))));
}
CommandData bedrockCommand = new CommandData(name, rootComponent.description().textDescription(),
Set.of(CommandData.Flag.NOT_CHEAT), CommandPermission.ANY, aliases,
Collections.emptyList(), data.toArray(new CommandOverloadData[0]));
bedrockCommands.add(bedrockCommand);
}
});
}
private void createParamData(CommandNode<GeyserCommandSource> node, List<List<CommandParamData>> bedrockData) {
CommandParamData data = new CommandParamData();
var component = node.component();
data.setName(component.name());
data.setOptional(component.optional());
var suggestionProvider = component.suggestionProvider();
if (suggestionProvider instanceof LiteralParser<GeyserCommandSource> parser) {
Map<String, Set<CommandEnumConstraint>> values = new LinkedHashMap<>();
for (String alias : parser.aliases()) {
values.put(alias, Set.of());
}
data.setEnumData(new CommandEnumData(component.name(), values, false));
} else if (suggestionProvider instanceof IntegerParser<GeyserCommandSource>) {
data.setType(CommandParam.INT);
} else if (suggestionProvider instanceof EnumParser<?,?> parser) {
LinkedHashMap<String, Set<CommandEnumConstraint>> map = new LinkedHashMap<>();
for (Enum<?> e : parser.acceptedValues()) {
map.put(e.name().toLowerCase(Locale.ROOT), Set.of());
}
data.setEnumData(new CommandEnumData(component.name().toLowerCase(Locale.ROOT), map, false));
} else {
data.setType(CommandParam.STRING);
}
// This, realistically, is not going to be used without extensions using internals and implementing complicated commands.
// It essentially does the same behavior as JavaCommandsTranslator#isCompatible.
// But, selfishly, I would like to use it, and in the future it's possible extensions can register commands
// using Cloud, and in that case this becomes relevant!
if (bedrockData.isEmpty()) {
List<CommandParamData> list = new ArrayList<>();
list.add(data);
bedrockData.add(list);
} else {
int size = bedrockData.size(); // Preserve original list size in case new entries get added.
for (int i = 0; i < size; i++) {
List<CommandParamData> cpdList = bedrockData.get(i);
if (cpdList.size() <= 1) { // No commands or parent will be root.
cpdList.add(data);
} else {
String parentName = node.parent().component().name(); // Should never be null.
if (!cpdList.get(cpdList.size() - 1).getName().equals(parentName)) { // We need to copy the list as this is a new branch.
for (int j = cpdList.size() - 2; j >= 0; j--) {
if (cpdList.get(j).getName().equals(parentName)) {
List<CommandParamData> newList = new ArrayList<>(cpdList.subList(0, j + 1));
newList.add(data);
bedrockData.add(newList);
break;
}
}
} else {
cpdList.add(data);
}
}
}
}
for (var child : node.children()) {
createParamData(child, bedrockData);
}
}
}

View file

@ -41,6 +41,7 @@ import org.cloudburstmc.protocol.bedrock.data.command.*;
import org.cloudburstmc.protocol.bedrock.packet.AvailableCommandsPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.java.ServerDefineCommandsEvent;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.item.enchantment.Enchantment;
import org.geysermc.geyser.registry.BlockRegistries;
@ -213,6 +214,10 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
commandData.add(createFakeHelpCommand());
}
if (session.getGeyser().platformType() == PlatformType.STANDALONE) {
session.getGeyser().commandRegistry().export(session, commandData);
}
// Add our commands to the AvailableCommandsPacket for the bedrock client
AvailableCommandsPacket availableCommandsPacket = new AvailableCommandsPacket();
availableCommandsPacket.getCommands().addAll(commandData);