mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-18 11:54:59 -04:00
target parser: basic property reading
This commit is contained in:
parent
4752334f3d
commit
f0942b0467
@ -19,4 +19,4 @@ abstract class ReaderError(
|
||||
val reader: CommandReader,
|
||||
val start: Int,
|
||||
val end: Int,
|
||||
) : Exception("Error at $start-$end: ${reader.string}")
|
||||
) : Exception("Error at $start-$end: ${reader.string} (at ${reader.string.substring(start, end)}")
|
||||
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.commands.errors.reader.map
|
||||
|
||||
import de.bixilon.minosoft.commands.errors.ReaderError
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
import de.bixilon.minosoft.commands.util.ReadResult
|
||||
|
||||
class DuplicatedKeyMapError(
|
||||
reader: CommandReader,
|
||||
val key: ReadResult<*>,
|
||||
val existingValue: Any?,
|
||||
) : ReaderError(reader, key.start, key.end)
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.commands.errors.reader.map
|
||||
|
||||
import de.bixilon.minosoft.commands.errors.ReaderError
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
import de.bixilon.minosoft.commands.util.ReadResult
|
||||
|
||||
class ExpectedKeyMapError(
|
||||
reader: CommandReader,
|
||||
result: ReadResult<*>,
|
||||
) : ReaderError(reader, result.start, result.end)
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.commands.errors.reader.map
|
||||
|
||||
import de.bixilon.minosoft.commands.errors.ReaderError
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
|
||||
class InvalidAssignCharMapError(
|
||||
reader: CommandReader,
|
||||
pointer: Int,
|
||||
val found: Int?,
|
||||
) : ReaderError(reader, pointer - 1, pointer)
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.commands.errors.reader.map
|
||||
|
||||
import de.bixilon.minosoft.commands.errors.ReaderError
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
|
||||
class InvalidMapSeparatorError(
|
||||
reader: CommandReader,
|
||||
pointer: Int,
|
||||
val found: Int?,
|
||||
) : ReaderError(reader, pointer - 1, pointer)
|
@ -22,7 +22,13 @@ import de.bixilon.minosoft.commands.parser.minecraft.target.targets.identifier.n
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.identifier.name.NameEntityTarget
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.identifier.uuid.InvalidUUIDError
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.identifier.uuid.UUIDEntityTarget
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.SelectorEntityTarget
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.error.InvalidSelectorKeyError
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.error.InvalidTargetSelector
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.TargetProperties
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.TargetProperty
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
import de.bixilon.minosoft.commands.util.ReadResult
|
||||
import de.bixilon.minosoft.data.registries.ResourceLocation
|
||||
import de.bixilon.minosoft.data.text.ChatComponent
|
||||
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
|
||||
@ -40,7 +46,7 @@ class TargetParser(
|
||||
override val placeholder = ChatComponent.of("<target>")
|
||||
|
||||
override fun parse(reader: CommandReader): EntityTarget {
|
||||
if (!reader.canPeekNext()) {
|
||||
if (!reader.canPeek()) {
|
||||
throw ExpectedArgumentError(reader)
|
||||
}
|
||||
return if (reader.peek() == '@'.code) {
|
||||
@ -50,8 +56,26 @@ class TargetParser(
|
||||
}
|
||||
}
|
||||
|
||||
fun CommandReader.parseSelector(): EntityTarget {
|
||||
TODO()
|
||||
fun CommandReader.parseSelector(): SelectorEntityTarget {
|
||||
unsafeRead('@'.code)
|
||||
val selectorChar = readNext() ?: throw ExpectedArgumentError(this)
|
||||
val selector = TargetSelectors.BY_CHAR[selectorChar.toChar()] ?: throw InvalidTargetSelector(this)
|
||||
|
||||
val properties: Map<String, TargetProperty> = readMap({ readKey() }, { readValue(it) }) ?: emptyMap()
|
||||
|
||||
return SelectorEntityTarget(selector, properties)
|
||||
}
|
||||
|
||||
private fun CommandReader.readKey(): String? {
|
||||
if (peek() == '"'.code) {
|
||||
return readUnquotedString()
|
||||
}
|
||||
return readUntil('='.code)
|
||||
}
|
||||
|
||||
private fun CommandReader.readValue(key: ReadResult<String>): TargetProperty {
|
||||
val target = TargetProperties[key.result] ?: throw InvalidSelectorKeyError(this, key)
|
||||
return target.read(this)
|
||||
}
|
||||
|
||||
fun parseEntityIdentifier(reader: CommandReader): EntityTarget {
|
||||
|
@ -13,20 +13,13 @@
|
||||
|
||||
package de.bixilon.minosoft.commands.parser.minecraft.target
|
||||
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.DistanceProperty
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.GamemodeProperty
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.NameProperty
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.TypeProperty
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.rotation.PitchRotation
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.rotation.YawRotation
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.sort.Sorting
|
||||
|
||||
data class TargetProperties(
|
||||
val selector: TargetSelectors,
|
||||
@Deprecated("")
|
||||
data class TargetPropertiesLEGACY(
|
||||
var x: Double?,
|
||||
var y: Double?,
|
||||
var z: Double?,
|
||||
var distance: DistanceProperty?,
|
||||
var volumeX: Double?,
|
||||
var volumeY: Double?,
|
||||
var volumeZ: Double?,
|
||||
@ -36,11 +29,6 @@ data class TargetProperties(
|
||||
var sort: Sorting?,
|
||||
var limit: Int? = null,
|
||||
var level: IntRange? = null,
|
||||
var gamemode: GamemodeProperty? = null,
|
||||
var name: NameProperty? = null,
|
||||
var xRotation: PitchRotation? = null,
|
||||
var yRotation: YawRotation? = null,
|
||||
var type: TypeProperty? = null,
|
||||
var nbt: Any? = null, // ToDo
|
||||
var advancements: Any? = null, // ToDo
|
||||
var predicate: Any? = null, // ToDo
|
||||
|
@ -13,6 +13,8 @@
|
||||
|
||||
package de.bixilon.minosoft.commands.parser.minecraft.target
|
||||
|
||||
import de.bixilon.kutil.enums.EnumUtil
|
||||
import de.bixilon.kutil.enums.ValuesEnum
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.sort.Sorting
|
||||
import de.bixilon.minosoft.data.entities.entities.Entity
|
||||
|
||||
@ -30,4 +32,16 @@ enum class TargetSelectors(
|
||||
fun sort(selected: MutableList<Entity>) {
|
||||
sorting.sort(selected)
|
||||
}
|
||||
|
||||
companion object : ValuesEnum<TargetSelectors> {
|
||||
override val VALUES: Array<TargetSelectors> = values()
|
||||
override val NAME_MAP: Map<String, TargetSelectors> = EnumUtil.getEnumValues(VALUES)
|
||||
val BY_CHAR: MutableMap<Char, TargetSelectors> = mutableMapOf()
|
||||
|
||||
init {
|
||||
for (value in VALUES) {
|
||||
BY_CHAR[value.char] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector
|
||||
|
||||
import de.bixilon.minosoft.commands.errors.parser.ParserError
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
import de.bixilon.minosoft.commands.util.ReadResult
|
||||
|
||||
|
||||
class SelectorParserError(
|
||||
reader: CommandReader,
|
||||
result: ReadResult<*>,
|
||||
) : ParserError(reader, result)
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.error
|
||||
|
||||
import de.bixilon.minosoft.commands.errors.parser.ParserError
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
import de.bixilon.minosoft.commands.util.ReadResult
|
||||
|
||||
class InvalidSelectorKeyError(
|
||||
reader: CommandReader,
|
||||
key: ReadResult<String>,
|
||||
) : ParserError(reader, key)
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.error
|
||||
|
||||
import de.bixilon.minosoft.commands.errors.ReaderError
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
|
||||
class InvalidTargetSelector(
|
||||
reader: CommandReader,
|
||||
) : ReaderError(reader, reader.pointer - 1, reader.pointer)
|
@ -34,4 +34,8 @@ object TargetProperties {
|
||||
fun register(factory: TargetPropertyFactory<*>) {
|
||||
properties[factory.name] = factory
|
||||
}
|
||||
|
||||
operator fun get(key: String): TargetPropertyFactory<*>? {
|
||||
return properties[key]
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ class EnumParser<E : Enum<*>>(
|
||||
}
|
||||
|
||||
fun CommandReader.readEnum(): E? {
|
||||
return values.getOrNull(readString()?.lowercase()) // ToDo: Allow ordinals
|
||||
return values.getOrNull(readWord()?.lowercase()) // ToDo: Allow ordinals
|
||||
}
|
||||
|
||||
override fun getSuggestions(reader: CommandReader): List<E> {
|
||||
|
@ -13,7 +13,12 @@
|
||||
|
||||
package de.bixilon.minosoft.commands.util
|
||||
|
||||
import de.bixilon.kutil.cast.CastUtil.unsafeCast
|
||||
import de.bixilon.minosoft.commands.errors.reader.*
|
||||
import de.bixilon.minosoft.commands.errors.reader.map.DuplicatedKeyMapError
|
||||
import de.bixilon.minosoft.commands.errors.reader.map.ExpectedKeyMapError
|
||||
import de.bixilon.minosoft.commands.errors.reader.map.InvalidAssignCharMapError
|
||||
import de.bixilon.minosoft.commands.errors.reader.map.InvalidMapSeparatorError
|
||||
import de.bixilon.minosoft.commands.errors.reader.number.NegativeNumberError
|
||||
import de.bixilon.minosoft.data.registries.ResourceLocation
|
||||
import de.bixilon.minosoft.util.KUtil.toResourceLocation
|
||||
@ -214,7 +219,7 @@ open class CommandReader(val string: String) {
|
||||
}
|
||||
val builder = StringBuilder()
|
||||
while (true) {
|
||||
val peek = peek()
|
||||
val peek = peekNext()
|
||||
if (peek == null) {
|
||||
if (required) {
|
||||
throw OutOfBoundsError(this, length - 1)
|
||||
@ -308,6 +313,51 @@ open class CommandReader(val string: String) {
|
||||
return ReadResult(start, end, read, result)
|
||||
}
|
||||
|
||||
fun <K, V> readMap(keyReader: CommandReader.() -> K?, valueReader: CommandReader.(key: ReadResult<K>) -> V): Map<K, V>? {
|
||||
if (!canPeekNext()) {
|
||||
return null
|
||||
}
|
||||
if (peekNext() != '['.code) {
|
||||
return null
|
||||
}
|
||||
readNext() // [
|
||||
val map: MutableMap<K, V> = mutableMapOf()
|
||||
while (true) {
|
||||
if (peek() == ']'.code) {
|
||||
break
|
||||
}
|
||||
skipWhitespaces()
|
||||
val key = readResult { keyReader(this) }
|
||||
if (key.result == null) {
|
||||
throw ExpectedKeyMapError(this, key)
|
||||
}
|
||||
val existing: V? = map[key.result]
|
||||
if (existing != null) {
|
||||
throw DuplicatedKeyMapError(this, key, existing)
|
||||
}
|
||||
val assign = read()
|
||||
if (assign != '='.code) {
|
||||
throw InvalidAssignCharMapError(this, pointer - 1, assign)
|
||||
}
|
||||
skipWhitespaces()
|
||||
val value = valueReader(this, key.unsafeCast())
|
||||
map[key.result] = value
|
||||
val end = read()
|
||||
if (end == ']'.code) {
|
||||
break
|
||||
}
|
||||
if (end != ','.code) {
|
||||
throw InvalidMapSeparatorError(this, pointer - 1, end)
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return string.substring(pointer, string.length)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val STRING_QUOTE = '"'.code
|
||||
const val STRING_SINGLE_QUOTE = '\''.code
|
||||
|
@ -13,15 +13,21 @@
|
||||
|
||||
package de.bixilon.minosoft.commands.parser.minecraft.target
|
||||
|
||||
import de.bixilon.minosoft.commands.errors.reader.map.DuplicatedKeyMapError
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.identifier.name.InvalidNameError
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.identifier.name.NameEntityTarget
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.identifier.uuid.InvalidUUIDError
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.identifier.uuid.UUIDEntityTarget
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.SelectorEntityTarget
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.GamemodeProperty
|
||||
import de.bixilon.minosoft.commands.parser.minecraft.target.targets.selector.properties.NameProperty
|
||||
import de.bixilon.minosoft.commands.util.CommandReader
|
||||
import de.bixilon.minosoft.data.abilities.Gamemodes
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
|
||||
internal class TargetParserTest {
|
||||
@ -75,4 +81,61 @@ internal class TargetParserTest {
|
||||
val parser = TargetParser()
|
||||
assertThrows<InvalidUUIDError> { parser.parse(reader) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSelectorDetection() {
|
||||
val reader = CommandReader("@a")
|
||||
val parser = TargetParser()
|
||||
val parsed = parser.parse(reader)
|
||||
assert(parsed is SelectorEntityTarget)
|
||||
parsed as SelectorEntityTarget
|
||||
}
|
||||
|
||||
private fun getSelector(command: String): SelectorEntityTarget {
|
||||
val reader = CommandReader(command)
|
||||
val parser = TargetParser()
|
||||
val parsed = parser.parse(reader)
|
||||
return parsed as SelectorEntityTarget
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleSelector() {
|
||||
val parsed = getSelector("@a")
|
||||
assertEquals(parsed.selector, TargetSelectors.ALL_PLAYERS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPropertiesEmpty() {
|
||||
val parsed = getSelector("@a")
|
||||
assertTrue(parsed.properties.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptyPropertiesEmpty() {
|
||||
val parsed = getSelector("@a[]")
|
||||
assertTrue(parsed.properties.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSingleProperties() {
|
||||
val parsed = getSelector("@a[name=Test]")
|
||||
assertEquals(parsed.properties.size, 1)
|
||||
assertTrue(parsed.properties["name"] is NameProperty)
|
||||
assertTrue((parsed.properties["name"] as NameProperty).name == "Test")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleProperties() {
|
||||
val parsed = getSelector("@a[name=Test,gamemode=creative]")
|
||||
assertEquals(parsed.properties.size, 2)
|
||||
assertTrue(parsed.properties["name"] is NameProperty)
|
||||
assertEquals((parsed.properties["name"] as NameProperty).name, "Test")
|
||||
assertTrue(parsed.properties["gamemode"] is GamemodeProperty)
|
||||
assertEquals((parsed.properties["gamemode"] as GamemodeProperty).gamemode, Gamemodes.CREATIVE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDuplicatedProperties() {
|
||||
assertThrows<DuplicatedKeyMapError> { getSelector("@a[name=Test,name=Test2]") }
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user