Add backwards compatibility for versions 1.0 thru 1.7.10
Dieser Commit ist enthalten in:
Ursprung
2c743e1c89
Commit
addfacb19c
2
ProtocolLib/.gitignore
vendored
2
ProtocolLib/.gitignore
vendored
@ -163,3 +163,5 @@ pip-log.txt
|
||||
|
||||
# Mac crap
|
||||
.DS_Store
|
||||
/target
|
||||
/target
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>cp1252</project.build.sourceEncoding>
|
||||
<powermock.version>1.5</powermock.version>
|
||||
<jarName>ProtocolLib</jarName>
|
||||
</properties>
|
||||
|
||||
<distributionManagement>
|
||||
@ -75,6 +75,7 @@
|
||||
<exclude>org.spigotmc:spigot</exclude>
|
||||
<exclude>org.spigotmc:spigot-api</exclude>
|
||||
<exclude>junit:junit</exclude>
|
||||
<exclude>com.google*</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
</configuration>
|
||||
@ -99,7 +100,7 @@
|
||||
<archive>
|
||||
<addMavenDescriptor>false</addMavenDescriptor>
|
||||
</archive>
|
||||
<finalName>${project.name}</finalName>
|
||||
<finalName>${jarName}</finalName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@ -186,6 +187,62 @@
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>backwards-compat</id>
|
||||
|
||||
<properties>
|
||||
<jarName>ProtocolLib-Legacy</jarName>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>2.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>false</shadedArtifactAttached>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>net.sf</pattern>
|
||||
<shadedPattern>com.comphenix.net.sf</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>com.google.common</pattern>
|
||||
<shadedPattern>com.comphenix.protocol.compat.com.google.common</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>io.netty</pattern>
|
||||
<shadedPattern>net.minecraft.util.io.netty</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>org.spigotmc:spigot</exclude>
|
||||
<exclude>org.spigotmc:spigot-api</exclude>
|
||||
<exclude>junit:junit</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<scm>
|
||||
@ -264,14 +321,24 @@
|
||||
<dependency>
|
||||
<groupId>org.powermock</groupId>
|
||||
<artifactId>powermock-module-junit4</artifactId>
|
||||
<version>${powermock.version}</version>
|
||||
<version>1.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.powermock</groupId>
|
||||
<artifactId>powermock-api-mockito</artifactId>
|
||||
<version>${powermock.version}</version>
|
||||
<version>1.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>17.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
<version>4.0.26.Final</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -83,7 +83,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
/**
|
||||
* The minimum version ProtocolLib has been tested with.
|
||||
*/
|
||||
public static final String MINIMUM_MINECRAFT_VERSION = "1.8";
|
||||
public static final String MINIMUM_MINECRAFT_VERSION = "1.0";
|
||||
|
||||
/**
|
||||
* The maximum version ProtocolLib has been tested with,
|
||||
|
@ -50,9 +50,9 @@ import com.comphenix.protocol.utility.MinecraftFields;
|
||||
import com.comphenix.protocol.utility.MinecraftMethods;
|
||||
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.MapMaker;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
/**
|
||||
* Represents a channel injector.
|
||||
@ -563,13 +563,14 @@ class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
||||
PACKET_LOGIN_CLIENT = loginClass;
|
||||
}
|
||||
if (loginClient == null) {
|
||||
loginClient = Accessors.getFieldAccessor(PACKET_LOGIN_CLIENT, GameProfile.class, true);
|
||||
loginClient = Accessors.getFieldAccessor(PACKET_LOGIN_CLIENT, MinecraftReflection.getGameProfileClass(), true);
|
||||
LOGIN_GAME_PROFILE = loginClient;
|
||||
}
|
||||
|
||||
// See if we are dealing with the login packet
|
||||
if (loginClass.equals(packetClass)) {
|
||||
GameProfile profile = (GameProfile) loginClient.get(packet);
|
||||
// GameProfile profile = (GameProfile) loginClient.get(packet);
|
||||
WrappedGameProfile profile = WrappedGameProfile.fromHandle(loginClient.get(packet));
|
||||
|
||||
// Save the channel injector
|
||||
factory.cacheInjector(profile.getName(), this);
|
||||
|
@ -12,8 +12,10 @@ import com.comphenix.protocol.PacketType.Sender;
|
||||
import com.comphenix.protocol.injector.packet.MapContainer;
|
||||
import com.comphenix.protocol.reflect.StructureModifier;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
import com.google.common.collect.BiMap;
|
||||
import com.google.common.collect.HashBiMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
@ -109,6 +111,12 @@ public class NettyProtocolRegistry {
|
||||
private synchronized void initialize() {
|
||||
Object[] protocols = enumProtocol.getEnumConstants();
|
||||
|
||||
// TODO: Fins a better less than 1.7 check
|
||||
if (MinecraftVersion.getCurrentVersion().compareTo(MinecraftVersion.BOUNTIFUL_UPDATE) <= 0) {
|
||||
initialize17();
|
||||
return;
|
||||
}
|
||||
|
||||
// ID to Packet class maps
|
||||
Map<Object, Map<Integer, Class<?>>> serverMaps = Maps.newLinkedHashMap();
|
||||
Map<Object, Map<Integer, Class<?>>> clientMaps = Maps.newLinkedHashMap();
|
||||
@ -140,13 +148,13 @@ public class NettyProtocolRegistry {
|
||||
result.containers.add(new MapContainer(map));
|
||||
}
|
||||
|
||||
// // Heuristic - there are more server packets than client packets
|
||||
// if (sum(clientMaps) > sum(serverMaps)) {
|
||||
// // Swap if this is violated
|
||||
// List<Map<Integer, Class<?>>> temp = serverMaps;
|
||||
// serverMaps = clientMaps;
|
||||
// clientMaps = temp;
|
||||
// }
|
||||
// Heuristic - there are more server packets than client packets
|
||||
/* if (sum(clientMaps) > sum(serverMaps)) {
|
||||
// Swap if this is violated
|
||||
List<Map<Integer, Class<?>>> temp = serverMaps;
|
||||
serverMaps = clientMaps;
|
||||
clientMaps = temp;
|
||||
} */
|
||||
|
||||
for (int i = 0; i < protocols.length; i++) {
|
||||
Object protocol = protocols[i];
|
||||
@ -159,6 +167,50 @@ public class NettyProtocolRegistry {
|
||||
if (clientMaps.containsKey(protocol))
|
||||
associatePackets(result, clientMaps.get(protocol), equivalent, Sender.CLIENT);
|
||||
}
|
||||
|
||||
// Exchange (thread safe, as we have only one writer)
|
||||
this.register = result;
|
||||
}
|
||||
|
||||
private synchronized void initialize17() {
|
||||
final Object[] protocols = enumProtocol.getEnumConstants();
|
||||
List<Map<Integer, Class<?>>> serverMaps = Lists.newArrayList();
|
||||
List<Map<Integer, Class<?>>> clientMaps = Lists.newArrayList();
|
||||
StructureModifier<Object> modifier = null;
|
||||
|
||||
// Result
|
||||
Register result = new Register();
|
||||
|
||||
for (Object protocol : protocols) {
|
||||
if (modifier == null)
|
||||
modifier = new StructureModifier<Object>(protocol.getClass().getSuperclass(), false);
|
||||
StructureModifier<Map<Integer, Class<?>>> maps = modifier.withTarget(protocol).withType(Map.class);
|
||||
|
||||
serverMaps.add(maps.read(0));
|
||||
clientMaps.add(maps.read(1));
|
||||
}
|
||||
// Maps we have to occationally check have changed
|
||||
for (Map<Integer, Class<?>> map : Iterables.concat(serverMaps, clientMaps)) {
|
||||
result.containers.add(new MapContainer(map));
|
||||
}
|
||||
|
||||
// Heuristic - there are more server packets than client packets
|
||||
if (sum(clientMaps) > sum(serverMaps)) {
|
||||
// Swap if this is violated
|
||||
List<Map<Integer, Class<?>>> temp = serverMaps;
|
||||
serverMaps = clientMaps;
|
||||
clientMaps = temp;
|
||||
}
|
||||
|
||||
for (int i = 0; i < protocols.length; i++) {
|
||||
Enum<?> enumProtocol = (Enum<?>) protocols[i];
|
||||
Protocol equivalent = Protocol.fromVanilla(enumProtocol);
|
||||
|
||||
// Associate known types
|
||||
associatePackets(result, serverMaps.get(i), equivalent, Sender.SERVER);
|
||||
associatePackets(result, clientMaps.get(i), equivalent, Sender.CLIENT);
|
||||
}
|
||||
|
||||
// Exchange (thread safe, as we have only one writer)
|
||||
this.register = result;
|
||||
}
|
||||
@ -175,16 +227,16 @@ public class NettyProtocolRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Retrieve the number of mapping in all the maps.
|
||||
// * @param maps - iterable of maps.
|
||||
// * @return The sum of all the entries.
|
||||
// */
|
||||
// private int sum(Iterable<? extends Map<Integer, Class<?>>> maps) {
|
||||
// int count = 0;
|
||||
//
|
||||
// for (Map<Integer, Class<?>> map : maps)
|
||||
// count += map.size();
|
||||
// return count;
|
||||
// }
|
||||
/**
|
||||
* Retrieve the number of mapping in all the maps.
|
||||
* @param maps - iterable of maps.
|
||||
* @return The sum of all the entries.
|
||||
*/
|
||||
private int sum(Iterable<? extends Map<Integer, Class<?>>> maps) {
|
||||
int count = 0;
|
||||
|
||||
for (Map<Integer, Class<?>> map : maps)
|
||||
count += map.size();
|
||||
return count;
|
||||
}
|
||||
}
|
@ -1,5 +1,18 @@
|
||||
/**
|
||||
* (c) 2015 dmulloy2
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2015 dmulloy2
|
||||
*
|
||||
* 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
package com.comphenix.protocol.injector.netty;
|
||||
|
||||
|
@ -128,6 +128,20 @@ public final class Accessors {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a method accessor for a field with the given name and equivalent type, or NULL.
|
||||
* @param clazz - the declaration class.
|
||||
* @param methodName - the method name.
|
||||
* @return The method accessor, or NULL if not found.
|
||||
*/
|
||||
public static MethodAccessor getMethodAcccessorOrNull(Class<?> clazz, String methodName) {
|
||||
try {
|
||||
return Accessors.getMethodAccessor(clazz, methodName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a specific constructor in a class.
|
||||
* @param clazz - the class.
|
||||
|
@ -40,12 +40,21 @@ public class BukkitCloner implements Cloner {
|
||||
List<Class<?>> classes = Lists.newArrayList();
|
||||
|
||||
classes.add(MinecraftReflection.getItemStackClass());
|
||||
classes.add(MinecraftReflection.getBlockPositionClass());
|
||||
classes.add(MinecraftReflection.getDataWatcherClass());
|
||||
|
||||
// Try to add position classes
|
||||
try {
|
||||
classes.add(MinecraftReflection.getBlockPositionClass());
|
||||
} catch (Throwable ex) { }
|
||||
|
||||
try {
|
||||
classes.add(MinecraftReflection.getChunkPositionClass());
|
||||
} catch (Throwable ex) { }
|
||||
|
||||
if (MinecraftReflection.isUsingNetty()) {
|
||||
classes.add(MinecraftReflection.getServerPingClass());
|
||||
}
|
||||
|
||||
this.clonableClasses = classes.toArray(new Class<?>[0]);
|
||||
}
|
||||
|
||||
|
@ -290,7 +290,6 @@ public class DefaultInstances implements InstanceProvider {
|
||||
|
||||
return createInstance(type, minimum, types, params);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// Nope, we couldn't create this type. Might for instance be NotConstructableException.
|
||||
}
|
||||
|
@ -596,8 +596,17 @@ public class MinecraftReflection {
|
||||
if (!isUsingNetty())
|
||||
throw new IllegalStateException("GameProfile does not exist in version 1.6.4 and earlier.");
|
||||
|
||||
// Yay, we can actually refer to it directly
|
||||
return GameProfile.class;
|
||||
try {
|
||||
return GameProfile.class;
|
||||
} catch (Throwable ex) {
|
||||
// As far as I can tell, the named entity spawn packet is the only packet that uses GameProfiles
|
||||
FuzzyReflection reflection = FuzzyReflection.fromClass(PacketType.Play.Server.NAMED_ENTITY_SPAWN.getPacketClass(), true);
|
||||
FuzzyFieldContract contract = FuzzyFieldContract.newBuilder()
|
||||
.banModifier(Modifier.STATIC)
|
||||
.typeMatches(FuzzyMatchers.matchRegex("(.*)(GameProfile)", 1))
|
||||
.build();
|
||||
return reflection.getField(contract).getType();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,18 @@
|
||||
/**
|
||||
* (c) 2015 dmulloy2
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2015 dmulloy2
|
||||
*
|
||||
* 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
package com.comphenix.protocol.wrappers;
|
||||
|
||||
|
@ -31,24 +31,29 @@ import java.util.Set;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.injector.BukkitUnwrapper;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.accessors.Accessors;
|
||||
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
|
||||
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
|
||||
import com.comphenix.protocol.reflect.accessors.ReadOnlyFieldAccessor;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.collection.ConvertedMap;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Iterators;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
/**
|
||||
* Wraps a DataWatcher that is used to transmit arbitrary key-value pairs with a given entity.
|
||||
@ -56,127 +61,127 @@ import com.google.common.collect.Iterators;
|
||||
* @author Kristian
|
||||
*/
|
||||
public class WrappedDataWatcher extends AbstractWrapper implements Iterable<WrappedWatchableObject> {
|
||||
// /**
|
||||
// * Every custom watchable type in Spigot #1628 and above.
|
||||
// * @author Kristian
|
||||
// */
|
||||
// public enum CustomType {
|
||||
// BYTE_SHORT("org.spigotmc.ProtocolData$ByteShort", 0, short.class),
|
||||
// DUAL_BYTE("org.spigotmc.ProtocolData$DualByte", 0, byte.class, byte.class),
|
||||
// HIDDEN_BYTE("org.spigotmc.ProtocolData$HiddenByte", 0, byte.class),
|
||||
// INT_BYTE("org.spigotmc.ProtocolData$IntByte", 2, int.class, byte.class),
|
||||
// DUAL_INT("org.spigotmc.ProtocolData$DualInt", 2, int.class, int.class);
|
||||
//
|
||||
// private Class<?> spigotClass;
|
||||
// private ConstructorAccessor constructor;
|
||||
// private FieldAccessor secondaryValue;
|
||||
// private int typeId;
|
||||
//
|
||||
// private CustomType(String className, int typeId, Class<?>... parameters) {
|
||||
// try {
|
||||
// this.spigotClass = Class.forName(className);
|
||||
// this.constructor = Accessors.getConstructorAccessor(spigotClass, parameters);
|
||||
// this.secondaryValue = parameters.length > 1 ? Accessors.getFieldAccessor(spigotClass, "value2", true) : null;
|
||||
//
|
||||
// } catch (ClassNotFoundException e) {
|
||||
// ProtocolLibrary.log(Level.WARNING, "Unable to find " + className);
|
||||
// this.spigotClass = null;
|
||||
// }
|
||||
// this.typeId = typeId;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Construct a new instance of this Spigot type.
|
||||
// * @param value - the value. Cannot be NULL.
|
||||
// * @return The instance to construct.
|
||||
// */
|
||||
// Object newInstance(Object value) {
|
||||
// return newInstance(value, null);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Construct a new instance of this Spigot type.
|
||||
// * <p>
|
||||
// * The secondary value may be NULL if this custom type does not contain a secondary value.
|
||||
// * @param value - the value.
|
||||
// * @param secondary - optional secondary value.
|
||||
// * @return
|
||||
// */
|
||||
// Object newInstance(Object value, Object secondary) {
|
||||
// Preconditions.checkNotNull(value, "value cannot be NULL.");
|
||||
//
|
||||
// if (hasSecondary()) {
|
||||
// return constructor.invoke(value, secondary);
|
||||
// } else {
|
||||
// if (secondary != null) {
|
||||
// throw new IllegalArgumentException("Cannot construct " + this + " with a secondary value");
|
||||
// }
|
||||
// return constructor.invoke(value);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Set the secondary value of a given type.
|
||||
// * @param instance - the instance.
|
||||
// * @param secondary - the secondary value.
|
||||
// */
|
||||
// void setSecondary(Object instance, Object secondary) {
|
||||
// if (!hasSecondary()) {
|
||||
// throw new IllegalArgumentException(this + " does not have a secondary value.");
|
||||
// }
|
||||
// secondaryValue.set(instance, secondary);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Get the secondary value of a type.
|
||||
// * @param instance - the instance.
|
||||
// * @return The secondary value.
|
||||
// */
|
||||
// Object getSecondary(Object instance) {
|
||||
// if (!hasSecondary()) {
|
||||
// throw new IllegalArgumentException(this + " does not have a secondary value.");
|
||||
// }
|
||||
// return secondaryValue.get(instance);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Determine if this type has a secondary value.
|
||||
// * @return TRUE if it does, FALSE otherwise.
|
||||
// */
|
||||
// public boolean hasSecondary() {
|
||||
// return secondaryValue != null;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Underlying Spigot class.
|
||||
// * @return The class.
|
||||
// */
|
||||
// public Class<?> getSpigotClass() {
|
||||
// return spigotClass;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * The equivalent type ID.
|
||||
// * @return The equivalent ID.
|
||||
// */
|
||||
// public int getTypeId() {
|
||||
// return typeId;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Retrieve the custom Spigot type of a value.
|
||||
// * @param value - the value.
|
||||
// * @return The Spigot type, or NULL if not found.
|
||||
// */
|
||||
// public static CustomType fromValue(Object value) {
|
||||
// for (CustomType type : CustomType.values()) {
|
||||
// if (type.getSpigotClass().isInstance(value)) {
|
||||
// return type;
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* Every custom watchable type in Spigot #1628 and above.
|
||||
* @author Kristian
|
||||
*/
|
||||
public enum CustomType {
|
||||
BYTE_SHORT("org.spigotmc.ProtocolData$ByteShort", 0, short.class),
|
||||
DUAL_BYTE("org.spigotmc.ProtocolData$DualByte", 0, byte.class, byte.class),
|
||||
HIDDEN_BYTE("org.spigotmc.ProtocolData$HiddenByte", 0, byte.class),
|
||||
INT_BYTE("org.spigotmc.ProtocolData$IntByte", 2, int.class, byte.class),
|
||||
DUAL_INT("org.spigotmc.ProtocolData$DualInt", 2, int.class, int.class);
|
||||
|
||||
private Class<?> spigotClass;
|
||||
private ConstructorAccessor constructor;
|
||||
private FieldAccessor secondaryValue;
|
||||
private int typeId;
|
||||
|
||||
private CustomType(String className, int typeId, Class<?>... parameters) {
|
||||
try {
|
||||
this.spigotClass = Class.forName(className);
|
||||
this.constructor = Accessors.getConstructorAccessor(spigotClass, parameters);
|
||||
this.secondaryValue = parameters.length > 1 ? Accessors.getFieldAccessor(spigotClass, "value2", true) : null;
|
||||
|
||||
} catch (ClassNotFoundException e) {
|
||||
ProtocolLibrary.log(Level.WARNING, "Unable to find " + className);
|
||||
this.spigotClass = null;
|
||||
}
|
||||
this.typeId = typeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new instance of this Spigot type.
|
||||
* @param value - the value. Cannot be NULL.
|
||||
* @return The instance to construct.
|
||||
*/
|
||||
Object newInstance(Object value) {
|
||||
return newInstance(value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new instance of this Spigot type.
|
||||
* <p>
|
||||
* The secondary value may be NULL if this custom type does not contain a secondary value.
|
||||
* @param value - the value.
|
||||
* @param secondary - optional secondary value.
|
||||
* @return
|
||||
*/
|
||||
Object newInstance(Object value, Object secondary) {
|
||||
Preconditions.checkNotNull(value, "value cannot be NULL.");
|
||||
|
||||
if (hasSecondary()) {
|
||||
return constructor.invoke(value, secondary);
|
||||
} else {
|
||||
if (secondary != null) {
|
||||
throw new IllegalArgumentException("Cannot construct " + this + " with a secondary value");
|
||||
}
|
||||
return constructor.invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the secondary value of a given type.
|
||||
* @param instance - the instance.
|
||||
* @param secondary - the secondary value.
|
||||
*/
|
||||
void setSecondary(Object instance, Object secondary) {
|
||||
if (!hasSecondary()) {
|
||||
throw new IllegalArgumentException(this + " does not have a secondary value.");
|
||||
}
|
||||
secondaryValue.set(instance, secondary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the secondary value of a type.
|
||||
* @param instance - the instance.
|
||||
* @return The secondary value.
|
||||
*/
|
||||
Object getSecondary(Object instance) {
|
||||
if (!hasSecondary()) {
|
||||
throw new IllegalArgumentException(this + " does not have a secondary value.");
|
||||
}
|
||||
return secondaryValue.get(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this type has a secondary value.
|
||||
* @return TRUE if it does, FALSE otherwise.
|
||||
*/
|
||||
public boolean hasSecondary() {
|
||||
return secondaryValue != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Underlying Spigot class.
|
||||
* @return The class.
|
||||
*/
|
||||
public Class<?> getSpigotClass() {
|
||||
return spigotClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* The equivalent type ID.
|
||||
* @return The equivalent ID.
|
||||
*/
|
||||
public int getTypeId() {
|
||||
return typeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the custom Spigot type of a value.
|
||||
* @param value - the value.
|
||||
* @return The Spigot type, or NULL if not found.
|
||||
*/
|
||||
public static CustomType fromValue(Object value) {
|
||||
for (CustomType type : CustomType.values()) {
|
||||
if (type.getSpigotClass().isInstance(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to assign integer IDs to given types.
|
||||
@ -601,20 +606,20 @@ public class WrappedDataWatcher extends AbstractWrapper implements Iterable<Wrap
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Set a watched byte with an optional secondary value.
|
||||
// * @param index - index of the watched byte.
|
||||
// * @param newValue - the new watched value.
|
||||
// * @param secondary - optional secondary value.
|
||||
// * @param update - whether or not to refresh every listening client.
|
||||
// * @throws FieldAccessException Cannot read underlying field.
|
||||
// */
|
||||
// public void setObject(int index, Object newValue, Object secondary, boolean update, CustomType type) throws FieldAccessException {
|
||||
// Object created = type.newInstance(newValue, secondary);
|
||||
//
|
||||
// // Now update the watcher
|
||||
// setObject(index, created, update);
|
||||
// }
|
||||
/**
|
||||
* Set a watched byte with an optional secondary value.
|
||||
* @param index - index of the watched byte.
|
||||
* @param newValue - the new watched value.
|
||||
* @param secondary - optional secondary value.
|
||||
* @param update - whether or not to refresh every listening client.
|
||||
* @throws FieldAccessException Cannot read underlying field.
|
||||
*/
|
||||
public void setObject(int index, Object newValue, Object secondary, boolean update, CustomType type) throws FieldAccessException {
|
||||
Object created = type.newInstance(newValue, secondary);
|
||||
|
||||
// Now update the watcher
|
||||
setObject(index, created, update);
|
||||
}
|
||||
|
||||
private Object getWatchedObject(int index) throws FieldAccessException {
|
||||
// We use the get-method first and foremost
|
||||
@ -718,7 +723,7 @@ public class WrappedDataWatcher extends AbstractWrapper implements Iterable<Wrap
|
||||
initializeSpigot(fuzzy);
|
||||
|
||||
// Any custom types
|
||||
// CUSTOM_MAP = initializeCustom();
|
||||
CUSTOM_MAP = initializeCustom();
|
||||
|
||||
// Initialize static type type
|
||||
TYPE_MAP = (Map<Class<?>, Integer>) TYPE_MAP_ACCESSOR.get(null);
|
||||
@ -737,17 +742,17 @@ public class WrappedDataWatcher extends AbstractWrapper implements Iterable<Wrap
|
||||
initializeMethods(fuzzy);
|
||||
}
|
||||
|
||||
// // For Spigot's bountiful update patch
|
||||
// private static Map<Class<?>, Integer> initializeCustom() {
|
||||
// Map<Class<?>, Integer> map = Maps.newHashMap();
|
||||
//
|
||||
// for (CustomType type : CustomType.values()) {
|
||||
// if (type.getSpigotClass() != null) {
|
||||
// map.put(type.getSpigotClass(), type.getTypeId());
|
||||
// }
|
||||
// }
|
||||
// return map;
|
||||
// }
|
||||
// For Spigot's bountiful update patch
|
||||
private static Map<Class<?>, Integer> initializeCustom() {
|
||||
Map<Class<?>, Integer> map = Maps.newHashMap();
|
||||
|
||||
for (CustomType type : CustomType.values()) {
|
||||
if (type.getSpigotClass() != null) {
|
||||
map.put(type.getSpigotClass(), type.getTypeId());
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// TODO: Remove, as this was fixed in build #1189 of Spigot
|
||||
private static void initializeSpigot(FuzzyReflection fuzzy) {
|
||||
|
@ -14,13 +14,12 @@ import com.comphenix.protocol.injector.BukkitUnwrapper;
|
||||
import com.comphenix.protocol.reflect.accessors.Accessors;
|
||||
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
|
||||
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
|
||||
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.collection.ConvertedMultimap;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
|
||||
/**
|
||||
* Represents a wrapper for a game profile.
|
||||
@ -29,9 +28,24 @@ import com.mojang.authlib.properties.Property;
|
||||
public class WrappedGameProfile extends AbstractWrapper {
|
||||
public static final ReportType REPORT_INVALID_UUID = new ReportType("Plugin %s created a profile with '%s' as an UUID.");
|
||||
|
||||
// Version 1.7.2 and 1.7.8 respectively
|
||||
private static final ConstructorAccessor CREATE_STRING_STRING = Accessors.getConstructorAccessorOrNull(GameProfile.class, String.class, String.class);
|
||||
private static final FieldAccessor GET_UUID_STRING = Accessors.getFieldAcccessorOrNull(GameProfile.class, "id", String.class);
|
||||
private static final Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
|
||||
|
||||
private static final ConstructorAccessor CREATE_STRING_STRING = Accessors.getConstructorAccessorOrNull(
|
||||
GAME_PROFILE, String.class, String.class);
|
||||
private static final ConstructorAccessor CREATE_UUID_STRING = Accessors.getConstructorAccessorOrNull(
|
||||
GAME_PROFILE, UUID.class, String.class);
|
||||
|
||||
private static final FieldAccessor GET_UUID_STRING = Accessors.getFieldAcccessorOrNull(
|
||||
GAME_PROFILE, "id", String.class);
|
||||
|
||||
private static final MethodAccessor GET_ID = Accessors.getMethodAcccessorOrNull(
|
||||
GAME_PROFILE, "getId");
|
||||
private static final MethodAccessor GET_NAME = Accessors.getMethodAcccessorOrNull(
|
||||
GAME_PROFILE, "getName");
|
||||
private static final MethodAccessor GET_PROPERTIES = Accessors.getMethodAcccessorOrNull(
|
||||
GAME_PROFILE, "getProperties");
|
||||
private static final MethodAccessor IS_COMPLETE = Accessors.getMethodAcccessorOrNull(
|
||||
GAME_PROFILE, "isComplete");
|
||||
|
||||
// Fetching game profile
|
||||
private static FieldAccessor PLAYER_PROFILE;
|
||||
@ -45,7 +59,7 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
|
||||
// Profile from a handle
|
||||
private WrappedGameProfile(Object profile) {
|
||||
super(GameProfile.class);
|
||||
super(GAME_PROFILE);
|
||||
setHandle(profile);
|
||||
}
|
||||
|
||||
@ -53,17 +67,18 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
* Retrieve the associated game profile of a player.
|
||||
* <p>
|
||||
* Note that this may not exist in the current Minecraft version.
|
||||
*
|
||||
* @param player - the player.
|
||||
* @return The game profile.
|
||||
*/
|
||||
public static WrappedGameProfile fromPlayer(Player player) {
|
||||
FieldAccessor accessor = PLAYER_PROFILE;
|
||||
Object nmsPlayer = BukkitUnwrapper.getInstance().unwrapItem(player);
|
||||
|
||||
if (accessor == null) {
|
||||
accessor = Accessors.getFieldAccessor(MinecraftReflection.getEntityHumanClass(), GameProfile.class, true);
|
||||
accessor = Accessors.getFieldAccessor(MinecraftReflection.getEntityHumanClass(), GAME_PROFILE, true);
|
||||
PLAYER_PROFILE = accessor;
|
||||
}
|
||||
|
||||
Object nmsPlayer = BukkitUnwrapper.getInstance().unwrapItem(player);
|
||||
return WrappedGameProfile.fromHandle(PLAYER_PROFILE.get(nmsPlayer));
|
||||
}
|
||||
|
||||
@ -71,16 +86,17 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
* Retrieve the associated game profile of an offline player.
|
||||
* <p>
|
||||
* Note that this may not exist in the current Minecraft version.
|
||||
*
|
||||
* @param player - the offline player.
|
||||
* @return The game profile.
|
||||
*/
|
||||
public static WrappedGameProfile fromOfflinePlayer(OfflinePlayer player) {
|
||||
FieldAccessor accessor = OFFLINE_PROFILE;
|
||||
|
||||
if (accessor == null) {
|
||||
accessor = Accessors.getFieldAccessor(player.getClass(), GameProfile.class, true);
|
||||
accessor = Accessors.getFieldAccessor(player.getClass(), GAME_PROFILE, true);
|
||||
OFFLINE_PROFILE = accessor;
|
||||
}
|
||||
|
||||
return WrappedGameProfile.fromHandle(OFFLINE_PROFILE.get(player));
|
||||
}
|
||||
|
||||
@ -91,17 +107,20 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
* IDs that cannot be parsed as an UUID will be hashed and form a version 3 UUID instead.
|
||||
* <p>
|
||||
* This method is deprecated for Minecraft 1.7.8 and above.
|
||||
*
|
||||
* @param id - the UUID of the player.
|
||||
* @param name - the name of the player.
|
||||
*/
|
||||
@Deprecated
|
||||
public WrappedGameProfile(String id, String name) {
|
||||
super(GameProfile.class);
|
||||
super(GAME_PROFILE);
|
||||
|
||||
if (CREATE_STRING_STRING != null) {
|
||||
setHandle(CREATE_STRING_STRING.invoke(id, name));
|
||||
} else if (CREATE_UUID_STRING != null) {
|
||||
setHandle(CREATE_UUID_STRING.invoke(parseUUID(id), name));
|
||||
} else {
|
||||
setHandle(new GameProfile(parseUUID(id), name));
|
||||
throw new IllegalArgumentException("Unsupported GameProfile constructor.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,31 +128,37 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
* Construct a new game profile with the given properties.
|
||||
* <p>
|
||||
* Note that at least one of the parameters must be non-null.
|
||||
*
|
||||
* @param uuid - the UUID of the player, or NULL.
|
||||
* @param name - the name of the player, or NULL.
|
||||
*/
|
||||
public WrappedGameProfile(UUID uuid, String name) {
|
||||
super(GameProfile.class);
|
||||
super(GAME_PROFILE);
|
||||
|
||||
if (CREATE_STRING_STRING != null) {
|
||||
setHandle(CREATE_STRING_STRING.invoke(uuid != null ? uuid.toString() : null, name));
|
||||
} else if (CREATE_UUID_STRING != null) {
|
||||
setHandle(CREATE_UUID_STRING.invoke(uuid, name));
|
||||
} else {
|
||||
setHandle(new GameProfile(uuid, name));
|
||||
throw new IllegalArgumentException("Unsupported GameProfile constructor.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a wrapper around an existing game profile.
|
||||
*
|
||||
* @param profile - the underlying profile, or NULL.
|
||||
*/
|
||||
public static WrappedGameProfile fromHandle(Object handle) {
|
||||
if (handle == null)
|
||||
return null;
|
||||
|
||||
return new WrappedGameProfile(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an UUID using very lax rules, as specified in {@link #WrappedGameProfile(UUID, String)}.
|
||||
*
|
||||
* @param id - text.
|
||||
* @return The corresponding UUID.
|
||||
* @throws IllegalArgumentException If we cannot parse the text.
|
||||
@ -143,13 +168,10 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
return id != null ? UUID.fromString(id) : null;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Warn once every hour (per plugin)
|
||||
ProtocolLibrary.getErrorReporter().reportWarning(
|
||||
WrappedGameProfile.class,
|
||||
Report.newBuilder(REPORT_INVALID_UUID).
|
||||
rateLimit(1, TimeUnit.HOURS).
|
||||
messageParam(PluginContext.getPluginCaller(new Exception()), id)
|
||||
);
|
||||
|
||||
ProtocolLibrary.getErrorReporter()
|
||||
.reportWarning(WrappedGameProfile.class, Report.newBuilder(REPORT_INVALID_UUID)
|
||||
.rateLimit(1, TimeUnit.HOURS)
|
||||
.messageParam(PluginContext.getPluginCaller(new Exception()), id));
|
||||
return UUID.nameUUIDFromBytes(id.getBytes(Charsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@ -157,10 +179,10 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
/**
|
||||
* Retrieve the UUID of the player.
|
||||
* <p>
|
||||
* Note that Minecraft 1.7.5 and earlier doesn't use UUIDs internally, and it may not be possible
|
||||
* to convert the string to an UUID.
|
||||
* Note that Minecraft 1.7.5 and earlier doesn't use UUIDs internally, and it may not be possible to convert the string to an UUID.
|
||||
* <p>
|
||||
* We use the same lax conversion as in {@link #WrappedGameProfile(String, String)}.
|
||||
*
|
||||
* @return The UUID, or NULL if the UUID is NULL.
|
||||
* @throws IllegalStateException If we cannot parse the internal ID as an UUID.
|
||||
*/
|
||||
@ -171,15 +193,19 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
try {
|
||||
if (GET_UUID_STRING != null) {
|
||||
uuid = parseUUID(getId());
|
||||
} else if (GET_ID != null) {
|
||||
uuid = (UUID) GET_ID.invoke(handle);
|
||||
} else {
|
||||
uuid = getProfile().getId();
|
||||
throw new IllegalStateException("Unsupported getId() method");
|
||||
}
|
||||
|
||||
// Cache for later
|
||||
parsedUUID = uuid;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalStateException("Cannot parse ID " + getId() + " as an UUID in player profile " + getName());
|
||||
throw new IllegalStateException("Cannot parse ID " + getId() + " as an UUID in player profile " + getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@ -189,36 +215,48 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
* Note that there's nothing stopping plugins from creating non-standard UUIDs.
|
||||
* <p>
|
||||
* In Minecraft 1.7.8 and later, this simply returns the string form of {@link #getUUID()}.
|
||||
*
|
||||
* @return The UUID of the player, or NULL if not computed.
|
||||
*/
|
||||
public String getId() {
|
||||
if (GET_UUID_STRING != null)
|
||||
if (GET_UUID_STRING != null) {
|
||||
return (String) GET_UUID_STRING.get(handle);
|
||||
final GameProfile profile = getProfile();
|
||||
return profile.getId() != null ? profile.getId().toString() : null;
|
||||
} else if (GET_ID != null) {
|
||||
UUID uuid = (UUID) GET_ID.invoke(handle);
|
||||
return uuid != null ? uuid.toString() : null;
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported getId() method");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the name of the player.
|
||||
*
|
||||
* @return The player name.
|
||||
*/
|
||||
public String getName() {
|
||||
return getProfile().getName();
|
||||
if (GET_NAME != null) {
|
||||
return (String) GET_NAME.invoke(handle);
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported getName() method");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the property map of signed values.
|
||||
*
|
||||
* @return Property map.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public Multimap<String, WrappedSignedProperty> getProperties() {
|
||||
Multimap<String, WrappedSignedProperty> result = propertyMap;
|
||||
|
||||
if (result == null) {
|
||||
result = new ConvertedMultimap<String, Property, WrappedSignedProperty>(
|
||||
GuavaWrappers.getBukkitMultimap(getProfile().getProperties())) {
|
||||
Multimap properties = (Multimap) GET_PROPERTIES.invoke(handle);
|
||||
result = new ConvertedMultimap<String, Object, WrappedSignedProperty>(GuavaWrappers.getBukkitMultimap(properties)) {
|
||||
@Override
|
||||
protected Property toInner(WrappedSignedProperty outer) {
|
||||
return (Property) outer.handle;
|
||||
protected Object toInner(WrappedSignedProperty outer) {
|
||||
return outer.handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -230,7 +268,7 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WrappedSignedProperty toOuter(Property inner) {
|
||||
protected WrappedSignedProperty toOuter(Object inner) {
|
||||
return WrappedSignedProperty.fromHandle(inner);
|
||||
}
|
||||
};
|
||||
@ -239,16 +277,9 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying GameProfile.
|
||||
* @return The GameProfile.
|
||||
*/
|
||||
private GameProfile getProfile() {
|
||||
return (GameProfile) handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new game profile with the same ID, but different name.
|
||||
*
|
||||
* @param name - the new name of the profile to create.
|
||||
* @return The new game profile.
|
||||
*/
|
||||
@ -258,6 +289,7 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
|
||||
/**
|
||||
* Construct a new game profile with the same name, but different id.
|
||||
*
|
||||
* @param id - the new id of the profile to create.
|
||||
* @return The new game profile.
|
||||
*/
|
||||
@ -267,15 +299,16 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
|
||||
/**
|
||||
* Determine if the game profile contains both an UUID and a name.
|
||||
*
|
||||
* @return TRUE if it does, FALSE otherwise.
|
||||
*/
|
||||
public boolean isComplete() {
|
||||
return getProfile().isComplete();
|
||||
return (Boolean) IS_COMPLETE.invoke(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.valueOf(getProfile());
|
||||
return String.valueOf(getHandle());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -291,8 +324,9 @@ public class WrappedGameProfile extends AbstractWrapper {
|
||||
|
||||
if (obj instanceof WrappedGameProfile) {
|
||||
WrappedGameProfile other = (WrappedGameProfile) obj;
|
||||
return Objects.equal(getProfile(), other.getProfile());
|
||||
return Objects.equal(getHandle(), other.getHandle());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
In neuem Issue referenzieren
Einen Benutzer sperren