Update to 1.13.1, rework cloning, fix a particle NPE
Dieser Commit ist enthalten in:
Ursprung
8ec31d83db
Commit
776ec56a2d
@ -21,20 +21,14 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import com.comphenix.protocol.reflect.EquivalentConverter;
|
import com.comphenix.protocol.reflect.EquivalentConverter;
|
||||||
import com.comphenix.protocol.reflect.StructureModifier;
|
import com.comphenix.protocol.reflect.StructureModifier;
|
||||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
import com.comphenix.protocol.wrappers.*;
|
||||||
import com.comphenix.protocol.wrappers.BlockPosition;
|
|
||||||
import com.comphenix.protocol.wrappers.BukkitConverters;
|
|
||||||
import com.comphenix.protocol.wrappers.ChunkPosition;
|
|
||||||
import com.comphenix.protocol.wrappers.MinecraftKey;
|
|
||||||
import com.comphenix.protocol.wrappers.WrappedBlockData;
|
|
||||||
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
|
|
||||||
import com.comphenix.protocol.wrappers.WrappedServerPing;
|
|
||||||
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
||||||
import com.comphenix.protocol.wrappers.nbt.NbtWrapper;
|
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,62 +37,56 @@ import com.google.common.collect.Maps;
|
|||||||
* @author Kristian
|
* @author Kristian
|
||||||
*/
|
*/
|
||||||
public class BukkitCloner implements Cloner {
|
public class BukkitCloner implements Cloner {
|
||||||
// List of classes we support
|
private static final Map<Class<?>, Function<Object, Object>> CLONERS = Maps.newConcurrentMap();
|
||||||
private final Map<Integer, Class<?>> clonableClasses = Maps.newConcurrentMap();
|
|
||||||
|
|
||||||
public BukkitCloner() {
|
private static void fromWrapper(Supplier<Class<?>> getClass, Function<Object, ClonableWrapper> fromHandle) {
|
||||||
addClass(0, MinecraftReflection.getItemStackClass());
|
|
||||||
addClass(1, MinecraftReflection.getDataWatcherClass());
|
|
||||||
|
|
||||||
// Try to add position classes
|
|
||||||
try {
|
try {
|
||||||
addClass(2, MinecraftReflection.getBlockPositionClass());
|
Class<?> nmsClass = getClass.get();
|
||||||
} catch (Throwable ex) {
|
if (nmsClass != null) {
|
||||||
|
CLONERS.put(nmsClass, nmsObject -> fromHandle.apply(nmsObject).deepClone().getHandle());
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fromConverter(Supplier<Class<?>> getClass, EquivalentConverter converter) {
|
||||||
try {
|
try {
|
||||||
addClass(3, MinecraftReflection.getChunkPositionClass());
|
Class<?> nmsClass = getClass.get();
|
||||||
} catch (Throwable ex) {
|
if (nmsClass != null) {
|
||||||
}
|
CLONERS.put(nmsClass, nmsObject -> converter.getGeneric(converter.getSpecific(nmsObject)));
|
||||||
|
}
|
||||||
if (MinecraftReflection.isUsingNetty()) {
|
} catch (RuntimeException ignored) { }
|
||||||
addClass(4, MinecraftReflection.getServerPingClass());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MinecraftReflection.watcherObjectExists()) {
|
|
||||||
addClass(5, MinecraftReflection.getDataWatcherSerializerClass());
|
|
||||||
addClass(6, MinecraftReflection.getMinecraftKeyClass());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fromManual(Supplier<Class<?>> getClass, Function<Object, Object> cloner) {
|
||||||
try {
|
try {
|
||||||
addClass(7, MinecraftReflection.getIBlockDataClass());
|
Class<?> nmsClass = getClass.get();
|
||||||
} catch (Throwable ex) {
|
if (nmsClass != null) {
|
||||||
|
CLONERS.put(nmsClass, cloner);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
static {
|
||||||
addClass(8, MinecraftReflection.getNonNullListClass());
|
fromManual(MinecraftReflection::getItemStackClass, source ->
|
||||||
} catch (Throwable ex) {
|
MinecraftReflection.getMinecraftItemStack(MinecraftReflection.getBukkitItemStack(source).clone()));
|
||||||
|
fromWrapper(MinecraftReflection::getDataWatcherClass, WrappedDataWatcher::new);
|
||||||
|
fromConverter(MinecraftReflection::getBlockPositionClass, BlockPosition.getConverter());
|
||||||
|
fromConverter(MinecraftReflection::getChunkPositionClass, ChunkPosition.getConverter());
|
||||||
|
fromWrapper(MinecraftReflection::getServerPingClass, WrappedServerPing::fromHandle);
|
||||||
|
fromConverter(MinecraftReflection::getMinecraftKeyClass, MinecraftKey.getConverter());
|
||||||
|
fromWrapper(MinecraftReflection::getIBlockDataClass, WrappedBlockData::fromHandle);
|
||||||
|
fromManual(MinecraftReflection::getNonNullListClass, source -> nonNullListCloner().clone(source));
|
||||||
|
fromWrapper(MinecraftReflection::getNBTBaseClass, NbtFactory::fromNMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
private Function<Object, Object> findCloner(Class<?> type) {
|
||||||
addClass(9, MinecraftReflection.getNBTBaseClass());
|
for (Entry<Class<?>, Function<Object, Object>> entry : CLONERS.entrySet()) {
|
||||||
} catch (Throwable ex) { }
|
if (entry.getKey().isAssignableFrom(type)) {
|
||||||
}
|
return entry.getValue();
|
||||||
|
|
||||||
private void addClass(int id, Class<?> clazz) {
|
|
||||||
if (clazz != null)
|
|
||||||
clonableClasses.put(id, clazz);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int findMatchingClass(Class<?> type) {
|
|
||||||
// See if is a subclass of any of our supported superclasses
|
|
||||||
for (Entry<Integer, Class<?>> entry : clonableClasses.entrySet()) {
|
|
||||||
if (entry.getValue().isAssignableFrom(type)) {
|
|
||||||
return entry.getKey();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -106,7 +94,7 @@ public class BukkitCloner implements Cloner {
|
|||||||
if (source == null)
|
if (source == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return findMatchingClass(source.getClass()) >= 0;
|
return findCloner(source.getClass()) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -114,43 +102,12 @@ public class BukkitCloner implements Cloner {
|
|||||||
if (source == null)
|
if (source == null)
|
||||||
throw new IllegalArgumentException("source cannot be NULL.");
|
throw new IllegalArgumentException("source cannot be NULL.");
|
||||||
|
|
||||||
// Convert to a wrapper
|
return findCloner(source.getClass()).apply(source);
|
||||||
switch (findMatchingClass(source.getClass())) {
|
|
||||||
case 0:
|
|
||||||
return MinecraftReflection.getMinecraftItemStack(MinecraftReflection.getBukkitItemStack(source).clone());
|
|
||||||
case 1:
|
|
||||||
EquivalentConverter<WrappedDataWatcher> dataConverter = BukkitConverters.getDataWatcherConverter();
|
|
||||||
return dataConverter.getGeneric(dataConverter.getSpecific(source).deepClone());
|
|
||||||
case 2:
|
|
||||||
EquivalentConverter<BlockPosition> blockConverter = BlockPosition.getConverter();
|
|
||||||
return blockConverter.getGeneric(blockConverter.getSpecific(source));
|
|
||||||
case 3:
|
|
||||||
EquivalentConverter<ChunkPosition> chunkConverter = ChunkPosition.getConverter();
|
|
||||||
return chunkConverter.getGeneric(chunkConverter.getSpecific(source));
|
|
||||||
case 4:
|
|
||||||
EquivalentConverter<WrappedServerPing> serverConverter = BukkitConverters.getWrappedServerPingConverter();
|
|
||||||
return serverConverter.getGeneric(serverConverter.getSpecific(source).deepClone());
|
|
||||||
case 5:
|
|
||||||
return source;
|
|
||||||
case 6:
|
|
||||||
EquivalentConverter<MinecraftKey> keyConverter = MinecraftKey.getConverter();
|
|
||||||
return keyConverter.getGeneric(keyConverter.getSpecific(source));
|
|
||||||
case 7:
|
|
||||||
EquivalentConverter<WrappedBlockData> blockDataConverter = BukkitConverters.getWrappedBlockDataConverter();
|
|
||||||
return blockDataConverter.getGeneric(blockDataConverter.getSpecific(source).deepClone());
|
|
||||||
case 8:
|
|
||||||
return nonNullListCloner().clone(source);
|
|
||||||
case 9:
|
|
||||||
NbtWrapper<?> clone = (NbtWrapper<?>) NbtFactory.fromNMS(source).deepClone();
|
|
||||||
return clone.getHandle();
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Cannot clone objects of type " + source.getClass());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Constructor<?> nonNullList = null;
|
private static Constructor<?> nonNullList = null;
|
||||||
|
|
||||||
private static final Cloner nonNullListCloner() {
|
private static Cloner nonNullListCloner() {
|
||||||
return new Cloner() {
|
return new Cloner() {
|
||||||
@Override
|
@Override
|
||||||
public boolean canClone(Object source) {
|
public boolean canClone(Object source) {
|
||||||
|
@ -26,11 +26,15 @@ import java.net.URI;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import com.google.common.primitives.Primitives;
|
import com.google.common.primitives.Primitives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,19 +46,41 @@ import com.google.common.primitives.Primitives;
|
|||||||
*/
|
*/
|
||||||
public class ImmutableDetector implements Cloner {
|
public class ImmutableDetector implements Cloner {
|
||||||
// Notable immutable classes we might encounter
|
// Notable immutable classes we might encounter
|
||||||
private static final Class<?>[] immutableClasses = {
|
private static final Set<Class<?>> immutableClasses = ImmutableSet.of(
|
||||||
StackTraceElement.class, BigDecimal.class,
|
StackTraceElement.class, BigDecimal.class,
|
||||||
BigInteger.class, Locale.class, UUID.class,
|
BigInteger.class, Locale.class, UUID.class,
|
||||||
URL.class, URI.class, Inet4Address.class,
|
URL.class, URI.class, Inet4Address.class,
|
||||||
Inet6Address.class, InetSocketAddress.class,
|
Inet6Address.class, InetSocketAddress.class,
|
||||||
SecretKey.class, PublicKey.class
|
SecretKey.class, PublicKey.class
|
||||||
};
|
);
|
||||||
|
|
||||||
|
private static final Set<Class<?>> immutableNMS = Sets.newConcurrentHashSet();
|
||||||
|
|
||||||
|
static {
|
||||||
|
add(MinecraftReflection::getGameProfileClass);
|
||||||
|
add(MinecraftReflection::getDataWatcherSerializerClass);
|
||||||
|
add(() -> MinecraftReflection.getMinecraftClass("SoundEffect"));
|
||||||
|
add(MinecraftReflection::getBlockClass);
|
||||||
|
add(MinecraftReflection::getItemClass);
|
||||||
|
add(MinecraftReflection::getFluidTypeClass);
|
||||||
|
add(MinecraftReflection::getParticleTypeClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void add(Supplier<Class<?>> getClass) {
|
||||||
|
try {
|
||||||
|
Class<?> clazz = getClass.get();
|
||||||
|
if (clazz != null) {
|
||||||
|
immutableNMS.add(clazz);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) { }
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canClone(Object source) {
|
public boolean canClone(Object source) {
|
||||||
// Don't accept NULL
|
// Don't accept NULL
|
||||||
if (source == null)
|
if (source == null) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return isImmutable(source.getClass());
|
return isImmutable(source.getClass());
|
||||||
}
|
}
|
||||||
@ -66,46 +92,35 @@ public class ImmutableDetector implements Cloner {
|
|||||||
*/
|
*/
|
||||||
public static boolean isImmutable(Class<?> type) {
|
public static boolean isImmutable(Class<?> type) {
|
||||||
// Cases that are definitely not true
|
// Cases that are definitely not true
|
||||||
if (type.isArray())
|
if (type.isArray()) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// All primitive types
|
// All primitive types
|
||||||
if (Primitives.isWrapperType(type) || String.class.equals(type))
|
if (Primitives.isWrapperType(type) || String.class.equals(type)) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// May not be true, but if so, that kind of code is broken anyways
|
// May not be true, but if so, that kind of code is broken anyways
|
||||||
if (isEnumWorkaround(type))
|
if (isEnumWorkaround(type)) {
|
||||||
return true;
|
|
||||||
|
|
||||||
for (Class<?> clazz : immutableClasses)
|
|
||||||
if (clazz.equals(type))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Check for known immutable classes in 1.7.2
|
|
||||||
if (MinecraftReflection.isUsingNetty()) {
|
|
||||||
if (type.equals(MinecraftReflection.getGameProfileClass())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for known immutable classes in 1.9
|
|
||||||
if (MinecraftReflection.watcherObjectExists()) {
|
|
||||||
if (type.equals(MinecraftReflection.getDataWatcherSerializerClass())
|
|
||||||
|| type.equals(MinecraftReflection.getMinecraftClass("SoundEffect"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MinecraftReflection.is(MinecraftReflection.getBlockClass(), type)
|
|
||||||
|| MinecraftReflection.is(MinecraftReflection.getItemClass(), type)
|
|
||||||
|| MinecraftReflection.is(MinecraftReflection.getFluidTypeClass(), type)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No good way to clone lambdas
|
||||||
if (type.getName().contains("$$Lambda$")) {
|
if (type.getName().contains("$$Lambda$")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (immutableClasses.contains(type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Class<?> clazz : immutableNMS) {
|
||||||
|
if (MinecraftReflection.is(clazz, type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Probably not
|
// Probably not
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -113,10 +128,13 @@ public class ImmutableDetector implements Cloner {
|
|||||||
// This is just great. Just great.
|
// This is just great. Just great.
|
||||||
private static boolean isEnumWorkaround(Class<?> enumClass) {
|
private static boolean isEnumWorkaround(Class<?> enumClass) {
|
||||||
while (enumClass != null) {
|
while (enumClass != null) {
|
||||||
if (enumClass.isEnum())
|
if (enumClass.isEnum()) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
enumClass = enumClass.getSuperclass();
|
enumClass = enumClass.getSuperclass();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ public class MinecraftProtocolVersion {
|
|||||||
map.put(new MinecraftVersion(1, 12, 1), 338);
|
map.put(new MinecraftVersion(1, 12, 1), 338);
|
||||||
map.put(new MinecraftVersion(1, 12, 2), 340);
|
map.put(new MinecraftVersion(1, 12, 2), 340);
|
||||||
map.put(new MinecraftVersion(1, 13, 0), 393);
|
map.put(new MinecraftVersion(1, 13, 0), 393);
|
||||||
|
map.put(new MinecraftVersion(1, 13, 1), 401);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1156,6 +1156,10 @@ public class MinecraftReflection {
|
|||||||
return getNullableNMS("FluidType");
|
return getNullableNMS("FluidType");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Class<?> getParticleTypeClass() {
|
||||||
|
return getNullableNMS("ParticleType");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the WorldType class.
|
* Retrieve the WorldType class.
|
||||||
* @return The WorldType class.
|
* @return The WorldType class.
|
||||||
@ -1360,7 +1364,7 @@ public class MinecraftReflection {
|
|||||||
|
|
||||||
public static Class<?> getDataWatcherSerializerClass() {
|
public static Class<?> getDataWatcherSerializerClass() {
|
||||||
// TODO Implement a fallback
|
// TODO Implement a fallback
|
||||||
return getMinecraftClass("DataWatcherSerializer");
|
return getNullableNMS("DataWatcherSerializer");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Class<?> getDataWatcherRegistryClass() {
|
public static Class<?> getDataWatcherRegistryClass() {
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.comphenix.protocol.wrappers;
|
||||||
|
|
||||||
|
public interface ClonableWrapper {
|
||||||
|
Object getHandle();
|
||||||
|
ClonableWrapper deepClone();
|
||||||
|
|
||||||
|
}
|
@ -33,7 +33,7 @@ import org.bukkit.Material;
|
|||||||
* @author dmulloy2
|
* @author dmulloy2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public abstract class WrappedBlockData extends AbstractWrapper {
|
public abstract class WrappedBlockData extends AbstractWrapper implements ClonableWrapper {
|
||||||
private static final Class<?> MAGIC_NUMBERS = MinecraftReflection.getCraftBukkitClass("util.CraftMagicNumbers");
|
private static final Class<?> MAGIC_NUMBERS = MinecraftReflection.getCraftBukkitClass("util.CraftMagicNumbers");
|
||||||
private static final Class<?> IBLOCK_DATA = MinecraftReflection.getIBlockDataClass();
|
private static final Class<?> IBLOCK_DATA = MinecraftReflection.getIBlockDataClass();
|
||||||
private static final Class<?> BLOCK = MinecraftReflection.getBlockClass();
|
private static final Class<?> BLOCK = MinecraftReflection.getBlockClass();
|
||||||
|
@ -42,7 +42,7 @@ import org.bukkit.inventory.ItemStack;
|
|||||||
* Represents a DataWatcher
|
* Represents a DataWatcher
|
||||||
* @author dmulloy2
|
* @author dmulloy2
|
||||||
*/
|
*/
|
||||||
public class WrappedDataWatcher extends AbstractWrapper implements Iterable<WrappedWatchableObject> {
|
public class WrappedDataWatcher extends AbstractWrapper implements Iterable<WrappedWatchableObject>, ClonableWrapper {
|
||||||
private static final Class<?> HANDLE_TYPE = MinecraftReflection.getDataWatcherClass();
|
private static final Class<?> HANDLE_TYPE = MinecraftReflection.getDataWatcherClass();
|
||||||
|
|
||||||
private static MethodAccessor GETTER = null;
|
private static MethodAccessor GETTER = null;
|
||||||
|
@ -91,6 +91,8 @@ public class WrappedParticle<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static WrappedParticle fromHandle(Object handle) {
|
public static WrappedParticle fromHandle(Object handle) {
|
||||||
|
ensureMethods();
|
||||||
|
|
||||||
Particle bukkit = (Particle) toBukkit.invoke(null, handle);
|
Particle bukkit = (Particle) toBukkit.invoke(null, handle);
|
||||||
Object data = null;
|
Object data = null;
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ import io.netty.handler.codec.base64.Base64;
|
|||||||
* Represents a server ping packet data.
|
* Represents a server ping packet data.
|
||||||
* @author Kristian
|
* @author Kristian
|
||||||
*/
|
*/
|
||||||
public class WrappedServerPing extends AbstractWrapper {
|
public class WrappedServerPing extends AbstractWrapper implements ClonableWrapper {
|
||||||
private static Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
|
private static Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
|
||||||
private static Class<?> GAME_PROFILE_ARRAY = MinecraftReflection.getArrayClass(GAME_PROFILE);
|
private static Class<?> GAME_PROFILE_ARRAY = MinecraftReflection.getArrayClass(GAME_PROFILE);
|
||||||
|
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
package com.comphenix.protocol.wrappers.nbt;
|
package com.comphenix.protocol.wrappers.nbt;
|
||||||
|
|
||||||
|
|
||||||
|
import com.comphenix.protocol.wrappers.ClonableWrapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a generic container for an NBT element.
|
* Represents a generic container for an NBT element.
|
||||||
* <p>
|
* <p>
|
||||||
@ -26,7 +28,7 @@ package com.comphenix.protocol.wrappers.nbt;
|
|||||||
* @author Kristian
|
* @author Kristian
|
||||||
* @param <TType> - type of the value that is stored.
|
* @param <TType> - type of the value that is stored.
|
||||||
*/
|
*/
|
||||||
public interface NbtBase<TType> {
|
public interface NbtBase<TType> extends ClonableWrapper {
|
||||||
/**
|
/**
|
||||||
* Accepts a NBT visitor.
|
* Accepts a NBT visitor.
|
||||||
* @param visitor - the hierarchical NBT visitor.
|
* @param visitor - the hierarchical NBT visitor.
|
||||||
@ -84,4 +86,8 @@ public interface NbtBase<TType> {
|
|||||||
* @return The cloned tag.
|
* @return The cloned tag.
|
||||||
*/
|
*/
|
||||||
public abstract NbtBase<TType> deepClone();
|
public abstract NbtBase<TType> deepClone();
|
||||||
|
|
||||||
|
default Object getHandle() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@ package com.comphenix.protocol.wrappers.nbt;
|
|||||||
|
|
||||||
import java.io.DataOutput;
|
import java.io.DataOutput;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.wrappers.ClonableWrapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that this NBT wraps an underlying net.minecraft.server instance.
|
* Indicates that this NBT wraps an underlying net.minecraft.server instance.
|
||||||
* <p>
|
* <p>
|
||||||
@ -28,7 +30,7 @@ import java.io.DataOutput;
|
|||||||
*
|
*
|
||||||
* @param <TType> - type of the value that is stored.
|
* @param <TType> - type of the value that is stored.
|
||||||
*/
|
*/
|
||||||
public interface NbtWrapper<TType> extends NbtBase<TType> {
|
public interface NbtWrapper<TType> extends NbtBase<TType>, ClonableWrapper {
|
||||||
/**
|
/**
|
||||||
* Retrieve the underlying net.minecraft.server instance.
|
* Retrieve the underlying net.minecraft.server instance.
|
||||||
* @return The NMS instance.
|
* @return The NMS instance.
|
||||||
|
@ -246,7 +246,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<version>2.19.1</version>
|
<version>2.21.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
@ -51,11 +51,11 @@ import io.netty.channel.socket.SocketChannel;
|
|||||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||||
import io.netty.handler.codec.MessageToByteEncoder;
|
import io.netty.handler.codec.MessageToByteEncoder;
|
||||||
import io.netty.util.AttributeKey;
|
import io.netty.util.AttributeKey;
|
||||||
import io.netty.util.concurrent.GenericFutureListener;
|
|
||||||
import io.netty.util.internal.TypeParameterMatcher;
|
import io.netty.util.internal.TypeParameterMatcher;
|
||||||
|
|
||||||
import net.sf.cglib.proxy.Factory;
|
import net.sf.cglib.proxy.Factory;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
@ -478,7 +478,11 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
|||||||
|
|
||||||
private void scheduleMainThread(final Object packetCopy) {
|
private void scheduleMainThread(final Object packetCopy) {
|
||||||
// Don't use BukkitExecutors for this - it has a bit of overhead
|
// Don't use BukkitExecutors for this - it has a bit of overhead
|
||||||
Bukkit.getScheduler().scheduleSyncDelayedTask(factory.getPlugin(), () -> invokeSendPacket(packetCopy));
|
Bukkit.getScheduler().scheduleSyncDelayedTask(factory.getPlugin(), () -> {
|
||||||
|
if (!closed) {
|
||||||
|
invokeSendPacket(packetCopy);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -644,6 +648,8 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
|||||||
* @param packet - the packet to send.
|
* @param packet - the packet to send.
|
||||||
*/
|
*/
|
||||||
private void invokeSendPacket(Object packet) {
|
private void invokeSendPacket(Object packet) {
|
||||||
|
Validate.isTrue(!closed, "cannot send packets to a closed channel");
|
||||||
|
|
||||||
// Attempt to send the packet with NetworkMarker.handle(), or the PlayerConnection if its active
|
// Attempt to send the packet with NetworkMarker.handle(), or the PlayerConnection if its active
|
||||||
try {
|
try {
|
||||||
if (player instanceof Factory) {
|
if (player instanceof Factory) {
|
||||||
@ -694,8 +700,14 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
|||||||
*/
|
*/
|
||||||
private Object getPlayerConnection() {
|
private Object getPlayerConnection() {
|
||||||
if (playerConnection == null) {
|
if (playerConnection == null) {
|
||||||
playerConnection = MinecraftFields.getPlayerConnection(getPlayer());
|
Player player = getPlayer();
|
||||||
|
if (player == null) {
|
||||||
|
throw new IllegalStateException("cannot send packet to offline player" + (playerName != null ? " " + playerName : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playerConnection = MinecraftFields.getPlayerConnection(player);
|
||||||
|
}
|
||||||
|
|
||||||
return playerConnection;
|
return playerConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -714,7 +726,7 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
|||||||
@Override
|
@Override
|
||||||
public Player getPlayer() {
|
public Player getPlayer() {
|
||||||
if (player == null && playerName != null) {
|
if (player == null && playerName != null) {
|
||||||
return Bukkit.getPlayer(playerName);
|
return Bukkit.getPlayerExact(playerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
|
@ -4,14 +4,14 @@ import com.comphenix.protocol.utility.Constants;
|
|||||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.DispenserRegistry;
|
import net.minecraft.server.v1_13_R2.DispenserRegistry;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.craftbukkit.v1_13_R1.CraftServer;
|
import org.bukkit.craftbukkit.v1_13_R2.CraftServer;
|
||||||
import org.bukkit.craftbukkit.v1_13_R1.inventory.CraftItemFactory;
|
import org.bukkit.craftbukkit.v1_13_R2.inventory.CraftItemFactory;
|
||||||
import org.bukkit.craftbukkit.v1_13_R1.util.Versioning;
|
import org.bukkit.craftbukkit.v1_13_R2.util.Versioning;
|
||||||
|
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@ -21,13 +21,27 @@ import static org.mockito.Mockito.*;
|
|||||||
* @author Kristian
|
* @author Kristian
|
||||||
*/
|
*/
|
||||||
public class BukkitInitialization {
|
public class BukkitInitialization {
|
||||||
private static boolean initialized;
|
private static final BukkitInitialization instance = new BukkitInitialization();
|
||||||
private static boolean packaged;
|
|
||||||
|
private BukkitInitialization() {
|
||||||
|
System.out.println("Created new BukkitInitialization on " + Thread.currentThread().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean initialized;
|
||||||
|
private boolean packaged;
|
||||||
|
|
||||||
|
public static synchronized void initializePackage() {
|
||||||
|
instance.setPackage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void initializeItemMeta() {
|
||||||
|
instance.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Bukkit and ProtocolLib such that we can perfrom unit testing
|
* Initialize Bukkit and ProtocolLib such that we can perfrom unit testing
|
||||||
*/
|
*/
|
||||||
public static void initializeItemMeta() {
|
private void initialize() {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
// Denote that we're done
|
// Denote that we're done
|
||||||
initialized = true;
|
initialized = true;
|
||||||
@ -62,7 +76,7 @@ public class BukkitInitialization {
|
|||||||
/**
|
/**
|
||||||
* Ensure that package names are correctly set up.
|
* Ensure that package names are correctly set up.
|
||||||
*/
|
*/
|
||||||
public static void initializePackage() {
|
private void setPackage() {
|
||||||
if (!packaged) {
|
if (!packaged) {
|
||||||
packaged = true;
|
packaged = true;
|
||||||
|
|
||||||
|
@ -25,9 +25,9 @@ import com.comphenix.protocol.PacketType.Protocol;
|
|||||||
import com.comphenix.protocol.PacketType.Sender;
|
import com.comphenix.protocol.PacketType.Sender;
|
||||||
import com.comphenix.protocol.injector.packet.PacketRegistry;
|
import com.comphenix.protocol.injector.packet.PacketRegistry;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.EnumProtocol;
|
import net.minecraft.server.v1_13_R2.EnumProtocol;
|
||||||
import net.minecraft.server.v1_13_R1.EnumProtocolDirection;
|
import net.minecraft.server.v1_13_R2.EnumProtocolDirection;
|
||||||
import net.minecraft.server.v1_13_R1.PacketLoginInStart;
|
import net.minecraft.server.v1_13_R2.PacketLoginInStart;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -36,8 +36,8 @@ import com.comphenix.protocol.wrappers.nbt.NbtCompound;
|
|||||||
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.*;
|
import net.minecraft.server.v1_13_R2.*;
|
||||||
import net.minecraft.server.v1_13_R1.PacketPlayOutUpdateAttributes.AttributeSnapshot;
|
import net.minecraft.server.v1_13_R2.PacketPlayOutUpdateAttributes.AttributeSnapshot;
|
||||||
|
|
||||||
import org.apache.commons.lang.SerializationUtils;
|
import org.apache.commons.lang.SerializationUtils;
|
||||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||||
|
@ -6,8 +6,8 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.ItemStack;
|
import net.minecraft.server.v1_13_R2.ItemStack;
|
||||||
import net.minecraft.server.v1_13_R1.NonNullList;
|
import net.minecraft.server.v1_13_R2.NonNullList;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -6,22 +6,22 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.ChatComponentText;
|
import net.minecraft.server.v1_13_R2.ChatComponentText;
|
||||||
import net.minecraft.server.v1_13_R1.ChunkCoordIntPair;
|
import net.minecraft.server.v1_13_R2.ChunkCoordIntPair;
|
||||||
import net.minecraft.server.v1_13_R1.DataWatcher;
|
import net.minecraft.server.v1_13_R2.DataWatcher;
|
||||||
import net.minecraft.server.v1_13_R1.IBlockData;
|
import net.minecraft.server.v1_13_R2.IBlockData;
|
||||||
import net.minecraft.server.v1_13_R1.IChatBaseComponent;
|
import net.minecraft.server.v1_13_R2.IChatBaseComponent;
|
||||||
import net.minecraft.server.v1_13_R1.IChatBaseComponent.ChatSerializer;
|
import net.minecraft.server.v1_13_R2.IChatBaseComponent.ChatSerializer;
|
||||||
import net.minecraft.server.v1_13_R1.NBTCompressedStreamTools;
|
import net.minecraft.server.v1_13_R2.NBTCompressedStreamTools;
|
||||||
import net.minecraft.server.v1_13_R1.PacketPlayOutUpdateAttributes.AttributeSnapshot;
|
import net.minecraft.server.v1_13_R2.PacketPlayOutUpdateAttributes.AttributeSnapshot;
|
||||||
import net.minecraft.server.v1_13_R1.PlayerConnection;
|
import net.minecraft.server.v1_13_R2.PlayerConnection;
|
||||||
import net.minecraft.server.v1_13_R1.ServerPing;
|
import net.minecraft.server.v1_13_R2.ServerPing;
|
||||||
import net.minecraft.server.v1_13_R1.ServerPing.ServerData;
|
import net.minecraft.server.v1_13_R2.ServerPing.ServerData;
|
||||||
import net.minecraft.server.v1_13_R1.ServerPing.ServerPingPlayerSample;
|
import net.minecraft.server.v1_13_R2.ServerPing.ServerPingPlayerSample;
|
||||||
|
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.craftbukkit.v1_13_R1.inventory.CraftItemStack;
|
import org.bukkit.craftbukkit.v1_13_R2.inventory.CraftItemStack;
|
||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
|
@ -6,7 +6,7 @@ import com.comphenix.protocol.BukkitInitialization;
|
|||||||
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
|
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
|
||||||
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.IntHashMap;
|
import net.minecraft.server.v1_13_R2.IntHashMap;
|
||||||
|
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
|
@ -16,14 +16,14 @@ public class ChunkCoordIntPairTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test() {
|
public void test() {
|
||||||
net.minecraft.server.v1_13_R1.ChunkCoordIntPair pair = new net.minecraft.server.v1_13_R1.ChunkCoordIntPair(1, 2);
|
net.minecraft.server.v1_13_R2.ChunkCoordIntPair pair = new net.minecraft.server.v1_13_R2.ChunkCoordIntPair(1, 2);
|
||||||
ChunkCoordIntPair specific = ChunkCoordIntPair.getConverter().getSpecific(pair);
|
ChunkCoordIntPair specific = ChunkCoordIntPair.getConverter().getSpecific(pair);
|
||||||
|
|
||||||
assertEquals(1, specific.getChunkX());
|
assertEquals(1, specific.getChunkX());
|
||||||
assertEquals(2, specific.getChunkZ());
|
assertEquals(2, specific.getChunkZ());
|
||||||
|
|
||||||
net.minecraft.server.v1_13_R1.ChunkCoordIntPair roundtrip =
|
net.minecraft.server.v1_13_R2.ChunkCoordIntPair roundtrip =
|
||||||
(net.minecraft.server.v1_13_R1.ChunkCoordIntPair) ChunkCoordIntPair.getConverter().
|
(net.minecraft.server.v1_13_R2.ChunkCoordIntPair) ChunkCoordIntPair.getConverter().
|
||||||
getGeneric(specific);
|
getGeneric(specific);
|
||||||
|
|
||||||
assertEquals(1, roundtrip.x);
|
assertEquals(1, roundtrip.x);
|
||||||
|
@ -2,12 +2,12 @@ package com.comphenix.protocol.wrappers;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.EntityHuman.EnumChatVisibility;
|
import net.minecraft.server.v1_13_R2.EntityHuman.EnumChatVisibility;
|
||||||
import net.minecraft.server.v1_13_R1.EnumDifficulty;
|
import net.minecraft.server.v1_13_R2.EnumDifficulty;
|
||||||
import net.minecraft.server.v1_13_R1.EnumGamemode;
|
import net.minecraft.server.v1_13_R2.EnumGamemode;
|
||||||
import net.minecraft.server.v1_13_R1.EnumProtocol;
|
import net.minecraft.server.v1_13_R2.EnumProtocol;
|
||||||
import net.minecraft.server.v1_13_R1.PacketPlayInClientCommand.EnumClientCommand;
|
import net.minecraft.server.v1_13_R2.PacketPlayInClientCommand.EnumClientCommand;
|
||||||
import net.minecraft.server.v1_13_R1.PacketPlayInUseEntity.EnumEntityUseAction;
|
import net.minecraft.server.v1_13_R2.PacketPlayInUseEntity.EnumEntityUseAction;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -6,9 +6,9 @@ import static org.junit.Assert.assertTrue;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.AttributeModifier;
|
import net.minecraft.server.v1_13_R2.AttributeModifier;
|
||||||
import net.minecraft.server.v1_13_R1.PacketPlayOutUpdateAttributes;
|
import net.minecraft.server.v1_13_R2.PacketPlayOutUpdateAttributes;
|
||||||
import net.minecraft.server.v1_13_R1.PacketPlayOutUpdateAttributes.AttributeSnapshot;
|
import net.minecraft.server.v1_13_R2.PacketPlayOutUpdateAttributes.AttributeSnapshot;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
@ -23,10 +23,10 @@ import com.comphenix.protocol.wrappers.WrappedDataWatcher.Registry;
|
|||||||
import com.comphenix.protocol.wrappers.WrappedDataWatcher.Serializer;
|
import com.comphenix.protocol.wrappers.WrappedDataWatcher.Serializer;
|
||||||
import com.comphenix.protocol.wrappers.WrappedDataWatcher.WrappedDataWatcherObject;
|
import com.comphenix.protocol.wrappers.WrappedDataWatcher.WrappedDataWatcherObject;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.EntityEgg;
|
import net.minecraft.server.v1_13_R2.EntityEgg;
|
||||||
|
|
||||||
import org.bukkit.craftbukkit.v1_13_R1.entity.CraftEgg;
|
import org.bukkit.craftbukkit.v1_13_R2.entity.CraftEgg;
|
||||||
import org.bukkit.craftbukkit.v1_13_R1.entity.CraftEntity;
|
import org.bukkit.craftbukkit.v1_13_R2.entity.CraftEntity;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@ -87,8 +87,8 @@ public class WrappedDataWatcherTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSerializers() {
|
public void testSerializers() {
|
||||||
Serializer blockPos = Registry.get(net.minecraft.server.v1_13_R1.BlockPosition.class, false);
|
Serializer blockPos = Registry.get(net.minecraft.server.v1_13_R2.BlockPosition.class, false);
|
||||||
Serializer optionalBlockPos = Registry.get(net.minecraft.server.v1_13_R1.BlockPosition.class, true);
|
Serializer optionalBlockPos = Registry.get(net.minecraft.server.v1_13_R2.BlockPosition.class, true);
|
||||||
assertNotSame(blockPos, optionalBlockPos);
|
assertNotSame(blockPos, optionalBlockPos);
|
||||||
|
|
||||||
// assertNull(Registry.get(ItemStack.class, false));
|
// assertNull(Registry.get(ItemStack.class, false));
|
||||||
|
@ -26,8 +26,8 @@ import java.io.DataInputStream;
|
|||||||
import java.io.DataOutput;
|
import java.io.DataOutput;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
|
|
||||||
import net.minecraft.server.v1_13_R1.ItemStack;
|
import net.minecraft.server.v1_13_R2.ItemStack;
|
||||||
import net.minecraft.server.v1_13_R1.Items;
|
import net.minecraft.server.v1_13_R2.Items;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
2
pom.xml
2
pom.xml
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<spigotVersion>1.13-R0.1-SNAPSHOT</spigotVersion>
|
<spigotVersion>1.13.1-R0.1-SNAPSHOT</spigotVersion>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
|
In neuem Issue referenzieren
Einen Benutzer sperren