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).
This commit is contained in:
KennyTV 2021-06-01 17:52:48 +02:00
parent 4011aee280
commit 104fa4e29f
No known key found for this signature in database
GPG Key ID: 6BE3B555EBC5982B
10 changed files with 317 additions and 46 deletions

View File

@ -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<Integer> supportedClientVersion, int serverVersion);
@ -141,6 +143,37 @@ public interface ProtocolManager {
*/
@Nullable List<ProtocolPathEntry> 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.
* <p>
* 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.
* <p>
* Negative examples if this returns true:
* <ul>
* 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.
* </ul>
* <p>
* Negative examples if this returns false:
* <ul>
* 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.
* </ul>
*
* @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)}.
*

View File

@ -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 + ")");
}
}

View File

@ -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<Protocol> 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<Protocol> 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<ProtocolPathEntry> 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<ProtocolPathEntry> 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<ProtocolPathEntry> 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<ProtocolPathEntry> protocolList = pathCache.get(protocolKey);
if (protocolList != null) {
return protocolList;
}
// Generate path
List<ProtocolPathEntry> outputPath = getProtocolPath(new ArrayList<>(), clientVersion, serverVersion);
// If it found a path, cache it.
if (outputPath != null) {
pathCache.put(protocolKey, outputPath);
// Calculate path
Int2ObjectSortedMap<Protocol> outputPath = getProtocolPath(new Int2ObjectLinkedOpenHashMap<>(), clientVersion, serverVersion);
if (outputPath == null) {
return null;
}
return outputPath;
List<ProtocolPathEntry> path = new ArrayList<>(outputPath.size());
for (Int2ObjectMap.Entry<Protocol> 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<ProtocolPathEntry> getProtocolPath(List<ProtocolPathEntry> current, int clientVersion, int serverVersion) {
//TODO optimize?
if (clientVersion == serverVersion) return null; // We're already there
private @Nullable Int2ObjectSortedMap<Protocol> getProtocolPath(Int2ObjectSortedMap<Protocol> 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<Protocol> inputMap = registryMap.get(clientVersion);
if (inputMap == null) {
// First, check if there is any protocols for this
Int2ObjectMap<Protocol> 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<ProtocolPathEntry> shortest = null;
for (Int2ObjectMap.Entry<Protocol> entry : inputMap.int2ObjectEntrySet()) {
// Ensure it wasn't caught by the other loop
if (entry.getIntKey() == serverVersion) continue;
Int2ObjectSortedMap<Protocol> shortest = null;
for (Int2ObjectMap.Entry<Protocol> 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<ProtocolPathEntry> newCurrent = new ArrayList<>(current);
newCurrent.add(pathEntry);
Int2ObjectSortedMap<Protocol> 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;

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, Object> config) {
}
@Override
public List<String> getUnsupportedOptions() {
return Collections.emptyList();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}