diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java index 16d910b8f..8f5243366 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java @@ -25,281 +25,297 @@ package org.geysermc.connector.network.translators.java; +import com.github.steveice10.mc.protocol.data.game.command.CommandNode; +import com.github.steveice10.mc.protocol.data.game.command.CommandParser; import com.github.steveice10.mc.protocol.packet.ingame.server.ServerDeclareCommandsPacket; +import com.nukkitx.protocol.bedrock.data.command.CommandData; +import com.nukkitx.protocol.bedrock.data.command.CommandEnumData; +import com.nukkitx.protocol.bedrock.data.command.CommandParamData; +import com.nukkitx.protocol.bedrock.data.command.CommandParamType; +import com.nukkitx.protocol.bedrock.packet.AvailableCommandsPacket; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; +import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + @Translator(packet = ServerDeclareCommandsPacket.class) public class JavaDeclareCommandsTranslator extends PacketTranslator { @Override public void translate(ServerDeclareCommandsPacket packet, GeyserSession session) { // Don't send command suggestions if they are disabled -// if (!session.getConnector().getConfig().isCommandSuggestions()) { -// session.getConnector().getLogger().debug("Not sending command suggestions as they are disabled."); -// return; -// } -// List commandData = new ArrayList<>(); -// Int2ObjectMap commands = new Int2ObjectOpenHashMap<>(); -// Int2ObjectMap> commandArgs = new Int2ObjectOpenHashMap<>(); -// -// // Get the first node, it should be a root node -// CommandNode rootNode = packet.getNodes()[packet.getFirstNodeIndex()]; -// -// // Loop through the root nodes to get all commands -// for (int nodeIndex : rootNode.getChildIndices()) { -// CommandNode node = packet.getNodes()[nodeIndex]; -// -// // Make sure we don't have duplicated commands (happens if there is more than 1 root node) -// if (commands.containsKey(nodeIndex)) { continue; } -// if (commands.containsValue(node.getName())) { continue; } -// -// // Get and update the commandArgs list with the found arguments -// if (node.getChildIndices().length >= 1) { -// for (int childIndex : node.getChildIndices()) { -// commandArgs.putIfAbsent(nodeIndex, new ArrayList<>()); -// commandArgs.get(nodeIndex).add(packet.getNodes()[childIndex]); -// } -// } -// -// // Insert the command name into the list -// commands.put(nodeIndex, node.getName()); -// } -// -// // The command flags, not sure what these do apart from break things -// List flags = new ArrayList<>(); -// -// // Loop through all the found commands -// for (int commandID : commands.keySet()) { -// String commandName = commands.get(commandID); -// -// // Create a basic alias -// CommandEnumData aliases = new CommandEnumData( commandName + "Aliases", new String[] { commandName.toLowerCase() }, false); -// -// // Get and parse all params -// CommandParamData[][] params = getParams(packet.getNodes()[commandID], packet.getNodes()); -// -// // Build the completed command and add it to the final list -// CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, params); -// commandData.add(data); -// } -// -// // Add our commands to the AvailableCommandsPacket for the bedrock client -// AvailableCommandsPacket availableCommandsPacket = new AvailableCommandsPacket(); -// for (CommandData data : commandData) { -// availableCommandsPacket.getCommands().add(data); -// } -// -// GeyserConnector.getInstance().getLogger().debug("Sending command packet of " + commandData.size() + " commands"); -// -// // Finally, send the commands to the client -// session.sendUpstreamPacket(availableCommandsPacket); -// } -// -// /** -// * Build the command parameter array for the given command -// * -// * @param commandNode The command to build the parameters for -// * @param allNodes Every command node -// * -// * @return An array of parameter option arrays -// */ -// private CommandParamData[][] getParams(CommandNode commandNode, CommandNode[] allNodes) { -// // Check if the command is an alias and redirect it -// if (commandNode.getRedirectIndex() != -1) { -// GeyserConnector.getInstance().getLogger().debug("Redirecting command " + commandNode.getName() + " to " + allNodes[commandNode.getRedirectIndex()].getName()); -// commandNode = allNodes[commandNode.getRedirectIndex()]; -// } -// -// if (commandNode.getChildIndices().length >= 1) { -// // Create the root param node and build all the children -// ParamInfo rootParam = new ParamInfo(commandNode, null); -// rootParam.buildChildren(allNodes); -// -// List treeData = rootParam.getTree(); -// CommandParamData[][] params = new CommandParamData[treeData.size()][]; -// -// // Fill the nested params array -// int i = 0; -// for (CommandParamData[] tree : treeData) { -// params[i] = tree; -// i++; -// } -// -// return params; -// } -// -// return new CommandParamData[0][0]; -// } -// -// /** -// * Convert Java edition command types to Bedrock edition -// * -// * @param parser Command type to convert -// * -// * @return Bedrock parameter data type -// */ -// private CommandParamData.Type mapCommandType(CommandParser parser) { -// if (parser == null) { return CommandParamData.Type.STRING; } //TODO: this -// -// switch (parser) { -// case FLOAT: -// return CommandParamData.Type.FLOAT; -// -// case INTEGER: -// return CommandParamData.Type.INT; -// -// case ENTITY: -// case GAME_PROFILE: -// return CommandParamData.Type.TARGET; -// -// case BLOCK_POS: -// return CommandParamData.Type.BLOCK_POSITION; -// -// case COLUMN_POS: -// case VEC3: -// return CommandParamData.Type.POSITION; -// -// case MESSAGE: -// return CommandParamData.Type.MESSAGE; -// -// case NBT: -// case NBT_COMPOUND_TAG: -// case NBT_TAG: -// case NBT_PATH: -// return CommandParamData.Type.JSON; -// -// case RESOURCE_LOCATION: -// return CommandParamData.Type.FILE_PATH; -// -// case INT_RANGE: -// return CommandParamData.Type.INT_RANGE; -// -// case BOOL: -// case DOUBLE: -// case STRING: -// case VEC2: -// case BLOCK_STATE: -// case BLOCK_PREDICATE: -// case ITEM_STACK: -// case ITEM_PREDICATE: -// case COLOR: -// case COMPONENT: -// case OBJECTIVE: -// case OBJECTIVE_CRITERIA: -// case OPERATION: // Possibly OPERATOR -// case PARTICLE: -// case ROTATION: -// case SCOREBOARD_SLOT: -// case SCORE_HOLDER: -// case SWIZZLE: -// case TEAM: -// case ITEM_SLOT: -// case MOB_EFFECT: -// case FUNCTION: -// case ENTITY_ANCHOR: -// case RANGE: -// case FLOAT_RANGE: -// case ITEM_ENCHANTMENT: -// case ENTITY_SUMMON: -// case DIMENSION: -// case TIME: -// default: -// return CommandParamData.Type.STRING; -// } -// } -// -// @Getter -// private class ParamInfo { -// private CommandNode paramNode; -// private CommandParamData paramData; -// private List children; -// -// /** -// * Create a new parameter info object -// * -// * @param paramNode CommandNode the parameter is for -// * @param paramData The existing parameters for the command -// */ -// public ParamInfo(CommandNode paramNode, CommandParamData paramData) { -// this.paramNode = paramNode; -// this.paramData = paramData; -// this.children = new ArrayList<>(); -// } -// -// /** -// * Build the array of all the child parameters (recursive) -// * -// * @param allNodes Every command node -// */ -// public void buildChildren(CommandNode[] allNodes) { -// int enumIndex = -1; -// -// for (int paramID : paramNode.getChildIndices()) { -// CommandNode paramNode = allNodes[paramID]; -// -// if (paramNode.getParser() == null) { -// if (enumIndex == -1) { -// enumIndex = children.size(); -// -// // Create the new enum command -// CommandEnumData enumData = new CommandEnumData(paramNode.getName(), new String[] { paramNode.getName() }, false); -// children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), false, enumData, mapCommandType(paramNode.getParser()), null, Collections.emptyList()))); -// } else { -// // Get the existing enum -// ParamInfo enumParamInfo = children.get(enumIndex); -// -// // Extend the current list of enum values -// String[] enumOptions = Arrays.copyOf(enumParamInfo.getParamData().getEnumData().getValues(), enumParamInfo.getParamData().getEnumData().getValues().length + 1); -// enumOptions[enumOptions.length - 1] = paramNode.getName(); -// -// // Re-create the command using the updated values -// CommandEnumData enumData = new CommandEnumData(enumParamInfo.getParamData().getEnumData().getName(), enumOptions, false); -// children.set(enumIndex, new ParamInfo(enumParamInfo.getParamNode(), new CommandParamData(enumParamInfo.getParamData().getName(), false, enumData, enumParamInfo.getParamData().getType(), null, Collections.emptyList()))); -// } -// }else{ -// // Put the non-enum param into the list -// children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), false, null, mapCommandType(paramNode.getParser()), null, Collections.emptyList()))); -// } -// } -// -// // Recursively build all child options -// for (ParamInfo child : children) { -// child.buildChildren(allNodes); -// } -// } -// -// /** -// * Get the tree of every parameter node (recursive) -// * -// * @return List of parameter options arrays for the command -// */ -// public List getTree() { -// List treeParamData = new ArrayList<>(); -// -// for (ParamInfo child : children) { -// // Get the tree from the child -// List childTree = child.getTree(); -// -// // Un-pack the tree append the child node to it and push into the list -// for (CommandParamData[] subchild : childTree) { -// CommandParamData[] tmpTree = new ArrayList() { -// { -// add(child.getParamData()); -// addAll(Arrays.asList(subchild)); -// } -// }.toArray(new CommandParamData[0]); -// -// treeParamData.add(tmpTree); -// } -// -// // If we have no more child parameters just the child -// if (childTree.size() == 0) { -// treeParamData.add(new CommandParamData[] { child.getParamData() }); -// } -// } -// -// return treeParamData; -// } + if (!session.getConnector().getConfig().isCommandSuggestions()) { + session.getConnector().getLogger().debug("Not sending command suggestions as they are disabled."); + return; + } + List commandData = new ArrayList<>(); + Int2ObjectMap commands = new Int2ObjectOpenHashMap<>(); + Int2ObjectMap> commandArgs = new Int2ObjectOpenHashMap<>(); + + // Get the first node, it should be a root node + CommandNode rootNode = packet.getNodes()[packet.getFirstNodeIndex()]; + + // Loop through the root nodes to get all commands + for (int nodeIndex : rootNode.getChildIndices()) { + CommandNode node = packet.getNodes()[nodeIndex]; + + // Make sure we don't have duplicated commands (happens if there is more than 1 root node) + if (commands.containsKey(nodeIndex)) { continue; } + if (commands.containsValue(node.getName())) { continue; } + + // Get and update the commandArgs list with the found arguments + if (node.getChildIndices().length >= 1) { + for (int childIndex : node.getChildIndices()) { + commandArgs.putIfAbsent(nodeIndex, new ArrayList<>()); + commandArgs.get(nodeIndex).add(packet.getNodes()[childIndex]); + } + } + + // Insert the command name into the list + commands.put(nodeIndex, node.getName()); + } + + // The command flags, not sure what these do apart from break things + List flags = new ArrayList<>(); + + // Loop through all the found commands + for (int commandID : commands.keySet()) { + String commandName = commands.get(commandID); + + // Create a basic alias + CommandEnumData aliases = new CommandEnumData( commandName + "Aliases", new String[] { commandName.toLowerCase() }, false); + + // Get and parse all params + CommandParamData[][] params = getParams(packet.getNodes()[commandID], packet.getNodes()); + + // Build the completed command and add it to the final list + CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, params); + commandData.add(data); + } + + // Add our commands to the AvailableCommandsPacket for the bedrock client + AvailableCommandsPacket availableCommandsPacket = new AvailableCommandsPacket(); + for (CommandData data : commandData) { + availableCommandsPacket.getCommands().add(data); + } + + GeyserConnector.getInstance().getLogger().debug("Sending command packet of " + commandData.size() + " commands"); + + // Finally, send the commands to the client + session.sendUpstreamPacket(availableCommandsPacket); + } + + /** + * Build the command parameter array for the given command + * + * @param commandNode The command to build the parameters for + * @param allNodes Every command node + * + * @return An array of parameter option arrays + */ + private CommandParamData[][] getParams(CommandNode commandNode, CommandNode[] allNodes) { + // Check if the command is an alias and redirect it + if (commandNode.getRedirectIndex() != -1) { + GeyserConnector.getInstance().getLogger().debug("Redirecting command " + commandNode.getName() + " to " + allNodes[commandNode.getRedirectIndex()].getName()); + commandNode = allNodes[commandNode.getRedirectIndex()]; + } + + if (commandNode.getChildIndices().length >= 1) { + // Create the root param node and build all the children + ParamInfo rootParam = new ParamInfo(commandNode, null); + rootParam.buildChildren(allNodes); + + List treeData = rootParam.getTree(); + CommandParamData[][] params = new CommandParamData[treeData.size()][]; + + // Fill the nested params array + int i = 0; + for (CommandParamData[] tree : treeData) { + params[i] = tree; + i++; + } + + return params; + } + + return new CommandParamData[0][0]; + } + + /** + * Convert Java edition command types to Bedrock edition + * + * @param parser Command type to convert + * + * @return Bedrock parameter data type + */ + private CommandParamType mapCommandType(CommandParser parser) { + if (parser == null) { return CommandParamType.STRING; } + + switch (parser) { + case FLOAT: + return CommandParamType.FLOAT; + + case INTEGER: + return CommandParamType.INT; + + case ENTITY: + case GAME_PROFILE: + return CommandParamType.TARGET; + + case BLOCK_POS: + return CommandParamType.BLOCK_POSITION; + + case COLUMN_POS: + case VEC3: + return CommandParamType.POSITION; + + case MESSAGE: + return CommandParamType.MESSAGE; + + case NBT: + case NBT_COMPOUND_TAG: + case NBT_TAG: + case NBT_PATH: + return CommandParamType.JSON; + + case RESOURCE_LOCATION: + return CommandParamType.FILE_PATH; + + case INT_RANGE: + return CommandParamType.INT_RANGE; + + case BOOL: + case DOUBLE: + case STRING: + case VEC2: + case BLOCK_STATE: + case BLOCK_PREDICATE: + case ITEM_STACK: + case ITEM_PREDICATE: + case COLOR: + case COMPONENT: + case OBJECTIVE: + case OBJECTIVE_CRITERIA: + case OPERATION: // Possibly OPERATOR + case PARTICLE: + case ROTATION: + case SCOREBOARD_SLOT: + case SCORE_HOLDER: + case SWIZZLE: + case TEAM: + case ITEM_SLOT: + case MOB_EFFECT: + case FUNCTION: + case ENTITY_ANCHOR: + case RANGE: + case FLOAT_RANGE: + case ITEM_ENCHANTMENT: + case ENTITY_SUMMON: + case DIMENSION: + case TIME: + default: + return CommandParamType.STRING; + } + } + + @Getter + private class ParamInfo { + private CommandNode paramNode; + private CommandParamData paramData; + private List children; + + /** + * Create a new parameter info object + * + * @param paramNode CommandNode the parameter is for + * @param paramData The existing parameters for the command + */ + public ParamInfo(CommandNode paramNode, CommandParamData paramData) { + this.paramNode = paramNode; + this.paramData = paramData; + this.children = new ArrayList<>(); + } + + /** + * Build the array of all the child parameters (recursive) + * + * @param allNodes Every command node + */ + public void buildChildren(CommandNode[] allNodes) { + int enumIndex = -1; + + for (int paramID : paramNode.getChildIndices()) { + CommandNode paramNode = allNodes[paramID]; + + if (paramNode.getParser() == null) { + if (enumIndex == -1) { + enumIndex = children.size(); + + // Create the new enum command + CommandEnumData enumData = new CommandEnumData(paramNode.getName(), new String[] { paramNode.getName() }, false); + children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), false, enumData, mapCommandType(paramNode.getParser()), null, Collections.emptyList()))); + } else { + // Get the existing enum + ParamInfo enumParamInfo = children.get(enumIndex); + + // Extend the current list of enum values + String[] enumOptions = Arrays.copyOf(enumParamInfo.getParamData().getEnumData().getValues(), enumParamInfo.getParamData().getEnumData().getValues().length + 1); + enumOptions[enumOptions.length - 1] = paramNode.getName(); + + // Re-create the command using the updated values + CommandEnumData enumData = new CommandEnumData(enumParamInfo.getParamData().getEnumData().getName(), enumOptions, false); + children.set(enumIndex, new ParamInfo(enumParamInfo.getParamNode(), new CommandParamData(enumParamInfo.getParamData().getName(), false, enumData, enumParamInfo.getParamData().getType(), null, Collections.emptyList()))); + } + }else{ + // Put the non-enum param into the list + children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), false, null, mapCommandType(paramNode.getParser()), null, Collections.emptyList()))); + } + } + + // Recursively build all child options + for (ParamInfo child : children) { + child.buildChildren(allNodes); + } + } + + /** + * Get the tree of every parameter node (recursive) + * + * @return List of parameter options arrays for the command + */ + public List getTree() { + List treeParamData = new ArrayList<>(); + + for (ParamInfo child : children) { + // Get the tree from the child + List childTree = child.getTree(); + + // Un-pack the tree append the child node to it and push into the list + for (CommandParamData[] subchild : childTree) { + CommandParamData[] tmpTree = new ArrayList() { + { + add(child.getParamData()); + addAll(Arrays.asList(subchild)); + } + }.toArray(new CommandParamData[0]); + + treeParamData.add(tmpTree); + } + + // If we have no more child parameters just the child + if (childTree.size() == 0) { + treeParamData.add(new CommandParamData[] { child.getParamData() }); + } + } + + return treeParamData; + } } }