From 104fa4e29f666a4bc5c3284171d5cb066c926c00 Mon Sep 17 00:00:00 2001 From: KennyTV Date: Tue, 1 Jun 2021 17:52:48 +0200 Subject: [PATCH] Optimize protocol path finding Not perfect, but better. This prevents the path checks from exponentially increasing (if it weren't for the maxProtocolPathSize fail safe). By default, a path will never go to a protocol version that puts it farther from the desired server protocol version, even if a path existed. Otherwise as well as previously, *all* possible paths will be checked until a fitting one is found. Negative examples if the new boolean is set to true: A possible path from 3 to 5 in order of 3->10->5 will be dismissed. A possible path from 5 to 3 in order of 5->0->3 will be dismissed. Negative examples if set to false: While searching for a path from 3 to 5, 3->2->1 could be checked first before 3->4->5 is found. While searching for a path from 5 to 3, 5->6->7 could be checked first before 5->4->3 is found. Assuming custom platforms like Bedrock protocol use the normal registering methods, they will have to change the boolean to false to revert to previous behavior (tho still somewhat better optimized). --- .../api/protocol/ProtocolManager.java | 33 +++++ .../api/protocol/version/ProtocolVersion.java | 14 +- .../protocol/ProtocolManagerImpl.java | 99 ++++++++----- .../common/dummy/DummyInitializer.java | 30 ++++ .../viaversion/common/dummy/TestConfig.java | 47 ++++++ .../viaversion/common/dummy/TestPlatform.java | 140 ++++++++++++++++++ .../common/entities/EntityTypesTest.java | 0 .../common/protocol/ProtocolVersionTest.java | 0 .../viaversion}/common/type/ItemTypeTest.java | 0 .../common/type/StringTypeTest.java | 0 10 files changed, 317 insertions(+), 46 deletions(-) create mode 100644 common/src/test/java/com/viaversion/viaversion/common/dummy/DummyInitializer.java create mode 100644 common/src/test/java/com/viaversion/viaversion/common/dummy/TestConfig.java create mode 100644 common/src/test/java/com/viaversion/viaversion/common/dummy/TestPlatform.java rename common/src/test/java/{us/myles/ViaVersion => com/viaversion/viaversion}/common/entities/EntityTypesTest.java (100%) rename common/src/test/java/{us/myles/ViaVersion => com/viaversion/viaversion}/common/protocol/ProtocolVersionTest.java (100%) rename common/src/test/java/{us/myles/ViaVersion => com/viaversion/viaversion}/common/type/ItemTypeTest.java (100%) rename common/src/test/java/{us/myles/ViaVersion => com/viaversion/viaversion}/common/type/StringTypeTest.java (100%) diff --git a/api/src/main/java/com/viaversion/viaversion/api/protocol/ProtocolManager.java b/api/src/main/java/com/viaversion/viaversion/api/protocol/ProtocolManager.java index d9cf45098..9f3f5bd7f 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/protocol/ProtocolManager.java +++ b/api/src/main/java/com/viaversion/viaversion/api/protocol/ProtocolManager.java @@ -109,6 +109,7 @@ public interface ProtocolManager { * @param protocol protocol to register * @param clientVersion supported client protocol versions * @param serverVersion output server protocol version the protocol converts to + * @throws IllegalArgumentException if the client protocol version is equal to the server protocol version */ void registerProtocol(Protocol protocol, ProtocolVersion clientVersion, ProtocolVersion serverVersion); @@ -118,6 +119,7 @@ public interface ProtocolManager { * @param protocol protocol to register * @param supportedClientVersion supported client protocol versions * @param serverVersion output server protocol version the protocol converts to + * @throws IllegalArgumentException if a supported client protocol version is equal to the server protocol version */ void registerProtocol(Protocol protocol, List supportedClientVersion, int serverVersion); @@ -141,6 +143,37 @@ public interface ProtocolManager { */ @Nullable List getProtocolPath(int clientVersion, int serverVersion); + /** + * Returns whether protocol path calculation expects the path to come closer to the expected version with each entry, true by default. + *

+ * In practice, this means a path will never go to a protocol version that puts it farther from the desired + * server protocol version, even if a path existed. + * If this is set to false, *all* possible paths will be checked until a fitting one is found. + *

+ * Negative examples if this returns true: + *

    + * A possible path from 3 to 5 in order of 3->10->5 will be dismissed. + * A possible path from 5 to 3 in order of 5->0->3 will be dismissed. + *
+ *

+ * Negative examples if this returns false: + *

    + * While searching for a path from 3 to 5, 3->2->1 could be checked first before 3->4->5 is found. + * While searching for a path from 5 to 3, 5->6->7 could be checked first before 5->4->3 is found. + *
+ * + * @return whether protocol path calculation expects the path to come closer to the expected version with each entry + */ + boolean onlyCheckLoweringPathEntries(); + + /** + * Sets whether protocol path calculation expects the path to come closer to the expected version with each entry. + * + * @param onlyCheckLoweringPathEntries whether protocol path calculation expects the path to come closer to the expected version with each entry + * @see #onlyCheckLoweringPathEntries() + */ + void setOnlyCheckLoweringPathEntries(boolean onlyCheckLoweringPathEntries); + /** * Returns the maximum protocol path size applied to {@link #getProtocolPath(int, int)}. * diff --git a/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java b/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java index 401508e66..c1de3c1c3 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java +++ b/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java @@ -113,26 +113,26 @@ public class ProtocolVersion { /** * Returns whether a protocol with the given protocol version is registered. * - * @param id protocol version + * @param version protocol version * @return true if this protocol version has been registered */ - public static boolean isRegistered(int id) { - return VERSIONS.containsKey(id); + public static boolean isRegistered(int version) { + return VERSIONS.containsKey(version); } /** * Returns a {@link ProtocolVersion} instance, even if this protocol version * has not been registered. See {@link #isRegistered(int)} berorehand or {@link #isKnown()}. * - * @param id protocol version + * @param version protocol version * @return registered or unknown {@link ProtocolVersion} */ - public static @NonNull ProtocolVersion getProtocol(int id) { - ProtocolVersion protocolVersion = VERSIONS.get(id); + public static @NonNull ProtocolVersion getProtocol(int version) { + ProtocolVersion protocolVersion = VERSIONS.get(version); if (protocolVersion != null) { return protocolVersion; } else { - return new ProtocolVersion(id, "Unknown (" + id + ")"); + return new ProtocolVersion(version, "Unknown (" + version + ")"); } } diff --git a/common/src/main/java/com/viaversion/viaversion/protocol/ProtocolManagerImpl.java b/common/src/main/java/com/viaversion/viaversion/protocol/ProtocolManagerImpl.java index 0c46ae171..eded44e2d 100644 --- a/common/src/main/java/com/viaversion/viaversion/protocol/ProtocolManagerImpl.java +++ b/common/src/main/java/com/viaversion/viaversion/protocol/ProtocolManagerImpl.java @@ -65,8 +65,10 @@ import com.viaversion.viaversion.protocols.protocol1_9to1_8.Protocol1_9To1_8; import com.viaversion.viaversion.protocols.protocol1_9to1_9_1.Protocol1_9To1_9_1; import com.viaversion.viaversion.util.Pair; import io.netty.buffer.ByteBuf; +import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap; import org.checkerframework.checker.nullness.qual.Nullable; import us.myles.ViaVersion.api.protocol.ProtocolRegistry; @@ -107,6 +109,7 @@ public class ProtocolManagerImpl implements ProtocolManager { private boolean mappingsLoaded; private ServerProtocolVersion serverProtocolVersion = new ServerProtocolVersionSingleton(-1); + private boolean onlyCheckLoweringPathEntries = true; private int maxProtocolPathSize = 50; public ProtocolManagerImpl() { @@ -173,8 +176,11 @@ public class ProtocolManagerImpl implements ProtocolManager { protocols.put(protocol.getClass(), protocol); - for (int version : supportedClientVersion) { - Int2ObjectMap protocolMap = registryMap.computeIfAbsent(version, s -> new Int2ObjectOpenHashMap<>(2)); + for (int clientVersion : supportedClientVersion) { + // Throw an error if supported client version = server version + Preconditions.checkArgument(clientVersion != serverVersion); + + Int2ObjectMap protocolMap = registryMap.computeIfAbsent(clientVersion, s -> new Int2ObjectOpenHashMap<>(2)); protocolMap.put(serverVersion, protocol); } @@ -212,32 +218,40 @@ public class ProtocolManagerImpl implements ProtocolManager { supportedVersions.clear(); supportedVersions.add(serverProtocolVersion.lowestSupportedVersion()); - for (ProtocolVersion versions : ProtocolVersion.getProtocols()) { - List paths = getProtocolPath(versions.getVersion(), serverProtocolVersion.lowestSupportedVersion()); - if (paths == null) continue; - supportedVersions.add(versions.getVersion()); - for (ProtocolPathEntry path : paths) { - supportedVersions.add(path.getOutputProtocolVersion()); + for (ProtocolVersion version : ProtocolVersion.getProtocols()) { + List protocolPath = getProtocolPath(version.getVersion(), serverProtocolVersion.lowestSupportedVersion()); + if (protocolPath == null) continue; + + supportedVersions.add(version.getVersion()); + for (ProtocolPathEntry pathEntry : protocolPath) { + supportedVersions.add(pathEntry.getOutputProtocolVersion()); } } } @Override public @Nullable List getProtocolPath(int clientVersion, int serverVersion) { - ProtocolPathKey protocolKey = new ProtocolPathKeyImpl(clientVersion, serverVersion); + if (clientVersion == serverVersion) return null; // Nothing to do! + // Check cache + ProtocolPathKey protocolKey = new ProtocolPathKeyImpl(clientVersion, serverVersion); List protocolList = pathCache.get(protocolKey); if (protocolList != null) { return protocolList; } - // Generate path - List outputPath = getProtocolPath(new ArrayList<>(), clientVersion, serverVersion); - // If it found a path, cache it. - if (outputPath != null) { - pathCache.put(protocolKey, outputPath); + // Calculate path + Int2ObjectSortedMap outputPath = getProtocolPath(new Int2ObjectLinkedOpenHashMap<>(), clientVersion, serverVersion); + if (outputPath == null) { + return null; } - return outputPath; + + List path = new ArrayList<>(outputPath.size()); + for (Int2ObjectMap.Entry entry : outputPath.int2ObjectEntrySet()) { + path.add(new ProtocolPathEntryImpl(entry.getIntKey(), entry.getValue())); + } + pathCache.put(protocolKey, path); + return path; } /** @@ -248,44 +262,41 @@ public class ProtocolManagerImpl implements ProtocolManager { * @param serverVersion desired output version * @return path that has been generated, null if failed */ - private @Nullable List getProtocolPath(List current, int clientVersion, int serverVersion) { - //TODO optimize? - if (clientVersion == serverVersion) return null; // We're already there + private @Nullable Int2ObjectSortedMap getProtocolPath(Int2ObjectSortedMap current, int clientVersion, int serverVersion) { if (current.size() > maxProtocolPathSize) return null; // Fail safe, protocol too complicated. - // First check if there is any protocols for this - Int2ObjectMap inputMap = registryMap.get(clientVersion); - if (inputMap == null) { + // First, check if there is any protocols for this + Int2ObjectMap toServerProtocolMap = registryMap.get(clientVersion); + if (toServerProtocolMap == null) { return null; // Not supported } - // Next check there isn't an obvious path - Protocol protocol = inputMap.get(serverVersion); + // Next, check if there is a direct, single Protocol path + Protocol protocol = toServerProtocolMap.get(serverVersion); if (protocol != null) { - current.add(new ProtocolPathEntryImpl(serverVersion, protocol)); + current.put(serverVersion, protocol); return current; // Easy solution } // There might be a more advanced solution... So we'll see if any of the others can get us there - List shortest = null; - for (Int2ObjectMap.Entry entry : inputMap.int2ObjectEntrySet()) { - // Ensure it wasn't caught by the other loop - if (entry.getIntKey() == serverVersion) continue; + Int2ObjectSortedMap shortest = null; + for (Int2ObjectMap.Entry entry : toServerProtocolMap.int2ObjectEntrySet()) { + // Ensure we don't go back to already contained versions + int translatedToVersion = entry.getIntKey(); + if (current.containsKey(translatedToVersion)) continue; - ProtocolPathEntry pathEntry = new ProtocolPathEntryImpl(entry.getIntKey(), entry.getValue()); - // Ensure no recursion - if (current.contains(pathEntry)) continue; + // Check if the new version is farther away than the current client version + if (onlyCheckLoweringPathEntries && Math.abs(serverVersion - translatedToVersion) > Math.abs(serverVersion - clientVersion)) { + continue; + } // Create a copy - List newCurrent = new ArrayList<>(current); - newCurrent.add(pathEntry); + Int2ObjectSortedMap newCurrent = new Int2ObjectLinkedOpenHashMap<>(current); + newCurrent.put(translatedToVersion, entry.getValue()); - // Calculate the rest of the protocol using the current path entry - newCurrent = getProtocolPath(newCurrent, entry.getIntKey(), serverVersion); - - // If it's shorter then choose it - if (newCurrent != null - && (shortest == null || shortest.size() > newCurrent.size())) { + // Calculate the rest of the protocol starting from translatedToVersion and take the shortest + newCurrent = getProtocolPath(newCurrent, translatedToVersion, serverVersion); + if (newCurrent != null && (shortest == null || newCurrent.size() < shortest.size())) { shortest = newCurrent; } } @@ -342,6 +353,16 @@ public class ProtocolManagerImpl implements ProtocolManager { return Collections.unmodifiableSortedSet(new TreeSet<>(supportedVersions)); } + @Override + public void setOnlyCheckLoweringPathEntries(boolean onlyCheckLoweringPathEntries) { + this.onlyCheckLoweringPathEntries = onlyCheckLoweringPathEntries; + } + + @Override + public boolean onlyCheckLoweringPathEntries() { + return onlyCheckLoweringPathEntries; + } + @Override public int getMaxProtocolPathSize() { return maxProtocolPathSize; diff --git a/common/src/test/java/com/viaversion/viaversion/common/dummy/DummyInitializer.java b/common/src/test/java/com/viaversion/viaversion/common/dummy/DummyInitializer.java new file mode 100644 index 000000000..96d3d09fc --- /dev/null +++ b/common/src/test/java/com/viaversion/viaversion/common/dummy/DummyInitializer.java @@ -0,0 +1,30 @@ +/* + * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion + * Copyright (C) 2016-2021 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viaversion.common.dummy; + +import com.viaversion.viaversion.ViaManagerImpl; +import com.viaversion.viaversion.api.Via; +import com.viaversion.viaversion.protocol.ProtocolManagerImpl; + +public final class DummyInitializer { + + public static void init() { + Via.init(new ViaManagerImpl(new TestPlatform(), null, null, null)); + ((ProtocolManagerImpl) Via.getManager().getProtocolManager()).registerProtocols(); + } +} diff --git a/common/src/test/java/com/viaversion/viaversion/common/dummy/TestConfig.java b/common/src/test/java/com/viaversion/viaversion/common/dummy/TestConfig.java new file mode 100644 index 000000000..abc1a4d48 --- /dev/null +++ b/common/src/test/java/com/viaversion/viaversion/common/dummy/TestConfig.java @@ -0,0 +1,47 @@ +/* + * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion + * Copyright (C) 2016-2021 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viaversion.common.dummy; + +import com.viaversion.viaversion.configuration.AbstractViaConfig; + +import java.io.File; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public final class TestConfig extends AbstractViaConfig { + + public TestConfig(File configFile) { + super(configFile); + } + + @Override + public URL getDefaultConfigURL() { + return null; + } + + @Override + protected void handleConfig(Map config) { + } + + @Override + public List getUnsupportedOptions() { + return Collections.emptyList(); + } +} diff --git a/common/src/test/java/com/viaversion/viaversion/common/dummy/TestPlatform.java b/common/src/test/java/com/viaversion/viaversion/common/dummy/TestPlatform.java new file mode 100644 index 000000000..eee861ba2 --- /dev/null +++ b/common/src/test/java/com/viaversion/viaversion/common/dummy/TestPlatform.java @@ -0,0 +1,140 @@ +/* + * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion + * Copyright (C) 2016-2021 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viaversion.common.dummy; + +import com.google.gson.JsonObject; +import com.viaversion.viaversion.ViaAPIBase; +import com.viaversion.viaversion.api.ViaAPI; +import com.viaversion.viaversion.api.command.ViaCommandSender; +import com.viaversion.viaversion.api.configuration.ConfigurationProvider; +import com.viaversion.viaversion.api.configuration.ViaVersionConfig; +import com.viaversion.viaversion.api.platform.PlatformTask; +import com.viaversion.viaversion.api.platform.ViaPlatform; +import io.netty.buffer.ByteBuf; + +import java.io.File; +import java.util.UUID; +import java.util.logging.Logger; + +public final class TestPlatform implements ViaPlatform { + + private static final Logger log = Logger.getGlobal(); + private final TestConfig testConfig = new TestConfig(null); + + @Override + public Logger getLogger() { + return log; + } + + @Override + public String getPlatformName() { + return "Test"; + } + + @Override + public String getPlatformVersion() { + return "test"; + } + + @Override + public String getPluginVersion() { + return "test"; + } + + @Override + public PlatformTask runAsync(Runnable runnable) { + return null; + } + + @Override + public PlatformTask runSync(Runnable runnable) { + return null; + } + + @Override + public PlatformTask runSync(Runnable runnable, long ticks) { + return null; + } + + @Override + public PlatformTask runRepeatingSync(Runnable runnable, long ticks) { + return null; + } + + @Override + public ViaCommandSender[] getOnlinePlayers() { + return new ViaCommandSender[0]; + } + + @Override + public void sendMessage(UUID uuid, String message) { + } + + @Override + public boolean kickPlayer(UUID uuid, String message) { + return false; + } + + @Override + public boolean isPluginEnabled() { + return false; + } + + @Override + public ViaAPI getApi() { + return new ViaAPIBase() { + @Override + public int getPlayerVersion(Object player) { + return 0; + } + + @Override + public void sendRawPacket(Object player, ByteBuf packet) { + } + }; + } + + @Override + public ViaVersionConfig getConf() { + return testConfig; + } + + @Override + public ConfigurationProvider getConfigurationProvider() { + return null; + } + + @Override + public File getDataFolder() { + return null; + } + + @Override + public void onReload() { + } + + @Override + public JsonObject getDump() { + return null; + } + + @Override + public boolean isOldClientsAllowed() { + return false; + } +} diff --git a/common/src/test/java/us/myles/ViaVersion/common/entities/EntityTypesTest.java b/common/src/test/java/com/viaversion/viaversion/common/entities/EntityTypesTest.java similarity index 100% rename from common/src/test/java/us/myles/ViaVersion/common/entities/EntityTypesTest.java rename to common/src/test/java/com/viaversion/viaversion/common/entities/EntityTypesTest.java diff --git a/common/src/test/java/us/myles/ViaVersion/common/protocol/ProtocolVersionTest.java b/common/src/test/java/com/viaversion/viaversion/common/protocol/ProtocolVersionTest.java similarity index 100% rename from common/src/test/java/us/myles/ViaVersion/common/protocol/ProtocolVersionTest.java rename to common/src/test/java/com/viaversion/viaversion/common/protocol/ProtocolVersionTest.java diff --git a/common/src/test/java/us/myles/ViaVersion/common/type/ItemTypeTest.java b/common/src/test/java/com/viaversion/viaversion/common/type/ItemTypeTest.java similarity index 100% rename from common/src/test/java/us/myles/ViaVersion/common/type/ItemTypeTest.java rename to common/src/test/java/com/viaversion/viaversion/common/type/ItemTypeTest.java diff --git a/common/src/test/java/us/myles/ViaVersion/common/type/StringTypeTest.java b/common/src/test/java/com/viaversion/viaversion/common/type/StringTypeTest.java similarity index 100% rename from common/src/test/java/us/myles/ViaVersion/common/type/StringTypeTest.java rename to common/src/test/java/com/viaversion/viaversion/common/type/StringTypeTest.java