mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 03:08:54 +01:00
ebiten: Add FPSModeType, FPSMode, SetFPSMode, and ScheduleFrame
This change adds these APIs: * type FPSModeType * func FPSMode * func SetFPSMode * func ScheduleFrame and deprecates these APIs: * func SetVsyncEnabled * func IsVsyncEnabled Closes #1556
This commit is contained in:
parent
cf8aa0a7b2
commit
1706d9436a
@ -164,15 +164,17 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
|
||||
|
||||
#import "Ebitenmobileview.objc.h"
|
||||
|
||||
@interface {{.PrefixUpper}}EbitenViewController : UIViewController
|
||||
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester>
|
||||
@end
|
||||
|
||||
@implementation {{.PrefixUpper}}EbitenViewController {
|
||||
UIView* metalView_;
|
||||
GLKView* glkView_;
|
||||
bool started_;
|
||||
bool active_;
|
||||
bool error_;
|
||||
UIView* metalView_;
|
||||
GLKView* glkView_;
|
||||
bool started_;
|
||||
bool active_;
|
||||
bool error_;
|
||||
CADisplayLink* displayLink_;
|
||||
bool explicitRendering_;
|
||||
}
|
||||
|
||||
- (UIView*)metalView {
|
||||
@ -214,8 +216,9 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
|
||||
[EAGLContext setCurrentContext:context];
|
||||
#endif
|
||||
|
||||
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
|
||||
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
|
||||
[displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
EbitenmobileviewSetRenderRequester(self);
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
@ -251,6 +254,10 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
|
||||
#else
|
||||
[[self glkView] setNeedsDisplay];
|
||||
#endif
|
||||
|
||||
if (explicitRendering_) {
|
||||
[displayLink_ setPaused:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -336,6 +343,25 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setExplicitRenderingMode:(BOOL)explicitRendering {
|
||||
@synchronized(self) {
|
||||
explicitRendering_ = explicitRendering;
|
||||
if (explicitRendering_) {
|
||||
[displayLink_ setPaused:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)requestRenderIfNeeded {
|
||||
@synchronized(self) {
|
||||
if (explicitRendering_) {
|
||||
// Resume the callback temporarily.
|
||||
// This is paused again soon in drawFrame.
|
||||
[displayLink_ setPaused:NO];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
`
|
||||
|
||||
@ -745,9 +771,10 @@ import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
|
||||
import {{.JavaPkg}}.ebitenmobileview.RenderRequester;
|
||||
import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;
|
||||
|
||||
class EbitenSurfaceView extends GLSurfaceView {
|
||||
class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
|
||||
|
||||
private class EbitenRenderer implements GLSurfaceView.Renderer {
|
||||
|
||||
@ -795,10 +822,27 @@ class EbitenSurfaceView extends GLSurfaceView {
|
||||
setEGLContextClientVersion(2);
|
||||
setEGLConfigChooser(8, 8, 8, 8, 0, 0);
|
||||
setRenderer(new EbitenRenderer());
|
||||
Ebitenmobileview.setRenderRequester(this);
|
||||
}
|
||||
|
||||
private void onErrorOnGameUpdate(Exception e) {
|
||||
((EbitenView)getParent()).onErrorOnGameUpdate(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setExplicitRenderingMode(boolean explictRendering) {
|
||||
if (explictRendering) {
|
||||
setRenderMode(RENDERMODE_WHEN_DIRTY);
|
||||
} else {
|
||||
setRenderMode(RENDERMODE_CONTINUOUSLY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void requestRenderIfNeeded() {
|
||||
if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
File diff suppressed because one or more lines are too long
@ -124,7 +124,7 @@ func (g *game) Update() error {
|
||||
fullscreen := ebiten.IsFullscreen()
|
||||
runnableOnUnfocused := ebiten.IsRunnableOnUnfocused()
|
||||
cursorMode := ebiten.CursorMode()
|
||||
vsyncEnabled := ebiten.IsVsyncEnabled()
|
||||
fpsMode := ebiten.FPSMode()
|
||||
tps := ebiten.MaxTPS()
|
||||
decorated := ebiten.IsWindowDecorated()
|
||||
positionX, positionY := ebiten.WindowPosition()
|
||||
@ -205,7 +205,14 @@ func (g *game) Update() error {
|
||||
}
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyV) {
|
||||
vsyncEnabled = !vsyncEnabled
|
||||
switch fpsMode {
|
||||
case ebiten.FPSModeVsyncOn:
|
||||
fpsMode = ebiten.FPSModeVsyncOffMaximum
|
||||
case ebiten.FPSModeVsyncOffMaximum:
|
||||
fpsMode = ebiten.FPSModeVsyncOffMinimum
|
||||
case ebiten.FPSModeVsyncOffMinimum:
|
||||
fpsMode = ebiten.FPSModeVsyncOn
|
||||
}
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyT) {
|
||||
switch tps {
|
||||
@ -249,10 +256,10 @@ func (g *game) Update() error {
|
||||
ebiten.SetRunnableOnUnfocused(runnableOnUnfocused)
|
||||
ebiten.SetCursorMode(cursorMode)
|
||||
|
||||
// Set vsync enabled only when this is needed.
|
||||
// This makes a bug around vsync initialization more explicit (#1364).
|
||||
if vsyncEnabled != ebiten.IsVsyncEnabled() {
|
||||
ebiten.SetVsyncEnabled(vsyncEnabled)
|
||||
// Set FPS mode enabled only when this is needed.
|
||||
// This makes a bug around FPS mode initialization more explicit (#1364).
|
||||
if fpsMode != ebiten.FPSMode() {
|
||||
ebiten.SetFPSMode(fpsMode)
|
||||
}
|
||||
ebiten.SetMaxTPS(tps)
|
||||
ebiten.SetWindowDecorated(decorated)
|
||||
@ -323,7 +330,7 @@ func (g *game) Draw(screen *ebiten.Image) {
|
||||
[U] Switch the runnable-on-unfocused state
|
||||
[C] Switch the cursor mode (visible, hidden, or captured)
|
||||
[I] Change the window icon (only for desktops)
|
||||
[V] Switch vsync
|
||||
[V] Switch the FPS mode
|
||||
[T] Switch TPS (ticks per second)
|
||||
[D] Switch the window decoration (only for desktops)
|
||||
[L] Switch the window floating state (only for desktops)
|
||||
@ -403,7 +410,9 @@ func main() {
|
||||
ebiten.SetWindowResizable(true)
|
||||
ebiten.MaximizeWindow()
|
||||
}
|
||||
ebiten.SetVsyncEnabled(*flagVsync)
|
||||
if !*flagVsync {
|
||||
ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMaximum)
|
||||
}
|
||||
if *flagAutoAdjusting {
|
||||
ebiten.SetWindowResizable(true)
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ type UI interface {
|
||||
|
||||
FPSMode() FPSMode
|
||||
SetFPSMode(mode FPSMode)
|
||||
ScheduleFrame()
|
||||
|
||||
IsScreenTransparent() bool
|
||||
SetScreenTransparent(transparent bool)
|
||||
@ -104,4 +105,5 @@ type FPSMode int
|
||||
const (
|
||||
FPSModeVsyncOn FPSMode = iota
|
||||
FPSModeVsyncOffMaximum
|
||||
FPSModeVsyncOffMinimum
|
||||
)
|
||||
|
@ -334,6 +334,10 @@ func PollEvents() {
|
||||
glfw.PollEvents()
|
||||
}
|
||||
|
||||
func PostEmptyEvent() {
|
||||
glfw.PostEmptyEvent()
|
||||
}
|
||||
|
||||
func SetMonitorCallback(cbfun func(monitor *Monitor, event PeripheralEvent)) {
|
||||
var gcb func(monitor *glfw.Monitor, event glfw.PeripheralEvent)
|
||||
if cbfun != nil {
|
||||
@ -360,6 +364,10 @@ func UpdateGamepadMappings(mapping string) bool {
|
||||
return glfw.UpdateGamepadMappings(mapping)
|
||||
}
|
||||
|
||||
func WaitEvents() {
|
||||
glfw.WaitEvents()
|
||||
}
|
||||
|
||||
func WindowHint(target Hint, hint int) {
|
||||
glfw.WindowHint(glfw.Hint(target), hint)
|
||||
}
|
||||
|
@ -517,6 +517,11 @@ func PollEvents() {
|
||||
panicErrorExceptForInvalidValue()
|
||||
}
|
||||
|
||||
func PostEmptyEvent() {
|
||||
glfwDLL.call("glfwPostEmptyEvent")
|
||||
panicError()
|
||||
}
|
||||
|
||||
func SetMonitorCallback(cbfun func(monitor *Monitor, event PeripheralEvent)) {
|
||||
var gcb uintptr
|
||||
if cbfun != nil {
|
||||
@ -554,6 +559,11 @@ func UpdateGamepadMappings(mapping string) bool {
|
||||
return r == True
|
||||
}
|
||||
|
||||
func WaitEvents() {
|
||||
glfwDLL.call("glfwWaitEvents")
|
||||
panicError()
|
||||
}
|
||||
|
||||
func WindowHint(target Hint, hint int) {
|
||||
glfwDLL.call("glfwWindowHint", uintptr(target), uintptr(hint))
|
||||
panicError()
|
||||
|
@ -639,6 +639,15 @@ func (u *UserInterface) FPSMode() driver.FPSMode {
|
||||
return v
|
||||
}
|
||||
|
||||
func (u *UserInterface) ScheduleFrame() {
|
||||
if !u.isRunning() {
|
||||
return
|
||||
}
|
||||
// As the main thread can be blocked, do not check the current FPS mode.
|
||||
// PostEmptyEvent is concurrent safe.
|
||||
glfw.PostEmptyEvent()
|
||||
}
|
||||
|
||||
func (u *UserInterface) CursorMode() driver.CursorMode {
|
||||
if !u.isRunning() {
|
||||
return u.getInitCursorMode()
|
||||
@ -964,8 +973,12 @@ func (u *UserInterface) update() (float64, float64, bool, error) {
|
||||
|
||||
outsideWidth, outsideHeight, outsideSizeChanged := u.updateSize()
|
||||
|
||||
// TODO: Updating the input can be skipped when clock.Update returns 0 (#1367).
|
||||
glfw.PollEvents()
|
||||
if u.fpsMode != driver.FPSModeVsyncOffMinimum {
|
||||
// TODO: Updating the input can be skipped when clock.Update returns 0 (#1367).
|
||||
glfw.PollEvents()
|
||||
} else {
|
||||
glfw.WaitEvents()
|
||||
}
|
||||
u.input.update(u.window, u.context)
|
||||
|
||||
for !u.isRunnableOnUnfocused() && u.window.GetAttrib(glfw.Focused) == 0 && !u.window.ShouldClose() {
|
||||
|
@ -403,6 +403,8 @@ func (i *Input) updateFromEvent(e js.Value) {
|
||||
case t.Equal(stringTouchstart) || t.Equal(stringTouchend) || t.Equal(stringTouchmove):
|
||||
i.updateTouchesFromEvent(e)
|
||||
}
|
||||
|
||||
i.ui.forceUpdateOnMinimumFPSMode()
|
||||
}
|
||||
|
||||
func (i *Input) setMouseCursorFromEvent(e js.Value) {
|
||||
|
@ -50,11 +50,13 @@ func driverCursorShapeToCSSCursor(cursor driver.CursorShape) string {
|
||||
type UserInterface struct {
|
||||
runnableOnUnfocused bool
|
||||
fpsMode driver.FPSMode
|
||||
renderingScheduled bool
|
||||
running bool
|
||||
initFocused bool
|
||||
cursorMode driver.CursorMode
|
||||
cursorPrevMode driver.CursorMode
|
||||
cursorShape driver.CursorShape
|
||||
onceUpdateCalled bool
|
||||
|
||||
sizeChanged bool
|
||||
|
||||
@ -146,6 +148,10 @@ func (u *UserInterface) FPSMode() driver.FPSMode {
|
||||
return u.fpsMode
|
||||
}
|
||||
|
||||
func (u *UserInterface) ScheduleFrame() {
|
||||
u.renderingScheduled = true
|
||||
}
|
||||
|
||||
func (u *UserInterface) CursorMode() driver.CursorMode {
|
||||
if !canvas.Truthy() {
|
||||
return driver.CursorModeHidden
|
||||
@ -281,6 +287,20 @@ func (u *UserInterface) updateImpl(force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserInterface) needsUpdate() bool {
|
||||
if u.fpsMode != driver.FPSModeVsyncOffMinimum {
|
||||
return true
|
||||
}
|
||||
if !u.onceUpdateCalled {
|
||||
return true
|
||||
}
|
||||
if u.renderingScheduled {
|
||||
return true
|
||||
}
|
||||
// TODO: Watch the gamepad state?
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *UserInterface) loop(context driver.UIContext) <-chan error {
|
||||
u.context = context
|
||||
|
||||
@ -290,17 +310,24 @@ func (u *UserInterface) loop(context driver.UIContext) <-chan error {
|
||||
|
||||
var cf js.Func
|
||||
f := func() {
|
||||
if err := u.update(); err != nil {
|
||||
close(reqStopAudioCh)
|
||||
<-resStopAudioCh
|
||||
if u.needsUpdate() {
|
||||
u.onceUpdateCalled = true
|
||||
u.renderingScheduled = false
|
||||
if err := u.update(); err != nil {
|
||||
close(reqStopAudioCh)
|
||||
<-resStopAudioCh
|
||||
|
||||
errCh <- err
|
||||
return
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
if u.fpsMode == driver.FPSModeVsyncOn {
|
||||
switch u.fpsMode {
|
||||
case driver.FPSModeVsyncOn:
|
||||
requestAnimationFrame.Invoke(cf)
|
||||
} else {
|
||||
case driver.FPSModeVsyncOffMaximum:
|
||||
setTimeout.Invoke(cf, 0)
|
||||
case driver.FPSModeVsyncOffMinimum:
|
||||
requestAnimationFrame.Invoke(cf)
|
||||
}
|
||||
}
|
||||
|
||||
@ -540,6 +567,13 @@ func setCanvasEventHandlers(v js.Value) {
|
||||
}))
|
||||
}
|
||||
|
||||
func (u *UserInterface) forceUpdateOnMinimumFPSMode() {
|
||||
if u.fpsMode != driver.FPSModeVsyncOffMinimum {
|
||||
return
|
||||
}
|
||||
u.updateImpl(true)
|
||||
}
|
||||
|
||||
func (u *UserInterface) Run(context driver.UIContext) error {
|
||||
if u.initFocused && window.Truthy() {
|
||||
// Do not focus the canvas when the current document is in an iframe.
|
||||
|
@ -112,6 +112,9 @@ type UserInterface struct {
|
||||
|
||||
input Input
|
||||
|
||||
fpsMode driver.FPSMode
|
||||
renderRequester RenderRequester
|
||||
|
||||
t *thread.OSThread
|
||||
|
||||
m sync.RWMutex
|
||||
@ -405,11 +408,19 @@ func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
|
||||
}
|
||||
|
||||
func (u *UserInterface) FPSMode() driver.FPSMode {
|
||||
return driver.FPSModeVsyncOn
|
||||
return u.fpsMode
|
||||
}
|
||||
|
||||
func (u *UserInterface) SetFPSMode(mode driver.FPSMode) {
|
||||
// Do nothing
|
||||
u.fpsMode = mode
|
||||
u.updateExplicitRenderingModeIfNeeded()
|
||||
}
|
||||
|
||||
func (u *UserInterface) updateExplicitRenderingModeIfNeeded() {
|
||||
if u.renderRequester == nil {
|
||||
return
|
||||
}
|
||||
u.renderRequester.SetExplicitRenderingMode(u.fpsMode == driver.FPSModeVsyncOffMinimum)
|
||||
}
|
||||
|
||||
func (u *UserInterface) DeviceScaleFactor() float64 {
|
||||
@ -459,4 +470,23 @@ type Gamepad struct {
|
||||
|
||||
func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch, gamepads []Gamepad) {
|
||||
u.input.update(keys, runes, touches, gamepads)
|
||||
if u.fpsMode == driver.FPSModeVsyncOffMinimum {
|
||||
u.renderRequester.RequestRenderIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
type RenderRequester interface {
|
||||
SetExplicitRenderingMode(explicitRendering bool)
|
||||
RequestRenderIfNeeded()
|
||||
}
|
||||
|
||||
func (u *UserInterface) SetRenderRequester(renderRequester RenderRequester) {
|
||||
u.renderRequester = renderRequester
|
||||
u.updateExplicitRenderingModeIfNeeded()
|
||||
}
|
||||
|
||||
func (u *UserInterface) ScheduleFrame() {
|
||||
if u.renderRequester != nil && u.fpsMode == driver.FPSModeVsyncOffMinimum {
|
||||
u.renderRequester.RequestRenderIfNeeded()
|
||||
}
|
||||
}
|
||||
|
@ -90,3 +90,12 @@ func OnContextLost() {
|
||||
func DeviceScale() float64 {
|
||||
return devicescale.GetAt(0, 0)
|
||||
}
|
||||
|
||||
type RenderRequester interface {
|
||||
SetExplicitRenderingMode(explicitRendering bool)
|
||||
RequestRenderIfNeeded()
|
||||
}
|
||||
|
||||
func SetRenderRequester(renderRequester RenderRequester) {
|
||||
mobile.Get().SetRenderRequester(renderRequester)
|
||||
}
|
||||
|
67
run.go
67
run.go
@ -312,7 +312,7 @@ func DeviceScaleFactor() float64 {
|
||||
// IsVsyncEnabled returns a boolean value indicating whether
|
||||
// the game uses the display's vsync.
|
||||
//
|
||||
// IsVsyncEnabled is concurrent-safe.
|
||||
// Deprecated: as of v2.2. Use FPSMode instead.
|
||||
func IsVsyncEnabled() bool {
|
||||
return uiDriver().FPSMode() == driver.FPSModeVsyncOn
|
||||
}
|
||||
@ -320,17 +320,7 @@ func IsVsyncEnabled() bool {
|
||||
// SetVsyncEnabled sets a boolean value indicating whether
|
||||
// the game uses the display's vsync.
|
||||
//
|
||||
// If the given value is true, the game tries to sync the display's refresh rate.
|
||||
// If false, the game ignores the display's refresh rate.
|
||||
// The initial value is true.
|
||||
// By disabling vsync, the game works more efficiently but consumes more CPU.
|
||||
//
|
||||
// Note that the state doesn't affect TPS (ticks per second, i.e. how many the run function is
|
||||
// updated per second).
|
||||
//
|
||||
// SetVsyncEnabled does nothing on mobiles so far.
|
||||
//
|
||||
// SetVsyncEnabled is concurrent-safe.
|
||||
// Deprecated: as of v2.2. Use SetFPSMode instead.
|
||||
func SetVsyncEnabled(enabled bool) {
|
||||
if enabled {
|
||||
uiDriver().SetFPSMode(driver.FPSModeVsyncOn)
|
||||
@ -339,10 +329,63 @@ func SetVsyncEnabled(enabled bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// FPSModeType is a type of FPS modes.
|
||||
type FPSModeType = driver.FPSMode
|
||||
|
||||
const (
|
||||
// FPSModeVsyncOn indicates that the game tries to sync the display's refresh rate.
|
||||
// FPSModeVsyncOn is the default mode.
|
||||
FPSModeVsyncOn FPSModeType = driver.FPSModeVsyncOn
|
||||
|
||||
// FPSModeVsyncOffMaximum indicates that the game doesn't sync with vsycn, and
|
||||
// the game is updated whenever possible.
|
||||
//
|
||||
// Be careful that FPSModeVsyncOffMaximum might consume a lot of battery power.
|
||||
//
|
||||
// In FPSModeVsyncOffMaximum, the game's Draw is called almost without sleeping.
|
||||
// The game's Update is called based on the specified TPS.
|
||||
FPSModeVsyncOffMaximum FPSModeType = driver.FPSModeVsyncOffMaximum
|
||||
|
||||
// FPSModeVsyncOffMinimum indicates that the game doesn't sync with vsycn, and
|
||||
// the game is updated only when necessary.
|
||||
//
|
||||
// FPSModeVsyncOffMinimum is useful for relatively static applications to save battery power.
|
||||
//
|
||||
// In FPSModeVsyncOffMinimum, the game's Update and Draw are called only when
|
||||
// 1) new inputting is detected, or 2) ScheduleFrame is called.
|
||||
// In FPSModeVsyncOffMinimum, TPS is SyncWithFPS no matter what TPS is specified at SetMaxTPS.
|
||||
FPSModeVsyncOffMinimum FPSModeType = driver.FPSModeVsyncOffMinimum
|
||||
)
|
||||
|
||||
// FPSMode returns the current FPS mode.
|
||||
//
|
||||
// FPSMode is concurrent-safe.
|
||||
func FPSMode() FPSModeType {
|
||||
return uiDriver().FPSMode()
|
||||
}
|
||||
|
||||
// SetFPSMode sets the FPS mode.
|
||||
// The default FPS mode is FPSModeVsycnOn.
|
||||
//
|
||||
// SetFPSMode is concurrent-safe.
|
||||
func SetFPSMode(mode FPSModeType) {
|
||||
uiDriver().SetFPSMode(mode)
|
||||
}
|
||||
|
||||
// ScheduleFrame schedules a next frame when the current FPS mode is FPSModeVsyncOffMinimum.
|
||||
//
|
||||
// ScheduleFrame is concurrent-safe.
|
||||
func ScheduleFrame() {
|
||||
uiDriver().ScheduleFrame()
|
||||
}
|
||||
|
||||
// MaxTPS returns the current maximum TPS.
|
||||
//
|
||||
// MaxTPS is concurrent-safe.
|
||||
func MaxTPS() int {
|
||||
if FPSMode() == FPSModeVsyncOffMinimum {
|
||||
return SyncWithFPS
|
||||
}
|
||||
return int(atomic.LoadInt32(¤tMaxTPS))
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user