Mirror von
https://github.com/ViaVersion/ViaVersion.git
synchronisiert 2024-11-03 14:50:30 +01:00
Merge remote-tracking branch 'origin/master' into dev
Dieser Commit ist enthalten in:
Commit
4f1e92f309
@ -39,8 +39,12 @@ import java.nio.file.Path;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
// Specific Via changes:
|
||||
// - Use OpenNBT tags
|
||||
// - Added readString/writeString methods from TagStringIO
|
||||
// - Has not been updated for the sake of keeping the class simple
|
||||
/**
|
||||
* See https://github.com/KyoriPowered/adventure.
|
||||
* Serialization operations for binary tags.
|
||||
*/
|
||||
public final class BinaryTagIO {
|
||||
private BinaryTagIO() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* This file is part of adventure, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) 2017-2020 KyoriPowered
|
||||
* Copyright (c) 2017-2021 KyoriPowered
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -23,7 +23,10 @@
|
||||
*/
|
||||
package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
|
||||
/* package */ final class CharBuffer {
|
||||
/**
|
||||
* A character buffer designed to be inspected by a parser.
|
||||
*/
|
||||
final class CharBuffer {
|
||||
private final CharSequence sequence;
|
||||
private int index;
|
||||
|
||||
@ -32,7 +35,7 @@ package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the character at the current position
|
||||
* Get the character at the current position.
|
||||
*
|
||||
* @return The current character
|
||||
*/
|
||||
@ -45,7 +48,7 @@ package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current character and advance
|
||||
* Get the current character and advance.
|
||||
*
|
||||
* @return current character
|
||||
*/
|
||||
@ -62,6 +65,10 @@ package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
return this.index < this.sequence.length();
|
||||
}
|
||||
|
||||
public boolean hasMore(final int offset) {
|
||||
return this.index + offset < this.sequence.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for the provided token, and advance the reader index past the {@code until} character.
|
||||
*
|
||||
@ -109,6 +116,23 @@ package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the next non-whitespace character is {@code token}, advance past it.
|
||||
*
|
||||
* <p>This method always consumes whitespace.</p>
|
||||
*
|
||||
* @param token next non-whitespace character to query
|
||||
* @return if the next non-whitespace character is {@code token}
|
||||
*/
|
||||
public boolean takeIf(final char token) {
|
||||
this.skipWhitespace();
|
||||
if (this.hasMore() && this.peek() == token) {
|
||||
this.advance();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public CharBuffer skipWhitespace() {
|
||||
while (this.hasMore() && Character.isWhitespace(this.peek())) this.advance();
|
||||
return this;
|
||||
|
@ -26,9 +26,9 @@ package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An exception thrown when parsing a string tag
|
||||
* An exception thrown when parsing a string tag.
|
||||
*/
|
||||
/* package */ class StringTagParseException extends IOException {
|
||||
class StringTagParseException extends IOException {
|
||||
private static final long serialVersionUID = -3001637554903912905L;
|
||||
private final CharSequence buffer;
|
||||
private final int position;
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* This file is part of adventure, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) 2017-2020 KyoriPowered
|
||||
* Copyright (c) 2017-2021 KyoriPowered
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -37,33 +37,39 @@ import com.github.steveice10.opennbt.tag.builtin.NumberTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ShortTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.LongStream;
|
||||
|
||||
// Specific Via changes:
|
||||
// - Use OpenNBT tags
|
||||
// - Small byteArray() optimization
|
||||
// - acceptLegacy = true by default
|
||||
final class TagStringReader {
|
||||
private static final int MAX_DEPTH = 512;
|
||||
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
private static final int[] EMPTY_INT_ARRAY = new int[0];
|
||||
private static final long[] EMPTY_LONG_ARRAY = new long[0];
|
||||
|
||||
/**
|
||||
* See https://github.com/KyoriPowered/adventure.
|
||||
*/
|
||||
/* package */ final class TagStringReader {
|
||||
private final CharBuffer buffer;
|
||||
private boolean acceptLegacy = true;
|
||||
private int depth;
|
||||
|
||||
public TagStringReader(final CharBuffer buffer) {
|
||||
TagStringReader(final CharBuffer buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
public CompoundTag compound() throws StringTagParseException {
|
||||
this.buffer.expect(Tokens.COMPOUND_BEGIN);
|
||||
final CompoundTag compoundTag = new CompoundTag();
|
||||
if (this.buffer.peek() == Tokens.COMPOUND_END) {
|
||||
this.buffer.take();
|
||||
if (this.buffer.takeIf(Tokens.COMPOUND_END)) {
|
||||
return compoundTag;
|
||||
}
|
||||
|
||||
while (this.buffer.hasMore()) {
|
||||
final String key = this.key();
|
||||
final Tag tag = this.tag();
|
||||
compoundTag.put(key, tag);
|
||||
compoundTag.put(this.key(), this.tag());
|
||||
if (this.separatorOrCompleteWith(Tokens.COMPOUND_END)) {
|
||||
return compoundTag;
|
||||
}
|
||||
@ -74,13 +80,11 @@ import java.util.stream.IntStream;
|
||||
public ListTag list() throws StringTagParseException {
|
||||
final ListTag listTag = new ListTag();
|
||||
this.buffer.expect(Tokens.ARRAY_BEGIN);
|
||||
final boolean prefixedIndex = this.buffer.peek() == '0' && this.buffer.peek(1) == ':';
|
||||
while (this.buffer.hasMore()) {
|
||||
if (this.buffer.peek() == Tokens.ARRAY_END) {
|
||||
this.buffer.advance();
|
||||
final boolean prefixedIndex = this.acceptLegacy && this.buffer.peek() == '0' && this.buffer.peek(1) == ':';
|
||||
if (!prefixedIndex && this.buffer.takeIf(Tokens.ARRAY_END)) {
|
||||
return listTag;
|
||||
}
|
||||
|
||||
while (this.buffer.hasMore()) {
|
||||
if (prefixedIndex) {
|
||||
this.buffer.takeUntil(':');
|
||||
}
|
||||
@ -99,11 +103,12 @@ import java.util.stream.IntStream;
|
||||
*
|
||||
* @return array-typed tag
|
||||
*/
|
||||
public Tag array(final char elementType) throws StringTagParseException {
|
||||
public Tag array(char elementType) throws StringTagParseException {
|
||||
this.buffer.expect(Tokens.ARRAY_BEGIN)
|
||||
.expect(elementType)
|
||||
.expect(Tokens.ARRAY_SIGNATURE_SEPARATOR);
|
||||
|
||||
elementType = Character.toLowerCase(elementType);
|
||||
if (elementType == Tokens.TYPE_BYTE) {
|
||||
return new ByteArrayTag(this.byteArray());
|
||||
} else if (elementType == Tokens.TYPE_INT) {
|
||||
@ -116,11 +121,15 @@ import java.util.stream.IntStream;
|
||||
}
|
||||
|
||||
private byte[] byteArray() throws StringTagParseException {
|
||||
final List<Byte> bytes = new ArrayList<>();
|
||||
if (this.buffer.takeIf(Tokens.ARRAY_END)) {
|
||||
return EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
|
||||
final IntList bytes = new IntArrayList();
|
||||
while (this.buffer.hasMore()) {
|
||||
final CharSequence value = this.buffer.skipWhitespace().takeUntil(Tokens.TYPE_BYTE);
|
||||
try {
|
||||
bytes.add(Byte.valueOf(value.toString()));
|
||||
bytes.add(Byte.parseByte(value.toString()));
|
||||
} catch (final NumberFormatException ex) {
|
||||
throw this.buffer.makeError("All elements of a byte array must be bytes!");
|
||||
}
|
||||
@ -128,7 +137,7 @@ import java.util.stream.IntStream;
|
||||
if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) {
|
||||
final byte[] result = new byte[bytes.size()];
|
||||
for (int i = 0; i < bytes.size(); ++i) {
|
||||
result[i] = bytes.get(i);
|
||||
result[i] = (byte) bytes.getInt(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -137,6 +146,10 @@ import java.util.stream.IntStream;
|
||||
}
|
||||
|
||||
private int[] intArray() throws StringTagParseException {
|
||||
if (this.buffer.takeIf(Tokens.ARRAY_END)) {
|
||||
return EMPTY_INT_ARRAY;
|
||||
}
|
||||
|
||||
final IntStream.Builder builder = IntStream.builder();
|
||||
while (this.buffer.hasMore()) {
|
||||
final Tag value = this.tag();
|
||||
@ -152,21 +165,21 @@ import java.util.stream.IntStream;
|
||||
}
|
||||
|
||||
private long[] longArray() throws StringTagParseException {
|
||||
final List<Long> longs = new ArrayList<>();
|
||||
if (this.buffer.takeIf(Tokens.ARRAY_END)) {
|
||||
return EMPTY_LONG_ARRAY;
|
||||
}
|
||||
|
||||
final LongStream.Builder longs = LongStream.builder();
|
||||
while (this.buffer.hasMore()) {
|
||||
final CharSequence value = this.buffer.skipWhitespace().takeUntil(Tokens.TYPE_LONG);
|
||||
try {
|
||||
longs.add(Long.valueOf(value.toString()));
|
||||
longs.add(Long.parseLong(value.toString()));
|
||||
} catch (final NumberFormatException ex) {
|
||||
throw this.buffer.makeError("All elements of a long array must be longs!");
|
||||
}
|
||||
|
||||
if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) {
|
||||
final long[] result = new long[longs.size()];
|
||||
for (int i = 0; i < longs.size(); ++i) {
|
||||
result[i] = longs.get(i);
|
||||
}
|
||||
return result;
|
||||
return longs.build().toArray();
|
||||
}
|
||||
}
|
||||
throw this.buffer.makeError("Reached end of document without array close");
|
||||
@ -181,7 +194,21 @@ import java.util.stream.IntStream;
|
||||
}
|
||||
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
while (this.buffer.peek() != ':') { // DO NOT CHECK FOR CHARACTER VALIDITY; LEGACY NBT ALLOWS ANY CHARACTER, EVEN WHEN UNQUOTED
|
||||
while (this.buffer.hasMore()) {
|
||||
final char peek = this.buffer.peek();
|
||||
if (!Tokens.id(peek)) {
|
||||
if (this.acceptLegacy) {
|
||||
// In legacy format, a key is any non-colon character, with escapes allowed
|
||||
if (peek == Tokens.ESCAPE_MARKER) {
|
||||
this.buffer.take(); // skip
|
||||
continue;
|
||||
} else if (peek != Tokens.COMPOUND_KEY_TERMINATOR) {
|
||||
builder.append(this.buffer.take());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
builder.append(this.buffer.take());
|
||||
}
|
||||
return builder.toString();
|
||||
@ -191,12 +218,17 @@ import java.util.stream.IntStream;
|
||||
}
|
||||
|
||||
public Tag tag() throws StringTagParseException {
|
||||
if (this.depth++ > MAX_DEPTH) {
|
||||
throw this.buffer.makeError("Exceeded maximum allowed depth of " + MAX_DEPTH + " when reading tag");
|
||||
}
|
||||
try {
|
||||
final char startToken = this.buffer.skipWhitespace().peek();
|
||||
switch (startToken) {
|
||||
case Tokens.COMPOUND_BEGIN:
|
||||
return this.compound();
|
||||
case Tokens.ARRAY_BEGIN:
|
||||
if (this.buffer.peek(2) == ';') { // we know we're an array tag
|
||||
// Maybe add in a legacy-only mode to read those?
|
||||
if (this.buffer.hasMore(2) && this.buffer.peek(2) == ';') { // we know we're an array tag
|
||||
return this.array(this.buffer.peek(1));
|
||||
} else {
|
||||
return this.list();
|
||||
@ -209,12 +241,15 @@ import java.util.stream.IntStream;
|
||||
default: // scalar
|
||||
return this.scalar();
|
||||
}
|
||||
} finally {
|
||||
this.depth--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A tag that is definitely some sort of scalar
|
||||
* A tag that is definitely some sort of scalar.
|
||||
*
|
||||
* <p>Does not detect quoted strings, so </p>
|
||||
* <p>Does not detect quoted strings, so those should have been parsed already.</p>
|
||||
*
|
||||
* @return a parsed tag
|
||||
*/
|
||||
@ -227,7 +262,7 @@ import java.util.stream.IntStream;
|
||||
if (builder.length() != 0) {
|
||||
Tag result = null;
|
||||
try {
|
||||
switch (Character.toUpperCase(current)) { // try to read and return as a number
|
||||
switch (Character.toLowerCase(current)) { // try to read and return as a number
|
||||
// case Tokens.TYPE_INTEGER: // handled below, ints are ~special~
|
||||
case Tokens.TYPE_BYTE:
|
||||
result = new ByteTag(Byte.parseByte(builder.toString()));
|
||||
@ -269,29 +304,33 @@ import java.util.stream.IntStream;
|
||||
try {
|
||||
return new IntTag(Integer.parseInt(built));
|
||||
} catch (final NumberFormatException ex) {
|
||||
try {
|
||||
return new DoubleTag(Double.parseDouble(built));
|
||||
} catch (final NumberFormatException ex2) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (built.equalsIgnoreCase(Tokens.LITERAL_TRUE)) {
|
||||
return new ByteTag((byte) 1);
|
||||
} else if (built.equalsIgnoreCase(Tokens.LITERAL_FALSE)) {
|
||||
return new ByteTag((byte) 0);
|
||||
}
|
||||
return new StringTag(built);
|
||||
|
||||
}
|
||||
|
||||
private boolean separatorOrCompleteWith(final char endCharacter) throws StringTagParseException {
|
||||
if (this.buffer.skipWhitespace().peek() == endCharacter) {
|
||||
this.buffer.take();
|
||||
if (this.buffer.takeIf(endCharacter)) {
|
||||
return true;
|
||||
}
|
||||
this.buffer.expect(Tokens.VALUE_SEPARATOR);
|
||||
if (this.buffer.skipWhitespace().peek() == endCharacter) {
|
||||
this.buffer.take();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove simple escape sequences from a string
|
||||
* Remove simple escape sequences from a string.
|
||||
*
|
||||
* @param withEscapes input string with escapes
|
||||
* @return string with escapes processed
|
||||
@ -310,4 +349,8 @@ import java.util.stream.IntStream;
|
||||
output.append(withEscapes.substring(lastEscape));
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
public void legacy(final boolean acceptLegacy) {
|
||||
this.acceptLegacy = acceptLegacy;
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import com.github.steveice10.opennbt.tag.builtin.IntTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.LongArrayTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.LongTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.NumberTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ShortTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
@ -41,12 +42,16 @@ import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Map;
|
||||
|
||||
// Specific Via changes:
|
||||
// - Use OpenNBT tags
|
||||
// - Has not been updated to support pretty printing and legacy writing since that is not needed
|
||||
/**
|
||||
* See https://github.com/KyoriPowered/adventure.
|
||||
* An emitter for the SNBT format.
|
||||
*
|
||||
* <p>Details on the format are described in the package documentation.</p>
|
||||
*/
|
||||
/* package */ final class TagStringWriter implements AutoCloseable {
|
||||
final class TagStringWriter implements AutoCloseable {
|
||||
private final Appendable out;
|
||||
private final String indent = " ";
|
||||
private int level;
|
||||
/**
|
||||
* Whether a {@link Tokens#VALUE_SEPARATOR} needs to be printed before the beginning of the next object.
|
||||
@ -73,17 +78,17 @@ import java.util.Map;
|
||||
} else if (tag instanceof StringTag) {
|
||||
return this.value(((StringTag) tag).getValue(), Tokens.EOF);
|
||||
} else if (tag instanceof ByteTag) {
|
||||
return this.value(Byte.toString(((ByteTag) tag).asByte()), Tokens.TYPE_BYTE);
|
||||
return this.value(Byte.toString(((NumberTag) tag).asByte()), Tokens.TYPE_BYTE);
|
||||
} else if (tag instanceof ShortTag) {
|
||||
return this.value(Short.toString(((ShortTag) tag).asShort()), Tokens.TYPE_SHORT);
|
||||
return this.value(Short.toString(((NumberTag) tag).asShort()), Tokens.TYPE_SHORT);
|
||||
} else if (tag instanceof IntTag) {
|
||||
return this.value(Integer.toString(((IntTag) tag).asInt()), Tokens.TYPE_INT);
|
||||
return this.value(Integer.toString(((NumberTag) tag).asInt()), Tokens.TYPE_INT);
|
||||
} else if (tag instanceof LongTag) {
|
||||
return this.value(Long.toString(((LongTag) tag).asLong()), Tokens.TYPE_LONG);
|
||||
return this.value(Long.toString(((NumberTag) tag).asLong()), Character.toUpperCase(Tokens.TYPE_LONG)); // special case
|
||||
} else if (tag instanceof FloatTag) {
|
||||
return this.value(Float.toString(((FloatTag) tag).asFloat()), Tokens.TYPE_FLOAT);
|
||||
return this.value(Float.toString(((NumberTag) tag).asFloat()), Tokens.TYPE_FLOAT);
|
||||
} else if (tag instanceof DoubleTag) {
|
||||
return this.value(Double.toString(((DoubleTag) tag).asDouble()), Tokens.TYPE_DOUBLE);
|
||||
return this.value(Double.toString(((NumberTag) tag).asDouble()), Tokens.TYPE_DOUBLE);
|
||||
} else {
|
||||
throw new IOException("Unknown tag type: " + tag.getClass().getSimpleName());
|
||||
// unknown!
|
||||
@ -92,7 +97,7 @@ import java.util.Map;
|
||||
|
||||
private TagStringWriter writeCompound(final CompoundTag tag) throws IOException {
|
||||
this.beginCompound();
|
||||
for (Map.Entry<String, Tag> entry : tag.entrySet()) {
|
||||
for (final Map.Entry<String, Tag> entry : tag.entrySet()) {
|
||||
this.key(entry.getKey());
|
||||
this.writeTag(entry.getValue());
|
||||
}
|
||||
@ -244,7 +249,6 @@ import java.util.Map;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (this.level != 0) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* This file is part of adventure, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) 2017-2020 KyoriPowered
|
||||
* Copyright (c) 2017-2021 KyoriPowered
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -23,10 +23,7 @@
|
||||
*/
|
||||
package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
|
||||
/**
|
||||
* See https://github.com/KyoriPowered/adventure.
|
||||
*/
|
||||
/* package */ final class Tokens {
|
||||
final class Tokens {
|
||||
// Compounds
|
||||
static final char COMPOUND_BEGIN = '{';
|
||||
static final char COMPOUND_END = '}';
|
||||
@ -43,20 +40,24 @@ package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
static final char DOUBLE_QUOTE = '"';
|
||||
static final char ESCAPE_MARKER = '\\';
|
||||
|
||||
static final char TYPE_BYTE = 'B';
|
||||
static final char TYPE_SHORT = 'S';
|
||||
static final char TYPE_INT = 'I'; // array only
|
||||
static final char TYPE_LONG = 'L';
|
||||
static final char TYPE_FLOAT = 'F';
|
||||
static final char TYPE_DOUBLE = 'D';
|
||||
static final char TYPE_BYTE = 'b';
|
||||
static final char TYPE_SHORT = 's';
|
||||
static final char TYPE_INT = 'i'; // array only
|
||||
static final char TYPE_LONG = 'l';
|
||||
static final char TYPE_FLOAT = 'f';
|
||||
static final char TYPE_DOUBLE = 'd';
|
||||
|
||||
static final String LITERAL_TRUE = "true";
|
||||
static final String LITERAL_FALSE = "false";
|
||||
|
||||
static final String NEWLINE = System.getProperty("line.separator", "\n");
|
||||
static final char EOF = '\0';
|
||||
|
||||
private Tokens() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if a character is a valid component in an identifier
|
||||
* Return if a character is a valid component in an identifier.
|
||||
*
|
||||
* <p>An identifier character must match the expression {@code [a-zA-Z0-9_+.-]}</p>
|
||||
*
|
||||
@ -73,8 +74,8 @@ package com.viaversion.viaversion.api.minecraft.nbt;
|
||||
|
||||
/**
|
||||
* Return whether a character could be at some position in a number.
|
||||
* <p>
|
||||
* A string passing this check does not necessarily mean it is syntactically valid
|
||||
*
|
||||
* <p>A string passing this check does not necessarily mean it is syntactically valid.</p>
|
||||
*
|
||||
* @param c character to check
|
||||
* @return if possibly part of a number
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren