Add rideboard emulation
This commit is contained in:
parent
eca46a9fc7
commit
4ead43e0f7
@ -1,5 +1,6 @@
|
||||
#include <GL/freeglut.h>
|
||||
#include <GL/glx.h>
|
||||
#include <X11/extensions/xf86vmode.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@ -108,7 +109,10 @@ int XDefineCursor(Display *display, Window w, Cursor cursor)
|
||||
int XStoreName(Display *display, Window w, const char *window_name)
|
||||
{
|
||||
int (*_XStoreName)(Display * display, Window w, const char *window_name) = dlsym(RTLD_NEXT, "XStoreName");
|
||||
return _XStoreName(display, w, getGameName());
|
||||
char gameTitle[256] = {0};
|
||||
strcat(gameTitle, getGameName());
|
||||
strcat(gameTitle, " (X11)");
|
||||
return _XStoreName(display, w, gameTitle);
|
||||
}
|
||||
|
||||
int XNextEvent(Display *display, XEvent *event_return)
|
||||
@ -118,30 +122,49 @@ int XNextEvent(Display *display, XEvent *event_return)
|
||||
int returnValue = _XNextEvent(display, event_return);
|
||||
switch (event_return->type)
|
||||
{
|
||||
|
||||
case KeyRelease:
|
||||
case KeyPress:
|
||||
{
|
||||
switch (event_return->xkey.keycode)
|
||||
{
|
||||
case 28:
|
||||
securityBoardSetSwitch(BUTTON_TEST, 1);
|
||||
securityBoardSetSwitch(BUTTON_TEST, event_return->type == KeyPress);
|
||||
break;
|
||||
case 39:
|
||||
securityBoardSetSwitch(BUTTON_SERVICE, 1);
|
||||
securityBoardSetSwitch(BUTTON_SERVICE, event_return->type == KeyPress);
|
||||
break;
|
||||
case 14:
|
||||
incrementCoin(PLAYER_1, event_return->type == KeyPress);
|
||||
break;
|
||||
case 15:
|
||||
incrementCoin(PLAYER_2, event_return->type == KeyPress);
|
||||
break;
|
||||
case 111:
|
||||
setSwitch(PLAYER_1, BUTTON_UP, event_return->type == KeyPress);
|
||||
break;
|
||||
case 116:
|
||||
setSwitch(PLAYER_1, BUTTON_DOWN, event_return->type == KeyPress);
|
||||
break;
|
||||
case 113:
|
||||
setSwitch(PLAYER_1, BUTTON_LEFT, event_return->type == KeyPress);
|
||||
break;
|
||||
case 114:
|
||||
setSwitch(PLAYER_1, BUTTON_RIGHT, event_return->type == KeyPress);
|
||||
break;
|
||||
case 10:
|
||||
setSwitch(PLAYER_1, BUTTON_START, event_return->type == KeyPress);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case KeyRelease:
|
||||
switch (event_return->xkey.keycode)
|
||||
|
||||
case MotionNotify:
|
||||
{
|
||||
case 28:
|
||||
securityBoardSetSwitch(BUTTON_TEST, 0);
|
||||
break;
|
||||
case 39:
|
||||
securityBoardSetSwitch(BUTTON_SERVICE, 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
setAnalogue(ANALOGUE_1, ((double)event_return->xmotion.x / (double)getConfig()->width) * 255.0);
|
||||
setAnalogue(ANALOGUE_2, ((double)event_return->xmotion.y / (double)getConfig()->height) * 255.0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -157,3 +180,8 @@ int XSetStandardProperties(Display *display, Window window, const char *window_n
|
||||
strcat(gameTitle, " (X11)");
|
||||
return _XSetStandardProperties(display, window, gameTitle, icon_name, icon_pixmap, argv, argc, hints);
|
||||
}
|
||||
|
||||
Bool XF86VidModeSwitchToMode(Display *display, int screen, XF86VidModeModeInfo *modeline)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
@ -128,6 +128,12 @@ void __attribute__((constructor)) hook_init()
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (getConfig()->emulateRideboard)
|
||||
{
|
||||
if (initRideboard() != 0)
|
||||
exit(1);
|
||||
}
|
||||
|
||||
securityBoardSetDipResolution(getConfig()->width, getConfig()->height);
|
||||
|
||||
printf("Loader init success\n");
|
||||
@ -222,7 +228,7 @@ ssize_t read(int fd, void *buf, size_t count)
|
||||
return baseboardRead(fd, buf, count);
|
||||
}
|
||||
|
||||
if (fd == hooks[SERIAL1] && getConfig()->emulateRideboard)
|
||||
if (fd == hooks[SERIAL0] && getConfig()->emulateRideboard)
|
||||
{
|
||||
return rideboardRead(fd, buf, count);
|
||||
}
|
||||
@ -244,7 +250,7 @@ ssize_t write(int fd, const void *buf, size_t count)
|
||||
return baseboardWrite(fd, buf, count);
|
||||
}
|
||||
|
||||
if (fd == hooks[SERIAL1] && getConfig()->emulateRideboard)
|
||||
if (fd == hooks[SERIAL0] && getConfig()->emulateRideboard)
|
||||
{
|
||||
return rideboardWrite(fd, buf, count);
|
||||
}
|
||||
|
@ -2,13 +2,391 @@
|
||||
#include <sys/types.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/serial.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "rideboard.h"
|
||||
|
||||
ssize_t rideboardRead(int fd, void *buf, size_t count) {
|
||||
RideState rideState = {0};
|
||||
|
||||
/**
|
||||
* SEGA Ride Board Emulator,
|
||||
* for use with the following Lindbergh games:
|
||||
*
|
||||
* Let's Go Jungle Special!
|
||||
* The House Of The Dead 4 Special
|
||||
*
|
||||
* Author: bobbydilley
|
||||
* Thanks to: doozer, ags, harm
|
||||
*/
|
||||
|
||||
/* Buffer packet sizes */
|
||||
#define OUTPUT_PACKET_SIZE 22
|
||||
#define INPUT_PACKET_SIZE 7
|
||||
|
||||
/* How long to wait for data before timing out */
|
||||
#define READ_TIMEOUT 500
|
||||
|
||||
/* Enums to represent the different states of the MotionSelectSwitch */
|
||||
#define MOTION_OFF 0
|
||||
#define MOTION_MILD 1
|
||||
#define MOTION_NORMAL 2
|
||||
|
||||
/**
|
||||
* Process the input packet from the lindbergh into usable data
|
||||
*
|
||||
* Take the information encoded in the input packet, and set the
|
||||
* state of the emulators virtual ride accordingly.
|
||||
*
|
||||
* @param packet The input packet to read from
|
||||
* @param state The ride state to write to
|
||||
* @returns 1 on success and 0 on failure
|
||||
*/
|
||||
int processInputPacket(char *packet, RideState *state)
|
||||
{
|
||||
/* Check the checksum is correct */
|
||||
char checksum = 0;
|
||||
for (int i = 1; i < INPUT_PACKET_SIZE - 1; i++)
|
||||
checksum ^= packet[i];
|
||||
|
||||
if (checksum != packet[INPUT_PACKET_SIZE - 1])
|
||||
{
|
||||
printf("Error: Input checksum failed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t rideboardWrite(int fd, const void *buf, size_t count) {
|
||||
/* Command Packet */
|
||||
state->Command = packet[1];
|
||||
|
||||
/* Seat Command Packet */
|
||||
state->SeatCommand = packet[2];
|
||||
|
||||
/* Gun Reaction / Blowers */
|
||||
state->PlayerOneBlowFront = (packet[3] & 0b00000001) > 0;
|
||||
state->PlayerTwoBlowFront = (packet[3] & 0b00000010) > 0;
|
||||
state->PlayerOneBlowBack = (packet[3] & 0b00000100) > 0;
|
||||
state->PlayerTwoBlowBack = (packet[3] & 0b00001000) > 0;
|
||||
state->PlayerOneGunReaction = (packet[3] & 0b00010000) > 0;
|
||||
state->PlayerTwoGunReaction = (packet[3] & 0b00100000) > 0;
|
||||
|
||||
/* Billboard Lamp */
|
||||
if (packet[4] & 0b10000000)
|
||||
{
|
||||
if (packet[4] & 0b00000001)
|
||||
state->BillboardLamp = LAMP_GREEN;
|
||||
else if (packet[4] & 0b00000010)
|
||||
state->BillboardLamp = LAMP_BLUE;
|
||||
else
|
||||
state->BillboardLamp = LAMP_RED;
|
||||
}
|
||||
else
|
||||
{
|
||||
state->BillboardLamp = LAMP_OFF;
|
||||
}
|
||||
|
||||
/* Other Lamp outputs */
|
||||
state->ResetLamp = (packet[5] & 0b00000001) > 0;
|
||||
state->ErrorLamp = (packet[5] & 0b00000010) > 0;
|
||||
state->SafetyLamp = (packet[5] & 0b00000100) > 0;
|
||||
state->GameStopLamp = (packet[5] & 0b00001000) > 0;
|
||||
state->FloorLamp = (packet[5] & 0b00010000) > 0;
|
||||
state->SpotLamp = (packet[5] & 0b00100000) > 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the output packet ready for sending to the Lindbergh
|
||||
*
|
||||
* Create the correct output packet from the ride state, taking
|
||||
* into account button positions and chair reports.
|
||||
*
|
||||
* @param packet The output packet to fill up
|
||||
* @param state The ride state to read from
|
||||
* @returns 1 on success and 0 on failure
|
||||
*/
|
||||
int processOutputPacket(char *packet, RideState *state)
|
||||
{
|
||||
/* Put the sync packet in byte 0 */
|
||||
packet[0] = 0xC0;
|
||||
|
||||
/* Unknown Packet */
|
||||
packet[1] = 0x00;
|
||||
|
||||
/* Unknown Packet */
|
||||
packet[2] = state->CommandReply; // Byte 0x30 (0xFF UPDATE MODE) (0x00 SERIAL CHECKING) (0x1A RIDE STOP)
|
||||
|
||||
/* Ride turn Test Enable */
|
||||
packet[3] = state->SeatCommandReply; // Byte 0x31
|
||||
|
||||
/* Packet 4 contains some buttons */
|
||||
packet[4] = 0x00;
|
||||
if (state->TowerGameStopButton)
|
||||
packet[4] = packet[4] | 0b00000001;
|
||||
|
||||
if (state->MotionSelectSwitch == MOTION_NORMAL)
|
||||
packet[4] = packet[4] | 0b00000010;
|
||||
|
||||
if (state->ResetButton)
|
||||
packet[4] = packet[4] | 0b00000100;
|
||||
|
||||
if (state->RideGameStopButton)
|
||||
packet[4] = packet[4] | 0b00001000;
|
||||
|
||||
if (state->InitButton)
|
||||
packet[4] = packet[4] | 0b00010000;
|
||||
|
||||
if (state->RearFootSensor)
|
||||
packet[4] = packet[4] | 0b00100000;
|
||||
|
||||
if (state->LeftFootSensor)
|
||||
packet[4] = packet[4] | 0b01000000;
|
||||
|
||||
/* Packet 5 contains some more buttons */
|
||||
packet[5] = 0x00;
|
||||
if (state->RightFootSensor)
|
||||
packet[5] = packet[5] | 0b00000001;
|
||||
|
||||
if (state->FrontFootSensor)
|
||||
packet[5] = packet[5] | 0b00000010;
|
||||
|
||||
if (state->RightDoorSensor)
|
||||
packet[5] = packet[5] | 0b00000100;
|
||||
|
||||
if (state->ArmrestSensor)
|
||||
packet[5] = packet[5] | 0b00001000;
|
||||
|
||||
if (state->PlayerOneSeatbeltSensor)
|
||||
packet[5] = packet[5] | 0b00010000;
|
||||
|
||||
if (state->PlayerTwoSeatbeltSensor)
|
||||
packet[5] = packet[5] | 0b00100000;
|
||||
|
||||
if (state->FrontPositionSensor)
|
||||
packet[5] = packet[5] | 0b01000000;
|
||||
|
||||
if (state->LeftDoorSensor)
|
||||
packet[5] = packet[5] | 0b10000000;
|
||||
|
||||
/* Packet 6 contains some more buttons */
|
||||
packet[6] = 0x00;
|
||||
if (state->RearPositionSensor)
|
||||
packet[6] = packet[6] | 0b00000001;
|
||||
|
||||
if (state->CWLimitSensor)
|
||||
packet[6] = packet[6] | 0b00000100;
|
||||
|
||||
if (state->CCWLimitSensor)
|
||||
packet[6] = packet[6] | 0b00001000;
|
||||
|
||||
if (state->MotorPower)
|
||||
packet[6] = packet[6] | 0b00010000;
|
||||
|
||||
if (state->MotionSelectSwitch == MOTION_MILD)
|
||||
packet[6] = packet[6] | 0b01000000;
|
||||
|
||||
if (state->MotionSelectSwitch == MOTION_OFF)
|
||||
packet[6] = packet[6] | 0b10000000;
|
||||
|
||||
/* Unknown Packet */
|
||||
packet[7] = 0x00; // Unknown
|
||||
|
||||
/* Used for the USB Update */
|
||||
packet[8] = 0x00; // 2ND BOOT (0 = Ignore, 1 = Update Success, 2 = Update Failed, 3 = Unknown)
|
||||
packet[9] = 0x00; // USB LOADER (0 = Ignore, 1 = Update Success, 2 = Update Failed, 3 = Unknown)
|
||||
packet[10] = 0x00; // Application (0 = Ignore, 1 = Update Success, 2 = Update Failed, 3 = Unknown)
|
||||
|
||||
packet[11] = 0x00; // 2ND BOOT SUM
|
||||
packet[12] = 0x00; // USB LOADER SUM
|
||||
packet[13] = 0x00; // Application SUM
|
||||
|
||||
packet[14] = 0x00; // 2ND BOOT Major
|
||||
packet[15] = 0x00; // 2ND BOOT Minor
|
||||
packet[16] = 0x00; // USB LOADER Major
|
||||
packet[17] = 0x00; // USB LOADER Minor
|
||||
packet[18] = 0x00; // Application Major
|
||||
packet[19] = 0x00; // Application Minor
|
||||
|
||||
/* Unknown Packet */
|
||||
packet[20] = 0x00; // Unknown
|
||||
|
||||
/* Calculate the checksum and place in the last space */
|
||||
char checksum = 0;
|
||||
for (int i = 1; i < OUTPUT_PACKET_SIZE - 1; i++)
|
||||
checksum ^= packet[i];
|
||||
|
||||
packet[OUTPUT_PACKET_SIZE - 1] = checksum;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process any physical emulation that should be done
|
||||
*
|
||||
* Process the input and output states and decide
|
||||
* what should be done on each cycle
|
||||
*
|
||||
* @param state The ride state to read from and write to
|
||||
* @returns 1 on success and 0 on failure
|
||||
*/
|
||||
int processEmulation(RideState *state)
|
||||
{
|
||||
// Respond with the correct response command
|
||||
switch (state->Command)
|
||||
{
|
||||
case 0x01: // Boot
|
||||
state->CommandReply = 0x01; // 0xFF to show update screen
|
||||
break;
|
||||
case 0x02: // Init check
|
||||
state->CommandReply = 0x03;
|
||||
break;
|
||||
case 0x03: // Reset to defaults confirmation
|
||||
state->RearFootSensor = 0;
|
||||
state->LeftFootSensor = 0;
|
||||
state->FrontFootSensor = 0;
|
||||
state->RightFootSensor = 0;
|
||||
state->ArmrestSensor = 0;
|
||||
state->PlayerOneSeatbeltSensor = 0;
|
||||
state->PlayerTwoSeatbeltSensor = 0;
|
||||
break;
|
||||
case 0x04: // Reset Success
|
||||
state->CommandReply = 0x05;
|
||||
break;
|
||||
case 0x19: // Work out what ADF means?
|
||||
state->CommandReply = 0x06;
|
||||
case 0x05: // Coin play, not coined up
|
||||
state->CommandReply = 0x05;
|
||||
break;
|
||||
case 0x06: // Ticket play, waiting for init by attendant (Not sure if this is correct)
|
||||
state->CommandReply = 0x08;
|
||||
break;
|
||||
case 0x08: // Kate Green talking about AMS
|
||||
state->CommandReply = 0x0A;
|
||||
break;
|
||||
case 0x0A: // Intro Video Playing (toggle seatbelts)
|
||||
state->CommandReply = 0x0A;
|
||||
state->PlayerOneSeatbeltSensor = !state->PlayerOneSeatbeltSensor;
|
||||
state->PlayerTwoSeatbeltSensor = !state->PlayerTwoSeatbeltSensor;
|
||||
break;
|
||||
case 0x0D: // Passed attract mode
|
||||
state->CommandReply = 0x0D;
|
||||
state->PlayerOneSeatbeltSensor = 0;
|
||||
state->PlayerTwoSeatbeltSensor = 0;
|
||||
break;
|
||||
case 0x11: // After game over get out! (Speed up?)
|
||||
case 0x13: // Test mode
|
||||
case 0x1A: // Emergency Stop
|
||||
case 0xFF: // USB Update
|
||||
default:
|
||||
state->CommandReply = state->Command;
|
||||
}
|
||||
|
||||
// Respond with the correct seat response
|
||||
switch (state->SeatCommand)
|
||||
{
|
||||
case 0x01:
|
||||
state->SeatCommandReply = 1;
|
||||
break;
|
||||
case 0x11:
|
||||
state->SeatCommandReply = 1;
|
||||
break;
|
||||
case 0x21:
|
||||
state->SeatCommandReply = 3;
|
||||
break;
|
||||
case 0x31:
|
||||
state->SeatCommandReply = 5;
|
||||
break;
|
||||
case 0x41:
|
||||
state->SeatCommandReply = 7;
|
||||
break;
|
||||
case 0x51:
|
||||
state->SeatCommandReply = 9;
|
||||
break;
|
||||
case 0x61:
|
||||
state->SeatCommandReply = 11;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void printStatus(RideState *state)
|
||||
{
|
||||
int end = 0;
|
||||
if (state->PlayerOneGunReaction && ++end)
|
||||
printf("PLAYER_ONE_GUN_REACTION ");
|
||||
if (state->PlayerTwoGunReaction && ++end)
|
||||
printf("PLAYER_TWO_GUN_REACTION ");
|
||||
if (state->PlayerOneBlowBack && ++end)
|
||||
printf("PLAYER_ONE_BLOW_BACK ");
|
||||
if (state->PlayerTwoBlowBack && ++end)
|
||||
printf("PLAYER_TWO_BLOW_BACK ");
|
||||
if (state->PlayerOneBlowFront && ++end)
|
||||
printf("PLAYER_ONE_BLOW_FRONT ");
|
||||
if (state->PlayerTwoBlowFront && ++end)
|
||||
printf("PLAYER_TWO_BLOW_FRONT ");
|
||||
if (state->GameStopLamp && ++end)
|
||||
printf("GAME_STOP_LAMP ");
|
||||
if (state->ResetLamp && ++end)
|
||||
printf("RESET_LAMP ");
|
||||
if (state->ErrorLamp && ++end)
|
||||
printf("ERROR_LAMP ");
|
||||
if (state->SafetyLamp && ++end)
|
||||
printf("SAFETY_LAMP ");
|
||||
if (state->FloorLamp && ++end)
|
||||
printf("FLOOR_LAMP ");
|
||||
if (state->SpotLamp && ++end)
|
||||
printf("SPOT_LAMP ");
|
||||
if (state->BillboardLamp != LAMP_OFF && ++end)
|
||||
{
|
||||
switch (state->BillboardLamp)
|
||||
{
|
||||
case LAMP_RED:
|
||||
printf("BILLBOARD_LAMP_RED ");
|
||||
break;
|
||||
case LAMP_GREEN:
|
||||
printf("BILLBOARD_LAMP_GREEN ");
|
||||
break;
|
||||
case LAMP_BLUE:
|
||||
printf("BILLBOARD_LAMP_BLUE ");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (end)
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretend to be the ride from SEGA's Special games.
|
||||
*
|
||||
* Communicate with a SEGA Lindbergh via serial to
|
||||
* emulate the inner working of a Special ride.
|
||||
*/
|
||||
int initRideboard()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t rideboardRead(int fd, void *buf, size_t count)
|
||||
{
|
||||
processOutputPacket((char *)buf, &rideState);
|
||||
return OUTPUT_PACKET_SIZE;
|
||||
}
|
||||
|
||||
ssize_t rideboardWrite(int fd, const void *buf, size_t count)
|
||||
{
|
||||
processInputPacket((char *)buf, &rideState);
|
||||
processEmulation(&rideState);
|
||||
return count;
|
||||
}
|
||||
|
@ -1,4 +1,64 @@
|
||||
#include <stdio.h>
|
||||
|
||||
/* Enum to define the colour of the billboard lamp */
|
||||
typedef enum
|
||||
{
|
||||
LAMP_OFF = 0,
|
||||
LAMP_GREEN,
|
||||
LAMP_BLUE,
|
||||
LAMP_RED
|
||||
} LampState;
|
||||
|
||||
/* Enums to represent the different states of the MotionSelectSwitch */
|
||||
#define MOTION_OFF 0
|
||||
#define MOTION_MILD 1
|
||||
#define MOTION_NORMAL 2
|
||||
|
||||
/* Struct to hold the internal state of the ride */
|
||||
typedef struct
|
||||
{
|
||||
/* COMMANDS */
|
||||
int Command;
|
||||
int CommandReply;
|
||||
int SeatCommand;
|
||||
int SeatCommandReply;
|
||||
/* OUTPUTS */
|
||||
int PlayerOneBlowFront;
|
||||
int PlayerOneBlowBack;
|
||||
int PlayerTwoBlowFront;
|
||||
int PlayerTwoBlowBack;
|
||||
int PlayerOneGunReaction;
|
||||
int PlayerTwoGunReaction;
|
||||
int ResetLamp;
|
||||
int ErrorLamp;
|
||||
int SafetyLamp;
|
||||
int GameStopLamp;
|
||||
int FloorLamp;
|
||||
int SpotLamp;
|
||||
LampState BillboardLamp;
|
||||
/* INPUTS */
|
||||
int InitButton;
|
||||
int ResetButton;
|
||||
int MotionSelectSwitch; // 0 Motion Off, 1 Motion Gentle, 2 Motion Normal
|
||||
int TowerGameStopButton;
|
||||
int RideGameStopButton;
|
||||
int RearFootSensor;
|
||||
int LeftFootSensor;
|
||||
int FrontFootSensor;
|
||||
int RightFootSensor;
|
||||
int RightDoorSensor;
|
||||
int LeftDoorSensor;
|
||||
int ArmrestSensor;
|
||||
int PlayerOneSeatbeltSensor;
|
||||
int PlayerTwoSeatbeltSensor;
|
||||
int RearPositionSensor;
|
||||
int FrontPositionSensor;
|
||||
int CCWLimitSensor;
|
||||
int CWLimitSensor;
|
||||
int MotorPower;
|
||||
} RideState;
|
||||
|
||||
int initRideboard();
|
||||
|
||||
ssize_t rideboardRead(int fd, void *buf, size_t count);
|
||||
ssize_t rideboardWrite(int fd, const void *buf, size_t count);
|
||||
|
Loading…
Reference in New Issue
Block a user