mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-10 15:04:03 +09:00
even more ui
This commit is contained in:
14
tsvm_executable/src/net/torvald/tsvm/EmuMenu.kt
Normal file
14
tsvm_executable/src/net/torvald/tsvm/EmuMenu.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package net.torvald.tsvm
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by minjaesong on 2022-10-25.
|
||||||
|
*/
|
||||||
|
interface EmuMenu {
|
||||||
|
|
||||||
|
fun update()
|
||||||
|
|
||||||
|
fun render(batch: SpriteBatch)
|
||||||
|
|
||||||
|
}
|
||||||
20
tsvm_executable/src/net/torvald/tsvm/ProfilesMenu.kt
Normal file
20
tsvm_executable/src/net/torvald/tsvm/ProfilesMenu.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package net.torvald.tsvm
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color
|
||||||
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by minjaesong on 2022-10-25.
|
||||||
|
*/
|
||||||
|
class ProfilesMenu(val w: Int, val h: Int) : EmuMenu {
|
||||||
|
|
||||||
|
override fun update() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun render(batch: SpriteBatch) {
|
||||||
|
batch.inUse {
|
||||||
|
batch.color = Color.LIME
|
||||||
|
batch.fillRect(0, 0, w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,9 @@ package net.torvald.tsvm;
|
|||||||
|
|
||||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
|
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
|
||||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
|
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
|
||||||
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
|
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
|
||||||
import net.torvald.tsvm.peripheral.*;
|
import com.badlogic.gdx.Gdx;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by minjaesong on 2022-10-22.
|
* Created by minjaesong on 2022-10-22.
|
||||||
@@ -19,6 +18,8 @@ public class TsvmEmulator {
|
|||||||
public static int WIDTH = 640 * 2;
|
public static int WIDTH = 640 * 2;
|
||||||
public static int HEIGHT = 480 * 2;
|
public static int HEIGHT = 480 * 2;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
ShaderProgram.pedantic = false;
|
ShaderProgram.pedantic = false;
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ public class TsvmEmulator {
|
|||||||
|
|
||||||
appConfig.setWindowedMode(WIDTH, HEIGHT);
|
appConfig.setWindowedMode(WIDTH, HEIGHT);
|
||||||
|
|
||||||
new Lwjgl3Application(new VMEmuExecutable(640, 480, 2, 2,"assets/"), appConfig);
|
new Lwjgl3Application(new VMEmuExecutableWrapper(640, 480, 2, 2,"assets/"), appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,60 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.torvald.terrarum.FlippingSpriteBatch
|
import net.torvald.terrarum.FlippingSpriteBatch
|
||||||
import net.torvald.terrarum.imagefont.TinyAlphNum
|
import net.torvald.terrarum.imagefont.TinyAlphNum
|
||||||
|
import net.torvald.tsvm.VMEmuExecutableWrapper.Companion.FONT
|
||||||
|
import net.torvald.tsvm.VMEmuExecutableWrapper.Companion.SQTEX
|
||||||
import net.torvald.tsvm.peripheral.GraphicsAdapter
|
import net.torvald.tsvm.peripheral.GraphicsAdapter
|
||||||
import net.torvald.tsvm.peripheral.ReferenceGraphicsAdapter2
|
import net.torvald.tsvm.peripheral.ReferenceGraphicsAdapter2
|
||||||
import net.torvald.tsvm.peripheral.TestDiskDrive
|
import net.torvald.tsvm.peripheral.TestDiskDrive
|
||||||
import net.torvald.tsvm.peripheral.TsvmBios
|
import net.torvald.tsvm.peripheral.TsvmBios
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
class VMEmuExecutableWrapper(val windowWidth: Int, val windowHeight: Int, var panelsX: Int, var panelsY: Int, val diskPathRoot: String) : ApplicationAdapter() {
|
||||||
|
|
||||||
|
private lateinit var executable: VMEmuExecutable
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
lateinit var SQTEX: Texture; private set
|
||||||
|
lateinit var FONT: TinyAlphNum; private set
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create() {
|
||||||
|
FONT = TinyAlphNum
|
||||||
|
SQTEX = Texture(Gdx.files.internal("net/torvald/tsvm/sq.tga"))
|
||||||
|
executable = VMEmuExecutable(windowWidth, windowHeight, panelsX, panelsY, diskPathRoot)
|
||||||
|
executable.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resize(width: Int, height: Int) {
|
||||||
|
executable.resize(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun render() {
|
||||||
|
executable.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pause() {
|
||||||
|
executable.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resume() {
|
||||||
|
executable.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
executable.dispose()
|
||||||
|
SQTEX.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by minjaesong on 2022-10-22.
|
* Created by minjaesong on 2022-10-22.
|
||||||
*/
|
*/
|
||||||
class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: Int, var panelsY: Int, val diskPathRoot: String) : ApplicationAdapter() {
|
class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: Int, var panelsY: Int, val diskPathRoot: String) : ApplicationAdapter() {
|
||||||
|
|
||||||
|
|
||||||
private data class VMRunnerInfo(val vm: VM, val name: String)
|
private data class VMRunnerInfo(val vm: VM, val name: String)
|
||||||
|
|
||||||
private val vms = arrayOfNulls<VMRunnerInfo>(this.panelsX * this.panelsY - 1) // index: # of the window where the reboot was requested
|
private val vms = arrayOfNulls<VMRunnerInfo>(this.panelsX * this.panelsY - 1) // index: # of the window where the reboot was requested
|
||||||
@@ -36,25 +77,16 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
var vmRunners = HashMap<Int, VMRunner>() // <VM's identifier, VMRunner>
|
var vmRunners = HashMap<Int, VMRunner>() // <VM's identifier, VMRunner>
|
||||||
var coroutineJobs = HashMap<Int, Job>() // <VM's identifier, Job>
|
var coroutineJobs = HashMap<Int, Job>() // <VM's identifier, Job>
|
||||||
|
|
||||||
lateinit var fullscreenQuad: Mesh
|
val fullscreenQuad = Mesh(
|
||||||
|
true, 4, 6,
|
||||||
private lateinit var sqtex: Texture
|
VertexAttribute.Position(),
|
||||||
|
VertexAttribute.ColorUnpacked(),
|
||||||
private lateinit var font: TinyAlphNum
|
VertexAttribute.TexCoords(0)
|
||||||
|
)
|
||||||
|
|
||||||
override fun create() {
|
override fun create() {
|
||||||
super.create()
|
super.create()
|
||||||
|
|
||||||
sqtex = Texture(Gdx.files.internal("net/torvald/tsvm/sq.tga"))
|
|
||||||
|
|
||||||
font = TinyAlphNum
|
|
||||||
|
|
||||||
fullscreenQuad = Mesh(
|
|
||||||
true, 4, 6,
|
|
||||||
VertexAttribute.Position(),
|
|
||||||
VertexAttribute.ColorUnpacked(),
|
|
||||||
VertexAttribute.TexCoords(0)
|
|
||||||
)
|
|
||||||
updateFullscreenQuad(AppLoader.WIDTH, AppLoader.HEIGHT)
|
updateFullscreenQuad(AppLoader.WIDTH, AppLoader.HEIGHT)
|
||||||
|
|
||||||
batch = SpriteBatch()
|
batch = SpriteBatch()
|
||||||
@@ -109,7 +141,8 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
private fun setCameraPosition(newX: Float, newY: Float) {
|
private fun setCameraPosition(newX: Float, newY: Float) {
|
||||||
camera.position.set((-newX + AppLoader.WIDTH / 2), (-newY + AppLoader.HEIGHT / 2), 0f) // deliberate integer division
|
camera.position.set((-newX + AppLoader.WIDTH / 2), (-newY + AppLoader.HEIGHT / 2), 0f) // deliberate integer division
|
||||||
camera.update()
|
camera.update()
|
||||||
batch.setProjectionMatrix(camera.combined)
|
batch.projectionMatrix = camera.combined
|
||||||
|
fbatch.projectionMatrix = camera.combined
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun gdxClearAndSetBlend(r: Float, g: Float, b: Float, a: Float) {
|
private fun gdxClearAndSetBlend(r: Float, g: Float, b: Float, a: Float) {
|
||||||
@@ -178,6 +211,8 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
if (it?.vm?.resetDown == true && index == currentVMselection) { reboot(it.vm) }
|
if (it?.vm?.resetDown == true && index == currentVMselection) { reboot(it.vm) }
|
||||||
it?.vm?.update(delta)
|
it?.vm?.update(delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defaultGuiBackgroundColour = Color(0x303039ff)
|
private val defaultGuiBackgroundColour = Color(0x303039ff)
|
||||||
@@ -197,9 +232,9 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
it.fillRect(xoff + windowWidth - 2, yoff, 2, windowHeight)
|
it.fillRect(xoff + windowWidth - 2, yoff, 2, windowHeight)
|
||||||
|
|
||||||
vmInfo?.name?.let { name ->
|
vmInfo?.name?.let { name ->
|
||||||
it.fillRect(xoff, yoff, (name.length + 2) * font.W, font.H)
|
it.fillRect(xoff, yoff, (name.length + 2) * FONT.W, FONT.H)
|
||||||
it.color = if (index == currentVMselection) EmulatorGuiToolkit.Theme.COL_ACTIVE else EmulatorGuiToolkit.Theme.COL_ACTIVE2
|
it.color = if (index == currentVMselection) EmulatorGuiToolkit.Theme.COL_ACTIVE else EmulatorGuiToolkit.Theme.COL_ACTIVE2
|
||||||
font.draw(it, name, xoff + font.W.toFloat(), yoff.toFloat())
|
FONT.draw(it, name, xoff + FONT.W.toFloat(), yoff.toFloat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,7 +267,7 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
fbatch.fillRect(pposX * windowWidth, pposY * windowHeight, windowWidth, windowHeight)
|
fbatch.fillRect(pposX * windowWidth, pposY * windowHeight, windowWidth, windowHeight)
|
||||||
// draw text
|
// draw text
|
||||||
fbatch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE
|
fbatch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE
|
||||||
font.draw(fbatch, "no graphics device available", xoff + (windowWidth - 196) / 2, yoff + (windowHeight - 12) / 2)
|
FONT.draw(fbatch, "no graphics device available", xoff + (windowWidth - 196) / 2, yoff + (windowHeight - 12) / 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +279,7 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
fbatch.fillRect(pposX * windowWidth, pposY * windowHeight, windowWidth, windowHeight)
|
fbatch.fillRect(pposX * windowWidth, pposY * windowHeight, windowWidth, windowHeight)
|
||||||
// draw text
|
// draw text
|
||||||
fbatch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE
|
fbatch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE
|
||||||
font.draw(fbatch, "no vm on this viewport", xoff + (windowWidth - 154) / 2, yoff + (windowHeight - 12) / 2)
|
FONT.draw(fbatch, "no vm on this viewport", xoff + (windowWidth - 154) / 2, yoff + (windowHeight - 12) / 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +306,6 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
super.dispose()
|
super.dispose()
|
||||||
sqtex.dispose()
|
|
||||||
batch.dispose()
|
batch.dispose()
|
||||||
fbatch.dispose()
|
fbatch.dispose()
|
||||||
fullscreenQuad.dispose()
|
fullscreenQuad.dispose()
|
||||||
@@ -279,58 +313,63 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
vms.forEach { it?.vm?.dispose() }
|
vms.forEach { it?.vm?.dispose() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SpriteBatch.fillRect(x: Int, y: Int, w: Int, h: Int) = this.draw(sqtex, x.toFloat(), y.toFloat(), w.toFloat(), h.toFloat())
|
private val menuTabW = windowWidth - 4
|
||||||
fun SpriteBatch.fillRect(x: Float, y: Float, w: Float, h: Float) = this.draw(sqtex, x, y, w, h)
|
private val menuTabH = windowHeight - 4 - FONT.H
|
||||||
fun SpriteBatch.fillRect(x: Int, y: Int, w: Float, h: Float) = this.draw(sqtex, x.toFloat(), y.toFloat(), w, h)
|
|
||||||
fun SpriteBatch.fillRect(x: Float, y: Float, w: Int, h: Int) = this.draw(sqtex, x, y, w.toFloat(), h.toFloat())
|
|
||||||
|
|
||||||
fun SpriteBatch.inUse(f: (SpriteBatch) -> Unit) {
|
private val menuTabs = listOf("Profiles", "Machine", "Peripherals", "Cards")
|
||||||
this.begin()
|
|
||||||
f(this)
|
|
||||||
this.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val menuTabs = listOf("Machine", "Peripherals", "Cards")
|
|
||||||
private val tabPos = (menuTabs + "").mapIndexed { index, _ -> 1 + menuTabs.subList(0, index).sumBy { it.length } + 2 * index }
|
private val tabPos = (menuTabs + "").mapIndexed { index, _ -> 1 + menuTabs.subList(0, index).sumBy { it.length } + 2 * index }
|
||||||
|
private val tabs = listOf(ProfilesMenu(menuTabW, menuTabH))
|
||||||
private var menuTabSel = 0
|
private var menuTabSel = 0
|
||||||
|
private val profilesPath = "profiles.json"
|
||||||
|
private val configPath = "config.json"
|
||||||
|
|
||||||
private fun drawMenu(batch: SpriteBatch, x: Float, y: Float) {
|
private fun drawMenu(batch: SpriteBatch, x: Float, y: Float) {
|
||||||
batch.inUse {
|
batch.inUse {
|
||||||
|
// background for the entire area
|
||||||
|
batch.color = defaultGuiBackgroundColour
|
||||||
|
batch.fillRect(x, y, windowWidth, windowHeight)
|
||||||
|
|
||||||
// draw the tab
|
// draw the tab
|
||||||
for (k in menuTabs.indices) {
|
for (k in menuTabs.indices) {
|
||||||
|
|
||||||
val textX = x + font.W * tabPos[k]
|
val textX = x + FONT.W * tabPos[k]
|
||||||
|
|
||||||
if (k == menuTabSel) {
|
if (k == menuTabSel) {
|
||||||
batch.color = EmulatorGuiToolkit.Theme.COL_HIGHLIGHT
|
batch.color = EmulatorGuiToolkit.Theme.COL_HIGHLIGHT
|
||||||
batch.fillRect(textX - font.W, y, font.W * (menuTabs[k].length + 2f), font.H.toFloat())
|
batch.fillRect(textX - FONT.W, y, FONT.W * (menuTabs[k].length + 2f), FONT.H.toFloat())
|
||||||
|
|
||||||
batch.color = EmulatorGuiToolkit.Theme.COL_ACTIVE
|
batch.color = EmulatorGuiToolkit.Theme.COL_ACTIVE
|
||||||
font.draw(batch, menuTabs[k], textX, y)
|
FONT.draw(batch, menuTabs[k], textX, y)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
batch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE
|
batch.color = EmulatorGuiToolkit.Theme.COL_TAB_NOT_SELECTED
|
||||||
batch.fillRect(textX - font.W, y, font.W * (menuTabs[k].length + 2f), font.H.toFloat())
|
batch.fillRect(textX - FONT.W, y, FONT.W * (menuTabs[k].length + 2f), FONT.H.toFloat())
|
||||||
|
|
||||||
batch.color = EmulatorGuiToolkit.Theme.COL_ACTIVE2
|
batch.color = EmulatorGuiToolkit.Theme.COL_ACTIVE2
|
||||||
font.draw(batch, menuTabs[k], textX, y)
|
FONT.draw(batch, menuTabs[k], textX, y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// tab edge
|
|
||||||
batch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE2
|
|
||||||
val edgeX = x + (tabPos.last() - 1) * font.W
|
|
||||||
val edgeW = windowWidth - (tabPos.last() - 1) * font.W
|
|
||||||
batch.fillRect(edgeX, y, edgeW, font.H)
|
|
||||||
|
|
||||||
// draw the window frame inside the tab
|
// draw the window frame inside the tab
|
||||||
batch.color = defaultGuiBackgroundColour
|
batch.color = EmulatorGuiToolkit.Theme.COL_INACTIVE
|
||||||
batch.fillRect(x, y + font.H, windowWidth, windowHeight - font.H)
|
batch.fillRect(x, y + FONT.H, windowWidth, windowHeight - FONT.H)
|
||||||
batch.color = EmulatorGuiToolkit.Theme.COL_HIGHLIGHT
|
batch.color = EmulatorGuiToolkit.Theme.COL_HIGHLIGHT
|
||||||
batch.fillRect(x, y + font.H, windowWidth.toFloat(), 2f)
|
batch.fillRect(x, y + FONT.H, windowWidth.toFloat(), 2f)
|
||||||
batch.fillRect(x, y + windowHeight - 2f, windowWidth.toFloat(), 2f)
|
batch.fillRect(x, y + windowHeight - 2f, windowWidth.toFloat(), 2f)
|
||||||
batch.fillRect(x, y + font.H, 2f, windowHeight - font.H - 2f)
|
batch.fillRect(x, y + FONT.H, 2f, windowHeight - FONT.H - 2f)
|
||||||
batch.fillRect(x + windowWidth - 2f, y + font.H, 2f, windowHeight - font.H - 2f)
|
batch.fillRect(x + windowWidth - 2f, y + FONT.H, 2f, windowHeight - FONT.H - 2f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCameraPosition(windowWidth * (panelsX-1) + 2f, windowHeight * (panelsY-1) + FONT.H + 2f)
|
||||||
|
tabs[menuTabSel].render(batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMenu() {
|
||||||
|
// update the tab
|
||||||
|
|
||||||
|
|
||||||
|
// actually update the view within the tabs
|
||||||
|
tabs[menuTabSel].update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +382,20 @@ object EmulatorGuiToolkit {
|
|||||||
val COL_ACTIVE2 = Color(0xfff600ff.toInt()) // yellow
|
val COL_ACTIVE2 = Color(0xfff600ff.toInt()) // yellow
|
||||||
val COL_HIGHLIGHT = Color(0xe43380ff.toInt()) // magenta
|
val COL_HIGHLIGHT = Color(0xe43380ff.toInt()) // magenta
|
||||||
val COL_DISABLED = Color(0xaaaaaaff.toInt())
|
val COL_DISABLED = Color(0xaaaaaaff.toInt())
|
||||||
|
|
||||||
|
val COL_TAB_NOT_SELECTED = Color(0x503cd4ff) // dark blue
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun SpriteBatch.fillRect(x: Int, y: Int, w: Int, h: Int) = this.draw(SQTEX, x.toFloat(), y.toFloat(), w.toFloat(), h.toFloat())
|
||||||
|
fun SpriteBatch.fillRect(x: Float, y: Float, w: Float, h: Float) = this.draw(SQTEX, x, y, w, h)
|
||||||
|
fun SpriteBatch.fillRect(x: Int, y: Int, w: Float, h: Float) = this.draw(SQTEX, x.toFloat(), y.toFloat(), w, h)
|
||||||
|
fun SpriteBatch.fillRect(x: Float, y: Float, w: Int, h: Int) = this.draw(SQTEX, x, y, w.toFloat(), h.toFloat())
|
||||||
|
|
||||||
|
fun SpriteBatch.inUse(f: (SpriteBatch) -> Unit) {
|
||||||
|
this.begin()
|
||||||
|
f(this)
|
||||||
|
this.end()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user