Add ordered waypoints (ColeWeight compat)
This commit is contained in:
@@ -13,6 +13,7 @@ import io.ktor.client.statement.*
|
|||||||
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
|
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
|
||||||
import net.minecraft.text.Text
|
import net.minecraft.text.Text
|
||||||
import moe.nea.firmament.apis.UrsaManager
|
import moe.nea.firmament.apis.UrsaManager
|
||||||
|
import moe.nea.firmament.events.CommandEvent
|
||||||
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
|
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
|
||||||
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
|
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
|
||||||
import moe.nea.firmament.features.world.FairySouls
|
import moe.nea.firmament.features.world.FairySouls
|
||||||
@@ -218,6 +219,7 @@ fun firmamentCommand() = literal("firmament") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CommandEvent.SubCommand.publish(CommandEvent.SubCommand(this@literal))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
|
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
|
||||||
|
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
@@ -12,6 +13,7 @@ import net.minecraft.command.CommandRegistryAccess
|
|||||||
import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode
|
import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode
|
||||||
import moe.nea.firmament.commands.DefaultSource
|
import moe.nea.firmament.commands.DefaultSource
|
||||||
import moe.nea.firmament.commands.literal
|
import moe.nea.firmament.commands.literal
|
||||||
|
import moe.nea.firmament.commands.thenLiteral
|
||||||
|
|
||||||
data class CommandEvent(
|
data class CommandEvent(
|
||||||
val dispatcher: CommandDispatcher<DefaultSource>,
|
val dispatcher: CommandDispatcher<DefaultSource>,
|
||||||
@@ -20,6 +22,20 @@ data class CommandEvent(
|
|||||||
) : FirmamentEvent() {
|
) : FirmamentEvent() {
|
||||||
companion object : FirmamentEventBus<CommandEvent>()
|
companion object : FirmamentEventBus<CommandEvent>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register subcommands to `/firm`. For new top level commands use [CommandEvent]. Cannot be used to register
|
||||||
|
* subcommands to other commands.
|
||||||
|
*/
|
||||||
|
data class SubCommand(
|
||||||
|
val builder: CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>,
|
||||||
|
) : FirmamentEvent() {
|
||||||
|
companion object : FirmamentEventBus<SubCommand>()
|
||||||
|
|
||||||
|
fun subcommand(name: String, block: CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>.() -> Unit) {
|
||||||
|
builder.thenLiteral(name, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteCommand(name: String) {
|
fun deleteCommand(name: String) {
|
||||||
dispatcher.root.children.removeIf { it.name.equals(name, ignoreCase = false) }
|
dispatcher.root.children.removeIf { it.name.equals(name, ignoreCase = false) }
|
||||||
serverCommands?.root?.children?.removeIf { it.name.equals(name, ignoreCase = false) }
|
serverCommands?.root?.children?.removeIf { it.name.equals(name, ignoreCase = false) }
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
package moe.nea.firmament.features.diana
|
package moe.nea.firmament.features.diana
|
||||||
|
|
||||||
import org.joml.Vector3f
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import net.minecraft.particle.ParticleTypes
|
import net.minecraft.particle.ParticleTypes
|
||||||
import net.minecraft.sound.SoundEvents
|
import net.minecraft.sound.SoundEvents
|
||||||
@@ -33,6 +32,7 @@ object AncestralSpadeSolver {
|
|||||||
|
|
||||||
fun isEnabled() =
|
fun isEnabled() =
|
||||||
DianaWaypoints.TConfig.ancestralSpadeSolver && SBData.skyblockLocation == "hub"
|
DianaWaypoints.TConfig.ancestralSpadeSolver && SBData.skyblockLocation == "hub"
|
||||||
|
|
||||||
fun onKeyBind(event: WorldKeyboardEvent) {
|
fun onKeyBind(event: WorldKeyboardEvent) {
|
||||||
if (!isEnabled()) return
|
if (!isEnabled()) return
|
||||||
if (!event.matches(DianaWaypoints.TConfig.ancestralSpadeTeleport)) return
|
if (!event.matches(DianaWaypoints.TConfig.ancestralSpadeTeleport)) return
|
||||||
@@ -99,8 +99,7 @@ object AncestralSpadeSolver {
|
|||||||
color(1f, 1f, 0f, 0.5f)
|
color(1f, 1f, 0f, 0.5f)
|
||||||
tinyBlock(it, 1f)
|
tinyBlock(it, 1f)
|
||||||
color(1f, 1f, 0f, 1f)
|
color(1f, 1f, 0f, 1f)
|
||||||
val cameraForward = Vector3f(0f, 0f, 1f).rotate(event.camera.rotation)
|
tracer(it, lineWidth = 3f)
|
||||||
line(event.camera.pos.add(Vec3d(cameraForward)), it, lineWidth = 3f)
|
|
||||||
}
|
}
|
||||||
if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) {
|
if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) {
|
||||||
color(0f, 1f, 0f, 0.7f)
|
color(0f, 1f, 0f, 0.7f)
|
||||||
|
|||||||
@@ -1,20 +1,38 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
|
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
|
||||||
|
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package moe.nea.firmament.features.world
|
package moe.nea.firmament.features.world
|
||||||
|
|
||||||
|
import me.shedaniel.math.Color
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlin.collections.component1
|
||||||
|
import kotlin.collections.component2
|
||||||
|
import kotlin.collections.set
|
||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import net.minecraft.command.argument.BlockPosArgumentType
|
||||||
|
import net.minecraft.server.command.ServerCommandSource
|
||||||
import net.minecraft.text.Text
|
import net.minecraft.text.Text
|
||||||
import net.minecraft.util.math.BlockPos
|
import net.minecraft.util.math.BlockPos
|
||||||
|
import net.minecraft.util.math.Vec3d
|
||||||
|
import moe.nea.firmament.Firmament
|
||||||
|
import moe.nea.firmament.commands.thenArgument
|
||||||
|
import moe.nea.firmament.commands.thenExecute
|
||||||
|
import moe.nea.firmament.commands.thenLiteral
|
||||||
|
import moe.nea.firmament.events.CommandEvent
|
||||||
import moe.nea.firmament.events.ProcessChatEvent
|
import moe.nea.firmament.events.ProcessChatEvent
|
||||||
|
import moe.nea.firmament.events.TickEvent
|
||||||
import moe.nea.firmament.events.WorldReadyEvent
|
import moe.nea.firmament.events.WorldReadyEvent
|
||||||
import moe.nea.firmament.events.WorldRenderLastEvent
|
import moe.nea.firmament.events.WorldRenderLastEvent
|
||||||
import moe.nea.firmament.features.FirmamentFeature
|
import moe.nea.firmament.features.FirmamentFeature
|
||||||
import moe.nea.firmament.gui.config.ManagedConfig
|
import moe.nea.firmament.gui.config.ManagedConfig
|
||||||
|
import moe.nea.firmament.util.ClipboardUtils
|
||||||
import moe.nea.firmament.util.MC
|
import moe.nea.firmament.util.MC
|
||||||
import moe.nea.firmament.util.TimeMark
|
import moe.nea.firmament.util.TimeMark
|
||||||
import moe.nea.firmament.util.render.RenderInWorldContext
|
import moe.nea.firmament.util.render.RenderInWorldContext
|
||||||
@@ -34,19 +52,34 @@ object Waypoints : FirmamentFeature {
|
|||||||
|
|
||||||
override val config get() = TConfig
|
override val config get() = TConfig
|
||||||
|
|
||||||
val temporaryWaypointList = mutableMapOf<String, TemporaryWaypoint>()
|
val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
|
||||||
val temporaryWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
|
val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
|
||||||
|
|
||||||
|
val waypoints = mutableListOf<BlockPos>()
|
||||||
|
var ordered = false
|
||||||
|
var orderedIndex = 0
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ColeWeightWaypoint(
|
||||||
|
val x: Int,
|
||||||
|
val y: Int,
|
||||||
|
val z: Int,
|
||||||
|
val r: Int = 0,
|
||||||
|
val g: Int = 0,
|
||||||
|
val b: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
override fun onLoad() {
|
override fun onLoad() {
|
||||||
WorldRenderLastEvent.subscribe { event ->
|
WorldRenderLastEvent.subscribe { event ->
|
||||||
temporaryWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
|
temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
|
||||||
if (temporaryWaypointList.isNotEmpty())
|
if (temporaryPlayerWaypointList.isNotEmpty())
|
||||||
RenderInWorldContext.renderInWorld(event) {
|
RenderInWorldContext.renderInWorld(event) {
|
||||||
color(1f, 1f, 0f, 1f)
|
color(1f, 1f, 0f, 1f)
|
||||||
temporaryWaypointList.forEach { (player, waypoint) ->
|
temporaryPlayerWaypointList.forEach { (player, waypoint) ->
|
||||||
block(waypoint.pos)
|
block(waypoint.pos)
|
||||||
}
|
}
|
||||||
color(1f, 1f, 1f, 1f)
|
color(1f, 1f, 1f, 1f)
|
||||||
temporaryWaypointList.forEach { (player, waypoint) ->
|
temporaryPlayerWaypointList.forEach { (player, waypoint) ->
|
||||||
val skin =
|
val skin =
|
||||||
MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }
|
MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }
|
||||||
?.skinTextures
|
?.skinTextures
|
||||||
@@ -73,12 +106,106 @@ object Waypoints : FirmamentFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorldReadyEvent.subscribe {
|
WorldReadyEvent.subscribe {
|
||||||
temporaryWaypointList.clear()
|
temporaryPlayerWaypointList.clear()
|
||||||
|
}
|
||||||
|
CommandEvent.SubCommand.subscribe { event ->
|
||||||
|
event.subcommand("waypoint") {
|
||||||
|
thenArgument("pos", BlockPosArgumentType.blockPos()) { pos ->
|
||||||
|
thenExecute {
|
||||||
|
val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer())
|
||||||
|
waypoints.add(position)
|
||||||
|
source.sendFeedback(
|
||||||
|
Text.stringifiedTranslatable(
|
||||||
|
"firmament.command.waypoint.added",
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
position.z
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.subcommand("waypoints") {
|
||||||
|
thenLiteral("clear") {
|
||||||
|
thenExecute {
|
||||||
|
waypoints.clear()
|
||||||
|
source.sendFeedback(Text.translatable("firmament.command.waypoint.clear"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thenLiteral("toggleordered") {
|
||||||
|
thenExecute {
|
||||||
|
ordered = !ordered
|
||||||
|
if (ordered) {
|
||||||
|
val p = MC.player?.pos ?: Vec3d.ZERO
|
||||||
|
orderedIndex =
|
||||||
|
waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0
|
||||||
|
}
|
||||||
|
source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thenLiteral("import") {
|
||||||
|
thenExecute {
|
||||||
|
val contents = ClipboardUtils.getTextContents()
|
||||||
|
val data = try {
|
||||||
|
Firmament.json.decodeFromString<List<ColeWeightWaypoint>>(contents)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Firmament.logger.error("Could not load waypoints from clipboard", ex)
|
||||||
|
source.sendError(Text.translatable("firmament.command.waypoint.import.error"))
|
||||||
|
return@thenExecute
|
||||||
|
}
|
||||||
|
waypoints.clear()
|
||||||
|
data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) }
|
||||||
|
source.sendFeedback(
|
||||||
|
Text.stringifiedTranslatable(
|
||||||
|
"firmament.command.waypoint.import",
|
||||||
|
data.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WorldRenderLastEvent.subscribe { event ->
|
||||||
|
if (waypoints.isEmpty()) return@subscribe
|
||||||
|
RenderInWorldContext.renderInWorld(event) {
|
||||||
|
if (!ordered) {
|
||||||
|
color(0f, 0.3f, 0.7f, 0.5f)
|
||||||
|
waypoints.forEach {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
orderedIndex %= waypoints.size
|
||||||
|
val firstColor = Color.ofRGBA(0, 200, 40, 180)
|
||||||
|
color(firstColor)
|
||||||
|
tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f)
|
||||||
|
waypoints.wrappingWindow(orderedIndex, 3)
|
||||||
|
.zip(
|
||||||
|
listOf(
|
||||||
|
firstColor,
|
||||||
|
Color.ofRGBA(180, 200, 40, 150),
|
||||||
|
Color.ofRGBA(180, 80, 20, 140),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.reversed()
|
||||||
|
.forEach { (pos, col) ->
|
||||||
|
color(col)
|
||||||
|
block(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TickEvent.subscribe {
|
||||||
|
if (waypoints.isEmpty() || !ordered) return@subscribe
|
||||||
|
orderedIndex %= waypoints.size
|
||||||
|
val p = MC.player?.pos ?: return@subscribe
|
||||||
|
if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) {
|
||||||
|
orderedIndex = (orderedIndex + 1) % waypoints.size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ProcessChatEvent.subscribe {
|
ProcessChatEvent.subscribe {
|
||||||
val matcher = temporaryWaypointMatcher.matcher(it.unformattedString)
|
val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString)
|
||||||
if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
|
if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
|
||||||
temporaryWaypointList[it.nameHeuristic] = TemporaryWaypoint(
|
temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(
|
||||||
BlockPos(
|
BlockPos(
|
||||||
matcher.group(1).toInt(),
|
matcher.group(1).toInt(),
|
||||||
matcher.group(2).toInt(),
|
matcher.group(2).toInt(),
|
||||||
@@ -90,3 +217,30 @@ object Waypoints : FirmamentFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
|
||||||
|
val result = ArrayList<E>(windowSize)
|
||||||
|
if (startIndex + windowSize < size) {
|
||||||
|
result.addAll(subList(startIndex, startIndex + windowSize))
|
||||||
|
} else {
|
||||||
|
result.addAll(subList(startIndex, size))
|
||||||
|
result.addAll(subList(0, minOf(size - startIndex - windowSize, startIndex)))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun FabricClientCommandSource.asFakeServer(): ServerCommandSource {
|
||||||
|
val source = this
|
||||||
|
return ServerCommandSource(
|
||||||
|
source.player,
|
||||||
|
source.position,
|
||||||
|
source.rotation,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
"FakeServerCommandSource",
|
||||||
|
Text.literal("FakeServerCommandSource"),
|
||||||
|
null,
|
||||||
|
source.player
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
|
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
|
||||||
|
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
@@ -44,6 +45,10 @@ class RenderInWorldContext private constructor(
|
|||||||
val effectiveFov = (MC.instance.gameRenderer as AccessorGameRenderer).getFov_firmament(camera, tickDelta, true)
|
val effectiveFov = (MC.instance.gameRenderer as AccessorGameRenderer).getFov_firmament(camera, tickDelta, true)
|
||||||
val effectiveFovScaleFactor = 1 / tan(toRadians(effectiveFov) / 2)
|
val effectiveFovScaleFactor = 1 / tan(toRadians(effectiveFov) / 2)
|
||||||
|
|
||||||
|
fun color(color: me.shedaniel.math.Color) {
|
||||||
|
color(color.red / 255F, color.green / 255f, color.blue / 255f, color.alpha / 255f)
|
||||||
|
}
|
||||||
|
|
||||||
fun color(red: Float, green: Float, blue: Float, alpha: Float) {
|
fun color(red: Float, green: Float, blue: Float, alpha: Float) {
|
||||||
RenderSystem.setShaderColor(red, green, blue, alpha)
|
RenderSystem.setShaderColor(red, green, blue, alpha)
|
||||||
}
|
}
|
||||||
@@ -140,6 +145,11 @@ class RenderInWorldContext private constructor(
|
|||||||
line(points.toList(), lineWidth)
|
line(points.toList(), lineWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun tracer(toWhere: Vec3d, lineWidth: Float = 3f) {
|
||||||
|
val cameraForward = Vector3f(0f, 0f, 1f).rotate(camera.rotation)
|
||||||
|
line(camera.pos.add(Vec3d(cameraForward)), toWhere, lineWidth = lineWidth)
|
||||||
|
}
|
||||||
|
|
||||||
fun line(points: List<Vec3d>, lineWidth: Float = 10F) {
|
fun line(points: List<Vec3d>, lineWidth: Float = 10F) {
|
||||||
RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram)
|
RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram)
|
||||||
RenderSystem.lineWidth(lineWidth / pow(camera.pos.squaredDistanceTo(points.first()), 0.25).toFloat())
|
RenderSystem.lineWidth(lineWidth / pow(camera.pos.squaredDistanceTo(points.first()), 0.25).toFloat())
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"firmament.command.toggle.no-property-found": "Could not find property %s",
|
"firmament.command.toggle.no-property-found": "Could not find property %s",
|
||||||
"firmament.command.toggle.not-a-toggle": "Property %s is not a toggle",
|
"firmament.command.toggle.not-a-toggle": "Property %s is not a toggle",
|
||||||
"firmament.command.toggle.toggled": "Toggled %s / %s %s",
|
"firmament.command.toggle.toggled": "Toggled %s / %s %s",
|
||||||
|
"firmament.command.waypoint.import": "Imported %s waypoints from clipboard.",
|
||||||
|
"firmament.command.waypoint.clear": "Cleared waypoints.",
|
||||||
|
"firmament.command.waypoint.added": "Added waypoint %s %s %s.",
|
||||||
|
"firmament.command.waypoint.import.error": "Could not import waypoints. Make sure they are on ColeWeight format:\n[{\"x\": 69, \"y\":420, \"z\": 36}]",
|
||||||
"firmament.pristine-profit.collection": "Collection: %s/h",
|
"firmament.pristine-profit.collection": "Collection: %s/h",
|
||||||
"firmament.pristine-profit.money": "Money: %s/h",
|
"firmament.pristine-profit.money": "Money: %s/h",
|
||||||
"firmament.toggle.true": "On",
|
"firmament.toggle.true": "On",
|
||||||
|
|||||||
Reference in New Issue
Block a user